@j0hanz/filesystem-mcp 1.13.2 → 1.14.1

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 (74) hide show
  1. package/README.md +162 -145
  2. package/dist/cli.js +2 -2
  3. package/dist/completions.js +54 -51
  4. package/dist/config.d.ts +13 -14
  5. package/dist/config.js +12 -12
  6. package/dist/index.js +1 -1
  7. package/dist/lib/abort.d.ts +7 -0
  8. package/dist/lib/abort.js +81 -0
  9. package/dist/lib/constants.d.ts +3 -1
  10. package/dist/lib/constants.js +8 -2
  11. package/dist/lib/errors.d.ts +7 -3
  12. package/dist/lib/errors.js +64 -41
  13. package/dist/lib/file-operations/core.d.ts +3 -3
  14. package/dist/lib/file-operations/core.js +23 -20
  15. package/dist/lib/file-operations/metadata.d.ts +2 -2
  16. package/dist/lib/file-operations/metadata.js +69 -22
  17. package/dist/lib/file-operations/search.d.ts +0 -1
  18. package/dist/lib/file-operations/search.js +87 -95
  19. package/dist/lib/file-operations/traversal.js +13 -15
  20. package/dist/lib/fs-helpers.d.ts +3 -10
  21. package/dist/lib/fs-helpers.js +29 -108
  22. package/dist/lib/globs.d.ts +2 -0
  23. package/dist/lib/globs.js +19 -0
  24. package/dist/lib/logger.d.ts +28 -0
  25. package/dist/lib/logger.js +91 -0
  26. package/dist/lib/observability.d.ts +7 -0
  27. package/dist/lib/observability.js +19 -9
  28. package/dist/lib/paths.js +55 -55
  29. package/dist/lib/resource-store.js +4 -4
  30. package/dist/lib/utils.d.ts +0 -12
  31. package/dist/lib/utils.js +0 -13
  32. package/dist/lib/zod-codecs.d.ts +2 -0
  33. package/dist/lib/zod-codecs.js +18 -0
  34. package/dist/pkg-info.d.ts +1 -0
  35. package/dist/pkg-info.js +2 -2
  36. package/dist/prompts.js +3 -3
  37. package/dist/resources/generated-instructions.js +41 -41
  38. package/dist/resources/tool-catalog.js +33 -58
  39. package/dist/resources/tool-info.d.ts +0 -1
  40. package/dist/resources/tool-info.js +44 -67
  41. package/dist/resources/workflows.js +47 -19
  42. package/dist/resources.d.ts +1 -1
  43. package/dist/resources.js +4 -4
  44. package/dist/schemas.d.ts +185 -465
  45. package/dist/schemas.js +174 -206
  46. package/dist/server/bootstrap.d.ts +12 -11
  47. package/dist/server/bootstrap.js +95 -86
  48. package/dist/server/roots-manager.d.ts +5 -2
  49. package/dist/server/roots-manager.js +9 -7
  50. package/dist/server/task-store.d.ts +10 -0
  51. package/dist/server/task-store.js +73 -0
  52. package/dist/tools/apply-patch.js +39 -20
  53. package/dist/tools/calculate-hash.js +14 -27
  54. package/dist/tools/create-directory.js +11 -9
  55. package/dist/tools/delete-file.js +19 -19
  56. package/dist/tools/diff-files.js +16 -18
  57. package/dist/tools/edit-file.js +11 -5
  58. package/dist/tools/list-directory.js +16 -21
  59. package/dist/tools/move-file.js +105 -100
  60. package/dist/tools/read-multiple.js +15 -10
  61. package/dist/tools/read.js +6 -7
  62. package/dist/tools/replace-in-files.js +76 -115
  63. package/dist/tools/roots.js +3 -7
  64. package/dist/tools/search-content.js +158 -203
  65. package/dist/tools/search-files.js +59 -50
  66. package/dist/tools/shared.d.ts +10 -0
  67. package/dist/tools/shared.js +105 -36
  68. package/dist/tools/stat-many.js +15 -9
  69. package/dist/tools/stat.js +6 -6
  70. package/dist/tools/task-support.d.ts +10 -9
  71. package/dist/tools/task-support.js +94 -23
  72. package/dist/tools/tree.js +4 -4
  73. package/dist/tools/write-file.js +11 -12
  74. package/package.json +10 -9
package/dist/schemas.js CHANGED
@@ -1,31 +1,32 @@
1
1
  import { z } from 'zod';
2
2
  import { DEFAULT_LIST_MAX_ENTRIES, DEFAULT_SEARCH_CONTENT_RESULTS, DEFAULT_SEARCH_RESULTS, DEFAULT_TREE_DEPTH, DEFAULT_TREE_ENTRIES, MAX_LIST_ENTRIES, MAX_SEARCH_DEPTH, MAX_SEARCH_RESULTS, MAX_TREE_DEPTH, MAX_TREE_ENTRIES, } from './lib/constants.js';
3
3
  import { ErrorCode } from './lib/errors.js';
