mcp-fs-shell-windows 0.2.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.
Files changed (59) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +147 -0
  3. package/dist/append_files/handler.js +32 -0
  4. package/dist/append_files/schema.js +7 -0
  5. package/dist/checksum_files/handler.js +33 -0
  6. package/dist/checksum_files/schema.js +5 -0
  7. package/dist/checksum_files_verif/handler.js +66 -0
  8. package/dist/checksum_files_verif/helpers.js +6 -0
  9. package/dist/checksum_files_verif/schema.js +8 -0
  10. package/dist/content_diff/handler.js +11 -0
  11. package/dist/content_diff/schema.js +7 -0
  12. package/dist/copy_files/handler.js +43 -0
  13. package/dist/copy_files/helpers.js +24 -0
  14. package/dist/copy_files/schema.js +7 -0
  15. package/dist/count_lines/handler.js +144 -0
  16. package/dist/count_lines/schema.js +9 -0
  17. package/dist/create_directories/handler.js +29 -0
  18. package/dist/create_directories/schema.js +4 -0
  19. package/dist/delete_files/handler.js +42 -0
  20. package/dist/delete_files/schema.js +5 -0
  21. package/dist/delete_files_by_pattern/handler.js +46 -0
  22. package/dist/delete_files_by_pattern/schema.js +6 -0
  23. package/dist/directory_tree/handler.js +7 -0
  24. package/dist/directory_tree/helpers.js +47 -0
  25. package/dist/directory_tree/schema.js +5 -0
  26. package/dist/edit_files/handler.js +62 -0
  27. package/dist/edit_files/schema.js +11 -0
  28. package/dist/file_diff/handler.js +28 -0
  29. package/dist/file_diff/schema.js +5 -0
  30. package/dist/file_info/handler.js +30 -0
  31. package/dist/file_info/schema.js +4 -0
  32. package/dist/fuzzy_find_files/handler.js +74 -0
  33. package/dist/fuzzy_find_files/schema.js +8 -0
  34. package/dist/helpers/checksum.js +10 -0
  35. package/dist/helpers/diff.js +10 -0
  36. package/dist/helpers/path.js +87 -0
  37. package/dist/index.js +36 -0
  38. package/dist/list_directory/handler.js +42 -0
  39. package/dist/list_directory/schema.js +9 -0
  40. package/dist/move_files/handler.js +68 -0
  41. package/dist/move_files/schema.js +8 -0
  42. package/dist/patch_files/handler.js +34 -0
  43. package/dist/patch_files/helpers.js +103 -0
  44. package/dist/patch_files/schema.js +18 -0
  45. package/dist/read_files/handler.js +36 -0
  46. package/dist/read_files/schema.js +6 -0
  47. package/dist/search_files/handler.js +15 -0
  48. package/dist/search_files/helpers.js +71 -0
  49. package/dist/search_files/schema.js +8 -0
  50. package/dist/search_glob/handler.js +77 -0
  51. package/dist/search_glob/schema.js +8 -0
  52. package/dist/search_regex/handler.js +159 -0
  53. package/dist/search_regex/schema.js +10 -0
  54. package/dist/server.js +698 -0
  55. package/dist/shell/handler.js +687 -0
  56. package/dist/shell/schema.js +69 -0
  57. package/dist/write_new_files/handler.js +55 -0
  58. package/dist/write_new_files/schema.js +9 -0
  59. package/package.json +37 -0
