ai-mind-map 1.13.0 → 1.14.1

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 (43) hide show
  1. package/dist/index.js +61 -42
  2. package/dist/index.js.map +1 -1
  3. package/dist/tools/advanced-tools.d.ts.map +1 -1
  4. package/dist/tools/advanced-tools.js +229 -194
  5. package/dist/tools/advanced-tools.js.map +1 -1
  6. package/dist/tools/change-tools.d.ts.map +1 -1
  7. package/dist/tools/change-tools.js +37 -23
  8. package/dist/tools/change-tools.js.map +1 -1
  9. package/dist/tools/context-tools.d.ts.map +1 -1
  10. package/dist/tools/context-tools.js +60 -40
  11. package/dist/tools/context-tools.js.map +1 -1
  12. package/dist/tools/digest-tools.d.ts.map +1 -1
  13. package/dist/tools/digest-tools.js +66 -64
  14. package/dist/tools/digest-tools.js.map +1 -1
  15. package/dist/tools/evolving-tools.d.ts.map +1 -1
  16. package/dist/tools/evolving-tools.js +26 -20
  17. package/dist/tools/evolving-tools.js.map +1 -1
  18. package/dist/tools/filesystem-tools.d.ts.map +1 -1
  19. package/dist/tools/filesystem-tools.js +424 -412
  20. package/dist/tools/filesystem-tools.js.map +1 -1
  21. package/dist/tools/flow-tools.d.ts.map +1 -1
  22. package/dist/tools/flow-tools.js +134 -107
  23. package/dist/tools/flow-tools.js.map +1 -1
  24. package/dist/tools/graph-tools.d.ts.map +1 -1
  25. package/dist/tools/graph-tools.js +48 -36
  26. package/dist/tools/graph-tools.js.map +1 -1
  27. package/dist/tools/memory-tools.d.ts.map +1 -1
  28. package/dist/tools/memory-tools.js +45 -33
  29. package/dist/tools/memory-tools.js.map +1 -1
  30. package/dist/tools/project-map-tool.d.ts +22 -0
  31. package/dist/tools/project-map-tool.d.ts.map +1 -0
  32. package/dist/tools/project-map-tool.js +877 -0
  33. package/dist/tools/project-map-tool.js.map +1 -0
  34. package/dist/tools/semantic-tools.d.ts.map +1 -1
  35. package/dist/tools/semantic-tools.js +49 -31
  36. package/dist/tools/semantic-tools.js.map +1 -1
  37. package/dist/tools/session-tools.d.ts.map +1 -1
  38. package/dist/tools/session-tools.js +227 -168
  39. package/dist/tools/session-tools.js.map +1 -1
  40. package/dist/tools/smart-tools.d.ts.map +1 -1
  41. package/dist/tools/smart-tools.js +178 -137
  42. package/dist/tools/smart-tools.js.map +1 -1
  43. 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
