codeseeker 1.8.1 → 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 -153
- 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 +36 -99
- package/dist/mcp/mcp-server.d.ts.map +1 -1
- package/dist/mcp/mcp-server.js +1161 -1932
- 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
|
-
const duplicate_code_detector_1 = require("../cli/services/analysis/deduplication/duplicate-code-detector");
|
|
71
|
-
const knowledge_graph_1 = require("../cli/knowledge/graph/knowledge-graph");
|
|
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,1995 +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
|
-
}
|
|
437
|
-
}
|
|
438
|
-
// Check cache first (only for 'full' mode - exists mode is fast enough)
|
|
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
|
-
}
|
|
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);
|
|
456
434
|
}
|
|
457
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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);
|
|
787
560
|
try {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
const projectStore = storageManager.getProjectStore();
|
|
791
|
-
let projectPath;
|
|
792
|
-
if (project) {
|
|
793
|
-
const projects = await projectStore.list();
|
|
794
|
-
const found = projects.find(p => p.name === project ||
|
|
795
|
-
p.path === project ||
|
|
796
|
-
path.basename(p.path) === project);
|
|
797
|
-
projectPath = found?.path || process.cwd();
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
projectPath = process.cwd();
|
|
801
|
-
}
|
|
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
|
-
}
|
|
561
|
+
if (!fs.existsSync(absolutePath))
|
|
562
|
+
continue;
|
|
816
563
|
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
};
|
|
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
|
+
});
|
|
869
578
|
}
|
|
870
|
-
catch
|
|
871
|
-
|
|
872
|
-
content: [{
|
|
873
|
-
type: 'text',
|
|
874
|
-
text: this.formatErrorMessage('Get file context', error instanceof Error ? error : String(error), { projectPath: project }),
|
|
875
|
-
}],
|
|
876
|
-
isError: true,
|
|
877
|
-
};
|
|
579
|
+
catch {
|
|
580
|
+
continue;
|
|
878
581
|
}
|
|
879
|
-
}
|
|
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
|
+
};
|
|
880
595
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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).',
|
|
894
665
|
inputSchema: {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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)'),
|
|
899
673
|
relationship_types: zod_1.z.array(zod_1.z.enum([
|
|
900
674
|
'imports', 'exports', 'calls', 'extends', 'implements', 'contains', 'uses', 'depends_on'
|
|
901
|
-
])).optional().describe('Filter
|
|
902
|
-
direction: zod_1.z.enum(['in', 'out', 'both']).optional().default('both')
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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'),
|
|
906
687
|
},
|
|
907
|
-
}, async (
|
|
688
|
+
}, async (params) => {
|
|
908
689
|
try {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
if (found) {
|
|
921
|
-
projectId = found.id;
|
|
922
|
-
projectPath = found.path;
|
|
923
|
-
}
|
|
924
|
-
else {
|
|
925
|
-
projectPath = process.cwd();
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
else {
|
|
929
|
-
projectPath = process.cwd();
|
|
930
|
-
const projects = await projectStore.list();
|
|
931
|
-
const found = projects.find(p => p.path === projectPath ||
|
|
932
|
-
path.basename(p.path) === path.basename(projectPath));
|
|
933
|
-
if (found) {
|
|
934
|
-
projectId = found.id;
|
|
935
|
-
projectPath = found.path;
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
if (!projectId) {
|
|
939
|
-
return {
|
|
940
|
-
content: [{
|
|
941
|
-
type: 'text',
|
|
942
|
-
text: 'Project not indexed. Use index first.',
|
|
943
|
-
}],
|
|
944
|
-
isError: true,
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
// Determine seed file paths
|
|
948
|
-
let seedFilePaths = [];
|
|
949
|
-
if (query) {
|
|
950
|
-
// Use semantic search to find seed files
|
|
951
|
-
const searchResults = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
|
|
952
|
-
seedFilePaths = searchResults.slice(0, 5).map(r => r.file.replace(/\\/g, '/'));
|
|
953
|
-
}
|
|
954
|
-
else if (filepaths && filepaths.length > 0) {
|
|
955
|
-
seedFilePaths = filepaths.map(fp => fp.replace(/\\/g, '/'));
|
|
956
|
-
}
|
|
957
|
-
else if (filepath) {
|
|
958
|
-
seedFilePaths = [filepath.replace(/\\/g, '/')];
|
|
959
|
-
}
|
|
960
|
-
else {
|
|
961
|
-
return {
|
|
962
|
-
content: [{
|
|
963
|
-
type: 'text',
|
|
964
|
-
text: 'Please provide filepath, filepaths, or query to explore relationships.',
|
|
965
|
-
}],
|
|
966
|
-
isError: true,
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
// Find all nodes for this project and get graph stats
|
|
970
|
-
const allNodes = await graphStore.findNodes(projectId);
|
|
971
|
-
const graphStats = {
|
|
972
|
-
total_nodes: allNodes.length,
|
|
973
|
-
file_nodes: allNodes.filter(n => n.type === 'file').length,
|
|
974
|
-
class_nodes: allNodes.filter(n => n.type === 'class').length,
|
|
975
|
-
function_nodes: allNodes.filter(n => n.type === 'function' || n.type === 'method').length,
|
|
976
|
-
};
|
|
977
|
-
// Find starting nodes using flexible path matching (like CLI's GraphAnalysisService)
|
|
978
|
-
const startNodes = allNodes.filter(n => {
|
|
979
|
-
const normalizedNodePath = n.filePath.replace(/\\/g, '/');
|
|
980
|
-
const nodeRelativePath = n.properties?.relativePath?.replace(/\\/g, '/');
|
|
981
|
-
return seedFilePaths.some(seedPath => {
|
|
982
|
-
const normalizedSeedPath = seedPath.replace(/\\/g, '/');
|
|
983
|
-
return (
|
|
984
|
-
// Exact matches
|
|
985
|
-
normalizedNodePath === normalizedSeedPath ||
|
|
986
|
-
nodeRelativePath === normalizedSeedPath ||
|
|
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
|
-
});
|
|
995
|
-
});
|
|
996
|
-
if (startNodes.length === 0) {
|
|
997
|
-
// List available files in graph with relative paths
|
|
998
|
-
const fileNodes = allNodes.filter(n => n.type === 'file').slice(0, 15);
|
|
999
|
-
const availableFiles = fileNodes.map(n => {
|
|
1000
|
-
const relPath = n.properties?.relativePath;
|
|
1001
|
-
return relPath || path.relative(projectPath, n.filePath);
|
|
1002
|
-
});
|
|
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
|
-
}
|
|
1018
|
-
// Traverse relationships from all start nodes (Seed + Expand)
|
|
1019
|
-
const visitedNodes = new Map();
|
|
1020
|
-
const collectedEdges = [];
|
|
1021
|
-
let truncated = false;
|
|
1022
|
-
const traverse = async (nodeId, currentDepth) => {
|
|
1023
|
-
// Stop if we've reached max_nodes limit
|
|
1024
|
-
if (visitedNodes.size >= max_nodes) {
|
|
1025
|
-
truncated = true;
|
|
1026
|
-
return;
|
|
1027
|
-
}
|
|
1028
|
-
if (currentDepth > Math.min(depth, 3) || visitedNodes.has(nodeId))
|
|
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),
|
|
1039
|
-
});
|
|
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
|
-
}
|
|
1071
|
-
// Format output
|
|
1072
|
-
const nodes = Array.from(visitedNodes.values());
|
|
1073
|
-
const uniqueEdges = collectedEdges.filter((e, i, arr) => arr.findIndex(x => x.from === e.from && x.to === e.to && x.type === e.type) === i);
|
|
1074
|
-
// Create a summary
|
|
1075
|
-
const summary = {
|
|
1076
|
-
graph_stats: graphStats,
|
|
1077
|
-
seed_files: startNodes.map(n => ({
|
|
1078
|
-
name: n.name,
|
|
1079
|
-
type: n.type,
|
|
1080
|
-
file: n.properties?.relativePath || path.relative(projectPath, n.filePath),
|
|
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
|
-
};
|
|
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 };
|
|
1120
701
|
}
|
|
1121
|
-
return {
|
|
1122
|
-
content: [{
|
|
1123
|
-
type: 'text',
|
|
1124
|
-
text: JSON.stringify(summary, null, 2),
|
|
1125
|
-
}],
|
|
1126
|
-
};
|
|
1127
702
|
}
|
|
1128
703
|
catch (error) {
|
|
1129
704
|
return {
|
|
1130
|
-
content: [{
|
|
1131
|
-
type: 'text',
|
|
1132
|
-
text: this.formatErrorMessage('Get code relationships', error instanceof Error ? error : String(error), { projectPath: project }),
|
|
1133
|
-
}],
|
|
705
|
+
content: [{ type: 'text', text: this.formatErrorMessage('Analyze', error instanceof Error ? error : String(error), { projectPath: params.project }) }],
|
|
1134
706
|
isError: true,
|
|
1135
707
|
};
|
|
1136
708
|
}
|
|
1137
709
|
});
|
|
1138
710
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
-
};
|
|
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;
|
|
1191
724
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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;
|
|
1200
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
|
+
});
|
|
1201
779
|
});
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
-
};
|
|
1253
|
-
}
|
|
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 */ }
|
|
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;
|
|
1305
822
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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()
|
|
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);
|
|
1322
826
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
text: JSON.stringify({ error: message }, null, 2),
|
|
1329
|
-
}],
|
|
1330
|
-
isError: true,
|
|
1331
|
-
};
|
|
827
|
+
};
|
|
828
|
+
for (const startNode of startNodes) {
|
|
829
|
+
if (visitedNodes.size >= max_nodes) {
|
|
830
|
+
truncated = true;
|
|
831
|
+
break;
|
|
1332
832
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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) }] };
|
|
1339
863
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
|
|
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 };
|
|
904
|
+
};
|
|
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
|
+
],
|
|
915
|
+
});
|
|
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
|
+
}));
|
|
1347
933
|
return {
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
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) }],
|
|
1354
950
|
};
|
|
1355
951
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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
|
-
};
|
|
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.',
|
|
996
|
+
});
|
|
1415
997
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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
|
-
};
|
|
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',
|
|
1009
|
+
});
|
|
1455
1010
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
+
});
|
|
1465
1022
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
}
|
|
1492
|
-
else {
|
|
1493
|
-
chunksCreated += result.chunksCreated;
|
|
1494
|
-
filesProcessed++;
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1023
|
+
}
|
|
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)));
|
|
1032
|
+
}
|
|
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
|
+
});
|
|
1497
1048
|
}
|
|
1498
1049
|
}
|
|
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
1050
|
}
|
|
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
1051
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
+
};
|
|
1073
|
+
}
|
|
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);
|
|
1566
1095
|
try {
|
|
1567
|
-
|
|
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
|
-
};
|
|
1096
|
+
standardsContent = fs.readFileSync(standardsPath, 'utf-8');
|
|
1625
1097
|
}
|
|
1626
|
-
catch
|
|
1098
|
+
catch {
|
|
1627
1099
|
return {
|
|
1628
|
-
content: [{
|
|
1629
|
-
type: 'text',
|
|
1630
|
-
text: this.formatErrorMessage('Get coding standards', error instanceof Error ? error : String(error), { projectPath: project }),
|
|
1631
|
-
}],
|
|
1100
|
+
content: [{ type: 'text', text: 'No coding standards detected. Index the project first.' }],
|
|
1632
1101
|
isError: true,
|
|
1633
1102
|
};
|
|
1634
1103
|
}
|
|
1635
|
-
}
|
|
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) }] };
|
|
1636
1111
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
'
|
|
1645
|
-
'Example: install_parsers({project: "/path/to/project"}) to auto-detect and install. ' +
|
|
1646
|
-
'Example: install_parsers({languages: ["python", "java"]}) to install specific parsers.',
|
|
1112
|
+
// ============================================================
|
|
1113
|
+
// TOOL 3: index
|
|
1114
|
+
// Combines: index (init), sync, projects (status), install_parsers, exclude
|
|
1115
|
+
// ============================================================
|
|
1116
|
+
registerIndexTool() {
|
|
1117
|
+
this.server.registerTool('index', {
|
|
1118
|
+
description: 'Index management. Actions: "init" (index project), "sync" (update changed files), ' +
|
|
1119
|
+
'"status" (list projects), "parsers" (install language parsers), "exclude" (manage exclusions).',
|
|
1647
1120
|
inputSchema: {
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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'),
|
|
1651
1138
|
},
|
|
1652
|
-
}, async (
|
|
1139
|
+
}, async (params) => {
|
|
1653
1140
|
try {
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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
|
-
};
|
|
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 };
|
|
1738
1154
|
}
|
|
1739
|
-
// No arguments - show usage
|
|
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
|
-
};
|
|
1757
1155
|
}
|
|
1758
1156
|
catch (error) {
|
|
1759
1157
|
return {
|
|
1760
|
-
content: [{
|
|
1761
|
-
type: 'text',
|
|
1762
|
-
text: this.formatErrorMessage('Manage language support', error instanceof Error ? error : String(error), { projectPath: project }),
|
|
1763
|
-
}],
|
|
1158
|
+
content: [{ type: 'text', text: this.formatErrorMessage('Index', error instanceof Error ? error : String(error), { projectPath: params.path || params.project }) }],
|
|
1764
1159
|
isError: true,
|
|
1765
1160
|
};
|
|
1766
1161
|
}
|
|
1767
1162
|
});
|
|
1768
1163
|
}
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
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)) {
|
|
1791
1212
|
try {
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1213
|
+
fs.unlinkSync(codingStandardsPath);
|
|
1214
|
+
}
|
|
1215
|
+
catch { /* ignore */ }
|
|
1216
|
+
}
|
|
1217
|
+
this.startBackgroundIndexing(projectId, projectName, absolutePath, true);
|
|
1218
|
+
return {
|
|
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) }],
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
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));
|
|
1240
|
+
}
|
|
1241
|
+
if (!found && projects.length === 1) {
|
|
1242
|
+
found = projects[0];
|
|
1243
|
+
}
|
|
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') {
|
|
1256
|
+
return {
|
|
1257
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1258
|
+
status: 'already_indexing', project: found.name,
|
|
1259
|
+
progress: existingJob.progress,
|
|
1260
|
+
}, null, 2) }],
|
|
1815
1261
|
};
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
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
|
-
};
|
|
1262
|
+
}
|
|
1263
|
+
const codingStandardsPath = path.join(found.path, '.codeseeker', 'coding-standards.json');
|
|
1264
|
+
if (fs.existsSync(codingStandardsPath)) {
|
|
1265
|
+
try {
|
|
1266
|
+
fs.unlinkSync(codingStandardsPath);
|
|
1859
1267
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
if (this.matchesExclusionPattern(filePath, normalizedPattern)) {
|
|
1887
|
-
// Delete from vector store
|
|
1888
|
-
await vectorStore.delete(result.document.id);
|
|
1889
|
-
filesRemoved++;
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1268
|
+
catch { /* ignore */ }
|
|
1269
|
+
}
|
|
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;
|
|
1288
|
+
try {
|
|
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
|
-
|
|
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
|
+
};
|
|
2024
1371
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
`• 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
|
+
};
|
|
2038
1384
|
}
|
|
2039
|
-
if (
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
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
|
+
};
|
|
2045
1395
|
}
|
|
2046
|
-
if (
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
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
|
+
};
|
|
2052
1414
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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 };
|
|
2059
1424
|
}
|
|
2060
|
-
if (
|
|
2061
|
-
|
|
2062
|
-
return `${operation} failed: Project not indexed.\n\n` +
|
|
2063
|
-
`ACTION REQUIRED:\n` +
|
|
2064
|
-
`• First run: ${pathHint}\n` +
|
|
2065
|
-
`• Then retry your search\n` +
|
|
2066
|
-
`• 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 };
|
|
2067
1427
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
*/
|
|
2085
|
-
registerFindDuplicatesTool() {
|
|
2086
|
-
this.server.registerTool('find_duplicates', {
|
|
2087
|
-
description: '**FIND DUPLICATE CODE** - Detects duplicate and similar code patterns using semantic analysis. ' +
|
|
2088
|
-
'Finds: exact copies, semantically similar code (same logic, different names), and structurally similar patterns. ' +
|
|
2089
|
-
'Use when: cleaning up codebase, finding copy-paste code, reducing maintenance burden. ' +
|
|
2090
|
-
'Returns groups of duplicates with consolidation suggestions and estimated savings. ' +
|
|
2091
|
-
'**IMPORTANT**: Always pass the project parameter with your workspace root path.',
|
|
2092
|
-
inputSchema: {
|
|
2093
|
-
project: zod_1.z.string().describe('Project path - REQUIRED: the workspace root path to analyze'),
|
|
2094
|
-
similarity_threshold: zod_1.z.number().optional().default(0.80)
|
|
2095
|
-
.describe('Minimum similarity score (0.0-1.0) to consider as duplicate. Default: 0.80'),
|
|
2096
|
-
min_lines: zod_1.z.number().optional().default(5)
|
|
2097
|
-
.describe('Minimum lines in a code block to analyze. Default: 5'),
|
|
2098
|
-
include_types: zod_1.z.array(zod_1.z.enum(['function', 'class', 'method', 'block'])).optional()
|
|
2099
|
-
.describe('Types of code to analyze. Default: all types'),
|
|
2100
|
-
},
|
|
2101
|
-
}, async ({ project, similarity_threshold = 0.80, min_lines = 5, 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)) {
|
|
2102
1444
|
try {
|
|
2103
|
-
|
|
2104
|
-
const projectStore = storageManager.getProjectStore();
|
|
2105
|
-
const projects = await projectStore.list();
|
|
2106
|
-
// Find the project
|
|
2107
|
-
const projectRecord = projects.find(p => p.name === project ||
|
|
2108
|
-
p.path === project ||
|
|
2109
|
-
path.basename(p.path) === project ||
|
|
2110
|
-
path.resolve(project) === p.path);
|
|
2111
|
-
const projectPath = projectRecord?.path || path.resolve(project);
|
|
2112
|
-
// Verify project exists
|
|
2113
|
-
if (!fs.existsSync(projectPath)) {
|
|
2114
|
-
return {
|
|
2115
|
-
content: [{
|
|
2116
|
-
type: 'text',
|
|
2117
|
-
text: `Project path not found: ${projectPath}`,
|
|
2118
|
-
}],
|
|
2119
|
-
isError: true,
|
|
2120
|
-
};
|
|
2121
|
-
}
|
|
2122
|
-
// Run duplicate detection
|
|
2123
|
-
const detector = new duplicate_code_detector_1.DuplicateCodeDetector();
|
|
2124
|
-
const report = await detector.analyzeProject(projectPath, {
|
|
2125
|
-
semanticSimilarityThreshold: similarity_threshold,
|
|
2126
|
-
minimumChunkSize: min_lines,
|
|
2127
|
-
includeTypes: include_types || ['function', 'class', 'method', 'block'],
|
|
2128
|
-
});
|
|
2129
|
-
// Format results
|
|
2130
|
-
const duplicateGroups = report.duplicateGroups.slice(0, 20).map(group => ({
|
|
2131
|
-
type: group.type,
|
|
2132
|
-
similarity: `${(group.similarity * 100).toFixed(1)}%`,
|
|
2133
|
-
files_affected: group.estimatedSavings.filesAffected,
|
|
2134
|
-
lines_savable: group.estimatedSavings.linesReduced,
|
|
2135
|
-
suggestion: group.consolidationSuggestion,
|
|
2136
|
-
locations: group.chunks.map(c => ({
|
|
2137
|
-
file: c.filePath,
|
|
2138
|
-
lines: `${c.startLine}-${c.endLine}`,
|
|
2139
|
-
name: c.functionName || c.className || 'code block',
|
|
2140
|
-
})),
|
|
2141
|
-
}));
|
|
2142
|
-
return {
|
|
2143
|
-
content: [{
|
|
2144
|
-
type: 'text',
|
|
2145
|
-
text: JSON.stringify({
|
|
2146
|
-
project: path.basename(projectPath),
|
|
2147
|
-
summary: {
|
|
2148
|
-
total_chunks_analyzed: report.totalChunksAnalyzed,
|
|
2149
|
-
exact_duplicates: report.summary.exactDuplicates,
|
|
2150
|
-
semantic_duplicates: report.summary.semanticDuplicates,
|
|
2151
|
-
structural_duplicates: report.summary.structuralDuplicates,
|
|
2152
|
-
total_lines_affected: report.summary.totalLinesAffected,
|
|
2153
|
-
potential_lines_saved: report.summary.potentialSavings,
|
|
2154
|
-
},
|
|
2155
|
-
duplicate_groups: duplicateGroups,
|
|
2156
|
-
recommendations: report.recommendations,
|
|
2157
|
-
}, null, 2),
|
|
2158
|
-
}],
|
|
2159
|
-
};
|
|
1445
|
+
exclusions = JSON.parse(fs.readFileSync(exclusionsPath, 'utf-8'));
|
|
2160
1446
|
}
|
|
2161
|
-
catch
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
const projectRecord = projects.find(p => p.name === project ||
|
|
2194
|
-
p.path === project ||
|
|
2195
|
-
path.basename(p.path) === project ||
|
|
2196
|
-
path.resolve(project) === p.path);
|
|
2197
|
-
if (!projectRecord) {
|
|
2198
|
-
return {
|
|
2199
|
-
content: [{
|
|
2200
|
-
type: 'text',
|
|
2201
|
-
text: `Project not found or not indexed: ${project}\n\n` +
|
|
2202
|
-
`Use index({path: "${project}"}) to index the project first.`,
|
|
2203
|
-
}],
|
|
2204
|
-
isError: true,
|
|
2205
|
-
};
|
|
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++;
|
|
1478
|
+
}
|
|
2206
1479
|
}
|
|
2207
|
-
// Load the knowledge graph for this project
|
|
2208
|
-
const knowledgeGraph = new knowledge_graph_1.SemanticKnowledgeGraph(projectRecord.path);
|
|
2209
|
-
// Run architectural insight detection (includes dead code, god classes, circular deps, etc.)
|
|
2210
|
-
const allInsights = await knowledgeGraph.detectArchitecturalInsights();
|
|
2211
|
-
// Filter by requested patterns
|
|
2212
|
-
const patterns = include_patterns || ['dead_code', 'god_class', 'circular_deps', 'feature_envy', 'coupling'];
|
|
2213
|
-
const patternMapping = {
|
|
2214
|
-
'dead_code': ['Dead Code'],
|
|
2215
|
-
'god_class': ['God Class'],
|
|
2216
|
-
'circular_deps': ['Circular Dependencies', 'Circular Dependency'],
|
|
2217
|
-
'feature_envy': ['Feature Envy'],
|
|
2218
|
-
'coupling': ['High Coupling', 'Inappropriate Intimacy'],
|
|
2219
|
-
};
|
|
2220
|
-
const selectedPatterns = patterns.flatMap(p => patternMapping[p] || []);
|
|
2221
|
-
const filteredInsights = allInsights.filter(insight => selectedPatterns.some(pattern => insight.pattern?.toLowerCase().includes(pattern.toLowerCase()) ||
|
|
2222
|
-
insight.description?.toLowerCase().includes(pattern.toLowerCase())));
|
|
2223
|
-
// Group by type
|
|
2224
|
-
const deadCode = filteredInsights.filter(i => i.pattern === 'Dead Code');
|
|
2225
|
-
const antiPatterns = filteredInsights.filter(i => i.type === 'anti_pattern' && i.pattern !== 'Dead Code');
|
|
2226
|
-
const couplingIssues = filteredInsights.filter(i => i.type === 'coupling_issue');
|
|
2227
|
-
return {
|
|
2228
|
-
content: [{
|
|
2229
|
-
type: 'text',
|
|
2230
|
-
text: JSON.stringify({
|
|
2231
|
-
project: projectRecord.name,
|
|
2232
|
-
summary: {
|
|
2233
|
-
total_issues: filteredInsights.length,
|
|
2234
|
-
dead_code_count: deadCode.length,
|
|
2235
|
-
anti_patterns_count: antiPatterns.length,
|
|
2236
|
-
coupling_issues_count: couplingIssues.length,
|
|
2237
|
-
},
|
|
2238
|
-
dead_code: deadCode.slice(0, 20).map(d => ({
|
|
2239
|
-
type: d.pattern,
|
|
2240
|
-
description: d.description,
|
|
2241
|
-
confidence: `${((d.confidence || 0) * 100).toFixed(0)}%`,
|
|
2242
|
-
impact: d.impact,
|
|
2243
|
-
recommendation: d.recommendation,
|
|
2244
|
-
})),
|
|
2245
|
-
anti_patterns: antiPatterns.slice(0, 10).map(a => ({
|
|
2246
|
-
type: a.pattern,
|
|
2247
|
-
description: a.description,
|
|
2248
|
-
confidence: `${((a.confidence || 0) * 100).toFixed(0)}%`,
|
|
2249
|
-
impact: a.impact,
|
|
2250
|
-
recommendation: a.recommendation,
|
|
2251
|
-
})),
|
|
2252
|
-
coupling_issues: couplingIssues.slice(0, 10).map(c => ({
|
|
2253
|
-
type: c.pattern,
|
|
2254
|
-
description: c.description,
|
|
2255
|
-
confidence: `${((c.confidence || 0) * 100).toFixed(0)}%`,
|
|
2256
|
-
impact: c.impact,
|
|
2257
|
-
recommendation: c.recommendation,
|
|
2258
|
-
})),
|
|
2259
|
-
}, null, 2),
|
|
2260
|
-
}],
|
|
2261
|
-
};
|
|
2262
1480
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
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);
|
|
1503
|
+
}
|
|
1504
|
+
else {
|
|
1505
|
+
notFound.push(normalizedPattern);
|
|
1506
|
+
}
|
|
2271
1507
|
}
|
|
2272
|
-
|
|
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 };
|
|
2273
1521
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
1522
|
+
// ============================================================
|
|
1523
|
+
// SERVER LIFECYCLE
|
|
1524
|
+
// ============================================================
|
|
2277
1525
|
async start() {
|
|
2278
|
-
// Use stderr for logging since stdout is for JSON-RPC
|
|
2279
1526
|
console.error('Starting CodeSeeker MCP server...');
|
|
2280
|
-
// Initialize storage manager first to ensure singleton is ready
|
|
2281
1527
|
const storageManager = await (0, storage_1.getStorageManager)();
|
|
2282
1528
|
console.error(`Storage mode: ${storageManager.getMode()}`);
|
|
2283
1529
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
2284
1530
|
await this.server.connect(transport);
|
|
2285
1531
|
console.error('CodeSeeker MCP server running on stdio');
|
|
2286
1532
|
}
|
|
2287
|
-
/**
|
|
2288
|
-
* Graceful shutdown - flush and close all storage before exit
|
|
2289
|
-
*/
|
|
2290
1533
|
async shutdown() {
|
|
2291
1534
|
console.error('Shutting down CodeSeeker MCP server...');
|
|
2292
1535
|
try {
|
|
2293
1536
|
const storageManager = await (0, storage_1.getStorageManager)();
|
|
2294
|
-
// Flush first to ensure data is saved
|
|
2295
1537
|
await storageManager.flushAll();
|
|
2296
1538
|
console.error('Storage flushed successfully');
|
|
2297
|
-
// Close to stop interval timers and release resources
|
|
2298
1539
|
await storageManager.closeAll();
|
|
2299
1540
|
console.error('Storage closed successfully');
|
|
2300
1541
|
}
|
|
@@ -2310,16 +1551,13 @@ exports.CodeSeekerMcpServer = CodeSeekerMcpServer;
|
|
|
2310
1551
|
async function startMcpServer() {
|
|
2311
1552
|
const server = new CodeSeekerMcpServer();
|
|
2312
1553
|
let isShuttingDown = false;
|
|
2313
|
-
// Register signal handlers for graceful shutdown
|
|
2314
1554
|
const shutdown = async (signal) => {
|
|
2315
|
-
// Prevent multiple shutdown attempts
|
|
2316
1555
|
if (isShuttingDown) {
|
|
2317
1556
|
console.error(`Already shutting down, ignoring ${signal}`);
|
|
2318
1557
|
return;
|
|
2319
1558
|
}
|
|
2320
1559
|
isShuttingDown = true;
|
|
2321
1560
|
console.error(`\nReceived ${signal}, shutting down gracefully...`);
|
|
2322
|
-
// Set a hard timeout to force exit if shutdown takes too long
|
|
2323
1561
|
const forceExitTimeout = setTimeout(() => {
|
|
2324
1562
|
console.error('Shutdown timeout, forcing exit...');
|
|
2325
1563
|
process.exit(1);
|
|
@@ -2337,29 +1575,20 @@ async function startMcpServer() {
|
|
|
2337
1575
|
};
|
|
2338
1576
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
2339
1577
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
2340
|
-
// CRITICAL: Handle stdin close - this is how MCP clients signal disconnect
|
|
2341
|
-
// On Windows, signals are unreliable, so stdin close is the primary shutdown mechanism
|
|
2342
1578
|
process.stdin.on('close', () => shutdown('stdin-close'));
|
|
2343
1579
|
process.stdin.on('end', () => shutdown('stdin-end'));
|
|
2344
|
-
// Also handle stdin errors (broken pipe, etc.)
|
|
2345
1580
|
process.stdin.on('error', (err) => {
|
|
2346
|
-
// EPIPE and similar errors mean the parent process disconnected
|
|
2347
1581
|
console.error(`stdin error: ${err.message}`);
|
|
2348
1582
|
shutdown('stdin-error');
|
|
2349
1583
|
});
|
|
2350
|
-
// Handle Windows-specific signals
|
|
2351
1584
|
if (process.platform === 'win32') {
|
|
2352
1585
|
process.on('SIGHUP', () => shutdown('SIGHUP'));
|
|
2353
|
-
// Windows: also listen for parent process disconnect via stdin
|
|
2354
|
-
// Resume stdin to ensure we receive close/end events
|
|
2355
1586
|
process.stdin.resume();
|
|
2356
1587
|
}
|
|
2357
|
-
// Handle uncaught exceptions - try to flush before crashing
|
|
2358
1588
|
process.on('uncaughtException', async (error) => {
|
|
2359
1589
|
console.error('Uncaught exception:', error);
|
|
2360
1590
|
await shutdown('uncaughtException');
|
|
2361
1591
|
});
|
|
2362
|
-
// Handle unhandled promise rejections
|
|
2363
1592
|
process.on('unhandledRejection', async (reason) => {
|
|
2364
1593
|
console.error('Unhandled rejection:', reason);
|
|
2365
1594
|
await shutdown('unhandledRejection');
|