@j0hanz/filesystem-mcp 1.8.0 → 1.9.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 (84) hide show
  1. package/README.md +635 -377
  2. package/dist/cli.js +2 -2
  3. package/dist/completions.js +2 -3
  4. package/dist/index.js +2 -2
  5. package/dist/lib/file-operations/{common.d.ts → core.d.ts} +6 -0
  6. package/dist/lib/file-operations/{common.js → core.js} +45 -0
  7. package/dist/lib/file-operations/metadata.d.ts +73 -0
  8. package/dist/lib/file-operations/metadata.js +889 -0
  9. package/dist/lib/file-operations/{search-content.d.ts → search.d.ts} +40 -7
  10. package/dist/lib/file-operations/{search-content.js → search.js} +418 -20
  11. package/dist/lib/file-operations/{glob-engine.d.ts → traversal.d.ts} +18 -1
  12. package/dist/lib/file-operations/{glob-engine.js → traversal.js} +25 -2
  13. package/dist/lib/fs-helpers.js +1 -2
  14. package/dist/lib/observability.js +1 -1
  15. package/dist/lib/{path-validation.d.ts → paths.d.ts} +11 -0
  16. package/dist/lib/{path-validation.js → paths.js} +145 -12
  17. package/dist/lib/utils.d.ts +15 -0
  18. package/dist/lib/utils.js +33 -0
  19. package/dist/resources/generated-instructions.js +6 -6
  20. package/dist/resources/tool-catalog.js +9 -9
  21. package/dist/resources/tool-info.js +3 -3
  22. package/dist/resources/workflows.js +10 -23
  23. package/dist/schemas.d.ts +11 -11
  24. package/dist/schemas.js +18 -31
  25. package/dist/server/bootstrap.d.ts +19 -1
  26. package/dist/server/bootstrap.js +130 -44
  27. package/dist/server/roots-manager.d.ts +5 -2
  28. package/dist/server/roots-manager.js +17 -5
  29. package/dist/tools/apply-patch.js +4 -3
  30. package/dist/tools/calculate-hash.js +4 -4
  31. package/dist/tools/create-directory.js +1 -1
  32. package/dist/tools/delete-file.js +3 -3
  33. package/dist/tools/diff-files.js +2 -2
  34. package/dist/tools/edit-file.js +14 -13
  35. package/dist/tools/list-directory.js +5 -7
  36. package/dist/tools/move-file.js +2 -1
  37. package/dist/tools/read-multiple.js +3 -4
  38. package/dist/tools/read.js +3 -3
  39. package/dist/tools/replace-in-files.js +8 -10
  40. package/dist/tools/roots.js +4 -6
  41. package/dist/tools/search-content.js +7 -9
  42. package/dist/tools/search-files.js +4 -6
  43. package/dist/tools/shared.d.ts +2 -1
  44. package/dist/tools/shared.js +2 -1
  45. package/dist/tools/stat-many.js +4 -3
  46. package/dist/tools/stat.js +4 -3
  47. package/dist/tools/task-support.js +1 -1
  48. package/dist/tools/tree.js +3 -4
  49. package/dist/tools/write-file.js +1 -1
  50. package/package.json +4 -4
  51. package/dist/lib/file-operations/file-info.d.ts +0 -10
  52. package/dist/lib/file-operations/file-info.js +0 -143
  53. package/dist/lib/file-operations/gitignore.d.ts +0 -6
  54. package/dist/lib/file-operations/gitignore.js +0 -45
  55. package/dist/lib/file-operations/glob-helpers.d.ts +0 -18
  56. package/dist/lib/file-operations/glob-helpers.js +0 -23
  57. package/dist/lib/file-operations/list-directory.d.ts +0 -14
  58. package/dist/lib/file-operations/list-directory.js +0 -252
  59. package/dist/lib/file-operations/read-multiple-files.d.ts +0 -25
  60. package/dist/lib/file-operations/read-multiple-files.js +0 -252
  61. package/dist/lib/file-operations/search-files.d.ts +0 -27
  62. package/dist/lib/file-operations/search-files.js +0 -216
  63. package/dist/lib/file-operations/search-matcher.d.ts +0 -10
  64. package/dist/lib/file-operations/search-matcher.js +0 -72
  65. package/dist/lib/file-operations/search-worker.d.ts +0 -2
  66. package/dist/lib/file-operations/search-worker.js +0 -131
  67. package/dist/lib/file-operations/tree.d.ts +0 -28
  68. package/dist/lib/file-operations/tree.js +0 -265
  69. package/dist/lib/option-utils.d.ts +0 -3
  70. package/dist/lib/option-utils.js +0 -15
  71. package/dist/lib/path-format.d.ts +0 -1
  72. package/dist/lib/path-format.js +0 -7
  73. package/dist/lib/path-policy.d.ts +0 -2
  74. package/dist/lib/path-policy.js +0 -100
  75. package/dist/lib/progress-reporting.d.ts +0 -11
  76. package/dist/lib/progress-reporting.js +0 -13
  77. package/dist/lib/type-guards.d.ts +0 -1
  78. package/dist/lib/type-guards.js +0 -3
  79. package/dist/server/capabilities.d.ts +0 -10
  80. package/dist/server/capabilities.js +0 -48
  81. package/dist/server/logging.d.ts +0 -7
  82. package/dist/server/logging.js +0 -41
  83. package/dist/server/types.d.ts +0 -4
  84. package/dist/server/types.js +0 -1
