codeseeker 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,16 @@
1
1
  "use strict";
2
2
  /**
3
- * CodeSeeker MCP Server
3
+ * CodeSeeker MCP Server (Consolidated)
4
4
  *
5
5
  * Exposes CodeSeeker's semantic search and code analysis capabilities
6
6
  * as an MCP (Model Context Protocol) server for use with Claude Desktop
7
7
  * and Claude Code.
8
8
  *
9
+ * OPTIMIZED: 12 tools consolidated to 3 to minimize token usage:
10
+ * 1. search - Code discovery (search, search+read, read-with-context)
11
+ * 2. analyze - Code analysis (dependencies, dead_code, duplicates, standards)
12
+ * 3. index - Index management (init, sync, status, parsers, exclude)
13
+ *
9
14
  * Usage:
10
15
  * codeseeker serve --mcp
11
16
  *
@@ -67,12 +72,15 @@ const indexing_service_1 = require("./indexing-service");
67
72
  const coding_standards_generator_1 = require("../cli/services/analysis/coding-standards-generator");
68
73
  const language_support_service_1 = require("../cli/services/project/language-support-service");
69
74
  const query_cache_service_1 = require("./query-cache-service");
70
- const duplicate_code_detector_1 = require("../cli/services/analysis/deduplication/duplicate-code-detector");
71
- const knowledge_graph_1 = require("../cli/knowledge/graph/knowledge-graph");
72
75
  // Version from package.json
73
76
  const VERSION = '2.0.0';
74
77
  /**
75
- * MCP Server for CodeSeeker
78
+ * MCP Server for CodeSeeker - Consolidated 3-tool architecture
79
+ *
80
+ * Tools:
81
+ * 1. search - Semantic code search with optional file reading
82
+ * 2. analyze - Code analysis (dependencies, dead_code, duplicates, standards)
83
+ * 3. index - Index management (init, sync, status, parsers, exclude)
76
84
  */
77
85
  class CodeSeekerMcpServer {
78
86
  server;
@@ -82,20 +90,15 @@ class CodeSeekerMcpServer {
82
90
  queryCache;
83
91
  // Background indexing state
84
92
  indexingJobs = new Map();
85
- // Mutex for concurrent indexing protection
86
93
  indexingMutex = new Set();
87
- // Cancellation tokens for running indexing jobs
88
94
  cancellationTokens = new Map();
89
95
  // Job cleanup interval (clean completed/failed jobs after 1 hour)
90
- JOB_TTL_MS = 60 * 60 * 1000; // 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,1995 +261,1281 @@ class CodeSeekerMcpServer {
306
261
  }
307
262
  currentPath = path.dirname(currentPath);
308
263
  }
309
- // No project found, return original path
310
264
  return startPath;
311
265
  }
