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
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2025 Anthropic, PBC (original @modelcontextprotocol/server-filesystem)
4
+ Copyright (c) 2025 fabienvauchelles (mcp-filesystem-extended base fork)
5
+ Copyright (c) 2026 Gerard DeLuca (robustness patches, shell tools, Windows hardening)
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # mcp-fs-shell-windows — filesystem + standalone Windows shell MCP server
2
+
3
+ A Node.js MCP server exposing **filesystem operations** and a **standalone shell** as tools. Heavily extended fork of [fabienvauchelles/mcp-filesystem-extended](https://github.com/fabienvauchelles/mcp-filesystem-extended), which itself extends the official [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem).
4
+
5
+ Developed and tested primarily on **Windows**: drive-letter roots (`C:\`, `D:\`) and UNC network-share roots (`\\server\\share`) are first-class, and the shell tools run commands through `cmd.exe` byte-for-byte — including background job management and detached interactive console windows.
6
+
7
+ ## Provenance
8
+
9
+ | Layer | Source |
10
+ |---|---|
11
+ | Official base | `@modelcontextprotocol/server-filesystem` (Anthropic, PBC; MIT) |
12
+ | Base fork | [fabienvauchelles/mcp-filesystem-extended](https://github.com/fabienvauchelles/mcp-filesystem-extended) @ `b6c317a` (MIT) |
13
+ | This fork | v0.2.1 tool parity + `transfer_files` rename, robustness patches, 8 shell tools (v0.2.2), verbatim argv (v0.2.4) — see [Differences](#differences-from-the-base-fork) |
14
+
15
+ ## Features
16
+
17
+ - **23 filesystem tools** — batched read/write/append/delete, move/copy, line-number patching, search (name / glob / regex / fuzzy), tree, counts, checksums, diffs, exact-match text editing. Confined to the allowed directories passed on the command line.
18
+ - **8 shell tools** — synchronous, background, and interactive command execution implemented *inside the MCP server process*: no client-side permission toggles, survives client restarts, works with any MCP client.
19
+ - **Windows-first** — drive roots, UNC roots, `cmd.exe /d /c` with verbatim argv, whole-process-tree kills (`taskkill /T /F`), detached interactive console windows.
20
+ - **NAS-safe startup** — an allowed directory that is missing or offline (e.g., a powered-down network share) logs a warning and the server continues instead of exiting.
21
+
22
+ ## Requirements
23
+
24
+ - Node.js >= 20 (developed and tested on Node 22).
25
+ - Windows for the shell tools (they spawn `cmd.exe`, `taskkill`, and `powershell`). The filesystem tools work on any platform Node runs on.
26
+ - `shell_python` requires Python on the system PATH.
27
+
28
+ ## Build
29
+
30
+ ```bat
31
+ npm ci
32
+ npm run build
33
+ ```
34
+
35
+ Run the server:
36
+
37
+ ```bat
38
+ node dist\index.js <allowed-dir> [more-dirs...]
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ Example MCP client config (Claude Desktop / LM Studio `mcp.json` style):
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "filesystem": {
49
+ "command": "node",
50
+ "args": [
51
+ "C:/path/to/repo/dist/index.js",
52
+ "C:\\",
53
+ "D:\\",
54
+ "\\\\server\\share"
55
+ ]
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ The filesystem tools can only touch the passed directories (and anything under them). A root that cannot be accessed at startup logs a warning and is skipped; the server keeps running. Note that the shell tools are **not** root-restricted — see [Security](#security).
62
+
63
+ ### Filesystem tools (23)
64
+
65
+ | Tool | Purpose |
66
+ |---|---|
67
+ | `read_files` | Read one or more files, line-numbered; `offset`/`limit` paging |
68
+ | `write_new_files` | Create files (per-file `overwrite`, optional `base64` encoding) |
69
+ | `append_files` | Append to files (creates them if missing) |
70
+ | `delete_files` | Delete files/directories (`recursive`) |
71
+ | `copy_file` | Copy files/directories (`recursive`, `overwrite`) |
72
+ | `transfer_files` | Move/rename items (`overwrite`, creates parent dirs) |
73
+ | `patch_files` | Patch by line ranges (`replace`/`insertBefore`/`delete`, `dryRun`, git-diff output) |
74
+ | `create_directories` | Create directory paths (nested chains) |
75
+ | `list_directory` | Single-level listing (sizes, pagination, type filter, ignore globs) |
76
+ | `directory_tree` | Recursive tree view (JSON) with exclusion patterns |
77
+ | `search_files` | Recursive case-insensitive name-substring search |
78
+ | `search_regex` | Content grep over a single file or directory tree (paged) |
79
+ | `search_glob` | Glob-based file search (e.g. `**/*.ts`, paged) |
80
+ | `fuzzy_find_files` | Typo-tolerant name/path search (Levenshtein) |
81
+ | `delete_files_by_pattern` | Regex delete within a single directory |
82
+ | `count_lines` | Line counts with filters |
83
+ | `file_diff` | Unified diff of two files |
84
+ | `content_diff` | Unified diff of two strings |
85
+ | `checksum_files` | md5/sha1/sha256/sha512 for files |
86
+ | `checksum_files_verif` | Verify files against expected hashes |
87
+ | `get_file_info` | Size, timestamps, type, permissions |
88
+ | `edit_files` | Exact-match text replacement, EOL-tolerant |
89
+ | `list_allowed_directories` | List the allowed root directories |
90
+
91
+ ### Shell tools (8, Windows-only)
92
+
93
+ User commands are spawned as `cmd.exe /d /c <command>` with **verbatim argv** — quotes, redirects, and metacharacters arrive byte-for-byte (Node `windowsVerbatimArguments`), and `/d` skips AutoRun registry entries.
94
+
95
+ | Tool | Purpose |
96
+ |---|---|
97
+ | `shell_run` | Synchronous command (default 5 s, max 28 s); non-zero exit or timeout returns the captured stdout/stderr instead of hiding it |
98
+ | `shell_test` | Test wrapper (runs with `CI=true`); never errors on a failing test |
99
+ | `shell_start` | Background job — returns immediately with a job ID; auto-kills at `timeout_hours` (max 10); 256 KB in-memory tail + log file |
100
+ | `shell_check` | Job status, stdout/stderr tails, log file path |
101
+ | `shell_cancel` | Kill the whole process tree (`taskkill /T /F`, with PID-liveness fallback) |
102
+ | `shell_terminal` | Open a visible, separate, interactive `cmd /k` console window running the command; the window persists after the command finishes; close it with `shell_cancel` |
103
+ | `shell_python` | Run a Python snippet (temp `.py` + system Python) |
104
+ | `shell_cwd` | Get/set the default working directory for the other shell tools (every tool also accepts a per-call `cwd`) |
105
+
106
+ Practical notes:
107
+
108
+ - Background jobs live in an **in-memory registry** inside the server process: the table is lost on server restart, but job log files persist (under `%TEMP%\mcp-filesystem-extended\shell-jobs`).
109
+ - `timeout /t` can fail when stdin is not an interactive console — use `ping -n N 127.0.0.1 >nul` for sleeps.
110
+ - Synchronous calls are bounded (max 28 s) so they cannot wedge the MCP request channel; anything longer should go through `shell_start`.
111
+
112
+ ## Docker
113
+
114
+ ```bat
115
+ docker build -t mcp/filesystem-extended .
116
+ docker run -i --rm -v /path/to/dir:/data mcp/filesystem-extended /data
117
+ ```
118
+
119
+ The filesystem tools work in the image; the shell tools require a Windows host with `cmd.exe`.
120
+
121
+ ## Differences from the base fork
122
+
123
+ **v0.2.1 — official-server parity:** head/tail reads (`offset`/`limit` in `read_files`), sizes in listings, `overwrite` in `write_new_files`, exact-match text editing (`edit_files`).
124
+
125
+ **`transfer_files` (renamed from `move_files`):** the canonical move/rename tool. Renamed because at least one MCP client (observed in LM Studio) silently drops tools named `move_files`/`mv_files` from the exposed tool list; `transfer_files` is registered last in the tools array.
126
+
127
+ **Robustness patches:**
128
+
129
+ - Missing/offline allowed roots warn at startup instead of exiting (NAS-down safe).
130
+ - `validatePath` walks to the nearest existing ancestor, so writes under brand-new nested directories work.
131
+ - `ensureDirectoryExists` at all `mkdir` sites (Node EPERM quirk on drive-root writes).
132
+ - Move-handler destination checks use errno `ENOENT`, so moves to fresh destinations succeed.
133
+ - `zod` pinned to `^3.23.8` (zod v4 breaks the SDK's schema conversion).
134
+
135
+ **v0.2.2 — shell tools** as listed above, with whole-process-tree kills and the detached interactive console window.
136
+
137
+ **v0.2.4 — verbatim argv:** `windowsVerbatimArguments` + `/d` on all user-command spawns (run/test/start and the terminal launcher).
138
+
139
+ ## Security
140
+
141
+ - The filesystem tools are confined to the allowed directories passed at startup (and their subtrees).
142
+ - The shell tools are **not** confined: anything the server process's user can execute will run, including against reachable network shares. Grant this server the same trust you would grant an unsandboxed shell.
143
+ - The server communicates over stdio and opens no network endpoints of its own.
144
+
145
+ ## License
146
+
147
+ MIT — see [LICENSE](./LICENSE). This project derives from MIT-licensed code (Anthropic, PBC; fabienvauchelles); all copyright notices are in the LICENSE file.
@@ -0,0 +1,32 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { validatePath, ensureDirectoryExists } from "../helpers/path.js";
4
+ export async function handleAppendFiles(files, allowedDirectories) {
5
+ const results = [];
6
+ const errors = [];
7
+ await Promise.all(files.map(async (file) => {
8
+ try {
9
+ const validPath = await validatePath(file.path, allowedDirectories);
10
+ // Ensure parent directory exists
11
+ const directory = path.dirname(validPath);
12
+ await ensureDirectoryExists(directory);
13
+ // Append content to file (create if doesn't exist)
14
+ await fs.appendFile(validPath, file.content, "utf-8");
15
+ results.push(`Successfully appended to ${file.path}`);
16
+ }
17
+ catch (error) {
18
+ const errorMessage = error instanceof Error ? error.message : String(error);
19
+ errors.push(`Failed to append to file ${file.path}: ${errorMessage}`);
20
+ }
21
+ }));
22
+ // Format the results
23
+ const successCount = results.length;
24
+ const errorCount = errors.length;
25
+ let output = `Processed ${successCount + errorCount} files:\n`;
26
+ output += `- ${successCount} files appended successfully\n`;
27
+ if (errorCount > 0) {
28
+ output += `- ${errorCount} files failed\n\n`;
29
+ output += "Errors:\n" + errors.join("\n");
30
+ }
31
+ return output;
32
+ }
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ export const AppendFilesArgsSchema = z.object({
3
+ files: z.array(z.object({
4
+ path: z.string().describe("Path to the file to append to"),
5
+ content: z.string().describe("Content to append to the file"),
6
+ })).describe("Array of files to append to"),
7
+ });
@@ -0,0 +1,33 @@
1
+ import { validatePath } from "../helpers/path.js";
2
+ import { calculateFileHash } from "../helpers/checksum.js";
3
+ export async function handleChecksumFiles(filePaths, algorithm, allowedDirectories) {
4
+ const results = [];
5
+ const errors = [];
6
+ await Promise.all(filePaths.map(async (filePath) => {
7
+ try {
8
+ // Validate the path
9
+ const validPath = await validatePath(filePath, allowedDirectories);
10
+ // Calculate the hash
11
+ const hash = await calculateFileHash(validPath, algorithm);
12
+ results.push({ path: filePath, hash });
13
+ }
14
+ catch (error) {
15
+ const errorMessage = error instanceof Error ? error.message : String(error);
16
+ errors.push({ path: filePath, error: errorMessage });
17
+ }
18
+ }));
19
+ // Format the results
20
+ let output = `Checksums (${algorithm}):\n\n`;
21
+ if (results.length > 0) {
22
+ for (const result of results) {
23
+ output += `${result.hash} ${result.path}\n`;
24
+ }
25
+ }
26
+ if (errors.length > 0) {
27
+ output += "\nErrors:\n";
28
+ for (const error of errors) {
29
+ output += `${error.path}: ${error.error}\n`;
30
+ }
31
+ }
32
+ return output;
33
+ }
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ export const ChecksumFilesArgsSchema = z.object({
3
+ paths: z.array(z.string()).describe("Paths to the files to generate checksums for"),
4
+ algorithm: z.enum(["md5", "sha1", "sha256", "sha512"]).default("sha256").describe("Hash algorithm to use"),
5
+ });
@@ -0,0 +1,66 @@
1
+ import { validatePath } from "../helpers/path.js";
2
+ import { calculateFileHash } from "../helpers/checksum.js";
3
+ import { validateHash } from "./helpers.js";
4
+ export async function handleChecksumFilesVerif(files, algorithm, allowedDirectories) {
5
+ const results = [];
6
+ const errors = [];
7
+ await Promise.all(files.map(async (file) => {
8
+ try {
9
+ // Validate the path
10
+ const validPath = await validatePath(file.path, allowedDirectories);
11
+ // Calculate the hash
12
+ const actualHash = await calculateFileHash(validPath, algorithm);
13
+ // Compare with expected hash
14
+ const isValid = validateHash(actualHash, file.expectedHash);
15
+ results.push({
16
+ path: file.path,
17
+ expectedHash: file.expectedHash,
18
+ actualHash,
19
+ valid: isValid
20
+ });
21
+ }
22
+ catch (error) {
23
+ const errorMessage = error instanceof Error ? error.message : String(error);
24
+ errors.push({
25
+ path: file.path,
26
+ expectedHash: file.expectedHash,
27
+ error: errorMessage
28
+ });
29
+ }
30
+ }));
31
+ // Count successes and failures
32
+ const validCount = results.filter(r => r.valid).length;
33
+ const invalidCount = results.filter(r => !r.valid).length;
34
+ const errorCount = errors.length;
35
+ // Format the results
36
+ let output = `Checksum Verification Results (${algorithm}):\n`;
37
+ output += `✅ Valid: ${validCount}\n`;
38
+ output += `❌ Invalid: ${invalidCount}\n`;
39
+ output += `⚠️ Errors: ${errorCount}\n\n`;
40
+ // Valid files
41
+ if (validCount > 0) {
42
+ output += "Valid Files:\n";
43
+ for (const result of results.filter(r => r.valid)) {
44
+ output += `✓ ${result.path}\n`;
45
+ }
46
+ output += "\n";
47
+ }
48
+ // Invalid files
49
+ if (invalidCount > 0) {
50
+ output += "Invalid Files:\n";
51
+ for (const result of results.filter(r => !r.valid)) {
52
+ output += `✗ ${result.path}\n`;
53
+ output += ` Expected: ${result.expectedHash}\n`;
54
+ output += ` Actual: ${result.actualHash}\n`;
55
+ }
56
+ output += "\n";
57
+ }
58
+ // Errors
59
+ if (errorCount > 0) {
60
+ output += "Errors:\n";
61
+ for (const error of errors) {
62
+ output += `! ${error.path}: ${error.error}\n`;
63
+ }
64
+ }
65
+ return output;
66
+ }
@@ -0,0 +1,6 @@
1
+ export function validateHash(hash, expectedHash) {
2
+ // Normalize hashes for comparison (convert to lowercase, trim)
3
+ const normalizedHash = hash.toLowerCase().trim();
4
+ const normalizedExpected = expectedHash.toLowerCase().trim();
5
+ return normalizedHash === normalizedExpected;
6
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ export const ChecksumFilesVerifArgsSchema = z.object({
3
+ files: z.array(z.object({
4
+ path: z.string().describe("Path to the file to verify"),
5
+ expectedHash: z.string().describe("Expected hash value to compare against"),
6
+ })).describe("Array of files to verify with their expected hashes"),
7
+ algorithm: z.enum(["md5", "sha1", "sha256", "sha512"]).default("sha256").describe("Hash algorithm to use"),
8
+ });
@@ -0,0 +1,11 @@
1
+ import { createUnifiedDiff } from "../helpers/diff.js";
2
+ export async function handleContentDiff(content1, content2, label1, label2) {
3
+ // Generate a diff between the two content strings
4
+ const diff = createUnifiedDiff(content1, content2, label1, label2);
5
+ // Format the output with code fence
6
+ let numBackticks = 3;
7
+ while (diff.includes('`'.repeat(numBackticks))) {
8
+ numBackticks++;
9
+ }
10
+ return `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}`;
11
+ }
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ export const ContentDiffArgsSchema = z.object({
3
+ content1: z.string().describe("First content string to compare"),
4
+ content2: z.string().describe("Second content string to compare"),
5
+ label1: z.string().optional().default("original").describe("Label for the first content"),
6
+ label2: z.string().optional().default("modified").describe("Label for the second content"),
7
+ });
@@ -0,0 +1,43 @@
1
+ import fs from "fs/promises";
2
+ import { copyDir } from "./helpers.js";
3
+ import { validatePath, ensureDirectoryExists } from "../helpers/path.js";
4
+ export async function handleCopyFile(sourcePath, destinationPath, recursive, overwrite, allowedDirectories) {
5
+ const validSourcePath = await validatePath(sourcePath, allowedDirectories);
6
+ const validDestPath = await validatePath(destinationPath, allowedDirectories);
7
+ try {
8
+ // Check if source exists
9
+ const sourceStats = await fs.stat(validSourcePath);
10
+ // Check if destination already exists
11
+ try {
12
+ await fs.access(validDestPath);
13
+ if (!overwrite) {
14
+ throw new Error(`Destination already exists: ${destinationPath}. Use overwrite=true to replace it.`);
15
+ }
16
+ }
17
+ catch (error) {
18
+ // This is expected for new destinations
19
+ if (!(error instanceof Error) || !error.message.includes('ENOENT')) {
20
+ throw error; // If error is something else, rethrow it
21
+ }
22
+ }
23
+ if (sourceStats.isDirectory()) {
24
+ if (!recursive) {
25
+ throw new Error(`Source is a directory. Use recursive=true to copy directories.`);
26
+ }
27
+ // Create the destination directory if it doesn't exist
28
+ await ensureDirectoryExists(validDestPath);
29
+ // Copy directory recursively
30
+ await copyDir(validSourcePath, validDestPath, allowedDirectories);
31
+ return `Successfully copied directory ${sourcePath} to ${destinationPath}`;
32
+ }
33
+ else {
34
+ // It's a file, copy it directly
35
+ await fs.copyFile(validSourcePath, validDestPath);
36
+ return `Successfully copied file ${sourcePath} to ${destinationPath}`;
37
+ }
38
+ }
39
+ catch (error) {
40
+ const errorMessage = error instanceof Error ? error.message : String(error);
41
+ throw new Error(`Error copying ${sourcePath} to ${destinationPath}: ${errorMessage}`);
42
+ }
43
+ }
@@ -0,0 +1,24 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { validatePath, ensureDirectoryExists } from "../helpers/path.js";
4
+ export async function copyDir(src, dest, allowedDirectories) {
5
+ // Create destination if it doesn't exist
6
+ await ensureDirectoryExists(dest);
7
+ // Get all files in the source directory
8
+ const entries = await fs.readdir(src, { withFileTypes: true });
9
+ for (const entry of entries) {
10
+ const srcPath = path.join(src, entry.name);
11
+ const destPath = path.join(dest, entry.name);
12
+ // Make sure paths are validated
13
+ await validatePath(srcPath, allowedDirectories);
14
+ await validatePath(destPath, allowedDirectories);
15
+ if (entry.isDirectory()) {
16
+ // Recursively copy subdirectories
17
+ await copyDir(srcPath, destPath, allowedDirectories);
18
+ }
19
+ else {
20
+ // Copy files
21
+ await fs.copyFile(srcPath, destPath);
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ export const CopyFileArgsSchema = z.object({
3
+ source: z.string(),
4
+ destination: z.string(),
5
+ recursive: z.boolean().default(false).describe('Copy directories recursively'),
6
+ overwrite: z.boolean().default(false).describe('Overwrite destination if it exists'),
7
+ });
@@ -0,0 +1,144 @@
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 handleCountLines(filePath, recursive, pattern, filePattern, excludePatterns, ignoreEmptyLines, allowedDirectories) {
6
+ // Validate the path
7
+ const validPath = await validatePath(filePath, allowedDirectories);
8
+ // Create regex for line matching if pattern is provided
9
+ let regex;
10
+ if (pattern) {
11
+ try {
12
+ regex = new RegExp(pattern);
13
+ }
14
+ catch (error) {
15
+ throw new Error(`Invalid regular expression: ${pattern}`);
16
+ }
17
+ }
18
+ // Get file stats to determine if it's a file or directory
19
+ const stats = await fs.stat(validPath);
20
+ let files = [];
21
+ if (stats.isFile()) {
22
+ // Count lines in a single file
23
+ const count = await countLinesInFile(validPath, regex, ignoreEmptyLines);
24
+ files.push(count);
25
+ }
26
+ else if (stats.isDirectory() && recursive) {
27
+ // Recursively count lines in a directory
28
+ files = await countLinesInDirectory(validPath, filePattern, excludePatterns, regex, ignoreEmptyLines, allowedDirectories);
29
+ }
30
+ else if (stats.isDirectory() && !recursive) {
31
+ throw new Error(`Path is a directory. Use recursive=true to count lines in all files.`);
32
+ }
33
+ else {
34
+ throw new Error(`Path is neither a file nor a directory.`);
35
+ }
36
+ // Format results
37
+ let output = '';
38
+ if (files.length === 0) {
39
+ output = `No files found matching the criteria.`;
40
+ }
41
+ else {
42
+ let totalLines = 0;
43
+ let totalMatchingLines = 0;
44
+ output = "Line counts:\n\n";
45
+ // Sort by line count (descending)
46
+ files.sort((a, b) => b.count - a.count);
47
+ for (const file of files) {
48
+ totalLines += file.count;
49
+ if (file.matchingCount !== undefined) {
50
+ totalMatchingLines += file.matchingCount;
51
+ }
52
+ if (pattern) {
53
+ output += `${file.file}: ${file.count} lines total, ${file.matchingCount} matching lines\n`;
54
+ }
55
+ else {
56
+ output += `${file.file}: ${file.count} lines\n`;
57
+ }
58
+ }
59
+ output += "\n";
60
+ output += `Total: ${files.length} files, ${totalLines} lines`;
61
+ if (pattern) {
62
+ output += `, ${totalMatchingLines} matching lines`;
63
+ }
64
+ }
65
+ return output;
66
+ }
67
+ async function countLinesInFile(filePath, regex, ignoreEmptyLines) {
68
+ // Read file content
69
+ const content = await fs.readFile(filePath, 'utf-8');
70
+ // Split into lines
71
+ const lines = content.split('\n');
72
+ let count = lines.length;
73
+ let matchingCount;
74
+ // Handle empty lines if needed
75
+ if (ignoreEmptyLines) {
76
+ count = lines.filter(line => line.trim() !== '').length;
77
+ }
78
+ // Count matching lines if regex is provided
79
+ if (regex) {
80
+ matchingCount = lines.filter(line => {
81
+ if (ignoreEmptyLines && line.trim() === '') {
82
+ return false;
83
+ }
84
+ return regex.test(line);
85
+ }).length;
86
+ }
87
+ return {
88
+ file: filePath,
89
+ count,
90
+ matchingCount
91
+ };
92
+ }
93
+ async function countLinesInDirectory(dirPath, filePattern, excludePatterns, regex, ignoreEmptyLines, allowedDirectories) {
94
+ const results = [];
95
+ async function processDirectory(currentPath) {
96
+ try {
97
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
98
+ for (const entry of entries) {
99
+ const fullPath = path.join(currentPath, entry.name);
100
+ try {
101
+ // Validate each path before processing
102
+ await validatePath(fullPath, allowedDirectories);
103
+ // Get relative path for glob matching
104
+ const relativePath = path.relative(dirPath, fullPath);
105
+ // Check if path should be excluded
106
+ const shouldExclude = excludePatterns.some(excludePattern => {
107
+ return minimatch(relativePath, excludePattern, { dot: true });
108
+ });
109
+ if (shouldExclude) {
110
+ continue;
111
+ }
112
+ if (entry.isDirectory()) {
113
+ // Recursively process subdirectories
114
+ await processDirectory(fullPath);
115
+ }
116
+ else if (entry.isFile()) {
117
+ // Check if file matches the file pattern
118
+ if (minimatch(entry.name, filePattern, { dot: true }) ||
119
+ minimatch(relativePath, filePattern, { dot: true })) {
120
+ try {
121
+ const count = await countLinesInFile(fullPath, regex, ignoreEmptyLines);
122
+ results.push(count);
123
+ }
124
+ catch (error) {
125
+ // Skip files that can't be read as text
126
+ continue;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ catch (error) {
132
+ // Skip invalid paths
133
+ continue;
134
+ }
135
+ }
136
+ }
137
+ catch (error) {
138
+ // Skip directories we can't read
139
+ return;
140
+ }
141
+ }
142
+ await processDirectory(dirPath);
143
+ return results;
144
+ }
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ export const CountLinesArgsSchema = z.object({
3
+ path: z.string().describe("Path to the file or directory to count lines"),
4
+ recursive: z.boolean().optional().default(false).describe("Whether to recursively count lines in directories"),
5
+ pattern: z.string().optional().describe("Regex pattern to match lines (only count matching lines)"),
6
+ filePattern: z.string().optional().default("**").describe("Glob pattern to match files when counting recursively"),
7
+ excludePatterns: z.array(z.string()).optional().default([]).describe("Glob patterns to exclude"),
8
+ ignoreEmptyLines: z.boolean().optional().default(false).describe("Whether to ignore empty lines"),
9
+ });
@@ -0,0 +1,29 @@
1
+ import fs from "fs/promises";
2
+ import { validatePath } from "../helpers/path.js";
3
+ export async function handleCreateDirectories(paths, allowedDirectories) {
4
+ const results = [];
5
+ const errors = [];
6
+ await Promise.all(paths.map(async (dirPath) => {
7
+ try {
8
+ // Validate path is within allowed directories
9
+ const validPath = await validatePath(dirPath, allowedDirectories);
10
+ // Create directory (and parent directories if needed)
11
+ await fs.mkdir(validPath, { recursive: true });
12
+ results.push(`Successfully created directory: ${dirPath}`);
13
+ }
14
+ catch (error) {
15
+ const errorMessage = error instanceof Error ? error.message : String(error);
16
+ errors.push(`Failed to create directory ${dirPath}: ${errorMessage}`);
17
+ }
18
+ }));
19
+ // Format the results
20
+ const successCount = results.length;
21
+ const errorCount = errors.length;
22
+ let output = `Processed ${successCount + errorCount} directories:\n`;
23
+ output += `- ${successCount} directories created successfully\n`;
24
+ if (errorCount > 0) {
25
+ output += `- ${errorCount} directories failed\n\n`;
26
+ output += "Errors:\n" + errors.join("\n");
27
+ }
28
+ return output;
29
+ }
@@ -0,0 +1,4 @@
1
+ import { z } from "zod";
2
+ export const CreateDirectoriesArgsSchema = z.object({
3
+ paths: z.array(z.string()).describe("Paths of directories to create"),
4
+ });