@j0hanz/filesystem-mcp 1.11.0 → 1.12.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 (42) hide show
  1. package/dist/completions.js +25 -0
  2. package/dist/lib/constants.d.ts +2 -0
  3. package/dist/lib/constants.js +2 -0
  4. package/dist/lib/file-operations/metadata.js +71 -142
  5. package/dist/lib/file-operations/search.js +119 -19
  6. package/dist/lib/file-operations/traversal.js +45 -102
  7. package/dist/lib/fs-helpers.js +175 -158
  8. package/dist/prompts.d.ts +1 -0
  9. package/dist/prompts.js +58 -0
  10. package/dist/resources/generated-instructions.js +11 -3
  11. package/dist/resources/tool-catalog.js +49 -3
  12. package/dist/resources/tool-info.d.ts +1 -0
  13. package/dist/resources/tool-info.js +139 -8
  14. package/dist/resources.js +2 -0
  15. package/dist/schemas.d.ts +2 -0
  16. package/dist/schemas.js +10 -4
  17. package/dist/server/bootstrap.js +41 -5
  18. package/dist/tools/apply-patch.js +30 -33
  19. package/dist/tools/calculate-hash.js +21 -24
  20. package/dist/tools/contract.d.ts +1 -1
  21. package/dist/tools/create-directory.js +3 -3
  22. package/dist/tools/delete-file.js +2 -2
  23. package/dist/tools/diff-files.js +32 -20
  24. package/dist/tools/edit-file.d.ts +4 -1
  25. package/dist/tools/edit-file.js +172 -104
  26. package/dist/tools/list-directory.js +105 -10
  27. package/dist/tools/move-file.js +3 -3
  28. package/dist/tools/read-multiple.d.ts +1 -1
  29. package/dist/tools/read-multiple.js +123 -99
  30. package/dist/tools/read.js +104 -76
  31. package/dist/tools/replace-in-files.d.ts +5 -2
  32. package/dist/tools/replace-in-files.js +142 -76
  33. package/dist/tools/roots.js +1 -1
  34. package/dist/tools/search-content.js +249 -218
  35. package/dist/tools/shared.js +39 -7
  36. package/dist/tools/stat-many.js +12 -19
  37. package/dist/tools/stat.js +1 -1
  38. package/dist/tools/task-support.d.ts +7 -0
  39. package/dist/tools/task-support.js +140 -101
  40. package/dist/tools/write-file.js +4 -4
  41. package/dist/tools.js +2 -2
  42. package/package.json +1 -1
@@ -3,6 +3,7 @@ import * as path from 'node:path';
3
3
  import { CompleteRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
4
  import { getAllowedDirectories, isPathWithinDirectories, normalizePath, toPosixPath, } from './lib/paths.js';
5
5
  import { isRecord } from './lib/utils.js';
6
+ import { getSortedToolContracts } from './resources/tool-info.js';
6
7
  const MAX_COMPLETION_ITEMS = 100;
7
8
  const COMPLETION_RATE_LIMIT_MS = 100;
8
9
  const MAX_COMPLETION_CACHE_KEYS = 128;
@@ -29,6 +30,9 @@ function extractTopicCompletions(instructions) {
29
30
  }
30
31
  return headers;
31
32
  }