@@ -0,0 +1,68 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { validatePath, ensureDirectoryExists } from "../helpers/path.js";
4
+ export async function handleMoveFiles(items, overwrite, allowedDirectories) {
5
+ const results = [];
6
+ const errors = [];
7
+ await Promise.all(items.map(async (item) => {
8
+ try {
9
+ // Validate both paths are within allowed directories
10
+ const validSource = await validatePath(item.source, allowedDirectories);
11
+ const validDestination = await validatePath(item.destination, allowedDirectories);
12
+ // Check if source exists
13
+ try {
14
+ await fs.access(validSource);
15
+ }
16
+ catch (error) {
17
+ throw new Error(`Source does not exist: ${item.source}`);
18
+ }
19
+ // Check if destination exists and handle based on overwrite flag
20
+ let destExists = true;
21
+ try {
22
+ await fs.access(validDestination);
23
+ }
24
+ catch (error) {
25
+ const code = error.code;
26
+ if (code === 'ENOENT') {
27
+ destExists = false;
28
+ }
29
+ else {
30
+ throw error;
31
+ }
32
+ }
33
+ if (destExists) {
34
+ if (!overwrite) {
35
+ throw new Error(`Destination already exists: ${item.destination}`);
36
+ }
37
+ // Remove existing destination so fs.rename can replace it
38
+ const destStats = await fs.stat(validDestination);
39
+ if (destStats.isDirectory()) {
40
+ await fs.rm(validDestination, { recursive: true, force: true });
41
+ }
42
+ else {
43
+ await fs.unlink(validDestination);
44
+ }
45
+ }
46
+ // Create parent directory for destination if it doesn't exist
47
+ const destDir = path.dirname(validDestination);
48
+ await ensureDirectoryExists(destDir);
49
+ // Move the file
50
+ await fs.rename(validSource, validDestination);
51
+ results.push(`Successfully moved ${item.source} to ${item.destination}`);
52
+ }
53
+ catch (error) {
54
+ const errorMessage = error instanceof Error ? error.message : String(error);
55
+ errors.push(`Failed to move ${item.source} to ${item.destination}: ${errorMessage}`);
56
+ }
57
+ }));
58
+ // Format the results
59
+ const successCount = results.length;
60
+ const errorCount = errors.length;
61
+ let output = `Processed ${successCount + errorCount} move operations:\n`;
62
+ output += `- ${successCount} items moved successfully\n`;
63
+ if (errorCount > 0) {
64
+ output += `- ${errorCount} operations failed\n\n`;
65
+ output += "Errors:\n" + errors.join("\n");
66
+ }
67
+ return output;
68
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ export const MoveFilesArgsSchema = z.object({
3
+ items: z.array(z.object({
4
+ source: z.string().describe("Path to the source file or directory"),
5
+ destination: z.string().describe("Path to the destination file or directory"),
6
+ })).describe("Array of source-destination pairs"),
7
+ overwrite: z.boolean().default(false).describe("Whether to overwrite existing files at destination"),
8
+ });
@@ -0,0 +1,34 @@
1
+ import { validatePath } from "../helpers/path.js";
2
+ import { applyFilePatches } from "./helpers.js";
3
+ export async function handlePatchFiles(files, dryRun, options, allowedDirectories) {
4
+ const results = [];
5
+ const errors = [];
6
+ await Promise.all(files.map(async (file) => {
7
+ try {
8
+ // Validate path is within allowed directories
9
+ const validPath = await validatePath(file.path, allowedDirectories);
10
+ // Apply patches to the file
11
+ const result = await applyFilePatches(validPath, file.patches, dryRun, options);
12
+ results.push(`File: ${file.path}\n${result}`);
13
+ }
14
+ catch (error) {
15
+ const errorMessage = error instanceof Error ? error.message : String(error);
16
+ errors.push(`Failed to patch ${file.path}: ${errorMessage}`);
17
+ }
18
+ }));
19
+ // Format the results
20
+ const successCount = results.length;
21
+ const errorCount = errors.length;
22
+ let output = `Processed ${successCount + errorCount} files:\n`;
23
+ output += `- ${successCount} files patched successfully\n`;
24
+ if (errorCount > 0) {
25
+ output += `- ${errorCount} files failed\n\n`;
26
+ output += "Errors:\n" + errors.join("\n\n");
27
+ }
28
+ // Add patch results for successful patches
29
+ if (successCount > 0) {
30
+ output += "\n\nPatch Results:\n" + "=".repeat(40) + "\n";
31
+ output += results.join("\n" + "=".repeat(40) + "\n");
32
+ }
33
+ return output;
34
+ }
@@ -0,0 +1,103 @@
1
+ import fs from "fs/promises";
2
+ import { createUnifiedDiff, normalizeLineEndings } from "../helpers/diff.js";
3
+ export async function applyFilePatches(filePath, patches, dryRun = false, options = { preserveIndentation: true }) {
4
+ // Read file content and normalize line endings
5
+ const content = normalizeLineEndings(await fs.readFile(filePath, "utf-8"));
6
+ // Split content into lines
7
+ const contentLines = content.split("\n");
8
+ // Sort patches by line number in descending order so every patch references
9
+ // the original line numbers (earlier lines keep their positions).
10
+ const sortedPatches = [...patches].sort((a, b) => b.startLine - a.startLine);
11
+ // Apply patches sequentially
12
+ let modifiedContentLines = [...contentLines];
13
+ const patchResults = [];
14
+ for (const patch of sortedPatches) {
15
+ const { startLine, endLine, newText } = patch;
16
+ const action = patch.action ?? "replace";
17
+ const patchResult = { patch, applied: false };
18
+ if (action === "insertBefore") {
19
+ // Insert newText before startLine (1 = before the first line;
20
+ // contentLines.length + 1 = append after the last line).
21
+ if (startLine < 1 || startLine > contentLines.length + 1) {
22
+ patchResult.message = `Invalid insert position: ${startLine} (file has ${contentLines.length} lines; valid positions 1-${contentLines.length + 1})`;
23
+ patchResults.push(patchResult);
24
+ throw new Error(patchResult.message);
25
+ }
26
+ const normalizedNew = normalizeLineEndings(newText);
27
+ let newLines = normalizedNew.split("\n");
28
+ if (options.preserveIndentation && newText.length > 0) {
29
+ // Detect indentation from the line being inserted before
30
+ const refIndex = Math.min(startLine - 1, contentLines.length - 1);
31
+ const originalIndent = modifiedContentLines[refIndex]?.match(/^\s*/)?.[0] || "";
32
+ newLines = newLines.map((line, idx) => {
33
+ if (idx === 0)
34
+ return originalIndent + line.trimStart();
35
+ return line;
36
+ });
37
+ }
38
+ modifiedContentLines.splice(startLine - 1, 0, ...newLines);
39
+ }
40
+ else if (action === "delete") {
41
+ // Remove lines startLine-endLine entirely
42
+ if (startLine < 1 || endLine < startLine || endLine > contentLines.length) {
43
+ patchResult.message = `Invalid line range: ${startLine}-${endLine} (file has ${contentLines.length} lines)`;
44
+ patchResults.push(patchResult);
45
+ throw new Error(patchResult.message);
46
+ }
47
+ modifiedContentLines.splice(startLine - 1, endLine - startLine + 1);
48
+ }
49
+ else {
50
+ // replace (default): original behavior
51
+ if (startLine < 1 || endLine < startLine || endLine > contentLines.length) {
52
+ patchResult.message = `Invalid line range: ${startLine}-${endLine} (file has ${contentLines.length} lines)`;
53
+ patchResults.push(patchResult);
54
+ throw new Error(patchResult.message);
55
+ }
56
+ const normalizedNew = normalizeLineEndings(newText);
57
+ const newLines = normalizedNew.split("\n");
58
+ if (options.preserveIndentation) {
59
+ // Detect indentation from the first line being replaced
60
+ const originalIndent = modifiedContentLines[startLine - 1].match(/^\s*/)?.[0] || "";
61
+ // Apply indentation to new lines
62
+ const indentedNewLines = newLines.map((line, idx) => {
63
+ if (idx === 0)
64
+ return originalIndent + line.trimStart();
65
+ return line;
66
+ });
67
+ // Replace lines
68
+ modifiedContentLines.splice(startLine - 1, endLine - startLine + 1, ...indentedNewLines);
69
+ }
70
+ else {
71
+ // Replace lines without preserving indentation
72
+ modifiedContentLines.splice(startLine - 1, endLine - startLine + 1, ...newLines);
73
+ }
74
+ }
75
+ patchResult.applied = true;
76
+ patchResults.push(patchResult);
77
+ }
78
+ const modifiedContent = modifiedContentLines.join("\n");
79
+ // Create unified diff
80
+ const diff = createUnifiedDiff(content, modifiedContent, filePath, filePath);
81
+ // Format diff with appropriate number of backticks
82
+ let numBackticks = 3;
83
+ while (diff.includes("`".repeat(numBackticks))) {
84
+ numBackticks++;
85
+ }
86
+ // Build result with detailed information
87
+ let resultText = `${"`".repeat(numBackticks)}diff\n${diff}${"`".repeat(numBackticks)}\n\n`;
88
+ // Add patch details
89
+ resultText += "Patch details:\n";
90
+ patchResults.forEach((result, i) => {
91
+ const { startLine, endLine } = result.patch;
92
+ const action = result.patch.action ?? "replace";
93
+ const range = action === "insertBefore" ? `before line ${startLine}` : `lines ${startLine}-${endLine}`;
94
+ resultText += `Patch ${i + 1}: ${result.applied ? "APPLIED" : "FAILED"} (${action}, ${range})\n`;
95
+ if (result.message) {
96
+ resultText += ` Message: ${result.message}\n`;
97
+ }
98
+ });
99
+ if (!dryRun) {
100
+ await fs.writeFile(filePath, modifiedContent, "utf-8");
101
+ }
102
+ return resultText;
103
+ }
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ // Options for patch operations
3
+ const PatchOptionsSchema = z.object({
4
+ preserveIndentation: z.boolean().default(true).describe("Preserve indentation when replacing/inserting text")
5
+ });
6
+ export const PatchFilesArgsSchema = z.object({
7
+ files: z.array(z.object({
8
+ path: z.string().describe("Path to the file to patch"),
9
+ patches: z.array(z.object({
10
+ startLine: z.number().int().min(1).describe("Line number where the patch starts (1-indexed). For insertBefore, the line to insert before (use total lines + 1 to append to the end)."),
11
+ endLine: z.number().int().min(1).describe("Line number where the patch ends (1-indexed). Ignored for insertBefore."),
12
+ newText: z.string().describe("Text to insert/replace with. Ignored for delete."),
13
+ action: z.enum(["replace", "insertBefore", "delete"]).optional().default("replace").describe("replace (default): replace lines startLine-endLine with newText. insertBefore: insert newText before startLine. delete: remove lines startLine-endLine."),
14
+ })).describe("Array of patches to apply to this file")
15
+ })).describe("Array of files to patch"),
16
+ dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format"),
17
+ options: PatchOptionsSchema.optional().describe("Options for controlling patch behavior")
18
+ });
@@ -0,0 +1,36 @@
1
+ import fs from "fs/promises";
2
+ import { validatePath } from "../helpers/path.js";
3
+ export async function handleReadFiles(filePaths, allowedDirectories, offset, limit) {
4
+ const results = await Promise.all(filePaths.map(async (filePath) => {
5
+ try {
6
+ const validPath = await validatePath(filePath, allowedDirectories);
7
+ const content = await fs.readFile(validPath, "utf-8");
8
+ // Split into lines, keeping original line numbers
9
+ const lines = content.split("\n");
10
+ const total = lines.length;
11
+ let startLine = 1;
12
+ let endLine = total;
13
+ if (offset !== undefined || limit !== undefined) {
14
+ startLine = offset ?? 1;
15
+ if (startLine > total) {
16
+ throw new Error(`offset ${startLine} is beyond end of file (${total} lines)`);
17
+ }
18
+ endLine = limit !== undefined ? Math.min(total, startLine + limit - 1) : total;
19
+ }
20
+ // Format content with line numbers
21
+ const numberedContent = lines
22
+ .slice(startLine - 1, endLine)
23
+ .map((line, idx) => `${startLine + idx}: ${line}`)
24
+ .join("\n");
25
+ const header = offset !== undefined || limit !== undefined
26
+ ? `${filePath}: [lines ${startLine}-${endLine} of ${total}]\n`
27
+ : `${filePath}:\n`;
28
+ return `${header}${numberedContent}\n`;
29
+ }
30
+ catch (error) {
31
+ const errorMessage = error instanceof Error ? error.message : String(error);
32
+ return `${filePath}: Error - ${errorMessage}`;
33
+ }
34
+ }));
35
+ return results.join("\n---\n");
36
+ }
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
2
+ export const ReadFilesArgsSchema = z.object({
3
+ paths: z.array(z.string()),
4
+ offset: z.number().int().min(1).optional().describe("Start reading from this line number (1-indexed). Omit to read from the beginning."),
5
+ limit: z.number().int().min(1).optional().describe("Maximum number of lines to read per file. Omit to read to the end."),
6
+ });
@@ -0,0 +1,15 @@
1
+ import { validatePath } from "../helpers/path.js";
2
+ import { searchFiles } from "./helpers.js";
3
+ export async function handleSearchFiles(directoryPath, pattern, excludePatterns, maxResults, offset, allowedDirectories) {
4
+ const validPath = await validatePath(directoryPath, allowedDirectories);
5
+ const { results, stopped } = await searchFiles(validPath, pattern, excludePatterns, allowedDirectories, maxResults, offset);
6
+ const shown = results.slice(offset - 1);
7
+ if (shown.length === 0) {
8
+ return stopped
9
+ ? `Found at least ${offset - 1} matching files, but offset ${offset} is beyond the collected range`
10
+ : "No matches found";
11
+ }
12
+ const end = offset + shown.length - 1;
13
+ const totalText = stopped ? `at least ${results.length}` : `${results.length}`;
14
+ return `Found ${totalText} files matching "${pattern}" (showing ${offset}-${end})\n\n` + shown.join("\n");
15
+ }
@@ -0,0 +1,71 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { minimatch } from "minimatch";
4
+ import { validatePath } from "../helpers/path.js";
5
+ export async function searchFiles(rootPath, pattern, excludePatterns = [], allowedDirectories, maxResults = 500, offset = 1) {
6
+ const results = [];
7
+ const effectiveCap = offset - 1 + maxResults;
8
+ let stopped = false;
9
+ async function search(currentPath) {
10
+ if (stopped)
11
+ return;
12
+ let entries;
13
+ try {
14
+ entries = await fs.readdir(currentPath, { withFileTypes: true });
15
+ }
16
+ catch {
17
+ return;
18
+ }
19
+ for (const entry of entries) {
20
+ if (stopped)
21
+ break;
22
+ const fullPath = path.join(currentPath, entry.name);
23
+ try {
24
+ // Validate each path before processing
25
+ await validatePath(fullPath, allowedDirectories);
26
+ // Check if path matches any exclude pattern
27
+ const relativePath = path.relative(rootPath, fullPath);
28
+ const shouldExclude = excludePatterns.some((excludePattern) => {
29
+ // Handle different pattern formats
30
+ // 1. If pattern already contains glob characters, use as is
31
+ // 2. If pattern is a simple name, match anywhere in path
32
+ // 3. If pattern is a path segment, match that segment
33
+ let globPattern = excludePattern;
34
+ if (!excludePattern.includes("*") && !excludePattern.includes("?")) {
35
+ if (excludePattern.includes("/")) {
36
+ globPattern = `**/${excludePattern}/**`;
37
+ }
38
+ else {
39
+ globPattern = `**/*${excludePattern}*/**`;
40
+ }
41
+ }
42
+ return minimatch(relativePath, globPattern, {
43
+ dot: true,
44
+ nocase: true,
45
+ matchBase: true,
46
+ });
47
+ });
48
+ if (shouldExclude) {
49
+ continue;
50
+ }
51
+ // Case-insensitive filename matching
52
+ if (entry.name.toLowerCase().includes(pattern.toLowerCase())) {
53
+ results.push(fullPath);
54
+ if (results.length >= effectiveCap) {
55
+ stopped = true;
56
+ break;
57
+ }
58
+ }
59
+ if (entry.isDirectory()) {
60
+ await search(fullPath);
61
+ }
62
+ }
63
+ catch (error) {
64
+ // Skip invalid paths during search
65
+ continue;
66
+ }
67
+ }
68
+ }
69
+ await search(rootPath);
70
+ return { results, stopped };
71
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ export const SearchFilesArgsSchema = z.object({
3
+ path: z.string(),
4
+ pattern: z.string(),
5
+ excludePatterns: z.array(z.string()).optional().default([]),
6
+ maxResults: z.number().optional().default(500).describe("Maximum number of results to return. Default: 500."),
7
+ offset: z.number().int().min(1).optional().default(1).describe("1-indexed result number to start from. Default: 1."),
8
+ });
@@ -0,0 +1,77 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { validatePath } from "../helpers/path.js";
4
+ import { minimatch } from "minimatch";
5
+ export async function handleSearchGlob(searchPath, pattern, excludePatterns, maxResults, offset, allowedDirectories) {
6
+ // Validate root path
7
+ const validRootPath = await validatePath(searchPath, allowedDirectories);
8
+ const results = [];
9
+ const effectiveCap = offset - 1 + maxResults;
10
+ let searchAborted = false;
11
+ async function findMatches(currentPath) {
12
+ if (searchAborted)
13
+ return;
14
+ try {
15
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
16
+ for (const entry of entries) {
17
+ if (searchAborted)
18
+ break;
19
+ const fullPath = path.join(currentPath, entry.name);
20
+ try {
21
+ // Validate each path before processing
22
+ await validatePath(fullPath, allowedDirectories);
23
+ // Get relative path for glob matching
24
+ const relativePath = path.relative(validRootPath, fullPath);
25
+ // Check if path should be excluded
26
+ const shouldExclude = excludePatterns.some((excludePattern) => {
27
+ return minimatch(relativePath, excludePattern, { dot: true });
28
+ });
29
+ if (shouldExclude) {
30
+ continue;
31
+ }
32
+ // Check if the path matches the provided pattern
33
+ if (minimatch(relativePath, pattern, { dot: true })) {
34
+ results.push(fullPath);
35
+ if (results.length >= effectiveCap) {
36
+ searchAborted = true;
37
+ break;
38
+ }
39
+ }
40
+ // Recursively search directories
41
+ if (entry.isDirectory()) {
42
+ await findMatches(fullPath);
43
+ }
44
+ }
45
+ catch (error) {
46
+ // Skip invalid paths during search
47
+ continue;
48
+ }
49
+ }
50
+ }
51
+ catch (error) {
52
+ // Skip directories we can't read
53
+ return;
54
+ }
55
+ }
56
+ await findMatches(validRootPath);
57
+ // Format the results
58
+ let output = "";
59
+ if (results.length === 0) {
60
+ output = `No files matching pattern: ${pattern}\n`;
61
+ }
62
+ else {
63
+ // Sort results by path, then apply pagination
64
+ results.sort();
65
+ const shown = results.slice(offset - 1);
66
+ const end = offset + shown.length - 1;
67
+ output = `Found ${results.length} files matching pattern: ${pattern} (showing ${offset}-${end})`;
68
+ if (searchAborted) {
69
+ output += ` (search stopped at ${effectiveCap} collected files)`;
70
+ }
71
+ output += "\n\n";
72
+ for (const result of shown) {
73
+ output += `${result}\n`;
74
+ }
75
+ }
76
+ return output;
77
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ export const SearchGlobArgsSchema = z.object({
3
+ path: z.string().describe("Root directory to search in"),
4
+ pattern: z.string().describe("Glob pattern to match files against (e.g. '**/*.js')"),
5
+ excludePatterns: z.array(z.string()).optional().default([]).describe("Glob patterns to exclude"),
6
+ maxResults: z.number().optional().default(500).describe("Maximum number of results to return"),
7
+ offset: z.number().int().min(1).optional().default(1).describe("1-indexed result number to start from. Default: 1."),
8
+ });
@@ -0,0 +1,159 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { validatePath } from "../helpers/path.js";
4
+ import { minimatch } from "minimatch";
5
+ export async function handleSearchRegex(searchPath, pattern, filePatterns, excludePatterns, maxResults, caseSensitive, offset, allowedDirectories) {
6
+ // Validate root path (may be a directory or a single file)
7
+ const validRootPath = await validatePath(searchPath, allowedDirectories);
8
+ const results = [];
9
+ let filesSearched = 0;
10
+ let matchesFound = 0;
11
+ let searchAborted = false;
12
+ const effectiveCap = offset - 1 + maxResults;
13
+ // Create regex pattern
14
+ const regexFlags = caseSensitive ? "mg" : "img";
15
+ let regex;
16
+ try {
17
+ regex = new RegExp(pattern, regexFlags);
18
+ }
19
+ catch (error) {
20
+ throw new Error(`Invalid regular expression: ${pattern}`);
21
+ }
22
+ async function scanFile(fullPath) {
23
+ if (searchAborted)
24
+ return;
25
+ try {
26
+ filesSearched++;
27
+ const content = await fs.readFile(fullPath, "utf-8");
28
+ const lines = content.split("\n");
29
+ regex.lastIndex = 0;
30
+ let match;
31
+ while ((match = regex.exec(content)) !== null) {
32
+ matchesFound++;
33
+ // Find the line number for this match
34
+ const matchPosition = match.index;
35
+ let lineNumber = 0;
36
+ let charCount = 0;
37
+ for (let i = 0; i < lines.length; i++) {
38
+ charCount += lines[i].length + 1; // +1 for the newline
39
+ if (charCount > matchPosition) {
40
+ lineNumber = i + 1;
41
+ break;
42
+ }
43
+ }
44
+ // Get the line content
45
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
46
+ results.push({
47
+ file: fullPath,
48
+ line: lineNumber,
49
+ content: lineContent,
50
+ match: match[0],
51
+ });
52
+ if (results.length >= effectiveCap) {
53
+ searchAborted = true;
54
+ break;
55
+ }
56
+ // Guard against zero-length matches (e.g. pattern "a*") looping forever
57
+ if (match[0].length === 0) {
58
+ regex.lastIndex++;
59
+ }
60
+ }
61
+ }
62
+ catch (error) {
63
+ // Skip files that can't be read as text
64
+ }
65
+ }
66
+ async function searchDirectory(dirPath) {
67
+ if (searchAborted)
68
+ return;
69
+ try {
70
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
71
+ for (const entry of entries) {
72
+ if (searchAborted)
73
+ break;
74
+ const fullPath = path.join(dirPath, entry.name);
75
+ try {
76
+ // Validate each path before processing
77
+ await validatePath(fullPath, allowedDirectories);
78
+ // Check if path matches any exclude pattern
79
+ const relativePath = path.relative(validRootPath, fullPath);
80
+ const shouldExclude = excludePatterns.some((excludePattern) => {
81
+ const globPattern = excludePattern.includes("*") ? excludePattern : `**/${excludePattern}/**`;
82
+ return minimatch(relativePath, globPattern, { dot: true });
83
+ });
84
+ if (shouldExclude) {
85
+ continue;
86
+ }
87
+ if (entry.isDirectory()) {
88
+ // Recursively search subdirectories
89
+ await searchDirectory(fullPath);
90
+ }
91
+ else if (entry.isFile()) {
92
+ // Check if file matches the file patterns (if any were provided)
93
+ const shouldInclude = filePatterns.length === 0 ||
94
+ filePatterns.some((filePattern) => {
95
+ return minimatch(entry.name, filePattern, { nocase: true });
96
+ });
97
+ if (shouldInclude) {
98
+ await scanFile(fullPath);
99
+ }
100
+ }
101
+ }
102
+ catch (error) {
103
+ // Skip invalid paths during search
104
+ continue;
105
+ }
106
+ }
107
+ }
108
+ catch (error) {
109
+ // Skip directories we can't read
110
+ return;
111
+ }
112
+ }
113
+ const rootStat = await fs.stat(validRootPath);
114
+ if (rootStat.isFile()) {
115
+ // Single-file search: apply the same file-pattern filter to the name
116
+ const shouldInclude = filePatterns.length === 0 ||
117
+ filePatterns.some((filePattern) => minimatch(path.basename(validRootPath), filePattern, { nocase: true }));
118
+ if (shouldInclude) {
119
+ await scanFile(validRootPath);
120
+ }
121
+ }
122
+ else {
123
+ await searchDirectory(validRootPath);
124
+ }
125
+ // Format the results
126
+ let output = "";
127
+ const shown = results.slice(offset - 1);
128
+ if (matchesFound === 0) {
129
+ output = `No matches found for regex: ${pattern}\n`;
130
+ output += `Searched ${filesSearched} files\n`;
131
+ }
132
+ else if (shown.length === 0) {
133
+ output = `Found ${matchesFound} matches in ${results.length} locations (offset ${offset} is beyond the collected range)\n`;
134
+ }
135
+ else {
136
+ const end = offset + shown.length - 1;
137
+ output = `Found ${matchesFound} matches in ${results.length} locations (showing ${offset}-${end})`;
138
+ if (searchAborted) {
139
+ output += ` (search stopped at ${effectiveCap} collected matches)`;
140
+ }
141
+ output += "\n\n";
142
+ // Group by file for more readable output
143
+ const fileGroups = new Map();
144
+ for (const result of shown) {
145
+ if (!fileGroups.has(result.file)) {
146
+ fileGroups.set(result.file, []);
147
+ }
148
+ fileGroups.get(result.file)?.push(result);
149
+ }
150
+ for (const [file, fileResults] of fileGroups.entries()) {
151
+ output += `File: ${file}\n`;
152
+ for (const result of fileResults) {
153
+ output += ` Line ${result.line}: ${result.content}\n`;
154
+ }
155
+ output += "\n";
156
+ }
157
+ }
158
+ return output;
159
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ export const SearchRegexArgsSchema = z.object({
3
+ path: z.string().describe("Root directory to search in, or a single file to search"),
4
+ pattern: z.string().describe("Regular expression pattern to search for in file contents"),
5
+ filePatterns: z.array(z.string()).optional().default([]).describe("File patterns to include (e.g. '*.js', '*.ts')"),
6
+ excludePatterns: z.array(z.string()).optional().default([]).describe("Patterns to exclude from search"),
7
+ maxResults: z.number().optional().default(100).describe("Maximum number of results to return"),
8
+ offset: z.number().int().min(1).optional().default(1).describe("1-indexed match number to start from. Default: 1."),
9
+ caseSensitive: z.boolean().optional().default(false).describe("Whether the search should be case-sensitive"),
10
+ });