@j0hanz/filesystem-mcp 1.9.1 → 1.11.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.
Files changed (45) hide show
  1. package/README.md +10 -10
  2. package/dist/completions.js +140 -115
  3. package/dist/lib/constants.d.ts +1 -0
  4. package/dist/lib/constants.js +2 -0
  5. package/dist/lib/file-operations/metadata.d.ts +5 -1
  6. package/dist/lib/file-operations/metadata.js +14 -1
  7. package/dist/lib/file-operations/search.d.ts +2 -0
  8. package/dist/lib/file-operations/search.js +59 -27
  9. package/dist/lib/fs-helpers.d.ts +3 -1
  10. package/dist/lib/fs-helpers.js +63 -0
  11. package/dist/lib/paths.js +79 -53
  12. package/dist/lib/resource-store.d.ts +2 -0
  13. package/dist/lib/resource-store.js +58 -17
  14. package/dist/prompts.d.ts +2 -0
  15. package/dist/prompts.js +51 -0
  16. package/dist/resources/generated-instructions.js +36 -9
  17. package/dist/resources/tool-catalog.js +30 -7
  18. package/dist/resources/tool-info.d.ts +4 -0
  19. package/dist/resources/tool-info.js +21 -3
  20. package/dist/resources/workflows.js +17 -5
  21. package/dist/schemas.d.ts +36 -1
  22. package/dist/schemas.js +73 -3
  23. package/dist/server/bootstrap.js +85 -65
  24. package/dist/tools/apply-patch.js +135 -31
  25. package/dist/tools/calculate-hash.js +13 -8
  26. package/dist/tools/create-directory.js +14 -3
  27. package/dist/tools/delete-file.js +1 -0
  28. package/dist/tools/diff-files.js +26 -8
  29. package/dist/tools/edit-file.js +11 -8
  30. package/dist/tools/list-directory.js +1 -6
  31. package/dist/tools/move-file.js +39 -7
  32. package/dist/tools/read-multiple.js +9 -1
  33. package/dist/tools/read.js +38 -6
  34. package/dist/tools/replace-in-files.js +72 -25
  35. package/dist/tools/roots.js +1 -0
  36. package/dist/tools/search-content.js +76 -48
  37. package/dist/tools/search-files.js +6 -7
  38. package/dist/tools/shared.d.ts +2 -1
  39. package/dist/tools/shared.js +36 -20
  40. package/dist/tools/stat-many.js +1 -1
  41. package/dist/tools/stat.js +4 -0
  42. package/dist/tools/task-support.js +4 -12
  43. package/dist/tools/tree.js +4 -0
  44. package/dist/tools/write-file.js +4 -2
  45. package/package.json +17 -8
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  [![Add to LM Studio](https://files.lmstudio.ai/deeplink/mcp-install-light.svg)](https://lmstudio.ai/install-mcp?name=filesystem-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBqMGhhbnovZmlsZXN5c3RlbS1tY3BAbGF0ZXN0Il19) [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=filesystem-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBqMGhhbnovZmlsZXN5c3RlbS1tY3BAbGF0ZXN0Il19) [![Install in Goose](https://block.github.io/goose/img/extension-install-dark.svg)](https://block.github.io/goose/extension?cmd=npx&arg=-y&arg=%40j0hanz%2Ffilesystem-mcp%40latest&id=%40j0hanz%2Ffilesystem-mcp&name=filesystem-mcp&description=MCP%20Server%20that%20enables%20LLMs%20to%20interact%20with%20the%20local%20filesystem.)
8
8
 
9
- MCP Server that enables LLMs to interact with the local filesystem. Provides 18 tools for navigation, search, file management, and analysis all scoped to allowed directories.
9
+ A local filesystem MCP server that lets LLMs and AI agents read, write, search, diff, patch, and manage files safely and efficiently. Built for reliable, structured, and controlled filesystem interaction.
10
10
 
11
11
  ## Overview
12
12
 
@@ -484,7 +484,7 @@ Make precise, targeted edits to source files. Use `edit` for surgical replacemen
484
484
 
485
485
  ### Diff and Patch Workflow
486
486
 
487
- Compare file versions and apply patches. Generate a unified diff with `diff_files`, preview with `apply_patch(dryRun: true)`, then apply. Useful for code review assistance and conflict resolution.
487
+ Compare file versions and apply patches. Generate a unified diff with `diff_files`, preview with `apply_patch(dryRun: true)`, then apply. Supports both single-file and multi-file patches (best-effort per file with per-file `results[]`).
488
488
 
489
489
  **Relevant tools:** `diff_files`, `apply_patch`
490
490
 
@@ -762,15 +762,15 @@ Generate a unified diff between two files. Output feeds directly into `apply_pat
762
762
 
763
763
  #### `apply_patch`
764
764
 
765
- Apply a unified diff patch to a file. Workflow: `diff_files` -> `apply_patch(dryRun)` -> `apply_patch`.
765
+ Apply a unified diff patch to one or more files. Single-file: throws on failure. Multi-file: best-effort per file with `results[]`. Workflow: `diff_files` -> `apply_patch(dryRun)` -> `apply_patch`.
766
766
 
767
- | Parameter | Type | Required | Description |
768
- | ------------------------ | ------- | -------- | ------------------------------------------ |
769
- | `path` | string | **yes** | Path to file to patch |
770
- | `patch` | string | **yes** | Unified diff with `@@` hunk headers |
771
- | `fuzzFactor` | integer | no | Max fuzzy mismatches per hunk (0-20) |
772
- | `autoConvertLineEndings` | boolean | no | Auto-convert line endings. Default: `true` |
773
- | `dryRun` | boolean | no | Validate without writing. Default: `false` |
767
+ | Parameter | Type | Required | Description |
768
+ | ------------------------ | ------- | -------- | ---------------------------------------------------------- |
769
+ | `path` | string | **yes** | Path to file (single) or base directory (multi-file patch) |
770
+ | `patch` | string | **yes** | Unified diff with `@@` hunk headers (single or multi-file) |
771
+ | `fuzzFactor` | integer | no | Max fuzzy mismatches per hunk (0-20) |
772
+ | `autoConvertLineEndings` | boolean | no | Auto-convert line endings. Default: `true` |
773
+ | `dryRun` | boolean | no | Validate without writing. Default: `false` |
774
774
 
775
775
  ---
776
776
 
@@ -5,6 +5,7 @@ import { getAllowedDirectories, isPathWithinDirectories, normalizePath, toPosixP
5
5
  import { isRecord } from './lib/utils.js';
6
6
  const MAX_COMPLETION_ITEMS = 100;
7
7
  const COMPLETION_RATE_LIMIT_MS = 100;
8
+ const MAX_COMPLETION_CACHE_KEYS = 128;
8
9
  // WeakMap keyed by McpServer instance so that each HTTP session gets isolated
9
10
  // rate-limit state. In stdio mode there is a single server; in HTTP mode every
10
11
  // session creates its own McpServer, so cross-session cache pollution is avoided.
@@ -39,6 +40,9 @@ const PATH_ARGUMENTS = new Set([
39
40
  'root',
40
41
  'cwd',
41
42
  ]);
43
+ const DESTINATION_CONTEXT_KEYS = ['source', 'path', 'cwd', 'root'];
44
+ const PRIMARY_PATH_CONTEXT_KEYS = ['path', 'cwd', 'root'];
45
+ const DEFAULT_CONTEXT_KEYS = ['path', 'source', 'cwd', 'root'];
42
46
  function isPathLikeArgumentName(argName) {
43
47
  return (PATH_ARGUMENTS.has(argName) ||
44
48
  argName.endsWith('paths') ||
@@ -48,6 +52,22 @@ function isPathLikeArgumentName(argName) {
48
52
  argName.endsWith('dirs') ||
49
53
  argName.endsWith('dir'));
50
54
  }
55
+ function isTemplateVariableChar(char) {
56
+ const code = char.charCodeAt(0);
57
+ const isDigit = code >= 48 && code <= 57;
58
+ const isUpper = code >= 65 && code <= 90;
59
+ const isLower = code >= 97 && code <= 122;
60
+ return isDigit || isUpper || isLower || code === 95;
61
+ }
62
+ function normalizeTemplateVariable(raw) {
63
+ let normalized = '';
64
+ for (const char of raw) {
65
+ if (isTemplateVariableChar(char)) {
66
+ normalized += char.toLowerCase();
67
+ }
68
+ }
69
+ return normalized;
70
+ }
51
71
  function parseResourceReference(value) {
52
72
  if (!isRecord(value))
53
73
  return undefined;
@@ -60,13 +80,6 @@ function parseResourceReference(value) {
60
80
  }
61
81
  function extractTemplateVariables(uri) {
62
82
  const vars = [];
63
- const isVariableChar = (char) => {
64
- const code = char.charCodeAt(0);
65
- const isDigit = code >= 48 && code <= 57;
66
- const isUpper = code >= 65 && code <= 90;
67
- const isLower = code >= 97 && code <= 122;
68
- return isDigit || isUpper || isLower || code === 95;
69
- };
70
83
  let index = 0;
71
84
  while (index < uri.length) {
72
85
  const start = uri.indexOf('{', index);
@@ -75,13 +88,7 @@ function extractTemplateVariables(uri) {
75
88
  const end = uri.indexOf('}', start + 1);
76
89
  if (end === -1)
77
90
  break;
78
- const raw = uri.slice(start + 1, end);
79
- let normalized = '';
80
- for (const char of raw) {
81
- if (isVariableChar(char)) {
82
- normalized += char.toLowerCase();
83
- }
84
- }
91
+ const normalized = normalizeTemplateVariable(uri.slice(start + 1, end));
85
92
  if (normalized.length > 0)
86
93
  vars.push(normalized);
87
94
  index = end + 1;
@@ -129,6 +136,43 @@ function extractContextArguments(value) {
129
136
  return undefined;
130
137
  return normalized;
131
138
  }
139
+ function serializeCompletionRef(ref) {
140
+ if (!isRecord(ref) || typeof ref['type'] !== 'string') {
141
+ return { type: 'unknown' };
142
+ }
143
+ if (ref['type'] === 'ref/prompt' && typeof ref['name'] === 'string') {
144
+ return { type: ref['type'], name: ref['name'] };
145
+ }
146
+ if (ref['type'] === 'ref/resource' && typeof ref['uri'] === 'string') {
147
+ return { type: ref['type'], uri: ref['uri'] };
148
+ }
149
+ return { type: ref['type'] };
150
+ }
151
+ function serializeContextArguments(contextArguments) {
152
+ if (!contextArguments)
153
+ return [];
154
+ return Object.entries(contextArguments).sort(([left], [right]) => left.localeCompare(right));
155
+ }
156
+ function buildCompletionCacheKey(params) {
157
+ return JSON.stringify({
158
+ argumentName: params.argumentName.toLowerCase(),
159
+ value: params.value,
160
+ ref: serializeCompletionRef(params.ref),
161
+ contextArguments: serializeContextArguments(params.contextArguments),
162
+ });
163
+ }
164
+ function rememberCompletionCacheValue(cache, key, value) {
165
+ if (cache.has(key)) {
166
+ cache.delete(key);
167
+ }
168
+ cache.set(key, value);
169
+ while (cache.size > MAX_COMPLETION_CACHE_KEYS) {
170
+ const oldest = cache.keys().next();
171
+ if (oldest.done)
172
+ break;
173
+ cache.delete(oldest.value);
174
+ }
175
+ }
132
176
  function hasTrailingSeparator(value) {
133
177
  return (value.endsWith(path.sep) || value.endsWith('/') || value.endsWith('\\'));
134
178
  }
@@ -180,16 +224,19 @@ function findAllowedRootByName(rootName, allowed) {
180
224
  function chooseContextKeys(argumentName) {
181
225
  const normalized = argumentName.toLowerCase();
182
226
  if (normalized === 'destination') {
183
- return ['source', 'path', 'cwd', 'root'];
227
+ return [...DESTINATION_CONTEXT_KEYS];
184
228
  }
185
229
  if (normalized === 'path' ||
186
230
  normalized === 'source' ||
187
231
  normalized === 'original' ||
188
232
  normalized === 'modified' ||
189
233
  normalized === 'file') {
190
- return ['path', 'cwd', 'root'];
234
+ return [...PRIMARY_PATH_CONTEXT_KEYS];
191
235
  }
192
- return ['path', 'source', 'cwd', 'root'];
236
+ return [...DEFAULT_CONTEXT_KEYS];
237
+ }
238
+ function hasContextArguments(contextArguments) {
239
+ return (contextArguments !== undefined && Object.keys(contextArguments).length > 0);
193
240
  }
194
241
  function resolveContextCandidatePath(candidate, allowed) {
195
242
  if (isAbsolutePathInput(candidate)) {
@@ -218,17 +265,7 @@ async function toAllowedContextDirectory(resolved, allowed) {
218
265
  return isPathWithinDirectories(parent, allowed) ? parent : undefined;
219
266
  }
220
267
  async function resolveContextBaseDirectory(argumentName, contextArguments, allowed) {
221
- if (!contextArguments) {
222
- return undefined;
223
- }
224
- let hasContextArgument = false;
225
- for (const key in contextArguments) {
226
- if (!Object.prototype.hasOwnProperty.call(contextArguments, key))
227
- continue;
228
- hasContextArgument = true;
229
- break;
230
- }
231
- if (!hasContextArgument) {
268
+ if (!hasContextArguments(contextArguments)) {
232
269
  return undefined;
233
270
  }
234
271
  const keys = chooseContextKeys(argumentName);
@@ -245,6 +282,54 @@ async function resolveContextBaseDirectory(argumentName, contextArguments, allow
245
282
  }
246
283
  return undefined;
247
284
  }
285
+ function withDirectorySeparator(value) {
286
+ return value.endsWith(path.sep) ? value : `${value}${path.sep}`;
287
+ }
288
+ function buildCompletionResult(values) {
289
+ return {
290
+ values: values.slice(0, MAX_COMPLETION_ITEMS),
291
+ total: values.length,
292
+ hasMore: values.length > MAX_COMPLETION_ITEMS,
293
+ };
294
+ }
295
+ function buildCompletionResponse(result) {
296
+ return { completion: result };
297
+ }
298
+ function sortCompletionMatches(matches) {
299
+ matches.sort((left, right) => {
300
+ const leftIsDir = left.endsWith(path.sep);
301
+ const rightIsDir = right.endsWith(path.sep);
302
+ if (leftIsDir && !rightIsDir)
303
+ return -1;
304
+ if (!leftIsDir && rightIsDir)
305
+ return 1;
306
+ return left.localeCompare(right);
307
+ });
308
+ }
309
+ function mergeCompletionMatches(...matchGroups) {
310
+ const uniqueMatches = new Set();
311
+ for (const group of matchGroups) {
312
+ for (const match of group) {
313
+ uniqueMatches.add(match);
314
+ }
315
+ }
316
+ const merged = [...uniqueMatches];
317
+ sortCompletionMatches(merged);
318
+ return merged;
319
+ }
320
+ function getRootPrefix(currentValue) {
321
+ const normalizedInput = toPosixPath(currentValue);
322
+ return (normalizedInput.split('/')[0] ?? '').toLowerCase();
323
+ }
324
+ function collectAllowedRoots(allowed, predicate) {
325
+ const matches = [];
326
+ for (const root of allowed) {
327
+ if (predicate(root)) {
328
+ matches.push(withDirectorySeparator(root));
329
+ }
330
+ }
331
+ return matches;
332
+ }
248
333
  function getSearchContext(currentValue, allowed, contextBase) {
249
334
  const trailingSeparator = hasTrailingSeparator(currentValue);
250
335
  if (isAbsolutePathInput(currentValue)) {
@@ -290,38 +375,22 @@ async function findMatchesInDirectory(searchDir, prefix, allowed) {
290
375
  return matches;
291
376
  }
292
377
  function findRootPrefixMatches(currentValue, allowed) {
293
- const normalizedInput = toPosixPath(currentValue);
294
- const rootPrefix = (normalizedInput.split('/')[0] ?? '').toLowerCase();
378
+ const rootPrefix = getRootPrefix(currentValue);
295
379
  if (!rootPrefix) {
296
- const matches = [];
297
- for (const root of allowed) {
298
- matches.push(`${root}${path.sep}`);
299
- }
300
- return matches;
380
+ return collectAllowedRoots(allowed, () => true);
301
381
  }
302
- const matches = [];
303
- for (const root of allowed) {
304
- if (!path.basename(root).toLowerCase().startsWith(rootPrefix))
305
- continue;
306
- matches.push(`${root}${path.sep}`);
307
- }
308
- return matches;
382
+ return collectAllowedRoots(allowed, (root) => path.basename(root).toLowerCase().startsWith(rootPrefix));
309
383
  }
310
384
  function findMatchingRoots(searchDir, prefix, allowed) {
311
- const matches = [];
312
385
  const lowerPrefix = prefix.toLowerCase();
313
386
  const normalizedSearchDir = normalizePath(searchDir);
314
- for (const root of allowed) {
387
+ return collectAllowedRoots(allowed, (root) => {
315
388
  const rootDir = path.dirname(root);
316
389
  // Check if root is a direct child of searchDir
317
- if (normalizePath(rootDir) === normalizedSearchDir) {
318
- const rootName = path.basename(root);
319
- if (rootName.toLowerCase().startsWith(lowerPrefix)) {
320
- matches.push(`${root}${path.sep}`);
321
- }
322
- }
323
- }
324
- return matches;
390
+ if (normalizePath(rootDir) !== normalizedSearchDir)
391
+ return false;
392
+ return path.basename(root).toLowerCase().startsWith(lowerPrefix);
393
+ });
325
394
  }
326
395
  export async function getPathCompletions(currentValue, options = {}) {
327
396
  const allowed = getAllowedDirectories();
@@ -329,47 +398,16 @@ export async function getPathCompletions(currentValue, options = {}) {
329
398
  const contextBase = await resolveContextBaseDirectory(options.argumentName ?? '', options.contextArguments, allowed);
330
399
  // If no value and no context base, suggest roots.
331
400
  if (!currentValue && !contextBase) {
332
- return {
333
- values: allowed,
334
- total: allowed.length,
335
- hasMore: false,
336
- };
401
+ return buildCompletionResult(allowed);
337
402
  }
338
403
  const context = getSearchContext(currentValue, allowed, contextBase);
339
404
  if (!context) {
340
- const rootMatches = findRootPrefixMatches(currentValue, allowed);
341
- const sliced = rootMatches.slice(0, MAX_COMPLETION_ITEMS);
342
- return {
343
- values: sliced,
344
- total: rootMatches.length,
345
- hasMore: rootMatches.length > MAX_COMPLETION_ITEMS,
346
- };
405
+ return buildCompletionResult(findRootPrefixMatches(currentValue, allowed));
347
406
  }
348
407
  const { searchDir, prefix } = context;
349
408
  const dirMatches = await findMatchesInDirectory(searchDir, prefix, allowed);
350
409
  const rootMatches = findMatchingRoots(searchDir, prefix, allowed);
351
- // Deduplicate and sort
352
- const unique = new Set();
353
- for (const match of dirMatches)
354
- unique.add(match);
355
- for (const match of rootMatches)
356
- unique.add(match);
357
- const uniqueMatches = Array.from(unique);
358
- uniqueMatches.sort((a, b) => {
359
- const aIsDir = a.endsWith(path.sep);
360
- const bIsDir = b.endsWith(path.sep);
361
- if (aIsDir && !bIsDir)
362
- return -1;
363
- if (!aIsDir && bIsDir)
364
- return 1;
365
- return a.localeCompare(b);
366
- });
367
- const sliced = uniqueMatches.slice(0, MAX_COMPLETION_ITEMS);
368
- return {
369
- values: sliced,
370
- total: uniqueMatches.length,
371
- hasMore: uniqueMatches.length > MAX_COMPLETION_ITEMS,
372
- };
410
+ return buildCompletionResult(mergeCompletionMatches(dirMatches, rootMatches));
373
411
  }
374
412
  catch {
375
413
  return { values: [] };
@@ -387,50 +425,37 @@ export function registerCompletions(server, instructions = '') {
387
425
  const filtered = currentValue
388
426
  ? topicValues.filter((v) => v.startsWith(currentValue))
389
427
  : topicValues;
390
- const sliced = filtered.slice(0, MAX_COMPLETION_ITEMS);
391
- return {
392
- completion: {
393
- values: sliced,
394
- total: filtered.length,
395
- hasMore: filtered.length > MAX_COMPLETION_ITEMS,
396
- },
397
- };
428
+ return buildCompletionResponse(buildCompletionResult(filtered));
398
429
  }
399
430
  const isPathArg = isPathLikeArgumentName(argName) ||
400
431
  isPathArgumentFromReference(argName, ref);
401
432
  if (!isPathArg) {
402
- return { completion: { values: [], total: 0, hasMore: false } };
433
+ return buildCompletionResponse(buildCompletionResult([]));
403
434
  }
435
+ const contextArguments = extractContextArguments(params.context);
436
+ const cacheKey = buildCompletionCacheKey({
437
+ argumentName: argName,
438
+ value: argument.value,
439
+ ref,
440
+ ...(contextArguments ? { contextArguments } : {}),
441
+ });
404
442
  const now = Date.now();
405
443
  const sessionState = getCompletionState(server);
406
- const lastCallMs = sessionState.lastCallMs.get(argName) ?? 0;
444
+ const lastCallMs = sessionState.lastCallMs.get(cacheKey) ?? 0;
407
445
  if (now - lastCallMs < COMPLETION_RATE_LIMIT_MS) {
408
- const lastResult = sessionState.lastResult.get(argName);
446
+ const lastResult = sessionState.lastResult.get(cacheKey);
409
447
  if (lastResult) {
410
- return {
411
- completion: {
412
- values: lastResult.values,
413
- total: lastResult.total,
414
- hasMore: lastResult.hasMore,
415
- },
416
- };
448
+ return buildCompletionResponse(lastResult);
417
449
  }
418
- return { completion: { values: [], total: 0, hasMore: false } };
450
+ return buildCompletionResponse(buildCompletionResult([]));
419
451
  }
420
- sessionState.lastCallMs.set(argName, now);
421
- const contextArguments = extractContextArguments(params.context);
452
+ rememberCompletionCacheValue(sessionState.lastCallMs, cacheKey, now);
422
453
  const { value } = argument;
423
454
  const completions = await getPathCompletions(value, {
424
455
  argumentName: argName,
425
456
  ...(contextArguments ? { contextArguments } : {}),
426
457
  });
427
- sessionState.lastResult.set(argName, completions);
428
- return {
429
- completion: {
430
- values: completions.values,
431
- total: completions.total,
432
- hasMore: completions.hasMore,
433
- },
434
- };
458
+ rememberCompletionCacheValue(sessionState.lastResult, cacheKey, completions);
459
+ return buildCompletionResponse(completions);
435
460
  });
436
461
  }
@@ -2,6 +2,7 @@ export declare function parseTrueEnvFlag(value: string | undefined): boolean;
2
2
  export declare function parseEnvInt(envVar: string, defaultValue: number, min: number, max: number): number;
3
3
  export declare const DEFAULT_LOG_LEVEL: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
4
4
  export declare const REQUIRED_MCP_PROTOCOL_VERSION = "2025-11-25";
5
+ export declare const DEFAULT_TASK_TTL_MS: number;
5
6
  export declare const PARALLEL_CONCURRENCY: number;
6
7
  export declare const MAX_SEARCHABLE_FILE_SIZE: number;
7
8
  export declare const MAX_TEXT_FILE_SIZE: number;
@@ -71,6 +71,8 @@ function parseEnvLogLevel(envVar, defaultValue) {
71
71
  }
72
72
  export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'info');
73
73
  export const REQUIRED_MCP_PROTOCOL_VERSION = '2025-11-25';
74
+ // Default TTL for MCP tasks when the client does not specify one (5 minutes).
75
+ export const DEFAULT_TASK_TTL_MS = 5 * 60 * 1000;
74
76
  // Auto-tuned parallelism based on CPU cores (no env override)
75
77
  const BYTES_PER_PARALLEL_TASK = 64 * MIB;
76
78
  const BYTES_PER_SEARCH_WORKER = 128 * MIB;
@@ -24,6 +24,7 @@ interface TreeEntry {
24
24
  name: string;
25
25
  type: EntryType;
26
26
  relativePath: string;
27
+ size?: number;
27
28
  children?: TreeEntry[];
28
29
  }
29
30
  interface TreeOptions {
@@ -31,6 +32,7 @@ interface TreeOptions {
31
32
  maxEntries?: number;
32
33
  includeHidden?: boolean;
33
34
  includeIgnored?: boolean;
35
+ includeSizes?: boolean;
34
36
  timeoutMs?: number;
35
37
  signal?: AbortSignal;
36
38
  onProgress?: (progress: {
@@ -51,8 +53,9 @@ interface ReadMultipleResult {
51
53
  content?: string;
52
54
  truncated?: boolean;
53
55
  totalLines?: number;
54
- readMode?: 'full' | 'head' | 'range';
56
+ readMode?: 'full' | 'head' | 'tail' | 'range';
55
57
  head?: number;
58
+ tail?: number;
56
59
  startLine?: number;
57
60
  endLine?: number;
58
61
  linesRead?: number;
@@ -64,6 +67,7 @@ interface ReadMultipleOptions {
64
67
  maxSize?: number;
65
68
  maxTotalSize?: number;
66
69
  head?: number;
70
+ tail?: number;
67
71
  startLine?: number;
68
72
  endLine?: number;
69
73
  signal?: AbortSignal;
@@ -408,6 +408,7 @@ function normalizeTreeOptions(options) {
408
408
  maxEntries: toSafeNonNegativeInt(options.maxEntries, 1000),
409
409
  includeHidden: toSafeBoolean(options.includeHidden, false),
410
410
  includeIgnored: toSafeBoolean(options.includeIgnored, false),
411
+ includeSizes: toSafeBoolean(options.includeSizes, false),
411
412
  timeoutMs: toSafePositiveInt(options.timeoutMs, DEFAULT_SEARCH_TIMEOUT_MS),
412
413
  };
413
414
  }
@@ -608,7 +609,7 @@ export async function treeDirectory(dirPath, options = {}) {
608
609
  maxDepth: normalized.maxDepth,
609
610
  followSymbolicLinks: false,
610
611
  onlyFiles: false,
611
- stats: false,
612
+ stats: normalized.includeSizes,
612
613
  suppressErrors: true,
613
614
  });
614
615
  for await (const entry of stream) {
@@ -629,6 +630,14 @@ export async function treeDirectory(dirPath, options = {}) {
629
630
  }
630
631
  const parent = ensureParentNodes(rootNode, nodeByPath, resolved.relativePosix);
631
632
  upsertChildNode(parent, nodeByPath, resolved, childPathIndexByParent);
633
+ if (normalized.includeSizes &&
634
+ resolved.type === 'file' &&
635
+ entry.stats) {
636
+ const node = nodeByPath.get(resolved.relativePosix);
637
+ if (node) {
638
+ node.size = entry.stats.size;
639
+ }
640
+ }
632
641
  totalEntries += 1;
633
642
  options.onProgress?.({ current: totalEntries });
634
643
  }
@@ -665,6 +674,8 @@ function buildReadMultipleResult(filePath, result) {
665
674
  output.totalLines = result.totalLines;
666
675
  if (result.head !== undefined)
667
676
  output.head = result.head;
677
+ if (result.tail !== undefined)
678
+ output.tail = result.tail;
668
679
  if (result.startLine !== undefined)
669
680
  output.startLine = result.startLine;
670
681
  if (result.endLine !== undefined)
@@ -709,6 +720,8 @@ function normalizeReadMultipleOptions(options) {
709
720
  function applyLineSelection(target, source) {
710
721
  if (source.head !== undefined)
711
722
  target.head = source.head;
723
+ if (source.tail !== undefined)
724
+ target.tail = source.tail;
712
725
  if (source.startLine !== undefined)
713
726
  target.startLine = source.startLine;
714
727
  if (source.endLine !== undefined)
@@ -5,6 +5,7 @@ export declare const MatcherOptionsSchema: z.ZodObject<{
5
5
  caseSensitive: z.ZodBoolean;
6
6
  wholeWord: z.ZodBoolean;
7
7
  isLiteral: z.ZodBoolean;
8
+ multiline: z.ZodBoolean;
8
9
  }, z.core.$strict>;
9
10
  export type MatcherOptions = z.infer<typeof MatcherOptionsSchema>;
10
11
  export type Matcher = (line: string) => number;
@@ -27,6 +28,7 @@ declare const SearchOptionsSchema: z.ZodObject<{
27
28
  contextLines: z.ZodInt;
28
29
  wholeWord: z.ZodBoolean;
29
30
  isLiteral: z.ZodBoolean;
31
+ multiline: z.ZodBoolean;
30
32
  includeHidden: z.ZodBoolean;
31
33
  baseNameMatch: z.ZodBoolean;
32
34
  caseSensitiveFileMatch: z.ZodBoolean;