ai-mind-map 1.12.3 → 1.12.4

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.
@@ -6,9 +6,12 @@
6
6
  * knowledge-graph-powered tools with fast, no-dependency operations.
7
7
  *
8
8
  * Tools:
9
- * - mindmap_list_dir — Directory listing (no indexing)
10
- * - mindmap_read_lines — Read specific line ranges with optional graph context
9
+ * - mindmap_list_dir — Directory listing (no indexing)
10
+ * - mindmap_read_lines — Read specific line ranges with optional graph context
11
11
  * - mindmap_project_summary — One-call project overview before indexing
12
+ * - mindmap_batch_read — Read multiple file regions in a single call
13
+ * - mindmap_grep — Text search across project files (like ripgrep)
14
+ * - mindmap_find_file — Find files by name pattern
12
15
  */
13
16
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
17
  import type { MindMapConfig } from '../types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"filesystem-tools.d.ts","sourceRoot":"","sources":["../../src/tools/filesystem-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAYH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAc,aAAa,EAAa,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAM7D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CAChC;AAolBD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,aAAa,EACrB,SAAS,GAAE,eAAkC,GAC5C,IAAI,CA4JN"}
1
+ {"version":3,"file":"filesystem-tools.d.ts","sourceRoot":"","sources":["../../src/tools/filesystem-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAeH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAc,aAAa,EAAa,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAM7D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CAChC;AAolBD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,aAAa,EACrB,SAAS,GAAE,eAAkC,GAC5C,IAAI,CAwsBN"}
@@ -6,12 +6,15 @@
6
6
  * knowledge-graph-powered tools with fast, no-dependency operations.
7
7
  *
8
8
  * Tools:
9
- * - mindmap_list_dir — Directory listing (no indexing)
10
- * - mindmap_read_lines — Read specific line ranges with optional graph context
9
+ * - mindmap_list_dir — Directory listing (no indexing)
10
+ * - mindmap_read_lines — Read specific line ranges with optional graph context
11
11
  * - mindmap_project_summary — One-call project overview before indexing
12
+ * - mindmap_batch_read — Read multiple file regions in a single call
13
+ * - mindmap_grep — Text search across project files (like ripgrep)
14
+ * - mindmap_find_file — Find files by name pattern
12
15
  */
13
16
  import { z } from 'zod';
14
- import { readFileSync, readdirSync, statSync, existsSync, } from 'node:fs';
17
+ import { readFileSync, readdirSync, statSync, existsSync, openSync, readSync, closeSync, } from 'node:fs';
15
18
  import { resolve, relative, extname, basename, join, isAbsolute } from 'node:path';
16
19
  const defaultEstimator = {
17
20
  estimate: (text) => Math.ceil(text.length / 4),
@@ -628,5 +631,492 @@ export function registerFilesystemTools(server, graph, config, estimator = defau
628
631
  return mcpText(fail(`Failed to generate project summary: ${msg}`));
629
632
  }
630
633
  });
