@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.
- package/dist/completions.js +25 -0
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/file-operations/metadata.js +71 -142
- package/dist/lib/file-operations/search.js +119 -19
- package/dist/lib/file-operations/traversal.js +45 -102
- package/dist/lib/fs-helpers.js +175 -158
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.js +58 -0
- package/dist/resources/generated-instructions.js +11 -3
- package/dist/resources/tool-catalog.js +49 -3
- package/dist/resources/tool-info.d.ts +1 -0
- package/dist/resources/tool-info.js +139 -8
- package/dist/resources.js +2 -0
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.js +10 -4
- package/dist/server/bootstrap.js +41 -5
- package/dist/tools/apply-patch.js +30 -33
- package/dist/tools/calculate-hash.js +21 -24
- package/dist/tools/contract.d.ts +1 -1
- package/dist/tools/create-directory.js +3 -3
- package/dist/tools/delete-file.js +2 -2
- package/dist/tools/diff-files.js +32 -20
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +172 -104
- package/dist/tools/list-directory.js +105 -10
- package/dist/tools/move-file.js +3 -3
- package/dist/tools/read-multiple.d.ts +1 -1
- package/dist/tools/read-multiple.js +123 -99
- package/dist/tools/read.js +104 -76
- package/dist/tools/replace-in-files.d.ts +5 -2
- package/dist/tools/replace-in-files.js +142 -76
- package/dist/tools/roots.js +1 -1
- package/dist/tools/search-content.js +249 -218
- package/dist/tools/shared.js +39 -7
- package/dist/tools/stat-many.js +12 -19
- package/dist/tools/stat.js +1 -1
- package/dist/tools/task-support.d.ts +7 -0
- package/dist/tools/task-support.js +140 -101
- package/dist/tools/write-file.js +4 -4
- package/dist/tools.js +2 -2
- package/package.json +1 -1
package/dist/completions.js
CHANGED
|
@@ -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) {
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/constants.js
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
63
|
-
const ext =
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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 =
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
197
|
-
|
|
198
|
-
if (!
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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,
|
|
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
|
|
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
|
|
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:
|
|
408
|
-
maxEntries:
|
|
409
|
-
includeHidden:
|
|
410
|
-
includeIgnored:
|
|
411
|
-
includeSizes:
|
|
412
|
-
timeoutMs:
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
661
|
+
function resolveNormalizedReadOptions(options) {
|
|
731
662
|
const { signal, ...rest } = options;
|
|
732
|
-
|
|
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
|
-
|
|
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 } =
|
|
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);
|