@@ -1,10 +1,114 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as os from 'node:os';
3
3
  import * as path from 'node:path';
4
+ import { AsyncLocalStorage } from 'node:async_hooks';
5
+ import { platform } from 'node:os';
4
6
  import { fileURLToPath } from 'node:url';
7
+ import { SENSITIVE_FILE_ALLOWLIST, SENSITIVE_FILE_DENYLIST, } from './constants.js';
5
8
  import { ErrorCode, isAbortError, isNodeError, McpError } from './errors.js';
6
9
  import { assertNotAborted, withAbort } from './fs-helpers.js';
7
- const IS_WINDOWS = os.platform() === 'win32';
10
+ const WINDOWS_PATH_SEPARATOR = '\\';
11
+ const POSIX_PATH_SEPARATOR = '/';
12
+ export function toPosixPath(value) {
13
+ return value.includes(WINDOWS_PATH_SEPARATOR)
14
+ ? value.replace(/\\/gu, POSIX_PATH_SEPARATOR)
15
+ : value;
16
+ }
17
+ const IS_WINDOWS = platform() === 'win32';
18
+ const WINDOWS_ABSOLUTE_RE = /^[a-z]:\//iu;
19
+ function normalizePathForMatch(input) {
20
+ return toPosixPath(path.normalize(input));
21
+ }
22
+ function normalizeForMatch(input) {
23
+ const normalized = normalizePathForMatch(input);
24
+ return IS_WINDOWS ? normalized.toLowerCase() : normalized;
25
+ }
26
+ function compilePatternGlobs(normalizedPattern) {
27
+ const globs = new Set([normalizedPattern]);
28
+ const isWindowsAbsolute = WINDOWS_ABSOLUTE_RE.test(normalizedPattern);
29
+ if (!normalizedPattern.startsWith('**/') && !isWindowsAbsolute) {
30
+ const withoutRoot = normalizedPattern.replace(/^\/+/u, '');
31
+ if (withoutRoot.length > 0) {
32
+ globs.add(`**/${withoutRoot}`);
33
+ }
34
+ }
35
+ return [...globs];
36
+ }
37
+ function compilePatterns(patterns) {
38
+ const unique = new Set();
39
+ for (const pattern of patterns) {
40
+ const trimmed = pattern.trim();
41
+ if (trimmed.length > 0) {
42
+ unique.add(trimmed);
43
+ }
44
+ }
45
+ const compiled = [];
46
+ for (const pattern of unique) {
47
+ const normalized = normalizeForMatch(pattern);
48
+ const matchesPath = normalized.includes('/');
49
+ compiled.push({
50
+ globs: matchesPath ? compilePatternGlobs(normalized) : [normalized],
51
+ matchesPath,
52
+ });
53
+ }
54
+ return compiled;
55
+ }
56
+ function toPatternSet(patterns) {
57
+ const pathGlobs = new Set();
58
+ const nameGlobs = new Set();
59
+ for (const pattern of patterns) {
60
+ const target = pattern.matchesPath ? pathGlobs : nameGlobs;
61
+ for (const glob of pattern.globs) {
62
+ target.add(glob);
63
+ }
64
+ }
65
+ return {
66
+ pathGlobs: [...pathGlobs],
67
+ nameGlobs: [...nameGlobs],
68
+ };
69
+ }
70
+ const DENY_PATTERNS = toPatternSet(compilePatterns(SENSITIVE_FILE_DENYLIST));
71
+ const ALLOW_PATTERNS = toPatternSet(compilePatterns(SENSITIVE_FILE_ALLOWLIST));
72
+ function uniquePair(primary, secondary) {
73
+ if (!secondary || secondary === primary)
74
+ return [primary];
75
+ return [primary, secondary];
76
+ }
77
+ function matchesAnyGlobs(globs, candidates) {
78
+ if (globs.length === 0 || candidates.length === 0)
79
+ return false;
80
+ for (const candidate of candidates) {
81
+ for (const glob of globs) {
82
+ if (path.posix.matchesGlob(candidate, glob))
83
+ return true;
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+ export function isSensitivePath(requestedPath, resolvedPath) {
89
+ if (DENY_PATTERNS.pathGlobs.length === 0 &&
90
+ DENY_PATTERNS.nameGlobs.length === 0) {
91
+ return false;
92
+ }
93
+ const normalizedRequested = normalizeForMatch(requestedPath);
94
+ const normalizedResolved = resolvedPath
95
+ ? normalizeForMatch(resolvedPath)
96
+ : undefined;
97
+ const pathCandidates = uniquePair(normalizedRequested, normalizedResolved);
98
+ const nameCandidates = uniquePair(path.posix.basename(normalizedRequested), normalizedResolved ? path.posix.basename(normalizedResolved) : undefined);
99
+ if (matchesAnyGlobs(ALLOW_PATTERNS.pathGlobs, pathCandidates) ||
100
+ matchesAnyGlobs(ALLOW_PATTERNS.nameGlobs, nameCandidates)) {
101
+ return false;
102
+ }
103
+ return (matchesAnyGlobs(DENY_PATTERNS.pathGlobs, pathCandidates) ||
104
+ matchesAnyGlobs(DENY_PATTERNS.nameGlobs, nameCandidates));
105
+ }
106
+ export function assertAllowedFileAccess(requestedPath, resolvedPath) {
107
+ if (!isSensitivePath(requestedPath, resolvedPath))
108
+ return;
109
+ throw new McpError(ErrorCode.E_ACCESS_DENIED, `Access denied: sensitive file blocked by policy (${requestedPath}). ` +
110
+ 'Set FS_CONTEXT_ALLOW_SENSITIVE=1 or use FS_CONTEXT_ALLOWLIST to override.', requestedPath);
111
+ }
8
112
  const HOMEDIR = os.homedir();
9
113
  const PATH_SEPARATOR = path.sep;
10
114
  const DRIVE_LETTER_REGEX = /^[A-Za-z]:/;
@@ -34,9 +138,18 @@ const RESERVED_DEVICE_NAMES = new Set([
34
138
  'LPT8',
35
139
  'LPT9',
36
140
  ]);
141
+ const allowedDirectoriesContext = new AsyncLocalStorage({
142
+ name: 'filesystem-mcp:allowed-directories',
143
+ });
37
144
  function dedupePreserveOrder(items) {
38
145
  return [...new Set(items)];
39
146
  }
147
+ function cloneAllowedDirectoriesState(state) {
148
+ return {
149
+ primary: [...state.primary],
150
+ expanded: [...state.expanded],
151
+ };
152
+ }
40
153
  function expandHome(filepath) {
41
154
  if (filepath === '~')
42
155
  return HOMEDIR;
@@ -115,26 +228,41 @@ function normalizeAllowedDirectories(dirs) {
115
228
  // single MCP session per process, so this is safe. In HTTP mode all HTTP
116
229
  // sessions within the same process share one policy — multi-tenant isolation
117
230
  // (different roots per session) requires separate server processes.
118
- let allowedDirectoriesExpanded = [];
119
- let allowedDirectoriesPrimary = [];
231
+ let defaultAllowedDirectoriesState = {
232
+ primary: [],
233
+ expanded: [],
234
+ };
120
235
  function setAllowedDirectoriesState(primary, expanded) {
121
- allowedDirectoriesPrimary = dedupePreserveOrder(primary);
122
- allowedDirectoriesExpanded = dedupePreserveOrder(expanded);
236
+ defaultAllowedDirectoriesState = {
237
+ primary: dedupePreserveOrder(primary),
238
+ expanded: dedupePreserveOrder(expanded),
239
+ };
240
+ }
241
+ function getActiveAllowedDirectoriesState() {
242
+ return allowedDirectoriesContext.getStore() ?? defaultAllowedDirectoriesState;
243
+ }
244
+ export function withAllowedDirectoriesState(state, run) {
245
+ return allowedDirectoriesContext.run(cloneAllowedDirectoriesState(state), run);
246
+ }
247
+ export function getAllowedDirectoriesState() {
248
+ return cloneAllowedDirectoriesState(getActiveAllowedDirectoriesState());
249
+ }
250
+ export function setAllowedDirectoriesStateResolved(state) {
251
+ setAllowedDirectoriesState(state.primary, state.expanded);
123
252
  }
124
253
  export function getAllowedDirectories() {
125
- return [...allowedDirectoriesExpanded];
254
+ return [...getActiveAllowedDirectoriesState().expanded];
126
255
  }
127
256
  export function isAllowedDirectoryRoot(normalizedPath) {
128
- for (const dir of allowedDirectoriesExpanded) {
257
+ for (const dir of getActiveAllowedDirectoriesState().expanded) {
129
258
  if (isSamePath(normalizedPath, dir))
130
259
  return true;
131
260
  }
132
261
  return false;
133
262
  }
134
263
  function getAllowedDirectoriesForRelativeResolution() {
135
- return allowedDirectoriesPrimary.length > 0
136
- ? allowedDirectoriesPrimary
137
- : allowedDirectoriesExpanded;
264
+ const state = getActiveAllowedDirectoriesState();
265
+ return state.primary.length > 0 ? state.primary : state.expanded;
138
266
  }
139
267
  function isPathInsideDirectory(normalizedDirectory, normalizedCandidate) {
140
268
  const root = normalizeForComparison(normalizedDirectory);
@@ -183,10 +311,14 @@ async function expandAllowedDirectories(primaryDirs, signal) {
183
311
  }
184
312
  return dedupePreserveOrder(expanded);
185
313
  }
186
- export async function setAllowedDirectoriesResolved(dirs, signal) {
314
+ export async function resolveAllowedDirectoriesState(dirs, signal) {
187
315
  const primary = normalizeAllowedDirectories(dirs);
188
316
  const expanded = await expandAllowedDirectories(primary, signal);
189
- setAllowedDirectoriesState(primary, expanded);
317
+ return { primary, expanded };
318
+ }
319
+ export async function setAllowedDirectoriesResolved(dirs, signal) {
320
+ const state = await resolveAllowedDirectoriesState(dirs, signal);
321
+ setAllowedDirectoriesStateResolved(state);
190
322
  }
191
323
  function ensureNonEmptyPath(requestedPath) {
192
324
  if (!requestedPath || requestedPath.trim().length === 0) {
@@ -393,6 +525,7 @@ export async function validatePathForWrite(requestedPath, signal) {
393
525
  allowedDirs,
394
526
  details: { normalizedPath: normalizedRequested },
395
527
  });
528
+ assertAllowedFileAccess(requestedPath, normalizedRequested);
396
529
  let current = normalizedRequested;
397
530
  for (;;) {
398
531
  try {
@@ -0,0 +1,15 @@
1
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
2
+ export declare function mergeOptions<T extends object>(defaults: T, overrides: Partial<T>): T;
3
+ export declare function omitOptionKeys<T extends object, K extends keyof T>(input: T, keys: readonly K[]): Omit<T, K>;
4
+ export declare function setIfDefined<T extends object, K extends keyof T>(target: T, key: K, value: T[K] | undefined): void;
5
+ export interface ProgressPayload {
6
+ current: number;
7
+ total?: number;
8
+ }
9
+ export type ProgressCallback = ((progress: ProgressPayload) => void) | undefined;
10
+ export interface PeriodicProgressOptions {
11
+ total?: number;
12
+ throttleModulo?: number;
13
+ force?: boolean;
14
+ }
15
+ export declare function reportPeriodicProgress(onProgress: ProgressCallback, current: number, options?: PeriodicProgressOptions): void;
@@ -0,0 +1,33 @@
1
+ // type-guards.ts
2
+ export function isRecord(value) {
3
+ return value !== null && typeof value === 'object';
4
+ }
5
+ // option-utils.ts
6
+ export function mergeOptions(defaults, overrides) {
7
+ return { ...defaults, ...overrides };
8
+ }
9
+ export function omitOptionKeys(input, keys) {
10
+ const output = { ...input };
11
+ for (const key of keys) {
12
+ Reflect.deleteProperty(output, key);
13
+ }
14
+ return output;
15
+ }
16
+ export function setIfDefined(target, key, value) {
17
+ if (value !== undefined) {
18
+ target[key] = value;
19
+ }
20
+ }
21
+ export function reportPeriodicProgress(onProgress, current, options = {}) {
22
+ if (!onProgress || current === 0)
23
+ return;
24
+ const throttleModulo = options.throttleModulo ?? 1;
25
+ const force = options.force ?? false;
26
+ if (!force && throttleModulo > 1 && current % throttleModulo !== 0) {
27
+ return;
28
+ }
29
+ onProgress({
30
+ current,
31
+ ...(options.total !== undefined ? { total: options.total } : {}),
32
+ });
33
+ }
@@ -2,7 +2,7 @@ import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
2
2
  import { buildCoreContextPack, getSharedConstraints, getToolContracts, } from './tool-info.js';
3
3
  import { buildWorkflowGuide } from './workflows.js';
4
4
  const INSTRUCTIONS_HEADER = `<role>
5
- Filesystem agent for local paths only. Operate inside allowed roots. Discover before action. Never guess paths.
5
+ Filesystem agent. Scope: allowed roots only. Discover paths before acting — never guess.
6
6
  </role>
7
7
 
8
8
  <tools_overview>
@@ -16,10 +16,10 @@ Filesystem agent for local paths only. Operate inside allowed roots. Discover be
16
16
 
17
17
  <resources>
18
18
  - \`internal://instructions\`: Full usage reference.
19
- - \`internal://tool-catalog\`: Tool routing and data-flow rules.
19
+ - \`internal://tool-catalog\`: Tool routing and data flow.
20
20
  - \`internal://workflows\`: Standard execution sequences.
21
- - \`internal://tool-info/{name}\`: Per-tool nuances and gotchas (example: \`internal://tool-info/read\`).
22
- - \`filesystem-mcp://result/{id}\`: Cached large output. If \`resourceUri\` is returned, call \`resources/read\` immediately.
21
+ - \`internal://tool-info/{name}\`: Per-tool nuances (e.g. \`internal://tool-info/read\`).
22
+ - \`filesystem-mcp://result/{id}\`: Cached large output — call \`resources/read\` immediately when \`resourceUri\` is returned.
23
23
  - \`filesystem-mcp://metrics\`: Per-tool runtime metrics.
24
24
  </resources>
25
25
 
@@ -35,8 +35,8 @@ ${getSharedConstraints()
35
35
  </constraints>
36
36
 
37
37
  <error_handling>
38
- - \`E_ACCESS_DENIED\` => call \`roots\`, then use an allowed path.
39
- - \`E_NOT_FOUND\` => call \`ls\` or \`find\`, then verify spelling.
38
+ - \`E_ACCESS_DENIED\` => call \`roots\`, use an allowed path.
39
+ - \`E_NOT_FOUND\` => call \`ls\` or \`find\`, verify spelling.
40
40
  - \`E_TOO_LARGE\` => use \`head\`, line ranges, or \`read_many\`.
41
41
  - \`E_TIMEOUT\` => reduce scope or result limits.
42
42
  </error_handling>
@@ -3,26 +3,26 @@ const CATALOG_GUIDE = `<tool_selection_guide>
3
3
  ## Cross-Tool Data Flow
4
4
 
5
5
  \`\`\`
6
- find (results[].path) -> grep.paths
7
- diff_files (patch text) -> apply_patch.patch
6
+ find(results[].path) -> grep.paths
7
+ diff_files(patch) -> apply_patch.patch
8
8
  \`\`\`
9
9
 
10
10
  ## Search Strategy
11
11
 
12
- - Use \`find\` for glob file discovery.
13
- - Use \`grep\` for text search.
14
- - Use \`search_and_replace\` only for replacement, never discovery.
12
+ - \`find\`: glob file discovery.
13
+ - \`grep\`: text content search.
14
+ - \`search_and_replace\`: replacement only, not discovery.
15
15
 
16
16
  ## Write Strategy
17
17
 
18
- - Use \`edit\` for precise, first-occurrence replacements in existing files.
19
- - Use \`write\` to create files or overwrite full contents.
20
- - Use \`search_and_replace\` for bulk multi-file replacements.
18
+ - \`edit\`: precise first-occurrence replacements.
19
+ - \`write\`: create files or overwrite full contents.
20
+ - \`search_and_replace\`: bulk multi-file replacements.
21
21
 
22
22
  ## Patch Management
23
23
 
24
24
  - Generate patches with \`diff_files\` first.
25
- - Run \`apply_patch\` with \`dryRun: true\` before writing.
25
+ - Validate with \`apply_patch(dryRun:true)\` before writing.
26
26
  - \`apply_patch\` accepts unified diffs only.
27
27
  </tool_selection_guide>
28
28
  `;
@@ -37,10 +37,10 @@ export function buildCoreContextPack() {
37
37
  }
38
38
  export function getSharedConstraints() {
39
39
  return [
40
- 'Use allowed roots only (provided by CLI negotiation).',
40
+ 'Use allowed roots only (from CLI negotiation).',
41
41
  'Sensitive paths are denylisted by default.',
42
- `Limits are enforced: max file size ${Math.floor(MAX_TEXT_FILE_SIZE / 1024 / 1024)}MB; search caps ${MAX_SEARCH_RESULTS} files and ${DEFAULT_SEARCH_CONTENT_RESULTS} lines.`,
43
- 'If a response includes `resourceUri`, call `resources/read` immediately; cached results expire on process restart.',
42
+ `Limits enforced: max file size ${Math.floor(MAX_TEXT_FILE_SIZE / 1024 / 1024)}MB; search caps ${MAX_SEARCH_RESULTS} files, ${DEFAULT_SEARCH_CONTENT_RESULTS} content matches.`,
43
+ 'If response includes `resourceUri`, call `resources/read` immediately — cached results expire on restart.',
44
44
  ];
45
45
  }
46
46
  export function buildToolInfo(name) {
@@ -1,33 +1,20 @@
1
1
  export function buildWorkflowGuide() {
2
2
  return `<workflows>
3
- ### A: EXPLORE
4
- Use when: you need directory layout or file content.
5
- 1. \`roots\` (list allowed paths).
6
- 2. \`ls\` (flat view) or \`tree\` (recursive view).
7
- 3. \`stat\` or \`stat_many\` (type and size checks).
8
- 4. \`read\` or \`read_many\` (read content).
3
+ ### A: EXPLORE — directory layout or file content
4
+ 1. \`roots\` → \`ls\` or \`tree\` → \`stat\`/\`stat_many\` → \`read\`/\`read_many\`.
9
5
  > **Strict:** Resolve paths first. Never guess.
10
6
 
11
- ### B: SEARCH
12
- Use when: you need files by pattern or content.
13
- 1. \`find\` (glob candidates).
14
- 2. \`grep\` (content matches).
15
- 3. \`read\` (verify matched context).
16
- > **Strict:** Do content search with \`grep\`, not \`find\`.
7
+ ### B: SEARCH — files by pattern or content
8
+ 1. \`find\` (glob) → \`grep\` (content) → \`read\` (verify).
9
+ > **Strict:** Content search with \`grep\`, not \`find\`.
17
10
 
18
- ### C: EDIT
19
- Use when: you need to modify files or layout.
20
- 1. \`edit\` (targeted string replacement).
21
- 2. \`search_and_replace\` (bulk replacements).
22
- 3. \`mv\` or \`rm\` (layout changes).
23
- 4. \`mkdir\` (directory creation).
11
+ ### C: EDIT — modify files or layout
12
+ 1. \`edit\` (targeted replacement) or \`search_and_replace\` (bulk).
13
+ 2. \`mv\`/\`rm\` (layout) or \`mkdir\` (create dirs).
24
14
  > **Strict:** Confirm destructive ops (\`write\`, \`mv\`, \`rm\`, bulk replace).
25
15
 
26
- ### D: PATCH
27
- Use when: applying unified diffs from \`diff_files\`.
28
- 1. \`diff_files\` (generate).
29
- 2. \`apply_patch\` (dryRun: true).
30
- 3. \`apply_patch\` (dryRun: false).
16
+ ### D: PATCH — apply unified diffs
17
+ 1. \`diff_files\` → \`apply_patch(dryRun:true)\` → \`apply_patch\`.
31
18
  > **Tip:** Feed \`diff_files\` output directly to \`apply_patch\`.
32
19
  </workflows>`;
33
20
  }
package/dist/schemas.d.ts CHANGED
@@ -34,13 +34,13 @@ export declare const ToolErrorResponseSchema: z.ZodObject<{
34
34
  }, z.core.$strict>;
35
35
  }, z.core.$strict>;
36
36
  declare const HeadLinesSchema: z.ZodOptional<z.ZodInt>;
37
- declare const LineNumberSchema: z.ZodNumber;
37
+ declare const LineNumberSchema: z.ZodInt;
38
38
  export declare const ListDirectoryInputSchema: z.ZodObject<{
39
39
  path: z.ZodOptional<z.ZodString>;
40
40
  includeHidden: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
41
41
  includeIgnored: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
42
- maxDepth: z.ZodOptional<z.ZodNumber>;
43
- maxEntries: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
42
+ maxDepth: z.ZodOptional<z.ZodInt>;
43
+ maxEntries: z.ZodDefault<z.ZodOptional<z.ZodInt>>;
44
44
  sortBy: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
45
45
  name: "name";
46
46
  size: "size";
@@ -55,7 +55,7 @@ export declare const ListAllowedDirectoriesInputSchema: z.ZodObject<{}, z.core.$
55
55
  export declare const SearchFilesInputSchema: z.ZodObject<{
56
56
  path: z.ZodOptional<z.ZodString>;
57
57
  pattern: z.ZodString;
58
- maxResults: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
58
+ maxResults: z.ZodDefault<z.ZodOptional<z.ZodInt>>;
59
59
  includeIgnored: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
60
60
  includeHidden: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
61
61
  sortBy: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
@@ -64,13 +64,13 @@ export declare const SearchFilesInputSchema: z.ZodObject<{
64
64
  path: "path";
65
65
  modified: "modified";
66
66
  }>>>;
67
- maxDepth: z.ZodOptional<z.ZodNumber>;
67
+ maxDepth: z.ZodOptional<z.ZodInt>;
68
68
  cursor: z.ZodOptional<z.ZodString>;
69
69
  }, z.core.$strict>;
70
70
  export declare const TreeInputSchema: z.ZodObject<{
71
71
  path: z.ZodOptional<z.ZodString>;
72
- maxDepth: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
73
- maxEntries: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
72
+ maxDepth: z.ZodDefault<z.ZodOptional<z.ZodInt>>;
73
+ maxEntries: z.ZodDefault<z.ZodOptional<z.ZodInt>>;
74
74
  includeHidden: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
75
75
  includeIgnored: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
76
76
  }, z.core.$strict>;
@@ -80,8 +80,8 @@ export declare const SearchContentInputSchema: z.ZodObject<{
80
80
  isRegex: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
81
81
  caseSensitive: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
82
82
  wholeWord: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
83
- contextLines: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
84
- maxResults: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
83
+ contextLines: z.ZodDefault<z.ZodOptional<z.ZodInt>>;
84
+ maxResults: z.ZodDefault<z.ZodOptional<z.ZodInt>>;
85
85
  filePattern: z.ZodDefault<z.ZodOptional<z.ZodString>>;
86
86
  includeHidden: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
87
87
  includeIgnored: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
@@ -658,7 +658,7 @@ export declare const CalculateHashOutputSchema: z.ZodObject<{
658
658
  export declare const DiffFilesInputSchema: z.ZodObject<{
659
659
  original: z.ZodString;
660
660
  modified: z.ZodString;
661
- context: z.ZodOptional<z.ZodNumber>;
661
+ context: z.ZodOptional<z.ZodInt>;
662
662
  ignoreWhitespace: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
663
663
  stripTrailingCr: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
664
664
  }, z.core.$strict>;
@@ -691,7 +691,7 @@ export declare const DiffFilesOutputSchema: z.ZodObject<{
691
691
  export declare const ApplyPatchInputSchema: z.ZodObject<{
692
692
  path: z.ZodString;
693
693
  patch: z.ZodString;
694
- fuzzFactor: z.ZodOptional<z.ZodNumber>;
694
+ fuzzFactor: z.ZodOptional<z.ZodInt>;
695
695
  autoConvertLineEndings: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
696
696
  dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
697
697
  }, z.core.$strict>;
package/dist/schemas.js CHANGED
@@ -16,8 +16,8 @@ function isSafeGlobPattern(value) {
16
16
  return true;
17
17
  }
18
18
  const MAX_PATH_LENGTH = 4096;
19
- const DESC_PATH_ROOT = 'Base directory (default: root). Absolute path required if multiple roots exist. Examples: "src", "src/components"';
20
- const DESC_PATH_REQUIRED = 'Absolute path to file or directory. Examples: "src/index.ts", "README.md"';
19
+ const DESC_PATH_ROOT = 'Base directory (default: root). Absolute path required if multiple roots.';
20
+ const DESC_PATH_REQUIRED = 'Absolute path to file or directory.';
21
21
  function defaultFalseBoolean(description) {
22
22
  return z.boolean().optional().default(false).describe(description);
23
23
  }
@@ -57,10 +57,7 @@ const HeadLinesSchema = z
57
57
  .max(100000, 'Max: 100,000')
58
58
  .optional()
59
59
  .describe('Read first N lines');
60
- const LineNumberSchema = z
61
- .number()
62
- .int({ error: 'Must be integer' })
63
- .min(1, 'Min: 1');
60
+ const LineNumberSchema = z.int({ error: 'Must be integer' }).min(1, 'Min: 1');
64
61
  function addReadRangeIssue(ctx, path, message) {
65
62
  ctx.addIssue({
66
63
  code: 'custom',
@@ -115,14 +112,12 @@ export const ListDirectoryInputSchema = z.strictObject({
115
112
  includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
116
113
  includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, .git, etc).'),
117
114
  maxDepth: z
118
- .number()
119
115
  .int({ error: 'Must be integer' })
120
116
  .min(1, 'Min: 1')
121
117
  .max(MAX_TREE_DEPTH, `Max: ${MAX_TREE_DEPTH}`)
122
118
  .optional()
123
119
  .describe('Max recursion depth when pattern is provided'),
124
120
  maxEntries: z
125
- .number()
126
121
  .int({ error: 'Must be integer' })
127
122
  .min(1, 'Min: 1')
128
123
  .max(MAX_LIST_ENTRIES, `Max: ${MAX_LIST_ENTRIES}`)
@@ -158,7 +153,6 @@ export const SearchFilesInputSchema = z.strictObject({
158
153
  })
159
154
  .describe('Glob pattern (e.g. "**/*.ts", "src/*.js")'),
160
155
  maxResults: z
161
- .number()
162
156
  .int({ error: 'Must be integer' })
163
157
  .min(1, 'Min: 1')
164
158
  .max(MAX_SEARCH_RESULTS, `Max: ${MAX_SEARCH_RESULTS}`)
@@ -171,7 +165,6 @@ export const SearchFilesInputSchema = z.strictObject({
171
165
  .default('path')
172
166
  .describe('Sort by path, name, size, or modified'),
173
167
  maxDepth: z
174
- .number()
175
168
  .int({ error: 'Must be integer' })
176
169
  .min(0, 'Min: 0')
177
170
  .max(MAX_SEARCH_DEPTH, `Max: ${MAX_SEARCH_DEPTH}`)
@@ -185,7 +178,6 @@ export const SearchFilesInputSchema = z.strictObject({
185
178
  export const TreeInputSchema = z.strictObject({
186
179
  path: OptionalPathSchema.describe(DESC_PATH_ROOT),
187
180
  maxDepth: z
188
- .number()
189
181
  .int({ error: 'Must be integer' })
190
182
  .min(0, 'Min: 0')
191
183
  .max(MAX_TREE_DEPTH, `Max: ${MAX_TREE_DEPTH}`)
@@ -193,7 +185,6 @@ export const TreeInputSchema = z.strictObject({
193
185
  .default(DEFAULT_TREE_DEPTH)
194
186
  .describe(`Depth (0=root node only, no children). Default: ${DEFAULT_TREE_DEPTH}`),
195
187
  maxEntries: z
196
- .number()
197
188
  .int({ error: 'Must be integer' })
198
189
  .min(1, 'Min: 1')
199
190
  .max(MAX_TREE_ENTRIES, `Max: ${MAX_TREE_ENTRIES}`)
@@ -209,12 +200,11 @@ export const SearchContentInputSchema = z.strictObject({
209
200
  .string()
210
201
  .min(1, 'Pattern required')
211
202
  .max(1000, 'Max 1000 chars')
212
- .describe('Literal text to search for by default; treated as RE2 regex when isRegex is true.'),
213
- isRegex: defaultFalseBoolean('Treat pattern as a RE2 regular expression. RE2 does not support lookahead, lookbehind, or backreferences.'),
214
- caseSensitive: defaultFalseBoolean('Case-sensitive matching (default: false — searches are case-insensitive).'),
203
+ .describe('Search text. RE2 regex when `isRegex=true`.'),
204
+ isRegex: defaultFalseBoolean('Treat pattern as RE2 regex (no lookahead/lookbehind/backrefs).'),
205
+ caseSensitive: defaultFalseBoolean('Case-sensitive matching. Default: case-insensitive.'),
215
206
  wholeWord: defaultFalseBoolean('Match whole words only'),
216
207
  contextLines: z
217
- .number()
218
208
  .int({ error: 'Must be integer' })
219
209
  .min(0, 'Min: 0')
220
210
  .max(50, 'Max: 50')
@@ -222,7 +212,6 @@ export const SearchContentInputSchema = z.strictObject({
222
212
  .default(0)
223
213
  .describe('Include N lines of context before/after matches'),
224
214
  maxResults: z
225
- .number()
226
215
  .int({ error: 'Must be integer' })
227
216
  .min(0, 'Min: 0')
228
217
  .max(MAX_SEARCH_RESULTS, `Max: ${MAX_SEARCH_RESULTS}`)
@@ -434,7 +423,7 @@ export const CreateDirectoryInputSchema = z
434
423
  .describe('Absolute paths to directories to create'),
435
424
  })
436
425
  .refine((data) => data.path !== undefined || data.paths !== undefined, {
437
- message: "Either 'path' or 'paths' must be provided",
426
+ error: "Either 'path' or 'paths' must be provided",
438
427
  path: ['path'],
439
428
  });
440
429
  export const CreateDirectoryOutputSchema = z.strictObject({
@@ -459,15 +448,15 @@ export const EditFileInputSchema = z.strictObject({
459
448
  .array(z.strictObject({
460
449
  oldText: z
461
450
  .string()
462
- .describe('Exact literal string to replace — must match character-for-character including whitespace and indentation. Include 3–5 lines of surrounding context to uniquely identify the location.'),
451
+ .describe('Exact literal string to replace (character-for-character). Include 3–5 lines of context for unique targeting.'),
463
452
  newText: z
464
453
  .string()
465
- .describe('Replacement string — preserve the indentation style of surrounding code.'),
454
+ .describe('Replacement string. Preserve surrounding indentation style.'),
466
455
  }))
467
456
  .min(1, 'Min 1 edit required')
468
457
  .describe('List of replacements to apply sequentially. Each edit replaces the first occurrence of oldText.'),
469
- dryRun: defaultFalseBoolean('Preview edits without writing. Check unmatchedEdits in the response to verify all oldText values were found.'),
470
- ignoreWhitespace: defaultFalseBoolean('Ignore leading/trailing whitespace and treat all whitespace sequences as equivalent when matching oldText.'),
458
+ dryRun: defaultFalseBoolean('Preview edits without writing. Check `unmatchedEdits` in response.'),
459
+ ignoreWhitespace: defaultFalseBoolean('Treat all whitespace sequences as equivalent when matching oldText.'),
471
460
  });
472
461
  export const EditFileOutputSchema = z.strictObject({
473
462
  ok: z.boolean(),
@@ -490,7 +479,7 @@ export const MoveFileInputSchema = z
490
479
  destination: RequiredPathSchema.describe('New path'),
491
480
  })
492
481
  .refine((data) => (data.source ?? data.sources) !== undefined, {
493
- message: "Either 'source' or 'sources' must be provided",
482
+ error: "Either 'source' or 'sources' must be provided",
494
483
  path: ['source'],
495
484
  });
496
485
  export const MoveFileOutputSchema = z.strictObject({
@@ -535,7 +524,6 @@ export const DiffFilesInputSchema = z.strictObject({
535
524
  original: RequiredPathSchema.describe('Path to original file'),
536
525
  modified: RequiredPathSchema.describe('Path to modified file'),
537
526
  context: z
538
- .number()
539
527
  .int({ error: 'Must be integer' })
540
528
  .min(0, 'Min: 0')
541
529
  .max(10000, 'Max: 10,000')
@@ -564,9 +552,8 @@ export const ApplyPatchInputSchema = z.strictObject({
564
552
  path: RequiredPathSchema.describe('Path to file to patch'),
565
553
  patch: z
566
554
  .string()
567
- .describe('Unified diff content to apply — must include @@ hunk headers. Generate with `diff_files`.'),
555
+ .describe('Unified diff with @@ hunk headers. Generate with `diff_files`.'),
568
556
  fuzzFactor: z
569
- .number()
570
557
  .int({ error: 'Must be integer' })
571
558
  .min(0, 'Min: 0')
572
559
  .max(20, 'Max: 20')
@@ -581,7 +568,7 @@ export const ApplyPatchInputSchema = z.strictObject({
581
568
  .boolean()
582
569
  .optional()
583
570
  .default(false)
584
- .describe('Validate the patch can be applied without writing. Check `applied` in the response before committing.'),
571
+ .describe('Validate patch without writing. Check `applied` before committing.'),
585
572
  });
586
573
  export const ApplyPatchOutputSchema = z.strictObject({
587
574
  ok: z.boolean(),
@@ -602,18 +589,18 @@ export const SearchAndReplaceInputSchema = z.strictObject({
602
589
  searchPattern: z
603
590
  .string()
604
591
  .min(1, 'Search pattern required')
605
- .describe('Text to search for. Matched literally by default; treated as RE2 regex when isRegex is true.'),
592
+ .describe('Text to search for. Literal by default; RE2 regex when `isRegex=true`.'),
606
593
  replacement: z.string().describe('Replacement text'),
607
- isRegex: defaultFalseBoolean('Treat searchPattern as a RE2 regular expression. Supports capture group references ($1, $2) in replacement.'),
594
+ isRegex: defaultFalseBoolean('Treat searchPattern as RE2 regex. Supports capture groups ($1, $2) in replacement.'),
608
595
  dryRun: defaultFalseBoolean('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
609
596
  includeHidden: z
610
597
  .boolean()
611
598
  .optional()
612
- .describe('Include hidden files and directories (starting with .) in the search scope. Default: false.'),
599
+ .describe('Include hidden files/directories (starting with .). Default: false.'),
613
600
  includeIgnored: z
614
601
  .boolean()
615
602
  .optional()
616
- .describe('Include files and directories ignored by .gitignore rules (e.g. node_modules, dist). Default: false.'),
603
+ .describe('Include .gitignore-ignored files (node_modules, dist). Default: false.'),
617
604
  returnDiff: z
618
605
  .boolean()
619
606
  .optional()