4
- function isSafeGlobPattern(value) {
5
- if (value.length === 0)
6
- return false;
7
- if (value.includes('**/**/**'))
8
- return false;
9
- const absolutePattern = /^([/\\]|[A-Za-z]:[/\\]|\\\\)/u;
10
- if (absolutePattern.test(value)) {
11
- return false;
12
- }
13
- if (/[\\/]\.\.(?:[/\\]|$)/u.test(value) || value.startsWith('..')) {
14
- return false;
15
- }
16
- return true;
17
- }
4
+ import { isSafeGlobPattern } from './lib/globs.js';
18
5
  const MAX_PATH_LENGTH = 4096;
19
6
  const DESC_PATH_ROOT = 'Base directory (default: root). Absolute path required if multiple roots.';
20
7
  const DESC_PATH_REQUIRED = 'Absolute path to file or directory.';
21
8
  function defaultFalseBoolean(description) {
22
9
  return z.boolean().optional().default(false).describe(description);
23
10
  }
11
+ const SuccessFlagSchema = z.literal(true);
12
+ const NonNegativeIntegerSchema = z.int().min(0, 'Min: 0');
13
+ const PositiveIntegerSchema = z.int().min(1, 'Min: 1');
14
+ const IsoDateTimeSchema = z.iso.datetime();
15
+ const Sha256HexSchema = z
16
+ .string()
17
+ .regex(/^[a-f0-9]{64}$/u, 'Expected SHA-256 hex digest');
24
18
  const PathSchemaBase = z
25
19
  .string()
26
20
  .max(MAX_PATH_LENGTH, `Path too long (max ${MAX_PATH_LENGTH} chars)`);
27
21
  const OptionalPathSchema = PathSchemaBase.optional();
28
22
  const RequiredPathSchema = PathSchemaBase.min(1, 'Path required');
23
+ const SafeGlobPatternSchema = z
24
+ .string()
25
+ .min(1, 'Pattern required')
26
+ .max(1000, 'Max 1000 chars')
27
+ .refine((val) => isSafeGlobPattern(val), {
28
+ error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
29
+ });
29
30
  const FileTypeSchema = z.enum(['file', 'directory', 'symlink', 'other']);
30
31
  const ListDirectorySortSchema = z.enum(['name', 'size', 'modified', 'type']);
31
32
  const SearchFilesSortSchema = z.enum(['name', 'size', 'modified', 'path']);
@@ -43,7 +44,7 @@ const TreeEntrySchema = z.lazy(() => z.strictObject({
43
44
  children: z.array(TreeEntrySchema).optional().describe('Children'),
44
45
  }));
