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.
- package/README.md +170 -181
- package/dist/cli/commands/services/semantic-search-orchestrator.d.ts +36 -4
- package/dist/cli/commands/services/semantic-search-orchestrator.d.ts.map +1 -1
- package/dist/cli/commands/services/semantic-search-orchestrator.js +238 -40
- package/dist/cli/commands/services/semantic-search-orchestrator.js.map +1 -1
- package/dist/cli/services/analysis/deduplication/duplicate-code-detector.js +1 -1
- package/dist/cli/services/analysis/deduplication/duplicate-code-detector.js.map +1 -1
- package/dist/cli/services/monitoring/file-scanning/file-scanner-config.json +126 -0
- package/dist/cli/services/search/ast-chunker.d.ts +37 -0
- package/dist/cli/services/search/ast-chunker.d.ts.map +1 -0
- package/dist/cli/services/search/ast-chunker.js +171 -0
- package/dist/cli/services/search/ast-chunker.js.map +1 -0
- package/dist/mcp/indexing-service.d.ts +0 -4
- package/dist/mcp/indexing-service.d.ts.map +1 -1
- package/dist/mcp/indexing-service.js +8 -25
- package/dist/mcp/indexing-service.js.map +1 -1
- package/dist/mcp/mcp-server.d.ts +23 -9
- package/dist/mcp/mcp-server.d.ts.map +1 -1
- package/dist/mcp/mcp-server.js +370 -328
- package/dist/mcp/mcp-server.js.map +1 -1
- package/dist/storage/embedded/minisearch-text-store.d.ts.map +1 -1
- package/dist/storage/embedded/minisearch-text-store.js +3 -2
- package/dist/storage/embedded/minisearch-text-store.js.map +1 -1
- package/dist/storage/embedded/sqlite-vector-store.d.ts.map +1 -1
- package/dist/storage/embedded/sqlite-vector-store.js +7 -1
- package/dist/storage/embedded/sqlite-vector-store.js.map +1 -1
- package/dist/storage/storage-manager.d.ts +2 -1
- package/dist/storage/storage-manager.d.ts.map +1 -1
- package/dist/storage/storage-manager.js +8 -2
- package/dist/storage/storage-manager.js.map +1 -1
- package/package.json +2 -2
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
407
|
-
this.registerAnalyzeTool();
|
|
408
|
-
this.registerIndexTool();
|
|
405
|
+
this.registerSentinelTool();
|
|
409
406
|
}
|
|
410
407
|
// ============================================================
|
|
411
|
-
// TOOL
|
|
412
|
-
//
|
|
408
|
+
// SENTINEL TOOL: codeseeker
|
|
409
|
+
// Hierarchical schema — each action carries its own nested params
|
|
413
410
|
// ============================================================
|
|
414
|
-
|
|
415
|
-
this.server.registerTool('
|
|
416
|
-
description: '
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
.
|
|
427
|
-
|
|
428
|
-
|
|
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 (
|
|
457
|
+
}, async (params) => {
|
|
431
458
|
try {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
|
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('
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
480
|
-
|
|
564
|
+
catch {
|
|
565
|
+
return { content: [{ type: 'text', text: JSON.stringify({ query, project, files_found: 0, results: [] }) }], isError: false };
|
|
481
566
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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:
|
|
584
|
+
content: [{ type: 'text', text: JSON.stringify(resp) }],
|
|
585
|
+
isError: false
|
|
491
586
|
};
|
|
492
587
|
}
|
|
493
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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({
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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
|
-
|
|
610
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
node.name
|
|
989
|
-
|
|
990
|
-
|
|
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: '
|
|
966
|
+
type: 'Unreferenced Symbol', name: node.name,
|
|
993
967
|
file: path.relative(projectRecord.path, node.filePath),
|
|
994
|
-
description:
|
|
995
|
-
confidence
|
|
996
|
-
recommendation:
|
|
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
|
-
|
|
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
|
|
1126
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
1112
1127
|
}
|
|
1113
1128
|
// ============================================================
|
|
1114
|
-
//
|
|
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
|
// ============================================================
|