@j0hanz/filesystem-mcp 1.1.2 → 1.2.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/README.md +514 -188
- package/dist/cli.js +29 -12
- package/dist/completions.js +50 -24
- package/dist/config.d.ts +3 -2
- package/dist/config.js +1 -1
- package/dist/index.js +14 -12
- package/dist/instructions.md +109 -97
- package/dist/lib/constants.js +25 -14
- package/dist/lib/errors.js +11 -6
- package/dist/lib/file-operations/common.d.ts +4 -0
- package/dist/lib/file-operations/common.js +9 -0
- package/dist/lib/file-operations/file-info.js +22 -10
- package/dist/lib/file-operations/gitignore.js +14 -11
- package/dist/lib/file-operations/glob-engine.d.ts +1 -0
- package/dist/lib/file-operations/glob-engine.js +46 -33
- package/dist/lib/file-operations/list-directory.js +31 -35
- package/dist/lib/file-operations/read-multiple-files.js +70 -62
- package/dist/lib/file-operations/search-content.js +83 -64
- package/dist/lib/file-operations/search-files.js +32 -30
- package/dist/lib/file-operations/search-worker.js +22 -12
- package/dist/lib/file-operations/tree.js +43 -34
- package/dist/lib/fs-helpers.js +61 -124
- package/dist/lib/observability.js +29 -28
- package/dist/lib/path-format.d.ts +1 -0
- package/dist/lib/path-format.js +7 -0
- package/dist/lib/path-policy.js +22 -20
- package/dist/lib/path-validation.js +13 -7
- package/dist/lib/resource-store.d.ts +2 -0
- package/dist/lib/resource-store.js +26 -5
- package/dist/lib/type-guards.d.ts +1 -0
- package/dist/lib/type-guards.js +3 -0
- package/dist/prompts.d.ts +1 -5
- package/dist/prompts.js +9 -16
- package/dist/resources.d.ts +1 -5
- package/dist/resources.js +12 -26
- package/dist/schemas.d.ts +213 -30
- package/dist/schemas.js +52 -90
- package/dist/server.js +85 -44
- package/dist/tools/apply-patch.js +24 -24
- package/dist/tools/calculate-hash.js +42 -45
- package/dist/tools/create-directory.js +18 -21
- package/dist/tools/delete-file.js +36 -39
- package/dist/tools/diff-files.js +16 -21
- package/dist/tools/edit-file.js +16 -20
- package/dist/tools/list-directory.js +25 -25
- package/dist/tools/move-file.js +18 -21
- package/dist/tools/read-multiple.js +56 -68
- package/dist/tools/read.js +27 -32
- package/dist/tools/replace-in-files.js +28 -35
- package/dist/tools/roots.js +9 -10
- package/dist/tools/search-content.js +74 -74
- package/dist/tools/search-files.js +45 -52
- package/dist/tools/shared.d.ts +44 -6
- package/dist/tools/shared.js +86 -64
- package/dist/tools/stat-many.js +45 -68
- package/dist/tools/stat.js +11 -39
- package/dist/tools/task-support.d.ts +9 -1
- package/dist/tools/task-support.js +86 -81
- package/dist/tools/tree.js +13 -30
- package/dist/tools/write-file.js +18 -21
- package/dist/tools.js +23 -18
- package/package.json +6 -7
|
@@ -4,6 +4,7 @@ import { isProbablyBinary } from '../fs-helpers.js';
|
|
|
4
4
|
import { startPerfMeasure } from '../observability.js';
|
|
5
5
|
import { buildMatcher, scanFileInWorker } from './search-content.js';
|
|
6
6
|
const matcherCache = new Map();
|
|
7
|
+
const MAX_MATCHER_CACHE_SIZE = 100;
|
|
7
8
|
function getMatcherCacheKey(pattern, options) {
|
|
8
9
|
const cs = options.caseSensitive ? '1' : '0';
|
|
9
10
|
const ww = options.wholeWord ? '1' : '0';
|
|
@@ -14,19 +15,25 @@ function getCachedMatcher(pattern, options) {
|
|
|
14
15
|
const key = getMatcherCacheKey(pattern, options);
|
|
15
16
|
const cached = matcherCache.get(key);
|
|
16
17
|
if (cached) {
|
|
17
|
-
|
|
18
|
-
matcherCache.set(key, cached);
|
|
18
|
+
refreshMatcherCacheEntry(key, cached);
|
|
19
19
|
return cached;
|
|
20
20
|
}
|
|
21
21
|
const matcher = buildMatcher(pattern, options);
|
|
22
|
+
refreshMatcherCacheEntry(key, matcher);
|
|
23
|
+
evictOldestMatcherIfNeeded();
|
|
24
|
+
return matcher;
|
|
25
|
+
}
|
|
26
|
+
function refreshMatcherCacheEntry(key, matcher) {
|
|
27
|
+
matcherCache.delete(key);
|
|
22
28
|
matcherCache.set(key, matcher);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
}
|
|
30
|
+
function evictOldestMatcherIfNeeded() {
|
|
31
|
+
if (matcherCache.size <= MAX_MATCHER_CACHE_SIZE)
|
|
32
|
+
return;
|
|
33
|
+
const firstKey = matcherCache.keys().next().value;
|
|
34
|
+
if (firstKey !== undefined) {
|
|
35
|
+
matcherCache.delete(firstKey);
|
|
28
36
|
}
|
|
29
|
-
return matcher;
|
|
30
37
|
}
|
|
31
38
|
const cancelledRequests = new Set();
|
|
32
39
|
const activeRequests = new Set();
|
|
@@ -45,6 +52,11 @@ function consumeCancelled(id) {
|
|
|
45
52
|
cancelledRequests.delete(id);
|
|
46
53
|
return true;
|
|
47
54
|
}
|
|
55
|
+
function markCancelledIfActive(id) {
|
|
56
|
+
if (activeRequests.has(id)) {
|
|
57
|
+
cancelledRequests.add(id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
48
60
|
function buildScanResponse(id, result) {
|
|
49
61
|
return {
|
|
50
62
|
type: 'result',
|
|
@@ -97,14 +109,12 @@ function handleMessage(message) {
|
|
|
97
109
|
void handleScanRequest(message);
|
|
98
110
|
break;
|
|
99
111
|
case 'cancel':
|
|
100
|
-
|
|
101
|
-
cancelledRequests.add(message.id);
|
|
102
|
-
}
|
|
112
|
+
markCancelledIfActive(message.id);
|
|
103
113
|
break;
|
|
104
114
|
case 'shutdown':
|
|
105
115
|
shuttingDown = true;
|
|
106
116
|
for (const id of activeRequests) {
|
|
107
|
-
|
|
117
|
+
markCancelledIfActive(id);
|
|
108
118
|
}
|
|
109
119
|
maybeFinishShutdown();
|
|
110
120
|
break;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../constants.js';
|
|
3
3
|
import { createTimedAbortSignal } from '../fs-helpers.js';
|
|
4
|
+
import { toPosixPath } from '../path-format.js';
|
|
4
5
|
import { isSensitivePath } from '../path-policy.js';
|
|
5
6
|
import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
6
7
|
import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
|
|
7
|
-
import { globEntries } from './glob-engine.js';
|
|
8
|
+
import { globEntries, resolveEntryType } from './glob-engine.js';
|
|
8
9
|
function toSafeNonNegativeInt(value, fallback) {
|
|
9
10
|
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
10
11
|
return fallback;
|
|
@@ -31,23 +32,23 @@ function normalizeOptions(options) {
|
|
|
31
32
|
timeoutMs: toSafePositiveInt(options.timeoutMs, DEFAULT_SEARCH_TIMEOUT_MS),
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
|
-
function resolveEntryType(dirent) {
|
|
35
|
-
if (dirent.isDirectory())
|
|
36
|
-
return 'directory';
|
|
37
|
-
if (dirent.isSymbolicLink())
|
|
38
|
-
return 'symlink';
|
|
39
|
-
if (dirent.isFile())
|
|
40
|
-
return 'file';
|
|
41
|
-
return 'other';
|
|
42
|
-
}
|
|
43
35
|
function ensureParentNodes(rootNode, nodeByPath, relativePath) {
|
|
44
|
-
const normalized = relativePath
|
|
36
|
+
const normalized = toPosixPath(relativePath);
|
|
45
37
|
if (normalized.length === 0 || normalized === '.')
|
|
46
38
|
return rootNode;
|
|
47
|
-
const segments =
|
|
39
|
+
const segments = [];
|
|
40
|
+
for (const segment of normalized.split('/')) {
|
|
41
|
+
if (segment.length > 0) {
|
|
42
|
+
segments.push(segment);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const parentSegmentCount = Math.max(0, segments.length - 1);
|
|
48
46
|
let current = rootNode;
|
|
49
47
|
let currentPath = '';
|
|
50
|
-
for (
|
|
48
|
+
for (let index = 0; index < parentSegmentCount; index += 1) {
|
|
49
|
+
const segment = segments[index];
|
|
50
|
+
if (!segment)
|
|
51
|
+
continue;
|
|
51
52
|
currentPath =
|
|
52
53
|
currentPath.length === 0
|
|
53
54
|
? segment
|
|
@@ -71,23 +72,24 @@ function ensureParentNodes(rootNode, nodeByPath, relativePath) {
|
|
|
71
72
|
function sortTree(node) {
|
|
72
73
|
if (!node.children)
|
|
73
74
|
return;
|
|
74
|
-
node.children.sort(
|
|
75
|
-
const typeRank = (t) => {
|
|
76
|
-
if (t === 'directory')
|
|
77
|
-
return 0;
|
|
78
|
-
if (t === 'file')
|
|
79
|
-
return 1;
|
|
80
|
-
return 2;
|
|
81
|
-
};
|
|
82
|
-
const diff = typeRank(a.type) - typeRank(b.type);
|
|
83
|
-
if (diff !== 0)
|
|
84
|
-
return diff;
|
|
85
|
-
return a.name.localeCompare(b.name);
|
|
86
|
-
});
|
|
75
|
+
node.children.sort(compareTreeEntries);
|
|
87
76
|
for (const child of node.children) {
|
|
88
77
|
sortTree(child);
|
|
89
78
|
}
|
|
90
79
|
}
|
|
80
|
+
function compareTreeEntries(a, b) {
|
|
81
|
+
const diff = getTreeTypeRank(a.type) - getTreeTypeRank(b.type);
|
|
82
|
+
if (diff !== 0)
|
|
83
|
+
return diff;
|
|
84
|
+
return a.name.localeCompare(b.name);
|
|
85
|
+
}
|
|
86
|
+
function getTreeTypeRank(type) {
|
|
87
|
+
if (type === 'directory')
|
|
88
|
+
return 0;
|
|
89
|
+
if (type === 'file')
|
|
90
|
+
return 1;
|
|
91
|
+
return 2;
|
|
92
|
+
}
|
|
91
93
|
function getStopReason(signal, totalEntries, maxEntries) {
|
|
92
94
|
if (signal.aborted) {
|
|
93
95
|
return 'aborted';
|
|
@@ -97,11 +99,11 @@ function getStopReason(signal, totalEntries, maxEntries) {
|
|
|
97
99
|
}
|
|
98
100
|
return undefined;
|
|
99
101
|
}
|
|
100
|
-
async function resolveTreeEntry(entry, root,
|
|
102
|
+
async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal) {
|
|
101
103
|
const type = resolveEntryType(entry.dirent);
|
|
102
104
|
if (type !== 'symlink') {
|
|
103
105
|
const normalized = normalizePath(entry.path);
|
|
104
|
-
if (!isPathWithinDirectories(normalized,
|
|
106
|
+
if (!isPathWithinDirectories(normalized, rootDirectories)) {
|
|
105
107
|
return null;
|
|
106
108
|
}
|
|
107
109
|
if (isSensitivePath(entry.path, normalized)) {
|
|
@@ -126,7 +128,7 @@ async function resolveTreeEntry(entry, root, rootNormalized, gitignoreMatcher, s
|
|
|
126
128
|
return null;
|
|
127
129
|
}
|
|
128
130
|
const relative = path.relative(root, entry.path) || path.basename(entry.path);
|
|
129
|
-
const relativePosix = relative
|
|
131
|
+
const relativePosix = toPosixPath(relative);
|
|
130
132
|
const name = path.basename(entry.path);
|
|
131
133
|
return { type, relativePosix, name };
|
|
132
134
|
}
|
|
@@ -154,7 +156,10 @@ function upsertChildNode(parent, nodeByPath, resolved, childPathIndexByParent) {
|
|
|
154
156
|
parent.children ??= [];
|
|
155
157
|
let seen = childPathIndexByParent.get(parent);
|
|
156
158
|
if (!seen) {
|
|
157
|
-
seen = new Set(
|
|
159
|
+
seen = new Set();
|
|
160
|
+
for (const entry of parent.children) {
|
|
161
|
+
seen.add(entry.relativePath);
|
|
162
|
+
}
|
|
158
163
|
childPathIndexByParent.set(parent, seen);
|
|
159
164
|
}
|
|
160
165
|
const key = child.relativePath;
|
|
@@ -201,9 +206,12 @@ export function formatTreeAscii(tree) {
|
|
|
201
206
|
nextPrefix = `${prefix}${continuation}`;
|
|
202
207
|
}
|
|
203
208
|
const count = node.children.length;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
for (let index = 0; index < count; index += 1) {
|
|
210
|
+
const child = node.children[index];
|
|
211
|
+
if (child) {
|
|
212
|
+
walk(child, nextPrefix, index === count - 1, false);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
207
215
|
};
|
|
208
216
|
walk(tree, '', true, true);
|
|
209
217
|
return lines.join('\n');
|
|
@@ -213,6 +221,7 @@ export async function treeDirectory(dirPath, options = {}) {
|
|
|
213
221
|
const { signal, cleanup } = createTimedAbortSignal(options.signal, normalized.timeoutMs);
|
|
214
222
|
const root = await validateExistingDirectory(dirPath, signal);
|
|
215
223
|
const rootNormalized = normalizePath(root);
|
|
224
|
+
const rootDirectories = [rootNormalized];
|
|
216
225
|
try {
|
|
217
226
|
const excludePatterns = normalized.includeIgnored
|
|
218
227
|
? []
|
|
@@ -249,7 +258,7 @@ export async function treeDirectory(dirPath, options = {}) {
|
|
|
249
258
|
truncated = true;
|
|
250
259
|
break;
|
|
251
260
|
}
|
|
252
|
-
const resolved = await resolveTreeEntry(entry, root,
|
|
261
|
+
const resolved = await resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal);
|
|
253
262
|
if (!resolved) {
|
|
254
263
|
continue;
|
|
255
264
|
}
|
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -11,11 +11,20 @@ import { validateExistingPath } from './path-validation.js';
|
|
|
11
11
|
function createAbortError(message = 'Operation aborted') {
|
|
12
12
|
return new DOMException(message, 'AbortError');
|
|
13
13
|
}
|
|
14
|
+
const SHARED_NOOP_SIGNAL = new AbortController().signal;
|
|
14
15
|
function normalizeAbortReason(reason, message) {
|
|
15
16
|
if (reason instanceof Error)
|
|
16
17
|
return reason;
|
|
17
18
|
return createAbortError(message);
|
|
18
19
|
}
|
|
20
|
+
function isFiniteNumber(value) {
|
|
21
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
22
|
+
}
|
|
23
|
+
function normalizeUnknownError(error) {
|
|
24
|
+
return error instanceof Error
|
|
25
|
+
? error
|
|
26
|
+
: new Error(formatUnknownErrorMessage(error));
|
|
27
|
+
}
|
|
19
28
|
export function assertNotAborted(signal, message) {
|
|
20
29
|
if (!signal)
|
|
21
30
|
return;
|
|
@@ -83,15 +92,13 @@ export function withAbort(promise, signal) {
|
|
|
83
92
|
})
|
|
84
93
|
.catch((error) => {
|
|
85
94
|
finish(() => {
|
|
86
|
-
reject(error
|
|
87
|
-
? error
|
|
88
|
-
: new Error(formatUnknownErrorMessage(error)));
|
|
95
|
+
reject(normalizeUnknownError(error));
|
|
89
96
|
});
|
|
90
97
|
});
|
|
91
98
|
});
|
|
92
99
|
}
|
|
93
100
|
export function createTimedAbortSignal(baseSignal, timeoutMs) {
|
|
94
|
-
const timeoutSignal =
|
|
101
|
+
const timeoutSignal = isFiniteNumber(timeoutMs)
|
|
95
102
|
? AbortSignal.timeout(timeoutMs)
|
|
96
103
|
: undefined;
|
|
97
104
|
if (baseSignal && timeoutSignal) {
|
|
@@ -109,8 +116,7 @@ export function createTimedAbortSignal(baseSignal, timeoutMs) {
|
|
|
109
116
|
return createNoopSignal();
|
|
110
117
|
}
|
|
111
118
|
function createNoopSignal() {
|
|
112
|
-
|
|
113
|
-
return { signal: controller.signal, cleanup: () => { } };
|
|
119
|
+
return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
|
|
114
120
|
}
|
|
115
121
|
function createForwardedSignal(baseSignal) {
|
|
116
122
|
return { signal: baseSignal, cleanup: () => { } };
|
|
@@ -118,118 +124,56 @@ function createForwardedSignal(baseSignal) {
|
|
|
118
124
|
function createParallelAbortError() {
|
|
119
125
|
return createAbortError();
|
|
120
126
|
}
|
|
121
|
-
function createState(items, processor, concurrency, signal) {
|
|
122
|
-
return {
|
|
123
|
-
items,
|
|
124
|
-
processor,
|
|
125
|
-
concurrency,
|
|
126
|
-
results: [],
|
|
127
|
-
errors: [],
|
|
128
|
-
nextIndex: 0,
|
|
129
|
-
aborted: Boolean(signal?.aborted),
|
|
130
|
-
inFlight: new Set(),
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
function attachAbortListener(state, signal) {
|
|
134
|
-
if (!signal || signal.aborted)
|
|
135
|
-
return () => { };
|
|
136
|
-
const onAbort = () => {
|
|
137
|
-
state.aborted = true;
|
|
138
|
-
};
|
|
139
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
140
|
-
return () => {
|
|
141
|
-
signal.removeEventListener('abort', onAbort);
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
function createAbortPromise(signal) {
|
|
145
|
-
if (!signal)
|
|
146
|
-
return { cleanup: () => { } };
|
|
147
|
-
if (signal.aborted)
|
|
148
|
-
return { abortPromise: Promise.resolve(), cleanup: () => { } };
|
|
149
|
-
let cleanup = () => { };
|
|
150
|
-
const abortPromise = new Promise((resolve) => {
|
|
151
|
-
const onAbort = () => {
|
|
152
|
-
resolve();
|
|
153
|
-
};
|
|
154
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
155
|
-
cleanup = () => {
|
|
156
|
-
signal.removeEventListener('abort', onAbort);
|
|
157
|
-
};
|
|
158
|
-
});
|
|
159
|
-
return { abortPromise, cleanup };
|
|
160
|
-
}
|
|
161
|
-
function canStartNext(state) {
|
|
162
|
-
return (!state.aborted &&
|
|
163
|
-
state.inFlight.size < state.concurrency &&
|
|
164
|
-
state.nextIndex < state.items.length);
|
|
165
|
-
}
|
|
166
|
-
async function createTask(item, index, state) {
|
|
167
|
-
try {
|
|
168
|
-
const result = await state.processor(item);
|
|
169
|
-
state.results.push(result);
|
|
170
|
-
}
|
|
171
|
-
catch (reason) {
|
|
172
|
-
const error = reason instanceof Error
|
|
173
|
-
? reason
|
|
174
|
-
: new Error(formatUnknownErrorMessage(reason));
|
|
175
|
-
state.errors.push({ index, error });
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function queueNextTask(state) {
|
|
179
|
-
const index = state.nextIndex;
|
|
180
|
-
state.nextIndex += 1;
|
|
181
|
-
const item = state.items[index];
|
|
182
|
-
if (item === undefined)
|
|
183
|
-
return;
|
|
184
|
-
const task = createTask(item, index, state);
|
|
185
|
-
state.inFlight.add(task);
|
|
186
|
-
void task.finally(() => {
|
|
187
|
-
state.inFlight.delete(task);
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
function startNextTasks(state) {
|
|
191
|
-
while (canStartNext(state)) {
|
|
192
|
-
queueNextTask(state);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
async function drainTasks(state, abortPromise) {
|
|
196
|
-
startNextTasks(state);
|
|
197
|
-
while (state.inFlight.size > 0) {
|
|
198
|
-
if (abortPromise) {
|
|
199
|
-
const nextTask = Promise.race(state.inFlight);
|
|
200
|
-
await Promise.race([nextTask, abortPromise]);
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
await Promise.race(state.inFlight);
|
|
204
|
-
}
|
|
205
|
-
if (state.aborted)
|
|
206
|
-
break;
|
|
207
|
-
startNextTasks(state);
|
|
208
|
-
}
|
|
209
|
-
if (state.inFlight.size > 0) {
|
|
210
|
-
await Promise.allSettled(state.inFlight);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
127
|
export async function processInParallel(items, processor, concurrency = PARALLEL_CONCURRENCY, signal) {
|
|
214
|
-
|
|
215
|
-
if (items.length === 0) {
|
|
216
|
-
cleanupAbortPromise();
|
|
128
|
+
if (items.length === 0)
|
|
217
129
|
return { results: [], errors: [] };
|
|
218
|
-
}
|
|
219
130
|
const effectiveConcurrency = normalizeConcurrency(concurrency);
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
131
|
+
// Pre-allocate slots by index to guarantee input-order output.
|
|
132
|
+
const resultSlots = new Array(items.length);
|
|
133
|
+
const errors = [];
|
|
134
|
+
if (signal?.aborted)
|
|
135
|
+
throw createParallelAbortError();
|
|
136
|
+
let nextIndex = 0;
|
|
137
|
+
const next = async () => {
|
|
138
|
+
while (nextIndex < items.length) {
|
|
139
|
+
if (signal?.aborted)
|
|
140
|
+
throw createParallelAbortError();
|
|
141
|
+
const index = nextIndex++;
|
|
142
|
+
// Check again because another worker might have incremented past length
|
|
143
|
+
if (index >= items.length)
|
|
144
|
+
break;
|
|
145
|
+
const item = items[index];
|
|
146
|
+
try {
|
|
147
|
+
const result = await processor(item);
|
|
148
|
+
if (signal?.aborted)
|
|
149
|
+
throw createParallelAbortError();
|
|
150
|
+
resultSlots[index] = result;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (signal?.aborted)
|
|
154
|
+
throw createParallelAbortError();
|
|
155
|
+
errors.push({
|
|
156
|
+
index,
|
|
157
|
+
error: normalizeUnknownError(error),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const workerCount = Math.min(items.length, effectiveConcurrency);
|
|
163
|
+
const workers = new Array(workerCount);
|
|
164
|
+
for (let index = 0; index < workerCount; index += 1) {
|
|
165
|
+
workers[index] = next();
|
|
228
166
|
}
|
|
229
|
-
|
|
167
|
+
await Promise.allSettled(workers);
|
|
168
|
+
if (signal?.aborted)
|
|
230
169
|
throw createParallelAbortError();
|
|
170
|
+
const results = [];
|
|
171
|
+
for (const slot of resultSlots) {
|
|
172
|
+
if (slot !== undefined) {
|
|
173
|
+
results.push(slot);
|
|
174
|
+
}
|
|
231
175
|
}
|
|
232
|
-
return { results
|
|
176
|
+
return { results, errors };
|
|
233
177
|
}
|
|
234
178
|
export function getFileType(stats) {
|
|
235
179
|
if (stats.isFile())
|
|
@@ -307,12 +251,6 @@ function validateReadOptions(options) {
|
|
|
307
251
|
if (hasEnd && !hasStart) {
|
|
308
252
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'endLine requires startLine');
|
|
309
253
|
}
|
|
310
|
-
if (options.startLine !== undefined && options.startLine < 1) {
|
|
311
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, 'startLine must be at least 1');
|
|
312
|
-
}
|
|
313
|
-
if (options.endLine !== undefined && options.endLine < 1) {
|
|
314
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, 'endLine must be at least 1');
|
|
315
|
-
}
|
|
316
254
|
if (options.startLine !== undefined &&
|
|
317
255
|
options.endLine !== undefined &&
|
|
318
256
|
options.endLine < options.startLine) {
|
|
@@ -326,7 +264,6 @@ function normalizeOptions(options) {
|
|
|
326
264
|
maxSize: Math.min(options.maxSize ?? MAX_TEXT_FILE_SIZE, MAX_TEXT_FILE_SIZE),
|
|
327
265
|
skipBinary: options.skipBinary ?? false,
|
|
328
266
|
};
|
|
329
|
-
assertPositiveSafeIntegerOption('maxSize', normalized.maxSize, 'maxSize must be at least 1');
|
|
330
267
|
if (options.head !== undefined) {
|
|
331
268
|
normalized.head = options.head;
|
|
332
269
|
}
|
|
@@ -590,12 +527,12 @@ async function readFullResult(handle, validPath, filePath, stats, normalized) {
|
|
|
590
527
|
async function readByMode(handle, validPath, filePath, stats, normalized) {
|
|
591
528
|
const mode = resolveReadMode(normalized);
|
|
592
529
|
if (mode === 'head') {
|
|
593
|
-
return
|
|
530
|
+
return readHeadResult(handle, validPath, filePath, normalized);
|
|
594
531
|
}
|
|
595
532
|
if (mode === 'range') {
|
|
596
|
-
return
|
|
533
|
+
return readRangeResult(handle, validPath, filePath, normalized);
|
|
597
534
|
}
|
|
598
|
-
return
|
|
535
|
+
return readFullResult(handle, validPath, filePath, stats, normalized);
|
|
599
536
|
}
|
|
600
537
|
function assertFileStats(filePath, stats) {
|
|
601
538
|
if (!stats.isFile()) {
|
|
@@ -620,14 +557,14 @@ async function readFileWithStatsInternal(filePath, validPath, stats, normalized)
|
|
|
620
557
|
}
|
|
621
558
|
export async function readFileWithStats(filePath, validPath, stats, options = {}) {
|
|
622
559
|
const normalized = prepareReadOptions(options);
|
|
623
|
-
return
|
|
560
|
+
return readFileWithStatsInternal(filePath, validPath, stats, normalized);
|
|
624
561
|
}
|
|
625
562
|
export async function readFile(filePath, options = {}) {
|
|
626
563
|
const normalized = prepareReadOptions(options);
|
|
627
564
|
const validPath = await validateExistingPath(filePath, normalized.signal);
|
|
628
565
|
assertNotAborted(normalized.signal);
|
|
629
566
|
const stats = await withAbort(fsp.stat(validPath), normalized.signal);
|
|
630
|
-
return
|
|
567
|
+
return readFileWithStatsInternal(filePath, validPath, stats, normalized);
|
|
631
568
|
}
|
|
632
569
|
export async function atomicWriteFile(filePath, content, options = {}) {
|
|
633
570
|
const { encoding = 'utf-8', signal } = options;
|
|
@@ -2,14 +2,16 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
2
2
|
import { hash } from 'node:crypto';
|
|
3
3
|
import { channel, tracingChannel } from 'node:diagnostics_channel';
|
|
4
4
|
import { monitorEventLoopDelay, performance, PerformanceObserver, } from 'node:perf_hooks';
|
|
5
|
+
import { isRecord } from './type-guards.js';
|
|
5
6
|
// --- Configuration ---
|
|
6
7
|
const ENV = process.env;
|
|
8
|
+
let _cachedConfig;
|
|
7
9
|
function readConfig() {
|
|
8
|
-
return {
|
|
10
|
+
return (_cachedConfig ??= {
|
|
9
11
|
enabled: isTrue(ENV['FS_CONTEXT_DIAGNOSTICS']),
|
|
10
12
|
detail: parseDetail(ENV['FS_CONTEXT_DIAGNOSTICS_DETAIL']),
|
|
11
13
|
logToolErrors: isTrue(ENV['FS_CONTEXT_TOOL_LOG_ERRORS']),
|
|
12
|
-
};
|
|
14
|
+
});
|
|
13
15
|
}
|
|
14
16
|
function isTrue(val) {
|
|
15
17
|
const norm = val?.trim().toLowerCase();
|
|
@@ -47,11 +49,8 @@ const toolContext = new AsyncLocalStorage({
|
|
|
47
49
|
let perfObserver;
|
|
48
50
|
let traceCounter = 0;
|
|
49
51
|
// --- Helpers: Result Analysis ---
|
|
50
|
-
function isObject(v) {
|
|
51
|
-
return typeof v === 'object' && v !== null;
|
|
52
|
-
}
|
|
53
52
|
function extractOutcome(result) {
|
|
54
|
-
if (!
|
|
53
|
+
if (!isRecord(result)) {
|
|
55
54
|
return { ok: true };
|
|
56
55
|
}
|
|
57
56
|
if (result['isError'] === true) {
|
|
@@ -64,7 +63,7 @@ function extractOutcome(result) {
|
|
|
64
63
|
return { ok: false, error: extractErrorMessage(result) };
|
|
65
64
|
}
|
|
66
65
|
const content = result['structuredContent'];
|
|
67
|
-
if (
|
|
66
|
+
if (isRecord(content) && typeof content['ok'] === 'boolean') {
|
|
68
67
|
if (content['ok'])
|
|
69
68
|
return { ok: true };
|
|
70
69
|
const err = extractResultError(content);
|
|
@@ -77,17 +76,17 @@ function extractErrorMessage(source) {
|
|
|
77
76
|
return source;
|
|
78
77
|
if (source instanceof Error)
|
|
79
78
|
return source.message;
|
|
80
|
-
if (
|
|
79
|
+
if (isRecord(source)) {
|
|
81
80
|
const struct = source['structuredContent'];
|
|
82
|
-
if (
|
|
81
|
+
if (isRecord(struct)) {
|
|
83
82
|
const err = struct['error'];
|
|
84
|
-
if (
|
|
83
|
+
if (isRecord(err) && typeof err['message'] === 'string')
|
|
85
84
|
return err['message'];
|
|
86
85
|
}
|
|
87
86
|
if (typeof source['message'] === 'string')
|
|
88
87
|
return source['message'];
|
|
89
88
|
const errObj = source['error'];
|
|
90
|
-
if (
|
|
89
|
+
if (isRecord(errObj) && typeof errObj['message'] === 'string') {
|
|
91
90
|
return errObj['message'];
|
|
92
91
|
}
|
|
93
92
|
}
|
|
@@ -100,7 +99,7 @@ function extractErrorMessage(source) {
|
|
|
100
99
|
}
|
|
101
100
|
function extractResultError(structured) {
|
|
102
101
|
const err = structured['error'];
|
|
103
|
-
return
|
|
102
|
+
return isRecord(err) && typeof err['message'] === 'string'
|
|
104
103
|
? err['message']
|
|
105
104
|
: undefined;
|
|
106
105
|
}
|
|
@@ -216,6 +215,10 @@ function normalizeContext(ctx) {
|
|
|
216
215
|
}
|
|
217
216
|
return { ...ctx, path: normalized };
|
|
218
217
|
}
|
|
218
|
+
function clearMeasureMarks(startMark, endMark) {
|
|
219
|
+
performance.clearMarks(startMark);
|
|
220
|
+
performance.clearMarks(endMark);
|
|
221
|
+
}
|
|
219
222
|
export function startPerfMeasure(name, detail) {
|
|
220
223
|
if (!readConfig().enabled || !CHANNELS.perf.hasSubscribers)
|
|
221
224
|
return undefined;
|
|
@@ -245,14 +248,12 @@ export function startPerfMeasure(name, detail) {
|
|
|
245
248
|
});
|
|
246
249
|
}
|
|
247
250
|
finally {
|
|
248
|
-
|
|
249
|
-
performance.clearMarks(endMark);
|
|
251
|
+
clearMeasureMarks(startMark, endMark);
|
|
250
252
|
}
|
|
251
253
|
});
|
|
252
254
|
}
|
|
253
255
|
catch {
|
|
254
|
-
|
|
255
|
-
performance.clearMarks(endMark);
|
|
256
|
+
clearMeasureMarks(startMark, endMark);
|
|
256
257
|
}
|
|
257
258
|
};
|
|
258
259
|
}
|
|
@@ -291,15 +292,15 @@ async function runAndObserve(tool, run, pubTool, pubPerf, logErrors, pathVal) {
|
|
|
291
292
|
if (pubTool)
|
|
292
293
|
publishToolStart(tool, pathVal);
|
|
293
294
|
let result;
|
|
294
|
-
|
|
295
|
-
let errorMsg;
|
|
295
|
+
const obs = { ok: false, errorMsg: undefined };
|
|
296
296
|
try {
|
|
297
297
|
result = await run();
|
|
298
|
-
|
|
298
|
+
const { ok, error } = extractOutcome(result);
|
|
299
|
+
obs.ok = ok;
|
|
300
|
+
obs.errorMsg = error;
|
|
299
301
|
}
|
|
300
302
|
catch (err) {
|
|
301
|
-
|
|
302
|
-
errorMsg = extractErrorMessage(err);
|
|
303
|
+
obs.errorMsg = extractErrorMessage(err);
|
|
303
304
|
throw err;
|
|
304
305
|
}
|
|
305
306
|
finally {
|
|
@@ -308,10 +309,10 @@ async function runAndObserve(tool, run, pubTool, pubPerf, logErrors, pathVal) {
|
|
|
308
309
|
if (pubPerf && eluStart)
|
|
309
310
|
publishPerfEnd(tool, durationMs, eluStart, loopMonitor);
|
|
310
311
|
if (pubTool)
|
|
311
|
-
publishToolEnd(tool, ok, durationMs, errorMsg);
|
|
312
|
-
updateMetrics(tool, ok, durationMs);
|
|
313
|
-
if (logErrors && !ok)
|
|
314
|
-
logError(tool, durationMs, errorMsg);
|
|
312
|
+
publishToolEnd(tool, obs.ok, durationMs, obs.errorMsg);
|
|
313
|
+
updateMetrics(tool, obs.ok, durationMs);
|
|
314
|
+
if (logErrors && !obs.ok)
|
|
315
|
+
logError(tool, durationMs, obs.errorMsg);
|
|
315
316
|
}
|
|
316
317
|
return result;
|
|
317
318
|
}
|
|
@@ -322,10 +323,10 @@ export async function withToolDiagnostics(tool, run, options) {
|
|
|
322
323
|
tool,
|
|
323
324
|
...(options?.path ? { path: options.path } : {}),
|
|
324
325
|
};
|
|
325
|
-
return
|
|
326
|
+
return toolContext.run(context, async () => {
|
|
326
327
|
if (!config.enabled) {
|
|
327
328
|
if (!config.logToolErrors)
|
|
328
|
-
return
|
|
329
|
+
return run();
|
|
329
330
|
const start = performance.now();
|
|
330
331
|
try {
|
|
331
332
|
const res = await run();
|
|
@@ -356,7 +357,7 @@ export async function withToolDiagnostics(tool, run, options) {
|
|
|
356
357
|
throw e;
|
|
357
358
|
}
|
|
358
359
|
}
|
|
359
|
-
return
|
|
360
|
+
return runAndObserve(tool, run, pubTool, pubPerf, config.logToolErrors, normalizedPath);
|
|
360
361
|
});
|
|
361
362
|
}
|
|
362
363
|
function logError(tool, durationMs, msg) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function toPosixPath(value: string): string;
|