codeseeker 1.8.2 → 1.9.0

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