@n0zer0d4y/vulcan-file-ops 1.2.13 → 1.3.0

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.
@@ -1,29 +1,72 @@
1
- import { createReadStream } from "fs";
1
+ import { createReadStream, promises as fs } from "fs";
2
2
  import path from "path";
3
3
  import { zodToJsonSchema } from "zod-to-json-schema";
4
4
  import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { ReadFileArgsSchema, AttachImageArgsSchema, ReadMultipleFilesArgsSchema, } from "../types/index.js";
6
- import { validatePath, readFileContent, tailFile, headFile, rangeFile, } from "../utils/lib.js";
6
+ import { validatePath, tailFile, headFile, rangeFile, } from "../utils/lib.js";
7
7
  import { isDocumentFile, parseDocument } from "../utils/document-parser.js";
8
+ import { MAX_IMAGE_ATTACH_BYTES, MAX_IMAGE_ATTACH_TOTAL_BYTES, MAX_TEXT_READ_BYTES, } from "../utils/limits.js";
8
9
  import { createPathArraySchema, sanitizeToolInputSchema, } from "../utils/tool-schema.js";
9
10
  const ToolInputSchema = ToolSchema.shape.inputSchema;
11
+ const formatMB = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
10
12
  // Reads a file as a stream of buffers, concatenates them, and then encodes
