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.
- package/LICENSE +23 -0
- package/README.md +147 -0
- package/dist/append_files/handler.js +32 -0
- package/dist/append_files/schema.js +7 -0
- package/dist/checksum_files/handler.js +33 -0
- package/dist/checksum_files/schema.js +5 -0
- package/dist/checksum_files_verif/handler.js +66 -0
- package/dist/checksum_files_verif/helpers.js +6 -0
- package/dist/checksum_files_verif/schema.js +8 -0
- package/dist/content_diff/handler.js +11 -0
- package/dist/content_diff/schema.js +7 -0
- package/dist/copy_files/handler.js +43 -0
- package/dist/copy_files/helpers.js +24 -0
- package/dist/copy_files/schema.js +7 -0
- package/dist/count_lines/handler.js +144 -0
- package/dist/count_lines/schema.js +9 -0
- package/dist/create_directories/handler.js +29 -0
- package/dist/create_directories/schema.js +4 -0
- package/dist/delete_files/handler.js +42 -0
- package/dist/delete_files/schema.js +5 -0
- package/dist/delete_files_by_pattern/handler.js +46 -0
- package/dist/delete_files_by_pattern/schema.js +6 -0
- package/dist/directory_tree/handler.js +7 -0
- package/dist/directory_tree/helpers.js +47 -0
- package/dist/directory_tree/schema.js +5 -0
- package/dist/edit_files/handler.js +62 -0
- package/dist/edit_files/schema.js +11 -0
- package/dist/file_diff/handler.js +28 -0
- package/dist/file_diff/schema.js +5 -0
- package/dist/file_info/handler.js +30 -0
- package/dist/file_info/schema.js +4 -0
- package/dist/fuzzy_find_files/handler.js +74 -0
- package/dist/fuzzy_find_files/schema.js +8 -0
- package/dist/helpers/checksum.js +10 -0
- package/dist/helpers/diff.js +10 -0
- package/dist/helpers/path.js +87 -0
- package/dist/index.js +36 -0
- package/dist/list_directory/handler.js +42 -0
- package/dist/list_directory/schema.js +9 -0
- package/dist/move_files/handler.js +68 -0
- package/dist/move_files/schema.js +8 -0
- package/dist/patch_files/handler.js +34 -0
- package/dist/patch_files/helpers.js +103 -0
- package/dist/patch_files/schema.js +18 -0
- package/dist/read_files/handler.js +36 -0
- package/dist/read_files/schema.js +6 -0
- package/dist/search_files/handler.js +15 -0
- package/dist/search_files/helpers.js +71 -0
- package/dist/search_files/schema.js +8 -0
- package/dist/search_glob/handler.js +77 -0
- package/dist/search_glob/schema.js +8 -0
- package/dist/search_regex/handler.js +159 -0
- package/dist/search_regex/schema.js +10 -0
- package/dist/server.js +698 -0
- package/dist/shell/handler.js +687 -0
- package/dist/shell/schema.js +69 -0
- package/dist/write_new_files/handler.js +55 -0
- package/dist/write_new_files/schema.js +9 -0
- 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
|
+
});
|