codeseeker 1.8.2 → 1.9.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.
- package/LICENSE +216 -216
- package/README.md +525 -524
- package/dist/cli/commands/handlers/install-command-handler.d.ts.map +1 -1
- package/dist/cli/commands/handlers/install-command-handler.js +7 -8
- package/dist/cli/commands/handlers/install-command-handler.js.map +1 -1
- package/dist/cli/commands/handlers/setup-command-handler.d.ts.map +1 -1
- package/dist/cli/commands/handlers/setup-command-handler.js +22 -20
- package/dist/cli/commands/handlers/setup-command-handler.js.map +1 -1
- package/dist/mcp/mcp-server.d.ts +35 -103
- package/dist/mcp/mcp-server.d.ts.map +1 -1
- package/dist/mcp/mcp-server.js +1162 -2109
- package/dist/mcp/mcp-server.js.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +20 -20
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* CodeSeeker MCP Server
|
|
3
|
+
* CodeSeeker MCP Server (Consolidated)
|
|
4
4
|
*
|
|
5
5
|
* Exposes CodeSeeker's semantic search and code analysis capabilities
|
|
6
6
|
* as an MCP (Model Context Protocol) server for use with Claude Desktop
|
|
7
7
|
* and Claude Code.
|
|
8
8
|
*
|
|
9
|
+
* OPTIMIZED: 12 tools consolidated to 3 to minimize token usage:
|
|
10
|
+
* 1. search - Code discovery (search, search+read, read-with-context)
|
|
11
|
+
* 2. analyze - Code analysis (dependencies, dead_code, duplicates, standards)
|
|
12
|
+
* 3. index - Index management (init, sync, status, parsers, exclude)
|
|
13
|
+
*
|
|
9
14
|
* Usage:
|
|
10
15
|
* codeseeker serve --mcp
|
|
11
16
|
*
|
|
@@ -67,12 +72,15 @@ const indexing_service_1 = require("./indexing-service");
|
|
|
67
72
|
const coding_standards_generator_1 = require("../cli/services/analysis/coding-standards-generator");
|
|
68
73
|
const language_support_service_1 = require("../cli/services/project/language-support-service");
|
|
69
74
|
const query_cache_service_1 = require("./query-cache-service");
|
|
70
|
-
// DuplicateCodeDetector no longer used - find_duplicates now uses indexed embeddings directly
|
|
71
|
-
// SemanticKnowledgeGraph no longer used - find_dead_code now uses indexed graph from storage manager
|
|
72
75
|
// Version from package.json
|
|
73
76
|
const VERSION = '2.0.0';
|
|
74
77
|
/**
|
|
75
|
-
* MCP Server for CodeSeeker
|
|
78
|
+
* MCP Server for CodeSeeker - Consolidated 3-tool architecture
|
|
79
|
+
*
|
|
80
|
+
* Tools:
|
|
81
|
+
* 1. search - Semantic code search with optional file reading
|
|
82
|
+
* 2. analyze - Code analysis (dependencies, dead_code, duplicates, standards)
|
|
83
|
+
* 3. index - Index management (init, sync, status, parsers, exclude)
|
|
76
84
|
*/
|
|
77
85
|
class CodeSeekerMcpServer {
|
|
78
86
|
server;
|
|
@@ -82,20 +90,15 @@ class CodeSeekerMcpServer {
|
|
|
82
90
|
queryCache;
|
|
83
91
|
// Background indexing state
|
|
84
92
|
indexingJobs = new Map();
|
|
85
|
-
// Mutex for concurrent indexing protection
|
|
86
93
|
indexingMutex = new Set();
|
|
87
|
-
// Cancellation tokens for running indexing jobs
|
|
88
94
|
cancellationTokens = new Map();
|
|
89
95
|
// Job cleanup interval (clean completed/failed jobs after 1 hour)
|
|
90
|
-
JOB_TTL_MS = 60 * 60 * 1000;
|
|
96
|
+
JOB_TTL_MS = 60 * 60 * 1000;
|
|
91
97
|
cleanupTimer = null;
|
|
92
98
|
// Dangerous paths that should never be indexed (security)
|
|
93
99
|
DANGEROUS_PATHS = [
|
|
94
|
-
// System directories
|
|
95
100
|
'/etc', '/var', '/usr', '/bin', '/sbin', '/lib', '/boot', '/root', '/proc', '/sys', '/dev',
|
|
96
|
-
// Windows system directories
|
|
97
101
|
'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)', 'C:\\ProgramData',
|
|
98
|
-
// User sensitive directories
|
|
99
102
|
'.ssh', '.gnupg', '.aws', '.azure', '.config',
|
|
100
103
|
];
|
|
101
104
|
constructor() {
|
|
@@ -108,33 +111,25 @@ class CodeSeekerMcpServer {
|
|
|
108
111
|
this.languageSupportService = new language_support_service_1.LanguageSupportService();
|
|
109
112
|
this.queryCache = (0, query_cache_service_1.getQueryCacheService)();
|
|
110
113
|
this.registerTools();
|
|
111
|
-
// Start cleanup timer for old indexing jobs
|
|
112
114
|
this.startJobCleanupTimer();
|
|
113
115
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
// ============================================================
|
|
117
|
+
// LIFECYCLE & UTILITY METHODS
|
|
118
|
+
// ============================================================
|
|
117
119
|
startJobCleanupTimer() {
|
|
118
|
-
// Clean up every 10 minutes
|
|
119
120
|
this.cleanupTimer = setInterval(() => {
|
|
120
121
|
this.cleanupOldJobs();
|
|
121
122
|
}, 10 * 60 * 1000);
|
|
122
|
-
// Ensure timer doesn't prevent process exit
|
|
123
123
|
if (this.cleanupTimer.unref) {
|
|
124
124
|
this.cleanupTimer.unref();
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Clean up completed/failed jobs older than TTL
|
|
129
|
-
*/
|
|
130
127
|
cleanupOldJobs() {
|
|
131
128
|
const now = Date.now();
|
|
132
129
|
const jobsToDelete = [];
|
|
133
130
|
for (const [projectId, job] of this.indexingJobs) {
|
|
134
|
-
// Only clean up non-running jobs
|
|
135
131
|
if (job.status !== 'running' && job.completedAt) {
|
|
136
|
-
|
|
137
|
-
if (age > this.JOB_TTL_MS) {
|
|
132
|
+
if (now - job.completedAt.getTime() > this.JOB_TTL_MS) {
|
|
138
133
|
jobsToDelete.push(projectId);
|
|
139
134
|
}
|
|
140
135
|
}
|
|
@@ -143,17 +138,11 @@ class CodeSeekerMcpServer {
|
|
|
143
138
|
this.indexingJobs.delete(projectId);
|
|
144
139
|
}
|
|
145
140
|
}
|
|
146
|
-
/**
|
|
147
|
-
* Validate that a path is safe to index (security)
|
|
148
|
-
* Returns error message if unsafe, null if safe
|
|
149
|
-
*/
|
|
150
141
|
validateProjectPath(projectPath) {
|
|
151
142
|
const normalizedPath = path.normalize(projectPath);
|
|
152
|
-
// Check for path traversal attempts
|
|
153
143
|
if (normalizedPath.includes('..')) {
|
|
154
144
|
return 'Path traversal detected: paths with ".." are not allowed';
|
|
155
145
|
}
|
|
156
|
-
// Check for dangerous system directories
|
|
157
146
|
const lowerPath = normalizedPath.toLowerCase();
|
|
158
147
|
for (const dangerous of this.DANGEROUS_PATHS) {
|
|
159
148
|
const lowerDangerous = dangerous.toLowerCase();
|
|
@@ -161,7 +150,6 @@ class CodeSeekerMcpServer {
|
|
|
161
150
|
return `Security: cannot index system directory "${dangerous}"`;
|
|
162
151
|
}
|
|
163
152
|
}
|
|
164
|
-
// Check path components for sensitive directories
|
|
165
153
|
const pathParts = normalizedPath.split(path.sep);
|
|
166
154
|
for (const part of pathParts) {
|
|
167
155
|
const lowerPart = part.toLowerCase();
|
|
@@ -169,17 +157,11 @@ class CodeSeekerMcpServer {
|
|
|
169
157
|
return `Security: cannot index sensitive directory "${part}"`;
|
|
170
158
|
}
|
|
171
159
|
}
|
|
172
|
-
return null;
|
|
160
|
+
return null;
|
|
173
161
|
}
|
|
174
|
-
/**
|
|
175
|
-
* Start background indexing for a project
|
|
176
|
-
* Returns immediately, indexing happens asynchronously
|
|
177
|
-
*/
|
|
178
162
|
startBackgroundIndexing(projectId, projectName, projectPath, clearExisting = true) {
|
|
179
|
-
// Create cancellation token
|
|
180
163
|
const cancellationToken = { cancelled: false };
|
|
181
164
|
this.cancellationTokens.set(projectId, cancellationToken);
|
|
182
|
-
// Create job entry
|
|
183
165
|
const job = {
|
|
184
166
|
projectId,
|
|
185
167
|
projectName,
|
|
@@ -194,9 +176,7 @@ class CodeSeekerMcpServer {
|
|
|
194
176
|
},
|
|
195
177
|
};
|
|
196
178
|
this.indexingJobs.set(projectId, job);
|
|
197
|
-
// Release mutex once job is registered (actual indexing is tracked by job status)
|
|
198
179
|
this.indexingMutex.delete(projectId);
|
|
199
|
-
// Start indexing in background (don't await)
|
|
200
180
|
this.runBackgroundIndexing(job, clearExisting, cancellationToken).catch((error) => {
|
|
201
181
|
job.status = 'failed';
|
|
202
182
|
job.error = error instanceof Error ? error.message : String(error);
|
|
@@ -204,9 +184,6 @@ class CodeSeekerMcpServer {
|
|
|
204
184
|
this.cancellationTokens.delete(projectId);
|
|
205
185
|
});
|
|
206
186
|
}
|
|
207
|
-
/**
|
|
208
|
-
* Cancel a running indexing job
|
|
209
|
-
*/
|
|
210
187
|
cancelIndexing(projectId) {
|
|
211
188
|
const token = this.cancellationTokens.get(projectId);
|
|
212
189
|
if (token) {
|
|
@@ -215,20 +192,15 @@ class CodeSeekerMcpServer {
|
|
|
215
192
|
}
|
|
216
193
|
return false;
|
|
217
194
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Run the actual indexing (called asynchronously)
|
|
220
|
-
*/
|
|
221
195
|
async runBackgroundIndexing(job, clearExisting, cancellationToken) {
|
|
222
196
|
try {
|
|
223
197
|
const storageManager = await (0, storage_1.getStorageManager)();
|
|
224
198
|
const vectorStore = storageManager.getVectorStore();
|
|
225
199
|
const graphStore = storageManager.getGraphStore();
|
|
226
|
-
// Clear existing data if requested
|
|
227
200
|
if (clearExisting) {
|
|
228
201
|
await vectorStore.deleteByProject(job.projectId);
|
|
229
202
|
await graphStore.deleteByProject(job.projectId);
|
|
230
203
|
}
|
|
231
|
-
// Check for cancellation before starting
|
|
232
204
|
if (cancellationToken.cancelled) {
|
|
233
205
|
job.status = 'failed';
|
|
234
206
|
job.error = 'Indexing cancelled by user';
|
|
@@ -236,9 +208,7 @@ class CodeSeekerMcpServer {
|
|
|
236
208
|
this.cancellationTokens.delete(job.projectId);
|
|
237
209
|
return;
|
|
238
210
|
}
|
|
239
|
-
// Run indexing with progress tracking
|
|
240
211
|
const result = await this.indexingService.indexProject(job.projectPath, job.projectId, (progress) => {
|
|
241
|
-
// Check for cancellation during indexing
|
|
242
212
|
if (cancellationToken.cancelled) {
|
|
243
213
|
throw new Error('Indexing cancelled by user');
|
|
244
214
|
}
|
|
@@ -249,7 +219,6 @@ class CodeSeekerMcpServer {
|
|
|
249
219
|
chunksCreated: progress.chunksCreated,
|
|
250
220
|
};
|
|
251
221
|
});
|
|
252
|
-
// Update job with results
|
|
253
222
|
job.status = 'completed';
|
|
254
223
|
job.completedAt = new Date();
|
|
255
224
|
job.result = {
|
|
@@ -259,24 +228,17 @@ class CodeSeekerMcpServer {
|
|
|
259
228
|
edgesCreated: result.edgesCreated,
|
|
260
229
|
durationMs: result.durationMs,
|
|
261
230
|
};
|
|
262
|
-
// Generate coding standards after indexing (if not cancelled)
|
|
263
231
|
if (!cancellationToken.cancelled) {
|
|
264
232
|
try {
|
|
265
233
|
const generator = new coding_standards_generator_1.CodingStandardsGenerator(vectorStore);
|
|
266
234
|
await generator.generateStandards(job.projectId, job.projectPath);
|
|
267
235
|
}
|
|
268
|
-
catch {
|
|
269
|
-
// Non-fatal - standards generation is optional
|
|
270
|
-
}
|
|
236
|
+
catch { /* Non-fatal */ }
|
|
271
237
|
}
|
|
272
|
-
// Invalidate query cache for this project (full reindex)
|
|
273
238
|
try {
|
|
274
239
|
await this.queryCache.invalidateProject(job.projectId);
|
|
275
240
|
}
|
|
276
|
-
catch {
|
|
277
|
-
// Non-fatal - cache invalidation is optional
|
|
278
|
-
}
|
|
279
|
-
// Clean up cancellation token
|
|
241
|
+
catch { /* Non-fatal */ }
|
|
280
242
|
this.cancellationTokens.delete(job.projectId);
|
|
281
243
|
}
|
|
282
244
|
catch (error) {
|
|
@@ -286,16 +248,9 @@ class CodeSeekerMcpServer {
|
|
|
286
248
|
this.cancellationTokens.delete(job.projectId);
|
|
287
249
|
}
|
|
288
250
|
}
|
|
289
|
-
/**
|
|
290
|
-
* Get indexing status for a project
|
|
291
|
-
*/
|
|
292
251
|
getIndexingStatus(projectId) {
|
|
293
252
|
return this.indexingJobs.get(projectId);
|
|
294
253
|
}
|
|
295
|
-
/**
|
|
296
|
-
* Find CodeSeeker project by walking up directory tree from startPath
|
|
297
|
-
* looking for .codeseeker/project.json
|
|
298
|
-
*/
|
|
299
254
|
async findProjectPath(startPath) {
|
|
300
255
|
let currentPath = path.resolve(startPath);
|
|
301
256
|
const root = path.parse(currentPath).root;
|
|
@@ -306,2171 +261,1281 @@ class CodeSeekerMcpServer {
|
|
|
306
261
|
}
|
|
307
262
|
currentPath = path.dirname(currentPath);
|
|
308
263
|
}
|
|
309
|
-
// No project found, return original path
|
|
310
264
|
return startPath;
|
|
311
265
|
}
|
|
266
|
+
generateProjectId(projectPath) {
|
|
267
|
+
return crypto.createHash('md5').update(projectPath).digest('hex');
|
|
268
|
+
}
|
|
269
|
+
async getAllProjectDocuments(vectorStore, projectId) {
|
|
270
|
+
const randomEmbedding = Array.from({ length: 384 }, () => Math.random() - 0.5);
|
|
271
|
+
const results = await vectorStore.searchByVector(randomEmbedding, projectId, 10000);
|
|
272
|
+
return results.map(r => r.document);
|
|
273
|
+
}
|
|
274
|
+
formatErrorMessage(operation, error, context) {
|
|
275
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
276
|
+
const lowerMessage = message.toLowerCase();
|
|
277
|
+
if (lowerMessage.includes('enoent') || lowerMessage.includes('not found') || lowerMessage.includes('no such file')) {
|
|
278
|
+
return `${operation} failed: File or directory not found. Verify: ${context?.projectPath || 'the specified path'}`;
|
|
279
|
+
}
|
|
280
|
+
if (lowerMessage.includes('eacces') || lowerMessage.includes('permission denied')) {
|
|
281
|
+
return `${operation} failed: Permission denied.`;
|
|
282
|
+
}
|
|
283
|
+
if (lowerMessage.includes('timeout') || lowerMessage.includes('timed out')) {
|
|
284
|
+
return `${operation} failed: Timed out. Check status with index({action: "status"}).`;
|
|
285
|
+
}
|
|
286
|
+
if (lowerMessage.includes('connection') || lowerMessage.includes('econnrefused') || lowerMessage.includes('network')) {
|
|
287
|
+
return `${operation} failed: Connection error. Check storage services.`;
|
|
288
|
+
}
|
|
289
|
+
if (lowerMessage.includes('not indexed') || lowerMessage.includes('no project')) {
|
|
290
|
+
const pathHint = context?.projectPath ? `index({action: "init", path: "${context.projectPath}"})` : 'index({action: "init", path: "/path/to/project"})';
|
|
291
|
+
return `${operation} failed: Project not indexed. Run: ${pathHint}`;
|
|
292
|
+
}
|
|
293
|
+
if (lowerMessage.includes('out of memory') || lowerMessage.includes('heap')) {
|
|
294
|
+
return `${operation} failed: Out of memory. Use index({action: "exclude"}) to skip large directories.`;
|
|
295
|
+
}
|
|
296
|
+
return `${operation} failed: ${message}`;
|
|
297
|
+
}
|
|
298
|
+
matchesExclusionPattern(filePath, pattern) {
|
|
299
|
+
const normalizedPath = filePath.replace(/\\/g, '/').toLowerCase();
|
|
300
|
+
const normalizedPattern = pattern.replace(/\\/g, '/').toLowerCase();
|
|
301
|
+
if (normalizedPath === normalizedPattern)
|
|
302
|
+
return true;
|
|
303
|
+
let regexPattern = normalizedPattern
|
|
304
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
305
|
+
.replace(/\*\*/g, '<<GLOBSTAR>>')
|
|
306
|
+
.replace(/\*/g, '[^/]*')
|
|
307
|
+
.replace(/<<GLOBSTAR>>/g, '.*')
|
|
308
|
+
.replace(/\?/g, '.');
|
|
309
|
+
if (!regexPattern.startsWith('.*')) {
|
|
310
|
+
regexPattern = `(^|/)${regexPattern}`;
|
|
311
|
+
}
|
|
312
|
+
regexPattern = `${regexPattern}(/.*)?$`;
|
|
313
|
+
try {
|
|
314
|
+
return new RegExp(regexPattern).test(normalizedPath);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return normalizedPath.includes(normalizedPattern.replace(/\*/g, ''));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
_getIndexingStatusForProject(projectId) {
|
|
321
|
+
const job = this.indexingJobs.get(projectId);
|
|
322
|
+
if (!job)
|
|
323
|
+
return null;
|
|
324
|
+
return {
|
|
325
|
+
indexing_status: job.status,
|
|
326
|
+
indexing_progress: job.progress,
|
|
327
|
+
indexing_result: job.result,
|
|
328
|
+
indexing_error: job.error,
|
|
329
|
+
indexing_started: job.startedAt.toISOString(),
|
|
330
|
+
indexing_completed: job.completedAt?.toISOString(),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Resolve project from name/path, returning project record and path.
|
|
335
|
+
* Shared helper for search and analyze tools.
|
|
336
|
+
*/
|
|
337
|
+
async resolveProject(project) {
|
|
338
|
+
const storageManager = await (0, storage_1.getStorageManager)();
|
|
339
|
+
const projectStore = storageManager.getProjectStore();
|
|
340
|
+
const projects = await projectStore.list();
|
|
341
|
+
if (project) {
|
|
342
|
+
const found = projects.find(p => p.name === project ||
|
|
343
|
+
p.path === project ||
|
|
344
|
+
path.basename(p.path) === project ||
|
|
345
|
+
path.resolve(project) === p.path);
|
|
346
|
+
if (found) {
|
|
347
|
+
return { projectPath: found.path, projectRecord: found };
|
|
348
|
+
}
|
|
349
|
+
return { projectPath: await this.findProjectPath(path.resolve(project)) };
|
|
350
|
+
}
|
|
351
|
+
if (projects.length === 0) {
|
|
352
|
+
return {
|
|
353
|
+
projectPath: '',
|
|
354
|
+
error: {
|
|
355
|
+
content: [{ type: 'text', text: 'No indexed projects. Use index({action: "init", path: "/path/to/project"}) first.' }],
|
|
356
|
+
isError: true,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (projects.length === 1) {
|
|
361
|
+
return { projectPath: projects[0].path, projectRecord: projects[0] };
|
|
362
|
+
}
|
|
363
|
+
const projectList = projects.map(p => ` - "${p.name}" (${p.path})`).join('\n');
|
|
364
|
+
return {
|
|
365
|
+
projectPath: '',
|
|
366
|
+
error: {
|
|
367
|
+
content: [{ type: 'text', text: `Multiple projects indexed. Specify project parameter:\n\n${projectList}` }],
|
|
368
|
+
isError: true,
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
312
372
|
/**
|
|
313
|
-
*
|
|
373
|
+
* Verify project has embeddings (is actually indexed).
|
|
314
374
|
*/
|
|
375
|
+
async verifyIndexed(projectPath, projectRecord) {
|
|
376
|
+
if (!projectRecord)
|
|
377
|
+
return {};
|
|
378
|
+
const storageManager = await (0, storage_1.getStorageManager)();
|
|
379
|
+
const vectorStore = storageManager.getVectorStore();
|
|
380
|
+
try {
|
|
381
|
+
const testResults = await vectorStore.searchByText('test', projectRecord.id, 1);
|
|
382
|
+
if (!testResults || testResults.length === 0) {
|
|
383
|
+
return {
|
|
384
|
+
error: {
|
|
385
|
+
content: [{ type: 'text', text: `Project "${path.basename(projectPath)}" not indexed. Run index({action: "init", path: "${projectPath}"}) first.` }],
|
|
386
|
+
isError: true,
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return {
|
|
393
|
+
error: {
|
|
394
|
+
content: [{ type: 'text', text: `Project "${path.basename(projectPath)}" needs indexing. Run index({action: "init", path: "${projectPath}"}) first.` }],
|
|
395
|
+
isError: true,
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return {};
|
|
400
|
+
}
|
|
401
|
+
// ============================================================
|
|
402
|
+
// TOOL REGISTRATION - 3 CONSOLIDATED TOOLS
|
|
403
|
+
// ============================================================
|
|
315
404
|
registerTools() {
|
|
316
405
|
this.registerSearchTool();
|
|
317
|
-
this.
|
|
318
|
-
this.registerReadWithContextTool();
|
|
319
|
-
this.registerShowDependenciesTool();
|
|
320
|
-
this.registerProjectsTool();
|
|
406
|
+
this.registerAnalyzeTool();
|
|
321
407
|
this.registerIndexTool();
|
|
322
|
-
this.registerSyncTool();
|
|
323
|
-
this.registerInstallParsersTool();
|
|
324
|
-
this.registerExcludeTool();
|
|
325
|
-
this.registerFindDuplicatesTool();
|
|
326
|
-
this.registerFindDeadCodeTool();
|
|
327
408
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
409
|
+
// ============================================================
|
|
410
|
+
// TOOL 1: search
|
|
411
|
+
// Combines: search, search_and_read, read_with_context
|
|
412
|
+
// ============================================================
|
|
331
413
|
registerSearchTool() {
|
|
332
414
|
this.server.registerTool('search', {
|
|
333
|
-
description: '
|
|
334
|
-
'
|
|
335
|
-
'ALWAYS use for: "Where is X handled?", "Find the auth logic", "How does Y work?", "What calls Z?" ' +
|
|
336
|
-
'Only fall back to grep when: you need exact literal strings, regex patterns, or already know the exact file. ' +
|
|
337
|
-
'Why better than grep: finds "user authentication" even if code says "login", "session", "credentials". ' +
|
|
338
|
-
'Examples: ❌ grep -r "damage.*ship" → ✅ search("how ships take damage"). ' +
|
|
339
|
-
'Returns absolute file paths ready for the Read tool. If not indexed, call index first. ' +
|
|
340
|
-
'**IMPORTANT**: Always pass the project parameter with the current working directory to ensure correct index is searched.',
|
|
415
|
+
description: 'Semantic code search. Finds code by meaning, not just text. ' +
|
|
416
|
+
'Pass query to search, add read=true to include file contents, or pass filepath instead to read a file with related context.',
|
|
341
417
|
inputSchema: {
|
|
342
|
-
query: zod_1.z.string().describe('Natural language query
|
|
343
|
-
|
|
344
|
-
|
|
418
|
+
query: zod_1.z.string().optional().describe('Natural language search query'),
|
|
419
|
+
filepath: zod_1.z.string().optional().describe('File path to read with semantic context (alternative to query)'),
|
|
420
|
+
project: zod_1.z.string().optional().describe('Project path or name'),
|
|
421
|
+
read: zod_1.z.boolean().optional().default(false).describe('Include file contents in results'),
|
|
422
|
+
limit: zod_1.z.number().optional().default(10).describe('Max results'),
|
|
423
|
+
max_lines: zod_1.z.number().optional().default(500).describe('Max lines per file when read=true'),
|
|
345
424
|
search_type: zod_1.z.enum(['hybrid', 'fts', 'vector', 'graph']).optional().default('hybrid')
|
|
346
|
-
.describe('Search method
|
|
425
|
+
.describe('Search method'),
|
|
347
426
|
mode: zod_1.z.enum(['full', 'exists']).optional().default('full')
|
|
348
|
-
.describe('
|
|
427
|
+
.describe('"exists" returns quick yes/no'),
|
|
349
428
|
},
|
|
350
|
-
}, async ({ query, project, limit = 10, search_type = 'hybrid', mode = 'full' }) => {
|
|
429
|
+
}, async ({ query, filepath, project, read = false, limit = 10, max_lines = 500, search_type = 'hybrid', mode = 'full' }) => {
|
|
351
430
|
try {
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const projects = await projectStore.list();
|
|
356
|
-
// Resolve project path - auto-detect if not provided
|
|
357
|
-
let projectPath;
|
|
358
|
-
let projectRecord;
|
|
359
|
-
if (project) {
|
|
360
|
-
// Try to find by name/path
|
|
361
|
-
projectRecord = projects.find(p => p.name === project ||
|
|
362
|
-
p.path === project ||
|
|
363
|
-
path.basename(p.path) === project ||
|
|
364
|
-
path.resolve(project) === p.path);
|
|
365
|
-
if (projectRecord) {
|
|
366
|
-
projectPath = projectRecord.path;
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
// Use provided path directly and try to find CodeSeeker project
|
|
370
|
-
projectPath = await this.findProjectPath(path.resolve(project));
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
else {
|
|
374
|
-
// No project specified - this is unreliable! MCP servers don't receive client's cwd.
|
|
375
|
-
// We'll try to auto-detect but require explicit parameter when ambiguous.
|
|
376
|
-
if (projects.length === 0) {
|
|
377
|
-
return {
|
|
378
|
-
content: [{
|
|
379
|
-
type: 'text',
|
|
380
|
-
text: `No indexed projects found. Use index to index a project first.`,
|
|
381
|
-
}],
|
|
382
|
-
isError: true,
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
else if (projects.length === 1) {
|
|
386
|
-
// Only one project indexed - safe to use it
|
|
387
|
-
projectRecord = projects[0];
|
|
388
|
-
projectPath = projectRecord.path;
|
|
389
|
-
}
|
|
390
|
-
else {
|
|
391
|
-
// Multiple projects - we can't reliably detect which one
|
|
392
|
-
// Return an error asking for explicit project parameter
|
|
393
|
-
const projectList = projects.map(p => ` - "${p.name}" (${p.path})`).join('\n');
|
|
394
|
-
return {
|
|
395
|
-
content: [{
|
|
396
|
-
type: 'text',
|
|
397
|
-
text: `⚠️ Multiple projects indexed. Please specify which project to search:\n\n` +
|
|
398
|
-
`${projectList}\n\n` +
|
|
399
|
-
`Example: search({query: "${query}", project: "/path/to/project"})\n\n` +
|
|
400
|
-
`TIP: Always pass the 'project' parameter with your workspace root path.`,
|
|
401
|
-
}],
|
|
402
|
-
isError: true,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
// Check if project is indexed by checking for embeddings
|
|
407
|
-
const vectorStore = storageManager.getVectorStore();
|
|
408
|
-
if (!projectRecord) {
|
|
409
|
-
projectRecord = await projectStore.findByPath(projectPath);
|
|
410
|
-
}
|
|
411
|
-
if (projectRecord) {
|
|
412
|
-
// Quick check: does this project have any embeddings?
|
|
413
|
-
try {
|
|
414
|
-
const testResults = await vectorStore.searchByText('test', projectRecord.id, 1);
|
|
415
|
-
if (!testResults || testResults.length === 0) {
|
|
416
|
-
return {
|
|
417
|
-
content: [{
|
|
418
|
-
type: 'text',
|
|
419
|
-
text: `⚠️ Project "${path.basename(projectPath)}" found but not indexed.\n\n` +
|
|
420
|
-
`ACTION REQUIRED: Call index({path: "${projectPath}"}) then retry this search.`,
|
|
421
|
-
}],
|
|
422
|
-
isError: true,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
catch (err) {
|
|
427
|
-
// If search fails, project likely not indexed
|
|
428
|
-
return {
|
|
429
|
-
content: [{
|
|
430
|
-
type: 'text',
|
|
431
|
-
text: `⚠️ Project "${path.basename(projectPath)}" needs indexing.\n\n` +
|
|
432
|
-
`ACTION REQUIRED: Call index({path: "${projectPath}"}) then retry this search.`,
|
|
433
|
-
}],
|
|
434
|
-
isError: true,
|
|
435
|
-
};
|
|
436
|
-
}
|
|
431
|
+
// Dispatch: filepath → read-with-context, read → search-and-read, else → search
|
|
432
|
+
if (filepath) {
|
|
433
|
+
return await this.handleReadWithContext(filepath, project, !read ? true : read);
|
|
437
434
|
}
|
|
438
|
-
|
|
439
|
-
let results;
|
|
440
|
-
let fromCache = false;
|
|
441
|
-
const cacheProjectId = projectRecord?.id || this.generateProjectId(projectPath);
|
|
442
|
-
if (mode === 'full') {
|
|
443
|
-
const cached = await this.queryCache.get(query, cacheProjectId, search_type);
|
|
444
|
-
if (cached) {
|
|
445
|
-
results = cached.results;
|
|
446
|
-
fromCache = true;
|
|
447
|
-
}
|
|
448
|
-
else {
|
|
449
|
-
// Perform actual search
|
|
450
|
-
results = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
|
|
451
|
-
// Cache results for future queries
|
|
452
|
-
if (results.length > 0) {
|
|
453
|
-
await this.queryCache.set(query, cacheProjectId, results, search_type);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
// exists mode - always search fresh (it's fast)
|
|
459
|
-
results = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
|
|
460
|
-
}
|
|
461
|
-
const limitedResults = results.slice(0, mode === 'exists' ? 5 : limit);
|
|
462
|
-
if (limitedResults.length === 0) {
|
|
463
|
-
// For exists mode, return structured response
|
|
464
|
-
if (mode === 'exists') {
|
|
465
|
-
return {
|
|
466
|
-
content: [{
|
|
467
|
-
type: 'text',
|
|
468
|
-
text: JSON.stringify({
|
|
469
|
-
exists: false,
|
|
470
|
-
query,
|
|
471
|
-
project: projectPath,
|
|
472
|
-
message: 'No matching code found',
|
|
473
|
-
}, null, 2),
|
|
474
|
-
}],
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
return {
|
|
478
|
-
content: [{
|
|
479
|
-
type: 'text',
|
|
480
|
-
text: `No results found for query: "${query}"\n\n` +
|
|
481
|
-
`This could mean:\n` +
|
|
482
|
-
`1. No matching code exists for this query\n` +
|
|
483
|
-
`2. Try different search terms or broader queries\n` +
|
|
484
|
-
`3. The project may need reindexing if code was recently added`,
|
|
485
|
-
}],
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
// For exists mode, return quick summary
|
|
489
|
-
if (mode === 'exists') {
|
|
490
|
-
const topResult = limitedResults[0];
|
|
491
|
-
const absolutePath = path.isAbsolute(topResult.file)
|
|
492
|
-
? topResult.file
|
|
493
|
-
: path.join(projectPath, topResult.file);
|
|
494
|
-
return {
|
|
495
|
-
content: [{
|
|
496
|
-
type: 'text',
|
|
497
|
-
text: JSON.stringify({
|
|
498
|
-
exists: true,
|
|
499
|
-
query,
|
|
500
|
-
project: projectPath,
|
|
501
|
-
total_matches: results.length,
|
|
502
|
-
top_file: absolutePath,
|
|
503
|
-
top_score: Math.round(topResult.similarity * 100) / 100,
|
|
504
|
-
hint: `Use Read tool with "${absolutePath}" to view the file`,
|
|
505
|
-
}, null, 2),
|
|
506
|
-
}],
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
// Format full results with absolute paths and match type info
|
|
510
|
-
const formattedResults = limitedResults.map((r, i) => {
|
|
511
|
-
const absolutePath = path.isAbsolute(r.file)
|
|
512
|
-
? r.file
|
|
513
|
-
: path.join(projectPath, r.file);
|
|
435
|
+
if (!query) {
|
|
514
436
|
return {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
relative_path: r.file,
|
|
518
|
-
score: Math.round(r.similarity * 100) / 100,
|
|
519
|
-
type: r.type,
|
|
520
|
-
// Include match source for better understanding of why file matched
|
|
521
|
-
match_source: r.debug?.matchSource || 'hybrid',
|
|
522
|
-
chunk: r.content.substring(0, 500) + (r.content.length > 500 ? '...' : ''),
|
|
523
|
-
lines: r.lineStart && r.lineEnd ? `${r.lineStart}-${r.lineEnd}` : undefined,
|
|
437
|
+
content: [{ type: 'text', text: 'Provide either query or filepath parameter.' }],
|
|
438
|
+
isError: true,
|
|
524
439
|
};
|
|
525
|
-
});
|
|
526
|
-
// Build response with truncation warning if applicable
|
|
527
|
-
const wasLimited = results.length > limit;
|
|
528
|
-
const response = {
|
|
529
|
-
query,
|
|
530
|
-
project: projectPath,
|
|
531
|
-
total_results: limitedResults.length,
|
|
532
|
-
search_type,
|
|
533
|
-
results: formattedResults,
|
|
534
|
-
};
|
|
535
|
-
// Add cache indicator
|
|
536
|
-
if (fromCache) {
|
|
537
|
-
response.cached = true;
|
|
538
440
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
response.truncated = true;
|
|
542
|
-
response.total_available = results.length;
|
|
543
|
-
response.hint = `Showing ${limit} of ${results.length} results. Use limit parameter to see more.`;
|
|
441
|
+
if (read) {
|
|
442
|
+
return await this.handleSearchAndRead(query, project, Math.min(limit, 3), Math.min(max_lines, 1000));
|
|
544
443
|
}
|
|
545
|
-
return
|
|
546
|
-
content: [{
|
|
547
|
-
type: 'text',
|
|
548
|
-
text: JSON.stringify(response, null, 2),
|
|
549
|
-
}],
|
|
550
|
-
};
|
|
444
|
+
return await this.handleSearch(query, project, limit, search_type, mode);
|
|
551
445
|
}
|
|
552
446
|
catch (error) {
|
|
553
447
|
return {
|
|
554
|
-
content: [{
|
|
555
|
-
type: 'text',
|
|
556
|
-
text: this.formatErrorMessage('Search', error instanceof Error ? error : String(error), { projectPath: project }),
|
|
557
|
-
}],
|
|
448
|
+
content: [{ type: 'text', text: this.formatErrorMessage('Search', error instanceof Error ? error : String(error), { projectPath: project }) }],
|
|
558
449
|
isError: true,
|
|
559
450
|
};
|
|
560
451
|
}
|
|
561
452
|
});
|
|
562
453
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
this.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
try {
|
|
585
|
-
// Cap the limits
|
|
586
|
-
const fileLimit = Math.min(max_files, 3);
|
|
587
|
-
const lineLimit = Math.min(max_lines, 1000);
|
|
588
|
-
// Get storage manager
|
|
589
|
-
const storageManager = await (0, storage_1.getStorageManager)();
|
|
590
|
-
const projectStore = storageManager.getProjectStore();
|
|
591
|
-
const projects = await projectStore.list();
|
|
592
|
-
// Resolve project path - auto-detect if not provided
|
|
593
|
-
let projectPath;
|
|
594
|
-
let projectRecord;
|
|
595
|
-
if (project) {
|
|
596
|
-
// Try to find by name/path
|
|
597
|
-
projectRecord = projects.find(p => p.name === project ||
|
|
598
|
-
p.path === project ||
|
|
599
|
-
path.basename(p.path) === project ||
|
|
600
|
-
path.resolve(project) === p.path);
|
|
601
|
-
if (projectRecord) {
|
|
602
|
-
projectPath = projectRecord.path;
|
|
603
|
-
}
|
|
604
|
-
else {
|
|
605
|
-
projectPath = await this.findProjectPath(path.resolve(project));
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
else {
|
|
609
|
-
// No project specified - require explicit parameter when ambiguous
|
|
610
|
-
if (projects.length === 0) {
|
|
611
|
-
return {
|
|
612
|
-
content: [{
|
|
613
|
-
type: 'text',
|
|
614
|
-
text: `No indexed projects found. Use index to index a project first.`,
|
|
615
|
-
}],
|
|
616
|
-
isError: true,
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
else if (projects.length === 1) {
|
|
620
|
-
// Only one project indexed - safe to use it
|
|
621
|
-
projectRecord = projects[0];
|
|
622
|
-
projectPath = projectRecord.path;
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
|
-
// Multiple projects - require explicit project parameter
|
|
626
|
-
const projectList = projects.map(p => ` - "${p.name}" (${p.path})`).join('\n');
|
|
627
|
-
return {
|
|
628
|
-
content: [{
|
|
629
|
-
type: 'text',
|
|
630
|
-
text: `⚠️ Multiple projects indexed. Please specify which project to search:\n\n` +
|
|
631
|
-
`${projectList}\n\n` +
|
|
632
|
-
`Example: search_and_read({query: "${query}", project: "/path/to/project"})\n\n` +
|
|
633
|
-
`TIP: Always pass the 'project' parameter with your workspace root path.`,
|
|
634
|
-
}],
|
|
635
|
-
isError: true,
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
// Check if project is indexed
|
|
640
|
-
const vectorStore = storageManager.getVectorStore();
|
|
641
|
-
if (!projectRecord) {
|
|
642
|
-
projectRecord = await projectStore.findByPath(projectPath);
|
|
643
|
-
}
|
|
644
|
-
if (projectRecord) {
|
|
645
|
-
try {
|
|
646
|
-
const testResults = await vectorStore.searchByText('test', projectRecord.id, 1);
|
|
647
|
-
if (!testResults || testResults.length === 0) {
|
|
648
|
-
return {
|
|
649
|
-
content: [{
|
|
650
|
-
type: 'text',
|
|
651
|
-
text: `⚠️ Project "${path.basename(projectPath)}" found but not indexed.\n\n` +
|
|
652
|
-
`ACTION REQUIRED: Call index({path: "${projectPath}"}) then retry.`,
|
|
653
|
-
}],
|
|
654
|
-
isError: true,
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
catch {
|
|
659
|
-
return {
|
|
660
|
-
content: [{
|
|
661
|
-
type: 'text',
|
|
662
|
-
text: `⚠️ Project "${path.basename(projectPath)}" needs indexing.\n\n` +
|
|
663
|
-
`ACTION REQUIRED: Call index({path: "${projectPath}"}) then retry.`,
|
|
664
|
-
}],
|
|
665
|
-
isError: true,
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
// Perform search
|
|
670
|
-
const results = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
|
|
671
|
-
if (results.length === 0) {
|
|
672
|
-
return {
|
|
673
|
-
content: [{
|
|
674
|
-
type: 'text',
|
|
675
|
-
text: JSON.stringify({
|
|
676
|
-
query,
|
|
677
|
-
project: projectPath,
|
|
678
|
-
found: false,
|
|
679
|
-
message: 'No matching code found. Try different search terms.',
|
|
680
|
-
}, null, 2),
|
|
681
|
-
}],
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
// Get unique files (a search may return multiple chunks from the same file)
|
|
685
|
-
const seenFiles = new Set();
|
|
686
|
-
const uniqueResults = [];
|
|
687
|
-
for (const r of results) {
|
|
688
|
-
const normalizedPath = r.file.replace(/\\/g, '/');
|
|
689
|
-
if (!seenFiles.has(normalizedPath)) {
|
|
690
|
-
seenFiles.add(normalizedPath);
|
|
691
|
-
uniqueResults.push(r);
|
|
692
|
-
if (uniqueResults.length >= fileLimit)
|
|
693
|
-
break;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
// Read each file
|
|
697
|
-
const files = [];
|
|
698
|
-
for (const result of uniqueResults) {
|
|
699
|
-
const absolutePath = path.isAbsolute(result.file)
|
|
700
|
-
? result.file
|
|
701
|
-
: path.join(projectPath, result.file);
|
|
702
|
-
try {
|
|
703
|
-
if (!fs.existsSync(absolutePath)) {
|
|
704
|
-
continue;
|
|
705
|
-
}
|
|
706
|
-
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
707
|
-
const lines = content.split('\n');
|
|
708
|
-
const truncated = lines.length > lineLimit;
|
|
709
|
-
const displayLines = truncated ? lines.slice(0, lineLimit) : lines;
|
|
710
|
-
// Add line numbers
|
|
711
|
-
const numberedContent = displayLines
|
|
712
|
-
.map((line, i) => `${String(i + 1).padStart(4)}│ ${line}`)
|
|
713
|
-
.join('\n');
|
|
714
|
-
files.push({
|
|
715
|
-
file: absolutePath,
|
|
716
|
-
relative_path: result.file,
|
|
717
|
-
score: Math.round(result.similarity * 100) / 100,
|
|
718
|
-
file_type: result.type,
|
|
719
|
-
match_source: result.debug?.matchSource || 'hybrid',
|
|
720
|
-
line_count: lines.length,
|
|
721
|
-
content: numberedContent + (truncated ? `\n... (truncated at ${lineLimit} lines)` : ''),
|
|
722
|
-
truncated,
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
catch (err) {
|
|
726
|
-
// Skip files we can't read
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
if (files.length === 0) {
|
|
731
|
-
return {
|
|
732
|
-
content: [{
|
|
733
|
-
type: 'text',
|
|
734
|
-
text: JSON.stringify({
|
|
735
|
-
query,
|
|
736
|
-
project: projectPath,
|
|
737
|
-
found: true,
|
|
738
|
-
readable: false,
|
|
739
|
-
message: 'Found matching files but could not read them.',
|
|
740
|
-
}, null, 2),
|
|
741
|
-
}],
|
|
742
|
-
};
|
|
454
|
+
async handleSearch(query, project, limit, search_type, mode) {
|
|
455
|
+
const { projectPath, projectRecord, error } = await this.resolveProject(project);
|
|
456
|
+
if (error)
|
|
457
|
+
return error;
|
|
458
|
+
const indexCheck = await this.verifyIndexed(projectPath, projectRecord);
|
|
459
|
+
if (indexCheck.error)
|
|
460
|
+
return indexCheck.error;
|
|
461
|
+
// Check cache (only for 'full' mode)
|
|
462
|
+
let results;
|
|
463
|
+
let fromCache = false;
|
|
464
|
+
const cacheProjectId = projectRecord?.id || this.generateProjectId(projectPath);
|
|
465
|
+
if (mode === 'full') {
|
|
466
|
+
const cached = await this.queryCache.get(query, cacheProjectId, search_type);
|
|
467
|
+
if (cached) {
|
|
468
|
+
results = cached.results;
|
|
469
|
+
fromCache = true;
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
results = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
|
|
473
|
+
if (results.length > 0) {
|
|
474
|
+
await this.queryCache.set(query, cacheProjectId, results, search_type);
|
|
743
475
|
}
|
|
744
|
-
return {
|
|
745
|
-
content: [{
|
|
746
|
-
type: 'text',
|
|
747
|
-
text: JSON.stringify({
|
|
748
|
-
query,
|
|
749
|
-
project: projectPath,
|
|
750
|
-
files_found: results.length,
|
|
751
|
-
files_returned: files.length,
|
|
752
|
-
results: files,
|
|
753
|
-
}, null, 2),
|
|
754
|
-
}],
|
|
755
|
-
};
|
|
756
476
|
}
|
|
757
|
-
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
results = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
|
|
480
|
+
}
|
|
481
|
+
const limitedResults = results.slice(0, mode === 'exists' ? 5 : limit);
|
|
482
|
+
if (limitedResults.length === 0) {
|
|
483
|
+
if (mode === 'exists') {
|
|
758
484
|
return {
|
|
759
|
-
content: [{
|
|
760
|
-
type: 'text',
|
|
761
|
-
text: this.formatErrorMessage('Find and read', error instanceof Error ? error : String(error), { projectPath: project }),
|
|
762
|
-
}],
|
|
763
|
-
isError: true,
|
|
485
|
+
content: [{ type: 'text', text: JSON.stringify({ exists: false, query, project: projectPath, message: 'No matching code found' }, null, 2) }],
|
|
764
486
|
};
|
|
765
487
|
}
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: 'text', text: `No results for: "${query}". Try different terms or reindex.` }],
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (mode === 'exists') {
|
|
493
|
+
const topResult = limitedResults[0];
|
|
494
|
+
const absolutePath = path.isAbsolute(topResult.file) ? topResult.file : path.join(projectPath, topResult.file);
|
|
495
|
+
return {
|
|
496
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
497
|
+
exists: true, query, project: projectPath,
|
|
498
|
+
total_matches: results.length, top_file: absolutePath,
|
|
499
|
+
top_score: Math.round(topResult.similarity * 100) / 100,
|
|
500
|
+
}, null, 2) }],
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const formattedResults = limitedResults.map((r, i) => {
|
|
504
|
+
const absolutePath = path.isAbsolute(r.file) ? r.file : path.join(projectPath, r.file);
|
|
505
|
+
return {
|
|
506
|
+
rank: i + 1,
|
|
507
|
+
file: absolutePath,
|
|
508
|
+
relative_path: r.file,
|
|
509
|
+
score: Math.round(r.similarity * 100) / 100,
|
|
510
|
+
type: r.type,
|
|
511
|
+
match_source: r.debug?.matchSource || 'hybrid',
|
|
512
|
+
chunk: r.content.substring(0, 500) + (r.content.length > 500 ? '...' : ''),
|
|
513
|
+
lines: r.lineStart && r.lineEnd ? `${r.lineStart}-${r.lineEnd}` : undefined,
|
|
514
|
+
};
|
|
766
515
|
});
|
|
516
|
+
const wasLimited = results.length > limit;
|
|
517
|
+
const response = {
|
|
518
|
+
query, project: projectPath,
|
|
519
|
+
total_results: limitedResults.length,
|
|
520
|
+
search_type, results: formattedResults,
|
|
521
|
+
};
|
|
522
|
+
if (fromCache)
|
|
523
|
+
response.cached = true;
|
|
524
|
+
if (wasLimited) {
|
|
525
|
+
response.truncated = true;
|
|
526
|
+
response.total_available = results.length;
|
|
527
|
+
}
|
|
528
|
+
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
|
767
529
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
530
|
+
async handleSearchAndRead(query, project, max_files, max_lines) {
|
|
531
|
+
const fileLimit = Math.min(max_files, 3);
|
|
532
|
+
const lineLimit = Math.min(max_lines, 1000);
|
|
533
|
+
const { projectPath, projectRecord, error } = await this.resolveProject(project);
|
|
534
|
+
if (error)
|
|
535
|
+
return error;
|
|
536
|
+
const indexCheck = await this.verifyIndexed(projectPath, projectRecord);
|
|
537
|
+
if (indexCheck.error)
|
|
538
|
+
return indexCheck.error;
|
|
539
|
+
const results = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
|
|
540
|
+
if (results.length === 0) {
|
|
541
|
+
return {
|
|
542
|
+
content: [{ type: 'text', text: JSON.stringify({ query, project: projectPath, found: false, message: 'No matching code found.' }, null, 2) }],
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
// Get unique files
|
|
546
|
+
const seenFiles = new Set();
|
|
547
|
+
const uniqueResults = [];
|
|
548
|
+
for (const r of results) {
|
|
549
|
+
const normalizedPath = r.file.replace(/\\/g, '/');
|
|
550
|
+
if (!seenFiles.has(normalizedPath)) {
|
|
551
|
+
seenFiles.add(normalizedPath);
|
|
552
|
+
uniqueResults.push(r);
|
|
553
|
+
if (uniqueResults.length >= fileLimit)
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const files = [];
|
|
558
|
+
for (const result of uniqueResults) {
|
|
559
|
+
const absolutePath = path.isAbsolute(result.file) ? result.file : path.join(projectPath, result.file);
|
|
560
|
+
try {
|
|
561
|
+
if (!fs.existsSync(absolutePath))
|
|
562
|
+
continue;
|
|
563
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
564
|
+
const lines = content.split('\n');
|
|
565
|
+
const truncated = lines.length > lineLimit;
|
|
566
|
+
const displayLines = truncated ? lines.slice(0, lineLimit) : lines;
|
|
567
|
+
const numberedContent = displayLines.map((line, i) => `${String(i + 1).padStart(4)}│ ${line}`).join('\n');
|
|
568
|
+
files.push({
|
|
569
|
+
file: absolutePath,
|
|
570
|
+
relative_path: result.file,
|
|
571
|
+
score: Math.round(result.similarity * 100) / 100,
|
|
572
|
+
file_type: result.type,
|
|
573
|
+
match_source: result.debug?.matchSource || 'hybrid',
|
|
574
|
+
line_count: lines.length,
|
|
575
|
+
content: numberedContent + (truncated ? `\n... (truncated at ${lineLimit} lines)` : ''),
|
|
576
|
+
truncated,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (files.length === 0) {
|
|
584
|
+
return {
|
|
585
|
+
content: [{ type: 'text', text: JSON.stringify({ query, project: projectPath, found: true, readable: false, message: 'Found matching files but could not read them.' }, null, 2) }],
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
590
|
+
query, project: projectPath,
|
|
591
|
+
files_found: results.length, files_returned: files.length,
|
|
592
|
+
results: files,
|
|
593
|
+
}, null, 2) }],
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
async handleReadWithContext(filepath, project, include_related) {
|
|
597
|
+
const storageManager = await (0, storage_1.getStorageManager)();
|
|
598
|
+
const projectStore = storageManager.getProjectStore();
|
|
599
|
+
let projectPath;
|
|
600
|
+
if (project) {
|
|
601
|
+
const projects = await projectStore.list();
|
|
602
|
+
const found = projects.find(p => p.name === project || p.path === project || path.basename(p.path) === project);
|
|
603
|
+
projectPath = found?.path || process.cwd();
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
projectPath = process.cwd();
|
|
607
|
+
}
|
|
608
|
+
const absolutePath = path.isAbsolute(filepath) ? filepath : path.join(projectPath, filepath);
|
|
609
|
+
if (!fs.existsSync(absolutePath)) {
|
|
610
|
+
return { content: [{ type: 'text', text: `File not found: ${absolutePath}` }], isError: true };
|
|
611
|
+
}
|
|
612
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
613
|
+
let relatedChunks = [];
|
|
614
|
+
if (include_related) {
|
|
615
|
+
const lines = content.split('\n');
|
|
616
|
+
const meaningfulLines = [];
|
|
617
|
+
for (const line of lines) {
|
|
618
|
+
const trimmed = line.trim();
|
|
619
|
+
if (!trimmed)
|
|
620
|
+
continue;
|
|
621
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))
|
|
622
|
+
continue;
|
|
623
|
+
if (trimmed.startsWith('import ') || trimmed.startsWith('from ') || trimmed.startsWith('require('))
|
|
624
|
+
continue;
|
|
625
|
+
if (trimmed.startsWith('#') && !trimmed.startsWith('##'))
|
|
626
|
+
continue;
|
|
627
|
+
if (trimmed.startsWith('using ') || trimmed.startsWith('namespace '))
|
|
628
|
+
continue;
|
|
629
|
+
meaningfulLines.push(trimmed);
|
|
630
|
+
if (meaningfulLines.length >= 5)
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
const fileName = path.basename(filepath);
|
|
634
|
+
const fileNameQuery = fileName.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
|
635
|
+
const contentQuery = meaningfulLines.join(' ').substring(0, 200);
|
|
636
|
+
const searchQuery = `${fileNameQuery} ${contentQuery}`.trim();
|
|
637
|
+
const results = await this.searchOrchestrator.performSemanticSearch(searchQuery || fileNameQuery, projectPath);
|
|
638
|
+
relatedChunks = results
|
|
639
|
+
.filter(r => !r.file.endsWith(path.basename(filepath)))
|
|
640
|
+
.slice(0, 5)
|
|
641
|
+
.map(r => ({
|
|
642
|
+
file: r.file,
|
|
643
|
+
chunk: r.content.substring(0, 300) + (r.content.length > 300 ? '...' : ''),
|
|
644
|
+
score: Math.round(r.similarity * 100) / 100,
|
|
645
|
+
}));
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
649
|
+
filepath: path.relative(projectPath, absolutePath),
|
|
650
|
+
content: content.length > 10000 ? content.substring(0, 10000) + '\n... (truncated)' : content,
|
|
651
|
+
line_count: content.split('\n').length,
|
|
652
|
+
related_chunks: include_related ? relatedChunks : undefined,
|
|
653
|
+
}, null, 2) }],
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
// ============================================================
|
|
657
|
+
// TOOL 2: analyze
|
|
658
|
+
// Combines: show_dependencies, find_duplicates, find_dead_code, standards
|
|
659
|
+
// ============================================================
|
|
660
|
+
registerAnalyzeTool() {
|
|
661
|
+
this.server.registerTool('analyze', {
|
|
662
|
+
description: 'Code analysis. Actions: "dependencies" (imports/calls/extends graph), ' +
|
|
663
|
+
'"dead_code" (unused code, anti-patterns), "duplicates" (similar code), ' +
|
|
664
|
+
'"standards" (auto-detected coding patterns).',
|
|
780
665
|
inputSchema: {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
666
|
+
action: zod_1.z.enum(['dependencies', 'dead_code', 'duplicates', 'standards']).describe('Analysis type'),
|
|
667
|
+
project: zod_1.z.string().describe('Project path or name'),
|
|
668
|
+
// dependencies params
|
|
669
|
+
filepath: zod_1.z.string().optional().describe('File for dependency analysis'),
|
|
670
|
+
filepaths: zod_1.z.array(zod_1.z.string()).optional().describe('Multiple files for dependency analysis'),
|
|
671
|
+
query: zod_1.z.string().optional().describe('Search query to find seed files for dependencies'),
|
|
672
|
+
depth: zod_1.z.number().optional().default(1).describe('Relationship hops (1-3)'),
|
|
673
|
+
relationship_types: zod_1.z.array(zod_1.z.enum([
|
|
674
|
+
'imports', 'exports', 'calls', 'extends', 'implements', 'contains', 'uses', 'depends_on'
|
|
675
|
+
])).optional().describe('Filter relationship types'),
|
|
676
|
+
direction: zod_1.z.enum(['in', 'out', 'both']).optional().default('both').describe('Relationship direction'),
|
|
677
|
+
max_nodes: zod_1.z.number().optional().default(50).describe('Max nodes'),
|
|
678
|
+
// duplicates params
|
|
679
|
+
similarity_threshold: zod_1.z.number().optional().default(0.80).describe('Min similarity for duplicates (0-1)'),
|
|
680
|
+
min_lines: zod_1.z.number().optional().default(5).describe('Min lines for duplicate analysis'),
|
|
681
|
+
// dead_code params
|
|
682
|
+
include_patterns: zod_1.z.array(zod_1.z.enum(['dead_code', 'god_class', 'circular_deps', 'feature_envy', 'coupling'])).optional()
|
|
683
|
+
.describe('Anti-patterns to detect'),
|
|
684
|
+
// standards params
|
|
685
|
+
category: zod_1.z.enum(['validation', 'error-handling', 'logging', 'testing', 'all']).optional().default('all')
|
|
686
|
+
.describe('Standards category'),
|
|
785
687
|
},
|
|
786
|
-
}, async (
|
|
688
|
+
}, async (params) => {
|
|
787
689
|
try {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
else {
|
|
800
|
-
projectPath = process.cwd();
|
|
690
|
+
switch (params.action) {
|
|
691
|
+
case 'dependencies':
|
|
692
|
+
return await this.handleShowDependencies(params);
|
|
693
|
+
case 'dead_code':
|
|
694
|
+
return await this.handleFindDeadCode(params);
|
|
695
|
+
case 'duplicates':
|
|
696
|
+
return await this.handleFindDuplicates(params);
|
|
697
|
+
case 'standards':
|
|
698
|
+
return await this.handleStandards(params);
|
|
699
|
+
default:
|
|
700
|
+
return { content: [{ type: 'text', text: `Unknown action: ${params.action}` }], isError: true };
|
|
801
701
|
}
|
|
802
|
-
// Resolve file path
|
|
803
|
-
const absolutePath = path.isAbsolute(filepath)
|
|
804
|
-
? filepath
|
|
805
|
-
: path.join(projectPath, filepath);
|
|
806
|
-
// Read file content
|
|
807
|
-
if (!fs.existsSync(absolutePath)) {
|
|
808
|
-
return {
|
|
809
|
-
content: [{
|
|
810
|
-
type: 'text',
|
|
811
|
-
text: `File not found: ${absolutePath}`,
|
|
812
|
-
}],
|
|
813
|
-
isError: true,
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
817
|
-
// Get related chunks if requested
|
|
818
|
-
let relatedChunks = [];
|
|
819
|
-
if (include_related) {
|
|
820
|
-
// Build a semantic search query from the file content
|
|
821
|
-
// Use the first meaningful lines of code (skip comments, imports, empty lines)
|
|
822
|
-
const lines = content.split('\n');
|
|
823
|
-
const meaningfulLines = [];
|
|
824
|
-
for (const line of lines) {
|
|
825
|
-
const trimmed = line.trim();
|
|
826
|
-
// Skip empty, comments, and import lines
|
|
827
|
-
if (!trimmed)
|
|
828
|
-
continue;
|
|
829
|
-
if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))
|
|
830
|
-
continue;
|
|
831
|
-
if (trimmed.startsWith('import ') || trimmed.startsWith('from ') || trimmed.startsWith('require('))
|
|
832
|
-
continue;
|
|
833
|
-
if (trimmed.startsWith('#') && !trimmed.startsWith('##'))
|
|
834
|
-
continue; // Skip Python comments but not markdown headers
|
|
835
|
-
if (trimmed.startsWith('using ') || trimmed.startsWith('namespace '))
|
|
836
|
-
continue; // C#
|
|
837
|
-
meaningfulLines.push(trimmed);
|
|
838
|
-
if (meaningfulLines.length >= 5)
|
|
839
|
-
break; // Use first 5 meaningful lines
|
|
840
|
-
}
|
|
841
|
-
// Create search query from file name + meaningful content
|
|
842
|
-
const fileName = path.basename(filepath);
|
|
843
|
-
const fileNameQuery = fileName.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
|
844
|
-
const contentQuery = meaningfulLines.join(' ').substring(0, 200);
|
|
845
|
-
const searchQuery = `${fileNameQuery} ${contentQuery}`.trim();
|
|
846
|
-
const results = await this.searchOrchestrator.performSemanticSearch(searchQuery || fileNameQuery, // Fallback to filename if no content
|
|
847
|
-
projectPath);
|
|
848
|
-
// Filter out the current file and limit results
|
|
849
|
-
relatedChunks = results
|
|
850
|
-
.filter(r => !r.file.endsWith(path.basename(filepath)))
|
|
851
|
-
.slice(0, 5)
|
|
852
|
-
.map(r => ({
|
|
853
|
-
file: r.file,
|
|
854
|
-
chunk: r.content.substring(0, 300) + (r.content.length > 300 ? '...' : ''),
|
|
855
|
-
score: Math.round(r.similarity * 100) / 100,
|
|
856
|
-
}));
|
|
857
|
-
}
|
|
858
|
-
return {
|
|
859
|
-
content: [{
|
|
860
|
-
type: 'text',
|
|
861
|
-
text: JSON.stringify({
|
|
862
|
-
filepath: path.relative(projectPath, absolutePath),
|
|
863
|
-
content: content.length > 10000 ? content.substring(0, 10000) + '\n... (truncated)' : content,
|
|
864
|
-
line_count: content.split('\n').length,
|
|
865
|
-
related_chunks: include_related ? relatedChunks : undefined,
|
|
866
|
-
}, null, 2),
|
|
867
|
-
}],
|
|
868
|
-
};
|
|
869
702
|
}
|
|
870
703
|
catch (error) {
|
|
871
704
|
return {
|
|
872
|
-
content: [{
|
|
873
|
-
type: 'text',
|
|
874
|
-
text: this.formatErrorMessage('Get file context', error instanceof Error ? error : String(error), { projectPath: project }),
|
|
875
|
-
}],
|
|
705
|
+
content: [{ type: 'text', text: this.formatErrorMessage('Analyze', error instanceof Error ? error : String(error), { projectPath: params.project }) }],
|
|
876
706
|
isError: true,
|
|
877
707
|
};
|
|
878
708
|
}
|
|
879
709
|
});
|
|
880
710
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
711
|
+
async handleShowDependencies(params) {
|
|
712
|
+
const { filepath, filepaths, query, depth = 1, relationship_types, direction = 'both', max_nodes = 50, project } = params;
|
|
713
|
+
const storageManager = await (0, storage_1.getStorageManager)();
|
|
714
|
+
const projectStore = storageManager.getProjectStore();
|
|
715
|
+
const graphStore = storageManager.getGraphStore();
|
|
716
|
+
let projectId;
|
|
717
|
+
let projectPath;
|
|
718
|
+
if (project) {
|
|
719
|
+
const projects = await projectStore.list();
|
|
720
|
+
const found = projects.find(p => p.name === project || p.path === project || path.basename(p.path) === project);
|
|
721
|
+
if (found) {
|
|
722
|
+
projectId = found.id;
|
|
723
|
+
projectPath = found.path;
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
projectPath = process.cwd();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
projectPath = process.cwd();
|
|
731
|
+
const projects = await projectStore.list();
|
|
732
|
+
const found = projects.find(p => p.path === projectPath || path.basename(p.path) === path.basename(projectPath));
|
|
733
|
+
if (found) {
|
|
734
|
+
projectId = found.id;
|
|
735
|
+
projectPath = found.path;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (!projectId) {
|
|
739
|
+
return { content: [{ type: 'text', text: 'Project not indexed. Use index({action: "init"}) first.' }], isError: true };
|
|
740
|
+
}
|
|
741
|
+
// Determine seed file paths
|
|
742
|
+
let seedFilePaths = [];
|
|
743
|
+
if (query) {
|
|
744
|
+
const searchResults = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
|
|
745
|
+
seedFilePaths = searchResults.slice(0, 5).map(r => r.file.replace(/\\/g, '/'));
|
|
746
|
+
}
|
|
747
|
+
else if (filepaths && filepaths.length > 0) {
|
|
748
|
+
seedFilePaths = filepaths.map(fp => fp.replace(/\\/g, '/'));
|
|
749
|
+
}
|
|
750
|
+
else if (filepath) {
|
|
751
|
+
seedFilePaths = [filepath.replace(/\\/g, '/')];
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
return {
|
|
755
|
+
content: [{ type: 'text', text: 'Provide filepath, filepaths, or query for dependency analysis.' }],
|
|
756
|
+
isError: true,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const allNodes = await graphStore.findNodes(projectId);
|
|
760
|
+
const graphStats = {
|
|
761
|
+
total_nodes: allNodes.length,
|
|
762
|
+
file_nodes: allNodes.filter(n => n.type === 'file').length,
|
|
763
|
+
class_nodes: allNodes.filter(n => n.type === 'class').length,
|
|
764
|
+
function_nodes: allNodes.filter(n => n.type === 'function' || n.type === 'method').length,
|
|
765
|
+
};
|
|
766
|
+
// Find starting nodes using flexible path matching
|
|
767
|
+
const startNodes = allNodes.filter(n => {
|
|
768
|
+
const normalizedNodePath = n.filePath.replace(/\\/g, '/');
|
|
769
|
+
const nodeRelativePath = n.properties?.relativePath?.replace(/\\/g, '/');
|
|
770
|
+
return seedFilePaths.some(seedPath => {
|
|
771
|
+
const normalizedSeedPath = seedPath.replace(/\\/g, '/');
|
|
772
|
+
return (normalizedNodePath === normalizedSeedPath ||
|
|
773
|
+
nodeRelativePath === normalizedSeedPath ||
|
|
774
|
+
normalizedNodePath.endsWith(normalizedSeedPath) ||
|
|
775
|
+
normalizedNodePath.endsWith('/' + normalizedSeedPath) ||
|
|
776
|
+
normalizedNodePath.includes('/' + normalizedSeedPath) ||
|
|
777
|
+
n.name === path.basename(normalizedSeedPath).replace(/\.[^.]+$/, ''));
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
if (startNodes.length === 0) {
|
|
781
|
+
const fileNodes = allNodes.filter(n => n.type === 'file').slice(0, 15);
|
|
782
|
+
const availableFiles = fileNodes.map(n => {
|
|
783
|
+
const relPath = n.properties?.relativePath;
|
|
784
|
+
return relPath || path.relative(projectPath, n.filePath);
|
|
785
|
+
});
|
|
786
|
+
return {
|
|
787
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
788
|
+
error: `No graph nodes found for: ${seedFilePaths.join(', ')}`,
|
|
789
|
+
available_files: availableFiles,
|
|
790
|
+
}, null, 2) }],
|
|
791
|
+
isError: true,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
// Traverse relationships
|
|
795
|
+
const visitedNodes = new Map();
|
|
796
|
+
const collectedEdges = [];
|
|
797
|
+
let truncated = false;
|
|
798
|
+
const traverse = async (nodeId, currentDepth) => {
|
|
799
|
+
if (visitedNodes.size >= max_nodes) {
|
|
800
|
+
truncated = true;
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (currentDepth > Math.min(depth, 3) || visitedNodes.has(nodeId))
|
|
804
|
+
return;
|
|
805
|
+
const node = await graphStore.getNode(nodeId);
|
|
806
|
+
if (!node)
|
|
807
|
+
return;
|
|
808
|
+
const relPath = node.properties?.relativePath;
|
|
809
|
+
visitedNodes.set(nodeId, {
|
|
810
|
+
id: node.id, type: node.type, name: node.name,
|
|
811
|
+
file: relPath || path.relative(projectPath, node.filePath),
|
|
812
|
+
});
|
|
813
|
+
const edges = await graphStore.getEdges(nodeId, direction);
|
|
814
|
+
for (const edge of edges) {
|
|
815
|
+
if (visitedNodes.size >= max_nodes) {
|
|
816
|
+
truncated = true;
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (relationship_types && relationship_types.length > 0) {
|
|
820
|
+
if (!relationship_types.includes(edge.type))
|
|
821
|
+
continue;
|
|
937
822
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
823
|
+
collectedEdges.push({ from: edge.source, to: edge.target, type: edge.type });
|
|
824
|
+
const nextNodeId = edge.source === nodeId ? edge.target : edge.source;
|
|
825
|
+
await traverse(nextNodeId, currentDepth + 1);
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
for (const startNode of startNodes) {
|
|
829
|
+
if (visitedNodes.size >= max_nodes) {
|
|
830
|
+
truncated = true;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
await traverse(startNode.id, 1);
|
|
834
|
+
}
|
|
835
|
+
const nodes = Array.from(visitedNodes.values());
|
|
836
|
+
const uniqueEdges = collectedEdges.filter((e, i, arr) => arr.findIndex(x => x.from === e.from && x.to === e.to && x.type === e.type) === i);
|
|
837
|
+
const summary = {
|
|
838
|
+
graph_stats: graphStats,
|
|
839
|
+
seed_files: startNodes.map(n => ({
|
|
840
|
+
name: n.name, type: n.type,
|
|
841
|
+
file: n.properties?.relativePath || path.relative(projectPath, n.filePath),
|
|
842
|
+
})),
|
|
843
|
+
traversal: { depth_requested: depth, direction, relationship_filters: relationship_types || 'all', max_nodes },
|
|
844
|
+
results: { seed_nodes: startNodes.length, nodes_found: nodes.length, relationships_found: uniqueEdges.length, truncated },
|
|
845
|
+
nodes: nodes.map(n => ({ name: n.name, type: n.type, file: n.file })),
|
|
846
|
+
relationships: uniqueEdges.map(e => {
|
|
847
|
+
const fromNode = visitedNodes.get(e.from);
|
|
848
|
+
const toNode = visitedNodes.get(e.to);
|
|
849
|
+
return { type: e.type, from: fromNode?.name || e.from, to: toNode?.name || e.to };
|
|
850
|
+
}),
|
|
851
|
+
};
|
|
852
|
+
if (truncated) {
|
|
853
|
+
summary.truncated_warning = {
|
|
854
|
+
message: `Results truncated at ${max_nodes} nodes.`,
|
|
855
|
+
recommendations: [
|
|
856
|
+
relationship_types ? null : 'Add relationship_types filter',
|
|
857
|
+
depth > 1 ? 'Reduce depth to 1' : null,
|
|
858
|
+
`Increase max_nodes (current: ${max_nodes})`,
|
|
859
|
+
].filter(Boolean),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] };
|
|
863
|
+
}
|
|
864
|
+
async handleFindDuplicates(params) {
|
|
865
|
+
const { project, similarity_threshold = 0.80, min_lines = 5 } = params;
|
|
866
|
+
const storageManager = await (0, storage_1.getStorageManager)();
|
|
867
|
+
const projectStore = storageManager.getProjectStore();
|
|
868
|
+
const vectorStore = storageManager.getVectorStore();
|
|
869
|
+
const projects = await projectStore.list();
|
|
870
|
+
const projectRecord = projects.find(p => p.name === project || p.path === project ||
|
|
871
|
+
path.basename(p.path) === project || path.resolve(project) === p.path);
|
|
872
|
+
if (!projectRecord) {
|
|
873
|
+
return {
|
|
874
|
+
content: [{ type: 'text', text: `Project not found: ${project}. Use index({action: "init"}) first.` }],
|
|
875
|
+
isError: true,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
const allDocs = await this.getAllProjectDocuments(vectorStore, projectRecord.id);
|
|
879
|
+
if (allDocs.length === 0) {
|
|
880
|
+
return {
|
|
881
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
882
|
+
project: projectRecord.name,
|
|
883
|
+
summary: { total_chunks_analyzed: 0, exact_duplicates: 0, semantic_duplicates: 0, total_lines_affected: 0, potential_lines_saved: 0 },
|
|
884
|
+
duplicate_groups: [],
|
|
885
|
+
}, null, 2) }],
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
const filteredDocs = allDocs.filter(doc => doc.content.split('\n').length >= min_lines);
|
|
889
|
+
const duplicateGroups = [];
|
|
890
|
+
const processed = new Set();
|
|
891
|
+
const EXACT_THRESHOLD = 0.98;
|
|
892
|
+
for (let i = 0; i < filteredDocs.length && duplicateGroups.length < 50; i++) {
|
|
893
|
+
const doc = filteredDocs[i];
|
|
894
|
+
if (processed.has(doc.id))
|
|
895
|
+
continue;
|
|
896
|
+
const similarDocs = await vectorStore.searchByVector(doc.embedding, projectRecord.id, 20);
|
|
897
|
+
const matches = similarDocs.filter(match => match.document.id !== doc.id && match.score >= similarity_threshold && !processed.has(match.document.id));
|
|
898
|
+
if (matches.length > 0) {
|
|
899
|
+
const maxScore = Math.max(...matches.map(m => m.score));
|
|
900
|
+
const type = maxScore >= EXACT_THRESHOLD ? 'exact' : 'semantic';
|
|
901
|
+
const getLines = (d) => {
|
|
902
|
+
const meta = d.metadata;
|
|
903
|
+
return { startLine: meta?.startLine, endLine: meta?.endLine };
|
|
976
904
|
};
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
// Ends with (for relative paths)
|
|
988
|
-
normalizedNodePath.endsWith(normalizedSeedPath) ||
|
|
989
|
-
normalizedNodePath.endsWith('/' + normalizedSeedPath) ||
|
|
990
|
-
// Contains match (for partial paths)
|
|
991
|
-
normalizedNodePath.includes('/' + normalizedSeedPath) ||
|
|
992
|
-
// Name match (for class/function names)
|
|
993
|
-
n.name === path.basename(normalizedSeedPath).replace(/\.[^.]+$/, ''));
|
|
994
|
-
});
|
|
905
|
+
duplicateGroups.push({
|
|
906
|
+
type, similarity: maxScore,
|
|
907
|
+
chunks: [
|
|
908
|
+
{ id: doc.id, filePath: doc.filePath, content: doc.content.substring(0, 200) + (doc.content.length > 200 ? '...' : ''), ...getLines(doc) },
|
|
909
|
+
...matches.map(m => ({
|
|
910
|
+
id: m.document.id, filePath: m.document.filePath,
|
|
911
|
+
content: m.document.content.substring(0, 200) + (m.document.content.length > 200 ? '...' : ''),
|
|
912
|
+
...getLines(m.document),
|
|
913
|
+
})),
|
|
914
|
+
],
|
|
995
915
|
});
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
916
|
+
processed.add(doc.id);
|
|
917
|
+
matches.forEach(m => processed.add(m.document.id));
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const exactDuplicates = duplicateGroups.filter(g => g.type === 'exact').length;
|
|
921
|
+
const semanticDuplicates = duplicateGroups.filter(g => g.type === 'semantic').length;
|
|
922
|
+
const totalLinesAffected = duplicateGroups.reduce((sum, g) => sum + g.chunks.reduce((chunkSum, c) => chunkSum + (c.endLine && c.startLine ? c.endLine - c.startLine + 1 : c.content.split('\n').length), 0), 0);
|
|
923
|
+
const formattedGroups = duplicateGroups.slice(0, 20).map(group => ({
|
|
924
|
+
type: group.type,
|
|
925
|
+
similarity: `${(group.similarity * 100).toFixed(1)}%`,
|
|
926
|
+
files_affected: new Set(group.chunks.map(c => c.filePath)).size,
|
|
927
|
+
locations: group.chunks.map(c => ({
|
|
928
|
+
file: path.relative(projectRecord.path, c.filePath),
|
|
929
|
+
lines: c.startLine && c.endLine ? `${c.startLine}-${c.endLine}` : 'N/A',
|
|
930
|
+
preview: c.content.substring(0, 100).replace(/\n/g, ' '),
|
|
931
|
+
})),
|
|
932
|
+
}));
|
|
933
|
+
return {
|
|
934
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
935
|
+
project: projectRecord.name,
|
|
936
|
+
summary: {
|
|
937
|
+
total_chunks_analyzed: filteredDocs.length,
|
|
938
|
+
exact_duplicates: exactDuplicates,
|
|
939
|
+
semantic_duplicates: semanticDuplicates,
|
|
940
|
+
total_lines_affected: totalLinesAffected,
|
|
941
|
+
potential_lines_saved: Math.floor(totalLinesAffected * 0.6),
|
|
942
|
+
},
|
|
943
|
+
duplicate_groups: formattedGroups,
|
|
944
|
+
recommendations: exactDuplicates > 0
|
|
945
|
+
? [`Found ${exactDuplicates} exact duplicate groups - prioritize consolidation`]
|
|
946
|
+
: semanticDuplicates > 0
|
|
947
|
+
? [`Found ${semanticDuplicates} semantic duplicates - review for potential abstraction`]
|
|
948
|
+
: ['No significant duplicates found above threshold'],
|
|
949
|
+
}, null, 2) }],
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
async handleFindDeadCode(params) {
|
|
953
|
+
const { project, include_patterns } = params;
|
|
954
|
+
const storageManager = await (0, storage_1.getStorageManager)();
|
|
955
|
+
const projectStore = storageManager.getProjectStore();
|
|
956
|
+
const graphStore = storageManager.getGraphStore();
|
|
957
|
+
const projects = await projectStore.list();
|
|
958
|
+
const projectRecord = projects.find(p => p.name === project || p.path === project ||
|
|
959
|
+
path.basename(p.path) === project || path.resolve(project) === p.path);
|
|
960
|
+
if (!projectRecord) {
|
|
961
|
+
return {
|
|
962
|
+
content: [{ type: 'text', text: `Project not found: ${project}. Use index({action: "init"}) first.` }],
|
|
963
|
+
isError: true,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
const allNodes = await graphStore.findNodes(projectRecord.id);
|
|
967
|
+
if (allNodes.length === 0) {
|
|
968
|
+
return {
|
|
969
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
970
|
+
project: projectRecord.name,
|
|
971
|
+
summary: { total_issues: 0, dead_code_count: 0, anti_patterns_count: 0, coupling_issues_count: 0 },
|
|
972
|
+
dead_code: [], anti_patterns: [], coupling_issues: [],
|
|
973
|
+
note: 'No graph data. Project may need reindexing.',
|
|
974
|
+
}, null, 2) }],
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
const patterns = include_patterns || ['dead_code', 'god_class', 'circular_deps', 'feature_envy', 'coupling'];
|
|
978
|
+
const deadCodeItems = [];
|
|
979
|
+
const antiPatternItems = [];
|
|
980
|
+
const couplingItems = [];
|
|
981
|
+
for (const node of allNodes) {
|
|
982
|
+
const inEdges = await graphStore.getEdges(node.id, 'in');
|
|
983
|
+
const outEdges = await graphStore.getEdges(node.id, 'out');
|
|
984
|
+
if (patterns.includes('dead_code')) {
|
|
985
|
+
const isEntryPoint = node.type === 'file' ||
|
|
986
|
+
node.name.toLowerCase().includes('main') ||
|
|
987
|
+
node.name.toLowerCase().includes('index') ||
|
|
988
|
+
node.name.toLowerCase().includes('app');
|
|
989
|
+
if (!isEntryPoint && (node.type === 'class' || node.type === 'function') && inEdges.length === 0) {
|
|
990
|
+
deadCodeItems.push({
|
|
991
|
+
type: 'Dead Code', name: node.name,
|
|
992
|
+
file: path.relative(projectRecord.path, node.filePath),
|
|
993
|
+
description: `Unused ${node.type}: ${node.name} - no incoming references`,
|
|
994
|
+
confidence: '70%', impact: 'medium',
|
|
995
|
+
recommendation: 'Review if needed. Remove if unused or add to exports.',
|
|
1002
996
|
});
|
|
1003
|
-
return {
|
|
1004
|
-
content: [{
|
|
1005
|
-
type: 'text',
|
|
1006
|
-
text: JSON.stringify({
|
|
1007
|
-
error: `No graph nodes found for: ${seedFilePaths.join(', ')}`,
|
|
1008
|
-
suggestion: query
|
|
1009
|
-
? 'The semantic search found files but they are not in the knowledge graph. Try re-indexing.'
|
|
1010
|
-
: 'The file(s) may not be indexed in the knowledge graph.',
|
|
1011
|
-
available_files: availableFiles,
|
|
1012
|
-
tip: 'Use relative paths like "src/mcp/mcp-server.ts" or a query like "authentication middleware"',
|
|
1013
|
-
}, null, 2),
|
|
1014
|
-
}],
|
|
1015
|
-
isError: true,
|
|
1016
|
-
};
|
|
1017
997
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
return;
|
|
1030
|
-
const node = await graphStore.getNode(nodeId);
|
|
1031
|
-
if (!node)
|
|
1032
|
-
return;
|
|
1033
|
-
const relPath = node.properties?.relativePath;
|
|
1034
|
-
visitedNodes.set(nodeId, {
|
|
1035
|
-
id: node.id,
|
|
1036
|
-
type: node.type,
|
|
1037
|
-
name: node.name,
|
|
1038
|
-
file: relPath || path.relative(projectPath, node.filePath),
|
|
998
|
+
}
|
|
999
|
+
if (patterns.includes('god_class') && node.type === 'class') {
|
|
1000
|
+
const containsEdges = outEdges.filter(e => e.type === 'contains');
|
|
1001
|
+
const dependsOnEdges = outEdges.filter(e => e.type === 'imports' || e.type === 'depends_on');
|
|
1002
|
+
if (containsEdges.length > 15 || dependsOnEdges.length > 10) {
|
|
1003
|
+
antiPatternItems.push({
|
|
1004
|
+
type: 'God Class', name: node.name,
|
|
1005
|
+
file: path.relative(projectRecord.path, node.filePath),
|
|
1006
|
+
description: `${node.name} has ${containsEdges.length} members, ${dependsOnEdges.length} dependencies`,
|
|
1007
|
+
confidence: '80%', impact: 'high',
|
|
1008
|
+
recommendation: 'Break down following Single Responsibility Principle',
|
|
1039
1009
|
});
|
|
1040
|
-
// Get edges based on direction
|
|
1041
|
-
const edges = await graphStore.getEdges(nodeId, direction);
|
|
1042
|
-
for (const edge of edges) {
|
|
1043
|
-
// Stop if we've reached max_nodes limit
|
|
1044
|
-
if (visitedNodes.size >= max_nodes) {
|
|
1045
|
-
truncated = true;
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
// Filter by relationship type if specified
|
|
1049
|
-
if (relationship_types && relationship_types.length > 0) {
|
|
1050
|
-
if (!relationship_types.includes(edge.type))
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
collectedEdges.push({
|
|
1054
|
-
from: edge.source,
|
|
1055
|
-
to: edge.target,
|
|
1056
|
-
type: edge.type,
|
|
1057
|
-
});
|
|
1058
|
-
// Continue traversal
|
|
1059
|
-
const nextNodeId = edge.source === nodeId ? edge.target : edge.source;
|
|
1060
|
-
await traverse(nextNodeId, currentDepth + 1);
|
|
1061
|
-
}
|
|
1062
|
-
};
|
|
1063
|
-
// Traverse from ALL start nodes (multiple seeds)
|
|
1064
|
-
for (const startNode of startNodes) {
|
|
1065
|
-
if (visitedNodes.size >= max_nodes) {
|
|
1066
|
-
truncated = true;
|
|
1067
|
-
break;
|
|
1068
|
-
}
|
|
1069
|
-
await traverse(startNode.id, 1);
|
|
1070
1010
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
})
|
|
1082
|
-
traversal: {
|
|
1083
|
-
depth_requested: depth,
|
|
1084
|
-
direction,
|
|
1085
|
-
relationship_filters: relationship_types || 'all',
|
|
1086
|
-
seed_method: query ? 'semantic_search' : (filepaths ? 'multiple_files' : 'single_file'),
|
|
1087
|
-
max_nodes,
|
|
1088
|
-
},
|
|
1089
|
-
results: {
|
|
1090
|
-
seed_nodes: startNodes.length,
|
|
1091
|
-
nodes_found: nodes.length,
|
|
1092
|
-
relationships_found: uniqueEdges.length,
|
|
1093
|
-
truncated,
|
|
1094
|
-
},
|
|
1095
|
-
nodes: nodes.map(n => ({
|
|
1096
|
-
name: n.name,
|
|
1097
|
-
type: n.type,
|
|
1098
|
-
file: n.file,
|
|
1099
|
-
})),
|
|
1100
|
-
relationships: uniqueEdges.map(e => {
|
|
1101
|
-
const fromNode = visitedNodes.get(e.from);
|
|
1102
|
-
const toNode = visitedNodes.get(e.to);
|
|
1103
|
-
return {
|
|
1104
|
-
type: e.type,
|
|
1105
|
-
from: fromNode?.name || e.from,
|
|
1106
|
-
to: toNode?.name || e.to,
|
|
1107
|
-
};
|
|
1108
|
-
}),
|
|
1109
|
-
};
|
|
1110
|
-
// Add truncation warning and recommendations if results were limited
|
|
1111
|
-
if (truncated) {
|
|
1112
|
-
summary.truncated_warning = {
|
|
1113
|
-
message: `Results truncated at ${max_nodes} nodes.`,
|
|
1114
|
-
recommendations: [
|
|
1115
|
-
relationship_types ? null : 'Add relationship_types filter (e.g., ["imports"])',
|
|
1116
|
-
depth > 1 ? 'Reduce depth to 1' : null,
|
|
1117
|
-
`Increase max_nodes (current: ${max_nodes})`,
|
|
1118
|
-
].filter(Boolean),
|
|
1119
|
-
};
|
|
1011
|
+
}
|
|
1012
|
+
if (patterns.includes('coupling') && (node.type === 'class' || node.type === 'file')) {
|
|
1013
|
+
const dependencies = outEdges.filter(e => e.type === 'imports' || e.type === 'depends_on');
|
|
1014
|
+
if (dependencies.length > 8) {
|
|
1015
|
+
couplingItems.push({
|
|
1016
|
+
type: 'High Coupling', name: node.name,
|
|
1017
|
+
file: path.relative(projectRecord.path, node.filePath),
|
|
1018
|
+
description: `${node.type} ${node.name} has ${dependencies.length} dependencies`,
|
|
1019
|
+
confidence: '75%', impact: 'high',
|
|
1020
|
+
recommendation: 'Reduce via interfaces or dependency injection',
|
|
1021
|
+
});
|
|
1120
1022
|
}
|
|
1121
|
-
return {
|
|
1122
|
-
content: [{
|
|
1123
|
-
type: 'text',
|
|
1124
|
-
text: JSON.stringify(summary, null, 2),
|
|
1125
|
-
}],
|
|
1126
|
-
};
|
|
1127
1023
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1024
|
+
}
|
|
1025
|
+
// Circular dependency detection
|
|
1026
|
+
if (patterns.includes('circular_deps')) {
|
|
1027
|
+
const fileNodes = allNodes.filter(n => n.type === 'file');
|
|
1028
|
+
const importMap = new Map();
|
|
1029
|
+
for (const fileNode of fileNodes) {
|
|
1030
|
+
const imports = await graphStore.getEdges(fileNode.id, 'out');
|
|
1031
|
+
importMap.set(fileNode.id, new Set(imports.filter(e => e.type === 'imports').map(e => e.target)));
|
|
1136
1032
|
}
|
|
1137
|
-
|
|
1033
|
+
for (const [fileId, imports] of importMap.entries()) {
|
|
1034
|
+
for (const targetId of imports) {
|
|
1035
|
+
const targetImports = importMap.get(targetId);
|
|
1036
|
+
if (targetImports?.has(fileId)) {
|
|
1037
|
+
const sourceNode = allNodes.find(n => n.id === fileId);
|
|
1038
|
+
const targetNode = allNodes.find(n => n.id === targetId);
|
|
1039
|
+
if (sourceNode && targetNode) {
|
|
1040
|
+
antiPatternItems.push({
|
|
1041
|
+
type: 'Circular Dependency',
|
|
1042
|
+
name: `${sourceNode.name} <-> ${targetNode.name}`,
|
|
1043
|
+
file: path.relative(projectRecord.path, sourceNode.filePath),
|
|
1044
|
+
description: `Bidirectional import between ${sourceNode.name} and ${targetNode.name}`,
|
|
1045
|
+
confidence: '90%', impact: 'high',
|
|
1046
|
+
recommendation: 'Break cycle using dependency inversion or extract shared code',
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1055
|
+
project: projectRecord.name,
|
|
1056
|
+
graph_stats: {
|
|
1057
|
+
total_nodes: allNodes.length,
|
|
1058
|
+
files: allNodes.filter(n => n.type === 'file').length,
|
|
1059
|
+
classes: allNodes.filter(n => n.type === 'class').length,
|
|
1060
|
+
functions: allNodes.filter(n => n.type === 'function').length,
|
|
1061
|
+
},
|
|
1062
|
+
summary: {
|
|
1063
|
+
total_issues: deadCodeItems.length + antiPatternItems.length + couplingItems.length,
|
|
1064
|
+
dead_code_count: deadCodeItems.length,
|
|
1065
|
+
anti_patterns_count: antiPatternItems.length,
|
|
1066
|
+
coupling_issues_count: couplingItems.length,
|
|
1067
|
+
},
|
|
1068
|
+
dead_code: deadCodeItems.slice(0, 20),
|
|
1069
|
+
anti_patterns: antiPatternItems.slice(0, 10),
|
|
1070
|
+
coupling_issues: couplingItems.slice(0, 10),
|
|
1071
|
+
}, null, 2) }],
|
|
1072
|
+
};
|
|
1138
1073
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1074
|
+
async handleStandards(params) {
|
|
1075
|
+
const { project, category = 'all' } = params;
|
|
1076
|
+
const storageManager = await (0, storage_1.getStorageManager)();
|
|
1077
|
+
const projectStore = storageManager.getProjectStore();
|
|
1078
|
+
const vectorStore = storageManager.getVectorStore();
|
|
1079
|
+
const projects = await projectStore.list();
|
|
1080
|
+
const found = projects.find(p => p.name === project || p.path === project || path.basename(p.path) === project);
|
|
1081
|
+
if (!found) {
|
|
1082
|
+
return {
|
|
1083
|
+
content: [{ type: 'text', text: `Project not found: ${project}. Use index({action: "status"}) to list projects.` }],
|
|
1084
|
+
isError: true,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
const standardsPath = path.join(found.path, '.codeseeker', 'coding-standards.json');
|
|
1088
|
+
let standardsContent;
|
|
1089
|
+
try {
|
|
1090
|
+
standardsContent = fs.readFileSync(standardsPath, 'utf-8');
|
|
1091
|
+
}
|
|
1092
|
+
catch {
|
|
1093
|
+
const generator = new coding_standards_generator_1.CodingStandardsGenerator(vectorStore);
|
|
1094
|
+
await generator.generateStandards(found.id, found.path);
|
|
1149
1095
|
try {
|
|
1150
|
-
|
|
1151
|
-
const projectStore = storageManager.getProjectStore();
|
|
1152
|
-
const vectorStore = storageManager.getVectorStore();
|
|
1153
|
-
const projects = await projectStore.list();
|
|
1154
|
-
if (projects.length === 0) {
|
|
1155
|
-
return {
|
|
1156
|
-
content: [{
|
|
1157
|
-
type: 'text',
|
|
1158
|
-
text: 'No projects indexed. Use index to add a project.',
|
|
1159
|
-
}],
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
// Get file and chunk counts for each project, plus indexing status
|
|
1163
|
-
const projectsWithCounts = await Promise.all(projects.map(async (p) => {
|
|
1164
|
-
const fileCount = await vectorStore.countFiles(p.id);
|
|
1165
|
-
const chunkCount = await vectorStore.count(p.id);
|
|
1166
|
-
// Check for background indexing status
|
|
1167
|
-
const indexingStatus = this._getIndexingStatusForProject(p.id);
|
|
1168
|
-
const projectInfo = {
|
|
1169
|
-
name: p.name,
|
|
1170
|
-
path: p.path,
|
|
1171
|
-
files: fileCount,
|
|
1172
|
-
chunks: chunkCount,
|
|
1173
|
-
last_indexed: p.updatedAt.toISOString(),
|
|
1174
|
-
};
|
|
1175
|
-
// Add indexing status if job exists
|
|
1176
|
-
if (indexingStatus) {
|
|
1177
|
-
Object.assign(projectInfo, indexingStatus);
|
|
1178
|
-
}
|
|
1179
|
-
return projectInfo;
|
|
1180
|
-
}));
|
|
1181
|
-
return {
|
|
1182
|
-
content: [{
|
|
1183
|
-
type: 'text',
|
|
1184
|
-
text: JSON.stringify({
|
|
1185
|
-
storage_mode: storageManager.getMode(),
|
|
1186
|
-
total_projects: projects.length,
|
|
1187
|
-
projects: projectsWithCounts,
|
|
1188
|
-
}, null, 2),
|
|
1189
|
-
}],
|
|
1190
|
-
};
|
|
1096
|
+
standardsContent = fs.readFileSync(standardsPath, 'utf-8');
|
|
1191
1097
|
}
|
|
1192
|
-
catch
|
|
1098
|
+
catch {
|
|
1193
1099
|
return {
|
|
1194
|
-
content: [{
|
|
1195
|
-
type: 'text',
|
|
1196
|
-
text: this.formatErrorMessage('List projects', error instanceof Error ? error : String(error)),
|
|
1197
|
-
}],
|
|
1100
|
+
content: [{ type: 'text', text: 'No coding standards detected. Index the project first.' }],
|
|
1198
1101
|
isError: true,
|
|
1199
1102
|
};
|
|
1200
1103
|
}
|
|
1201
|
-
}
|
|
1104
|
+
}
|
|
1105
|
+
const standards = JSON.parse(standardsContent);
|
|
1106
|
+
let result = standards;
|
|
1107
|
+
if (category !== 'all') {
|
|
1108
|
+
result = { ...standards, standards: { [category]: standards.standards[category] || {} } };
|
|
1109
|
+
}
|
|
1110
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1202
1111
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1112
|
+
// ============================================================
|
|
1113
|
+
// TOOL 3: index
|
|
1114
|
+
// Combines: index (init), sync, projects (status), install_parsers, exclude
|
|
1115
|
+
// ============================================================
|
|
1207
1116
|
registerIndexTool() {
|
|
1208
1117
|
this.server.registerTool('index', {
|
|
1209
|
-
description: 'Index
|
|
1210
|
-
'
|
|
1211
|
-
'Run once per project, then use sync for incremental updates. ' +
|
|
1212
|
-
'Example: index({path: "/home/user/my-app"}) indexes all files in my-app.' +
|
|
1213
|
-
'\n\nNOTE: Indexing runs in BACKGROUND to prevent timeouts. Use projects() to check indexing status.',
|
|
1118
|
+
description: 'Index management. Actions: "init" (index project), "sync" (update changed files), ' +
|
|
1119
|
+
'"status" (list projects), "parsers" (install language parsers), "exclude" (manage exclusions).',
|
|
1214
1120
|
inputSchema: {
|
|
1215
|
-
|
|
1216
|
-
|
|
1121
|
+
action: zod_1.z.enum(['init', 'sync', 'status', 'parsers', 'exclude']).describe('Action'),
|
|
1122
|
+
path: zod_1.z.string().optional().describe('Project directory (for init)'),
|
|
1123
|
+
project: zod_1.z.string().optional().describe('Project name or path'),
|
|
1124
|
+
name: zod_1.z.string().optional().describe('Project name (for init)'),
|
|
1125
|
+
// sync params
|
|
1126
|
+
changes: zod_1.z.array(zod_1.z.object({
|
|
1127
|
+
type: zod_1.z.enum(['created', 'modified', 'deleted']),
|
|
1128
|
+
path: zod_1.z.string(),
|
|
1129
|
+
})).optional().describe('File changes for sync'),
|
|
1130
|
+
full_reindex: zod_1.z.boolean().optional().default(false).describe('Full reindex'),
|
|
1131
|
+
// parsers params
|
|
1132
|
+
languages: zod_1.z.array(zod_1.z.string()).optional().describe('Languages to install parsers for'),
|
|
1133
|
+
list_available: zod_1.z.boolean().optional().default(false).describe('List available parsers'),
|
|
1134
|
+
// exclude params
|
|
1135
|
+
exclude_action: zod_1.z.enum(['exclude', 'include', 'list']).optional().describe('Exclusion sub-action'),
|
|
1136
|
+
paths: zod_1.z.array(zod_1.z.string()).optional().describe('Paths/patterns to exclude/include'),
|
|
1137
|
+
reason: zod_1.z.string().optional().describe('Exclusion reason'),
|
|
1217
1138
|
},
|
|
1218
|
-
}, async (
|
|
1139
|
+
}, async (params) => {
|
|
1219
1140
|
try {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
isError: true,
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
if (!fs.existsSync(absolutePath)) {
|
|
1237
|
-
return {
|
|
1238
|
-
content: [{
|
|
1239
|
-
type: 'text',
|
|
1240
|
-
text: `Directory not found: ${absolutePath}`,
|
|
1241
|
-
}],
|
|
1242
|
-
isError: true,
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
if (!fs.statSync(absolutePath).isDirectory()) {
|
|
1246
|
-
return {
|
|
1247
|
-
content: [{
|
|
1248
|
-
type: 'text',
|
|
1249
|
-
text: `Not a directory: ${absolutePath}`,
|
|
1250
|
-
}],
|
|
1251
|
-
isError: true,
|
|
1252
|
-
};
|
|
1141
|
+
switch (params.action) {
|
|
1142
|
+
case 'init':
|
|
1143
|
+
return await this.handleIndexInit(params);
|
|
1144
|
+
case 'sync':
|
|
1145
|
+
return await this.handleSync(params);
|
|
1146
|
+
case 'status':
|
|
1147
|
+
return await this.handleProjects();
|
|
1148
|
+
case 'parsers':
|
|
1149
|
+
return await this.handleInstallParsers(params);
|
|
1150
|
+
case 'exclude':
|
|
1151
|
+
return await this.handleExclude(params);
|
|
1152
|
+
default:
|
|
1153
|
+
return { content: [{ type: 'text', text: `Unknown action: ${params.action}` }], isError: true };
|
|
1253
1154
|
}
|
|
1254
|
-
const projectName = name || path.basename(absolutePath);
|
|
1255
|
-
const projectId = this.generateProjectId(absolutePath);
|
|
1256
|
-
// Mutex: prevent concurrent indexing of same project (race condition protection)
|
|
1257
|
-
if (this.indexingMutex.has(projectId)) {
|
|
1258
|
-
return {
|
|
1259
|
-
content: [{
|
|
1260
|
-
type: 'text',
|
|
1261
|
-
text: JSON.stringify({
|
|
1262
|
-
status: 'already_indexing',
|
|
1263
|
-
project_name: projectName,
|
|
1264
|
-
project_path: absolutePath,
|
|
1265
|
-
message: 'Indexing request already being processed. Please wait.',
|
|
1266
|
-
}, null, 2),
|
|
1267
|
-
}],
|
|
1268
|
-
};
|
|
1269
|
-
}
|
|
1270
|
-
// Check if already indexing (from job status)
|
|
1271
|
-
const existingJob = this.getIndexingStatus(projectId);
|
|
1272
|
-
if (existingJob?.status === 'running') {
|
|
1273
|
-
return {
|
|
1274
|
-
content: [{
|
|
1275
|
-
type: 'text',
|
|
1276
|
-
text: JSON.stringify({
|
|
1277
|
-
status: 'already_indexing',
|
|
1278
|
-
project_name: projectName,
|
|
1279
|
-
project_path: absolutePath,
|
|
1280
|
-
progress: existingJob.progress,
|
|
1281
|
-
message: 'Indexing already in progress. Use projects() to check status.',
|
|
1282
|
-
}, null, 2),
|
|
1283
|
-
}],
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
// Acquire mutex before starting
|
|
1287
|
-
this.indexingMutex.add(projectId);
|
|
1288
|
-
// Get storage and create project entry
|
|
1289
|
-
const storageManager = await (0, storage_1.getStorageManager)();
|
|
1290
|
-
const projectStore = storageManager.getProjectStore();
|
|
1291
|
-
// Create or update project
|
|
1292
|
-
await projectStore.upsert({
|
|
1293
|
-
id: projectId,
|
|
1294
|
-
name: projectName,
|
|
1295
|
-
path: absolutePath,
|
|
1296
|
-
metadata: { indexedAt: new Date().toISOString(), indexing: true },
|
|
1297
|
-
});
|
|
1298
|
-
// Delete coding standards file (will be regenerated)
|
|
1299
|
-
const codingStandardsPath = path.join(absolutePath, '.codeseeker', 'coding-standards.json');
|
|
1300
|
-
if (fs.existsSync(codingStandardsPath)) {
|
|
1301
|
-
try {
|
|
1302
|
-
fs.unlinkSync(codingStandardsPath);
|
|
1303
|
-
}
|
|
1304
|
-
catch { /* ignore */ }
|
|
1305
|
-
}
|
|
1306
|
-
// Start background indexing (returns immediately)
|
|
1307
|
-
this.startBackgroundIndexing(projectId, projectName, absolutePath, true);
|
|
1308
|
-
// Return immediately with "started" status
|
|
1309
|
-
return {
|
|
1310
|
-
content: [{
|
|
1311
|
-
type: 'text',
|
|
1312
|
-
text: JSON.stringify({
|
|
1313
|
-
status: 'indexing_started',
|
|
1314
|
-
project_name: projectName,
|
|
1315
|
-
project_path: absolutePath,
|
|
1316
|
-
message: 'Indexing started in background. Search will work with partial results. Use projects() to check progress.',
|
|
1317
|
-
}, null, 2),
|
|
1318
|
-
}],
|
|
1319
|
-
};
|
|
1320
|
-
// OLD SYNCHRONOUS CODE REMOVED - was causing MCP timeouts
|
|
1321
|
-
// Now handled by startBackgroundIndexing()
|
|
1322
1155
|
}
|
|
1323
1156
|
catch (error) {
|
|
1324
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1325
1157
|
return {
|
|
1326
|
-
content: [{
|
|
1327
|
-
type: 'text',
|
|
1328
|
-
text: JSON.stringify({ error: message }, null, 2),
|
|
1329
|
-
}],
|
|
1158
|
+
content: [{ type: 'text', text: this.formatErrorMessage('Index', error instanceof Error ? error : String(error), { projectPath: params.path || params.project }) }],
|
|
1330
1159
|
isError: true,
|
|
1331
1160
|
};
|
|
1332
1161
|
}
|
|
1333
1162
|
});
|
|
1334
1163
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
const
|
|
1345
|
-
if (
|
|
1346
|
-
return
|
|
1164
|
+
async handleIndexInit(params) {
|
|
1165
|
+
const projectPath = params.path;
|
|
1166
|
+
if (!projectPath) {
|
|
1167
|
+
return {
|
|
1168
|
+
content: [{ type: 'text', text: 'path parameter required for init action.' }],
|
|
1169
|
+
isError: true,
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
const absolutePath = path.isAbsolute(projectPath) ? projectPath : path.resolve(projectPath);
|
|
1173
|
+
const pathError = this.validateProjectPath(absolutePath);
|
|
1174
|
+
if (pathError) {
|
|
1175
|
+
return { content: [{ type: 'text', text: pathError }], isError: true };
|
|
1176
|
+
}
|
|
1177
|
+
if (!fs.existsSync(absolutePath)) {
|
|
1178
|
+
return { content: [{ type: 'text', text: `Directory not found: ${absolutePath}` }], isError: true };
|
|
1179
|
+
}
|
|
1180
|
+
if (!fs.statSync(absolutePath).isDirectory()) {
|
|
1181
|
+
return { content: [{ type: 'text', text: `Not a directory: ${absolutePath}` }], isError: true };
|
|
1182
|
+
}
|
|
1183
|
+
const projectName = params.name || path.basename(absolutePath);
|
|
1184
|
+
const projectId = this.generateProjectId(absolutePath);
|
|
1185
|
+
if (this.indexingMutex.has(projectId)) {
|
|
1186
|
+
return {
|
|
1187
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1188
|
+
status: 'already_indexing', project_name: projectName,
|
|
1189
|
+
message: 'Indexing request already being processed.',
|
|
1190
|
+
}, null, 2) }],
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
const existingJob = this.getIndexingStatus(projectId);
|
|
1194
|
+
if (existingJob?.status === 'running') {
|
|
1195
|
+
return {
|
|
1196
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1197
|
+
status: 'already_indexing', project_name: projectName,
|
|
1198
|
+
progress: existingJob.progress,
|
|
1199
|
+
message: 'Indexing in progress. Check with index({action: "status"}).',
|
|
1200
|
+
}, null, 2) }],
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
this.indexingMutex.add(projectId);
|
|
1204
|
+
const storageManager = await (0, storage_1.getStorageManager)();
|
|
1205
|
+
const projectStore = storageManager.getProjectStore();
|
|
1206
|
+
await projectStore.upsert({
|
|
1207
|
+
id: projectId, name: projectName, path: absolutePath,
|
|
1208
|
+
metadata: { indexedAt: new Date().toISOString(), indexing: true },
|
|
1209
|
+
});
|
|
1210
|
+
const codingStandardsPath = path.join(absolutePath, '.codeseeker', 'coding-standards.json');
|
|
1211
|
+
if (fs.existsSync(codingStandardsPath)) {
|
|
1212
|
+
try {
|
|
1213
|
+
fs.unlinkSync(codingStandardsPath);
|
|
1214
|
+
}
|
|
1215
|
+
catch { /* ignore */ }
|
|
1216
|
+
}
|
|
1217
|
+
this.startBackgroundIndexing(projectId, projectName, absolutePath, true);
|
|
1347
1218
|
return {
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
indexing_started: job.startedAt.toISOString(),
|
|
1353
|
-
indexing_completed: job.completedAt?.toISOString(),
|
|
1219
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1220
|
+
status: 'indexing_started', project_name: projectName, project_path: absolutePath,
|
|
1221
|
+
message: 'Indexing started in background. Use index({action: "status"}) to check progress.',
|
|
1222
|
+
}, null, 2) }],
|
|
1354
1223
|
};
|
|
1355
1224
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
})).optional().describe('Array of file changes (not needed if full_reindex is true)'),
|
|
1372
|
-
full_reindex: zod_1.z.boolean().optional().default(false)
|
|
1373
|
-
.describe('Trigger a complete re-index of the project. Use after git pull, branch switch, or major changes.'),
|
|
1374
|
-
},
|
|
1375
|
-
}, async ({ project, changes, full_reindex = false }) => {
|
|
1376
|
-
try {
|
|
1377
|
-
const startTime = Date.now();
|
|
1378
|
-
// Resolve project
|
|
1379
|
-
const storageManager = await (0, storage_1.getStorageManager)();
|
|
1380
|
-
const projectStore = storageManager.getProjectStore();
|
|
1381
|
-
const vectorStore = storageManager.getVectorStore();
|
|
1382
|
-
const graphStore = storageManager.getGraphStore();
|
|
1383
|
-
const projects = await projectStore.list();
|
|
1384
|
-
// Auto-detect project if not provided
|
|
1385
|
-
let found;
|
|
1386
|
-
if (project) {
|
|
1387
|
-
found = projects.find(p => p.name === project ||
|
|
1388
|
-
p.path === project ||
|
|
1389
|
-
path.basename(p.path) === project ||
|
|
1390
|
-
path.resolve(project) === p.path);
|
|
1391
|
-
}
|
|
1392
|
-
else {
|
|
1393
|
-
// Try to find project from file paths in changes
|
|
1394
|
-
if (changes && changes.length > 0) {
|
|
1395
|
-
const firstPath = changes[0].path;
|
|
1396
|
-
if (path.isAbsolute(firstPath)) {
|
|
1397
|
-
found = projects.find(p => firstPath.startsWith(p.path));
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
// If still not found, use single project if only one exists
|
|
1401
|
-
if (!found && projects.length === 1) {
|
|
1402
|
-
found = projects[0];
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
if (!found) {
|
|
1406
|
-
return {
|
|
1407
|
-
content: [{
|
|
1408
|
-
type: 'text',
|
|
1409
|
-
text: project
|
|
1410
|
-
? `Project not found: ${project}. Use projects to see available projects.`
|
|
1411
|
-
: `Could not auto-detect project. Specify project name or use absolute paths in changes. Available: ${projects.map(p => p.name).join(', ')}`,
|
|
1412
|
-
}],
|
|
1413
|
-
isError: true,
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
|
-
// Full reindex mode - NOW RUNS IN BACKGROUND
|
|
1417
|
-
if (full_reindex) {
|
|
1418
|
-
// Check if already indexing
|
|
1419
|
-
const existingJob = this.getIndexingStatus(found.id);
|
|
1420
|
-
if (existingJob?.status === 'running') {
|
|
1421
|
-
return {
|
|
1422
|
-
content: [{
|
|
1423
|
-
type: 'text',
|
|
1424
|
-
text: JSON.stringify({
|
|
1425
|
-
status: 'already_indexing',
|
|
1426
|
-
project: found.name,
|
|
1427
|
-
progress: existingJob.progress,
|
|
1428
|
-
message: 'Full reindex already in progress. Use projects() to check status.',
|
|
1429
|
-
}, null, 2),
|
|
1430
|
-
}],
|
|
1431
|
-
};
|
|
1432
|
-
}
|
|
1433
|
-
// Delete coding standards file (will be regenerated)
|
|
1434
|
-
const codingStandardsPath = path.join(found.path, '.codeseeker', 'coding-standards.json');
|
|
1435
|
-
if (fs.existsSync(codingStandardsPath)) {
|
|
1436
|
-
try {
|
|
1437
|
-
fs.unlinkSync(codingStandardsPath);
|
|
1438
|
-
}
|
|
1439
|
-
catch { /* ignore */ }
|
|
1440
|
-
}
|
|
1441
|
-
// Start background indexing (returns immediately)
|
|
1442
|
-
this.startBackgroundIndexing(found.id, found.name, found.path, true);
|
|
1443
|
-
// Return immediately with "started" status
|
|
1444
|
-
return {
|
|
1445
|
-
content: [{
|
|
1446
|
-
type: 'text',
|
|
1447
|
-
text: JSON.stringify({
|
|
1448
|
-
status: 'reindex_started',
|
|
1449
|
-
mode: 'full_reindex',
|
|
1450
|
-
project: found.name,
|
|
1451
|
-
message: 'Full reindex started in background. Search will work with partial results. Use projects() to check progress.',
|
|
1452
|
-
}, null, 2),
|
|
1453
|
-
}],
|
|
1454
|
-
};
|
|
1455
|
-
}
|
|
1456
|
-
// Incremental update mode - use IndexingService
|
|
1457
|
-
if (!changes || changes.length === 0) {
|
|
1458
|
-
return {
|
|
1459
|
-
content: [{
|
|
1460
|
-
type: 'text',
|
|
1461
|
-
text: 'No changes provided. Either pass file changes or set full_reindex: true.',
|
|
1462
|
-
}],
|
|
1463
|
-
isError: true,
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
let chunksCreated = 0;
|
|
1467
|
-
let chunksDeleted = 0;
|
|
1468
|
-
let filesProcessed = 0;
|
|
1469
|
-
let filesSkipped = 0;
|
|
1470
|
-
const errors = [];
|
|
1471
|
-
for (const change of changes) {
|
|
1472
|
-
const relativePath = path.isAbsolute(change.path)
|
|
1473
|
-
? path.relative(found.path, change.path)
|
|
1474
|
-
: change.path;
|
|
1475
|
-
try {
|
|
1476
|
-
if (change.type === 'deleted') {
|
|
1477
|
-
// Remove chunks for deleted file using IndexingService
|
|
1478
|
-
const result = await this.indexingService.deleteFile(found.id, relativePath);
|
|
1479
|
-
if (result.success) {
|
|
1480
|
-
chunksDeleted += result.deleted;
|
|
1481
|
-
filesProcessed++;
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
else {
|
|
1485
|
-
// created or modified: re-index the file using IndexingService
|
|
1486
|
-
// Uses two-stage change detection: mtime (~0.1ms) then hash (~1-5ms)
|
|
1487
|
-
const result = await this.indexingService.indexSingleFile(found.path, relativePath, found.id);
|
|
1488
|
-
if (result.success) {
|
|
1489
|
-
if (result.skipped) {
|
|
1490
|
-
filesSkipped++;
|
|
1491
|
-
}
|
|
1492
|
-
else {
|
|
1493
|
-
chunksCreated += result.chunksCreated;
|
|
1494
|
-
filesProcessed++;
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
catch (error) {
|
|
1500
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1501
|
-
errors.push(`${change.path}: ${msg}`);
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
// Update coding standards if pattern-related files changed
|
|
1505
|
-
try {
|
|
1506
|
-
const changedPaths = changes.map(c => c.path);
|
|
1507
|
-
const generator = new coding_standards_generator_1.CodingStandardsGenerator(vectorStore);
|
|
1508
|
-
await generator.updateStandards(found.id, found.path, changedPaths);
|
|
1509
|
-
}
|
|
1510
|
-
catch (error) {
|
|
1511
|
-
// Don't fail the whole operation if standards update fails
|
|
1512
|
-
console.error('Failed to update coding standards:', error);
|
|
1513
|
-
}
|
|
1514
|
-
// Invalidate query cache for this project (files changed)
|
|
1515
|
-
let cacheInvalidated = 0;
|
|
1516
|
-
try {
|
|
1517
|
-
cacheInvalidated = await this.queryCache.invalidateProject(found.id);
|
|
1518
|
-
}
|
|
1519
|
-
catch (error) {
|
|
1520
|
-
// Don't fail if cache invalidation fails
|
|
1521
|
-
console.error('Failed to invalidate query cache:', error);
|
|
1522
|
-
}
|
|
1523
|
-
const duration = Date.now() - startTime;
|
|
1524
|
-
return {
|
|
1525
|
-
content: [{
|
|
1526
|
-
type: 'text',
|
|
1527
|
-
text: JSON.stringify({
|
|
1528
|
-
success: errors.length === 0,
|
|
1529
|
-
mode: 'incremental',
|
|
1530
|
-
project: found.name,
|
|
1531
|
-
changes_processed: changes.length,
|
|
1532
|
-
files_reindexed: filesProcessed,
|
|
1533
|
-
files_skipped: filesSkipped > 0 ? filesSkipped : undefined,
|
|
1534
|
-
chunks_created: chunksCreated,
|
|
1535
|
-
chunks_deleted: chunksDeleted,
|
|
1536
|
-
cache_invalidated: cacheInvalidated > 0 ? cacheInvalidated : undefined,
|
|
1537
|
-
duration_ms: duration,
|
|
1538
|
-
note: filesSkipped > 0 ? `${filesSkipped} file(s) unchanged (skipped via mtime/hash check)` : undefined,
|
|
1539
|
-
errors: errors.length > 0 ? errors.slice(0, 5) : undefined,
|
|
1540
|
-
}, null, 2),
|
|
1541
|
-
}],
|
|
1542
|
-
};
|
|
1543
|
-
}
|
|
1544
|
-
catch (error) {
|
|
1545
|
-
return {
|
|
1546
|
-
content: [{
|
|
1547
|
-
type: 'text',
|
|
1548
|
-
text: this.formatErrorMessage('Process file changes', error instanceof Error ? error : String(error), { projectPath: project }),
|
|
1549
|
-
}],
|
|
1550
|
-
isError: true,
|
|
1551
|
-
};
|
|
1225
|
+
async handleSync(params) {
|
|
1226
|
+
const { project, changes, full_reindex = false } = params;
|
|
1227
|
+
const startTime = Date.now();
|
|
1228
|
+
const storageManager = await (0, storage_1.getStorageManager)();
|
|
1229
|
+
const projectStore = storageManager.getProjectStore();
|
|
1230
|
+
const vectorStore = storageManager.getVectorStore();
|
|
1231
|
+
const projects = await projectStore.list();
|
|
1232
|
+
let found;
|
|
1233
|
+
if (project) {
|
|
1234
|
+
found = projects.find(p => p.name === project || p.path === project ||
|
|
1235
|
+
path.basename(p.path) === project || path.resolve(project) === p.path);
|
|
1236
|
+
}
|
|
1237
|
+
else {
|
|
1238
|
+
if (changes && changes.length > 0 && path.isAbsolute(changes[0].path)) {
|
|
1239
|
+
found = projects.find(p => changes[0].path.startsWith(p.path));
|
|
1552
1240
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
this.server.registerTool('standards', {
|
|
1556
|
-
description: 'Get auto-detected coding patterns and standards for a project. ' +
|
|
1557
|
-
'Returns validation patterns, error handling patterns, logging patterns, and testing patterns ' +
|
|
1558
|
-
'discovered from the codebase. Use this to write code that follows project conventions. ' +
|
|
1559
|
-
'Example: standards({project: "my-app", category: "validation"})',
|
|
1560
|
-
inputSchema: {
|
|
1561
|
-
project: zod_1.z.string().describe('Project name or path'),
|
|
1562
|
-
category: zod_1.z.enum(['validation', 'error-handling', 'logging', 'testing', 'all']).optional().default('all')
|
|
1563
|
-
.describe('Category of standards to retrieve (default: all)'),
|
|
1564
|
-
},
|
|
1565
|
-
}, async ({ project, category = 'all' }) => {
|
|
1566
|
-
try {
|
|
1567
|
-
// Resolve project
|
|
1568
|
-
const storageManager = await (0, storage_1.getStorageManager)();
|
|
1569
|
-
const projectStore = storageManager.getProjectStore();
|
|
1570
|
-
const vectorStore = storageManager.getVectorStore();
|
|
1571
|
-
const projects = await projectStore.list();
|
|
1572
|
-
const found = projects.find(p => p.name === project ||
|
|
1573
|
-
p.path === project ||
|
|
1574
|
-
path.basename(p.path) === project);
|
|
1575
|
-
if (!found) {
|
|
1576
|
-
return {
|
|
1577
|
-
content: [{
|
|
1578
|
-
type: 'text',
|
|
1579
|
-
text: `Project not found: ${project}. Use projects to see available projects.`,
|
|
1580
|
-
}],
|
|
1581
|
-
isError: true,
|
|
1582
|
-
};
|
|
1583
|
-
}
|
|
1584
|
-
// Try to load standards file
|
|
1585
|
-
const standardsPath = path.join(found.path, '.codeseeker', 'coding-standards.json');
|
|
1586
|
-
let standardsContent;
|
|
1587
|
-
try {
|
|
1588
|
-
standardsContent = fs.readFileSync(standardsPath, 'utf-8');
|
|
1589
|
-
}
|
|
1590
|
-
catch (error) {
|
|
1591
|
-
// Standards file doesn't exist - generate it now
|
|
1592
|
-
const generator = new coding_standards_generator_1.CodingStandardsGenerator(vectorStore);
|
|
1593
|
-
await generator.generateStandards(found.id, found.path);
|
|
1594
|
-
// Try reading again
|
|
1595
|
-
try {
|
|
1596
|
-
standardsContent = fs.readFileSync(standardsPath, 'utf-8');
|
|
1597
|
-
}
|
|
1598
|
-
catch {
|
|
1599
|
-
return {
|
|
1600
|
-
content: [{
|
|
1601
|
-
type: 'text',
|
|
1602
|
-
text: 'No coding standards detected yet. The project may need to be indexed first using index.',
|
|
1603
|
-
}],
|
|
1604
|
-
isError: true,
|
|
1605
|
-
};
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
const standards = JSON.parse(standardsContent);
|
|
1609
|
-
// Filter by category if requested
|
|
1610
|
-
let result = standards;
|
|
1611
|
-
if (category !== 'all') {
|
|
1612
|
-
result = {
|
|
1613
|
-
...standards,
|
|
1614
|
-
standards: {
|
|
1615
|
-
[category]: standards.standards[category] || {}
|
|
1616
|
-
}
|
|
1617
|
-
};
|
|
1618
|
-
}
|
|
1619
|
-
return {
|
|
1620
|
-
content: [{
|
|
1621
|
-
type: 'text',
|
|
1622
|
-
text: JSON.stringify(result, null, 2),
|
|
1623
|
-
}],
|
|
1624
|
-
};
|
|
1241
|
+
if (!found && projects.length === 1) {
|
|
1242
|
+
found = projects[0];
|
|
1625
1243
|
}
|
|
1626
|
-
|
|
1244
|
+
}
|
|
1245
|
+
if (!found) {
|
|
1246
|
+
return {
|
|
1247
|
+
content: [{ type: 'text', text: project
|
|
1248
|
+
? `Project not found: ${project}. Use index({action: "status"}) to see projects.`
|
|
1249
|
+
: `Could not auto-detect project. Specify project. Available: ${projects.map(p => p.name).join(', ')}` }],
|
|
1250
|
+
isError: true,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
if (full_reindex) {
|
|
1254
|
+
const existingJob = this.getIndexingStatus(found.id);
|
|
1255
|
+
if (existingJob?.status === 'running') {
|
|
1627
1256
|
return {
|
|
1628
|
-
content: [{
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
isError: true,
|
|
1257
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1258
|
+
status: 'already_indexing', project: found.name,
|
|
1259
|
+
progress: existingJob.progress,
|
|
1260
|
+
}, null, 2) }],
|
|
1633
1261
|
};
|
|
1634
1262
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
*/
|
|
1640
|
-
registerInstallParsersTool() {
|
|
1641
|
-
this.server.registerTool('install_parsers', {
|
|
1642
|
-
description: 'Analyze project languages and install Tree-sitter parsers for better code understanding. ' +
|
|
1643
|
-
'Detects which programming languages are used in a project and installs enhanced parsers. ' +
|
|
1644
|
-
'Enhanced parsers provide better AST extraction for imports, classes, functions, and relationships. ' +
|
|
1645
|
-
'Example: install_parsers({project: "/path/to/project"}) to auto-detect and install. ' +
|
|
1646
|
-
'Example: install_parsers({languages: ["python", "java"]}) to install specific parsers.',
|
|
1647
|
-
inputSchema: {
|
|
1648
|
-
project: zod_1.z.string().optional().describe('Project path to analyze for languages (auto-detects needed parsers)'),
|
|
1649
|
-
languages: zod_1.z.array(zod_1.z.string()).optional().describe('Specific languages to install parsers for (e.g., ["python", "java", "csharp"])'),
|
|
1650
|
-
list_available: zod_1.z.boolean().optional().default(false).describe('List all available language parsers and their status'),
|
|
1651
|
-
},
|
|
1652
|
-
}, async ({ project, languages, list_available = false }) => {
|
|
1653
|
-
try {
|
|
1654
|
-
// List mode - show available parsers
|
|
1655
|
-
if (list_available) {
|
|
1656
|
-
const parsers = await this.languageSupportService.checkInstalledParsers();
|
|
1657
|
-
const installed = parsers.filter(p => p.installed);
|
|
1658
|
-
const available = parsers.filter(p => !p.installed);
|
|
1659
|
-
return {
|
|
1660
|
-
content: [{
|
|
1661
|
-
type: 'text',
|
|
1662
|
-
text: JSON.stringify({
|
|
1663
|
-
installed_parsers: installed.map(p => ({
|
|
1664
|
-
language: p.language,
|
|
1665
|
-
extensions: p.extensions,
|
|
1666
|
-
quality: p.quality,
|
|
1667
|
-
description: p.description,
|
|
1668
|
-
})),
|
|
1669
|
-
available_parsers: available.map(p => ({
|
|
1670
|
-
language: p.language,
|
|
1671
|
-
extensions: p.extensions,
|
|
1672
|
-
npm_package: p.npmPackage,
|
|
1673
|
-
quality: p.quality,
|
|
1674
|
-
description: p.description,
|
|
1675
|
-
})),
|
|
1676
|
-
install_command: available.length > 0
|
|
1677
|
-
? `Use install_parsers({languages: [${available.slice(0, 3).map(p => `"${p.language.toLowerCase()}"`).join(', ')}]})`
|
|
1678
|
-
: 'All parsers are already installed!',
|
|
1679
|
-
}, null, 2),
|
|
1680
|
-
}],
|
|
1681
|
-
};
|
|
1682
|
-
}
|
|
1683
|
-
// Install specific languages
|
|
1684
|
-
if (languages && languages.length > 0) {
|
|
1685
|
-
const result = await this.languageSupportService.installLanguageParsers(languages);
|
|
1686
|
-
return {
|
|
1687
|
-
content: [{
|
|
1688
|
-
type: 'text',
|
|
1689
|
-
text: JSON.stringify({
|
|
1690
|
-
success: result.success,
|
|
1691
|
-
installed: result.installed,
|
|
1692
|
-
failed: result.failed.length > 0 ? result.failed : undefined,
|
|
1693
|
-
message: result.message,
|
|
1694
|
-
next_step: result.success
|
|
1695
|
-
? 'Reindex your project to use the new parsers: sync({project: "...", full_reindex: true})'
|
|
1696
|
-
: 'Check the errors above and try again.',
|
|
1697
|
-
}, null, 2),
|
|
1698
|
-
}],
|
|
1699
|
-
};
|
|
1700
|
-
}
|
|
1701
|
-
// Analyze project and suggest parsers
|
|
1702
|
-
if (project) {
|
|
1703
|
-
const projectPath = path.isAbsolute(project)
|
|
1704
|
-
? project
|
|
1705
|
-
: path.resolve(project);
|
|
1706
|
-
if (!fs.existsSync(projectPath)) {
|
|
1707
|
-
return {
|
|
1708
|
-
content: [{
|
|
1709
|
-
type: 'text',
|
|
1710
|
-
text: `Directory not found: ${projectPath}`,
|
|
1711
|
-
}],
|
|
1712
|
-
isError: true,
|
|
1713
|
-
};
|
|
1714
|
-
}
|
|
1715
|
-
const analysis = await this.languageSupportService.analyzeProjectLanguages(projectPath);
|
|
1716
|
-
// If there are missing parsers, offer to install them
|
|
1717
|
-
const missingLanguages = analysis.missingParsers.map(p => p.language.toLowerCase());
|
|
1718
|
-
return {
|
|
1719
|
-
content: [{
|
|
1720
|
-
type: 'text',
|
|
1721
|
-
text: JSON.stringify({
|
|
1722
|
-
project: projectPath,
|
|
1723
|
-
detected_languages: analysis.detectedLanguages,
|
|
1724
|
-
installed_parsers: analysis.installedParsers,
|
|
1725
|
-
missing_parsers: analysis.missingParsers.map(p => ({
|
|
1726
|
-
language: p.language,
|
|
1727
|
-
npm_package: p.npmPackage,
|
|
1728
|
-
quality: p.quality,
|
|
1729
|
-
description: p.description,
|
|
1730
|
-
})),
|
|
1731
|
-
recommendations: analysis.recommendations,
|
|
1732
|
-
install_command: missingLanguages.length > 0
|
|
1733
|
-
? `Use install_parsers({languages: [${missingLanguages.map(l => `"${l}"`).join(', ')}]}) to install enhanced parsers`
|
|
1734
|
-
: 'All detected languages have parsers installed!',
|
|
1735
|
-
}, null, 2),
|
|
1736
|
-
}],
|
|
1737
|
-
};
|
|
1263
|
+
const codingStandardsPath = path.join(found.path, '.codeseeker', 'coding-standards.json');
|
|
1264
|
+
if (fs.existsSync(codingStandardsPath)) {
|
|
1265
|
+
try {
|
|
1266
|
+
fs.unlinkSync(codingStandardsPath);
|
|
1738
1267
|
}
|
|
1739
|
-
|
|
1740
|
-
return {
|
|
1741
|
-
content: [{
|
|
1742
|
-
type: 'text',
|
|
1743
|
-
text: JSON.stringify({
|
|
1744
|
-
usage: {
|
|
1745
|
-
analyze_project: 'install_parsers({project: "/path/to/project"}) - Detect languages and suggest parsers',
|
|
1746
|
-
install_specific: 'install_parsers({languages: ["python", "java"]}) - Install parsers for specific languages',
|
|
1747
|
-
list_available: 'install_parsers({list_available: true}) - Show all available parsers',
|
|
1748
|
-
},
|
|
1749
|
-
supported_languages: [
|
|
1750
|
-
'TypeScript (bundled)', 'JavaScript (bundled)',
|
|
1751
|
-
'Python', 'Java', 'C#', 'Go', 'Rust',
|
|
1752
|
-
'C', 'C++', 'Ruby', 'PHP', 'Swift', 'Kotlin'
|
|
1753
|
-
],
|
|
1754
|
-
}, null, 2),
|
|
1755
|
-
}],
|
|
1756
|
-
};
|
|
1268
|
+
catch { /* ignore */ }
|
|
1757
1269
|
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
}],
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
description: 'Dynamically manage which files are included or excluded from the index. ' +
|
|
1777
|
-
'Use this to exclude files that shouldn\'t be searched (e.g., Library/, build outputs, generated files) ' +
|
|
1778
|
-
'or include files that were incorrectly excluded. Exclusions persist in .codeseeker/exclusions.json. ' +
|
|
1779
|
-
'Example: exclude({action: "exclude", project: "my-app", paths: ["Library/**", "Temp/**"]}) ' +
|
|
1780
|
-
'to exclude Unity folders. Changes take effect immediately - excluded files are removed from the index.',
|
|
1781
|
-
inputSchema: {
|
|
1782
|
-
action: zod_1.z.enum(['exclude', 'include', 'list']).describe('Action: "exclude" adds paths to exclusion list and removes from index, ' +
|
|
1783
|
-
'"include" removes paths from exclusion list (they will be indexed on next reindex), ' +
|
|
1784
|
-
'"list" shows current exclusions'),
|
|
1785
|
-
project: zod_1.z.string().describe('Project name or path'),
|
|
1786
|
-
paths: zod_1.z.array(zod_1.z.string()).optional().describe('File paths or glob patterns to exclude/include (e.g., ["Library/**", "Temp/**", "*.generated.cs"]). ' +
|
|
1787
|
-
'Required for exclude/include actions.'),
|
|
1788
|
-
reason: zod_1.z.string().optional().describe('Optional reason for the exclusion (for documentation)'),
|
|
1789
|
-
},
|
|
1790
|
-
}, async ({ action, project, paths, reason }) => {
|
|
1270
|
+
this.startBackgroundIndexing(found.id, found.name, found.path, true);
|
|
1271
|
+
return {
|
|
1272
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1273
|
+
status: 'reindex_started', project: found.name,
|
|
1274
|
+
message: 'Full reindex started. Use index({action: "status"}) to check.',
|
|
1275
|
+
}, null, 2) }],
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
if (!changes || changes.length === 0) {
|
|
1279
|
+
return {
|
|
1280
|
+
content: [{ type: 'text', text: 'No changes provided. Either pass changes or set full_reindex: true.' }],
|
|
1281
|
+
isError: true,
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
let chunksCreated = 0, chunksDeleted = 0, filesProcessed = 0, filesSkipped = 0;
|
|
1285
|
+
const errors = [];
|
|
1286
|
+
for (const change of changes) {
|
|
1287
|
+
const relativePath = path.isAbsolute(change.path) ? path.relative(found.path, change.path) : change.path;
|
|
1791
1288
|
try {
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
const projects = await projectStore.list();
|
|
1798
|
-
const found = projects.find(p => p.name === project ||
|
|
1799
|
-
p.path === project ||
|
|
1800
|
-
path.basename(p.path) === project);
|
|
1801
|
-
if (!found) {
|
|
1802
|
-
return {
|
|
1803
|
-
content: [{
|
|
1804
|
-
type: 'text',
|
|
1805
|
-
text: `Project not found: ${project}. Use projects to see available projects.`,
|
|
1806
|
-
}],
|
|
1807
|
-
isError: true,
|
|
1808
|
-
};
|
|
1809
|
-
}
|
|
1810
|
-
// Load or create exclusions file
|
|
1811
|
-
const exclusionsPath = path.join(found.path, '.codeseeker', 'exclusions.json');
|
|
1812
|
-
let exclusions = {
|
|
1813
|
-
patterns: [],
|
|
1814
|
-
lastModified: new Date().toISOString()
|
|
1815
|
-
};
|
|
1816
|
-
// Ensure .codeseeker directory exists
|
|
1817
|
-
const codeseekerDir = path.join(found.path, '.codeseeker');
|
|
1818
|
-
if (!fs.existsSync(codeseekerDir)) {
|
|
1819
|
-
fs.mkdirSync(codeseekerDir, { recursive: true });
|
|
1820
|
-
}
|
|
1821
|
-
// Load existing exclusions
|
|
1822
|
-
if (fs.existsSync(exclusionsPath)) {
|
|
1823
|
-
try {
|
|
1824
|
-
exclusions = JSON.parse(fs.readFileSync(exclusionsPath, 'utf-8'));
|
|
1825
|
-
}
|
|
1826
|
-
catch {
|
|
1827
|
-
// Invalid JSON, start fresh
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
// Handle list action
|
|
1831
|
-
if (action === 'list') {
|
|
1832
|
-
return {
|
|
1833
|
-
content: [{
|
|
1834
|
-
type: 'text',
|
|
1835
|
-
text: JSON.stringify({
|
|
1836
|
-
project: found.name,
|
|
1837
|
-
project_path: found.path,
|
|
1838
|
-
exclusions_file: exclusionsPath,
|
|
1839
|
-
total_exclusions: exclusions.patterns.length,
|
|
1840
|
-
patterns: exclusions.patterns,
|
|
1841
|
-
last_modified: exclusions.lastModified,
|
|
1842
|
-
usage: {
|
|
1843
|
-
exclude: 'exclude({action: "exclude", project: "...", paths: ["pattern/**"]})',
|
|
1844
|
-
include: 'exclude({action: "include", project: "...", paths: ["pattern/**"]})',
|
|
1845
|
-
}
|
|
1846
|
-
}, null, 2),
|
|
1847
|
-
}],
|
|
1848
|
-
};
|
|
1849
|
-
}
|
|
1850
|
-
// Validate paths for exclude/include
|
|
1851
|
-
if (!paths || paths.length === 0) {
|
|
1852
|
-
return {
|
|
1853
|
-
content: [{
|
|
1854
|
-
type: 'text',
|
|
1855
|
-
text: 'No paths provided. Please specify paths or patterns to exclude/include.',
|
|
1856
|
-
}],
|
|
1857
|
-
isError: true,
|
|
1858
|
-
};
|
|
1859
|
-
}
|
|
1860
|
-
// Handle exclude action
|
|
1861
|
-
if (action === 'exclude') {
|
|
1862
|
-
const addedPatterns = [];
|
|
1863
|
-
const alreadyExcluded = [];
|
|
1864
|
-
let filesRemoved = 0;
|
|
1865
|
-
for (const pattern of paths) {
|
|
1866
|
-
// Normalize pattern (use forward slashes)
|
|
1867
|
-
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
1868
|
-
// Check if already excluded
|
|
1869
|
-
if (exclusions.patterns.some(p => p.pattern === normalizedPattern)) {
|
|
1870
|
-
alreadyExcluded.push(normalizedPattern);
|
|
1871
|
-
continue;
|
|
1872
|
-
}
|
|
1873
|
-
// Add to exclusions
|
|
1874
|
-
exclusions.patterns.push({
|
|
1875
|
-
pattern: normalizedPattern,
|
|
1876
|
-
reason: reason,
|
|
1877
|
-
addedAt: new Date().toISOString()
|
|
1878
|
-
});
|
|
1879
|
-
addedPatterns.push(normalizedPattern);
|
|
1880
|
-
// Remove matching files from the vector store and graph
|
|
1881
|
-
// Search for files matching this pattern
|
|
1882
|
-
const results = await vectorStore.searchByText(normalizedPattern, found.id, 1000);
|
|
1883
|
-
for (const result of results) {
|
|
1884
|
-
const filePath = result.document.filePath.replace(/\\/g, '/');
|
|
1885
|
-
// Check if file matches the exclusion pattern
|
|
1886
|
-
if (this.matchesExclusionPattern(filePath, normalizedPattern)) {
|
|
1887
|
-
// Delete from vector store
|
|
1888
|
-
await vectorStore.delete(result.document.id);
|
|
1889
|
-
filesRemoved++;
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1289
|
+
if (change.type === 'deleted') {
|
|
1290
|
+
const result = await this.indexingService.deleteFile(found.id, relativePath);
|
|
1291
|
+
if (result.success) {
|
|
1292
|
+
chunksDeleted += result.deleted;
|
|
1293
|
+
filesProcessed++;
|
|
1892
1294
|
}
|
|
1893
|
-
// Save exclusions
|
|
1894
|
-
exclusions.lastModified = new Date().toISOString();
|
|
1895
|
-
fs.writeFileSync(exclusionsPath, JSON.stringify(exclusions, null, 2));
|
|
1896
|
-
// Flush to persist deletions
|
|
1897
|
-
await vectorStore.flush();
|
|
1898
|
-
return {
|
|
1899
|
-
content: [{
|
|
1900
|
-
type: 'text',
|
|
1901
|
-
text: JSON.stringify({
|
|
1902
|
-
success: true,
|
|
1903
|
-
action: 'exclude',
|
|
1904
|
-
project: found.name,
|
|
1905
|
-
patterns_added: addedPatterns,
|
|
1906
|
-
already_excluded: alreadyExcluded.length > 0 ? alreadyExcluded : undefined,
|
|
1907
|
-
files_removed_from_index: filesRemoved,
|
|
1908
|
-
total_exclusions: exclusions.patterns.length,
|
|
1909
|
-
message: addedPatterns.length > 0
|
|
1910
|
-
? `Added ${addedPatterns.length} exclusion pattern(s). ${filesRemoved} file chunk(s) removed from index.`
|
|
1911
|
-
: 'No new patterns added (all were already excluded).',
|
|
1912
|
-
note: 'Excluded files will not appear in search results. Use action: "include" to re-enable indexing.'
|
|
1913
|
-
}, null, 2),
|
|
1914
|
-
}],
|
|
1915
|
-
};
|
|
1916
1295
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
1923
|
-
const index = exclusions.patterns.findIndex(p => p.pattern === normalizedPattern);
|
|
1924
|
-
if (index >= 0) {
|
|
1925
|
-
exclusions.patterns.splice(index, 1);
|
|
1926
|
-
removedPatterns.push(normalizedPattern);
|
|
1296
|
+
else {
|
|
1297
|
+
const result = await this.indexingService.indexSingleFile(found.path, relativePath, found.id);
|
|
1298
|
+
if (result.success) {
|
|
1299
|
+
if (result.skipped) {
|
|
1300
|
+
filesSkipped++;
|
|
1927
1301
|
}
|
|
1928
1302
|
else {
|
|
1929
|
-
|
|
1303
|
+
chunksCreated += result.chunksCreated;
|
|
1304
|
+
filesProcessed++;
|
|
1930
1305
|
}
|
|
1931
1306
|
}
|
|
1932
|
-
// Save exclusions
|
|
1933
|
-
exclusions.lastModified = new Date().toISOString();
|
|
1934
|
-
fs.writeFileSync(exclusionsPath, JSON.stringify(exclusions, null, 2));
|
|
1935
|
-
return {
|
|
1936
|
-
content: [{
|
|
1937
|
-
type: 'text',
|
|
1938
|
-
text: JSON.stringify({
|
|
1939
|
-
success: true,
|
|
1940
|
-
action: 'include',
|
|
1941
|
-
project: found.name,
|
|
1942
|
-
patterns_removed: removedPatterns,
|
|
1943
|
-
not_found: notFound.length > 0 ? notFound : undefined,
|
|
1944
|
-
total_exclusions: exclusions.patterns.length,
|
|
1945
|
-
message: removedPatterns.length > 0
|
|
1946
|
-
? `Removed ${removedPatterns.length} exclusion pattern(s). ` +
|
|
1947
|
-
`Files matching these patterns will be indexed on next reindex.`
|
|
1948
|
-
: 'No patterns were removed (none matched).',
|
|
1949
|
-
next_step: removedPatterns.length > 0
|
|
1950
|
-
? 'Run sync({project: "...", full_reindex: true}) to index the previously excluded files.'
|
|
1951
|
-
: undefined
|
|
1952
|
-
}, null, 2),
|
|
1953
|
-
}],
|
|
1954
|
-
};
|
|
1955
1307
|
}
|
|
1956
|
-
// Should not reach here
|
|
1957
|
-
return {
|
|
1958
|
-
content: [{
|
|
1959
|
-
type: 'text',
|
|
1960
|
-
text: `Unknown action: ${action}`,
|
|
1961
|
-
}],
|
|
1962
|
-
isError: true,
|
|
1963
|
-
};
|
|
1964
1308
|
}
|
|
1965
1309
|
catch (error) {
|
|
1966
|
-
|
|
1967
|
-
content: [{
|
|
1968
|
-
type: 'text',
|
|
1969
|
-
text: this.formatErrorMessage('Manage index exclusions', error instanceof Error ? error : String(error), { projectPath: project }),
|
|
1970
|
-
}],
|
|
1971
|
-
isError: true,
|
|
1972
|
-
};
|
|
1310
|
+
errors.push(`${change.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1973
1311
|
}
|
|
1974
|
-
});
|
|
1975
|
-
}
|
|
1976
|
-
/**
|
|
1977
|
-
* Check if a file path matches an exclusion pattern
|
|
1978
|
-
* Supports glob-like patterns: ** for any path, * for any segment
|
|
1979
|
-
*/
|
|
1980
|
-
matchesExclusionPattern(filePath, pattern) {
|
|
1981
|
-
// Normalize paths
|
|
1982
|
-
const normalizedPath = filePath.replace(/\\/g, '/').toLowerCase();
|
|
1983
|
-
const normalizedPattern = pattern.replace(/\\/g, '/').toLowerCase();
|
|
1984
|
-
// Direct match
|
|
1985
|
-
if (normalizedPath === normalizedPattern) {
|
|
1986
|
-
return true;
|
|
1987
1312
|
}
|
|
1988
|
-
//
|
|
1989
|
-
// ** matches any path including slashes
|
|
1990
|
-
// * matches any characters except slashes
|
|
1991
|
-
let regexPattern = normalizedPattern
|
|
1992
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars except * and ?
|
|
1993
|
-
.replace(/\*\*/g, '<<GLOBSTAR>>') // Temporarily replace **
|
|
1994
|
-
.replace(/\*/g, '[^/]*') // * matches anything except /
|
|
1995
|
-
.replace(/<<GLOBSTAR>>/g, '.*') // ** matches anything including /
|
|
1996
|
-
.replace(/\?/g, '.'); // ? matches any single char
|
|
1997
|
-
// Check if pattern should match at start
|
|
1998
|
-
if (!regexPattern.startsWith('.*')) {
|
|
1999
|
-
// Pattern like "Library/**" should match "Library/foo" but not "src/Library/foo"
|
|
2000
|
-
// Unless it's a more specific path pattern
|
|
2001
|
-
if (normalizedPattern.includes('/')) {
|
|
2002
|
-
regexPattern = `(^|/)${regexPattern}`;
|
|
2003
|
-
}
|
|
2004
|
-
else {
|
|
2005
|
-
regexPattern = `(^|/)${regexPattern}`;
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
// Allow matching at end without trailing slash
|
|
2009
|
-
regexPattern = `${regexPattern}(/.*)?$`;
|
|
1313
|
+
// Update coding standards
|
|
2010
1314
|
try {
|
|
2011
|
-
const
|
|
2012
|
-
|
|
1315
|
+
const changedPaths = changes.map(c => c.path);
|
|
1316
|
+
const generator = new coding_standards_generator_1.CodingStandardsGenerator(vectorStore);
|
|
1317
|
+
await generator.updateStandards(found.id, found.path, changedPaths);
|
|
2013
1318
|
}
|
|
2014
|
-
catch {
|
|
2015
|
-
|
|
2016
|
-
|
|
1319
|
+
catch { /* Non-fatal */ }
|
|
1320
|
+
// Invalidate cache
|
|
1321
|
+
let cacheInvalidated = 0;
|
|
1322
|
+
try {
|
|
1323
|
+
cacheInvalidated = await this.queryCache.invalidateProject(found.id);
|
|
2017
1324
|
}
|
|
1325
|
+
catch { /* Non-fatal */ }
|
|
1326
|
+
const duration = Date.now() - startTime;
|
|
1327
|
+
return {
|
|
1328
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1329
|
+
success: errors.length === 0,
|
|
1330
|
+
mode: 'incremental', project: found.name,
|
|
1331
|
+
changes_processed: changes.length,
|
|
1332
|
+
files_reindexed: filesProcessed,
|
|
1333
|
+
files_skipped: filesSkipped > 0 ? filesSkipped : undefined,
|
|
1334
|
+
chunks_created: chunksCreated,
|
|
1335
|
+
chunks_deleted: chunksDeleted,
|
|
1336
|
+
cache_invalidated: cacheInvalidated > 0 ? cacheInvalidated : undefined,
|
|
1337
|
+
duration_ms: duration,
|
|
1338
|
+
errors: errors.length > 0 ? errors.slice(0, 5) : undefined,
|
|
1339
|
+
}, null, 2) }],
|
|
1340
|
+
};
|
|
2018
1341
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
1342
|
+
async handleProjects() {
|
|
1343
|
+
const storageManager = await (0, storage_1.getStorageManager)();
|
|
1344
|
+
const projectStore = storageManager.getProjectStore();
|
|
1345
|
+
const vectorStore = storageManager.getVectorStore();
|
|
1346
|
+
const projects = await projectStore.list();
|
|
1347
|
+
if (projects.length === 0) {
|
|
1348
|
+
return {
|
|
1349
|
+
content: [{ type: 'text', text: 'No projects indexed. Use index({action: "init", path: "/path/to/project"}).' }],
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
const projectsWithCounts = await Promise.all(projects.map(async (p) => {
|
|
1353
|
+
const fileCount = await vectorStore.countFiles(p.id);
|
|
1354
|
+
const chunkCount = await vectorStore.count(p.id);
|
|
1355
|
+
const indexingStatus = this._getIndexingStatusForProject(p.id);
|
|
1356
|
+
const projectInfo = {
|
|
1357
|
+
name: p.name, path: p.path, files: fileCount, chunks: chunkCount,
|
|
1358
|
+
last_indexed: p.updatedAt.toISOString(),
|
|
1359
|
+
};
|
|
1360
|
+
if (indexingStatus)
|
|
1361
|
+
Object.assign(projectInfo, indexingStatus);
|
|
1362
|
+
return projectInfo;
|
|
1363
|
+
}));
|
|
1364
|
+
return {
|
|
1365
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1366
|
+
storage_mode: storageManager.getMode(),
|
|
1367
|
+
total_projects: projects.length,
|
|
1368
|
+
projects: projectsWithCounts,
|
|
1369
|
+
}, null, 2) }],
|
|
1370
|
+
};
|
|
2036
1371
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
`• Ensure you have read permissions`;
|
|
1372
|
+
async handleInstallParsers(params) {
|
|
1373
|
+
const { project, languages, list_available = false } = params;
|
|
1374
|
+
if (list_available) {
|
|
1375
|
+
const parsers = await this.languageSupportService.checkInstalledParsers();
|
|
1376
|
+
const installed = parsers.filter(p => p.installed);
|
|
1377
|
+
const available = parsers.filter(p => !p.installed);
|
|
1378
|
+
return {
|
|
1379
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1380
|
+
installed_parsers: installed.map(p => ({ language: p.language, extensions: p.extensions, quality: p.quality })),
|
|
1381
|
+
available_parsers: available.map(p => ({ language: p.language, extensions: p.extensions, npm_package: p.npmPackage, quality: p.quality })),
|
|
1382
|
+
}, null, 2) }],
|
|
1383
|
+
};
|
|
2050
1384
|
}
|
|
2051
|
-
if (
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
1385
|
+
if (languages && languages.length > 0) {
|
|
1386
|
+
const result = await this.languageSupportService.installLanguageParsers(languages);
|
|
1387
|
+
return {
|
|
1388
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1389
|
+
success: result.success, installed: result.installed,
|
|
1390
|
+
failed: result.failed.length > 0 ? result.failed : undefined,
|
|
1391
|
+
message: result.message,
|
|
1392
|
+
next_step: result.success ? 'Reindex: index({action: "sync", project: "...", full_reindex: true})' : undefined,
|
|
1393
|
+
}, null, 2) }],
|
|
1394
|
+
};
|
|
2057
1395
|
}
|
|
2058
|
-
if (
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
1396
|
+
if (project) {
|
|
1397
|
+
const projectPath = path.isAbsolute(project) ? project : path.resolve(project);
|
|
1398
|
+
if (!fs.existsSync(projectPath)) {
|
|
1399
|
+
return { content: [{ type: 'text', text: `Directory not found: ${projectPath}` }], isError: true };
|
|
1400
|
+
}
|
|
1401
|
+
const analysis = await this.languageSupportService.analyzeProjectLanguages(projectPath);
|
|
1402
|
+
const missingLanguages = analysis.missingParsers.map(p => p.language.toLowerCase());
|
|
1403
|
+
return {
|
|
1404
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1405
|
+
project: projectPath,
|
|
1406
|
+
detected_languages: analysis.detectedLanguages,
|
|
1407
|
+
installed_parsers: analysis.installedParsers,
|
|
1408
|
+
missing_parsers: analysis.missingParsers.map(p => ({ language: p.language, npm_package: p.npmPackage })),
|
|
1409
|
+
install_command: missingLanguages.length > 0
|
|
1410
|
+
? `index({action: "parsers", languages: [${missingLanguages.map(l => `"${l}"`).join(', ')}]})`
|
|
1411
|
+
: 'All parsers installed!',
|
|
1412
|
+
}, null, 2) }],
|
|
1413
|
+
};
|
|
2064
1414
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
1415
|
+
return {
|
|
1416
|
+
content: [{ type: 'text', text: 'Provide project path or languages, or set list_available: true.' }],
|
|
1417
|
+
isError: true,
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
async handleExclude(params) {
|
|
1421
|
+
const { project, exclude_action, paths: excludePaths, reason } = params;
|
|
1422
|
+
if (!project) {
|
|
1423
|
+
return { content: [{ type: 'text', text: 'project parameter required for exclude action.' }], isError: true };
|
|
2071
1424
|
}
|
|
2072
|
-
if (
|
|
2073
|
-
|
|
2074
|
-
return `${operation} failed: Project not indexed.\n\n` +
|
|
2075
|
-
`ACTION REQUIRED:\n` +
|
|
2076
|
-
`• First run: ${pathHint}\n` +
|
|
2077
|
-
`• Then retry your search\n` +
|
|
2078
|
-
`• Use projects() to see indexed projects`;
|
|
1425
|
+
if (!exclude_action) {
|
|
1426
|
+
return { content: [{ type: 'text', text: 'exclude_action required (exclude, include, or list).' }], isError: true };
|
|
2079
1427
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
*/
|
|
2097
|
-
registerFindDuplicatesTool() {
|
|
2098
|
-
this.server.registerTool('find_duplicates', {
|
|
2099
|
-
description: '**FIND DUPLICATE CODE** - Detects duplicate and similar code patterns using semantic analysis. ' +
|
|
2100
|
-
'Finds: exact copies, semantically similar code (same logic, different names), and structurally similar patterns. ' +
|
|
2101
|
-
'Use when: cleaning up codebase, finding copy-paste code, reducing maintenance burden. ' +
|
|
2102
|
-
'Returns groups of duplicates with consolidation suggestions and estimated savings. ' +
|
|
2103
|
-
'**IMPORTANT**: Always pass the project parameter with your workspace root path.',
|
|
2104
|
-
inputSchema: {
|
|
2105
|
-
project: zod_1.z.string().describe('Project path - REQUIRED: the workspace root path to analyze'),
|
|
2106
|
-
similarity_threshold: zod_1.z.number().optional().default(0.80)
|
|
2107
|
-
.describe('Minimum similarity score (0.0-1.0) to consider as duplicate. Default: 0.80'),
|
|
2108
|
-
min_lines: zod_1.z.number().optional().default(5)
|
|
2109
|
-
.describe('Minimum lines in a code block to analyze. Default: 5'),
|
|
2110
|
-
include_types: zod_1.z.array(zod_1.z.enum(['function', 'class', 'method', 'block'])).optional()
|
|
2111
|
-
.describe('Types of code to analyze. Default: all types'),
|
|
2112
|
-
},
|
|
2113
|
-
}, async ({ project, similarity_threshold = 0.80, min_lines = 5, include_types: _include_types }) => {
|
|
1428
|
+
const storageManager = await (0, storage_1.getStorageManager)();
|
|
1429
|
+
const projectStore = storageManager.getProjectStore();
|
|
1430
|
+
const vectorStore = storageManager.getVectorStore();
|
|
1431
|
+
const projects = await projectStore.list();
|
|
1432
|
+
const found = projects.find(p => p.name === project || p.path === project || path.basename(p.path) === project);
|
|
1433
|
+
if (!found) {
|
|
1434
|
+
return { content: [{ type: 'text', text: `Project not found: ${project}.` }], isError: true };
|
|
1435
|
+
}
|
|
1436
|
+
// Load exclusions
|
|
1437
|
+
const exclusionsPath = path.join(found.path, '.codeseeker', 'exclusions.json');
|
|
1438
|
+
let exclusions = { patterns: [], lastModified: new Date().toISOString() };
|
|
1439
|
+
const codeseekerDir = path.join(found.path, '.codeseeker');
|
|
1440
|
+
if (!fs.existsSync(codeseekerDir)) {
|
|
1441
|
+
fs.mkdirSync(codeseekerDir, { recursive: true });
|
|
1442
|
+
}
|
|
1443
|
+
if (fs.existsSync(exclusionsPath)) {
|
|
2114
1444
|
try {
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
semantic_duplicates: 0,
|
|
2149
|
-
structural_duplicates: 0,
|
|
2150
|
-
total_lines_affected: 0,
|
|
2151
|
-
potential_lines_saved: 0,
|
|
2152
|
-
},
|
|
2153
|
-
duplicate_groups: [],
|
|
2154
|
-
recommendations: ['No indexed chunks found. Run index() first.'],
|
|
2155
|
-
}, null, 2),
|
|
2156
|
-
}],
|
|
2157
|
-
};
|
|
2158
|
-
}
|
|
2159
|
-
// Filter by min_lines if metadata contains line info
|
|
2160
|
-
const filteredDocs = allDocs.filter(doc => {
|
|
2161
|
-
const lineCount = doc.content.split('\n').length;
|
|
2162
|
-
return lineCount >= min_lines;
|
|
2163
|
-
});
|
|
2164
|
-
// Find duplicate groups using indexed embeddings
|
|
2165
|
-
const duplicateGroups = [];
|
|
2166
|
-
const processed = new Set();
|
|
2167
|
-
const EXACT_THRESHOLD = 0.98;
|
|
2168
|
-
// For each chunk, find similar chunks using cosine similarity
|
|
2169
|
-
for (let i = 0; i < filteredDocs.length && duplicateGroups.length < 50; i++) {
|
|
2170
|
-
const doc = filteredDocs[i];
|
|
2171
|
-
if (processed.has(doc.id))
|
|
2172
|
-
continue;
|
|
2173
|
-
// Find similar documents using vector search
|
|
2174
|
-
const similarDocs = await vectorStore.searchByVector(doc.embedding, projectRecord.id, 20 // Get top 20 similar
|
|
2175
|
-
);
|
|
2176
|
-
// Filter by threshold and exclude self
|
|
2177
|
-
const matches = similarDocs.filter(match => match.document.id !== doc.id &&
|
|
2178
|
-
match.score >= similarity_threshold &&
|
|
2179
|
-
!processed.has(match.document.id));
|
|
2180
|
-
if (matches.length > 0) {
|
|
2181
|
-
// Determine type (exact vs semantic)
|
|
2182
|
-
const maxScore = Math.max(...matches.map(m => m.score));
|
|
2183
|
-
const type = maxScore >= EXACT_THRESHOLD ? 'exact' : 'semantic';
|
|
2184
|
-
// Extract line info from metadata if available
|
|
2185
|
-
const getLines = (d) => {
|
|
2186
|
-
const meta = d.metadata;
|
|
2187
|
-
return {
|
|
2188
|
-
startLine: meta?.startLine,
|
|
2189
|
-
endLine: meta?.endLine,
|
|
2190
|
-
};
|
|
2191
|
-
};
|
|
2192
|
-
duplicateGroups.push({
|
|
2193
|
-
type,
|
|
2194
|
-
similarity: maxScore,
|
|
2195
|
-
chunks: [
|
|
2196
|
-
{
|
|
2197
|
-
id: doc.id,
|
|
2198
|
-
filePath: doc.filePath,
|
|
2199
|
-
content: doc.content.substring(0, 200) + (doc.content.length > 200 ? '...' : ''),
|
|
2200
|
-
...getLines(doc),
|
|
2201
|
-
},
|
|
2202
|
-
...matches.map(m => ({
|
|
2203
|
-
id: m.document.id,
|
|
2204
|
-
filePath: m.document.filePath,
|
|
2205
|
-
content: m.document.content.substring(0, 200) + (m.document.content.length > 200 ? '...' : ''),
|
|
2206
|
-
...getLines(m.document),
|
|
2207
|
-
})),
|
|
2208
|
-
],
|
|
2209
|
-
});
|
|
2210
|
-
// Mark all as processed
|
|
2211
|
-
processed.add(doc.id);
|
|
2212
|
-
matches.forEach(m => processed.add(m.document.id));
|
|
1445
|
+
exclusions = JSON.parse(fs.readFileSync(exclusionsPath, 'utf-8'));
|
|
1446
|
+
}
|
|
1447
|
+
catch { /* start fresh */ }
|
|
1448
|
+
}
|
|
1449
|
+
if (exclude_action === 'list') {
|
|
1450
|
+
return {
|
|
1451
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1452
|
+
project: found.name,
|
|
1453
|
+
total_exclusions: exclusions.patterns.length,
|
|
1454
|
+
patterns: exclusions.patterns,
|
|
1455
|
+
}, null, 2) }],
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
if (!excludePaths || excludePaths.length === 0) {
|
|
1459
|
+
return { content: [{ type: 'text', text: 'No paths provided for exclude/include.' }], isError: true };
|
|
1460
|
+
}
|
|
1461
|
+
if (exclude_action === 'exclude') {
|
|
1462
|
+
const addedPatterns = [];
|
|
1463
|
+
const alreadyExcluded = [];
|
|
1464
|
+
let filesRemoved = 0;
|
|
1465
|
+
for (const pattern of excludePaths) {
|
|
1466
|
+
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
1467
|
+
if (exclusions.patterns.some(p => p.pattern === normalizedPattern)) {
|
|
1468
|
+
alreadyExcluded.push(normalizedPattern);
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
exclusions.patterns.push({ pattern: normalizedPattern, reason, addedAt: new Date().toISOString() });
|
|
1472
|
+
addedPatterns.push(normalizedPattern);
|
|
1473
|
+
const results = await vectorStore.searchByText(normalizedPattern, found.id, 1000);
|
|
1474
|
+
for (const result of results) {
|
|
1475
|
+
if (this.matchesExclusionPattern(result.document.filePath.replace(/\\/g, '/'), normalizedPattern)) {
|
|
1476
|
+
await vectorStore.delete(result.document.id);
|
|
1477
|
+
filesRemoved++;
|
|
2213
1478
|
}
|
|
2214
1479
|
}
|
|
2215
|
-
// Calculate summary
|
|
2216
|
-
const exactDuplicates = duplicateGroups.filter(g => g.type === 'exact').length;
|
|
2217
|
-
const semanticDuplicates = duplicateGroups.filter(g => g.type === 'semantic').length;
|
|
2218
|
-
const totalLinesAffected = duplicateGroups.reduce((sum, g) => sum + g.chunks.reduce((chunkSum, c) => chunkSum + (c.endLine && c.startLine ? c.endLine - c.startLine + 1 : c.content.split('\n').length), 0), 0);
|
|
2219
|
-
// Format results
|
|
2220
|
-
const formattedGroups = duplicateGroups.slice(0, 20).map(group => ({
|
|
2221
|
-
type: group.type,
|
|
2222
|
-
similarity: `${(group.similarity * 100).toFixed(1)}%`,
|
|
2223
|
-
files_affected: new Set(group.chunks.map(c => c.filePath)).size,
|
|
2224
|
-
locations: group.chunks.map(c => ({
|
|
2225
|
-
file: path.relative(projectRecord.path, c.filePath),
|
|
2226
|
-
lines: c.startLine && c.endLine ? `${c.startLine}-${c.endLine}` : 'N/A',
|
|
2227
|
-
preview: c.content.substring(0, 100).replace(/\n/g, ' '),
|
|
2228
|
-
})),
|
|
2229
|
-
}));
|
|
2230
|
-
return {
|
|
2231
|
-
content: [{
|
|
2232
|
-
type: 'text',
|
|
2233
|
-
text: JSON.stringify({
|
|
2234
|
-
project: projectRecord.name,
|
|
2235
|
-
summary: {
|
|
2236
|
-
total_chunks_analyzed: filteredDocs.length,
|
|
2237
|
-
exact_duplicates: exactDuplicates,
|
|
2238
|
-
semantic_duplicates: semanticDuplicates,
|
|
2239
|
-
structural_duplicates: 0, // Not computed in this approach
|
|
2240
|
-
total_lines_affected: totalLinesAffected,
|
|
2241
|
-
potential_lines_saved: Math.floor(totalLinesAffected * 0.6), // Estimate
|
|
2242
|
-
},
|
|
2243
|
-
duplicate_groups: formattedGroups,
|
|
2244
|
-
recommendations: exactDuplicates > 0
|
|
2245
|
-
? [`Found ${exactDuplicates} exact duplicate groups - prioritize consolidation`]
|
|
2246
|
-
: semanticDuplicates > 0
|
|
2247
|
-
? [`Found ${semanticDuplicates} semantic duplicates - review for potential abstraction`]
|
|
2248
|
-
: ['No significant duplicates found above threshold'],
|
|
2249
|
-
}, null, 2),
|
|
2250
|
-
}],
|
|
2251
|
-
};
|
|
2252
1480
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
project: zod_1.z.string().describe('Project path - REQUIRED: the workspace root path to analyze'),
|
|
2276
|
-
include_patterns: zod_1.z.array(zod_1.z.enum(['dead_code', 'god_class', 'circular_deps', 'feature_envy', 'coupling'])).optional()
|
|
2277
|
-
.describe('Anti-patterns to detect. Default: all patterns'),
|
|
2278
|
-
},
|
|
2279
|
-
}, async ({ project, include_patterns }) => {
|
|
2280
|
-
try {
|
|
2281
|
-
const storageManager = await (0, storage_1.getStorageManager)();
|
|
2282
|
-
const projectStore = storageManager.getProjectStore();
|
|
2283
|
-
const graphStore = storageManager.getGraphStore();
|
|
2284
|
-
const projects = await projectStore.list();
|
|
2285
|
-
// Find the project
|
|
2286
|
-
const projectRecord = projects.find(p => p.name === project ||
|
|
2287
|
-
p.path === project ||
|
|
2288
|
-
path.basename(p.path) === project ||
|
|
2289
|
-
path.resolve(project) === p.path);
|
|
2290
|
-
if (!projectRecord) {
|
|
2291
|
-
return {
|
|
2292
|
-
content: [{
|
|
2293
|
-
type: 'text',
|
|
2294
|
-
text: `Project not found or not indexed: ${project}\n\n` +
|
|
2295
|
-
`Use index({path: "${project}"}) to index the project first.`,
|
|
2296
|
-
}],
|
|
2297
|
-
isError: true,
|
|
2298
|
-
};
|
|
2299
|
-
}
|
|
2300
|
-
// Use the indexed graph data from storage manager (same as show_dependencies)
|
|
2301
|
-
const allNodes = await graphStore.findNodes(projectRecord.id);
|
|
2302
|
-
if (allNodes.length === 0) {
|
|
2303
|
-
return {
|
|
2304
|
-
content: [{
|
|
2305
|
-
type: 'text',
|
|
2306
|
-
text: JSON.stringify({
|
|
2307
|
-
project: projectRecord.name,
|
|
2308
|
-
summary: {
|
|
2309
|
-
total_issues: 0,
|
|
2310
|
-
dead_code_count: 0,
|
|
2311
|
-
anti_patterns_count: 0,
|
|
2312
|
-
coupling_issues_count: 0,
|
|
2313
|
-
},
|
|
2314
|
-
dead_code: [],
|
|
2315
|
-
anti_patterns: [],
|
|
2316
|
-
coupling_issues: [],
|
|
2317
|
-
note: 'No graph data found. The project may need reindexing with graph building enabled.',
|
|
2318
|
-
}, null, 2),
|
|
2319
|
-
}],
|
|
2320
|
-
};
|
|
2321
|
-
}
|
|
2322
|
-
// Analyze the indexed graph for dead code and anti-patterns
|
|
2323
|
-
const patterns = include_patterns || ['dead_code', 'god_class', 'circular_deps', 'feature_envy', 'coupling'];
|
|
2324
|
-
// Build analysis results from indexed graph
|
|
2325
|
-
const deadCodeItems = [];
|
|
2326
|
-
const antiPatternItems = [];
|
|
2327
|
-
const couplingItems = [];
|
|
2328
|
-
// Analyze each node for issues
|
|
2329
|
-
for (const node of allNodes) {
|
|
2330
|
-
// Get edges for this node
|
|
2331
|
-
const inEdges = await graphStore.getEdges(node.id, 'in');
|
|
2332
|
-
const outEdges = await graphStore.getEdges(node.id, 'out');
|
|
2333
|
-
// Dead code detection: nodes with no incoming references (except entry points)
|
|
2334
|
-
if (patterns.includes('dead_code')) {
|
|
2335
|
-
const isEntryPoint = node.type === 'file' ||
|
|
2336
|
-
node.name.toLowerCase().includes('main') ||
|
|
2337
|
-
node.name.toLowerCase().includes('index') ||
|
|
2338
|
-
node.name.toLowerCase().includes('app');
|
|
2339
|
-
// A class/function with no incoming calls/imports is potentially dead
|
|
2340
|
-
if (!isEntryPoint && (node.type === 'class' || node.type === 'function') && inEdges.length === 0) {
|
|
2341
|
-
deadCodeItems.push({
|
|
2342
|
-
type: 'Dead Code',
|
|
2343
|
-
name: node.name,
|
|
2344
|
-
file: path.relative(projectRecord.path, node.filePath),
|
|
2345
|
-
description: `Unused ${node.type}: ${node.name} - no incoming references found`,
|
|
2346
|
-
confidence: '70%',
|
|
2347
|
-
impact: 'medium',
|
|
2348
|
-
recommendation: 'Review if this code is needed. Remove if unused or add to exports if it should be public.',
|
|
2349
|
-
});
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
// God class detection: classes with too many methods/dependencies
|
|
2353
|
-
if (patterns.includes('god_class') && node.type === 'class') {
|
|
2354
|
-
const containsEdges = outEdges.filter(e => e.type === 'contains');
|
|
2355
|
-
const dependsOnEdges = outEdges.filter(e => e.type === 'imports' || e.type === 'depends_on');
|
|
2356
|
-
if (containsEdges.length > 15 || dependsOnEdges.length > 10) {
|
|
2357
|
-
antiPatternItems.push({
|
|
2358
|
-
type: 'God Class',
|
|
2359
|
-
name: node.name,
|
|
2360
|
-
file: path.relative(projectRecord.path, node.filePath),
|
|
2361
|
-
description: `Class ${node.name} has ${containsEdges.length} members and ${dependsOnEdges.length} dependencies`,
|
|
2362
|
-
confidence: '80%',
|
|
2363
|
-
impact: 'high',
|
|
2364
|
-
recommendation: 'Break down into smaller, focused classes following Single Responsibility Principle',
|
|
2365
|
-
});
|
|
2366
|
-
}
|
|
2367
|
-
}
|
|
2368
|
-
// High coupling detection
|
|
2369
|
-
if (patterns.includes('coupling') && (node.type === 'class' || node.type === 'file')) {
|
|
2370
|
-
const dependencies = outEdges.filter(e => e.type === 'imports' || e.type === 'depends_on');
|
|
2371
|
-
if (dependencies.length > 8) {
|
|
2372
|
-
couplingItems.push({
|
|
2373
|
-
type: 'High Coupling',
|
|
2374
|
-
name: node.name,
|
|
2375
|
-
file: path.relative(projectRecord.path, node.filePath),
|
|
2376
|
-
description: `${node.type} ${node.name} has ${dependencies.length} dependencies`,
|
|
2377
|
-
confidence: '75%',
|
|
2378
|
-
impact: 'high',
|
|
2379
|
-
recommendation: 'Reduce dependencies using interfaces, dependency injection, or service locator pattern',
|
|
2380
|
-
});
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
1481
|
+
exclusions.lastModified = new Date().toISOString();
|
|
1482
|
+
fs.writeFileSync(exclusionsPath, JSON.stringify(exclusions, null, 2));
|
|
1483
|
+
await vectorStore.flush();
|
|
1484
|
+
return {
|
|
1485
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1486
|
+
success: true, action: 'exclude', project: found.name,
|
|
1487
|
+
patterns_added: addedPatterns,
|
|
1488
|
+
already_excluded: alreadyExcluded.length > 0 ? alreadyExcluded : undefined,
|
|
1489
|
+
files_removed_from_index: filesRemoved,
|
|
1490
|
+
total_exclusions: exclusions.patterns.length,
|
|
1491
|
+
}, null, 2) }],
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
if (exclude_action === 'include') {
|
|
1495
|
+
const removedPatterns = [];
|
|
1496
|
+
const notFound = [];
|
|
1497
|
+
for (const pattern of excludePaths) {
|
|
1498
|
+
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
1499
|
+
const index = exclusions.patterns.findIndex(p => p.pattern === normalizedPattern);
|
|
1500
|
+
if (index >= 0) {
|
|
1501
|
+
exclusions.patterns.splice(index, 1);
|
|
1502
|
+
removedPatterns.push(normalizedPattern);
|
|
2383
1503
|
}
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
const fileNodes = allNodes.filter(n => n.type === 'file');
|
|
2387
|
-
const importMap = new Map();
|
|
2388
|
-
for (const fileNode of fileNodes) {
|
|
2389
|
-
const imports = await graphStore.getEdges(fileNode.id, 'out');
|
|
2390
|
-
const importTargets = new Set(imports.filter(e => e.type === 'imports').map(e => e.target));
|
|
2391
|
-
importMap.set(fileNode.id, importTargets);
|
|
2392
|
-
}
|
|
2393
|
-
// Check for circular imports (A imports B and B imports A)
|
|
2394
|
-
for (const [fileId, imports] of importMap.entries()) {
|
|
2395
|
-
for (const targetId of imports) {
|
|
2396
|
-
const targetImports = importMap.get(targetId);
|
|
2397
|
-
if (targetImports?.has(fileId)) {
|
|
2398
|
-
const sourceNode = allNodes.find(n => n.id === fileId);
|
|
2399
|
-
const targetNode = allNodes.find(n => n.id === targetId);
|
|
2400
|
-
if (sourceNode && targetNode) {
|
|
2401
|
-
antiPatternItems.push({
|
|
2402
|
-
type: 'Circular Dependency',
|
|
2403
|
-
name: `${sourceNode.name} <-> ${targetNode.name}`,
|
|
2404
|
-
file: path.relative(projectRecord.path, sourceNode.filePath),
|
|
2405
|
-
description: `Bidirectional import between ${sourceNode.name} and ${targetNode.name}`,
|
|
2406
|
-
confidence: '90%',
|
|
2407
|
-
impact: 'high',
|
|
2408
|
-
recommendation: 'Break the cycle using dependency inversion or extracting shared code',
|
|
2409
|
-
});
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
1504
|
+
else {
|
|
1505
|
+
notFound.push(normalizedPattern);
|
|
2414
1506
|
}
|
|
2415
|
-
return {
|
|
2416
|
-
content: [{
|
|
2417
|
-
type: 'text',
|
|
2418
|
-
text: JSON.stringify({
|
|
2419
|
-
project: projectRecord.name,
|
|
2420
|
-
graph_stats: {
|
|
2421
|
-
total_nodes: allNodes.length,
|
|
2422
|
-
files: allNodes.filter(n => n.type === 'file').length,
|
|
2423
|
-
classes: allNodes.filter(n => n.type === 'class').length,
|
|
2424
|
-
functions: allNodes.filter(n => n.type === 'function').length,
|
|
2425
|
-
},
|
|
2426
|
-
summary: {
|
|
2427
|
-
total_issues: deadCodeItems.length + antiPatternItems.length + couplingItems.length,
|
|
2428
|
-
dead_code_count: deadCodeItems.length,
|
|
2429
|
-
anti_patterns_count: antiPatternItems.length,
|
|
2430
|
-
coupling_issues_count: couplingItems.length,
|
|
2431
|
-
},
|
|
2432
|
-
dead_code: deadCodeItems.slice(0, 20),
|
|
2433
|
-
anti_patterns: antiPatternItems.slice(0, 10),
|
|
2434
|
-
coupling_issues: couplingItems.slice(0, 10),
|
|
2435
|
-
}, null, 2),
|
|
2436
|
-
}],
|
|
2437
|
-
};
|
|
2438
1507
|
}
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
1508
|
+
exclusions.lastModified = new Date().toISOString();
|
|
1509
|
+
fs.writeFileSync(exclusionsPath, JSON.stringify(exclusions, null, 2));
|
|
1510
|
+
return {
|
|
1511
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1512
|
+
success: true, action: 'include', project: found.name,
|
|
1513
|
+
patterns_removed: removedPatterns,
|
|
1514
|
+
not_found: notFound.length > 0 ? notFound : undefined,
|
|
1515
|
+
total_exclusions: exclusions.patterns.length,
|
|
1516
|
+
next_step: removedPatterns.length > 0 ? 'index({action: "sync", project: "...", full_reindex: true})' : undefined,
|
|
1517
|
+
}, null, 2) }],
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
return { content: [{ type: 'text', text: `Unknown exclude_action: ${exclude_action}` }], isError: true };
|
|
2449
1521
|
}
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
1522
|
+
// ============================================================
|
|
1523
|
+
// SERVER LIFECYCLE
|
|
1524
|
+
// ============================================================
|
|
2453
1525
|
async start() {
|
|
2454
|
-
// Use stderr for logging since stdout is for JSON-RPC
|
|
2455
1526
|
console.error('Starting CodeSeeker MCP server...');
|
|
2456
|
-
// Initialize storage manager first to ensure singleton is ready
|
|
2457
1527
|
const storageManager = await (0, storage_1.getStorageManager)();
|
|
2458
1528
|
console.error(`Storage mode: ${storageManager.getMode()}`);
|
|
2459
1529
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
2460
1530
|
await this.server.connect(transport);
|
|
2461
1531
|
console.error('CodeSeeker MCP server running on stdio');
|
|
2462
1532
|
}
|
|
2463
|
-
/**
|
|
2464
|
-
* Graceful shutdown - flush and close all storage before exit
|
|
2465
|
-
*/
|
|
2466
1533
|
async shutdown() {
|
|
2467
1534
|
console.error('Shutting down CodeSeeker MCP server...');
|
|
2468
1535
|
try {
|
|
2469
1536
|
const storageManager = await (0, storage_1.getStorageManager)();
|
|
2470
|
-
// Flush first to ensure data is saved
|
|
2471
1537
|
await storageManager.flushAll();
|
|
2472
1538
|
console.error('Storage flushed successfully');
|
|
2473
|
-
// Close to stop interval timers and release resources
|
|
2474
1539
|
await storageManager.closeAll();
|
|
2475
1540
|
console.error('Storage closed successfully');
|
|
2476
1541
|
}
|
|
@@ -2486,16 +1551,13 @@ exports.CodeSeekerMcpServer = CodeSeekerMcpServer;
|
|
|
2486
1551
|
async function startMcpServer() {
|
|
2487
1552
|
const server = new CodeSeekerMcpServer();
|
|
2488
1553
|
let isShuttingDown = false;
|
|
2489
|
-
// Register signal handlers for graceful shutdown
|
|
2490
1554
|
const shutdown = async (signal) => {
|
|
2491
|
-
// Prevent multiple shutdown attempts
|
|
2492
1555
|
if (isShuttingDown) {
|
|
2493
1556
|
console.error(`Already shutting down, ignoring ${signal}`);
|
|
2494
1557
|
return;
|
|
2495
1558
|
}
|
|
2496
1559
|
isShuttingDown = true;
|
|
2497
1560
|
console.error(`\nReceived ${signal}, shutting down gracefully...`);
|
|
2498
|
-
// Set a hard timeout to force exit if shutdown takes too long
|
|
2499
1561
|
const forceExitTimeout = setTimeout(() => {
|
|
2500
1562
|
console.error('Shutdown timeout, forcing exit...');
|
|
2501
1563
|
process.exit(1);
|
|
@@ -2513,29 +1575,20 @@ async function startMcpServer() {
|
|
|
2513
1575
|
};
|
|
2514
1576
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
2515
1577
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
2516
|
-
// CRITICAL: Handle stdin close - this is how MCP clients signal disconnect
|
|
2517
|
-
// On Windows, signals are unreliable, so stdin close is the primary shutdown mechanism
|
|
2518
1578
|
process.stdin.on('close', () => shutdown('stdin-close'));
|
|
2519
1579
|
process.stdin.on('end', () => shutdown('stdin-end'));
|
|
2520
|
-
// Also handle stdin errors (broken pipe, etc.)
|
|
2521
1580
|
process.stdin.on('error', (err) => {
|
|
2522
|
-
// EPIPE and similar errors mean the parent process disconnected
|
|
2523
1581
|
console.error(`stdin error: ${err.message}`);
|
|
2524
1582
|
shutdown('stdin-error');
|
|
2525
1583
|
});
|
|
2526
|
-
// Handle Windows-specific signals
|
|
2527
1584
|
if (process.platform === 'win32') {
|
|
2528
1585
|
process.on('SIGHUP', () => shutdown('SIGHUP'));
|
|
2529
|
-
// Windows: also listen for parent process disconnect via stdin
|
|
2530
|
-
// Resume stdin to ensure we receive close/end events
|
|
2531
1586
|
process.stdin.resume();
|
|
2532
1587
|
}
|
|
2533
|
-
// Handle uncaught exceptions - try to flush before crashing
|
|
2534
1588
|
process.on('uncaughtException', async (error) => {
|
|
2535
1589
|
console.error('Uncaught exception:', error);
|
|
2536
1590
|
await shutdown('uncaughtException');
|
|
2537
1591
|
});
|
|
2538
|
-
// Handle unhandled promise rejections
|
|
2539
1592
|
process.on('unhandledRejection', async (reason) => {
|
|
2540
1593
|
console.error('Unhandled rejection:', reason);
|
|
2541
1594
|
await shutdown('unhandledRejection');
|