11
- // the result to a Base64 string. This is a memory-efficient way to handle
12
- // binary data from a stream before the final encoding.
13
- async function readFileAsBase64Stream(filePath) {
13
+ // the result to a Base64 string. At most `maxBytes` are read: a file that
14
+ // grew past the limit after it was checked is rejected rather than read.
15
+ async function readFileAsBase64Stream(filePath, maxBytes) {
14
16
  return new Promise((resolve, reject) => {
15
- const stream = createReadStream(filePath);
17
+ const stream = createReadStream(filePath, { start: 0, end: maxBytes });
16
18
  const chunks = [];
19
+ let total = 0;
17
20
  stream.on("data", (chunk) => {
21
+ total += chunk.length;
18
22
  chunks.push(chunk);
19
23
  });
20
24
  stream.on("end", () => {
25
+ if (total > maxBytes) {
26
+ reject(new Error(`Image ${path.basename(filePath)} exceeds the ${formatMB(maxBytes)} limit`));
27
+ return;
28
+ }
21
29
  const finalBuffer = Buffer.concat(chunks);
22
30
  resolve(finalBuffer.toString("base64"));
23
31
  });
24
32
  stream.on("error", (err) => reject(err));
25
33
  });
26
34
  }
35
+ /**
36
+ * Read a whole text file ("full" mode), refusing files larger than
37
+ * MAX_TEXT_READ_BYTES (VFO-15). The read itself is bounded too, so a file
38
+ * that grows after the size check (or a device/FIFO that reports size 0)
39
+ * cannot exhaust memory.
40
+ */
41
+ async function readFullTextFile(filePath) {
42
+ const tooLarge = (size) => new Error(`File is too large to read in full mode (${size}; limit ` +
43
+ `${formatMB(MAX_TEXT_READ_BYTES)}). Use mode "head", "tail" or "range" ` +
44
+ `to read part of it.`);
45
+ const handle = await fs.open(filePath, "r");
46
+ try {
47
+ const { size } = await handle.stat();
48
+ if (size > MAX_TEXT_READ_BYTES) {
49
+ throw tooLarge(formatMB(size));
50
+ }
51
+ const buffer = Buffer.alloc(64 * 1024);
52
+ const chunks = [];
53
+ let total = 0;
54
+ for (;;) {
55
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
56
+ if (bytesRead === 0)
57
+ break;
58
+ total += bytesRead;
59
+ if (total > MAX_TEXT_READ_BYTES) {
60
+ throw tooLarge(`more than ${formatMB(MAX_TEXT_READ_BYTES)}`);
61
+ }
62
+ chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
63
+ }
64
+ return Buffer.concat(chunks).toString("utf-8");
65
+ }
66
+ finally {
67
+ await handle.close();
68
+ }
69
+ }
27
70
  export function getReadTools() {
28
71
  return [
29
72
  {
@@ -43,7 +86,8 @@ export function getReadTools() {
43
86
  "to the AI model as if uploaded directly by the user, enabling the AI to see " +
44
87
  "and describe visual content, read text in images, analyze diagrams, etc. " +
45
88
  "Supports attaching a single image or multiple images at once. " +
46
- "Supports PNG, JPEG, GIF, WebP, BMP, and SVG formats. " +
89
+ "Supports PNG, JPEG, GIF and WebP (the formats vision models accept). " +
90
+ "SVG files are returned as their XML markup text; BMP is not supported (convert to PNG). " +
47
91
  "Note: This requires the MCP client to support vision capabilities. " +
48
92
  "Only works within allowed directories.",
49
93
  inputSchema: {
@@ -122,7 +166,7 @@ export async function handleReadTool(name, args) {
122
166
  }
123
167
  case "full":
124
168
  default: {
125
- const content = await readFileContent(validPath);
169
+ const content = await readFullTextFile(validPath);
126
170
  return {
127
171
  content: [{ type: "text", text: content }],
128
172
  };
@@ -138,29 +182,58 @@ export async function handleReadTool(name, args) {
138
182
  const paths = Array.isArray(parsed.data.path)
139
183
  ? parsed.data.path
140
184
  : [parsed.data.path];
141
- // Supported image formats only (no audio)
185
+ // Raster formats accepted by vision models. SVG is vector markup (XML),
186
+ // which vision APIs reject as an image, so it is returned as text.
142
187
  const mimeTypes = {
143
188
  ".png": "image/png",
144
189
  ".jpg": "image/jpeg",
145
190
  ".jpeg": "image/jpeg",
146
191
  ".gif": "image/gif",
147
192
  ".webp": "image/webp",
148
- ".bmp": "image/bmp",
149
193
  ".svg": "image/svg+xml",
150
194
  };
151
- // Process all images
152
- const imageContents = await Promise.all(paths.map(async (imagePath) => {
195
+ // Validate all images and check sizes before reading any of them
196
+ const images = await Promise.all(paths.map(async (imagePath) => {
153
197
  const validPath = await validatePath(imagePath);
154
198
  const extension = path.extname(validPath).toLowerCase();
155
199
  const mimeType = mimeTypes[extension];
200
+ if (extension === ".bmp") {
201
+ throw new Error(`BMP images are not accepted by vision models: ${imagePath}. ` +
202
+ `Convert it to PNG or JPEG first.`);
203
+ }
156
204
  if (!mimeType) {
157
205
  throw new Error(`Unsupported image format: ${extension}. ` +
158
- `Supported formats: PNG, JPEG, GIF, WebP, BMP, SVG`);
206
+ `Supported formats: PNG, JPEG, GIF, WebP (SVG is returned as markup text)`);
207
+ }
208
+ const { size } = await fs.stat(validPath);
209
+ return { imagePath, validPath, mimeType, size };
210
+ }));
211
+ // Size caps (VFO-15): per image and per call
212
+ const oversized = images.filter((i) => i.size > MAX_IMAGE_ATTACH_BYTES);
213
+ if (oversized.length > 0) {
214
+ throw new Error(`Image(s) exceed the ${formatMB(MAX_IMAGE_ATTACH_BYTES)} per-image limit: ` +
215
+ oversized
216
+ .map((i) => `${i.imagePath} (${formatMB(i.size)})`)
217
+ .join(", "));
218
+ }
219
+ const totalSize = images.reduce((sum, i) => sum + i.size, 0);
220
+ if (totalSize > MAX_IMAGE_ATTACH_TOTAL_BYTES) {
221
+ throw new Error(`Images total ${formatMB(totalSize)}, which exceeds the ` +
222
+ `${formatMB(MAX_IMAGE_ATTACH_TOTAL_BYTES)} limit per attach_image call. ` +
223
+ `Attach fewer images per call.`);
224
+ }
225
+ const imageContents = await Promise.all(images.map(async ({ imagePath, validPath, mimeType }) => {
226
+ if (mimeType === "image/svg+xml") {
227
+ const markup = await fs.readFile(validPath, "utf-8");
228
+ return {
229
+ type: "text",
230
+ text: `SVG image ${imagePath} (vector markup; vision models accept only PNG, JPEG, GIF and WebP, so the SVG source is provided as text):\n` +
231
+ markup,
232
+ };
159
233
  }
160
- const data = await readFileAsBase64Stream(validPath);
161
234
  return {
162
235
  type: "image",
163
- data: data,
236
+ data: await readFileAsBase64Stream(validPath, MAX_IMAGE_ATTACH_BYTES),
164
237
  mimeType: mimeType,
165
238
  };
166
239
  }));
@@ -219,7 +292,7 @@ export async function handleReadTool(name, args) {
219
292
  break;
220
293
  case "full":
221
294
  default:
222
- content = await readFileContent(validPath);
295
+ content = await readFullTextFile(validPath);
223
296
  break;
224
297
  }
225
298
  return `${fileRequest.path}:\n${content}\n`;
@@ -1,21 +1,28 @@
1
1
  import { zodToJsonSchema } from "zod-to-json-schema";
2
2
  import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
3
3
  import { ShellCommandArgsSchema, } from "../types/index.js";
4
- import { validateCommand, extractRootCommands, isDangerousCommand, getShellConfig, } from "../utils/command-validation.js";
4
+ import { validateCommand, getDangerousRoots, getShellConfig, } from "../utils/command-validation.js";
5
5
  import { executeShellCommand, } from "../utils/shell-execution.js";
6
6
  import { validatePath, getAllowedDirectories } from "../utils/lib.js";
7
- import { extractPathsFromCommand } from "../utils/command-path-extraction.js";
8
- import { isPathWithinAllowedDirectories } from "../utils/path-validation.js";
7
+ import { findDisallowedCommandPaths } from "../utils/command-path-extraction.js";
8
+ import { parseShellCommand, getRootCommands } from "../utils/shell-parser.js";
9
9
  import { sanitizeToolInputSchema } from "../utils/tool-schema.js";
10
10
  const ToolInputSchema = ToolSchema.shape.inputSchema;
11
11
  // Global state for approved commands
12
12
  let approvedCommands = new Set();
13
13
  let alwaysApprovedCommands = new Set();
14
+ // Root commands the operator allows to match dangerous patterns
15
+ let dangerousCommandAllowlist = new Set();
14
16
  /**
15
- * Initialize the shell tool with approved commands
17
+ * Initialize the shell tool with approved commands and (optionally) the root
18
+ * commands that may run even when they match a dangerous pattern.
16
19
  */
17
- export function initializeShellTool(commands) {
20
+ export function initializeShellTool(commands, dangerousCommands = []) {
18
21
  approvedCommands = new Set(commands);
22
+ dangerousCommandAllowlist = new Set(dangerousCommands);
23
+ }
24
+ export function getDangerousCommandAllowlist() {
25
+ return Array.from(dangerousCommandAllowlist);
19
26
  }
20
27
  /**
21
28
  * Get the list of approved commands
@@ -57,8 +64,13 @@ export function getShellTools() {
57
64
  `\n\n⚠️ SECURITY REQUIREMENTS:\n` +
58
65
  `- At least ONE approved directory must be configured before executing any shell commands\n` +
59
66
  `- Working directory (workdir parameter or process.cwd()) MUST be within allowed directories\n` +
60
- `- All file/directory paths in command arguments are validated against allowed directories\n` +
61
- `- Command substitution and dangerous patterns may be restricted\n` +
67
+ `- All file/directory paths in arguments and redirections are validated against allowed directories (symlinks are resolved)\n` +
68
+ `- Every command in a chain must be approved; chain with ';', '&&', '||' or '|' on a single line` +
69
+ (shellConfig.platform === "Windows"
70
+ ? ` (on Windows use ';' or '|': Windows PowerShell 5.1 does not support '&&' or '||')\n`
71
+ : `\n`) +
72
+ `- Not allowed: newlines, command substitution, backticks, a lone '&', ( ) { } grouping or script blocks, heredocs, escaped quotes (\\" \\')\n` +
73
+ `- Commands matching dangerous patterns are blocked unless the server operator allowed them\n` +
62
74
  `\n` +
63
75
  `If no workdir is specified, the server's current working directory will be used and validated.` +
64
76
  approvedCommandsText +
@@ -107,19 +119,16 @@ export async function handleShellTool(name, args) {
107
119
  }
108
120
  // Validate arguments
109
121
  const validatedArgs = ShellCommandArgsSchema.parse(args);
110
- // Validate command security
122
+ // Validate command security (substitution, control characters, grammar)
111
123
  const commandValidation = validateCommand(validatedArgs.command, false);
112
124
  if (!commandValidation.allowed) {
113
- throw new Error(`Command validation failed: ${commandValidation.reason}`);
114
- }
115
- // Extract root commands for approval checking
116
- const rootCommands = extractRootCommands(validatedArgs.command);
117
- const allApproved = rootCommands.every((cmd) => approvedCommands.has(cmd) || alwaysApprovedCommands.has(cmd));
118
- // SECURITY FIX: Block ALL unapproved commands immediately
119
- // Previously, commands were only blocked if (!allApproved && (requiresApproval || isDangerous))
120
- // This allowed unapproved non-dangerous commands to execute by default
121
- if (!allApproved) {
122
- const unapprovedCommands = rootCommands.filter((cmd) => !approvedCommands.has(cmd) && !alwaysApprovedCommands.has(cmd));
125
+ throw new Error(`Access denied: Command validation failed: ${commandValidation.reason}`);
126
+ }
127
+ const parsed = parseShellCommand(validatedArgs.command);
128
+ // Every command in the chain must be approved
129
+ const rootCommands = getRootCommands(parsed);
130
+ const unapprovedCommands = rootCommands.filter((cmd) => !approvedCommands.has(cmd) && !alwaysApprovedCommands.has(cmd));
131
+ if (rootCommands.length === 0 || unapprovedCommands.length > 0) {
123
132
  const approvedList = Array.from(approvedCommands).join(", ");
124
133
  throw new Error(`Access denied: Command not in approved list.\n` +
125
134
  `Unapproved commands: ${unapprovedCommands.join(", ")}\n` +
@@ -127,19 +136,18 @@ export async function handleShellTool(name, args) {
127
136
  `Approved commands: ${approvedList || "(none configured)"}\n\n` +
128
137
  `To execute this command, add it to --approved-commands configuration.`);
129
138
  }
130
- // SECURITY: Check dangerous patterns even for approved commands
131
- // This provides defense-in-depth against accidentally approving dangerous commands
132
- const isDangerous = isDangerousCommand(validatedArgs.command);
133
- if (isDangerous && !validatedArgs.requiresApproval) {
134
- throw new Error(`⚠️ Dangerous command pattern detected.\n` +
139
+ // Dangerous patterns are blocked even for approved commands unless the
140
+ // server operator explicitly allowed that root command. The caller cannot
141
+ // opt in: requiresApproval is ignored.
142
+ const blockedDangerousRoots = getDangerousRoots(parsed).filter((cmd) => !dangerousCommandAllowlist.has(cmd));
143
+ if (blockedDangerousRoots.length > 0) {
144
+ throw new Error(`Access denied: Dangerous command pattern detected.\n` +
135
145
  `Command: ${validatedArgs.command}\n` +
136
- `This command requires explicit approval.\n` +
137
- `Set requiresApproval: true in the command arguments to proceed.`);
146
+ `Blocked commands: ${blockedDangerousRoots.join(", ")}\n` +
147
+ `The server operator can allow these with --allow-dangerous-commands.`);
138
148
  }
139
- // SECURITY FIX: Validate working directory ALWAYS (not just if provided)
140
- // This prevents bypass via process.cwd() when workdir is omitted
149
+ // Validate working directory ALWAYS (not just if provided)
141
150
  const allowedDirs = getAllowedDirectories();
142
- // Require at least one approved directory for shell execution
143
151
  if (allowedDirs.length === 0) {
144
152
  throw new Error(`Access denied: Shell execution requires at least one approved directory.\n` +
145
153
  `No allowed directories are currently configured.\n` +
@@ -150,7 +158,6 @@ export async function handleShellTool(name, args) {
150
158
  `\n` +
151
159
  `Example: register_directory with path "C:/path/to/your/project"`);
152
160
  }
153
- // Always validate working directory against allowed directories
154
161
  let workdir = validatedArgs.workdir || process.cwd();
155
162
  try {
156
163
  workdir = await validatePath(workdir);
@@ -167,44 +174,23 @@ export async function handleShellTool(name, args) {
167
174
  ` 1. Register the directory using register_directory tool, OR\n` +
168
175
  ` 2. Specify a workdir parameter within an approved directory`);
169
176
  }
170
- // Extract and validate paths from command arguments
177
+ // Validate every file system operand (arguments and redirection targets),
178
+ // resolving symlinks/junctions
179
+ let deniedPaths;
171
180
  try {
172
- const extractedPaths = extractPathsFromCommand(validatedArgs.command, workdir);
173
- if (extractedPaths.length > 0) {
174
- const allowedDirs = getAllowedDirectories();
175
- // If no allowed directories are configured, block all paths for security
176
- if (allowedDirs.length === 0) {
177
- throw new Error(`Access denied: Command contains paths but no allowed directories are configured.\n` +
178
- `Extracted paths:\n` +
179
- extractedPaths.map((p) => ` - ${p}`).join("\n") +
180
- `\n\nPlease configure allowed directories using --approved-folders or register_directory tool.`);
181
- }
182
- // Validate each extracted path
183
- const invalidPaths = [];
184
- for (const extractedPath of extractedPaths) {
185
- if (!isPathWithinAllowedDirectories(extractedPath, allowedDirs)) {
186
- invalidPaths.push(extractedPath);
187
- }
188
- }
189
- if (invalidPaths.length > 0) {
190
- throw new Error(`Access denied: Command contains paths outside allowed directories:\n` +
191
- invalidPaths.map((p) => ` - ${p}`).join("\n") +
192
- `\n\nAllowed directories:\n` +
193
- allowedDirs.map((d) => ` - ${d}`).join("\n") +
194
- `\n\nTo access these paths, register their parent directories using register_directory tool.`);
195
- }
196
- }
181
+ deniedPaths = await findDisallowedCommandPaths(parsed, workdir);
197
182
  }
198
183
  catch (error) {
199
- // If path extraction fails, be conservative and block
200
- // (better to block than allow potentially unsafe commands)
201
- if (error instanceof Error && error.message.includes("Access denied")) {
202
- throw error;
203
- }
204
- // For extraction errors, block the command to be safe
205
- throw new Error(`Path validation failed: ${error instanceof Error ? error.message : String(error)}\n` +
184
+ throw new Error(`Access denied: Path validation failed: ${error instanceof Error ? error.message : String(error)}\n` +
206
185
  `Command blocked for security. Please ensure all paths in the command are within allowed directories.`);
207
186
  }
187
+ if (deniedPaths.length > 0) {
188
+ throw new Error(`Access denied: Command contains paths outside allowed directories:\n` +
189
+ deniedPaths.map((p) => ` - ${p}`).join("\n") +
190
+ `\n\nAllowed directories:\n` +
191
+ allowedDirs.map((d) => ` - ${d}`).join("\n") +
192
+ `\n\nTo access these paths, register their parent directories using register_directory tool.`);
193
+ }
208
194
  // Execute command
209
195
  try {
210
196
  const result = await executeShellCommand(validatedArgs.command, {
@@ -1,9 +1,8 @@
1
1
  import { zodToJsonSchema } from "zod-to-json-schema";
2
2
  import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
3
3
  import path from "path";
4
- import { promises as fs } from "fs";
5
4
  import { WriteFileArgsSchema, WriteMultipleFilesArgsSchema, EditFileArgsSchema, } from "../types/index.js";
6
- import { validatePath, writeFileContent, readFileContent, applyFileEdits, } from "../utils/lib.js";
5
+ import { validatePath, writeFileContent, writeBinaryFileAtomic, readFileContent, applyFileEdits, getFileStats, } from "../utils/lib.js";
7
6
  import { isHTMLContent, convertHTMLToPDF, convertHTMLToDOCX, } from "../utils/html-to-document.js";
8
7
  import { sanitizeToolInputSchema } from "../utils/tool-schema.js";
9
8
  const ToolInputSchema = ToolSchema.shape.inputSchema;
@@ -28,15 +27,15 @@ async function writeFileBasedOnExtension(validPath, content) {
28
27
  title: fileTitle,
29
28
  author: "vulcan-file-ops",
30
29
  });
31
- // SECURITY: validPath pre-validated by validatePath() - safe from path traversal (CWE-23)
32
- await fs.writeFile(validPath, pdfBuffer);
30
+ // SECURITY: validPath pre-validated by validatePath(); atomic, symlink-safe write (VFO-17)
31
+ await writeBinaryFileAtomic(validPath, pdfBuffer);
33
32
  }
34
33
  else {
35
34
  // Fallback to simple text PDF for plain text
36
35
  const { createSimpleTextPDF } = await import("../utils/pdf-writer.js");
37
36
  const pdfBuffer = await createSimpleTextPDF(content);
38
- // SECURITY: validPath pre-validated by validatePath() - safe from path traversal (CWE-23)
39
- await fs.writeFile(validPath, pdfBuffer);
37
+ // SECURITY: validPath pre-validated by validatePath(); atomic, symlink-safe write (VFO-17)
38
+ await writeBinaryFileAtomic(validPath, pdfBuffer);
40
39
  }
41
40
  }
42
41
  else if (ext === ".docx") {
@@ -46,15 +45,15 @@ async function writeFileBasedOnExtension(validPath, content) {
46
45
  title: fileTitle,
47
46
  author: "vulcan-file-ops",
48
47
  });
49
- // SECURITY: validPath pre-validated by validatePath() - safe from path traversal (CWE-23)
50
- await fs.writeFile(validPath, docxBuffer);
48
+ // SECURITY: validPath pre-validated by validatePath(); atomic, symlink-safe write (VFO-17)
49
+ await writeBinaryFileAtomic(validPath, docxBuffer);
51
50
  }
52
51
  else {
53
52
  // Fallback to simple text DOCX for plain text
54
53
  const { createSimpleDOCX } = await import("../utils/docx-writer.js");
55
54
  const docxBuffer = await createSimpleDOCX(content);
56
- // SECURITY: validPath pre-validated by validatePath() - safe from path traversal (CWE-23)
57
- await fs.writeFile(validPath, docxBuffer);
55
+ // SECURITY: validPath pre-validated by validatePath(); atomic, symlink-safe write (VFO-17)
56
+ await writeBinaryFileAtomic(validPath, docxBuffer);
58
57
  }
59
58
  }
60
59
  else {
@@ -391,25 +390,25 @@ export async function handleWriteTool(name, args) {
391
390
  createParentIfMissing: true,
392
391
  });
393
392
  return {
393
+ success: true,
394
394
  path: file.path,
395
395
  validPath,
396
396
  content: file.content,
397
- success: true,
398
397
  };
399
398
  }
400
399
  catch (error) {
401
400
  return {
402
- path: file.path,
403
- content: file.content,
404
401
  success: false,
402
+ path: file.path,
405
403
  error: error instanceof Error ? error.message : String(error),
406
404
  };
407
405
  }
408
406
  });
409
407
  const validatedFiles = await Promise.all(validationPromises);
410
- // Separate valid and invalid files
411
- const validFiles = validatedFiles.filter((f) => f.success);
412
- const invalidFiles = validatedFiles.filter((f) => !f.success);
408
+ // Separate valid and invalid files. The type guard (no cast) ensures
409
+ // only entries carrying a validatePath() result can reach the writer.
410
+ const validFiles = validatedFiles.flatMap((f) => (f.success ? [f] : []));
411
+ const invalidFiles = validatedFiles.flatMap((f) => f.success ? [] : [f]);
413
412
  // If any paths are invalid, fail the entire operation
414
413
  if (invalidFiles.length > 0) {
415
414
  const errorMessages = invalidFiles
@@ -421,10 +420,13 @@ export async function handleWriteTool(name, args) {
421
420
  const writePromises = validFiles.map(async (file) => {
422
421
  try {
423
422
  await writeFileBasedOnExtension(file.validPath, file.content);
423
+ // Report the size of the written file: for PDF/DOCX this is the
424
+ // generated document, not the length of the HTML input.
425
+ const { size } = await getFileStats(file.validPath);
424
426
  return {
425
427
  path: file.path,
426
428
  success: true,
427
- size: Buffer.byteLength(file.content, "utf8"),
429
+ size,
428
430
  };
429
431
  }
430
432
  catch (error) {
@@ -342,7 +342,10 @@ export const GrepArgsSchema = z.object({
342
342
  pattern: z.string().describe("Regex pattern to search for"),
343
343
  path: z.string().optional().describe("Directory/file to search (optional)"),
344
344
  type: z.string().optional().describe("File type filter: js, py, ts, etc"),
345
- glob: z.string().optional().describe("Glob filter: *.js, **/*.test.ts"),
345
+ glob: z
346
+ .string()
347
+ .optional()
348
+ .describe("Glob filter. Without a slash it matches file names at any depth (*.md, *.test.ts); with a slash it matches the path relative to the search root (src/**/*.ts)"),
346
349
  "-i": z.boolean().optional().default(false).describe("Case insensitive"),
347
350
  "-A": z.number().optional().describe("Lines after match"),
348
351
  "-B": z.number().optional().describe("Lines before match"),
@@ -429,7 +432,7 @@ export const ShellCommandArgsSchema = z.object({
429
432
  .boolean()
430
433
  .optional()
431
434
  .default(false)
432
- .describe("Indicates if this command requires explicit user approval. " +
433
- "Set to true for potentially dangerous operations (installing packages, deleting files, etc.)."),
435
+ .describe("Deprecated and ignored. Kept for backward compatibility. " +
436
+ "Commands matching dangerous patterns can only be allowed by the server operator (--allow-dangerous-commands)."),
434
437
  });
435
438
  //# sourceMappingURL=index.js.map