- server.tool('mindmap_project_summary', 'Get a complete project overview in a single call — WITHOUT indexing. ' +
565
- 'Detects project type, counts files by language, builds a compact directory tree, ' +
566
- 'finds entry points, and reads key config files. Works instantly on any project.', {
567
- projectPath: z.string().describe('Absolute path to the project root'),
568
- }, async ({ projectPath }) => {
569
- try {
570
- if (!existsSync(projectPath)) {
571
- return mcpText(fail(`Project path not found: ${projectPath}`));
572
- }
573
- const stat = statSync(projectPath);
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
- catch {
607
- // use directory name
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
- // Build compact directory tree
611
- const directoryTree = buildCompactTree(projectPath, projectPath, gitignoreNames);
612
- // Read config summaries
613
- const configSummary = readConfigSummary(projectPath);
614
- const result = {
615
- projectPath,
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
- 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.', {
683
- pattern: z.string().describe('Search pattern (literal text or regex)'),
684
- regex: z.boolean().default(false).describe('Treat pattern as regex'),
685
- fileGlob: z.string().optional().describe("Filter by file extension pattern, e.g. '*.cs', '*.ts'"),
686
- caseSensitive: z.boolean().default(false).describe('Case-sensitive matching'),
687
- contextLines: z.number().int().min(0).max(5).default(0).describe('Lines of context before/after each match (max 5)'),
688
- maxResults: z.number().int().min(1).default(100).describe('Maximum results to return'),
689
- path: z.string().optional().describe('Search within a specific subdirectory (absolute or relative to projectRoot)'),
690
- invertMatch: z.boolean().default(false).describe('Return lines that do NOT match'),
691
- wholeWord: z.boolean().default(false).describe('Match whole words only'),
692
- }, async ({ pattern, regex: isRegex, fileGlob, caseSensitive, contextLines, maxResults, path: subPath, invertMatch, wholeWord }) => {
693
- try {
694
- // Determine search root
695
- let searchRoot = config.projectRoot;
696
- if (subPath) {
697
- const resolved = resolvePath(subPath, config.projectRoot, true);
698
- if (!resolved) {
699
- return mcpText(fail(`Path "${subPath}" resolves outside the project root`));
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
- if (!existsSync(resolved)) {
702
- return mcpText(fail(`Path not found: ${resolved}`));
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
- searchRoot = resolved;
705
- }
706
- // Build file glob regex
707
- let globRegex = null;
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
- // Skip large files
748
- try {
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
- catch {
754
- continue;
726
+ if (wholeWord) {
727
+ searchPattern = `\\b${searchPattern}\\b`;
755
728
  }
756
- // Skip binary files
757
- if (isBinaryFile(filePath))
758
- continue;
759
- let content;
729
+ const flags = caseSensitive ? 'g' : 'gi';
730
+ let searchRegex;
760
731
  try {
761
- content = readFileSync(filePath, 'utf-8');
732
+ searchRegex = new RegExp(searchPattern, flags);
762
733
  }
763
- catch {
764
- continue;
734
+ catch (err) {
735
+ const msg = err instanceof Error ? err.message : String(err);
736
+ return mcpText(fail(`Invalid regex pattern: ${msg}`));
765
737
  }
766
- const lines = content.split('\n');
767
- const relativePath = relative(config.projectRoot, filePath).replace(/\\/g, '/');
768
- for (let i = 0; i < lines.length; i++) {
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
- const line = lines[i];
774
- searchRegex.lastIndex = 0;
775
- const matches = line.match(searchRegex);
776
- const hasMatch = matches !== null && matches.length > 0;
777
- if (invertMatch ? !hasMatch : hasMatch) {
778
- const matchCount = invertMatch ? 0 : (matches?.length ?? 0);
779
- totalMatches += matchCount || 1;
780
- filesWithMatches.add(relativePath);
781
- const entry = {
782
- file: relativePath,
783
- line: line,
784
- lineNumber: i + 1,
785
- matchCount,
786
- };
787
- if (contextLines > 0) {
788
- const beforeStart = Math.max(0, i - contextLines);
789
- const afterEnd = Math.min(lines.length - 1, i + contextLines);
790
- entry.before = lines.slice(beforeStart, i);
791
- entry.after = lines.slice(i + 1, afterEnd + 1);
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
- return mcpText(ok({
798
- pattern,
799
- totalMatches,
800
- totalFiles: filePaths.length,
801
- filesWithMatches: filesWithMatches.size,
802
- truncated,
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
- 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.', {
861
- pattern: z.string().describe('Filename pattern to search for (supports wildcards: * and ?)'),
862
- regex: z.boolean().default(false).describe('Treat pattern as regex instead of glob'),
863
- type: z.enum(['file', 'dir', 'all']).default('file').describe("Filter by type: 'file', 'dir', or 'all'"),
864
- maxDepth: z.number().int().min(1).default(20).describe('Maximum directory depth to search'),
865
- maxResults: z.number().int().min(1).default(50).describe('Maximum results'),
866
- path: z.string().optional().describe('Search within a specific subdirectory'),
867
- includeSize: z.boolean().default(true).describe('Include file size in results'),
868
- includeModified: z.boolean().default(false).describe('Include last modified timestamp'),
869
- sortBy: z.enum(['relevance', 'name', 'size', 'modified']).default('relevance').describe("Sort by 'relevance', 'name', 'size', or 'modified'"),
870
- }, async ({ pattern, regex: isRegex, type: filterType, maxDepth, maxResults, path: subPath, includeSize, includeModified, sortBy }) => {
871
- try {
872
- // Determine search root
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
- if (isRegex) {
888
- matchRegex = new RegExp(pattern, 'i');
889
- }
890
- else {
891
- matchRegex = globToRegex(pattern);
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
- catch (err) {
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
- const indexedFiles = graph.getIndexedFiles();
904
- if (indexedFiles && indexedFiles.length > 0) {
905
- usedIndex = true;
906
- for (const filePath of indexedFiles) {
907
- candidates.push({
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
- // Graph not ready, fall through to filesystem walk
902
+ catch (err) {
903
+ const msg = err instanceof Error ? err.message : String(err);
904
+ return mcpText(fail(`Invalid pattern: ${msg}`));
917
905
  }
918
- }
919
- // Fallback to filesystem walk if no indexed files or path specified
920
- if (!usedIndex) {
921
- walkForFind(searchRoot, filterType, maxDepth, candidates);
922
- }
923
- // Filter by pattern
924
- const matched = [];
925
- for (const candidate of candidates) {
926
- if (matchRegex.test(candidate.name)) {
927
- const relativePath = relative(config.projectRoot, candidate.path).replace(/\\/g, '/');
928
- const entry = {
929
- path: candidate.path,
930
- relativePath,
931
- name: candidate.name,
932
- type: candidate.type,
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
- catch {
949
- // Can't stat — skip size/modified
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
- // Sort results
956
- matched.sort((a, b) => {
957
- switch (sortBy) {
958
- case 'name':
959
- return a.name.localeCompare(b.name);
960
- case 'size':
961
- return (b.sizeBytes ?? 0) - (a.sizeBytes ?? 0);
962
- case 'modified':
963
- return (b.modifiedAt ?? '').localeCompare(a.modifiedAt ?? '');
964
- case 'relevance':
965
- default: {
966
- if (a.score !== b.score)
967
- return b.score - a.score;
968
- return a.name.localeCompare(b.name);
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
- const truncated = matched.length > maxResults;
973
- const results = matched.slice(0, maxResults);
974
- // Clean up internal-only sort fields if not requested
975
- if (!includeSize) {
976
- for (const r of results)
977
- delete r.sizeBytes;
991
+ return mcpText(ok({
992
+ pattern,
993
+ totalResults: matched.length,
994
+ truncated,
995
+ results,
996
+ }, estimator));
978
997
  }
979
- if (!includeModified) {
980
- for (const r of results)
981
- delete r.modifiedAt;
998
+ catch (err) {
999
+ const msg = err instanceof Error ? err.message : String(err);
1000
+ return mcpText(fail(`Find file failed: ${msg}`));
982
1001
  }
983
- return mcpText(ok({
984
- pattern,
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
- server.tool('mindmap_batch_read', 'Read multiple file regions in a single call. Eliminates per-call overhead when reading from several files. ' +
997
- 'Each file request specifies a path and optional line range. Returns all results at once.', {
998
- files: z.array(z.object({
999
- path: z.string().describe('Absolute or relative file path'),
1000
- startLine: z.number().int().min(1).default(1).describe('First line to read (1-indexed)'),
1001
- endLine: z.number().int().min(1).optional().describe('Last line to read (1-indexed). Omit to read 100 lines from startLine'),
1002
- label: z.string().optional().describe('Optional label/tag for this read (for AI to reference)'),
1003
- })).min(1).max(20).describe('Array of file read requests (max 20)'),
1004
- includeContext: z.boolean().default(true).describe('If true and graph is indexed, include containing symbol info'),
1005
- }, async ({ files: fileRequests, includeContext }) => {
1006
- const MAX_BATCH_FILES = 20;
1007
- const MAX_LINES_PER_READ = 300;
1008
- const DEFAULT_BATCH_WINDOW = 100;
1009
- const capped = fileRequests.slice(0, MAX_BATCH_FILES);
1010
- const results = [];
1011
- for (const req of capped) {
1012
- try {
1013
- // Resolve path: if not absolute, resolve against projectRoot
1014
- let resolved;
1015
- if (isAbsolute(req.path)) {
1016
- resolved = resolve(req.path);
1017
- }
1018
- else {
1019
- resolved = resolvePath(req.path, config.projectRoot, false);
1020
- }
1021
- if (!resolved) {
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: req.path,
1108
+ path: resolved,
1024
1109
  label: req.label,
1025
- startLine: req.startLine,
1026
- endLine: 0,
1027
- totalLines: 0,
1028
- lines: [],
1029
- error: `Path "${req.path}" resolves outside the project root`,
1110
+ startLine: effectiveStart,
1111
+ endLine: effectiveEnd,
1112
+ totalLines,
1113
+ lines,
1114
+ ...(containingSymbol ? { containingSymbol } : {}),
1030
1115
  });
1031
- continue;
1032
1116
  }
1033
- if (!existsSync(resolved)) {
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: `File not found: ${resolved}`,
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
- return mcpText(ok({ totalFiles: results.length, results }, estimator));
1120
- });
1130
+ return mcpText(ok({ totalFiles: results.length, results }, estimator));
1131
+ });
1132
+ }
1121
1133
  }
1122
1134
  //# sourceMappingURL=filesystem-tools.js.map