33
+ function extractToolNameCompletions() {
34
+ return getSortedToolContracts().map((contract) => contract.name);
35
+ }
32
36
  const PATH_ARGUMENTS = new Set([
33
37
  'path',
34
38
  'source',
@@ -415,6 +419,7 @@ export async function getPathCompletions(currentValue, options = {}) {
415
419
  }
416
420
  export function registerCompletions(server, instructions = '') {
417
421
  const topicValues = extractTopicCompletions(instructions);
422
+ const toolNameValues = extractToolNameCompletions();
418
423
  server.server.setRequestHandler(CompleteRequestSchema, async (request) => {
419
424
  const { params } = request;
420
425
  const { argument, ref } = params;
@@ -427,6 +432,26 @@ export function registerCompletions(server, instructions = '') {
427
432
  : topicValues;
428
433
  return buildCompletionResponse(buildCompletionResult(filtered));
429
434
  }
435
+ if (isRecord(ref) &&
436
+ ref['type'] === 'ref/prompt' &&
437
+ ref['name'] === 'get-tool-help' &&
438
+ argName === 'name') {
439
+ const currentValue = argument.value.toLowerCase();
440
+ const filtered = currentValue
441
+ ? toolNameValues.filter((value) => value.startsWith(currentValue))
442
+ : toolNameValues;
443
+ return buildCompletionResponse(buildCompletionResult(filtered));
444
+ }
445
+ if (isRecord(ref) &&
446
+ ref['type'] === 'ref/resource' &&
447
+ ref['uri'] === 'internal://tool-info/{name}' &&
448
+ argName === 'name') {
449
+ const currentValue = argument.value.toLowerCase();
450
+ const filtered = currentValue
451
+ ? toolNameValues.filter((value) => value.startsWith(currentValue))
452
+ : toolNameValues;
453
+ return buildCompletionResponse(buildCompletionResult(filtered));
454
+ }
430
455
  const isPathArg = isPathLikeArgumentName(argName) ||
431
456
  isPathArgumentFromReference(argName, ref);
432
457
  if (!isPathArg) {
@@ -3,6 +3,8 @@ export declare function parseEnvInt(envVar: string, defaultValue: number, min: n
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
5
  export declare const DEFAULT_TASK_TTL_MS: number;
6
+ export declare const MAX_TASK_TTL_MS: number;
7
+ export declare const MAX_CONCURRENT_TASKS: number;
6
8
  export declare const PARALLEL_CONCURRENCY: number;
7
9
  export declare const MAX_SEARCHABLE_FILE_SIZE: number;
8
10
  export declare const MAX_TEXT_FILE_SIZE: number;
@@ -73,6 +73,8 @@ export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'i
73
73
  export const REQUIRED_MCP_PROTOCOL_VERSION = '2025-11-25';
74
74
  // Default TTL for MCP tasks when the client does not specify one (5 minutes).
75
75
  export const DEFAULT_TASK_TTL_MS = 5 * 60 * 1000;
76
+ export const MAX_TASK_TTL_MS = parseEnvInt('FILESYSTEM_MCP_MAX_TASK_TTL_MS', 60 * 60 * 1000, 1_000, 24 * 60 * 60 * 1000);
77
+ export const MAX_CONCURRENT_TASKS = parseEnvInt('FILESYSTEM_MCP_MAX_CONCURRENT_TASKS', 100, 1, 10_000);
76
78
  // Auto-tuned parallelism based on CPU cores (no env override)
77
79
  const BYTES_PER_PARALLEL_TASK = 64 * MIB;
78
80
  const BYTES_PER_SEARCH_WORKER = 128 * MIB;
@@ -6,6 +6,12 @@ import { assertNotAborted, getFileType, isHidden, processInParallel, readFile, r
6
6
  import { assertAllowedFileAccess, isPathWithinDirectories, isSensitivePath, normalizePath, toPosixPath, validateExistingDirectory, validateExistingPath, validateExistingPathDetailed, } from '../paths.js';
7
7
  import { applyIndexedErrors, applyIndexedValues, isEntryAccessibleByType, isIgnoredByGitignore, loadRootGitignore, needsStatsForSort, resolveEntryType, resolveStopReason, withOptionalStoppedReason, } from './core.js';
8
8
  import { globEntries } from './traversal.js';
9
+ const ACCESS_DEPS = {
10
+ normalizePath,
11
+ isPathWithinDirectories,
12
+ isSensitivePath,
13
+ validateSymlinkPath: validateExistingPathDetailed,
14
+ };
9
15
  const PERM_STRINGS = [
10
16
  '---',
11
17
  '--x',
@@ -18,13 +24,9 @@ const PERM_STRINGS = [
18
24
  ];
19
25
  const UNKNOWN_PATH = '(unknown)';
20
26
  function getPermissions(mode) {
21
- const ownerIndex = (mode >> 6) & 0b111;
22
- const groupIndex = (mode >> 3) & 0b111;
23
- const otherIndex = mode & 0b111;
24
- const owner = PERM_STRINGS[ownerIndex] ?? '---';
25
- const group = PERM_STRINGS[groupIndex] ?? '---';
26
- const other = PERM_STRINGS[otherIndex] ?? '---';
27
- return `${owner}${group}${other}`;
27
+ return ((PERM_STRINGS[(mode >> 6) & 0b111] ?? '---') +
28
+ (PERM_STRINGS[(mode >> 3) & 0b111] ?? '---') +
29
+ (PERM_STRINGS[mode & 0b111] ?? '---'));
28
30
  }
29
31
  function buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, symlinkTarget) {
30
32
  const tokenEstimate = stats.isFile() ? Math.ceil(stats.size / 4) : undefined;
@@ -59,8 +61,8 @@ export async function getFileInfo(filePath, options = {}) {
59
61
  assertNotAborted(signal);
60
62
  const { requestedPath, resolvedPath, isSymlink } = await validateExistingPathDetailed(filePath, signal);
61
63
  assertAllowedFileAccess(requestedPath, resolvedPath);
62
- const name = path.basename(requestedPath);
63
- const ext = path.extname(name).toLowerCase();
64
+ const { base: name, ext: rawExt } = path.parse(requestedPath);
65
+ const ext = rawExt.toLowerCase();
64
66
  const includeMimeType = options.includeMimeType !== false;
65
67
  const mimeType = includeMimeType && ext.length > 0 ? getMimeType(ext) : undefined;
66
68
  const symlinkTarget = isSymlink
@@ -75,25 +77,21 @@ function buildEmptyResult() {
75
77
  summary: { total: 0, succeeded: 0, failed: 0, totalSize: 0 },
76
78
  };
77
79
  }
78
- async function processFileInfo(filePath, options) {
79
- const info = await getFileInfo(filePath, options);
80
- return { path: filePath, info };
81
- }
82
80
  function buildIndexedPathTasks(paths) {
83
81
  const tasks = [];
84
82
  for (let index = 0; index < paths.length; index += 1) {
85
83
  const filePath = paths[index];
86
- if (filePath === undefined)
87
- continue;
88
- tasks.push({ filePath, index });
84
+ if (filePath !== undefined) {
85
+ tasks.push({ filePath, index });
86
+ }
89
87
  }
90
88
  return tasks;
91
89
  }
92
90
  async function readFileInfoInParallel(paths, options) {
93
91
  return processInParallel(buildIndexedPathTasks(paths), async ({ filePath, index }) => {
94
- const value = await processFileInfo(filePath, options);
92
+ const info = await getFileInfo(filePath, options);
95
93
  options.onProgress?.();
96
- return { index, value };
94
+ return { index, value: { path: filePath, info } };
97
95
  }, PARALLEL_CONCURRENCY, options.signal);
98
96
  }
99
97
  function calculateSummary(results) {
@@ -119,10 +117,9 @@ function calculateSummary(results) {
119
117
  export async function getMultipleFileInfo(paths, options = {}) {
120
118
  if (paths.length === 0)
121
119
  return buildEmptyResult();
122
- const output = new Array(paths.length);
123
- for (let index = 0; index < paths.length; index += 1) {
124
- output[index] = { path: paths[index] ?? UNKNOWN_PATH };
125
- }
120
+ const output = Array.from(paths, (p) => ({
121
+ path: p,
122
+ }));
126
123
  const { results, errors } = await readFileInfoInParallel(paths, options);
127
124
  applyIndexedValues(output, results);
128
125
  applyIndexedErrors({
@@ -141,13 +138,7 @@ export async function getMultipleFileInfo(paths, options = {}) {
141
138
  summary: calculateSummary(output),
142
139
  };
143
140
  }
144
- function normalizePattern(pattern) {
145
- if (!pattern || pattern.length === 0)
146
- return undefined;
147
- return pattern;
148
- }
149
141
  function normalizeListOptions(options) {
150
- const pattern = normalizePattern(options.pattern);
151
142
  const normalized = {
152
143
  includeHidden: options.includeHidden ?? false,
153
144
  excludePatterns: options.excludePatterns ?? [],
@@ -156,71 +147,49 @@ function normalizeListOptions(options) {
156
147
  sortBy: options.sortBy ?? 'name',
157
148
  includeSymlinkTargets: options.includeSymlinkTargets ?? false,
158
149
  timeoutMs: options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
159
- ...(pattern !== undefined ? { pattern } : {}),
160
150
  };
151
+ if (options.pattern && options.pattern.length > 0) {
152
+ normalized.pattern = options.pattern;
153
+ }
161
154
  return normalized;
162
155
  }
156
+ const SORT_COMPARATORS = {
157
+ name: (a, b) => a.name.localeCompare(b.name),
158
+ type: (a, b) => a.type.localeCompare(b.type),
159
+ size: (a, b) => (a.size ?? 0) - (b.size ?? 0),
160
+ modified: (a, b) => (a.modified?.getTime() ?? 0) - (b.modified?.getTime() ?? 0),
161
+ };
163
162
  function sortEntries(entries, sortBy) {
164
- const compare = {
165
- name: (a, b) => a.name.localeCompare(b.name),
166
- type: (a, b) => a.type.localeCompare(b.type),
167
- size: (a, b) => (a.size ?? 0) - (b.size ?? 0),
168
- modified: (a, b) => (a.modified?.getTime() ?? 0) - (b.modified?.getTime() ?? 0),
169
- }[sortBy];
170
- entries.sort(compare);
163
+ entries.sort(SORT_COMPARATORS[sortBy]);
171
164
  }
172
165
  function resolveMaxDepth(normalized) {
173
- if (!normalized.pattern) {
174
- return 1;
175
- }
176
- return normalized.maxDepth;
166
+ return normalized.pattern ? normalized.maxDepth : 1;
177
167
  }
178
168
  async function* readDirectoryEntries(basePath, normalized, needsStats, signal) {
179
169
  const dirents = await withAbort(fsp.readdir(basePath, { withFileTypes: true }), signal);
180
- const entries = [];
181
- for (const dirent of dirents) {
182
- if (!normalized.includeHidden && isHidden(dirent.name)) {
183
- continue;
184
- }
185
- entries.push({ dirent, entryPath: path.join(basePath, dirent.name) });
186
- }
187
170
  if (!needsStats) {
188
- for (const entry of entries) {
189
- yield {
190
- path: entry.entryPath,
191
- dirent: entry.dirent,
192
- };
171
+ for (const dirent of dirents) {
172
+ if (!normalized.includeHidden && isHidden(dirent.name))
173
+ continue;
174
+ yield { path: path.join(basePath, dirent.name), dirent };
193
175
  }
194
176
  return;
195
177
  }
196
- const { results, errors } = await processInParallel(entries.map((_, index) => index), async (index) => {
197
- const candidate = entries[index];
198
- if (!candidate) {
199
- throw new Error(`Entry index out of range: ${String(index)}`);
200
- }
201
- return {
202
- index,
203
- stats: await withAbort(fsp.lstat(candidate.entryPath), signal),
204
- };
205
- }, PARALLEL_CONCURRENCY, signal);
178
+ const filtered = [];
179
+ for (const dirent of dirents) {
180
+ if (!normalized.includeHidden && isHidden(dirent.name))
181
+ continue;
182
+ filtered.push({ dirent, entryPath: path.join(basePath, dirent.name) });
183
+ }
184
+ const { results, errors } = await processInParallel(filtered, async ({ entryPath, dirent }) => ({
185
+ path: entryPath,
186
+ dirent,
187
+ stats: await withAbort(fsp.lstat(entryPath), signal),
188
+ }), PARALLEL_CONCURRENCY, signal);
206
189
  if (errors.length > 0) {
207
190
  throw errors[0]?.error ?? new Error('Failed to read entry stats');
208
191
  }
209
- const statsByIndex = [];
210
- for (const result of results) {
211
- statsByIndex[result.index] = result.stats;
212
- }
213
- for (let index = 0; index < entries.length; index += 1) {
214
- const entry = entries[index];
215
- if (!entry)
216
- continue;
217
- const stats = statsByIndex[index];
218
- yield {
219
- path: entry.entryPath,
220
- dirent: entry.dirent,
221
- ...(stats ? { stats } : {}),
222
- };
223
- }
192
+ yield* results;
224
193
  }
225
194
  function createEntryStream(basePath, normalized, maxDepth, needsStats, signal) {
226
195
  if (shouldUseFastPath(normalized, maxDepth)) {
@@ -286,13 +255,9 @@ async function enqueueAppendEntry(entry, entryType, ctx, pending, flushPending)
286
255
  appendEntry(entry, entryType, undefined, ctx);
287
256
  return;
288
257
  }
289
- // Preserve the original behavior: resolve symlink targets in parallel
290
- // when includeSymlinkTargets is enabled (bounded by PARALLEL_CONCURRENCY).
291
- const task = (async () => {
292
- const symlinkTarget = await resolveSymlinkTarget(entryType, ctx.includeSymlinkTargets, entry.path);
293
- appendEntry(entry, entryType, symlinkTarget, ctx);
294
- })();
295
- pending.push(task);
258
+ pending.push(resolveSymlinkTarget(entryType, true, entry.path).then((target) => {
259
+ appendEntry(entry, entryType, target, ctx);
260
+ }));
296
261
  if (pending.length >= PARALLEL_CONCURRENCY) {
297
262
  await flushPending();
298
263
  }
@@ -316,12 +281,6 @@ async function collectEntries(basePath, normalized, signal, needsStats, maxDepth
316
281
  const totals = { files: 0, directories: 0 };
317
282
  const counters = { skippedInaccessible: 0, symlinksNotFollowed: 0 };
318
283
  const basePathDirectories = [basePath];
319
- const accessDeps = {
320
- normalizePath,
321
- isPathWithinDirectories,
322
- isSensitivePath,
323
- validateSymlinkPath: validateExistingPathDetailed,
324
- };
325
284
  let truncated = false;
326
285
  let stoppedReason;
327
286
  const pending = [];
@@ -354,7 +313,7 @@ async function collectEntries(basePath, normalized, signal, needsStats, maxDepth
354
313
  }
355
314
  const entryType = resolveEntryType(entry.dirent);
356
315
  trackSymlink(entryType, normalized.includeSymlinkTargets, counters);
357
- const accessible = await isEntryAccessibleByType(entry.path, entryType, basePathDirectories, signal, accessDeps);
316
+ const accessible = await isEntryAccessibleByType(entry.path, entryType, basePathDirectories, signal, ACCESS_DEPS);
358
317
  if (!accessible) {
359
318
  counters.skippedInaccessible += 1;
360
319
  continue;
@@ -385,43 +344,27 @@ export async function listDirectory(dirPath, options = {}) {
385
344
  return { path: basePath, entries, summary };
386
345
  });
387
346
  }
388
- function toSafeNonNegativeInt(value, fallback) {
389
- if (typeof value !== 'number' || !Number.isFinite(value))
390
- return fallback;
391
- const asInt = Math.floor(value);
392
- return asInt >= 0 ? asInt : fallback;
393
- }
394
- function toSafePositiveInt(value, fallback) {
347
+ function clampInt(value, fallback, min) {
395
348
  if (typeof value !== 'number' || !Number.isFinite(value))
396
349
  return fallback;
397
350
  const asInt = Math.floor(value);
398
- return asInt > 0 ? asInt : fallback;
399
- }
400
- function toSafeBoolean(value, fallback) {
401
- if (typeof value !== 'boolean')
402
- return fallback;
403
- return value;
351
+ return asInt >= min ? asInt : fallback;
404
352
  }
405
353
  function normalizeTreeOptions(options) {
406
354
  return {
407
- maxDepth: toSafeNonNegativeInt(options.maxDepth, 5),
408
- maxEntries: toSafeNonNegativeInt(options.maxEntries, 1000),
409
- includeHidden: toSafeBoolean(options.includeHidden, false),
410
- includeIgnored: toSafeBoolean(options.includeIgnored, false),
411
- includeSizes: toSafeBoolean(options.includeSizes, false),
412
- timeoutMs: toSafePositiveInt(options.timeoutMs, DEFAULT_SEARCH_TIMEOUT_MS),
355
+ maxDepth: clampInt(options.maxDepth, 5, 0),
356
+ maxEntries: clampInt(options.maxEntries, 1000, 0),
357
+ includeHidden: options.includeHidden ?? false,
358
+ includeIgnored: options.includeIgnored ?? false,
359
+ includeSizes: options.includeSizes ?? false,
360
+ timeoutMs: clampInt(options.timeoutMs, DEFAULT_SEARCH_TIMEOUT_MS, 1),
413
361
  };
414
362
  }
415
363
  function ensureParentNodes(rootNode, nodeByPath, relativePath) {
416
364
  const normalized = toPosixPath(relativePath);
417
365
  if (normalized.length === 0 || normalized === '.')
418
366
  return rootNode;
419
- const segments = [];
420
- for (const segment of normalized.split('/')) {
421
- if (segment.length > 0) {
422
- segments.push(segment);
423
- }
424
- }
367
+ const segments = normalized.split('/').filter(Boolean);
425
368
  const parentSegmentCount = Math.max(0, segments.length - 1);
426
369
  let current = rootNode;
427
370
  let currentPath = '';
@@ -430,9 +373,7 @@ function ensureParentNodes(rootNode, nodeByPath, relativePath) {
430
373
  if (!segment)
431
374
  continue;
432
375
  currentPath =
433
- currentPath.length === 0
434
- ? segment
435
- : path.posix.join(currentPath, segment);
376
+ currentPath.length === 0 ? segment : `${currentPath}/${segment}`;
436
377
  let child = nodeByPath.get(currentPath);
437
378
  if (!child) {
438
379
  child = {
@@ -463,12 +404,9 @@ function compareTreeEntries(a, b) {
463
404
  return diff;
464
405
  return a.name.localeCompare(b.name);
465
406
  }
407
+ const TREE_TYPE_RANKS = { directory: 0, file: 1 };
466
408
  function getTreeTypeRank(type) {
467
- if (type === 'directory')
468
- return 0;
469
- if (type === 'file')
470
- return 1;
471
- return 2;
409
+ return TREE_TYPE_RANKS[type] ?? 2;
472
410
  }
473
411
  async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, accessDeps) {
474
412
  const type = resolveEntryType(entry.dirent);
@@ -482,8 +420,7 @@ async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher,
482
420
  })) {
483
421
  return null;
484
422
  }
485
- const relative = path.relative(root, entry.path) || path.basename(entry.path);
486
- const relativePosix = toPosixPath(relative);
423
+ const relativePosix = toPosixPath(resolveRelativePath(root, entry.path));
487
424
  const name = path.basename(entry.path);
488
425
  return { type, relativePosix, name };
489
426
  }
@@ -577,12 +514,6 @@ export async function treeDirectory(dirPath, options = {}) {
577
514
  const root = await validateExistingDirectory(dirPath, signal);
578
515
  const rootNormalized = normalizePath(root);
579
516
  const rootDirectories = [rootNormalized];
580
- const accessDeps = {
581
- normalizePath,
582
- isPathWithinDirectories,
583
- isSensitivePath,
584
- validateSymlinkPath: validateExistingPathDetailed,
585
- };
586
517
  const excludePatterns = normalized.includeIgnored
587
518
  ? []
588
519
  : DEFAULT_EXCLUDE_PATTERNS;
@@ -624,7 +555,7 @@ export async function treeDirectory(dirPath, options = {}) {
624
555
  truncated = true;
625
556
  break;
626
557
  }
627
- const resolved = await resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, accessDeps);
558
+ const resolved = await resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, ACCESS_DEPS);
628
559
  if (!resolved) {
629
560
  continue;
630
561
  }
@@ -727,12 +658,14 @@ function applyLineSelection(target, source) {
727
658
  if (source.endLine !== undefined)
728
659
  target.endLine = source.endLine;
729
660
  }
730
- function resolveListDirectoryNormalizedOptions(options) {
661
+ function resolveNormalizedReadOptions(options) {
731
662
  const { signal, ...rest } = options;
732
- return {
663
+ const result = {
733
664
  normalized: normalizeReadMultipleOptions(rest),
734
- ...(signal ? { signal } : {}),
735
665
  };
666
+ if (signal)
667
+ result.signal = signal;
668
+ return result;
736
669
  }
737
670
  async function validateFile(filePath, index, signal) {
738
671
  const validPath = await validateExistingPath(filePath, signal);
@@ -818,11 +751,7 @@ async function collectFileBudget(filePaths, maxTotalSize, maxSize, signal) {
818
751
  return { skippedBudget, validated };
819
752
  }
820
753
  function buildOutput(filePaths) {
821
- const output = new Array(filePaths.length);
822
- for (let index = 0; index < filePaths.length; index += 1) {
823
- output[index] = { path: filePaths[index] ?? UNKNOWN_PATH };
824
- }
825
- return output;
754
+ return Array.from(filePaths, (fp) => ({ path: fp }));
826
755
  }
827
756
  function resolveErrorOriginalIndex(failureIndex, filesToProcess, totalInputFiles) {
828
757
  // processInParallel implementations vary: some return error indices relative to
@@ -882,7 +811,7 @@ function applySkippedBudget(output, skippedBudget, filePaths, maxTotalSize) {
882
811
  export async function readMultipleFiles(filePaths, options = {}) {
883
812
  if (filePaths.length === 0)
884
813
  return [];
885
- const { normalized, signal } = resolveListDirectoryNormalizedOptions(options);
814
+ const { normalized, signal } = resolveNormalizedReadOptions(options);
886
815
  const output = buildOutput(filePaths);
887
816
  const { skippedBudget, validated } = await collectFileBudget(filePaths, normalized.maxTotalSize, normalized.maxSize, signal);
888
817
  const filesToProcess = buildFilesToProcess(filePaths, validated, skippedBudget);