634
+ // ── mindmap_grep ──────────────────────────────────────────────
635
+ /** Detect binary files by checking for null bytes in the first 512 bytes */
636
+ function isBinaryFile(filePath) {
637
+ try {
638
+ const buf = Buffer.alloc(512);
639
+ const fd = openSync(filePath, 'r');
640
+ const bytesRead = readSync(fd, buf, 0, 512, 0);
641
+ closeSync(fd);
642
+ for (let i = 0; i < bytesRead; i++) {
643
+ if (buf[i] === 0)
644
+ return true;
645
+ }
646
+ return false;
647
+ }
648
+ catch {
649
+ return false;
650
+ }
651
+ }
652
+ /** Max file size (1 MB) for grep scanning */
653
+ const MAX_GREP_FILE_SIZE = 1_048_576;
654
+ function walkForGrep(dir, fileGlob, results) {
655
+ let entries;
656
+ try {
657
+ entries = readdirSync(dir, { withFileTypes: true });
658
+ }
659
+ catch {
660
+ return;
661
+ }
662
+ for (const entry of entries) {
663
+ if (entry.name.startsWith('.'))
664
+ continue;
665
+ const fullPath = join(dir, entry.name);
666
+ if (entry.isDirectory()) {
667
+ if (SKIP_DIRS.has(entry.name))
668
+ continue;
669
+ walkForGrep(fullPath, fileGlob, results);
670
+ }
671
+ else if (entry.isFile()) {
672
+ if (fileGlob) {
673
+ const ext = extname(entry.name).toLowerCase();
674
+ const nameToTest = ext || entry.name;
675
+ if (!fileGlob.test(entry.name) && !fileGlob.test(nameToTest))
676
+ continue;
677
+ }
678
+ results.push(fullPath);
679
+ }
680
+ }
681
+ }
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`));
700
+ }
701
+ if (!existsSync(resolved)) {
702
+ return mcpText(fail(`Path not found: ${resolved}`));
703
+ }
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;
746
+ }
747
+ // Skip large files
748
+ try {
749
+ const stat = statSync(filePath);
750
+ if (stat.size > MAX_GREP_FILE_SIZE)
751
+ continue;
752
+ }
753
+ catch {
754
+ continue;
755
+ }
756
+ // Skip binary files
757
+ if (isBinaryFile(filePath))
758
+ continue;
759
+ let content;
760
+ try {
761
+ content = readFileSync(filePath, 'utf-8');
762
+ }
763
+ catch {
764
+ continue;
765
+ }
766
+ const lines = content.split('\n');
767
+ const relativePath = relative(config.projectRoot, filePath).replace(/\\/g, '/');
768
+ for (let i = 0; i < lines.length; i++) {
769
+ if (results.length >= maxResults) {
770
+ truncated = true;
771
+ break;
772
+ }
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);
792
+ }
793
+ results.push(entry);
794
+ }
795
+ }
796
+ }
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
+ // ── mindmap_find_file ─────────────────────────────────────────
812
+ /** Convert a glob pattern (with * and ?) to a RegExp */
813
+ function globToRegex(glob) {
814
+ const escaped = glob.replace(/[.+^${}()|[\]]/g, '\\$&');
815
+ const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
816
+ return new RegExp(`^${pattern}$`, 'i');
817
+ }
818
+ function walkForFind(dir, filterType, maxDepth, results, depth = 0) {
819
+ if (depth > maxDepth)
820
+ return;
821
+ let entries;
822
+ try {
823
+ entries = readdirSync(dir, { withFileTypes: true });
824
+ }
825
+ catch {
826
+ return;
827
+ }
828
+ for (const entry of entries) {
829
+ if (entry.name.startsWith('.'))
830
+ continue;
831
+ const fullPath = join(dir, entry.name);
832
+ if (entry.isDirectory()) {
833
+ if (SKIP_DIRS.has(entry.name))
834
+ continue;
835
+ if (filterType === 'dir' || filterType === 'all') {
836
+ results.push({ path: fullPath, name: entry.name, type: 'dir' });
837
+ }
838
+ walkForFind(fullPath, filterType, maxDepth, results, depth + 1);
839
+ }
840
+ else if (entry.isFile()) {
841
+ if (filterType === 'file' || filterType === 'all') {
842
+ results.push({ path: fullPath, name: entry.name, type: 'file' });
843
+ }
844
+ }
845
+ }
846
+ }
847
+ function scoreMatch(name, rawPattern) {
848
+ const nameLower = name.toLowerCase();
849
+ const patternLower = rawPattern.toLowerCase();
850
+ // Strip glob chars for literal comparisons
851
+ const literalPattern = rawPattern.replace(/[*?]/g, '').toLowerCase();
852
+ if (nameLower === literalPattern)
853
+ return 100;
854
+ if (nameLower.startsWith(literalPattern))
855
+ return 80;
856
+ if (nameLower.includes(literalPattern))
857
+ return 60;
858
+ return 40;
859
+ }
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;
886
+ try {
887
+ if (isRegex) {
888
+ matchRegex = new RegExp(pattern, 'i');
889
+ }
890
+ else {
891
+ matchRegex = globToRegex(pattern);
892
+ }
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')) {
902
+ 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
+ }
913
+ }
914
+ }
915
+ catch {
916
+ // Graph not ready, fall through to filesystem walk
917
+ }
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();
947
+ }
948
+ catch {
949
+ // Can't stat — skip size/modified
950
+ }
951
+ }
952
+ matched.push(entry);
953
+ }
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);
969
+ }
970
+ }
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;
978
+ }
979
+ if (!includeModified) {
980
+ for (const r of results)
981
+ delete r.modifiedAt;
982
+ }
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
+ });
995
+ // ── 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) {
1022
+ results.push({
1023
+ path: req.path,
1024
+ 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`,
1030
+ });
1031
+ continue;
1032
+ }
1033
+ if (!existsSync(resolved)) {
1034
+ results.push({
1035
+ path: req.path,
1036
+ label: req.label,
1037
+ startLine: req.startLine,
1038
+ endLine: 0,
1039
+ totalLines: 0,
1040
+ lines: [],
1041
+ error: `File not found: ${resolved}`,
1042
+ });
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
+ }
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
+ }
1118
+ }
1119
+ return mcpText(ok({ totalFiles: results.length, results }, estimator));
1120
+ });
631
1121
  }
632
1122
  //# sourceMappingURL=filesystem-tools.js.map