codeseeker 1.11.1 → 2.0.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.
Files changed (31) hide show
  1. package/README.md +170 -181
  2. package/dist/cli/commands/services/semantic-search-orchestrator.d.ts +36 -4
  3. package/dist/cli/commands/services/semantic-search-orchestrator.d.ts.map +1 -1
  4. package/dist/cli/commands/services/semantic-search-orchestrator.js +238 -40
  5. package/dist/cli/commands/services/semantic-search-orchestrator.js.map +1 -1
  6. package/dist/cli/services/analysis/deduplication/duplicate-code-detector.js +1 -1
  7. package/dist/cli/services/analysis/deduplication/duplicate-code-detector.js.map +1 -1
  8. package/dist/cli/services/monitoring/file-scanning/file-scanner-config.json +126 -0
  9. package/dist/cli/services/search/ast-chunker.d.ts +37 -0
  10. package/dist/cli/services/search/ast-chunker.d.ts.map +1 -0
  11. package/dist/cli/services/search/ast-chunker.js +171 -0
  12. package/dist/cli/services/search/ast-chunker.js.map +1 -0
  13. package/dist/mcp/indexing-service.d.ts +0 -4
  14. package/dist/mcp/indexing-service.d.ts.map +1 -1
  15. package/dist/mcp/indexing-service.js +8 -25
  16. package/dist/mcp/indexing-service.js.map +1 -1
  17. package/dist/mcp/mcp-server.d.ts +23 -9
  18. package/dist/mcp/mcp-server.d.ts.map +1 -1
  19. package/dist/mcp/mcp-server.js +370 -328
  20. package/dist/mcp/mcp-server.js.map +1 -1
  21. package/dist/storage/embedded/minisearch-text-store.d.ts.map +1 -1
  22. package/dist/storage/embedded/minisearch-text-store.js +3 -2
  23. package/dist/storage/embedded/minisearch-text-store.js.map +1 -1
  24. package/dist/storage/embedded/sqlite-vector-store.d.ts.map +1 -1
  25. package/dist/storage/embedded/sqlite-vector-store.js +7 -1
  26. package/dist/storage/embedded/sqlite-vector-store.js.map +1 -1
  27. package/dist/storage/storage-manager.d.ts +2 -1
  28. package/dist/storage/storage-manager.d.ts.map +1 -1
  29. package/dist/storage/storage-manager.js +8 -2
  30. package/dist/storage/storage-manager.js.map +1 -1
  31. package/package.json +2 -2
@@ -6,10 +6,9 @@
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)
9
+ * Single sentinel tool: codeseeker
10
+ * Routes to: search (q), symbol lookup (sym), graph traversal (graph),
11
+ * analysis (analyze), index management (index)
13
12
  *
14
13
  * Usage:
15
14
  * codeseeker serve --mcp