45
46
  const ErrorSchema = z.strictObject({
46
- code: z.enum(ErrorCode).describe('Error code (e.g. E_NOT_FOUND)'),
47
+ code: z.enum(ErrorCode).describe('Error code (e.g. NOT_FOUND)'),
47
48
  message: z.string().describe('Human-readable message'),
48
49
  path: z.string().optional().describe('Relevant path'),
49
50
  suggestion: z.string().optional().describe('Fix suggestion'),
@@ -61,11 +62,12 @@ const TailLinesSchema = z
61
62
  .optional()
62
63
  .describe('Read last N lines');
63
64
  const LineNumberSchema = z.int({ error: 'Must be integer' }).min(1, 'Min: 1');
64
- function addReadRangeIssue(ctx, path, message) {
65
+ function addReadRangeIssue(ctx, input, path, message) {
65
66
  ctx.addIssue({
66
67
  code: 'custom',
67
68
  path: [path],
68
69
  message,
70
+ input,
69
71
  });
70
72
  }
71
73
  const validateReadRange = (value, ctx) => {
@@ -74,18 +76,14 @@ const validateReadRange = (value, ctx) => {
74
76
  const hasStart = value.startLine !== undefined;
75
77
  const hasEnd = value.endLine !== undefined;
76
78
  if (hasHead && (hasStart || hasEnd)) {
77
- addReadRangeIssue(ctx, 'head', "Cannot use 'head' with 'startLine'/'endLine'");
79
+ addReadRangeIssue(ctx, value, 'head', "Cannot use 'head' with 'startLine'/'endLine'");
78
80
  }
79
81
  if (hasTail && (hasHead || hasStart || hasEnd)) {
80
- addReadRangeIssue(ctx, 'tail', "Cannot use 'tail' with 'head'/'startLine'/'endLine'");
82
+ addReadRangeIssue(ctx, value, 'tail', "Cannot use 'tail' with 'head'/'startLine'/'endLine'");
81
83
  }
82
- if (hasEnd && !hasStart) {
83
- addReadRangeIssue(ctx, 'endLine', "'endLine' requires 'startLine'");
84
- }
85
- if (value.startLine !== undefined &&
86
- value.endLine !== undefined &&
87
- value.endLine < value.startLine) {
88
- addReadRangeIssue(ctx, 'endLine', "'endLine' must be >= 'startLine'");
84
+ const effectiveStart = value.startLine ?? 1;
85
+ if (value.endLine !== undefined && value.endLine < effectiveStart) {
86
+ addReadRangeIssue(ctx, value, 'endLine', "'endLine' must be >= 'startLine'");
89
87
  }
90
88
  };
91
89
  function createReadRangeInputFields(descriptions) {
@@ -100,20 +98,20 @@ const FileInfoSchema = z.strictObject({
100
98
  name: z.string().describe('Name'),
101
99
  path: z.string().describe('Absolute path'),
102
100
  type: FileTypeSchema.describe('Type'),
103
- size: z.number().describe('Size (bytes)'),
104
- tokenEstimate: z.number().optional().describe('Est. tokens (size/4)'),
105
- created: z.string().describe('Created'),
106
- modified: z.string().describe('Modified'),
107
- accessed: z.string().describe('Accessed'),
101
+ size: NonNegativeIntegerSchema.describe('Size (bytes)'),
102
+ tokenEstimate: NonNegativeIntegerSchema.optional().describe('Est. tokens (size/4)'),
103
+ created: IsoDateTimeSchema.describe('Created'),
104
+ modified: IsoDateTimeSchema.describe('Modified'),
105
+ accessed: IsoDateTimeSchema.describe('Accessed'),
108
106
  permissions: z.string().describe('Permissions'),
109
107
  isHidden: z.boolean().describe('Hidden?'),
110
108
  mimeType: z.string().optional().describe('MIME type'),
111
109
  symlinkTarget: z.string().optional().describe('Target (symlink)'),
112
110
  });
113
111
  const OperationSummarySchema = z.strictObject({
114
- total: z.number().describe('Total'),
115
- succeeded: z.number().describe('Succeeded'),
116
- failed: z.number().describe('Failed'),
112
+ total: NonNegativeIntegerSchema.describe('Total'),
113
+ succeeded: NonNegativeIntegerSchema.describe('Succeeded'),
114
+ failed: NonNegativeIntegerSchema.describe('Failed'),
117
115
  });
118
116
  export const ListDirectoryInputSchema = z.strictObject({
119
117
  path: OptionalPathSchema.describe(DESC_PATH_ROOT),
@@ -135,12 +133,7 @@ export const ListDirectoryInputSchema = z.strictObject({
135
133
  sortBy: ListDirectorySortSchema.optional()
136
134
  .default('name')
137
135
  .describe('Sort field (name, size, modified, type)'),
138
- pattern: z
139
- .string()
140
- .min(1, 'Pattern required')
141
- .max(1000, 'Max 1000 chars')
142
- .optional()
143
- .describe('Optional glob pattern filter (e.g. "**/*.ts")'),
136
+ pattern: SafeGlobPatternSchema.optional().describe('Optional glob pattern filter (e.g. "**/*.ts")'),
144
137
  includeSymlinkTargets: defaultFalseBoolean('Resolve and include symlink targets in results'),
145
138
  cursor: z
146
139
  .string()
@@ -152,14 +145,7 @@ export const ListAllowedDirectoriesInputSchema = z
152
145
  .describe('No input parameters.');
153
146
  export const SearchFilesInputSchema = z.strictObject({
154
147
  path: OptionalPathSchema.describe(DESC_PATH_ROOT),
155
- pattern: z
156
- .string()
157
- .min(1, 'Pattern required')
158
- .max(1000, 'Max 1000 chars')
159
- .refine((val) => isSafeGlobPattern(val), {
160
- error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
161
- })
162
- .describe('Glob pattern (e.g. "**/*.ts", "src/*.js")'),
148
+ pattern: SafeGlobPatternSchema.describe('Glob pattern (e.g. "**/*.ts", "src/*.js")'),
163
149
  maxResults: z
164
150
  .int({ error: 'Must be integer' })
165
151
  .min(1, 'Min: 1')
@@ -227,16 +213,11 @@ export const SearchContentInputSchema = z.strictObject({
227
213
  .optional()
228
214
  .default(DEFAULT_SEARCH_CONTENT_RESULTS)
229
215
  .describe(`Maximum match rows to return. Default: ${DEFAULT_SEARCH_CONTENT_RESULTS}`),
230
- filePattern: z
231
- .string()
232
- .min(1, 'Pattern required')
233
- .max(1000, 'Max 1000 chars')
234
- .optional()
216
+ filePattern: SafeGlobPatternSchema.optional()
235
217
  .default('**/*')
236
218
  .describe('Glob for candidate files (e.g. "**/*.ts")'),
237
219
  includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
238
220
  includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, etc).'),
239
- multiline: defaultFalseBoolean('Multi-line mode. ^ and $ match line boundaries when isRegex=true.'),
240
221
  });
241
222
  export const ReadFileInputSchema = z
242
223
  .strictObject({
@@ -244,13 +225,13 @@ export const ReadFileInputSchema = z
244
225
  ...createReadRangeInputFields({
245
226
  head: 'Read first N lines (preview)',
246
227
  tail: 'Read last N lines',
247
- startLine: 'Start line (1-based, inclusive)',
248
- endLine: 'End line (1-based, inclusive). Requires startLine.',
228
+ startLine: 'Start line (1-based, inclusive). Defaults to 1 when endLine is set.',
229
+ endLine: 'End line (1-based, inclusive). Defaults to last line when startLine is set.',
249
230
  }),
250
231
  includeHash: defaultFalseBoolean('Include SHA-256 hash of full file content'),
251
232
  })
252
233
  .superRefine(validateReadRange)
253
- .describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'. 'endLine' requires 'startLine'.");
234
+ .describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'.");
254
235
  export const ReadMultipleFilesInputSchema = z
255
236
  .strictObject({
256
237
  paths: z
@@ -261,12 +242,12 @@ export const ReadMultipleFilesInputSchema = z
261
242
  ...createReadRangeInputFields({
262
243
  head: 'Read first N lines of each file',
263
244
  tail: 'Read last N lines of each file',
264
- startLine: 'Start line (1-based, inclusive) per file',
265
- endLine: 'End line (1-based, inclusive) per file. Requires startLine.',
245
+ startLine: 'Start line (1-based, inclusive) per file. Defaults to 1 when endLine is set.',
246
+ endLine: 'End line (1-based, inclusive) per file. Defaults to last line when startLine is set.',
266
247
  }),
267
248
  })
268
249
  .superRefine(validateReadRange)
269
- .describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'. 'endLine' requires 'startLine'.");
250
+ .describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'.");
270
251
  export const GetFileInfoInputSchema = z.strictObject({
271
252
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
272
253
  });
@@ -278,62 +259,49 @@ export const GetMultipleFileInfoInputSchema = z.strictObject({
278
259
  .describe('File/directory paths. e.g. ["src", "lib"]'),
279
260
  });
280
261
  export const ListAllowedDirectoriesOutputSchema = z.strictObject({
281
- ok: z.boolean(),
262
+ ok: SuccessFlagSchema,
282
263
  directories: z.array(z.string()).optional().describe('Allowed directories'),
283
- rootsCount: z.number().optional().describe('Number of roots'),
284
- hasMultipleRoots: z
285
- .boolean()
286
- .optional()
287
- .describe('Multiple roots configured'),
288
- error: ErrorSchema.optional(),
289
264
  });
290
265
  export const ListDirectoryOutputSchema = z.strictObject({
291
- ok: z.boolean(),
266
+ ok: SuccessFlagSchema,
292
267
  path: z.string().optional(),
293
268
  entries: z
294
269
  .array(z.strictObject({
295
270
  name: z.string().describe('Entry name'),
296
271
  relativePath: z.string().optional(),
297
272
  type: FileTypeSchema,
298
- size: z.number().optional(),
299
- modified: z.string().optional(),
273
+ size: NonNegativeIntegerSchema.optional(),
274
+ modified: IsoDateTimeSchema.optional(),
300
275
  }))
301
276
  .optional(),
302
- totalEntries: z.number().optional(),
277
+ totalEntries: NonNegativeIntegerSchema.optional(),
303
278
  truncated: z.boolean().optional(),
304
- entriesScanned: z.number().optional(),
305
- entriesVisible: z.number().optional(),
306
- totalFiles: z.number().optional(),
307
- totalDirectories: z.number().optional(),
308
- maxDepthReached: z.number().optional(),
279
+ totalFiles: NonNegativeIntegerSchema.optional(),
280
+ totalDirectories: NonNegativeIntegerSchema.optional(),
309
281
  stoppedReason: ListDirectoryStopReasonSchema.optional(),
310
- skippedInaccessible: z.number().optional(),
311
- symlinksNotFollowed: z.number().optional(),
282
+ skippedInaccessible: NonNegativeIntegerSchema.optional(),
312
283
  nextCursor: z
313
284
  .string()
314
285
  .optional()
315
286
  .describe('Cursor for the next page; absent on the final page'),
316
- error: ErrorSchema.optional(),
317
287
  });
318
288
  const SearchSummarySchema = z.strictObject({
319
- totalMatches: z.number().optional().describe('Total matches found'),
289
+ totalMatches: NonNegativeIntegerSchema.optional().describe('Total matches found'),
320
290
  truncated: z.boolean().optional().describe('Results truncated?'),
321
291
  resourceUri: z.string().optional().describe('Full results URI'),
322
- error: ErrorSchema.optional(),
323
292
  });
324
293
  export const SearchFilesOutputSchema = SearchSummarySchema.extend({
325
- ok: z.boolean(),
294
+ ok: SuccessFlagSchema,
326
295
  root: z.string().optional().describe('Search root'),
327
- pattern: z.string().optional().describe('Glob pattern used'),
328
296
  results: z
329
297
  .array(z.strictObject({
330
298
  path: z.string().describe('Relative path'),
331
- size: z.number().optional(),
332
- modified: z.string().optional(),
299
+ size: NonNegativeIntegerSchema.optional(),
300
+ modified: IsoDateTimeSchema.optional(),
333
301
  }))
334
302
  .optional(),
335
- filesScanned: z.number().optional().describe('Files scanned'),
336
- skippedInaccessible: z.number().optional().describe('Inaccessible files'),
303
+ filesScanned: NonNegativeIntegerSchema.optional().describe('Files scanned'),
304
+ skippedInaccessible: NonNegativeIntegerSchema.optional().describe('Inaccessible files'),
337
305
  stoppedReason: SearchStopReasonSchema.optional().describe('Why search stopped'),
338
306
  nextCursor: z
339
307
  .string()
@@ -341,70 +309,49 @@ export const SearchFilesOutputSchema = SearchSummarySchema.extend({
341
309
  .describe('Cursor for the next page; absent on the final page'),
342
310
  });
343
311
  export const SearchContentOutputSchema = SearchSummarySchema.extend({
344
- ok: z.boolean(),
345
- patternType: z
346
- .enum(['literal', 'regex'])
347
- .optional()
348
- .describe('Pattern interpretation'),
349
- caseSensitive: z.boolean().optional().describe('Case-sensitive matching'),
312
+ ok: SuccessFlagSchema,
350
313
  matches: z
351
314
  .array(z.strictObject({
352
315
  file: z.string().describe('Relative path'),
353
- line: z.number(),
354
- column: z
355
- .number()
356
- .optional()
357
- .describe('Column of first match (0-based)'),
316
+ line: PositiveIntegerSchema,
317
+ column: NonNegativeIntegerSchema.optional().describe('Column of first match (0-based)'),
358
318
  content: z.string(),
359
- matchCount: z.number(),
319
+ matchCount: PositiveIntegerSchema,
360
320
  contextBefore: z.array(z.string()).optional(),
361
321
  contextAfter: z.array(z.string()).optional(),
362
322
  }))
363
323
  .optional(),
364
- filesScanned: z.number().optional().describe('Files scanned'),
365
- filesMatched: z.number().optional().describe('Files with matches'),
366
- skippedTooLarge: z.number().optional().describe('Files skipped: too large'),
367
- skippedBinary: z.number().optional().describe('Files skipped: binary'),
368
- skippedInaccessible: z
369
- .number()
370
- .optional()
371
- .describe('Files skipped: inaccessible'),
372
- linesSkippedDueToRegexTimeout: z
373
- .number()
374
- .optional()
375
- .describe('Lines skipped due to regex timeout'),
324
+ filesScanned: NonNegativeIntegerSchema.optional().describe('Files scanned'),
325
+ filesMatched: NonNegativeIntegerSchema.optional().describe('Files with matches'),
326
+ skippedTooLarge: NonNegativeIntegerSchema.optional().describe('Files skipped: too large'),
327
+ skippedBinary: NonNegativeIntegerSchema.optional().describe('Files skipped: binary'),
328
+ skippedInaccessible: NonNegativeIntegerSchema.optional().describe('Files skipped: inaccessible'),
376
329
  stoppedReason: SearchStopReasonSchema.optional().describe('Why search stopped'),
377
330
  });
378
331
  export const TreeOutputSchema = z.strictObject({
379
- ok: z.boolean(),
332
+ ok: SuccessFlagSchema,
380
333
  root: z.string().optional(),
381
334
  tree: TreeEntrySchema.optional(),
382
335
  ascii: z.string().optional(),
383
336
  truncated: z.boolean().optional(),
384
- totalEntries: z.number().optional(),
385
- error: ErrorSchema.optional(),
337
+ totalEntries: NonNegativeIntegerSchema.optional(),
386
338
  });
387
339
  const ReadResultSchema = z.strictObject({
388
340
  content: z.string().optional().describe('Content'),
389
341
  truncated: z.boolean().optional().describe('Truncated?'),
390
342
  resourceUri: z.string().optional().describe('Full content URI'),
391
- totalLines: z.number().optional().describe('Total lines'),
392
- readMode: z
393
- .enum(['full', 'head', 'tail', 'range'])
394
- .optional()
395
- .describe('Mode'),
396
- head: z.number().optional().describe('Head lines'),
397
- tail: z.number().optional().describe('Tail lines'),
398
- startLine: z.number().optional().describe('Start line'),
399
- endLine: z.number().optional().describe('End line'),
400
- linesRead: z.number().optional().describe('Lines read'),
343
+ totalLines: NonNegativeIntegerSchema.optional().describe('Total lines'),
344
+ head: PositiveIntegerSchema.optional().describe('Head lines'),
345
+ tail: PositiveIntegerSchema.optional().describe('Tail lines'),
346
+ startLine: PositiveIntegerSchema.optional().describe('Start line'),
347
+ endLine: PositiveIntegerSchema.optional().describe('End line'),
348
+ linesRead: NonNegativeIntegerSchema.optional().describe('Lines read'),
401
349
  hasMoreLines: z.boolean().optional().describe('More lines?'),
402
350
  });
403
351
  export const ReadFileOutputSchema = ReadResultSchema.extend({
404
- ok: z.boolean(),
352
+ ok: SuccessFlagSchema,
405
353
  path: z.string().optional(),
406
- contentHash: z.string().optional().describe('SHA-256 of full file content'),
407
- error: ErrorSchema.optional(),
354
+ contentHash: Sha256HexSchema.optional().describe('SHA-256 of full file content'),
408
355
  });
409
356
  const ReadMultipleFileResultSchema = ReadResultSchema.extend({
410
357
  path: z.string().describe('File path'),
@@ -412,60 +359,77 @@ const ReadMultipleFileResultSchema = ReadResultSchema.extend({
412
359
  .enum(['head', 'tail', 'range', 'externalized'])
413
360
  .optional()
414
361
  .describe('Why content was truncated'),
415
- maxTotalSize: z.number().optional().describe('Max total size budget'),
416
- error: z.string().optional().describe('Error message'),
362
+ error: ErrorSchema.optional().describe('Structured error details'),
417
363
  });
418
364
  export const ReadMultipleFilesOutputSchema = z.strictObject({
419
- ok: z.boolean(),
365
+ ok: SuccessFlagSchema,
420
366
  results: z.array(ReadMultipleFileResultSchema).optional(),
421
367
  summary: OperationSummarySchema.optional(),
422
- error: ErrorSchema.optional(),
423
368
  });
424
369
  export const GetFileInfoOutputSchema = z.strictObject({
425
- ok: z.boolean(),
370
+ ok: SuccessFlagSchema,
426
371
  info: FileInfoSchema.optional(),
427
- error: ErrorSchema.optional(),
428
372
  });
429
373
  export const GetMultipleFileInfoOutputSchema = z.strictObject({
430
- ok: z.boolean(),
374
+ ok: SuccessFlagSchema,
431
375
  results: z
432
376
  .array(z.strictObject({
433
377
  path: z.string(),
434
378
  info: FileInfoSchema.optional(),
435
- error: z.string().optional(),
379
+ error: ErrorSchema.optional(),
436
380
  }))
437
381
  .optional(),
438
382
  summary: OperationSummarySchema.optional(),
439
- error: ErrorSchema.optional(),
440
383
  });
441
384
  export const CreateDirectoryInputSchema = z
442
385
  .strictObject({
443
386
  path: RequiredPathSchema.optional().describe(DESC_PATH_REQUIRED),
444
387
  paths: z
445
388
  .array(RequiredPathSchema)
389
+ .min(1, 'Min 1 path required')
446
390
  .optional()
447
391
  .describe('Absolute paths to directories to create'),
448
392
  })
449
- .refine((data) => data.path !== undefined || data.paths !== undefined, {
450
- error: "Either 'path' or 'paths' must be provided",
451
- path: ['path'],
393
+ .superRefine((data, ctx) => {
394
+ const hasPath = data.path !== undefined;
395
+ const hasPaths = data.paths !== undefined;
396
+ if (!hasPath && !hasPaths) {
397
+ ctx.addIssue({
398
+ code: 'custom',
399
+ path: ['path'],
400
+ message: "Either 'path' or 'paths' must be provided",
401
+ input: data,
402
+ });
403
+ }
404
+ if (hasPath && hasPaths) {
405
+ ctx.addIssue({
406
+ code: 'custom',
407
+ path: ['path'],
408
+ message: "Provide either 'path' or 'paths', not both",
409
+ input: data,
410
+ });
411
+ ctx.addIssue({
412
+ code: 'custom',
413
+ path: ['paths'],
414
+ message: "Provide either 'path' or 'paths', not both",
415
+ input: data,
416
+ });
417
+ }
452
418
  })
453
419
  .describe("Provide either 'path' or 'paths'.");
454
420
  export const CreateDirectoryOutputSchema = z.strictObject({
455
- ok: z.boolean(),
421
+ ok: SuccessFlagSchema,
456
422
  path: z.string().optional(),
457
423
  paths: z.array(z.string()).optional(),
458
- error: ErrorSchema.optional(),
459
424
  });
460
425
  export const WriteFileInputSchema = z.strictObject({
461
426
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
462
427
  content: z.string().describe('Content to write'),
463
428
  });
464
429
  export const WriteFileOutputSchema = z.strictObject({
465
- ok: z.boolean(),
430
+ ok: SuccessFlagSchema,
466
431
  path: z.string().optional(),
467
- bytesWritten: z.number().optional(),
468
- error: ErrorSchema.optional(),
432
+ bytesWritten: NonNegativeIntegerSchema.optional(),
469
433
  });
470
434
  export const EditFileInputSchema = z.strictObject({
471
435
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
@@ -473,6 +437,8 @@ export const EditFileInputSchema = z.strictObject({
473
437
  .array(z.strictObject({
474
438
  oldText: z
475
439
  .string()
440
+ .min(1, 'oldText required')
441
+ .max(102400, 'Max 100KB')
476
442
  .describe('Exact literal string to replace (character-for-character). Include 3–5 lines of context for unique targeting.'),
477
443
  newText: z
478
444
  .string()
@@ -486,11 +452,11 @@ export const EditFileInputSchema = z.strictObject({
486
452
  export const EditFileOutputSchema = z.strictObject({
487
453
  ok: z.boolean(),
488
454
  path: z.string().optional(),
489
- appliedEdits: z.number().optional(),
490
- linesAdded: z.number().optional().describe('Lines added'),
491
- linesRemoved: z.number().optional().describe('Lines removed'),
455
+ appliedEdits: NonNegativeIntegerSchema.optional(),
456
+ linesAdded: NonNegativeIntegerSchema.optional().describe('Lines added'),
457
+ linesRemoved: NonNegativeIntegerSchema.optional().describe('Lines removed'),
492
458
  lineRange: z
493
- .tuple([z.number(), z.number()])
459
+ .tuple([PositiveIntegerSchema, PositiveIntegerSchema])
494
460
  .optional()
495
461
  .describe('Line range modified [start, end] (1-based)'),
496
462
  unmatchedEdits: z
@@ -498,17 +464,42 @@ export const EditFileOutputSchema = z.strictObject({
498
464
  .optional()
499
465
  .describe('Edits that could not be applied'),
500
466
  diff: z.string().optional().describe('Unified diff of changes (dryRun)'),
501
- error: ErrorSchema.optional(),
502
467
  });
503
468
  export const MoveFileInputSchema = z
504
469
  .strictObject({
505
470
  source: RequiredPathSchema.optional().describe('Path to move (deprecated: use sources)'),
506
- sources: z.array(RequiredPathSchema).optional().describe('Paths to move'),
471
+ sources: z
472
+ .array(RequiredPathSchema)
473
+ .min(1, 'Min 1 source required')
474
+ .optional()
475
+ .describe('Paths to move'),
507
476
  destination: RequiredPathSchema.describe('New path'),
508
477
  })
509
- .refine((data) => (data.source ?? data.sources) !== undefined, {
510
- error: "Either 'source' or 'sources' must be provided",
511
- path: ['source'],
478
+ .superRefine((data, ctx) => {
479
+ const hasSource = data.source !== undefined;
480
+ const hasSources = data.sources !== undefined;
481
+ if (!hasSource && !hasSources) {
482
+ ctx.addIssue({
483
+ code: 'custom',
484
+ path: ['source'],
485
+ message: "Either 'source' or 'sources' must be provided",
486
+ input: data,
487
+ });
488
+ }
489
+ if (hasSource && hasSources) {
490
+ ctx.addIssue({
491
+ code: 'custom',
492
+ path: ['source'],
493
+ message: "Provide either 'source' or 'sources', not both",
494
+ input: data,
495
+ });
496
+ ctx.addIssue({
497
+ code: 'custom',
498
+ path: ['sources'],
499
+ message: "Provide either 'source' or 'sources', not both",
500
+ input: data,
501
+ });
502
+ }
512
503
  })
513
504
  .describe("Provide either 'source' or 'sources'.");
514
505
  export const MoveFileOutputSchema = z.strictObject({
@@ -519,11 +510,10 @@ export const MoveFileOutputSchema = z.strictObject({
519
510
  failed: z
520
511
  .array(z.strictObject({
521
512
  source: z.string().describe('Source path'),
522
- error: z.string().describe('Error message'),
513
+ error: ErrorSchema.describe('Structured error details'),
523
514
  }))
524
515
  .optional()
525
516
  .describe('List of files that failed to move'),
526
- error: ErrorSchema.optional(),
527
517
  });
528
518
  export const DeleteFileInputSchema = z.strictObject({
529
519
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
@@ -531,23 +521,18 @@ export const DeleteFileInputSchema = z.strictObject({
531
521
  ignoreIfNotExists: defaultFalseBoolean('No error if missing'),
532
522
  });
533
523
  export const DeleteFileOutputSchema = z.strictObject({
534
- ok: z.boolean(),
524
+ ok: SuccessFlagSchema,
535
525
  path: z.string().optional(),
536
- error: ErrorSchema.optional(),
537
526
  });
538
527
  export const CalculateHashInputSchema = z.strictObject({
539
528
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
540
529
  });
541
530
  export const CalculateHashOutputSchema = z.strictObject({
542
- ok: z.boolean(),
531
+ ok: SuccessFlagSchema,
543
532
  path: z.string().optional(),
544
- hash: z.string().optional().describe('SHA-256 hash'),
533
+ hash: Sha256HexSchema.optional().describe('SHA-256 hash'),
545
534
  isDirectory: z.boolean().optional().describe('True if path is a directory'),
546
- fileCount: z
547
- .number()
548
- .optional()
549
- .describe('Number of files hashed (directories only)'),
550
- error: ErrorSchema.optional(),
535
+ fileCount: NonNegativeIntegerSchema.optional().describe('Number of files hashed (directories only)'),
551
536
  });
552
537
  export const DiffFilesInputSchema = z.strictObject({
553
538
  original: RequiredPathSchema.describe('Path to original file'),
@@ -570,15 +555,14 @@ export const DiffFilesInputSchema = z.strictObject({
570
555
  .describe('Strip trailing carriage returns before diffing'),
571
556
  });
572
557
  export const DiffFilesOutputSchema = z.strictObject({
573
- ok: z.boolean(),
558
+ ok: SuccessFlagSchema,
574
559
  diff: z.string().optional().describe('Unified diff content'),
575
560
  isIdentical: z.boolean().optional().describe('True if files are identical'),
576
- linesAdded: z.number().optional().describe('Lines added'),
577
- linesRemoved: z.number().optional().describe('Lines removed'),
578
- hunksCount: z.number().optional().describe('Number of diff hunks'),
561
+ linesAdded: NonNegativeIntegerSchema.optional().describe('Lines added'),
562
+ linesRemoved: NonNegativeIntegerSchema.optional().describe('Lines removed'),
563
+ hunksCount: NonNegativeIntegerSchema.optional().describe('Number of diff hunks'),
579
564
  truncated: z.boolean().optional().describe('Diff content truncated?'),
580
565
  resourceUri: z.string().optional().describe('Full diff content URI'),
581
- error: ErrorSchema.optional(),
582
566
  });
583
567
  export const ApplyPatchInputSchema = z.strictObject({
584
568
  path: RequiredPathSchema.describe('Path to file to patch'),
@@ -610,33 +594,25 @@ export const ApplyPatchOutputSchema = z.strictObject({
610
594
  ok: z.boolean(),
611
595
  path: z.string().optional(),
612
596
  applied: z.boolean().optional(),
613
- hunksApplied: z.number().optional().describe('Hunks applied'),
614
- linesAdded: z.number().optional().describe('Lines added'),
615
- linesRemoved: z.number().optional().describe('Lines removed'),
597
+ hunksApplied: NonNegativeIntegerSchema.optional().describe('Hunks applied'),
598
+ linesAdded: NonNegativeIntegerSchema.optional().describe('Lines added'),
599
+ linesRemoved: NonNegativeIntegerSchema.optional().describe('Lines removed'),
616
600
  results: z
617
601
  .array(z.strictObject({
618
602
  path: z.string().describe('File path'),
619
603
  applied: z.boolean().describe('Patch applied successfully'),
620
- hunksApplied: z.number().optional().describe('Hunks applied'),
621
- linesAdded: z.number().optional().describe('Lines added'),
622
- linesRemoved: z.number().optional().describe('Lines removed'),
623
- error: z.string().optional().describe('Error message'),
604
+ hunksApplied: NonNegativeIntegerSchema.optional().describe('Hunks applied'),
605
+ linesAdded: NonNegativeIntegerSchema.optional().describe('Lines added'),
606
+ linesRemoved: NonNegativeIntegerSchema.optional().describe('Lines removed'),
607
+ error: ErrorSchema.optional().describe('Structured error details'),
624
608
  }))
625
609
  .optional()
626
610
  .describe('Per-file results for multi-file patches'),
627
- error: ErrorSchema.optional(),
628
611
  });
629
612
  export const SearchAndReplaceInputSchema = z.strictObject({
630
613
  path: OptionalPathSchema.describe(DESC_PATH_ROOT),
631
- filePattern: z
632
- .string()
633
- .min(1, 'Pattern required')
634
- .max(1000, 'Max 1000 chars')
635
- .optional()
614
+ filePattern: SafeGlobPatternSchema.optional()
636
615
  .default('**/*')
637
- .refine((val) => isSafeGlobPattern(val), {
638
- error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
639
- })
640
616
  .describe('Glob to filter files. Default: **/*'),
641
617
  searchPattern: z
642
618
  .string()
@@ -651,18 +627,9 @@ export const SearchAndReplaceInputSchema = z.strictObject({
651
627
  .default(true)
652
628
  .describe('Case-sensitive matching. Default: true.'),
653
629
  dryRun: defaultFalseBoolean('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
654
- includeHidden: z
655
- .boolean()
656
- .optional()
657
- .describe('Include hidden files/directories (starting with .). Default: false.'),
658
- includeIgnored: z
659
- .boolean()
660
- .optional()
661
- .describe('Include .gitignore-ignored files (node_modules, dist). Default: false.'),
662
- returnDiff: z
663
- .boolean()
664
- .optional()
665
- .describe('Return unified diff of changes even if dryRun is false. Default: false.'),
630
+ includeHidden: defaultFalseBoolean('Include hidden files/directories (starting with .). Default: false.'),
631
+ includeIgnored: defaultFalseBoolean('Include .gitignore-ignored files (node_modules, dist). Default: false.'),
632
+ returnDiff: defaultFalseBoolean('Return unified diff of changes even if dryRun is false. Default: false.'),
666
633
  maxFiles: z
667
634
  .int({ error: 'Must be integer' })
668
635
  .min(1, 'Min: 1')
@@ -671,22 +638,22 @@ export const SearchAndReplaceInputSchema = z.strictObject({
671
638
  .describe('Max files to process before stopping'),
672
639
  });
673
640
  export const SearchAndReplaceOutputSchema = z.strictObject({
674
- ok: z.boolean(),
675
- matches: z.number().optional().describe('Total matches found'),
676
- filesChanged: z.number().optional().describe('Files modified'),
677
- processedFiles: z.number().optional().describe('Files processed'),
678
- failedFiles: z.number().optional().describe('Files skipped due to errors'),
641
+ ok: SuccessFlagSchema,
642
+ matches: NonNegativeIntegerSchema.optional().describe('Total matches found'),
643
+ filesChanged: NonNegativeIntegerSchema.optional().describe('Files modified'),
644
+ processedFiles: NonNegativeIntegerSchema.optional().describe('Files processed'),
645
+ failedFiles: NonNegativeIntegerSchema.optional().describe('Files skipped due to errors'),
679
646
  failures: z
680
647
  .array(z.strictObject({
681
648
  path: z.string().describe('File path'),
682
- error: z.string().describe('Error message'),
649
+ error: ErrorSchema.describe('Structured error details'),
683
650
  }))
684
651
  .optional()
685
652
  .describe('Sample of per-file errors'),
686
653
  changedFiles: z
687
654
  .array(z.strictObject({
688
655
  path: z.string().describe('File path'),
689
- matches: z.number().describe('Matches in file'),
656
+ matches: PositiveIntegerSchema.describe('Matches in file'),
690
657
  }))
691
658
  .optional()
692
659
  .describe('Sample of changed files'),
@@ -694,7 +661,10 @@ export const SearchAndReplaceOutputSchema = z.strictObject({
694
661
  .boolean()
695
662
  .optional()
696
663
  .describe('Changed file list truncated'),
697
- diff: z.string().optional().describe('Unified diff of changes (dryRun only)'),
664
+ diff: z
665
+ .string()
666
+ .optional()
667
+ .describe('Unified diff of changes when `dryRun` or `returnDiff` is enabled'),
698
668
  diffTruncated: z
699
669
  .boolean()
700
670
  .optional()
@@ -703,6 +673,4 @@ export const SearchAndReplaceOutputSchema = z.strictObject({
703
673
  .enum(['maxFiles'])
704
674
  .optional()
705
675
  .describe('Why processing stopped early'),
706
- dryRun: z.boolean().optional(),
707
- error: ErrorSchema.optional(),
708
676
  });