ai-mind-map 1.13.0 → 1.14.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/dist/index.js +58 -42
- package/dist/index.js.map +1 -1
- package/dist/tools/advanced-tools.d.ts.map +1 -1
- package/dist/tools/advanced-tools.js +229 -194
- package/dist/tools/advanced-tools.js.map +1 -1
- package/dist/tools/change-tools.d.ts.map +1 -1
- package/dist/tools/change-tools.js +37 -23
- package/dist/tools/change-tools.js.map +1 -1
- package/dist/tools/context-tools.d.ts.map +1 -1
- package/dist/tools/context-tools.js +60 -40
- package/dist/tools/context-tools.js.map +1 -1
- package/dist/tools/digest-tools.d.ts.map +1 -1
- package/dist/tools/digest-tools.js +66 -64
- package/dist/tools/digest-tools.js.map +1 -1
- package/dist/tools/evolving-tools.d.ts.map +1 -1
- package/dist/tools/evolving-tools.js +26 -20
- package/dist/tools/evolving-tools.js.map +1 -1
- package/dist/tools/filesystem-tools.d.ts.map +1 -1
- package/dist/tools/filesystem-tools.js +424 -412
- package/dist/tools/filesystem-tools.js.map +1 -1
- package/dist/tools/flow-tools.d.ts.map +1 -1
- package/dist/tools/flow-tools.js +134 -107
- package/dist/tools/flow-tools.js.map +1 -1
- package/dist/tools/graph-tools.d.ts.map +1 -1
- package/dist/tools/graph-tools.js +48 -36
- package/dist/tools/graph-tools.js.map +1 -1
- package/dist/tools/memory-tools.d.ts.map +1 -1
- package/dist/tools/memory-tools.js +45 -33
- package/dist/tools/memory-tools.js.map +1 -1
- package/dist/tools/semantic-tools.d.ts.map +1 -1
- package/dist/tools/semantic-tools.js +49 -31
- package/dist/tools/semantic-tools.js.map +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +227 -168
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/smart-tools.d.ts.map +1 -1
- package/dist/tools/smart-tools.js +178 -137
- package/dist/tools/smart-tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -561,76 +561,79 @@ export function registerFilesystemTools(server, graph, config, estimator = defau
|
|
|
561
561
|
}
|
|
562
562
|
});
|
|
563
563
|
// ── mindmap_project_summary ───────────────────────────────────
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
'
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (!stat.isDirectory()) {
|
|
575
|
-
return mcpText(fail(`Not a directory: ${projectPath}`));
|
|
576
|
-
}
|
|
577
|
-
// Parse .gitignore for extra skip rules
|
|
578
|
-
const gitignoreNames = parseGitignoreSimple(join(projectPath, '.gitignore'));
|
|
579
|
-
// Walk the directory tree
|
|
580
|
-
const state = {
|
|
581
|
-
totalFiles: 0,
|
|
582
|
-
totalDirs: 0,
|
|
583
|
-
languages: {},
|
|
584
|
-
entryPoints: [],
|
|
585
|
-
entriesWalked: 0,
|
|
586
|
-
truncated: false,
|
|
587
|
-
};
|
|
588
|
-
walkDirectory(projectPath, projectPath, gitignoreNames, state);
|
|
589
|
-
// Sort languages by count (descending)
|
|
590
|
-
const sortedLanguages = {};
|
|
591
|
-
const langEntries = Object.entries(state.languages).sort((a, b) => b[1] - a[1]);
|
|
592
|
-
for (const [ext, count] of langEntries) {
|
|
593
|
-
sortedLanguages[ext] = count;
|
|
594
|
-
}
|
|
595
|
-
// Detect project type
|
|
596
|
-
const projectTypes = detectProjectType(projectPath);
|
|
597
|
-
// Derive project name
|
|
598
|
-
let projectName = basename(projectPath);
|
|
599
|
-
const pkgPath = join(projectPath, 'package.json');
|
|
600
|
-
if (existsSync(pkgPath)) {
|
|
601
|
-
try {
|
|
602
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
603
|
-
if (pkg.name)
|
|
604
|
-
projectName = pkg.name;
|
|
564
|
+
// CONSOLIDATED: mindmap_project_summary disabled to reduce schema payload
|
|
565
|
+
if (false) {
|
|
566
|
+
server.tool('mindmap_project_summary', 'Get a complete project overview in a single call — WITHOUT indexing. ' +
|
|
567
|
+
'Detects project type, counts files by language, builds a compact directory tree, ' +
|
|
568
|
+
'finds entry points, and reads key config files. Works instantly on any project.', {
|
|
569
|
+
projectPath: z.string().describe('Absolute path to the project root'),
|
|
570
|
+
}, async ({ projectPath }) => {
|
|
571
|
+
try {
|
|
572
|
+
if (!existsSync(projectPath)) {
|
|
573
|
+
return mcpText(fail(`Project path not found: ${projectPath}`));
|
|
605
574
|
}
|
|
606
|
-
|
|
607
|
-
|
|
575
|
+
const stat = statSync(projectPath);
|
|
576
|
+
if (!stat.isDirectory()) {
|
|
577
|
+
return mcpText(fail(`Not a directory: ${projectPath}`));
|
|
578
|
+
}
|
|
579
|
+
// Parse .gitignore for extra skip rules
|
|
580
|
+
const gitignoreNames = parseGitignoreSimple(join(projectPath, '.gitignore'));
|
|
581
|
+
// Walk the directory tree
|
|
582
|
+
const state = {
|
|
583
|
+
totalFiles: 0,
|
|
584
|
+
totalDirs: 0,
|
|
585
|
+
languages: {},
|
|
586
|
+
entryPoints: [],
|
|
587
|
+
entriesWalked: 0,
|
|
588
|
+
truncated: false,
|
|
589
|
+
};
|
|
590
|
+
walkDirectory(projectPath, projectPath, gitignoreNames, state);
|
|
591
|
+
// Sort languages by count (descending)
|
|
592
|
+
const sortedLanguages = {};
|
|
593
|
+
const langEntries = Object.entries(state.languages).sort((a, b) => b[1] - a[1]);
|
|
594
|
+
for (const [ext, count] of langEntries) {
|
|
595
|
+
sortedLanguages[ext] = count;
|
|
596
|
+
}
|
|
597
|
+
// Detect project type
|
|
598
|
+
const projectTypes = detectProjectType(projectPath);
|
|
599
|
+
// Derive project name
|
|
600
|
+
let projectName = basename(projectPath);
|
|
601
|
+
const pkgPath = join(projectPath, 'package.json');
|
|
602
|
+
if (existsSync(pkgPath)) {
|
|
603
|
+
try {
|
|
604
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
605
|
+
if (pkg.name)
|
|
606
|
+
projectName = pkg.name;
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
// use directory name
|
|
610
|
+
}
|
|
608
611
|
}
|
|
612
|
+
// Build compact directory tree
|
|
613
|
+
const directoryTree = buildCompactTree(projectPath, projectPath, gitignoreNames);
|
|
614
|
+
// Read config summaries
|
|
615
|
+
const configSummary = readConfigSummary(projectPath);
|
|
616
|
+
const result = {
|
|
617
|
+
projectPath,
|
|
618
|
+
projectName,
|
|
619
|
+
projectType: projectTypes,
|
|
620
|
+
totalFiles: state.totalFiles,
|
|
621
|
+
totalDirs: state.totalDirs,
|
|
622
|
+
languages: sortedLanguages,
|
|
623
|
+
directoryTree: directoryTree || '(empty)',
|
|
624
|
+
entryPoints: state.entryPoints,
|
|
625
|
+
configSummary,
|
|
626
|
+
estimatedIndexTime: estimateIndexTime(state.totalFiles),
|
|
627
|
+
...(state.truncated ? { truncated: true, note: `Scan capped at ${MAX_WALK_ENTRIES} entries` } : {}),
|
|
628
|
+
};
|
|
629
|
+
return mcpText(ok(result, estimator));
|
|
609
630
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
projectName,
|
|
617
|
-
projectType: projectTypes,
|
|
618
|
-
totalFiles: state.totalFiles,
|
|
619
|
-
totalDirs: state.totalDirs,
|
|
620
|
-
languages: sortedLanguages,
|
|
621
|
-
directoryTree: directoryTree || '(empty)',
|
|
622
|
-
entryPoints: state.entryPoints,
|
|
623
|
-
configSummary,
|
|
624
|
-
estimatedIndexTime: estimateIndexTime(state.totalFiles),
|
|
625
|
-
...(state.truncated ? { truncated: true, note: `Scan capped at ${MAX_WALK_ENTRIES} entries` } : {}),
|
|
626
|
-
};
|
|
627
|
-
return mcpText(ok(result, estimator));
|
|
628
|
-
}
|
|
629
|
-
catch (err) {
|
|
630
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
631
|
-
return mcpText(fail(`Failed to generate project summary: ${msg}`));
|
|
632
|
-
}
|
|
633
|
-
});
|
|
631
|
+
catch (err) {
|
|
632
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
633
|
+
return mcpText(fail(`Failed to generate project summary: ${msg}`));
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
}
|
|
634
637
|
// ── mindmap_grep ──────────────────────────────────────────────
|
|
635
638
|
/** Detect binary files by checking for null bytes in the first 512 bytes */
|
|
636
639
|
function isBinaryFile(filePath) {
|
|
@@ -679,135 +682,138 @@ export function registerFilesystemTools(server, graph, config, estimator = defau
|
|
|
679
682
|
}
|
|
680
683
|
}
|
|
681
684
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (
|
|
699
|
-
|
|
685
|
+
// CONSOLIDATED: mindmap_grep disabled to reduce schema payload
|
|
686
|
+
if (false) {
|
|
687
|
+
server.tool('mindmap_grep', 'Powerful text search across project files. Searches file contents for a pattern (literal or regex) with optional context lines, file filtering, and word-boundary matching. Like ripgrep but through MCP. Returns matching lines with file paths and line numbers.', {
|
|
688
|
+
pattern: z.string().describe('Search pattern (literal text or regex)'),
|
|
689
|
+
regex: z.boolean().default(false).describe('Treat pattern as regex'),
|
|
690
|
+
fileGlob: z.string().optional().describe("Filter by file extension pattern, e.g. '*.cs', '*.ts'"),
|
|
691
|
+
caseSensitive: z.boolean().default(false).describe('Case-sensitive matching'),
|
|
692
|
+
contextLines: z.number().int().min(0).max(5).default(0).describe('Lines of context before/after each match (max 5)'),
|
|
693
|
+
maxResults: z.number().int().min(1).default(100).describe('Maximum results to return'),
|
|
694
|
+
path: z.string().optional().describe('Search within a specific subdirectory (absolute or relative to projectRoot)'),
|
|
695
|
+
invertMatch: z.boolean().default(false).describe('Return lines that do NOT match'),
|
|
696
|
+
wholeWord: z.boolean().default(false).describe('Match whole words only'),
|
|
697
|
+
}, async ({ pattern, regex: isRegex, fileGlob, caseSensitive, contextLines, maxResults, path: subPath, invertMatch, wholeWord }) => {
|
|
698
|
+
try {
|
|
699
|
+
// Determine search root
|
|
700
|
+
let searchRoot = config.projectRoot;
|
|
701
|
+
if (subPath) {
|
|
702
|
+
const resolved = resolvePath(subPath, config.projectRoot, true);
|
|
703
|
+
if (!resolved) {
|
|
704
|
+
return mcpText(fail(`Path "${subPath}" resolves outside the project root`));
|
|
705
|
+
}
|
|
706
|
+
if (!existsSync(resolved)) {
|
|
707
|
+
return mcpText(fail(`Path not found: ${resolved}`));
|
|
708
|
+
}
|
|
709
|
+
searchRoot = resolved;
|
|
700
710
|
}
|
|
701
|
-
|
|
702
|
-
|
|
711
|
+
// Build file glob regex
|
|
712
|
+
let globRegex = null;
|
|
713
|
+
if (fileGlob) {
|
|
714
|
+
const escaped = fileGlob.replace(/[.+^${}()|[\]]/g, '\\$&');
|
|
715
|
+
const globPattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
716
|
+
globRegex = new RegExp(`^${globPattern}$`, 'i');
|
|
703
717
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
if (fileGlob) {
|
|
709
|
-
const escaped = fileGlob.replace(/[.+^${}()|[\]]/g, '\\$&');
|
|
710
|
-
const globPattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
711
|
-
globRegex = new RegExp(`^${globPattern}$`, 'i');
|
|
712
|
-
}
|
|
713
|
-
// Build search regex
|
|
714
|
-
let searchPattern;
|
|
715
|
-
if (isRegex) {
|
|
716
|
-
searchPattern = pattern;
|
|
717
|
-
}
|
|
718
|
-
else {
|
|
719
|
-
searchPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
720
|
-
}
|
|
721
|
-
if (wholeWord) {
|
|
722
|
-
searchPattern = `\\b${searchPattern}\\b`;
|
|
723
|
-
}
|
|
724
|
-
const flags = caseSensitive ? 'g' : 'gi';
|
|
725
|
-
let searchRegex;
|
|
726
|
-
try {
|
|
727
|
-
searchRegex = new RegExp(searchPattern, flags);
|
|
728
|
-
}
|
|
729
|
-
catch (err) {
|
|
730
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
731
|
-
return mcpText(fail(`Invalid regex pattern: ${msg}`));
|
|
732
|
-
}
|
|
733
|
-
// Collect files to search
|
|
734
|
-
const filePaths = [];
|
|
735
|
-
walkForGrep(searchRoot, globRegex, filePaths);
|
|
736
|
-
filePaths.sort();
|
|
737
|
-
// Search files
|
|
738
|
-
const results = [];
|
|
739
|
-
let totalMatches = 0;
|
|
740
|
-
const filesWithMatches = new Set();
|
|
741
|
-
let truncated = false;
|
|
742
|
-
for (const filePath of filePaths) {
|
|
743
|
-
if (results.length >= maxResults) {
|
|
744
|
-
truncated = true;
|
|
745
|
-
break;
|
|
718
|
+
// Build search regex
|
|
719
|
+
let searchPattern;
|
|
720
|
+
if (isRegex) {
|
|
721
|
+
searchPattern = pattern;
|
|
746
722
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const stat = statSync(filePath);
|
|
750
|
-
if (stat.size > MAX_GREP_FILE_SIZE)
|
|
751
|
-
continue;
|
|
723
|
+
else {
|
|
724
|
+
searchPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
752
725
|
}
|
|
753
|
-
|
|
754
|
-
|
|
726
|
+
if (wholeWord) {
|
|
727
|
+
searchPattern = `\\b${searchPattern}\\b`;
|
|
755
728
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
continue;
|
|
759
|
-
let content;
|
|
729
|
+
const flags = caseSensitive ? 'g' : 'gi';
|
|
730
|
+
let searchRegex;
|
|
760
731
|
try {
|
|
761
|
-
|
|
732
|
+
searchRegex = new RegExp(searchPattern, flags);
|
|
762
733
|
}
|
|
763
|
-
catch {
|
|
764
|
-
|
|
734
|
+
catch (err) {
|
|
735
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
736
|
+
return mcpText(fail(`Invalid regex pattern: ${msg}`));
|
|
765
737
|
}
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
|
|
738
|
+
// Collect files to search
|
|
739
|
+
const filePaths = [];
|
|
740
|
+
walkForGrep(searchRoot, globRegex, filePaths);
|
|
741
|
+
filePaths.sort();
|
|
742
|
+
// Search files
|
|
743
|
+
const results = [];
|
|
744
|
+
let totalMatches = 0;
|
|
745
|
+
const filesWithMatches = new Set();
|
|
746
|
+
let truncated = false;
|
|
747
|
+
for (const filePath of filePaths) {
|
|
769
748
|
if (results.length >= maxResults) {
|
|
770
749
|
truncated = true;
|
|
771
750
|
break;
|
|
772
751
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
752
|
+
// Skip large files
|
|
753
|
+
try {
|
|
754
|
+
const stat = statSync(filePath);
|
|
755
|
+
if (stat.size > MAX_GREP_FILE_SIZE)
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
// Skip binary files
|
|
762
|
+
if (isBinaryFile(filePath))
|
|
763
|
+
continue;
|
|
764
|
+
let content;
|
|
765
|
+
try {
|
|
766
|
+
content = readFileSync(filePath, 'utf-8');
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const lines = content.split('\n');
|
|
772
|
+
const relativePath = relative(config.projectRoot, filePath).replace(/\\/g, '/');
|
|
773
|
+
for (let i = 0; i < lines.length; i++) {
|
|
774
|
+
if (results.length >= maxResults) {
|
|
775
|
+
truncated = true;
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
const line = lines[i];
|
|
779
|
+
searchRegex.lastIndex = 0;
|
|
780
|
+
const matches = line.match(searchRegex);
|
|
781
|
+
const hasMatch = matches !== null && matches.length > 0;
|
|
782
|
+
if (invertMatch ? !hasMatch : hasMatch) {
|
|
783
|
+
const matchCount = invertMatch ? 0 : (matches?.length ?? 0);
|
|
784
|
+
totalMatches += matchCount || 1;
|
|
785
|
+
filesWithMatches.add(relativePath);
|
|
786
|
+
const entry = {
|
|
787
|
+
file: relativePath,
|
|
788
|
+
line: line,
|
|
789
|
+
lineNumber: i + 1,
|
|
790
|
+
matchCount,
|
|
791
|
+
};
|
|
792
|
+
if (contextLines > 0) {
|
|
793
|
+
const beforeStart = Math.max(0, i - contextLines);
|
|
794
|
+
const afterEnd = Math.min(lines.length - 1, i + contextLines);
|
|
795
|
+
entry.before = lines.slice(beforeStart, i);
|
|
796
|
+
entry.after = lines.slice(i + 1, afterEnd + 1);
|
|
797
|
+
}
|
|
798
|
+
results.push(entry);
|
|
792
799
|
}
|
|
793
|
-
results.push(entry);
|
|
794
800
|
}
|
|
795
801
|
}
|
|
802
|
+
return mcpText(ok({
|
|
803
|
+
pattern,
|
|
804
|
+
totalMatches,
|
|
805
|
+
totalFiles: filePaths.length,
|
|
806
|
+
filesWithMatches: filesWithMatches.size,
|
|
807
|
+
truncated,
|
|
808
|
+
results,
|
|
809
|
+
}, estimator));
|
|
796
810
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
results,
|
|
804
|
-
}, estimator));
|
|
805
|
-
}
|
|
806
|
-
catch (err) {
|
|
807
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
808
|
-
return mcpText(fail(`Grep failed: ${msg}`));
|
|
809
|
-
}
|
|
810
|
-
});
|
|
811
|
+
catch (err) {
|
|
812
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
813
|
+
return mcpText(fail(`Grep failed: ${msg}`));
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
}
|
|
811
817
|
// ── mindmap_find_file ─────────────────────────────────────────
|
|
812
818
|
/** Convert a glob pattern (with * and ?) to a RegExp */
|
|
813
819
|
function globToRegex(glob) {
|
|
@@ -857,180 +863,259 @@ export function registerFilesystemTools(server, graph, config, estimator = defau
|
|
|
857
863
|
return 60;
|
|
858
864
|
return 40;
|
|
859
865
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
let searchRoot = config.projectRoot;
|
|
874
|
-
if (subPath) {
|
|
875
|
-
const resolved = resolvePath(subPath, config.projectRoot, true);
|
|
876
|
-
if (!resolved) {
|
|
877
|
-
return mcpText(fail(`Path "${subPath}" resolves outside the project root`));
|
|
878
|
-
}
|
|
879
|
-
if (!existsSync(resolved)) {
|
|
880
|
-
return mcpText(fail(`Path not found: ${resolved}`));
|
|
881
|
-
}
|
|
882
|
-
searchRoot = resolved;
|
|
883
|
-
}
|
|
884
|
-
// Build match regex
|
|
885
|
-
let matchRegex;
|
|
866
|
+
// CONSOLIDATED: mindmap_find_file disabled to reduce schema payload
|
|
867
|
+
if (false) {
|
|
868
|
+
server.tool('mindmap_find_file', 'Find files by name pattern across the project. Supports wildcards (* and ?) and regex. Searches indexed files first for instant results, falls back to filesystem walk. Returns paths sorted by relevance.', {
|
|
869
|
+
pattern: z.string().describe('Filename pattern to search for (supports wildcards: * and ?)'),
|
|
870
|
+
regex: z.boolean().default(false).describe('Treat pattern as regex instead of glob'),
|
|
871
|
+
type: z.enum(['file', 'dir', 'all']).default('file').describe("Filter by type: 'file', 'dir', or 'all'"),
|
|
872
|
+
maxDepth: z.number().int().min(1).default(20).describe('Maximum directory depth to search'),
|
|
873
|
+
maxResults: z.number().int().min(1).default(50).describe('Maximum results'),
|
|
874
|
+
path: z.string().optional().describe('Search within a specific subdirectory'),
|
|
875
|
+
includeSize: z.boolean().default(true).describe('Include file size in results'),
|
|
876
|
+
includeModified: z.boolean().default(false).describe('Include last modified timestamp'),
|
|
877
|
+
sortBy: z.enum(['relevance', 'name', 'size', 'modified']).default('relevance').describe("Sort by 'relevance', 'name', 'size', or 'modified'"),
|
|
878
|
+
}, async ({ pattern, regex: isRegex, type: filterType, maxDepth, maxResults, path: subPath, includeSize, includeModified, sortBy }) => {
|
|
886
879
|
try {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
880
|
+
// Determine search root
|
|
881
|
+
let searchRoot = config.projectRoot;
|
|
882
|
+
if (subPath) {
|
|
883
|
+
const resolved = resolvePath(subPath, config.projectRoot, true);
|
|
884
|
+
if (!resolved) {
|
|
885
|
+
return mcpText(fail(`Path "${subPath}" resolves outside the project root`));
|
|
886
|
+
}
|
|
887
|
+
if (!existsSync(resolved)) {
|
|
888
|
+
return mcpText(fail(`Path not found: ${resolved}`));
|
|
889
|
+
}
|
|
890
|
+
searchRoot = resolved;
|
|
892
891
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
896
|
-
return mcpText(fail(`Invalid pattern: ${msg}`));
|
|
897
|
-
}
|
|
898
|
-
// Try indexed files first (only if searching from project root and filterType includes files)
|
|
899
|
-
let candidates = [];
|
|
900
|
-
let usedIndex = false;
|
|
901
|
-
if (!subPath && (filterType === 'file' || filterType === 'all')) {
|
|
892
|
+
// Build match regex
|
|
893
|
+
let matchRegex;
|
|
902
894
|
try {
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
path: filePath,
|
|
909
|
-
name: basename(filePath),
|
|
910
|
-
type: 'file',
|
|
911
|
-
});
|
|
912
|
-
}
|
|
895
|
+
if (isRegex) {
|
|
896
|
+
matchRegex = new RegExp(pattern, 'i');
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
matchRegex = globToRegex(pattern);
|
|
913
900
|
}
|
|
914
901
|
}
|
|
915
|
-
catch {
|
|
916
|
-
|
|
902
|
+
catch (err) {
|
|
903
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
904
|
+
return mcpText(fail(`Invalid pattern: ${msg}`));
|
|
917
905
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
score: scoreMatch(candidate.name, pattern),
|
|
934
|
-
};
|
|
935
|
-
if (includeSize || includeModified || sortBy === 'size' || sortBy === 'modified') {
|
|
936
|
-
try {
|
|
937
|
-
const st = statSync(candidate.path);
|
|
938
|
-
if (includeSize)
|
|
939
|
-
entry.sizeBytes = st.size;
|
|
940
|
-
if (includeModified)
|
|
941
|
-
entry.modifiedAt = st.mtime.toISOString();
|
|
942
|
-
// Store for sorting even if not included in output
|
|
943
|
-
if (sortBy === 'size' && !includeSize)
|
|
944
|
-
entry.sizeBytes = st.size;
|
|
945
|
-
if (sortBy === 'modified' && !includeModified)
|
|
946
|
-
entry.modifiedAt = st.mtime.toISOString();
|
|
906
|
+
// Try indexed files first (only if searching from project root and filterType includes files)
|
|
907
|
+
let candidates = [];
|
|
908
|
+
let usedIndex = false;
|
|
909
|
+
if (!subPath && (filterType === 'file' || filterType === 'all')) {
|
|
910
|
+
try {
|
|
911
|
+
const indexedFiles = graph.getIndexedFiles();
|
|
912
|
+
if (indexedFiles && indexedFiles.length > 0) {
|
|
913
|
+
usedIndex = true;
|
|
914
|
+
for (const filePath of indexedFiles) {
|
|
915
|
+
candidates.push({
|
|
916
|
+
path: filePath,
|
|
917
|
+
name: basename(filePath),
|
|
918
|
+
type: 'file',
|
|
919
|
+
});
|
|
920
|
+
}
|
|
947
921
|
}
|
|
948
|
-
|
|
949
|
-
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
// Graph not ready, fall through to filesystem walk
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
// Fallback to filesystem walk if no indexed files or path specified
|
|
928
|
+
if (!usedIndex) {
|
|
929
|
+
walkForFind(searchRoot, filterType, maxDepth, candidates);
|
|
930
|
+
}
|
|
931
|
+
// Filter by pattern
|
|
932
|
+
const matched = [];
|
|
933
|
+
for (const candidate of candidates) {
|
|
934
|
+
if (matchRegex.test(candidate.name)) {
|
|
935
|
+
const relativePath = relative(config.projectRoot, candidate.path).replace(/\\/g, '/');
|
|
936
|
+
const entry = {
|
|
937
|
+
path: candidate.path,
|
|
938
|
+
relativePath,
|
|
939
|
+
name: candidate.name,
|
|
940
|
+
type: candidate.type,
|
|
941
|
+
score: scoreMatch(candidate.name, pattern),
|
|
942
|
+
};
|
|
943
|
+
if (includeSize || includeModified || sortBy === 'size' || sortBy === 'modified') {
|
|
944
|
+
try {
|
|
945
|
+
const st = statSync(candidate.path);
|
|
946
|
+
if (includeSize)
|
|
947
|
+
entry.sizeBytes = st.size;
|
|
948
|
+
if (includeModified)
|
|
949
|
+
entry.modifiedAt = st.mtime.toISOString();
|
|
950
|
+
// Store for sorting even if not included in output
|
|
951
|
+
if (sortBy === 'size' && !includeSize)
|
|
952
|
+
entry.sizeBytes = st.size;
|
|
953
|
+
if (sortBy === 'modified' && !includeModified)
|
|
954
|
+
entry.modifiedAt = st.mtime.toISOString();
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
// Can't stat — skip size/modified
|
|
958
|
+
}
|
|
950
959
|
}
|
|
960
|
+
matched.push(entry);
|
|
951
961
|
}
|
|
952
|
-
matched.push(entry);
|
|
953
962
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
return b.
|
|
968
|
-
|
|
963
|
+
// Sort results
|
|
964
|
+
matched.sort((a, b) => {
|
|
965
|
+
switch (sortBy) {
|
|
966
|
+
case 'name':
|
|
967
|
+
return a.name.localeCompare(b.name);
|
|
968
|
+
case 'size':
|
|
969
|
+
return (b.sizeBytes ?? 0) - (a.sizeBytes ?? 0);
|
|
970
|
+
case 'modified':
|
|
971
|
+
return (b.modifiedAt ?? '').localeCompare(a.modifiedAt ?? '');
|
|
972
|
+
case 'relevance':
|
|
973
|
+
default: {
|
|
974
|
+
if (a.score !== b.score)
|
|
975
|
+
return b.score - a.score;
|
|
976
|
+
return a.name.localeCompare(b.name);
|
|
977
|
+
}
|
|
969
978
|
}
|
|
979
|
+
});
|
|
980
|
+
const truncated = matched.length > maxResults;
|
|
981
|
+
const results = matched.slice(0, maxResults);
|
|
982
|
+
// Clean up internal-only sort fields if not requested
|
|
983
|
+
if (!includeSize) {
|
|
984
|
+
for (const r of results)
|
|
985
|
+
delete r.sizeBytes;
|
|
986
|
+
}
|
|
987
|
+
if (!includeModified) {
|
|
988
|
+
for (const r of results)
|
|
989
|
+
delete r.modifiedAt;
|
|
970
990
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
delete r.sizeBytes;
|
|
991
|
+
return mcpText(ok({
|
|
992
|
+
pattern,
|
|
993
|
+
totalResults: matched.length,
|
|
994
|
+
truncated,
|
|
995
|
+
results,
|
|
996
|
+
}, estimator));
|
|
978
997
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
998
|
+
catch (err) {
|
|
999
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1000
|
+
return mcpText(fail(`Find file failed: ${msg}`));
|
|
982
1001
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
totalResults: matched.length,
|
|
986
|
-
truncated,
|
|
987
|
-
results,
|
|
988
|
-
}, estimator));
|
|
989
|
-
}
|
|
990
|
-
catch (err) {
|
|
991
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
992
|
-
return mcpText(fail(`Find file failed: ${msg}`));
|
|
993
|
-
}
|
|
994
|
-
});
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
995
1004
|
// ── mindmap_batch_read ────────────────────────────────────────
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
path
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
resolved
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1005
|
+
// CONSOLIDATED: mindmap_batch_read disabled to reduce schema payload
|
|
1006
|
+
if (false) {
|
|
1007
|
+
server.tool('mindmap_batch_read', 'Read multiple file regions in a single call. Eliminates per-call overhead when reading from several files. ' +
|
|
1008
|
+
'Each file request specifies a path and optional line range. Returns all results at once.', {
|
|
1009
|
+
files: z.array(z.object({
|
|
1010
|
+
path: z.string().describe('Absolute or relative file path'),
|
|
1011
|
+
startLine: z.number().int().min(1).default(1).describe('First line to read (1-indexed)'),
|
|
1012
|
+
endLine: z.number().int().min(1).optional().describe('Last line to read (1-indexed). Omit to read 100 lines from startLine'),
|
|
1013
|
+
label: z.string().optional().describe('Optional label/tag for this read (for AI to reference)'),
|
|
1014
|
+
})).min(1).max(20).describe('Array of file read requests (max 20)'),
|
|
1015
|
+
includeContext: z.boolean().default(true).describe('If true and graph is indexed, include containing symbol info'),
|
|
1016
|
+
}, async ({ files: fileRequests, includeContext }) => {
|
|
1017
|
+
const MAX_BATCH_FILES = 20;
|
|
1018
|
+
const MAX_LINES_PER_READ = 300;
|
|
1019
|
+
const DEFAULT_BATCH_WINDOW = 100;
|
|
1020
|
+
const capped = fileRequests.slice(0, MAX_BATCH_FILES);
|
|
1021
|
+
const results = [];
|
|
1022
|
+
for (const req of capped) {
|
|
1023
|
+
try {
|
|
1024
|
+
// Resolve path: if not absolute, resolve against projectRoot
|
|
1025
|
+
let resolved;
|
|
1026
|
+
if (isAbsolute(req.path)) {
|
|
1027
|
+
resolved = resolve(req.path);
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
resolved = resolvePath(req.path, config.projectRoot, false);
|
|
1031
|
+
}
|
|
1032
|
+
if (!resolved) {
|
|
1033
|
+
results.push({
|
|
1034
|
+
path: req.path,
|
|
1035
|
+
label: req.label,
|
|
1036
|
+
startLine: req.startLine,
|
|
1037
|
+
endLine: 0,
|
|
1038
|
+
totalLines: 0,
|
|
1039
|
+
lines: [],
|
|
1040
|
+
error: `Path "${req.path}" resolves outside the project root`,
|
|
1041
|
+
});
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (!existsSync(resolved)) {
|
|
1045
|
+
results.push({
|
|
1046
|
+
path: req.path,
|
|
1047
|
+
label: req.label,
|
|
1048
|
+
startLine: req.startLine,
|
|
1049
|
+
endLine: 0,
|
|
1050
|
+
totalLines: 0,
|
|
1051
|
+
lines: [],
|
|
1052
|
+
error: `File not found: ${resolved}`,
|
|
1053
|
+
});
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
const content = readFileSync(resolved, 'utf-8');
|
|
1057
|
+
const allLines = content.split('\n');
|
|
1058
|
+
const totalLines = allLines.length;
|
|
1059
|
+
// Normalise line range (1-indexed)
|
|
1060
|
+
const effectiveStart = Math.max(1, req.startLine);
|
|
1061
|
+
let effectiveEnd;
|
|
1062
|
+
if (req.endLine !== undefined) {
|
|
1063
|
+
effectiveEnd = Math.min(req.endLine, totalLines);
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
effectiveEnd = Math.min(effectiveStart + DEFAULT_BATCH_WINDOW - 1, totalLines);
|
|
1067
|
+
}
|
|
1068
|
+
// Cap at MAX_LINES_PER_READ
|
|
1069
|
+
if (effectiveEnd - effectiveStart + 1 > MAX_LINES_PER_READ) {
|
|
1070
|
+
effectiveEnd = effectiveStart + MAX_LINES_PER_READ - 1;
|
|
1071
|
+
}
|
|
1072
|
+
const lines = allLines.slice(effectiveStart - 1, effectiveEnd);
|
|
1073
|
+
// Graph context: find containing symbol
|
|
1074
|
+
let containingSymbol;
|
|
1075
|
+
if (includeContext && graph) {
|
|
1076
|
+
try {
|
|
1077
|
+
const nodes = graph.getFileStructure(resolved);
|
|
1078
|
+
if (nodes && nodes.length > 0) {
|
|
1079
|
+
let bestMatch = null;
|
|
1080
|
+
let bestRange = Infinity;
|
|
1081
|
+
for (const node of nodes) {
|
|
1082
|
+
if (node.type !== 'file' &&
|
|
1083
|
+
node.startLine <= effectiveStart &&
|
|
1084
|
+
node.endLine >= effectiveStart) {
|
|
1085
|
+
const range = node.endLine - node.startLine;
|
|
1086
|
+
if (range < bestRange) {
|
|
1087
|
+
bestRange = range;
|
|
1088
|
+
bestMatch = node;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (bestMatch) {
|
|
1093
|
+
containingSymbol = {
|
|
1094
|
+
name: bestMatch.name,
|
|
1095
|
+
type: bestMatch.type,
|
|
1096
|
+
signature: bestMatch.signature,
|
|
1097
|
+
startLine: bestMatch.startLine,
|
|
1098
|
+
endLine: bestMatch.endLine,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
catch {
|
|
1104
|
+
// Graph not indexed yet or file not in graph — that's fine
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1022
1107
|
results.push({
|
|
1023
|
-
path:
|
|
1108
|
+
path: resolved,
|
|
1024
1109
|
label: req.label,
|
|
1025
|
-
startLine:
|
|
1026
|
-
endLine:
|
|
1027
|
-
totalLines
|
|
1028
|
-
lines
|
|
1029
|
-
|
|
1110
|
+
startLine: effectiveStart,
|
|
1111
|
+
endLine: effectiveEnd,
|
|
1112
|
+
totalLines,
|
|
1113
|
+
lines,
|
|
1114
|
+
...(containingSymbol ? { containingSymbol } : {}),
|
|
1030
1115
|
});
|
|
1031
|
-
continue;
|
|
1032
1116
|
}
|
|
1033
|
-
|
|
1117
|
+
catch (err) {
|
|
1118
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1034
1119
|
results.push({
|
|
1035
1120
|
path: req.path,
|
|
1036
1121
|
label: req.label,
|
|
@@ -1038,85 +1123,12 @@ export function registerFilesystemTools(server, graph, config, estimator = defau
|
|
|
1038
1123
|
endLine: 0,
|
|
1039
1124
|
totalLines: 0,
|
|
1040
1125
|
lines: [],
|
|
1041
|
-
error: `
|
|
1126
|
+
error: `Failed to read: ${msg}`,
|
|
1042
1127
|
});
|
|
1043
|
-
continue;
|
|
1044
|
-
}
|
|
1045
|
-
const content = readFileSync(resolved, 'utf-8');
|
|
1046
|
-
const allLines = content.split('\n');
|
|
1047
|
-
const totalLines = allLines.length;
|
|
1048
|
-
// Normalise line range (1-indexed)
|
|
1049
|
-
const effectiveStart = Math.max(1, req.startLine);
|
|
1050
|
-
let effectiveEnd;
|
|
1051
|
-
if (req.endLine !== undefined) {
|
|
1052
|
-
effectiveEnd = Math.min(req.endLine, totalLines);
|
|
1053
1128
|
}
|
|
1054
|
-
else {
|
|
1055
|
-
effectiveEnd = Math.min(effectiveStart + DEFAULT_BATCH_WINDOW - 1, totalLines);
|
|
1056
|
-
}
|
|
1057
|
-
// Cap at MAX_LINES_PER_READ
|
|
1058
|
-
if (effectiveEnd - effectiveStart + 1 > MAX_LINES_PER_READ) {
|
|
1059
|
-
effectiveEnd = effectiveStart + MAX_LINES_PER_READ - 1;
|
|
1060
|
-
}
|
|
1061
|
-
const lines = allLines.slice(effectiveStart - 1, effectiveEnd);
|
|
1062
|
-
// Graph context: find containing symbol
|
|
1063
|
-
let containingSymbol;
|
|
1064
|
-
if (includeContext && graph) {
|
|
1065
|
-
try {
|
|
1066
|
-
const nodes = graph.getFileStructure(resolved);
|
|
1067
|
-
if (nodes && nodes.length > 0) {
|
|
1068
|
-
let bestMatch = null;
|
|
1069
|
-
let bestRange = Infinity;
|
|
1070
|
-
for (const node of nodes) {
|
|
1071
|
-
if (node.type !== 'file' &&
|
|
1072
|
-
node.startLine <= effectiveStart &&
|
|
1073
|
-
node.endLine >= effectiveStart) {
|
|
1074
|
-
const range = node.endLine - node.startLine;
|
|
1075
|
-
if (range < bestRange) {
|
|
1076
|
-
bestRange = range;
|
|
1077
|
-
bestMatch = node;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
if (bestMatch) {
|
|
1082
|
-
containingSymbol = {
|
|
1083
|
-
name: bestMatch.name,
|
|
1084
|
-
type: bestMatch.type,
|
|
1085
|
-
signature: bestMatch.signature,
|
|
1086
|
-
startLine: bestMatch.startLine,
|
|
1087
|
-
endLine: bestMatch.endLine,
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
catch {
|
|
1093
|
-
// Graph not indexed yet or file not in graph — that's fine
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
results.push({
|
|
1097
|
-
path: resolved,
|
|
1098
|
-
label: req.label,
|
|
1099
|
-
startLine: effectiveStart,
|
|
1100
|
-
endLine: effectiveEnd,
|
|
1101
|
-
totalLines,
|
|
1102
|
-
lines,
|
|
1103
|
-
...(containingSymbol ? { containingSymbol } : {}),
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
catch (err) {
|
|
1107
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1108
|
-
results.push({
|
|
1109
|
-
path: req.path,
|
|
1110
|
-
label: req.label,
|
|
1111
|
-
startLine: req.startLine,
|
|
1112
|
-
endLine: 0,
|
|
1113
|
-
totalLines: 0,
|
|
1114
|
-
lines: [],
|
|
1115
|
-
error: `Failed to read: ${msg}`,
|
|
1116
|
-
});
|
|
1117
1129
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1130
|
+
return mcpText(ok({ totalFiles: results.length, results }, estimator));
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1121
1133
|
}
|
|
1122
1134
|
//# sourceMappingURL=filesystem-tools.js.map
|