@@ -403,312 +402,264 @@ class CodeSeekerMcpServer {
403
402
  // TOOL REGISTRATION - 3 CONSOLIDATED TOOLS
404
403
  // ============================================================
405
404
  registerTools() {
406
- this.registerSearchTool();
407
- this.registerAnalyzeTool();
408
- this.registerIndexTool();
405
+ this.registerSentinelTool();
409
406
  }
410
407
  // ============================================================
411
- // TOOL 1: search
412
- // Combines: search, search_and_read, read_with_context
408
+ // SENTINEL TOOL: codeseeker
409
+ // Hierarchical schema each action carries its own nested params
413
410
  // ============================================================
414
- registerSearchTool() {
415
- this.server.registerTool('search', {
416
- description: 'Semantic code search. Finds code by meaning, not just text. ' +
417
- 'Pass query to search, add read=true to include file contents, or pass filepath instead to read a file with related context.',
411
+ registerSentinelTool() {
412
+ this.server.registerTool('codeseeker', {
413
+ description: 'Code intelligence: search (q), symbol lookup (sym), graph traversal (graph), analysis (analyze), index management (index).',
418
414
  inputSchema: {
419
- query: zod_1.z.string().optional().describe('Natural language search query'),
420
- filepath: zod_1.z.string().optional().describe('File path to read with semantic context (alternative to query)'),
415
+ action: zod_1.z.enum(['search', 'sym', 'graph', 'analyze', 'index'])
416
+ .describe('Routing key fill only the matching nested param group'),
421
417
  project: zod_1.z.string().optional().describe('Project path or name'),
422
- read: zod_1.z.boolean().optional().default(false).describe('Include file contents in results'),
423
- limit: zod_1.z.number().optional().default(10).describe('Max results'),
424
- max_lines: zod_1.z.number().optional().default(500).describe('Max lines per file when read=true'),
425
- search_type: zod_1.z.enum(['hybrid', 'fts', 'vector', 'graph']).optional().default('hybrid')
426
- .describe('Search method'),
427
- mode: zod_1.z.enum(['full', 'exists']).optional().default('full')
428
- .describe('"exists" returns quick yes/no'),
418
+ search: zod_1.z.object({
419
+ q: zod_1.z.string().describe('Natural language query'),
420
+ exists: zod_1.z.boolean().optional().default(false).describe('Quick yes/no returns {found,count,top_file}'),
421
+ full: zod_1.z.boolean().optional().default(false).describe('Add snippet to each result (default: summary only)'),
422
+ limit: zod_1.z.number().optional().default(10),
423
+ type: zod_1.z.enum(['hybrid', 'fts', 'vector']).optional().default('hybrid'),
424
+ }).optional().describe('Params for action=search'),
425
+ sym: zod_1.z.object({
426
+ name: zod_1.z.string().describe('Symbol name (exact or partial)'),
427
+ full: zod_1.z.boolean().optional().default(false).describe('Include resolved relationships'),
428
+ }).optional().describe('Params for action=sym'),
429
+ graph: zod_1.z.object({
430
+ seed: zod_1.z.string().optional().describe('Seed file (relative path)'),
431
+ q: zod_1.z.string().optional().describe('Query to find seed files semantically'),
432
+ depth: zod_1.z.number().optional().default(1).describe('Traversal depth 1-3'),
433
+ rel: zod_1.z.array(zod_1.z.enum(['imports', 'exports', 'calls', 'extends', 'implements', 'contains', 'uses', 'depends_on'])).optional(),
434
+ dir: zod_1.z.enum(['in', 'out', 'both']).optional().default('both'),
435
+ max: zod_1.z.number().optional().default(50),
436
+ }).optional().describe('Params for action=graph'),
437
+ analyze: zod_1.z.object({
438
+ kind: zod_1.z.enum(['duplicates', 'dead_code', 'standards']).describe('Analysis type'),
439
+ threshold: zod_1.z.number().optional().default(0.80).describe('Similarity threshold for duplicates'),
440
+ min_lines: zod_1.z.number().optional().default(5),
441
+ patterns: zod_1.z.array(zod_1.z.enum(['dead_code', 'god_class', 'circular_deps', 'feature_envy', 'coupling'])).optional(),
442
+ category: zod_1.z.enum(['validation', 'error-handling', 'logging', 'testing', 'all']).optional().default('all'),
443
+ }).optional().describe('Params for action=analyze'),
444
+ index: zod_1.z.object({
445
+ op: zod_1.z.enum(['init', 'sync', 'status', 'parsers', 'exclude']).describe('Operation'),
446
+ path: zod_1.z.string().optional().describe('Project dir (op=init)'),
447
+ name: zod_1.z.string().optional().describe('Project name (op=init)'),
448
+ changes: zod_1.z.array(zod_1.z.object({ type: zod_1.z.enum(['created', 'modified', 'deleted']), path: zod_1.z.string() })).optional(),
449
+ full_reindex: zod_1.z.boolean().optional().default(false),
450
+ languages: zod_1.z.array(zod_1.z.string()).optional(),
451
+ list_available: zod_1.z.boolean().optional().default(false),
452
+ exclude_op: zod_1.z.enum(['exclude', 'include', 'list']).optional(),
453
+ paths: zod_1.z.array(zod_1.z.string()).optional(),
454
+ reason: zod_1.z.string().optional(),
455
+ }).optional().describe('Params for action=index'),
429
456
  },
430
- }, async ({ query, filepath, project, read = false, limit = 10, max_lines = 500, search_type = 'hybrid', mode = 'full' }) => {
457
+ }, async (params) => {
431
458
  try {
432
- // Dispatch: filepath → read-with-context, read → search-and-read, else → search
433
- if (filepath) {
434
- return await this.handleReadWithContext(filepath, project, !read ? true : read);
435
- }
436
- if (!query) {
437
- return {
438
- content: [{ type: 'text', text: 'Provide either query or filepath parameter.' }],
439
- isError: true,
440
- };
441
- }
442
- if (read) {
443
- return await this.handleSearchAndRead(query, project, Math.min(limit, 3), Math.min(max_lines, 1000));
459
+ switch (params.action) {
460
+ case 'search': {
461
+ const s = params.search;
462
+ if (!s)
463
+ return { content: [{ type: 'text', text: 'Provide search params.' }], isError: true };
464
+ return await this.handleSearch(s.q, params.project, s.limit ?? 10, s.type ?? 'hybrid', s.exists ?? false, s.full ?? false);
465
+ }
466
+ case 'sym': {
467
+ const s = params.sym;
468
+ if (!s)
469
+ return { content: [{ type: 'text', text: 'Provide sym params.' }], isError: true };
470
+ return await this.handleSymbolLookup(s.name, params.project, s.full ?? false);
471
+ }
472
+ case 'graph': {
473
+ const g = params.graph;
474
+ if (!g)
475
+ return { content: [{ type: 'text', text: 'Provide graph params.' }], isError: true };
476
+ const { projectPath, error } = await this.resolveProject(params.project);
477
+ if (error)
478
+ return error;
479
+ return await this.handleShowDependencies({
480
+ project: projectPath,
481
+ filepath: g.seed,
482
+ query: g.q,
483
+ depth: g.depth,
484
+ relationship_types: g.rel,
485
+ direction: g.dir,
486
+ max_nodes: g.max,
487
+ });
488
+ }
489
+ case 'analyze': {
490
+ const a = params.analyze;
491
+ if (!a)
492
+ return { content: [{ type: 'text', text: 'Provide analyze params.' }], isError: true };
493
+ if (!params.project)
494
+ return { content: [{ type: 'text', text: 'project required for analyze.' }], isError: true };
495
+ switch (a.kind) {
496
+ case 'duplicates': return await this.handleFindDuplicates({ project: params.project, similarity_threshold: a.threshold, min_lines: a.min_lines });
497
+ case 'dead_code': return await this.handleFindDeadCode({ project: params.project, include_patterns: a.patterns });
498
+ case 'standards': return await this.handleStandards({ project: params.project, category: a.category });
499
+ }
500
+ break;
501
+ }
502
+ case 'index': {
503
+ const ix = params.index;
504
+ if (!ix)
505
+ return { content: [{ type: 'text', text: 'Provide index params.' }], isError: true };
506
+ switch (ix.op) {
507
+ case 'init': return await this.handleIndexInit({ path: ix.path, name: ix.name || params.project });
508
+ case 'sync': return await this.handleSync({ project: params.project, changes: ix.changes, full_reindex: ix.full_reindex });
509
+ case 'status': return await this.handleProjects();
510
+ case 'parsers': return await this.handleInstallParsers({ project: params.project, languages: ix.languages, list_available: ix.list_available });
511
+ case 'exclude': return await this.handleExclude({ project: params.project, exclude_action: ix.exclude_op, paths: ix.paths, reason: ix.reason });
512
+ }
513
+ break;
514
+ }
444
515
  }
445
- return await this.handleSearch(query, project, limit, search_type, mode);
516
+ return { content: [{ type: 'text', text: `Unknown action` }], isError: true };
446
517
  }
447
518
  catch (error) {
448
519
  return {
449
- content: [{ type: 'text', text: this.formatErrorMessage('Search', error instanceof Error ? error : String(error), { projectPath: project }) }],
520
+ content: [{ type: 'text', text: this.formatErrorMessage('CodeSeeker', error instanceof Error ? error : String(error), { projectPath: params.project }) }],
450
521
  isError: true,
451
522
  };
452
523
  }
453
524
  });
454
525
  }
455
- async handleSearch(query, project, limit, search_type, mode) {
456
- const { projectPath, projectRecord, error } = await this.resolveProject(project);
457
- if (error)
458
- return error;
459
- const indexCheck = await this.verifyIndexed(projectPath, projectRecord);
460
- if (indexCheck.error)
461
- return indexCheck.error;
462
- // Check cache (only for 'full' mode)
463
- let results;
464
- let fromCache = false;
465
- const cacheProjectId = projectRecord?.id || this.generateProjectId(projectPath);
466
- if (mode === 'full') {
467
- const cached = await this.queryCache.get(query, cacheProjectId, search_type);
468
- if (cached) {
469
- results = cached.results;
470
- fromCache = true;
526
+ /** Extract the first meaningful declaration line from a code chunk */
527
+ extractSignature(content) {
528
+ const skip = /^\s*($|\/\/|\/\*|\*|#!|import |from |require\(|using |namespace )/;
529
+ for (const line of content.split('\n')) {
530
+ if (!skip.test(line))
531
+ return line.trim().substring(0, 120);
532
+ }
533
+ return content.split('\n')[0]?.trim().substring(0, 120) || '';
534
+ }
535
+ // ── Public test-accessible handlers ───────────────────────────────────────
536
+ // ── Public test-accessible handlers ───────────────────────────────────────
537
+ /** Search and return file contents for top results. Used by tests via `as any`. */
538
+ async handleSearchAndRead(query, project, limit, tokenBudget) {
539
+ const inner = await this.handleSearch(query, project, limit, 'hybrid', false, false);
540
+ const text = inner.content[0]?.type === 'text' ? inner.content[0].text : '';
541
+ try {
542
+ const parsed = JSON.parse(text);
543
+ if (!parsed.results) {
544
+ return { content: [{ type: 'text', text: JSON.stringify({ query, project, files_found: 0, results: [] }) }], isError: false };
471
545
  }
472
- else {
473
- results = await this.searchOrchestrator.performSemanticSearch(query, projectPath, search_type);
474
- if (results.length > 0) {
475
- await this.queryCache.set(query, cacheProjectId, results, search_type);
546
+ const charBudget = tokenBudget * 4;
547
+ let remaining = charBudget;
548
+ const results = parsed.results.map((r) => {
549
+ const filepath = r.file && require('path').isAbsolute(r.file) ? r.file
550
+ : require('path').join(parsed.project_path ?? '', r.file);
551
+ let content = '';
552
+ try {
553
+ const raw = require('fs').readFileSync(filepath, 'utf-8');
554
+ content = raw.substring(0, Math.min(raw.length, remaining));
555
+ remaining = Math.max(0, remaining - content.length);
476
556
  }
477
- }
557
+ catch {
558
+ content = '';
559
+ }
560
+ return { file: r.file, score: r.score, content };
561
+ });
562
+ return { content: [{ type: 'text', text: JSON.stringify({ query, project, files_found: results.length, results }) }], isError: false };
478
563
  }
479
- else {
480
- results = await this.searchOrchestrator.performSemanticSearch(query, projectPath, search_type);
564
+ catch {
565
+ return { content: [{ type: 'text', text: JSON.stringify({ query, project, files_found: 0, results: [] }) }], isError: false };
481
566
  }
482
- const limitedResults = results.slice(0, mode === 'exists' ? 5 : limit);
483
- if (limitedResults.length === 0) {
484
- if (mode === 'exists') {
485
- return {
486
- content: [{ type: 'text', text: JSON.stringify({ exists: false, query, project: projectPath, message: 'No matching code found' }, null, 2) }],
487
- };
567
+ }
568
+ /** Read a file from disk and return its content. Used by tests via `as any`. */
569
+ async handleReadWithContext(filepath, _project, includeSummary) {
570
+ const fs = require('fs');
571
+ try {
572
+ const content = fs.readFileSync(filepath, 'utf-8');
573
+ const lineCount = content.split('\n').length;
574
+ const resp = {
575
+ file: filepath,
576
+ filepath,
577
+ content,
578
+ line_count: lineCount,
579
+ };
580
+ if (includeSummary) {
581
+ resp.related_chunks = []; // graph neighbors not available in disk-only mode
488
582
  }
489
583
  return {
490
- content: [{ type: 'text', text: `No results for: "${query}". Try different terms or reindex.` }],
584
+ content: [{ type: 'text', text: JSON.stringify(resp) }],
585
+ isError: false
491
586
  };
492
587
  }
493
- if (mode === 'exists') {
494
- const topResult = limitedResults[0];
495
- const absolutePath = path.isAbsolute(topResult.file) ? topResult.file : path.join(projectPath, topResult.file);
588
+ catch {
496
589
  return {
497
- content: [{ type: 'text', text: JSON.stringify({
498
- exists: true, query, project: projectPath,
499
- total_matches: results.length, top_file: absolutePath,
500
- top_score: Math.round(topResult.similarity * 100) / 100,
501
- }, null, 2) }],
590
+ content: [{ type: 'text', text: JSON.stringify({ file: filepath, error: 'File not found' }) }],
591
+ isError: true
502
592
  };
503
593
  }
504
- const formattedResults = limitedResults.map((r, i) => {
505
- const absolutePath = path.isAbsolute(r.file) ? r.file : path.join(projectPath, r.file);
506
- return {
507
- rank: i + 1,
508
- file: absolutePath,
509
- relative_path: r.file,
510
- score: Math.round(r.similarity * 100) / 100,
511
- type: r.type,
512
- match_source: r.debug?.matchSource || 'hybrid',
513
- chunk: r.content.substring(0, 500) + (r.content.length > 500 ? '...' : ''),
514
- lines: r.lineStart && r.lineEnd ? `${r.lineStart}-${r.lineEnd}` : undefined,
515
- };
516
- });
517
- const wasLimited = results.length > limit;
518
- const response = {
519
- query, project: projectPath,
520
- total_results: limitedResults.length,
521
- search_type, results: formattedResults,
522
- };
523
- if (fromCache)
524
- response.cached = true;
525
- if (wasLimited) {
526
- response.truncated = true;
527
- response.total_available = results.length;
528
- }
529
- return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
530
594
  }
531
- async handleSearchAndRead(query, project, max_files, max_lines) {
532
- const fileLimit = Math.min(max_files, 3);
533
- const lineLimit = Math.min(max_lines, 1000);
595
+ async handleSearch(query, project, limit, search_type, exists, full) {
534
596
  const { projectPath, projectRecord, error } = await this.resolveProject(project);
535
597
  if (error)
536
598
  return error;
537
599
  const indexCheck = await this.verifyIndexed(projectPath, projectRecord);
538
600
  if (indexCheck.error)
539
601
  return indexCheck.error;
540
- const results = await this.searchOrchestrator.performSemanticSearch(query, projectPath);
541
- if (results.length === 0) {
542
- return {
543
- content: [{ type: 'text', text: JSON.stringify({ query, project: projectPath, found: false, message: 'No matching code found.' }, null, 2) }],
544
- };
545
- }
546
- // Get unique files
547
- const seenFiles = new Set();
548
- const uniqueResults = [];
549
- for (const r of results) {
550
- const normalizedPath = r.file.replace(/\\/g, '/');
551
- if (!seenFiles.has(normalizedPath)) {
552
- seenFiles.add(normalizedPath);
553
- uniqueResults.push(r);
554
- if (uniqueResults.length >= fileLimit)
555
- break;
556
- }
557
- }
558
- const files = [];
559
- for (const result of uniqueResults) {
560
- const absolutePath = path.isAbsolute(result.file) ? result.file : path.join(projectPath, result.file);
561
- try {
562
- if (!fs.existsSync(absolutePath))
563
- continue;
564
- const content = fs.readFileSync(absolutePath, 'utf-8');
565
- const lines = content.split('\n');
566
- const truncated = lines.length > lineLimit;
567
- const displayLines = truncated ? lines.slice(0, lineLimit) : lines;
568
- const numberedContent = displayLines.map((line, i) => `${String(i + 1).padStart(4)}│ ${line}`).join('\n');
569
- files.push({
570
- file: absolutePath,
571
- relative_path: result.file,
572
- score: Math.round(result.similarity * 100) / 100,
573
- file_type: result.type,
574
- match_source: result.debug?.matchSource || 'hybrid',
575
- line_count: lines.length,
576
- content: numberedContent + (truncated ? `\n... (truncated at ${lineLimit} lines)` : ''),
577
- truncated,
578
- });
579
- }
580
- catch {
581
- continue;
602
+ const cacheProjectId = projectRecord?.id || this.generateProjectId(projectPath);
603
+ const cap = exists ? 5 : limit;
604
+ // exists mode: skip cache, quick check
605
+ if (exists) {
606
+ const results = await this.searchOrchestrator.performSemanticSearch(query, projectPath, search_type);
607
+ if (results.length === 0) {
608
+ return { content: [{ type: 'text', text: JSON.stringify({ found: false, query }) }] };
582
609
  }
583
- }
584
- if (files.length === 0) {
610
+ const top = results[0];
585
611
  return {
586
- content: [{ type: 'text', text: JSON.stringify({ query, project: projectPath, found: true, readable: false, message: 'Found matching files but could not read them.' }, null, 2) }],
612
+ content: [{ type: 'text', text: JSON.stringify({
613
+ found: true, count: results.length,
614
+ top_file: path.relative(projectPath, path.isAbsolute(top.file) ? top.file : path.join(projectPath, top.file)),
615
+ score: Math.round(top.similarity * 100) / 100,
616
+ }) }],
587
617
  };
588
618
  }
589
- return {
590
- content: [{ type: 'text', text: JSON.stringify({
591
- query, project: projectPath,
592
- files_found: results.length, files_returned: files.length,
593
- results: files,
594
- }, null, 2) }],
595
- };
596
- }
597
- async handleReadWithContext(filepath, project, include_related) {
598
- const storageManager = await (0, storage_1.getStorageManager)();
599
- const projectStore = storageManager.getProjectStore();
600
- let projectPath;
601
- if (project) {
602
- const projects = await projectStore.list();
603
- const found = projects.find(p => p.name === project || p.path === project || path.basename(p.path) === project);
604
- projectPath = found?.path || process.cwd();
619
+ // full search — use cache
620
+ let results;
621
+ let fromCache = false;
622
+ const cached = await this.queryCache.get(query, cacheProjectId, search_type);
623
+ if (cached) {
624
+ results = cached.results;
625
+ fromCache = true;
605
626
  }
606
627
  else {
607
- projectPath = process.cwd();
628
+ results = await this.searchOrchestrator.performSemanticSearch(query, projectPath, search_type);
629
+ if (results.length > 0)
630
+ await this.queryCache.set(query, cacheProjectId, results, search_type);
608
631
  }
609
- const absolutePath = path.isAbsolute(filepath) ? filepath : path.join(projectPath, filepath);
610
- if (!fs.existsSync(absolutePath)) {
611
- return { content: [{ type: 'text', text: `File not found: ${absolutePath}` }], isError: true };
612
- }
613
- const content = fs.readFileSync(absolutePath, 'utf-8');
614
- let relatedChunks = [];
615
- if (include_related) {
616
- const lines = content.split('\n');
617
- const meaningfulLines = [];
618
- for (const line of lines) {
619
- const trimmed = line.trim();
620
- if (!trimmed)
621
- continue;
622
- if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))
623
- continue;
624
- if (trimmed.startsWith('import ') || trimmed.startsWith('from ') || trimmed.startsWith('require('))
625
- continue;
626
- if (trimmed.startsWith('#') && !trimmed.startsWith('##'))
627
- continue;
628
- if (trimmed.startsWith('using ') || trimmed.startsWith('namespace '))
629
- continue;
630
- meaningfulLines.push(trimmed);
631
- if (meaningfulLines.length >= 5)
632
- break;
633
- }
634
- const fileName = path.basename(filepath);
635
- const fileNameQuery = fileName.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
636
- const contentQuery = meaningfulLines.join(' ').substring(0, 200);
637
- const searchQuery = `${fileNameQuery} ${contentQuery}`.trim();
638
- const results = await this.searchOrchestrator.performSemanticSearch(searchQuery || fileNameQuery, projectPath);
639
- relatedChunks = results
640
- .filter(r => !r.file.endsWith(path.basename(filepath)))
641
- .slice(0, 5)
642
- .map(r => ({
643
- file: r.file,
644
- chunk: r.content.substring(0, 300) + (r.content.length > 300 ? '...' : ''),
645
- score: Math.round(r.similarity * 100) / 100,
646
- }));
632
+ if (results.length === 0) {
633
+ return { content: [{ type: 'text', text: `No results for: "${query}". Try different terms or reindex.` }] };
647
634
  }
648
- return {
649
- content: [{ type: 'text', text: JSON.stringify({
650
- filepath: path.relative(projectPath, absolutePath),
651
- content: content.length > 10000 ? content.substring(0, 10000) + '\n... (truncated)' : content,
652
- line_count: content.split('\n').length,
653
- related_chunks: include_related ? relatedChunks : undefined,
654
- }, null, 2) }],
635
+ const limited = results.slice(0, cap);
636
+ const formatted = limited.map((r, i) => {
637
+ const rel = path.isAbsolute(r.file) ? path.relative(projectPath, r.file) : r.file;
638
+ const node = {
639
+ rank: i + 1,
640
+ file: rel,
641
+ score: Math.round(r.similarity * 100) / 100,
642
+ lines: r.lineStart && r.lineEnd ? `${r.lineStart}-${r.lineEnd}` : undefined,
643
+ sig: this.extractSignature(r.content),
644
+ };
645
+ if (full)
646
+ node.snippet = r.content.substring(0, 300) + (r.content.length > 300 ? '…' : '');
647
+ return node;
648
+ });
649
+ const resp = {
650
+ query, project: path.basename(projectPath),
651
+ count: limited.length, results: formatted,
655
652
  };
653
+ if (fromCache)
654
+ resp.cached = true;
655
+ if (results.length > cap) {
656
+ resp.more = results.length - cap;
657
+ }
658
+ return { content: [{ type: 'text', text: JSON.stringify(resp) }] };
656
659
  }
657
660
  // ============================================================
658
- // TOOL 2: analyze
659
- // Combines: show_dependencies, find_duplicates, find_dead_code, standards
661
+ // HANDLERS: graph, duplicates, dead_code, standards
660
662
  // ============================================================
661
- registerAnalyzeTool() {
662
- this.server.registerTool('analyze', {
663
- description: 'Code analysis. Actions: "dependencies" (imports/calls/extends graph), ' +
664
- '"dead_code" (unused code, anti-patterns), "duplicates" (similar code), ' +
665
- '"standards" (auto-detected coding patterns).',
666
- inputSchema: {
667
- action: zod_1.z.enum(['dependencies', 'dead_code', 'duplicates', 'standards']).describe('Analysis type'),
668
- project: zod_1.z.string().describe('Project path or name'),
669
- // dependencies params
670
- filepath: zod_1.z.string().optional().describe('File for dependency analysis'),
671
- filepaths: zod_1.z.array(zod_1.z.string()).optional().describe('Multiple files for dependency analysis'),
672
- query: zod_1.z.string().optional().describe('Search query to find seed files for dependencies'),
673
- depth: zod_1.z.number().optional().default(1).describe('Relationship hops (1-3)'),
674
- relationship_types: zod_1.z.array(zod_1.z.enum([
675
- 'imports', 'exports', 'calls', 'extends', 'implements', 'contains', 'uses', 'depends_on'
676
- ])).optional().describe('Filter relationship types'),
677
- direction: zod_1.z.enum(['in', 'out', 'both']).optional().default('both').describe('Relationship direction'),
678
- max_nodes: zod_1.z.number().optional().default(50).describe('Max nodes'),
679
- // duplicates params
680
- similarity_threshold: zod_1.z.number().optional().default(0.80).describe('Min similarity for duplicates (0-1)'),
681
- min_lines: zod_1.z.number().optional().default(5).describe('Min lines for duplicate analysis'),
682
- // dead_code params
683
- include_patterns: zod_1.z.array(zod_1.z.enum(['dead_code', 'god_class', 'circular_deps', 'feature_envy', 'coupling'])).optional()
684
- .describe('Anti-patterns to detect'),
685
- // standards params
686
- category: zod_1.z.enum(['validation', 'error-handling', 'logging', 'testing', 'all']).optional().default('all')
687
- .describe('Standards category'),
688
- },
689
- }, async (params) => {
690
- try {
691
- switch (params.action) {
692
- case 'dependencies':
693
- return await this.handleShowDependencies(params);
694
- case 'dead_code':
695
- return await this.handleFindDeadCode(params);
696
- case 'duplicates':
697
- return await this.handleFindDuplicates(params);
698
- case 'standards':
699
- return await this.handleStandards(params);
700
- default:
701
- return { content: [{ type: 'text', text: `Unknown action: ${params.action}` }], isError: true };
702
- }
703
- }
704
- catch (error) {
705
- return {
706
- content: [{ type: 'text', text: this.formatErrorMessage('Analyze', error instanceof Error ? error : String(error), { projectPath: params.project }) }],
707
- isError: true,
708
- };
709
- }
710
- });
711
- }
712
663
  async handleShowDependencies(params) {
713
664
  const { filepath, filepaths, query, depth = 1, relationship_types, direction = 'both', max_nodes = 50, project } = params;
714
665
  const storageManager = await (0, storage_1.getStorageManager)();
@@ -737,7 +688,7 @@ class CodeSeekerMcpServer {
737
688
  }
738
689
  }
739
690
  if (!projectId) {
740
- return { content: [{ type: 'text', text: 'Project not indexed. Use index({action: "init"}) first.' }], isError: true };
691
+ return { content: [{ type: 'text', text: 'Project not indexed. Run codeseeker({action:"index",index:{op:"init",path:"..."}}) first.' }], isError: true };
741
692
  }
742
693
  // Determine seed file paths
743
694
  let seedFilePaths = [];
@@ -860,7 +811,7 @@ class CodeSeekerMcpServer {
860
811
  ].filter(Boolean),
861
812
  };
862
813
  }
863
- return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] };
814
+ return { content: [{ type: 'text', text: JSON.stringify(summary) }] };
864
815
  }
865
816
  async handleFindDuplicates(params) {
866
817
  const { project, similarity_threshold = 0.80, min_lines = 5 } = params;
@@ -979,21 +930,46 @@ class CodeSeekerMcpServer {
979
930
  const deadCodeItems = [];
980
931
  const antiPatternItems = [];
981
932
  const couplingItems = [];
933
+ // Entry-point name fragments — files/symbols with these names are never flagged as dead.
934
+ // Covers common naming conventions across frameworks and languages.
935
+ const ENTRY_POINT_NAMES = ['main', 'index', 'app', 'server', 'cli', 'worker', 'bin', 'entry', 'bootstrap', 'start', 'init', 'run', 'launch'];
936
+ const isEntryPointName = (name) => {
937
+ const lower = name.toLowerCase();
938
+ return ENTRY_POINT_NAMES.some(ep => lower === ep || lower.startsWith(ep + '.') || lower.endsWith('/' + ep));
939
+ };
982
940
  for (const node of allNodes) {
983
941
  const inEdges = await graphStore.getEdges(node.id, 'in');
984
942
  const outEdges = await graphStore.getEdges(node.id, 'out');
985
943
  if (patterns.includes('dead_code')) {
986
- const isEntryPoint = node.type === 'file' ||
987
- node.name.toLowerCase().includes('main') ||
988
- node.name.toLowerCase().includes('index') ||
989
- node.name.toLowerCase().includes('app');
990
- if (!isEntryPoint && (node.type === 'class' || node.type === 'function') && inEdges.length === 0) {
944
+ // Files are handled separately in the orphaned-files pass below.
945
+ if (node.type === 'class' || node.type === 'function') {
946
+ if (isEntryPointName(node.name) || isEntryPointName(node.filePath))
947
+ continue;
948
+ if (inEdges.length > 0)
949
+ continue;
950
+ // Read export and visibility metadata stored by the graph builder.
951
+ const props = node.properties;
952
+ const isExported = props?.isExported === true || props?.is_exported === true;
953
+ const visibility = props?.visibility ?? 'unknown';
954
+ // Exported symbols may be consumed by other packages — skip, don't guess.
955
+ if (isExported)
956
+ continue;
957
+ // Tier confidence by how much we know about visibility.
958
+ // private/internal with no callers: strong signal.
959
+ // public/unknown unexported: moderate signal (dynamic dispatch, callbacks possible).
960
+ const isPrivate = visibility === 'private' || visibility === 'internal';
961
+ const confidence = isPrivate ? '85%' : '60%';
962
+ const caveat = isPrivate
963
+ ? 'Not exported and no detected callers.'
964
+ : 'Not exported and no detected static callers — dynamic dispatch or callbacks may still use this.';
991
965
  deadCodeItems.push({
992
- type: 'Dead Code', name: node.name,
966
+ type: 'Unreferenced Symbol', name: node.name,
993
967
  file: path.relative(projectRecord.path, node.filePath),
994
- description: `Unused ${node.type}: ${node.name} - no incoming references`,
995
- confidence: '70%', impact: 'medium',
996
- recommendation: 'Review if needed. Remove if unused or add to exports.',
968
+ description: `${node.type} "${node.name}" has no incoming static references. ${caveat}`,
969
+ confidence, impact: 'low',
970
+ recommendation: isPrivate
971
+ ? 'Safe to remove if no dynamic usage (reflection, eval, plugin system).'
972
+ : 'Verify no dynamic callers before removing. Consider exporting if used externally.',
997
973
  });
998
974
  }
999
975
  }
@@ -1023,6 +999,37 @@ class CodeSeekerMcpServer {
1023
999
  }
1024
1000
  }
1025
1001
  }
1002
+ // Orphaned-file detection — uses import edges only (the most reliable graph signal).
1003
+ // A file with no inbound imports from within the project and no entry-point name is
1004
+ // a strong candidate for removal. Confidence is higher than symbol-level dead code
1005
+ // because import edges are static and unambiguous.
1006
+ const orphanedFiles = [];
1007
+ if (patterns.includes('dead_code')) {
1008
+ const fileNodes = allNodes.filter(n => n.type === 'file');
1009
+ // Build inbound import count per file node
1010
+ const inboundImportCount = new Map(fileNodes.map(n => [n.id, 0]));
1011
+ for (const fileNode of fileNodes) {
1012
+ const outEdges = await graphStore.getEdges(fileNode.id, 'out');
1013
+ for (const edge of outEdges) {
1014
+ if (edge.type === 'imports' && inboundImportCount.has(edge.target)) {
1015
+ inboundImportCount.set(edge.target, (inboundImportCount.get(edge.target) ?? 0) + 1);
1016
+ }
1017
+ }
1018
+ }
1019
+ for (const fileNode of fileNodes) {
1020
+ if (isEntryPointName(fileNode.name) || isEntryPointName(fileNode.filePath))
1021
+ continue;
1022
+ if ((inboundImportCount.get(fileNode.id) ?? 0) === 0) {
1023
+ const relPath = path.relative(projectRecord.path, fileNode.filePath);
1024
+ orphanedFiles.push({
1025
+ file: relPath,
1026
+ description: `No other project file imports "${fileNode.name}". Could be an entry point, script, or dead file.`,
1027
+ confidence: '80%',
1028
+ recommendation: 'Verify this is not an entry point, CLI script, or plugin. Remove if truly unused.',
1029
+ });
1030
+ }
1031
+ }
1032
+ }
1026
1033
  // Circular dependency detection
1027
1034
  if (patterns.includes('circular_deps')) {
1028
1035
  const fileNodes = allNodes.filter(n => n.type === 'file');
@@ -1060,13 +1067,21 @@ class CodeSeekerMcpServer {
1060
1067
  classes: allNodes.filter(n => n.type === 'class').length,
1061
1068
  functions: allNodes.filter(n => n.type === 'function').length,
1062
1069
  },
1070
+ // Limitations of static graph analysis — AI should consider these before acting.
1071
+ graph_limitations: [
1072
+ 'Call edges use regex heuristics — dynamic dispatch, callbacks, and event handlers are not detected.',
1073
+ 'Export status is only reliable for TypeScript/JS (Babel AST). Other languages use conventions.',
1074
+ 'Symbols consumed by external packages will appear unreferenced.',
1075
+ ],
1063
1076
  summary: {
1064
- total_issues: deadCodeItems.length + antiPatternItems.length + couplingItems.length,
1065
- dead_code_count: deadCodeItems.length,
1077
+ total_issues: deadCodeItems.length + orphanedFiles.length + antiPatternItems.length + couplingItems.length,
1078
+ unreferenced_symbols: deadCodeItems.length,
1079
+ orphaned_files: orphanedFiles.length,
1066
1080
  anti_patterns_count: antiPatternItems.length,
1067
1081
  coupling_issues_count: couplingItems.length,
1068
1082
  },
1069
1083
  dead_code: deadCodeItems.slice(0, 20),
1084
+ orphaned_files: orphanedFiles.slice(0, 20),
1070
1085
  anti_patterns: antiPatternItems.slice(0, 10),
1071
1086
  coupling_issues: couplingItems.slice(0, 10),
1072
1087
  }, null, 2) }],
@@ -1108,60 +1123,11 @@ class CodeSeekerMcpServer {
1108
1123
  if (category !== 'all') {
1109
1124
  result = { ...standards, standards: { [category]: standards.standards[category] || {} } };
1110
1125
  }
1111
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1126
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
1112
1127
  }
1113
1128
  // ============================================================
1114
- // TOOL 3: index
1115
- // Combines: index (init), sync, projects (status), install_parsers, exclude
1129
+ // HANDLERS: index management
1116
1130
  // ============================================================
1117
- registerIndexTool() {
1118
- this.server.registerTool('index', {
1119
- description: 'Index management. Actions: "init" (index project), "sync" (update changed files), ' +
1120
- '"status" (list projects), "parsers" (install language parsers), "exclude" (manage exclusions).',
1121
- inputSchema: {
1122
- action: zod_1.z.enum(['init', 'sync', 'status', 'parsers', 'exclude']).describe('Action'),
1123
- path: zod_1.z.string().optional().describe('Project directory (for init)'),
1124
- project: zod_1.z.string().optional().describe('Project name or path'),
1125
- name: zod_1.z.string().optional().describe('Project name (for init)'),
1126
- // sync params
1127
- changes: zod_1.z.array(zod_1.z.object({
1128
- type: zod_1.z.enum(['created', 'modified', 'deleted']),
1129
- path: zod_1.z.string(),
1130
- })).optional().describe('File changes for sync'),
1131
- full_reindex: zod_1.z.boolean().optional().default(false).describe('Full reindex'),
1132
- // parsers params
1133
- languages: zod_1.z.array(zod_1.z.string()).optional().describe('Languages to install parsers for'),
1134
- list_available: zod_1.z.boolean().optional().default(false).describe('List available parsers'),
1135
- // exclude params
1136
- exclude_action: zod_1.z.enum(['exclude', 'include', 'list']).optional().describe('Exclusion sub-action'),
1137
- paths: zod_1.z.array(zod_1.z.string()).optional().describe('Paths/patterns to exclude/include'),
1138
- reason: zod_1.z.string().optional().describe('Exclusion reason'),
1139
- },
1140
- }, async (params) => {
1141
- try {
1142
- switch (params.action) {
1143
- case 'init':
1144
- return await this.handleIndexInit(params);
1145
- case 'sync':
1146
- return await this.handleSync(params);
1147
- case 'status':
1148
- return await this.handleProjects();
1149
- case 'parsers':
1150
- return await this.handleInstallParsers(params);
1151
- case 'exclude':
1152
- return await this.handleExclude(params);
1153
- default:
1154
- return { content: [{ type: 'text', text: `Unknown action: ${params.action}` }], isError: true };
1155
- }
1156
- }
1157
- catch (error) {
1158
- return {
1159
- content: [{ type: 'text', text: this.formatErrorMessage('Index', error instanceof Error ? error : String(error), { projectPath: params.path || params.project }) }],
1160
- isError: true,
1161
- };
1162
- }
1163
- });
1164
- }
1165
1131
  async handleIndexInit(params) {
1166
1132
  const projectPath = params.path;
1167
1133
  if (!projectPath) {
@@ -1530,6 +1496,82 @@ class CodeSeekerMcpServer {
1530
1496
  }
1531
1497
  return { content: [{ type: 'text', text: `Unknown exclude_action: ${exclude_action}` }], isError: true };
1532
1498
  }
1499
+ async handleSymbolLookup(sym, project, full) {
1500
+ const storageManager = await (0, storage_1.getStorageManager)();
1501
+ const projectStore = storageManager.getProjectStore();
1502
+ const graphStore = storageManager.getGraphStore();
1503
+ let projectId;
1504
+ let projectPath;
1505
+ if (project) {
1506
+ const projects = await projectStore.list();
1507
+ const found = projects.find(p => p.name === project || p.path === project || path.basename(p.path) === project);
1508
+ if (found) {
1509
+ projectId = found.id;
1510
+ projectPath = found.path;
1511
+ }
1512
+ else
1513
+ projectPath = process.cwd();
1514
+ }
1515
+ else {
1516
+ projectPath = process.cwd();
1517
+ const projects = await projectStore.list();
1518
+ const found = projects.find(p => p.path === projectPath || path.basename(p.path) === path.basename(projectPath));
1519
+ if (found) {
1520
+ projectId = found.id;
1521
+ projectPath = found.path;
1522
+ }
1523
+ }
1524
+ if (!projectId) {
1525
+ return { content: [{ type: 'text', text: 'Project not indexed. Run codeseeker({action:"index",index:{op:"init",path:"..."}}) first.' }], isError: true };
1526
+ }
1527
+ const allNodes = await graphStore.findNodes(projectId);
1528
+ const symLower = sym.toLowerCase();
1529
+ // Exact matches first, then partial — exclude file nodes (they add noise)
1530
+ const exact = allNodes.filter(n => n.type !== 'file' && n.name.toLowerCase() === symLower);
1531
+ const partial = allNodes.filter(n => n.type !== 'file' && n.name.toLowerCase() !== symLower && n.name.toLowerCase().includes(symLower));
1532
+ const matches = [...exact, ...partial].slice(0, 20);
1533
+ if (matches.length === 0) {
1534
+ return { content: [{ type: 'text', text: JSON.stringify({ found: false, sym }) }] };
1535
+ }
1536
+ // Build a node-ID → {name,type,file} map for peer resolution (only load what we need)
1537
+ const nodeCache = new Map();
1538
+ const resolveNode = async (id) => {
1539
+ if (nodeCache.has(id))
1540
+ return nodeCache.get(id);
1541
+ const n = await graphStore.getNode(id);
1542
+ if (!n)
1543
+ return null;
1544
+ const rel = n.properties?.relativePath || path.relative(projectPath, n.filePath);
1545
+ const entry = { name: n.name, type: n.type, file: rel };
1546
+ nodeCache.set(id, entry);
1547
+ return entry;
1548
+ };
1549
+ const symbols = await Promise.all(matches.map(async (n) => {
1550
+ const rel = n.properties?.relativePath || path.relative(projectPath, n.filePath);
1551
+ const edges = await graphStore.getEdges(n.id, 'both');
1552
+ const entry = {
1553
+ name: n.name,
1554
+ type: n.type,
1555
+ file: rel,
1556
+ in: edges.filter(e => e.target === n.id).length,
1557
+ out: edges.filter(e => e.source === n.id).length,
1558
+ };
1559
+ if (full) {
1560
+ const resolved = await Promise.all(edges.slice(0, 5).map(async (e) => {
1561
+ const peerId = e.source === n.id ? e.target : e.source;
1562
+ const peer = await resolveNode(peerId);
1563
+ return peer ? { rel: e.type, dir: e.source === n.id ? 'out' : 'in', ...peer } : null;
1564
+ }));
1565
+ entry.edges = resolved.filter(Boolean);
1566
+ }
1567
+ return entry;
1568
+ }));
1569
+ return {
1570
+ content: [{ type: 'text', text: JSON.stringify({
1571
+ sym, count: matches.length, symbols,
1572
+ }) }],
1573
+ };
1574
+ }
1533
1575
  // ============================================================
1534
1576
  // SERVER LIFECYCLE
1535
1577
  // ============================================================