266
+ generateProjectId(projectPath) {
267
+ return crypto.createHash('md5').update(projectPath).digest('hex');
268
+ }
269
+ async getAllProjectDocuments(vectorStore, projectId) {
270
+ const randomEmbedding = Array.from({ length: 384 }, () => Math.random() - 0.5);
271
+ const results = await vectorStore.searchByVector(randomEmbedding, projectId, 10000);
272
+ return results.map(r => r.document);
273
+ }
274
+ formatErrorMessage(operation, error, context) {
275
+ const message = error instanceof Error ? error.message : String(error);
276
+ const lowerMessage = message.toLowerCase();
277
+ if (lowerMessage.includes('enoent') || lowerMessage.includes('not found') || lowerMessage.includes('no such file')) {
278
+ return `${operation} failed: File or directory not found. Verify: ${context?.projectPath || 'the specified path'}`;
279
+ }
280
+ if (lowerMessage.includes('eacces') || lowerMessage.includes('permission denied')) {
281
+ return `${operation} failed: Permission denied.`;
282
+ }
283
+ if (lowerMessage.includes('timeout') || lowerMessage.includes('timed out')) {
284
+ return `${operation} failed: Timed out. Check status with index({action: "status"}).`;
285
+ }
286
+ if (lowerMessage.includes('connection') || lowerMessage.includes('econnrefused') || lowerMessage.includes('network')) {
287
+ return `${operation} failed: Connection error. Check storage services.`;
288
+ }
289
+ if (lowerMessage.includes('not indexed') || lowerMessage.includes('no project')) {
290
+ const pathHint = context?.projectPath ? `index({action: "init", path: "${context.projectPath}"})` : 'index({action: "init", path: "/path/to/project"})';
291
+ return `${operation} failed: Project not indexed. Run: ${pathHint}`;
292
+ }
293
+ if (lowerMessage.includes('out of memory') || lowerMessage.includes('heap')) {
294
+ return `${operation} failed: Out of memory. Use index({action: "exclude"}) to skip large directories.`;
295
+ }
296
+ return `${operation} failed: ${message}`;
297
+ }
298
+ matchesExclusionPattern(filePath, pattern) {
299
+ const normalizedPath = filePath.replace(/\\/g, '/').toLowerCase();
300
+ const normalizedPattern = pattern.replace(/\\/g, '/').toLowerCase();
301
+ if (normalizedPath === normalizedPattern)
302
+ return true;
303
+ let regexPattern = normalizedPattern
304
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
305
+ .replace(/\*\*/g, '<<GLOBSTAR>>')
306
+ .replace(/\*/g, '[^/]*')
307
+ .replace(/<<GLOBSTAR>>/g, '.*')
308
+ .replace(/\?/g, '.');
309
+ if (!regexPattern.startsWith('.*')) {
310
+ regexPattern = `(^|/)${regexPattern}`;
311
+ }
312
+ regexPattern = `${regexPattern}(/.*)?$`;
313
+ try {
314
+ return new RegExp(regexPattern).test(normalizedPath);
315
+ }
316
+ catch {
317
+ return normalizedPath.includes(normalizedPattern.replace(/\*/g, ''));
318
+ }
319
+ }
320
+ _getIndexingStatusForProject(projectId) {
321
+ const job = this.indexingJobs.get(projectId);
322
+ if (!job)
323
+ return null;
324
+ return {
325
+ indexing_status: job.status,
326
+ indexing_progress: job.progress,
327
+ indexing_result: job.result,
328
+ indexing_error: job.error,
329
+ indexing_started: job.startedAt.toISOString(),
330
+ indexing_completed: job.completedAt?.toISOString(),
331
+ };
332
+ }
333
+ /**
334
+ * Resolve project from name/path, returning project record and path.
335
+ * Shared helper for search and analyze tools.
336
+ */
337
+ async resolveProject(project) {
338
+ const storageManager = await (0, storage_1.getStorageManager)();
339
+ const projectStore = storageManager.getProjectStore();
340
+ const projects = await projectStore.list();
341
+ if (project) {
342
+ const found = projects.find(p => p.name === project ||
343
+ p.path === project ||
344
+ path.basename(p.path) === project ||
345
+ path.resolve(project) === p.path);
346
+ if (found) {
347
+ return { projectPath: found.path, projectRecord: found };
348
+ }
349
+ return { projectPath: await this.findProjectPath(path.resolve(project)) };
350
+ }
351
+ if (projects.length === 0) {
352
+ return {
353
+ projectPath: '',
354
+ error: {
355
+ content: [{ type: 'text', text: 'No indexed projects. Use index({action: "init", path: "/path/to/project"}) first.' }],
356
+ isError: true,
357
+ },
358
+ };
359
+ }
360
+ if (projects.length === 1) {
361
+ return { projectPath: projects[0].path, projectRecord: projects[0] };
362
+ }
363
+ const projectList = projects.map(p => ` - "${p.name}" (${p.path})`).join('\n');
364
+ return {
365
+ projectPath: '',
366
+ error: {
367
+ content: [{ type: 'text', text: `Multiple projects indexed. Specify project parameter:\n\n${projectList}` }],
368
+ isError: true,
369
+ },
370
+ };
371
+ }
312
372
  /**
313
- * 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
- }
437
- }
438
- // Check cache first (only for 'full' mode - exists mode is fast enough)
439
- let results;
440
- let fromCache = false;
441
- const cacheProjectId = projectRecord?.id || this.generateProjectId(projectPath);
442
- if (mode === 'full') {
443
- const cached = await this.queryCache.get(query, cacheProjectId, search_type);
444
- if (cached) {
445
- results = cached.results;
446
- fromCache = true;
447
- }
448
- else {
449
- // Perform actual search
450
- results = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
451
- // Cache results for future queries
452
- if (results.length > 0) {
453
- await this.queryCache.set(query, cacheProjectId, results, search_type);
454
- }
455
- }
431
+ // Dispatch: filepath → read-with-context, read → search-and-read, else → search
432
+ if (filepath) {
433
+ return await this.handleReadWithContext(filepath, project, !read ? true : read);
456
434
  }
457
- 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.',
780
- 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)'),
785
- },
786
- }, async ({ filepath, include_related = true, project }) => {
530
+ async handleSearchAndRead(query, project, max_files, max_lines) {
531
+ const fileLimit = Math.min(max_files, 3);
532
+ const lineLimit = Math.min(max_lines, 1000);
533
+ const { projectPath, projectRecord, error } = await this.resolveProject(project);
534
+ if (error)
535
+ return error;
536
+ const indexCheck = await this.verifyIndexed(projectPath, projectRecord);
537
+ if (indexCheck.error)
538
+ return indexCheck.error;
539
+ const results = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
540
+ if (results.length === 0) {
541
+ return {
542
+ content: [{ type: 'text', text: JSON.stringify({ query, project: projectPath, found: false, message: 'No matching code found.' }, null, 2) }],
543
+ };
544
+ }
545
+ // Get unique files
546
+ const seenFiles = new Set();
547
+ const uniqueResults = [];
548
+ for (const r of results) {
549
+ const normalizedPath = r.file.replace(/\\/g, '/');
550
+ if (!seenFiles.has(normalizedPath)) {
551
+ seenFiles.add(normalizedPath);
552
+ uniqueResults.push(r);
553
+ if (uniqueResults.length >= fileLimit)
554
+ break;
555
+ }
556
+ }
557
+ const files = [];
558
+ for (const result of uniqueResults) {
559
+ const absolutePath = path.isAbsolute(result.file) ? result.file : path.join(projectPath, result.file);
787
560
  try {
788
- // 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();
801
- }
802
- // Resolve file path
803
- const absolutePath = path.isAbsolute(filepath)
804
- ? filepath
805
- : path.join(projectPath, filepath);
806
- // Read file content
807
- if (!fs.existsSync(absolutePath)) {
808
- return {
809
- content: [{
810
- type: 'text',
811
- text: `File not found: ${absolutePath}`,
812
- }],
813
- isError: true,
814
- };
815
- }
561
+ if (!fs.existsSync(absolutePath))
562
+ continue;
816
563
  const content = fs.readFileSync(absolutePath, 'utf-8');
817
- // 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
- };
564
+ const lines = content.split('\n');
565
+ const truncated = lines.length > lineLimit;
566
+ const displayLines = truncated ? lines.slice(0, lineLimit) : lines;
567
+ const numberedContent = displayLines.map((line, i) => `${String(i + 1).padStart(4)}│ ${line}`).join('\n');
568
+ files.push({
569
+ file: absolutePath,
570
+ relative_path: result.file,
571
+ score: Math.round(result.similarity * 100) / 100,
572
+ file_type: result.type,
573
+ match_source: result.debug?.matchSource || 'hybrid',
574
+ line_count: lines.length,
575
+ content: numberedContent + (truncated ? `\n... (truncated at ${lineLimit} lines)` : ''),
576
+ truncated,
577
+ });
869
578
  }
870
- catch (error) {
871
- return {
872
- content: [{
873
- type: 'text',
874
- text: this.formatErrorMessage('Get file context', error instanceof Error ? error : String(error), { projectPath: project }),
875
- }],
876
- isError: true,
877
- };
579
+ catch {
580
+ continue;
878
581
  }
879
- });
582
+ }
583
+ if (files.length === 0) {
584
+ return {
585
+ content: [{ type: 'text', text: JSON.stringify({ query, project: projectPath, found: true, readable: false, message: 'Found matching files but could not read them.' }, null, 2) }],
586
+ };
587
+ }
588
+ return {
589
+ content: [{ type: 'text', text: JSON.stringify({
590
+ query, project: projectPath,
591
+ files_found: results.length, files_returned: files.length,
592
+ results: files,
593
+ }, null, 2) }],
594
+ };
880
595
  }
881
- /**
882
- * 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.',
596
+ async handleReadWithContext(filepath, project, include_related) {
597
+ const storageManager = await (0, storage_1.getStorageManager)();
598
+ const projectStore = storageManager.getProjectStore();
599
+ let projectPath;
600
+ if (project) {
601
+ const projects = await projectStore.list();
602
+ const found = projects.find(p => p.name === project || p.path === project || path.basename(p.path) === project);
603
+ projectPath = found?.path || process.cwd();
604
+ }
605
+ else {
606
+ projectPath = process.cwd();
607
+ }
608
+ const absolutePath = path.isAbsolute(filepath) ? filepath : path.join(projectPath, filepath);
609
+ if (!fs.existsSync(absolutePath)) {
610
+ return { content: [{ type: 'text', text: `File not found: ${absolutePath}` }], isError: true };
611
+ }
612
+ const content = fs.readFileSync(absolutePath, 'utf-8');
613
+ let relatedChunks = [];
614
+ if (include_related) {
615
+ const lines = content.split('\n');
616
+ const meaningfulLines = [];
617
+ for (const line of lines) {
618
+ const trimmed = line.trim();
619
+ if (!trimmed)
620
+ continue;
621
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))
622
+ continue;
623
+ if (trimmed.startsWith('import ') || trimmed.startsWith('from ') || trimmed.startsWith('require('))
624
+ continue;
625
+ if (trimmed.startsWith('#') && !trimmed.startsWith('##'))
626
+ continue;
627
+ if (trimmed.startsWith('using ') || trimmed.startsWith('namespace '))
628
+ continue;
629
+ meaningfulLines.push(trimmed);
630
+ if (meaningfulLines.length >= 5)
631
+ break;
632
+ }
633
+ const fileName = path.basename(filepath);
634
+ const fileNameQuery = fileName.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
635
+ const contentQuery = meaningfulLines.join(' ').substring(0, 200);
636
+ const searchQuery = `${fileNameQuery} ${contentQuery}`.trim();
637
+ const results = await this.searchOrchestrator.performSemanticSearch(searchQuery || fileNameQuery, projectPath);
638
+ relatedChunks = results
639
+ .filter(r => !r.file.endsWith(path.basename(filepath)))
640
+ .slice(0, 5)
641
+ .map(r => ({
642
+ file: r.file,
643
+ chunk: r.content.substring(0, 300) + (r.content.length > 300 ? '...' : ''),
644
+ score: Math.round(r.similarity * 100) / 100,
645
+ }));
646
+ }
647
+ return {
648
+ content: [{ type: 'text', text: JSON.stringify({
649
+ filepath: path.relative(projectPath, absolutePath),
650
+ content: content.length > 10000 ? content.substring(0, 10000) + '\n... (truncated)' : content,
651
+ line_count: content.split('\n').length,
652
+ related_chunks: include_related ? relatedChunks : undefined,
653
+ }, null, 2) }],
654
+ };
655
+ }
656
+ // ============================================================
657
+ // TOOL 2: analyze
658
+ // Combines: show_dependencies, find_duplicates, find_dead_code, standards
659
+ // ============================================================
660
+ registerAnalyzeTool() {
661
+ this.server.registerTool('analyze', {
662
+ description: 'Code analysis. Actions: "dependencies" (imports/calls/extends graph), ' +
663
+ '"dead_code" (unused code, anti-patterns), "duplicates" (similar code), ' +
664
+ '"standards" (auto-detected coding patterns).',
894
665
  inputSchema: {
895
- 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.'),
666
+ action: zod_1.z.enum(['dependencies', 'dead_code', 'duplicates', 'standards']).describe('Analysis type'),
667
+ project: zod_1.z.string().describe('Project path or name'),
668
+ // dependencies params
669
+ filepath: zod_1.z.string().optional().describe('File for dependency analysis'),
670
+ filepaths: zod_1.z.array(zod_1.z.string()).optional().describe('Multiple files for dependency analysis'),
671
+ query: zod_1.z.string().optional().describe('Search query to find seed files for dependencies'),
672
+ depth: zod_1.z.number().optional().default(1).describe('Relationship hops (1-3)'),
899
673
  relationship_types: zod_1.z.array(zod_1.z.enum([
900
674
  'imports', 'exports', 'calls', 'extends', 'implements', 'contains', 'uses', 'depends_on'
901
- ])).optional().describe('Filter 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'),
675
+ ])).optional().describe('Filter relationship types'),
676
+ direction: zod_1.z.enum(['in', 'out', 'both']).optional().default('both').describe('Relationship direction'),
677
+ max_nodes: zod_1.z.number().optional().default(50).describe('Max nodes'),
678
+ // duplicates params
679
+ similarity_threshold: zod_1.z.number().optional().default(0.80).describe('Min similarity for duplicates (0-1)'),
680
+ min_lines: zod_1.z.number().optional().default(5).describe('Min lines for duplicate analysis'),
681
+ // dead_code params
682
+ include_patterns: zod_1.z.array(zod_1.z.enum(['dead_code', 'god_class', 'circular_deps', 'feature_envy', 'coupling'])).optional()
683
+ .describe('Anti-patterns to detect'),
684
+ // standards params
685
+ category: zod_1.z.enum(['validation', 'error-handling', 'logging', 'testing', 'all']).optional().default('all')
686
+ .describe('Standards category'),
906
687
  },
907
- }, async ({ filepath, filepaths, query, depth = 1, relationship_types, direction = 'both', max_nodes = 50, project }) => {
688
+ }, async (params) => {
908
689
  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
- }
937
- }
938
- if (!projectId) {
939
- return {
940
- content: [{
941
- type: 'text',
942
- text: 'Project not indexed. Use index first.',
943
- }],
944
- isError: true,
945
- };
946
- }
947
- // Determine seed file paths
948
- let seedFilePaths = [];
949
- if (query) {
950
- // Use semantic search to find seed files
951
- const searchResults = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
952
- seedFilePaths = searchResults.slice(0, 5).map(r => r.file.replace(/\\/g, '/'));
953
- }
954
- else if (filepaths && filepaths.length > 0) {
955
- seedFilePaths = filepaths.map(fp => fp.replace(/\\/g, '/'));
956
- }
957
- else if (filepath) {
958
- seedFilePaths = [filepath.replace(/\\/g, '/')];
959
- }
960
- else {
961
- return {
962
- content: [{
963
- type: 'text',
964
- text: 'Please provide filepath, filepaths, or query to explore relationships.',
965
- }],
966
- isError: true,
967
- };
968
- }
969
- // Find all nodes for this project and get graph stats
970
- const allNodes = await graphStore.findNodes(projectId);
971
- const graphStats = {
972
- total_nodes: allNodes.length,
973
- file_nodes: allNodes.filter(n => n.type === 'file').length,
974
- class_nodes: allNodes.filter(n => n.type === 'class').length,
975
- function_nodes: allNodes.filter(n => n.type === 'function' || n.type === 'method').length,
976
- };
977
- // Find starting nodes using flexible path matching (like CLI's GraphAnalysisService)
978
- const startNodes = allNodes.filter(n => {
979
- const normalizedNodePath = n.filePath.replace(/\\/g, '/');
980
- const nodeRelativePath = n.properties?.relativePath?.replace(/\\/g, '/');
981
- return seedFilePaths.some(seedPath => {
982
- const normalizedSeedPath = seedPath.replace(/\\/g, '/');
983
- return (
984
- // Exact matches
985
- normalizedNodePath === normalizedSeedPath ||
986
- nodeRelativePath === normalizedSeedPath ||
987
- // Ends with (for relative paths)
988
- normalizedNodePath.endsWith(normalizedSeedPath) ||
989
- normalizedNodePath.endsWith('/' + normalizedSeedPath) ||
990
- // Contains match (for partial paths)
991
- normalizedNodePath.includes('/' + normalizedSeedPath) ||
992
- // Name match (for class/function names)
993
- n.name === path.basename(normalizedSeedPath).replace(/\.[^.]+$/, ''));
994
- });
995
- });
996
- if (startNodes.length === 0) {
997
- // List available files in graph with relative paths
998
- const fileNodes = allNodes.filter(n => n.type === 'file').slice(0, 15);
999
- const availableFiles = fileNodes.map(n => {
1000
- const relPath = n.properties?.relativePath;
1001
- return relPath || path.relative(projectPath, n.filePath);
1002
- });
1003
- return {
1004
- content: [{
1005
- type: 'text',
1006
- text: JSON.stringify({
1007
- error: `No graph nodes found for: ${seedFilePaths.join(', ')}`,
1008
- suggestion: query
1009
- ? 'The semantic search found files but they are not in the knowledge graph. Try re-indexing.'
1010
- : 'The file(s) may not be indexed in the knowledge graph.',
1011
- available_files: availableFiles,
1012
- tip: 'Use relative paths like "src/mcp/mcp-server.ts" or a query like "authentication middleware"',
1013
- }, null, 2),
1014
- }],
1015
- isError: true,
1016
- };
1017
- }
1018
- // Traverse relationships from all start nodes (Seed + Expand)
1019
- const visitedNodes = new Map();
1020
- const collectedEdges = [];
1021
- let truncated = false;
1022
- const traverse = async (nodeId, currentDepth) => {
1023
- // Stop if we've reached max_nodes limit
1024
- if (visitedNodes.size >= max_nodes) {
1025
- truncated = true;
1026
- return;
1027
- }
1028
- if (currentDepth > Math.min(depth, 3) || visitedNodes.has(nodeId))
1029
- return;
1030
- const node = await graphStore.getNode(nodeId);
1031
- if (!node)
1032
- return;
1033
- const relPath = node.properties?.relativePath;
1034
- visitedNodes.set(nodeId, {
1035
- id: node.id,
1036
- type: node.type,
1037
- name: node.name,
1038
- file: relPath || path.relative(projectPath, node.filePath),
1039
- });
1040
- // Get edges based on direction
1041
- const edges = await graphStore.getEdges(nodeId, direction);
1042
- for (const edge of edges) {
1043
- // Stop if we've reached max_nodes limit
1044
- if (visitedNodes.size >= max_nodes) {
1045
- truncated = true;
1046
- return;
1047
- }
1048
- // Filter by relationship type if specified
1049
- if (relationship_types && relationship_types.length > 0) {
1050
- if (!relationship_types.includes(edge.type))
1051
- continue;
1052
- }
1053
- collectedEdges.push({
1054
- from: edge.source,
1055
- to: edge.target,
1056
- type: edge.type,
1057
- });
1058
- // Continue traversal
1059
- const nextNodeId = edge.source === nodeId ? edge.target : edge.source;
1060
- await traverse(nextNodeId, currentDepth + 1);
1061
- }
1062
- };
1063
- // Traverse from ALL start nodes (multiple seeds)
1064
- for (const startNode of startNodes) {
1065
- if (visitedNodes.size >= max_nodes) {
1066
- truncated = true;
1067
- break;
1068
- }
1069
- await traverse(startNode.id, 1);
1070
- }
1071
- // Format output
1072
- const nodes = Array.from(visitedNodes.values());
1073
- const uniqueEdges = collectedEdges.filter((e, i, arr) => arr.findIndex(x => x.from === e.from && x.to === e.to && x.type === e.type) === i);
1074
- // Create a summary
1075
- const summary = {
1076
- graph_stats: graphStats,
1077
- seed_files: startNodes.map(n => ({
1078
- name: n.name,
1079
- type: n.type,
1080
- file: n.properties?.relativePath || path.relative(projectPath, n.filePath),
1081
- })),
1082
- traversal: {
1083
- depth_requested: depth,
1084
- direction,
1085
- relationship_filters: relationship_types || 'all',
1086
- seed_method: query ? 'semantic_search' : (filepaths ? 'multiple_files' : 'single_file'),
1087
- max_nodes,
1088
- },
1089
- results: {
1090
- seed_nodes: startNodes.length,
1091
- nodes_found: nodes.length,
1092
- relationships_found: uniqueEdges.length,
1093
- truncated,
1094
- },
1095
- nodes: nodes.map(n => ({
1096
- name: n.name,
1097
- type: n.type,
1098
- file: n.file,
1099
- })),
1100
- relationships: uniqueEdges.map(e => {
1101
- const fromNode = visitedNodes.get(e.from);
1102
- const toNode = visitedNodes.get(e.to);
1103
- return {
1104
- type: e.type,
1105
- from: fromNode?.name || e.from,
1106
- to: toNode?.name || e.to,
1107
- };
1108
- }),
1109
- };
1110
- // Add truncation warning and recommendations if results were limited
1111
- if (truncated) {
1112
- summary.truncated_warning = {
1113
- message: `Results truncated at ${max_nodes} nodes.`,
1114
- recommendations: [
1115
- relationship_types ? null : 'Add relationship_types filter (e.g., ["imports"])',
1116
- depth > 1 ? 'Reduce depth to 1' : null,
1117
- `Increase max_nodes (current: ${max_nodes})`,
1118
- ].filter(Boolean),
1119
- };
690
+ switch (params.action) {
691
+ case 'dependencies':
692
+ return await this.handleShowDependencies(params);
693
+ case 'dead_code':
694
+ return await this.handleFindDeadCode(params);
695
+ case 'duplicates':
696
+ return await this.handleFindDuplicates(params);
697
+ case 'standards':
698
+ return await this.handleStandards(params);
699
+ default:
700
+ return { content: [{ type: 'text', text: `Unknown action: ${params.action}` }], isError: true };
1120
701
  }
1121
- return {
1122
- content: [{
1123
- type: 'text',
1124
- text: JSON.stringify(summary, null, 2),
1125
- }],
1126
- };
1127
702
  }
1128
703
  catch (error) {
1129
704
  return {
1130
- content: [{
1131
- type: 'text',
1132
- text: this.formatErrorMessage('Get code relationships', error instanceof Error ? error : String(error), { projectPath: project }),
1133
- }],
705
+ content: [{ type: 'text', text: this.formatErrorMessage('Analyze', error instanceof Error ? error : String(error), { projectPath: params.project }) }],
1134
706
  isError: true,
1135
707
  };
1136
708
  }
1137
709
  });
1138
710
  }
1139
- /**
1140
- * 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 () => {
1149
- 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
- };
711
+ async handleShowDependencies(params) {
712
+ const { filepath, filepaths, query, depth = 1, relationship_types, direction = 'both', max_nodes = 50, project } = params;
713
+ const storageManager = await (0, storage_1.getStorageManager)();
714
+ const projectStore = storageManager.getProjectStore();
715
+ const graphStore = storageManager.getGraphStore();
716
+ let projectId;
717
+ let projectPath;
718
+ if (project) {
719
+ const projects = await projectStore.list();
720
+ const found = projects.find(p => p.name === project || p.path === project || path.basename(p.path) === project);
721
+ if (found) {
722
+ projectId = found.id;
723
+ projectPath = found.path;
1191
724
  }
1192
- catch (error) {
1193
- return {
1194
- content: [{
1195
- type: 'text',
1196
- text: this.formatErrorMessage('List projects', error instanceof Error ? error : String(error)),
1197
- }],
1198
- isError: true,
1199
- };
725
+ else {
726
+ projectPath = process.cwd();
727
+ }
728
+ }
729
+ else {
730
+ projectPath = process.cwd();
731
+ const projects = await projectStore.list();
732
+ const found = projects.find(p => p.path === projectPath || path.basename(p.path) === path.basename(projectPath));
733
+ if (found) {
734
+ projectId = found.id;
735
+ projectPath = found.path;
1200
736
  }
737
+ }
738
+ if (!projectId) {
739
+ return { content: [{ type: 'text', text: 'Project not indexed. Use index({action: "init"}) first.' }], isError: true };
740
+ }
741
+ // Determine seed file paths
742
+ let seedFilePaths = [];
743
+ if (query) {
744
+ const searchResults = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
745
+ seedFilePaths = searchResults.slice(0, 5).map(r => r.file.replace(/\\/g, '/'));
746
+ }
747
+ else if (filepaths && filepaths.length > 0) {
748
+ seedFilePaths = filepaths.map(fp => fp.replace(/\\/g, '/'));
749
+ }
750
+ else if (filepath) {
751
+ seedFilePaths = [filepath.replace(/\\/g, '/')];
752
+ }
753
+ else {
754
+ return {
755
+ content: [{ type: 'text', text: 'Provide filepath, filepaths, or query for dependency analysis.' }],
756
+ isError: true,
757
+ };
758
+ }
759
+ const allNodes = await graphStore.findNodes(projectId);
760
+ const graphStats = {
761
+ total_nodes: allNodes.length,
762
+ file_nodes: allNodes.filter(n => n.type === 'file').length,
763
+ class_nodes: allNodes.filter(n => n.type === 'class').length,
764
+ function_nodes: allNodes.filter(n => n.type === 'function' || n.type === 'method').length,
765
+ };
766
+ // Find starting nodes using flexible path matching
767
+ const startNodes = allNodes.filter(n => {
768
+ const normalizedNodePath = n.filePath.replace(/\\/g, '/');
769
+ const nodeRelativePath = n.properties?.relativePath?.replace(/\\/g, '/');
770
+ return seedFilePaths.some(seedPath => {
771
+ const normalizedSeedPath = seedPath.replace(/\\/g, '/');
772
+ return (normalizedNodePath === normalizedSeedPath ||
773
+ nodeRelativePath === normalizedSeedPath ||
774
+ normalizedNodePath.endsWith(normalizedSeedPath) ||
775
+ normalizedNodePath.endsWith('/' + normalizedSeedPath) ||
776
+ normalizedNodePath.includes('/' + normalizedSeedPath) ||
777
+ n.name === path.basename(normalizedSeedPath).replace(/\.[^.]+$/, ''));
778
+ });
1201
779
  });
1202
- }
1203
- /**
1204
- * Tool 5: Index a project (with proper embeddings and knowledge graph)
1205
- * NOW RUNS IN BACKGROUND to prevent MCP timeouts
1206
- */
1207
- registerIndexTool() {
1208
- 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.',
1214
- 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)'),
1217
- },
1218
- }, async (args) => {
1219
- 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
- };
1253
- }
1254
- const projectName = name || path.basename(absolutePath);
1255
- const projectId = this.generateProjectId(absolutePath);
1256
- // Mutex: prevent concurrent indexing of same project (race condition protection)
1257
- if (this.indexingMutex.has(projectId)) {
1258
- return {
1259
- content: [{
1260
- type: 'text',
1261
- text: JSON.stringify({
1262
- status: 'already_indexing',
1263
- project_name: projectName,
1264
- project_path: absolutePath,
1265
- message: 'Indexing request already being processed. Please wait.',
1266
- }, null, 2),
1267
- }],
1268
- };
1269
- }
1270
- // Check if already indexing (from job status)
1271
- const existingJob = this.getIndexingStatus(projectId);
1272
- if (existingJob?.status === 'running') {
1273
- return {
1274
- content: [{
1275
- type: 'text',
1276
- text: JSON.stringify({
1277
- status: 'already_indexing',
1278
- project_name: projectName,
1279
- project_path: absolutePath,
1280
- progress: existingJob.progress,
1281
- message: 'Indexing already in progress. Use projects() to check status.',
1282
- }, null, 2),
1283
- }],
1284
- };
1285
- }
1286
- // Acquire mutex before starting
1287
- this.indexingMutex.add(projectId);
1288
- // Get storage and create project entry
1289
- const storageManager = await (0, storage_1.getStorageManager)();
1290
- const projectStore = storageManager.getProjectStore();
1291
- // Create or update project
1292
- await projectStore.upsert({
1293
- id: projectId,
1294
- name: projectName,
1295
- path: absolutePath,
1296
- metadata: { indexedAt: new Date().toISOString(), indexing: true },
1297
- });
1298
- // Delete coding standards file (will be regenerated)
1299
- const codingStandardsPath = path.join(absolutePath, '.codeseeker', 'coding-standards.json');
1300
- if (fs.existsSync(codingStandardsPath)) {
1301
- try {
1302
- fs.unlinkSync(codingStandardsPath);
1303
- }
1304
- catch { /* ignore */ }
780
+ if (startNodes.length === 0) {
781
+ const fileNodes = allNodes.filter(n => n.type === 'file').slice(0, 15);
782
+ const availableFiles = fileNodes.map(n => {
783
+ const relPath = n.properties?.relativePath;
784
+ return relPath || path.relative(projectPath, n.filePath);
785
+ });
786
+ return {
787
+ content: [{ type: 'text', text: JSON.stringify({
788
+ error: `No graph nodes found for: ${seedFilePaths.join(', ')}`,
789
+ available_files: availableFiles,
790
+ }, null, 2) }],
791
+ isError: true,
792
+ };
793
+ }
794
+ // Traverse relationships
795
+ const visitedNodes = new Map();
796
+ const collectedEdges = [];
797
+ let truncated = false;
798
+ const traverse = async (nodeId, currentDepth) => {
799
+ if (visitedNodes.size >= max_nodes) {
800
+ truncated = true;
801
+ return;
802
+ }
803
+ if (currentDepth > Math.min(depth, 3) || visitedNodes.has(nodeId))
804
+ return;
805
+ const node = await graphStore.getNode(nodeId);
806
+ if (!node)
807
+ return;
808
+ const relPath = node.properties?.relativePath;
809
+ visitedNodes.set(nodeId, {
810
+ id: node.id, type: node.type, name: node.name,
811
+ file: relPath || path.relative(projectPath, node.filePath),
812
+ });
813
+ const edges = await graphStore.getEdges(nodeId, direction);
814
+ for (const edge of edges) {
815
+ if (visitedNodes.size >= max_nodes) {
816
+ truncated = true;
817
+ return;
818
+ }
819
+ if (relationship_types && relationship_types.length > 0) {
820
+ if (!relationship_types.includes(edge.type))
821
+ continue;
1305
822
  }
1306
- // 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()
823
+ collectedEdges.push({ from: edge.source, to: edge.target, type: edge.type });
824
+ const nextNodeId = edge.source === nodeId ? edge.target : edge.source;
825
+ await traverse(nextNodeId, currentDepth + 1);
1322
826
  }
1323
- catch (error) {
1324
- const message = error instanceof Error ? error.message : String(error);
1325
- return {
1326
- content: [{
1327
- type: 'text',
1328
- text: JSON.stringify({ error: message }, null, 2),
1329
- }],
1330
- isError: true,
1331
- };
827
+ };
828
+ for (const startNode of startNodes) {
829
+ if (visitedNodes.size >= max_nodes) {
830
+ truncated = true;
831
+ break;
1332
832
  }
1333
- });
1334
- }
1335
- // 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()
833
+ await traverse(startNode.id, 1);
834
+ }
835
+ const nodes = Array.from(visitedNodes.values());
836
+ const uniqueEdges = collectedEdges.filter((e, i, arr) => arr.findIndex(x => x.from === e.from && x.to === e.to && x.type === e.type) === i);
837
+ const summary = {
838
+ graph_stats: graphStats,
839
+ seed_files: startNodes.map(n => ({
840
+ name: n.name, type: n.type,
841
+ file: n.properties?.relativePath || path.relative(projectPath, n.filePath),
842
+ })),
843
+ traversal: { depth_requested: depth, direction, relationship_filters: relationship_types || 'all', max_nodes },
844
+ results: { seed_nodes: startNodes.length, nodes_found: nodes.length, relationships_found: uniqueEdges.length, truncated },
845
+ nodes: nodes.map(n => ({ name: n.name, type: n.type, file: n.file })),
846
+ relationships: uniqueEdges.map(e => {
847
+ const fromNode = visitedNodes.get(e.from);
848
+ const toNode = visitedNodes.get(e.to);
849
+ return { type: e.type, from: fromNode?.name || e.from, to: toNode?.name || e.to };
850
+ }),
851
+ };
852
+ if (truncated) {
853
+ summary.truncated_warning = {
854
+ message: `Results truncated at ${max_nodes} nodes.`,
855
+ recommendations: [
856
+ relationship_types ? null : 'Add relationship_types filter',
857
+ depth > 1 ? 'Reduce depth to 1' : null,
858
+ `Increase max_nodes (current: ${max_nodes})`,
859
+ ].filter(Boolean),
860
+ };
861
+ }
862
+ return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] };
1339
863
  }
1340
- /**
1341
- * 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;
864
+ async handleFindDuplicates(params) {
865
+ const { project, similarity_threshold = 0.80, min_lines = 5 } = params;
866
+ const storageManager = await (0, storage_1.getStorageManager)();
867
+ const projectStore = storageManager.getProjectStore();
868
+ const vectorStore = storageManager.getVectorStore();
869
+ const projects = await projectStore.list();
870
+ const projectRecord = projects.find(p => p.name === project || p.path === project ||
871
+ path.basename(p.path) === project || path.resolve(project) === p.path);
872
+ if (!projectRecord) {
873
+ return {
874
+ content: [{ type: 'text', text: `Project not found: ${project}. Use index({action: "init"}) first.` }],
875
+ isError: true,
876
+ };
877
+ }
878
+ const allDocs = await this.getAllProjectDocuments(vectorStore, projectRecord.id);
879
+ if (allDocs.length === 0) {
880
+ return {
881
+ content: [{ type: 'text', text: JSON.stringify({
882
+ project: projectRecord.name,
883
+ summary: { total_chunks_analyzed: 0, exact_duplicates: 0, semantic_duplicates: 0, total_lines_affected: 0, potential_lines_saved: 0 },
884
+ duplicate_groups: [],
885
+ }, null, 2) }],
886
+ };
887
+ }
888
+ const filteredDocs = allDocs.filter(doc => doc.content.split('\n').length >= min_lines);
889
+ const duplicateGroups = [];
890
+ const processed = new Set();
891
+ const EXACT_THRESHOLD = 0.98;
892
+ for (let i = 0; i < filteredDocs.length && duplicateGroups.length < 50; i++) {
893
+ const doc = filteredDocs[i];
894
+ if (processed.has(doc.id))
895
+ continue;
896
+ const similarDocs = await vectorStore.searchByVector(doc.embedding, projectRecord.id, 20);
897
+ const matches = similarDocs.filter(match => match.document.id !== doc.id && match.score >= similarity_threshold && !processed.has(match.document.id));
898
+ if (matches.length > 0) {
899
+ const maxScore = Math.max(...matches.map(m => m.score));
900
+ const type = maxScore >= EXACT_THRESHOLD ? 'exact' : 'semantic';
901
+ const getLines = (d) => {
902
+ const meta = d.metadata;
903
+ return { startLine: meta?.startLine, endLine: meta?.endLine };
904
+ };
905
+ duplicateGroups.push({
906
+ type, similarity: maxScore,
907
+ chunks: [
908
+ { id: doc.id, filePath: doc.filePath, content: doc.content.substring(0, 200) + (doc.content.length > 200 ? '...' : ''), ...getLines(doc) },
909
+ ...matches.map(m => ({
910
+ id: m.document.id, filePath: m.document.filePath,
911
+ content: m.document.content.substring(0, 200) + (m.document.content.length > 200 ? '...' : ''),
912
+ ...getLines(m.document),
913
+ })),
914
+ ],
915
+ });
916
+ processed.add(doc.id);
917
+ matches.forEach(m => processed.add(m.document.id));
918
+ }
919
+ }
920
+ const exactDuplicates = duplicateGroups.filter(g => g.type === 'exact').length;
921
+ const semanticDuplicates = duplicateGroups.filter(g => g.type === 'semantic').length;
922
+ const totalLinesAffected = duplicateGroups.reduce((sum, g) => sum + g.chunks.reduce((chunkSum, c) => chunkSum + (c.endLine && c.startLine ? c.endLine - c.startLine + 1 : c.content.split('\n').length), 0), 0);
923
+ const formattedGroups = duplicateGroups.slice(0, 20).map(group => ({
924
+ type: group.type,
925
+ similarity: `${(group.similarity * 100).toFixed(1)}%`,
926
+ files_affected: new Set(group.chunks.map(c => c.filePath)).size,
927
+ locations: group.chunks.map(c => ({
928
+ file: path.relative(projectRecord.path, c.filePath),
929
+ lines: c.startLine && c.endLine ? `${c.startLine}-${c.endLine}` : 'N/A',
930
+ preview: c.content.substring(0, 100).replace(/\n/g, ' '),
931
+ })),
932
+ }));
1347
933
  return {
1348
- 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(),
934
+ content: [{ type: 'text', text: JSON.stringify({
935
+ project: projectRecord.name,
936
+ summary: {
937
+ total_chunks_analyzed: filteredDocs.length,
938
+ exact_duplicates: exactDuplicates,
939
+ semantic_duplicates: semanticDuplicates,
940
+ total_lines_affected: totalLinesAffected,
941
+ potential_lines_saved: Math.floor(totalLinesAffected * 0.6),
942
+ },
943
+ duplicate_groups: formattedGroups,
944
+ recommendations: exactDuplicates > 0
945
+ ? [`Found ${exactDuplicates} exact duplicate groups - prioritize consolidation`]
946
+ : semanticDuplicates > 0
947
+ ? [`Found ${semanticDuplicates} semantic duplicates - review for potential abstraction`]
948
+ : ['No significant duplicates found above threshold'],
949
+ }, null, 2) }],
1354
950
  };
1355
951
  }
1356
- /**
1357
- * 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
- };
952
+ async handleFindDeadCode(params) {
953
+ const { project, include_patterns } = params;
954
+ const storageManager = await (0, storage_1.getStorageManager)();
955
+ const projectStore = storageManager.getProjectStore();
956
+ const graphStore = storageManager.getGraphStore();
957
+ const projects = await projectStore.list();
958
+ const projectRecord = projects.find(p => p.name === project || p.path === project ||
959
+ path.basename(p.path) === project || path.resolve(project) === p.path);
960
+ if (!projectRecord) {
961
+ return {
962
+ content: [{ type: 'text', text: `Project not found: ${project}. Use index({action: "init"}) first.` }],
963
+ isError: true,
964
+ };
965
+ }
966
+ const allNodes = await graphStore.findNodes(projectRecord.id);
967
+ if (allNodes.length === 0) {
968
+ return {
969
+ content: [{ type: 'text', text: JSON.stringify({
970
+ project: projectRecord.name,
971
+ summary: { total_issues: 0, dead_code_count: 0, anti_patterns_count: 0, coupling_issues_count: 0 },
972
+ dead_code: [], anti_patterns: [], coupling_issues: [],
973
+ note: 'No graph data. Project may need reindexing.',
974
+ }, null, 2) }],
975
+ };
976
+ }
977
+ const patterns = include_patterns || ['dead_code', 'god_class', 'circular_deps', 'feature_envy', 'coupling'];
978
+ const deadCodeItems = [];
979
+ const antiPatternItems = [];
980
+ const couplingItems = [];
981
+ for (const node of allNodes) {
982
+ const inEdges = await graphStore.getEdges(node.id, 'in');
983
+ const outEdges = await graphStore.getEdges(node.id, 'out');
984
+ if (patterns.includes('dead_code')) {
985
+ const isEntryPoint = node.type === 'file' ||
986
+ node.name.toLowerCase().includes('main') ||
987
+ node.name.toLowerCase().includes('index') ||
988
+ node.name.toLowerCase().includes('app');
989
+ if (!isEntryPoint && (node.type === 'class' || node.type === 'function') && inEdges.length === 0) {
990
+ deadCodeItems.push({
991
+ type: 'Dead Code', name: node.name,
992
+ file: path.relative(projectRecord.path, node.filePath),
993
+ description: `Unused ${node.type}: ${node.name} - no incoming references`,
994
+ confidence: '70%', impact: 'medium',
995
+ recommendation: 'Review if needed. Remove if unused or add to exports.',
996
+ });
1415
997
  }
1416
- // 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
- };
998
+ }
999
+ if (patterns.includes('god_class') && node.type === 'class') {
1000
+ const containsEdges = outEdges.filter(e => e.type === 'contains');
1001
+ const dependsOnEdges = outEdges.filter(e => e.type === 'imports' || e.type === 'depends_on');
1002
+ if (containsEdges.length > 15 || dependsOnEdges.length > 10) {
1003
+ antiPatternItems.push({
1004
+ type: 'God Class', name: node.name,
1005
+ file: path.relative(projectRecord.path, node.filePath),
1006
+ description: `${node.name} has ${containsEdges.length} members, ${dependsOnEdges.length} dependencies`,
1007
+ confidence: '80%', impact: 'high',
1008
+ recommendation: 'Break down following Single Responsibility Principle',
1009
+ });
1455
1010
  }
1456
- // 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
- };
1011
+ }
1012
+ if (patterns.includes('coupling') && (node.type === 'class' || node.type === 'file')) {
1013
+ const dependencies = outEdges.filter(e => e.type === 'imports' || e.type === 'depends_on');
1014
+ if (dependencies.length > 8) {
1015
+ couplingItems.push({
1016
+ type: 'High Coupling', name: node.name,
1017
+ file: path.relative(projectRecord.path, node.filePath),
1018
+ description: `${node.type} ${node.name} has ${dependencies.length} dependencies`,
1019
+ confidence: '75%', impact: 'high',
1020
+ recommendation: 'Reduce via interfaces or dependency injection',
1021
+ });
1465
1022
  }
1466
- 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
- }
1023
+ }
1024
+ }
1025
+ // Circular dependency detection
1026
+ if (patterns.includes('circular_deps')) {
1027
+ const fileNodes = allNodes.filter(n => n.type === 'file');
1028
+ const importMap = new Map();
1029
+ for (const fileNode of fileNodes) {
1030
+ const imports = await graphStore.getEdges(fileNode.id, 'out');
1031
+ importMap.set(fileNode.id, new Set(imports.filter(e => e.type === 'imports').map(e => e.target)));
1032
+ }
1033
+ for (const [fileId, imports] of importMap.entries()) {
1034
+ for (const targetId of imports) {
1035
+ const targetImports = importMap.get(targetId);
1036
+ if (targetImports?.has(fileId)) {
1037
+ const sourceNode = allNodes.find(n => n.id === fileId);
1038
+ const targetNode = allNodes.find(n => n.id === targetId);
1039
+ if (sourceNode && targetNode) {
1040
+ antiPatternItems.push({
1041
+ type: 'Circular Dependency',
1042
+ name: `${sourceNode.name} <-> ${targetNode.name}`,
1043
+ file: path.relative(projectRecord.path, sourceNode.filePath),
1044
+ description: `Bidirectional import between ${sourceNode.name} and ${targetNode.name}`,
1045
+ confidence: '90%', impact: 'high',
1046
+ recommendation: 'Break cycle using dependency inversion or extract shared code',
1047
+ });
1497
1048
  }
1498
1049
  }
1499
- catch (error) {
1500
- const msg = error instanceof Error ? error.message : String(error);
1501
- errors.push(`${change.path}: ${msg}`);
1502
- }
1503
- }
1504
- // Update coding standards if pattern-related files changed
1505
- try {
1506
- const changedPaths = changes.map(c => c.path);
1507
- const generator = new coding_standards_generator_1.CodingStandardsGenerator(vectorStore);
1508
- await generator.updateStandards(found.id, found.path, changedPaths);
1509
- }
1510
- catch (error) {
1511
- // Don't fail the whole operation if standards update fails
1512
- console.error('Failed to update coding standards:', error);
1513
- }
1514
- // Invalidate query cache for this project (files changed)
1515
- let cacheInvalidated = 0;
1516
- try {
1517
- cacheInvalidated = await this.queryCache.invalidateProject(found.id);
1518
- }
1519
- catch (error) {
1520
- // Don't fail if cache invalidation fails
1521
- console.error('Failed to invalidate query cache:', error);
1522
1050
  }
1523
- const duration = Date.now() - startTime;
1524
- return {
1525
- content: [{
1526
- type: 'text',
1527
- text: JSON.stringify({
1528
- success: errors.length === 0,
1529
- mode: 'incremental',
1530
- project: found.name,
1531
- changes_processed: changes.length,
1532
- files_reindexed: filesProcessed,
1533
- files_skipped: filesSkipped > 0 ? filesSkipped : undefined,
1534
- chunks_created: chunksCreated,
1535
- chunks_deleted: chunksDeleted,
1536
- cache_invalidated: cacheInvalidated > 0 ? cacheInvalidated : undefined,
1537
- duration_ms: duration,
1538
- note: filesSkipped > 0 ? `${filesSkipped} file(s) unchanged (skipped via mtime/hash check)` : undefined,
1539
- errors: errors.length > 0 ? errors.slice(0, 5) : undefined,
1540
- }, null, 2),
1541
- }],
1542
- };
1543
1051
  }
1544
- 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
- };
1552
- }
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' }) => {
1052
+ }
1053
+ return {
1054
+ content: [{ type: 'text', text: JSON.stringify({
1055
+ project: projectRecord.name,
1056
+ graph_stats: {
1057
+ total_nodes: allNodes.length,
1058
+ files: allNodes.filter(n => n.type === 'file').length,
1059
+ classes: allNodes.filter(n => n.type === 'class').length,
1060
+ functions: allNodes.filter(n => n.type === 'function').length,
1061
+ },
1062
+ summary: {
1063
+ total_issues: deadCodeItems.length + antiPatternItems.length + couplingItems.length,
1064
+ dead_code_count: deadCodeItems.length,
1065
+ anti_patterns_count: antiPatternItems.length,
1066
+ coupling_issues_count: couplingItems.length,
1067
+ },
1068
+ dead_code: deadCodeItems.slice(0, 20),
1069
+ anti_patterns: antiPatternItems.slice(0, 10),
1070
+ coupling_issues: couplingItems.slice(0, 10),
1071
+ }, null, 2) }],
1072
+ };
1073
+ }
1074
+ async handleStandards(params) {
1075
+ const { project, category = 'all' } = params;
1076
+ const storageManager = await (0, storage_1.getStorageManager)();
1077
+ const projectStore = storageManager.getProjectStore();
1078
+ const vectorStore = storageManager.getVectorStore();
1079
+ const projects = await projectStore.list();
1080
+ const found = projects.find(p => p.name === project || p.path === project || path.basename(p.path) === project);
1081
+ if (!found) {
1082
+ return {
1083
+ content: [{ type: 'text', text: `Project not found: ${project}. Use index({action: "status"}) to list projects.` }],
1084
+ isError: true,
1085
+ };
1086
+ }
1087
+ const standardsPath = path.join(found.path, '.codeseeker', 'coding-standards.json');
1088
+ let standardsContent;
1089
+ try {
1090
+ standardsContent = fs.readFileSync(standardsPath, 'utf-8');
1091
+ }
1092
+ catch {
1093
+ const generator = new coding_standards_generator_1.CodingStandardsGenerator(vectorStore);
1094
+ await generator.generateStandards(found.id, found.path);
1566
1095
  try {
1567
- // 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
- };
1096
+ standardsContent = fs.readFileSync(standardsPath, 'utf-8');
1625
1097
  }
1626
- catch (error) {
1098
+ catch {
1627
1099
  return {
1628
- content: [{
1629
- type: 'text',
1630
- text: this.formatErrorMessage('Get coding standards', error instanceof Error ? error : String(error), { projectPath: project }),
1631
- }],
1100
+ content: [{ type: 'text', text: 'No coding standards detected. Index the project first.' }],
1632
1101
  isError: true,
1633
1102
  };
1634
1103
  }
1635
- });
1104
+ }
1105
+ const standards = JSON.parse(standardsContent);
1106
+ let result = standards;
1107
+ if (category !== 'all') {
1108
+ result = { ...standards, standards: { [category]: standards.standards[category] || {} } };
1109
+ }
1110
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1636
1111
  }
1637
- /**
1638
- * 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.',
1112
+ // ============================================================
1113
+ // TOOL 3: index
1114
+ // Combines: index (init), sync, projects (status), install_parsers, exclude
1115
+ // ============================================================
1116
+ registerIndexTool() {
1117
+ this.server.registerTool('index', {
1118
+ description: 'Index management. Actions: "init" (index project), "sync" (update changed files), ' +
1119
+ '"status" (list projects), "parsers" (install language parsers), "exclude" (manage exclusions).',
1647
1120
  inputSchema: {
1648
- 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'),
1121
+ action: zod_1.z.enum(['init', 'sync', 'status', 'parsers', 'exclude']).describe('Action'),
1122
+ path: zod_1.z.string().optional().describe('Project directory (for init)'),
1123
+ project: zod_1.z.string().optional().describe('Project name or path'),
1124
+ name: zod_1.z.string().optional().describe('Project name (for init)'),
1125
+ // sync params
1126
+ changes: zod_1.z.array(zod_1.z.object({
1127
+ type: zod_1.z.enum(['created', 'modified', 'deleted']),
1128
+ path: zod_1.z.string(),
1129
+ })).optional().describe('File changes for sync'),
1130
+ full_reindex: zod_1.z.boolean().optional().default(false).describe('Full reindex'),
1131
+ // parsers params
1132
+ languages: zod_1.z.array(zod_1.z.string()).optional().describe('Languages to install parsers for'),
1133
+ list_available: zod_1.z.boolean().optional().default(false).describe('List available parsers'),
1134
+ // exclude params
1135
+ exclude_action: zod_1.z.enum(['exclude', 'include', 'list']).optional().describe('Exclusion sub-action'),
1136
+ paths: zod_1.z.array(zod_1.z.string()).optional().describe('Paths/patterns to exclude/include'),
1137
+ reason: zod_1.z.string().optional().describe('Exclusion reason'),
1651
1138
  },
1652
- }, async ({ project, languages, list_available = false }) => {
1139
+ }, async (params) => {
1653
1140
  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
- };
1141
+ switch (params.action) {
1142
+ case 'init':
1143
+ return await this.handleIndexInit(params);
1144
+ case 'sync':
1145
+ return await this.handleSync(params);
1146
+ case 'status':
1147
+ return await this.handleProjects();
1148
+ case 'parsers':
1149
+ return await this.handleInstallParsers(params);
1150
+ case 'exclude':
1151
+ return await this.handleExclude(params);
1152
+ default:
1153
+ return { content: [{ type: 'text', text: `Unknown action: ${params.action}` }], isError: true };
1738
1154
  }
1739
- // No arguments - show usage
1740
- return {
1741
- content: [{
1742
- type: 'text',
1743
- text: JSON.stringify({
1744
- usage: {
1745
- analyze_project: 'install_parsers({project: "/path/to/project"}) - Detect languages and suggest parsers',
1746
- install_specific: 'install_parsers({languages: ["python", "java"]}) - Install parsers for specific languages',
1747
- list_available: 'install_parsers({list_available: true}) - Show all available parsers',
1748
- },
1749
- supported_languages: [
1750
- 'TypeScript (bundled)', 'JavaScript (bundled)',
1751
- 'Python', 'Java', 'C#', 'Go', 'Rust',
1752
- 'C', 'C++', 'Ruby', 'PHP', 'Swift', 'Kotlin'
1753
- ],
1754
- }, null, 2),
1755
- }],
1756
- };
1757
1155
  }
1758
1156
  catch (error) {
1759
1157
  return {
1760
- content: [{
1761
- type: 'text',
1762
- text: this.formatErrorMessage('Manage language support', error instanceof Error ? error : String(error), { projectPath: project }),
1763
- }],
1158
+ content: [{ type: 'text', text: this.formatErrorMessage('Index', error instanceof Error ? error : String(error), { projectPath: params.path || params.project }) }],
1764
1159
  isError: true,
1765
1160
  };
1766
1161
  }
1767
1162
  });
1768
1163
  }
1769
- /**
1770
- * 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 }) => {
1164
+ async handleIndexInit(params) {
1165
+ const projectPath = params.path;
1166
+ if (!projectPath) {
1167
+ return {
1168
+ content: [{ type: 'text', text: 'path parameter required for init action.' }],
1169
+ isError: true,
1170
+ };
1171
+ }
1172
+ const absolutePath = path.isAbsolute(projectPath) ? projectPath : path.resolve(projectPath);
1173
+ const pathError = this.validateProjectPath(absolutePath);
1174
+ if (pathError) {
1175
+ return { content: [{ type: 'text', text: pathError }], isError: true };
1176
+ }
1177
+ if (!fs.existsSync(absolutePath)) {
1178
+ return { content: [{ type: 'text', text: `Directory not found: ${absolutePath}` }], isError: true };
1179
+ }
1180
+ if (!fs.statSync(absolutePath).isDirectory()) {
1181
+ return { content: [{ type: 'text', text: `Not a directory: ${absolutePath}` }], isError: true };
1182
+ }
1183
+ const projectName = params.name || path.basename(absolutePath);
1184
+ const projectId = this.generateProjectId(absolutePath);
1185
+ if (this.indexingMutex.has(projectId)) {
1186
+ return {
1187
+ content: [{ type: 'text', text: JSON.stringify({
1188
+ status: 'already_indexing', project_name: projectName,
1189
+ message: 'Indexing request already being processed.',
1190
+ }, null, 2) }],
1191
+ };
1192
+ }
1193
+ const existingJob = this.getIndexingStatus(projectId);
1194
+ if (existingJob?.status === 'running') {
1195
+ return {
1196
+ content: [{ type: 'text', text: JSON.stringify({
1197
+ status: 'already_indexing', project_name: projectName,
1198
+ progress: existingJob.progress,
1199
+ message: 'Indexing in progress. Check with index({action: "status"}).',
1200
+ }, null, 2) }],
1201
+ };
1202
+ }
1203
+ this.indexingMutex.add(projectId);
1204
+ const storageManager = await (0, storage_1.getStorageManager)();
1205
+ const projectStore = storageManager.getProjectStore();
1206
+ await projectStore.upsert({
1207
+ id: projectId, name: projectName, path: absolutePath,
1208
+ metadata: { indexedAt: new Date().toISOString(), indexing: true },
1209
+ });
1210
+ const codingStandardsPath = path.join(absolutePath, '.codeseeker', 'coding-standards.json');
1211
+ if (fs.existsSync(codingStandardsPath)) {
1791
1212
  try {
1792
- // 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()
1213
+ fs.unlinkSync(codingStandardsPath);
1214
+ }
1215
+ catch { /* ignore */ }
1216
+ }
1217
+ this.startBackgroundIndexing(projectId, projectName, absolutePath, true);
1218
+ return {
1219
+ content: [{ type: 'text', text: JSON.stringify({
1220
+ status: 'indexing_started', project_name: projectName, project_path: absolutePath,
1221
+ message: 'Indexing started in background. Use index({action: "status"}) to check progress.',
1222
+ }, null, 2) }],
1223
+ };
1224
+ }
1225
+ async handleSync(params) {
1226
+ const { project, changes, full_reindex = false } = params;
1227
+ const startTime = Date.now();
1228
+ const storageManager = await (0, storage_1.getStorageManager)();
1229
+ const projectStore = storageManager.getProjectStore();
1230
+ const vectorStore = storageManager.getVectorStore();
1231
+ const projects = await projectStore.list();
1232
+ let found;
1233
+ if (project) {
1234
+ found = projects.find(p => p.name === project || p.path === project ||
1235
+ path.basename(p.path) === project || path.resolve(project) === p.path);
1236
+ }
1237
+ else {
1238
+ if (changes && changes.length > 0 && path.isAbsolute(changes[0].path)) {
1239
+ found = projects.find(p => changes[0].path.startsWith(p.path));
1240
+ }
1241
+ if (!found && projects.length === 1) {
1242
+ found = projects[0];
1243
+ }
1244
+ }
1245
+ if (!found) {
1246
+ return {
1247
+ content: [{ type: 'text', text: project
1248
+ ? `Project not found: ${project}. Use index({action: "status"}) to see projects.`
1249
+ : `Could not auto-detect project. Specify project. Available: ${projects.map(p => p.name).join(', ')}` }],
1250
+ isError: true,
1251
+ };
1252
+ }
1253
+ if (full_reindex) {
1254
+ const existingJob = this.getIndexingStatus(found.id);
1255
+ if (existingJob?.status === 'running') {
1256
+ return {
1257
+ content: [{ type: 'text', text: JSON.stringify({
1258
+ status: 'already_indexing', project: found.name,
1259
+ progress: existingJob.progress,
1260
+ }, null, 2) }],
1815
1261
  };
1816
- // 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
- };
1262
+ }
1263
+ const codingStandardsPath = path.join(found.path, '.codeseeker', 'coding-standards.json');
1264
+ if (fs.existsSync(codingStandardsPath)) {
1265
+ try {
1266
+ fs.unlinkSync(codingStandardsPath);
1859
1267
  }
1860
- // 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
- }
1268
+ catch { /* ignore */ }
1269
+ }
1270
+ this.startBackgroundIndexing(found.id, found.name, found.path, true);
1271
+ return {
1272
+ content: [{ type: 'text', text: JSON.stringify({
1273
+ status: 'reindex_started', project: found.name,
1274
+ message: 'Full reindex started. Use index({action: "status"}) to check.',
1275
+ }, null, 2) }],
1276
+ };
1277
+ }
1278
+ if (!changes || changes.length === 0) {
1279
+ return {
1280
+ content: [{ type: 'text', text: 'No changes provided. Either pass changes or set full_reindex: true.' }],
1281
+ isError: true,
1282
+ };
1283
+ }
1284
+ let chunksCreated = 0, chunksDeleted = 0, filesProcessed = 0, filesSkipped = 0;
1285
+ const errors = [];
1286
+ for (const change of changes) {
1287
+ const relativePath = path.isAbsolute(change.path) ? path.relative(found.path, change.path) : change.path;
1288
+ try {
1289
+ if (change.type === 'deleted') {
1290
+ const result = await this.indexingService.deleteFile(found.id, relativePath);
1291
+ if (result.success) {
1292
+ chunksDeleted += result.deleted;
1293
+ filesProcessed++;
1892
1294
  }
1893
- // Save exclusions
1894
- exclusions.lastModified = new Date().toISOString();
1895
- fs.writeFileSync(exclusionsPath, JSON.stringify(exclusions, null, 2));
1896
- // Flush to persist deletions
1897
- await vectorStore.flush();
1898
- return {
1899
- content: [{
1900
- type: 'text',
1901
- text: JSON.stringify({
1902
- success: true,
1903
- action: 'exclude',
1904
- project: found.name,
1905
- patterns_added: addedPatterns,
1906
- already_excluded: alreadyExcluded.length > 0 ? alreadyExcluded : undefined,
1907
- files_removed_from_index: filesRemoved,
1908
- total_exclusions: exclusions.patterns.length,
1909
- message: addedPatterns.length > 0
1910
- ? `Added ${addedPatterns.length} exclusion pattern(s). ${filesRemoved} file chunk(s) removed from index.`
1911
- : 'No new patterns added (all were already excluded).',
1912
- note: 'Excluded files will not appear in search results. Use action: "include" to re-enable indexing.'
1913
- }, null, 2),
1914
- }],
1915
- };
1916
1295
  }
1917
- // 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');
1342
+ async handleProjects() {
1343
+ const storageManager = await (0, storage_1.getStorageManager)();
1344
+ const projectStore = storageManager.getProjectStore();
1345
+ const vectorStore = storageManager.getVectorStore();
1346
+ const projects = await projectStore.list();
1347
+ if (projects.length === 0) {
1348
+ return {
1349
+ content: [{ type: 'text', text: 'No projects indexed. Use index({action: "init", path: "/path/to/project"}).' }],
1350
+ };
1351
+ }
1352
+ const projectsWithCounts = await Promise.all(projects.map(async (p) => {
1353
+ const fileCount = await vectorStore.countFiles(p.id);
1354
+ const chunkCount = await vectorStore.count(p.id);
1355
+ const indexingStatus = this._getIndexingStatusForProject(p.id);
1356
+ const projectInfo = {
1357
+ name: p.name, path: p.path, files: fileCount, chunks: chunkCount,
1358
+ last_indexed: p.updatedAt.toISOString(),
1359
+ };
1360
+ if (indexingStatus)
1361
+ Object.assign(projectInfo, indexingStatus);
1362
+ return projectInfo;
1363
+ }));
1364
+ return {
1365
+ content: [{ type: 'text', text: JSON.stringify({
1366
+ storage_mode: storageManager.getMode(),
1367
+ total_projects: projects.length,
1368
+ projects: projectsWithCounts,
1369
+ }, null, 2) }],
1370
+ };
2024
1371
  }
2025
- /**
2026
- * Generate actionable error message based on error type
2027
- */
2028
- formatErrorMessage(operation, error, context) {
2029
- const message = error instanceof Error ? error.message : String(error);
2030
- const lowerMessage = message.toLowerCase();
2031
- // Common error patterns with actionable guidance
2032
- if (lowerMessage.includes('enoent') || lowerMessage.includes('not found') || lowerMessage.includes('no such file')) {
2033
- return `${operation} failed: File or directory not found.\n\n` +
2034
- `TROUBLESHOOTING:\n` +
2035
- `• Verify the path exists: ${context?.projectPath || 'the specified path'}\n` +
2036
- `• Check for typos in the path\n` +
2037
- `• Ensure you have read permissions`;
1372
+ async handleInstallParsers(params) {
1373
+ const { project, languages, list_available = false } = params;
1374
+ if (list_available) {
1375
+ const parsers = await this.languageSupportService.checkInstalledParsers();
1376
+ const installed = parsers.filter(p => p.installed);
1377
+ const available = parsers.filter(p => !p.installed);
1378
+ return {
1379
+ content: [{ type: 'text', text: JSON.stringify({
1380
+ installed_parsers: installed.map(p => ({ language: p.language, extensions: p.extensions, quality: p.quality })),
1381
+ available_parsers: available.map(p => ({ language: p.language, extensions: p.extensions, npm_package: p.npmPackage, quality: p.quality })),
1382
+ }, null, 2) }],
1383
+ };
2038
1384
  }
2039
- if (lowerMessage.includes('eacces') || lowerMessage.includes('permission denied')) {
2040
- return `${operation} failed: Permission denied.\n\n` +
2041
- `TROUBLESHOOTING:\n` +
2042
- `• Check file/folder permissions\n` +
2043
- `• Run with appropriate access rights\n` +
2044
- `• 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
+ };
2045
1395
  }
2046
- if (lowerMessage.includes('timeout') || lowerMessage.includes('timed out')) {
2047
- return `${operation} failed: Operation timed out.\n\n` +
2048
- `TROUBLESHOOTING:\n` +
2049
- `• Try again - the operation may complete on retry\n` +
2050
- `• For large projects, indexing runs in background - check status with projects()\n` +
2051
- `• 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
+ };
2052
1414
  }
2053
- if (lowerMessage.includes('connection') || lowerMessage.includes('econnrefused') || lowerMessage.includes('network')) {
2054
- return `${operation} failed: Connection error.\n\n` +
2055
- `TROUBLESHOOTING:\n` +
2056
- `• Check if storage services are running (if using server mode)\n` +
2057
- `• Verify network connectivity\n` +
2058
- `• 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 };
2059
1424
  }
2060
- if (lowerMessage.includes('not indexed') || lowerMessage.includes('no project')) {
2061
- const pathHint = context?.projectPath ? `index({path: "${context.projectPath}"})` : 'index({path: "/path/to/project"})';
2062
- return `${operation} failed: Project not indexed.\n\n` +
2063
- `ACTION REQUIRED:\n` +
2064
- `• First run: ${pathHint}\n` +
2065
- `• Then retry your search\n` +
2066
- `• Use projects() to see indexed projects`;
1425
+ if (!exclude_action) {
1426
+ return { content: [{ type: 'text', text: 'exclude_action required (exclude, include, or list).' }], isError: true };
2067
1427
  }
2068
- if (lowerMessage.includes('out of memory') || lowerMessage.includes('heap')) {
2069
- return `${operation} failed: Out of memory.\n\n` +
2070
- `TROUBLESHOOTING:\n` +
2071
- `• Try indexing fewer files at once\n` +
2072
- `• Use exclude() to skip large directories (e.g., node_modules, dist)\n` +
2073
- `• Increase Node.js memory: NODE_OPTIONS=--max-old-space-size=4096`;
2074
- }
2075
- // Default: include original message with generic guidance
2076
- return `${operation} failed: ${message}\n\n` +
2077
- `TROUBLESHOOTING:\n` +
2078
- `• Check the error message above for details\n` +
2079
- `• Use projects() to verify project status\n` +
2080
- `• Try sync({project: "...", full_reindex: true}) if index seems corrupted`;
2081
- }
2082
- /**
2083
- * Tool 10: Find duplicate code patterns
2084
- */
2085
- registerFindDuplicatesTool() {
2086
- this.server.registerTool('find_duplicates', {
2087
- description: '**FIND DUPLICATE CODE** - Detects duplicate and similar code patterns using semantic analysis. ' +
2088
- 'Finds: exact copies, semantically similar code (same logic, different names), and structurally similar patterns. ' +
2089
- 'Use when: cleaning up codebase, finding copy-paste code, reducing maintenance burden. ' +
2090
- 'Returns groups of duplicates with consolidation suggestions and estimated savings. ' +
2091
- '**IMPORTANT**: Always pass the project parameter with your workspace root path.',
2092
- inputSchema: {
2093
- project: zod_1.z.string().describe('Project path - REQUIRED: the workspace root path to analyze'),
2094
- similarity_threshold: zod_1.z.number().optional().default(0.80)
2095
- .describe('Minimum similarity score (0.0-1.0) to consider as duplicate. Default: 0.80'),
2096
- min_lines: zod_1.z.number().optional().default(5)
2097
- .describe('Minimum lines in a code block to analyze. Default: 5'),
2098
- include_types: zod_1.z.array(zod_1.z.enum(['function', 'class', 'method', 'block'])).optional()
2099
- .describe('Types of code to analyze. Default: all types'),
2100
- },
2101
- }, async ({ project, similarity_threshold = 0.80, min_lines = 5, include_types }) => {
1428
+ const storageManager = await (0, storage_1.getStorageManager)();
1429
+ const projectStore = storageManager.getProjectStore();
1430
+ const vectorStore = storageManager.getVectorStore();
1431
+ const projects = await projectStore.list();
1432
+ const found = projects.find(p => p.name === project || p.path === project || path.basename(p.path) === project);
1433
+ if (!found) {
1434
+ return { content: [{ type: 'text', text: `Project not found: ${project}.` }], isError: true };
1435
+ }
1436
+ // Load exclusions
1437
+ const exclusionsPath = path.join(found.path, '.codeseeker', 'exclusions.json');
1438
+ let exclusions = { patterns: [], lastModified: new Date().toISOString() };
1439
+ const codeseekerDir = path.join(found.path, '.codeseeker');
1440
+ if (!fs.existsSync(codeseekerDir)) {
1441
+ fs.mkdirSync(codeseekerDir, { recursive: true });
1442
+ }
1443
+ if (fs.existsSync(exclusionsPath)) {
2102
1444
  try {
2103
- const storageManager = await (0, storage_1.getStorageManager)();
2104
- const projectStore = storageManager.getProjectStore();
2105
- const projects = await projectStore.list();
2106
- // Find the project
2107
- const projectRecord = projects.find(p => p.name === project ||
2108
- p.path === project ||
2109
- path.basename(p.path) === project ||
2110
- path.resolve(project) === p.path);
2111
- const projectPath = projectRecord?.path || path.resolve(project);
2112
- // Verify project exists
2113
- if (!fs.existsSync(projectPath)) {
2114
- return {
2115
- content: [{
2116
- type: 'text',
2117
- text: `Project path not found: ${projectPath}`,
2118
- }],
2119
- isError: true,
2120
- };
2121
- }
2122
- // Run duplicate detection
2123
- const detector = new duplicate_code_detector_1.DuplicateCodeDetector();
2124
- const report = await detector.analyzeProject(projectPath, {
2125
- semanticSimilarityThreshold: similarity_threshold,
2126
- minimumChunkSize: min_lines,
2127
- includeTypes: include_types || ['function', 'class', 'method', 'block'],
2128
- });
2129
- // Format results
2130
- const duplicateGroups = report.duplicateGroups.slice(0, 20).map(group => ({
2131
- type: group.type,
2132
- similarity: `${(group.similarity * 100).toFixed(1)}%`,
2133
- files_affected: group.estimatedSavings.filesAffected,
2134
- lines_savable: group.estimatedSavings.linesReduced,
2135
- suggestion: group.consolidationSuggestion,
2136
- locations: group.chunks.map(c => ({
2137
- file: c.filePath,
2138
- lines: `${c.startLine}-${c.endLine}`,
2139
- name: c.functionName || c.className || 'code block',
2140
- })),
2141
- }));
2142
- return {
2143
- content: [{
2144
- type: 'text',
2145
- text: JSON.stringify({
2146
- project: path.basename(projectPath),
2147
- summary: {
2148
- total_chunks_analyzed: report.totalChunksAnalyzed,
2149
- exact_duplicates: report.summary.exactDuplicates,
2150
- semantic_duplicates: report.summary.semanticDuplicates,
2151
- structural_duplicates: report.summary.structuralDuplicates,
2152
- total_lines_affected: report.summary.totalLinesAffected,
2153
- potential_lines_saved: report.summary.potentialSavings,
2154
- },
2155
- duplicate_groups: duplicateGroups,
2156
- recommendations: report.recommendations,
2157
- }, null, 2),
2158
- }],
2159
- };
1445
+ exclusions = JSON.parse(fs.readFileSync(exclusionsPath, 'utf-8'));
2160
1446
  }
2161
- catch (error) {
2162
- return {
2163
- content: [{
2164
- type: 'text',
2165
- text: this.formatErrorMessage('Find duplicates', error instanceof Error ? error : String(error), { projectPath: project }),
2166
- }],
2167
- isError: true,
2168
- };
2169
- }
2170
- });
2171
- }
2172
- /**
2173
- * Tool 11: Find dead/unused code
2174
- */
2175
- registerFindDeadCodeTool() {
2176
- this.server.registerTool('find_dead_code', {
2177
- description: '**FIND DEAD/UNUSED CODE** - Detects code that is never used or referenced. ' +
2178
- 'Uses the knowledge graph to find: unused classes, unused functions, isolated code components. ' +
2179
- 'Also detects: God classes (too many responsibilities), circular dependencies, feature envy. ' +
2180
- 'Use when: cleaning up codebase, finding code to remove, improving architecture. ' +
2181
- '**IMPORTANT**: Always pass the project parameter. Project must be indexed first.',
2182
- inputSchema: {
2183
- project: zod_1.z.string().describe('Project path - REQUIRED: the workspace root path to analyze'),
2184
- include_patterns: zod_1.z.array(zod_1.z.enum(['dead_code', 'god_class', 'circular_deps', 'feature_envy', 'coupling'])).optional()
2185
- .describe('Anti-patterns to detect. Default: all patterns'),
2186
- },
2187
- }, async ({ project, include_patterns }) => {
2188
- try {
2189
- const storageManager = await (0, storage_1.getStorageManager)();
2190
- const projectStore = storageManager.getProjectStore();
2191
- const projects = await projectStore.list();
2192
- // Find the project
2193
- const projectRecord = projects.find(p => p.name === project ||
2194
- p.path === project ||
2195
- path.basename(p.path) === project ||
2196
- path.resolve(project) === p.path);
2197
- if (!projectRecord) {
2198
- return {
2199
- content: [{
2200
- type: 'text',
2201
- text: `Project not found or not indexed: ${project}\n\n` +
2202
- `Use index({path: "${project}"}) to index the project first.`,
2203
- }],
2204
- isError: true,
2205
- };
1447
+ catch { /* start fresh */ }
1448
+ }
1449
+ if (exclude_action === 'list') {
1450
+ return {
1451
+ content: [{ type: 'text', text: JSON.stringify({
1452
+ project: found.name,
1453
+ total_exclusions: exclusions.patterns.length,
1454
+ patterns: exclusions.patterns,
1455
+ }, null, 2) }],
1456
+ };
1457
+ }
1458
+ if (!excludePaths || excludePaths.length === 0) {
1459
+ return { content: [{ type: 'text', text: 'No paths provided for exclude/include.' }], isError: true };
1460
+ }
1461
+ if (exclude_action === 'exclude') {
1462
+ const addedPatterns = [];
1463
+ const alreadyExcluded = [];
1464
+ let filesRemoved = 0;
1465
+ for (const pattern of excludePaths) {
1466
+ const normalizedPattern = pattern.replace(/\\/g, '/');
1467
+ if (exclusions.patterns.some(p => p.pattern === normalizedPattern)) {
1468
+ alreadyExcluded.push(normalizedPattern);
1469
+ continue;
1470
+ }
1471
+ exclusions.patterns.push({ pattern: normalizedPattern, reason, addedAt: new Date().toISOString() });
1472
+ addedPatterns.push(normalizedPattern);
1473
+ const results = await vectorStore.searchByText(normalizedPattern, found.id, 1000);
1474
+ for (const result of results) {
1475
+ if (this.matchesExclusionPattern(result.document.filePath.replace(/\\/g, '/'), normalizedPattern)) {
1476
+ await vectorStore.delete(result.document.id);
1477
+ filesRemoved++;
1478
+ }
2206
1479
  }
2207
- // Load the knowledge graph for this project
2208
- const knowledgeGraph = new knowledge_graph_1.SemanticKnowledgeGraph(projectRecord.path);
2209
- // Run architectural insight detection (includes dead code, god classes, circular deps, etc.)
2210
- const allInsights = await knowledgeGraph.detectArchitecturalInsights();
2211
- // Filter by requested patterns
2212
- const patterns = include_patterns || ['dead_code', 'god_class', 'circular_deps', 'feature_envy', 'coupling'];
2213
- const patternMapping = {
2214
- 'dead_code': ['Dead Code'],
2215
- 'god_class': ['God Class'],
2216
- 'circular_deps': ['Circular Dependencies', 'Circular Dependency'],
2217
- 'feature_envy': ['Feature Envy'],
2218
- 'coupling': ['High Coupling', 'Inappropriate Intimacy'],
2219
- };
2220
- const selectedPatterns = patterns.flatMap(p => patternMapping[p] || []);
2221
- const filteredInsights = allInsights.filter(insight => selectedPatterns.some(pattern => insight.pattern?.toLowerCase().includes(pattern.toLowerCase()) ||
2222
- insight.description?.toLowerCase().includes(pattern.toLowerCase())));
2223
- // Group by type
2224
- const deadCode = filteredInsights.filter(i => i.pattern === 'Dead Code');
2225
- const antiPatterns = filteredInsights.filter(i => i.type === 'anti_pattern' && i.pattern !== 'Dead Code');
2226
- const couplingIssues = filteredInsights.filter(i => i.type === 'coupling_issue');
2227
- return {
2228
- content: [{
2229
- type: 'text',
2230
- text: JSON.stringify({
2231
- project: projectRecord.name,
2232
- summary: {
2233
- total_issues: filteredInsights.length,
2234
- dead_code_count: deadCode.length,
2235
- anti_patterns_count: antiPatterns.length,
2236
- coupling_issues_count: couplingIssues.length,
2237
- },
2238
- dead_code: deadCode.slice(0, 20).map(d => ({
2239
- type: d.pattern,
2240
- description: d.description,
2241
- confidence: `${((d.confidence || 0) * 100).toFixed(0)}%`,
2242
- impact: d.impact,
2243
- recommendation: d.recommendation,
2244
- })),
2245
- anti_patterns: antiPatterns.slice(0, 10).map(a => ({
2246
- type: a.pattern,
2247
- description: a.description,
2248
- confidence: `${((a.confidence || 0) * 100).toFixed(0)}%`,
2249
- impact: a.impact,
2250
- recommendation: a.recommendation,
2251
- })),
2252
- coupling_issues: couplingIssues.slice(0, 10).map(c => ({
2253
- type: c.pattern,
2254
- description: c.description,
2255
- confidence: `${((c.confidence || 0) * 100).toFixed(0)}%`,
2256
- impact: c.impact,
2257
- recommendation: c.recommendation,
2258
- })),
2259
- }, null, 2),
2260
- }],
2261
- };
2262
1480
  }
2263
- catch (error) {
2264
- return {
2265
- content: [{
2266
- type: 'text',
2267
- text: this.formatErrorMessage('Find dead code', error instanceof Error ? error : String(error), { projectPath: project }),
2268
- }],
2269
- isError: true,
2270
- };
1481
+ exclusions.lastModified = new Date().toISOString();
1482
+ fs.writeFileSync(exclusionsPath, JSON.stringify(exclusions, null, 2));
1483
+ await vectorStore.flush();
1484
+ return {
1485
+ content: [{ type: 'text', text: JSON.stringify({
1486
+ success: true, action: 'exclude', project: found.name,
1487
+ patterns_added: addedPatterns,
1488
+ already_excluded: alreadyExcluded.length > 0 ? alreadyExcluded : undefined,
1489
+ files_removed_from_index: filesRemoved,
1490
+ total_exclusions: exclusions.patterns.length,
1491
+ }, null, 2) }],
1492
+ };
1493
+ }
1494
+ if (exclude_action === 'include') {
1495
+ const removedPatterns = [];
1496
+ const notFound = [];
1497
+ for (const pattern of excludePaths) {
1498
+ const normalizedPattern = pattern.replace(/\\/g, '/');
1499
+ const index = exclusions.patterns.findIndex(p => p.pattern === normalizedPattern);
1500
+ if (index >= 0) {
1501
+ exclusions.patterns.splice(index, 1);
1502
+ removedPatterns.push(normalizedPattern);
1503
+ }
1504
+ else {
1505
+ notFound.push(normalizedPattern);
1506
+ }
2271
1507
  }
2272
- });
1508
+ exclusions.lastModified = new Date().toISOString();
1509
+ fs.writeFileSync(exclusionsPath, JSON.stringify(exclusions, null, 2));
1510
+ return {
1511
+ content: [{ type: 'text', text: JSON.stringify({
1512
+ success: true, action: 'include', project: found.name,
1513
+ patterns_removed: removedPatterns,
1514
+ not_found: notFound.length > 0 ? notFound : undefined,
1515
+ total_exclusions: exclusions.patterns.length,
1516
+ next_step: removedPatterns.length > 0 ? 'index({action: "sync", project: "...", full_reindex: true})' : undefined,
1517
+ }, null, 2) }],
1518
+ };
1519
+ }
1520
+ return { content: [{ type: 'text', text: `Unknown exclude_action: ${exclude_action}` }], isError: true };
2273
1521
  }
2274
- /**
2275
- * Start the MCP server
2276
- */
1522
+ // ============================================================
1523
+ // SERVER LIFECYCLE
1524
+ // ============================================================
2277
1525
  async start() {
2278
- // Use stderr for logging since stdout is for JSON-RPC
2279
1526
  console.error('Starting CodeSeeker MCP server...');
2280
- // Initialize storage manager first to ensure singleton is ready
2281
1527
  const storageManager = await (0, storage_1.getStorageManager)();
2282
1528
  console.error(`Storage mode: ${storageManager.getMode()}`);
2283
1529
  const transport = new stdio_js_1.StdioServerTransport();
2284
1530
  await this.server.connect(transport);
2285
1531
  console.error('CodeSeeker MCP server running on stdio');
2286
1532
  }
2287
- /**
2288
- * Graceful shutdown - flush and close all storage before exit
2289
- */
2290
1533
  async shutdown() {
2291
1534
  console.error('Shutting down CodeSeeker MCP server...');
2292
1535
  try {
2293
1536
  const storageManager = await (0, storage_1.getStorageManager)();
2294
- // Flush first to ensure data is saved
2295
1537
  await storageManager.flushAll();
2296
1538
  console.error('Storage flushed successfully');
2297
- // Close to stop interval timers and release resources
2298
1539
  await storageManager.closeAll();
2299
1540
  console.error('Storage closed successfully');
2300
1541
  }
@@ -2310,16 +1551,13 @@ exports.CodeSeekerMcpServer = CodeSeekerMcpServer;
2310
1551
  async function startMcpServer() {
2311
1552
  const server = new CodeSeekerMcpServer();
2312
1553
  let isShuttingDown = false;
2313
- // Register signal handlers for graceful shutdown
2314
1554
  const shutdown = async (signal) => {
2315
- // Prevent multiple shutdown attempts
2316
1555
  if (isShuttingDown) {
2317
1556
  console.error(`Already shutting down, ignoring ${signal}`);
2318
1557
  return;
2319
1558
  }
2320
1559
  isShuttingDown = true;
2321
1560
  console.error(`\nReceived ${signal}, shutting down gracefully...`);
2322
- // Set a hard timeout to force exit if shutdown takes too long
2323
1561
  const forceExitTimeout = setTimeout(() => {
2324
1562
  console.error('Shutdown timeout, forcing exit...');
2325
1563
  process.exit(1);
@@ -2337,29 +1575,20 @@ async function startMcpServer() {
2337
1575
  };
2338
1576
  process.on('SIGINT', () => shutdown('SIGINT'));
2339
1577
  process.on('SIGTERM', () => shutdown('SIGTERM'));
2340
- // CRITICAL: Handle stdin close - this is how MCP clients signal disconnect
2341
- // On Windows, signals are unreliable, so stdin close is the primary shutdown mechanism
2342
1578
  process.stdin.on('close', () => shutdown('stdin-close'));
2343
1579
  process.stdin.on('end', () => shutdown('stdin-end'));
2344
- // Also handle stdin errors (broken pipe, etc.)
2345
1580
  process.stdin.on('error', (err) => {
2346
- // EPIPE and similar errors mean the parent process disconnected
2347
1581
  console.error(`stdin error: ${err.message}`);
2348
1582
  shutdown('stdin-error');
2349
1583
  });
2350
- // Handle Windows-specific signals
2351
1584
  if (process.platform === 'win32') {
2352
1585
  process.on('SIGHUP', () => shutdown('SIGHUP'));
2353
- // Windows: also listen for parent process disconnect via stdin
2354
- // Resume stdin to ensure we receive close/end events
2355
1586
  process.stdin.resume();
2356
1587
  }
2357
- // Handle uncaught exceptions - try to flush before crashing
2358
1588
  process.on('uncaughtException', async (error) => {
2359
1589
  console.error('Uncaught exception:', error);
2360
1590
  await shutdown('uncaughtException');
2361
1591
  });
2362
- // Handle unhandled promise rejections
2363
1592
  process.on('unhandledRejection', async (reason) => {
2364
1593
  console.error('Unhandled rejection:', reason);
2365
1594
  await shutdown('unhandledRejection');