@j0hanz/filesystem-mcp 1.11.1 → 1.13.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/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/lib/utils.d.ts +4 -0
- package/dist/lib/utils.js +26 -0
- package/dist/resources/tool-info.js +23 -19
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +8 -4
- package/dist/server/bootstrap.js +39 -4
- package/dist/server/roots-manager.d.ts +1 -1
- package/dist/server/roots-manager.js +8 -13
- package/dist/tools/apply-patch.js +34 -38
- package/dist/tools/calculate-hash.js +21 -24
- package/dist/tools/diff-files.js +26 -18
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +183 -115
- package/dist/tools/list-directory.js +105 -10
- package/dist/tools/read-multiple.d.ts +1 -1
- package/dist/tools/read-multiple.js +101 -65
- package/dist/tools/read.js +103 -75
- package/dist/tools/replace-in-files.js +85 -38
- package/dist/tools/search-content.js +266 -217
- package/dist/tools/shared.js +35 -4
- package/dist/tools/task-support.d.ts +7 -0
- package/dist/tools/task-support.js +137 -100
- package/dist/tools.js +2 -2
- package/package.json +1 -1
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -1,15 +1,66 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
1
53
|
import * as fsp from 'node:fs/promises';
|
|
2
54
|
import * as path from 'node:path';
|
|
3
55
|
import { isUtf8 } from 'node:buffer';
|
|
4
56
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { Writable } from 'node:stream';
|
|
6
|
-
import { pipeline } from 'node:stream/promises';
|
|
7
57
|
import { BINARY_CHECK_BUFFER_SIZE, KNOWN_BINARY_EXTENSIONS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from './constants.js';
|
|
8
58
|
import { ErrorCode, McpError, normalizeUnknownError } from './errors.js';
|
|
9
59
|
import { assertAllowedFileAccess, validateExistingPath } from './paths.js';
|
|
10
60
|
function createAbortError(message = 'Operation aborted') {
|
|
11
61
|
return new DOMException(message, 'AbortError');
|
|
12
62
|
}
|
|
63
|
+
const READ_ONLY_FILE_FLAG = 'r';
|
|
13
64
|
const SHARED_NOOP_SIGNAL = new AbortController().signal;
|
|
14
65
|
function normalizeAbortReason(reason, message) {
|
|
15
66
|
if (reason instanceof Error)
|
|
@@ -55,39 +106,22 @@ function getAbortError(signal, message) {
|
|
|
55
106
|
export function withAbort(promise, signal) {
|
|
56
107
|
if (!signal)
|
|
57
108
|
return promise;
|
|
58
|
-
|
|
109
|
+
signal.throwIfAborted();
|
|
59
110
|
return new Promise((resolve, reject) => {
|
|
60
|
-
let settled = false;
|
|
61
|
-
const finish = (run) => {
|
|
62
|
-
if (settled)
|
|
63
|
-
return;
|
|
64
|
-
settled = true;
|
|
65
|
-
signal.removeEventListener('abort', onAbort);
|
|
66
|
-
run();
|
|
67
|
-
};
|
|
68
111
|
const onAbort = () => {
|
|
69
|
-
|
|
70
|
-
reject(getAbortError(signal));
|
|
71
|
-
});
|
|
112
|
+
reject(getAbortError(signal));
|
|
72
113
|
};
|
|
73
|
-
signal.
|
|
74
|
-
try {
|
|
75
|
-
signal.throwIfAborted();
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
114
|
+
if (signal.aborted) {
|
|
78
115
|
onAbort();
|
|
79
116
|
return;
|
|
80
117
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
finish(() => {
|
|
89
|
-
reject(normalizeUnknownError(error));
|
|
90
|
-
});
|
|
118
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
119
|
+
promise.then((value) => {
|
|
120
|
+
signal.removeEventListener('abort', onAbort);
|
|
121
|
+
resolve(value);
|
|
122
|
+
}, (error) => {
|
|
123
|
+
signal.removeEventListener('abort', onAbort);
|
|
124
|
+
reject(normalizeUnknownError(error));
|
|
91
125
|
});
|
|
92
126
|
});
|
|
93
127
|
}
|
|
@@ -102,12 +136,12 @@ export function createTimedAbortSignal(baseSignal, timeoutMs) {
|
|
|
102
136
|
};
|
|
103
137
|
}
|
|
104
138
|
if (baseSignal) {
|
|
105
|
-
return
|
|
139
|
+
return { signal: baseSignal, cleanup: () => { } };
|
|
106
140
|
}
|
|
107
141
|
if (timeoutSignal) {
|
|
108
142
|
return { signal: timeoutSignal, cleanup: () => { } };
|
|
109
143
|
}
|
|
110
|
-
return
|
|
144
|
+
return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
|
|
111
145
|
}
|
|
112
146
|
export async function withTimedAbortSignal(baseSignal, timeoutMs, run) {
|
|
113
147
|
const { signal, cleanup } = createTimedAbortSignal(baseSignal, timeoutMs);
|
|
@@ -118,12 +152,6 @@ export async function withTimedAbortSignal(baseSignal, timeoutMs, run) {
|
|
|
118
152
|
cleanup();
|
|
119
153
|
}
|
|
120
154
|
}
|
|
121
|
-
function createNoopSignal() {
|
|
122
|
-
return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
|
|
123
|
-
}
|
|
124
|
-
function createForwardedSignal(baseSignal) {
|
|
125
|
-
return { signal: baseSignal, cleanup: () => { } };
|
|
126
|
-
}
|
|
127
155
|
function createParallelAbortError() {
|
|
128
156
|
return createAbortError();
|
|
129
157
|
}
|
|
@@ -193,20 +221,8 @@ function hasKnownBinaryExtension(filePath) {
|
|
|
193
221
|
const ext = path.extname(filePath).toLowerCase();
|
|
194
222
|
return KNOWN_BINARY_EXTENSIONS.has(ext);
|
|
195
223
|
}
|
|
196
|
-
async function
|
|
197
|
-
|
|
198
|
-
return fn(existingHandle);
|
|
199
|
-
}
|
|
200
|
-
const effectivePath = await validateExistingPath(filePath, signal);
|
|
201
|
-
const handle = await withAbort(fsp.open(effectivePath, 'r'), signal);
|
|
202
|
-
try {
|
|
203
|
-
return await fn(handle);
|
|
204
|
-
}
|
|
205
|
-
finally {
|
|
206
|
-
await handle.close().catch((error) => {
|
|
207
|
-
console.error('Failed to close file handle:', error);
|
|
208
|
-
});
|
|
209
|
-
}
|
|
224
|
+
async function openReadableFileHandle(filePath, signal) {
|
|
225
|
+
return withAbort(fsp.open(filePath, READ_ONLY_FILE_FLAG), signal);
|
|
210
226
|
}
|
|
211
227
|
async function readProbe(handle, signal) {
|
|
212
228
|
const buffer = Buffer.alloc(BINARY_CHECK_BUFFER_SIZE);
|
|
@@ -222,13 +238,29 @@ function hasUtf16Bom(slice) {
|
|
|
222
238
|
(slice[0] === 0xfe && slice[1] === 0xff)));
|
|
223
239
|
}
|
|
224
240
|
export async function isProbablyBinary(filePath, existingHandle, signal) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
241
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
242
|
+
try {
|
|
243
|
+
if (hasKnownBinaryExtension(filePath)) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
if (existingHandle) {
|
|
247
|
+
const slice = await readProbe(existingHandle, signal);
|
|
248
|
+
return isBinarySlice(slice);
|
|
249
|
+
}
|
|
250
|
+
const effectivePath = await validateExistingPath(filePath, signal);
|
|
251
|
+
const handle = __addDisposableResource(env_1, await openReadableFileHandle(effectivePath, signal), true);
|
|
229
252
|
const slice = await readProbe(handle, signal);
|
|
230
253
|
return isBinarySlice(slice);
|
|
231
|
-
}
|
|
254
|
+
}
|
|
255
|
+
catch (e_1) {
|
|
256
|
+
env_1.error = e_1;
|
|
257
|
+
env_1.hasError = true;
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
const result_1 = __disposeResources(env_1);
|
|
261
|
+
if (result_1)
|
|
262
|
+
await result_1;
|
|
263
|
+
}
|
|
232
264
|
}
|
|
233
265
|
function isBinarySlice(slice) {
|
|
234
266
|
if (slice.length === 0)
|
|
@@ -316,42 +348,35 @@ const STREAM_CHUNK_SIZE = 64 * 1024;
|
|
|
316
348
|
function createTooLargeError(bytesRead, maxSize, requestedPath) {
|
|
317
349
|
return new McpError(ErrorCode.E_TOO_LARGE, `File exceeds maximum size (${bytesRead} > ${maxSize}): ${requestedPath}`, requestedPath, { size: bytesRead, maxSize });
|
|
318
350
|
}
|
|
319
|
-
class BufferCollector extends Writable {
|
|
320
|
-
#chunks = [];
|
|
321
|
-
#totalSize = 0;
|
|
322
|
-
#maxSize;
|
|
323
|
-
#requestedPath;
|
|
324
|
-
constructor(maxSize, requestedPath) {
|
|
325
|
-
super({ autoDestroy: true });
|
|
326
|
-
this.#maxSize = maxSize;
|
|
327
|
-
this.#requestedPath = requestedPath;
|
|
328
|
-
}
|
|
329
|
-
_write(chunk, _encoding, callback) {
|
|
330
|
-
const buffer = Buffer.isBuffer(chunk)
|
|
331
|
-
? chunk
|
|
332
|
-
: Buffer.from(chunk, _encoding);
|
|
333
|
-
this.#totalSize += buffer.length;
|
|
334
|
-
if (this.#totalSize > this.#maxSize) {
|
|
335
|
-
callback(createTooLargeError(this.#totalSize, this.#maxSize, this.#requestedPath));
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
this.#chunks.push(buffer);
|
|
339
|
-
callback();
|
|
340
|
-
}
|
|
341
|
-
getResult() {
|
|
342
|
-
return Buffer.concat(this.#chunks, this.#totalSize);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
351
|
async function readFileBufferWithLimit(handle, maxSize, requestedPath, signal) {
|
|
346
352
|
const stream = handle.createReadStream({
|
|
347
353
|
start: 0,
|
|
348
354
|
highWaterMark: STREAM_CHUNK_SIZE,
|
|
349
355
|
autoClose: false,
|
|
350
356
|
emitClose: false,
|
|
357
|
+
signal,
|
|
351
358
|
});
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
359
|
+
const chunks = [];
|
|
360
|
+
let totalSize = 0;
|
|
361
|
+
try {
|
|
362
|
+
for await (const chunk of stream) {
|
|
363
|
+
const buffer = Buffer.isBuffer(chunk)
|
|
364
|
+
? chunk
|
|
365
|
+
: Buffer.from(chunk);
|
|
366
|
+
totalSize += buffer.length;
|
|
367
|
+
if (totalSize > maxSize) {
|
|
368
|
+
stream.destroy();
|
|
369
|
+
throw createTooLargeError(totalSize, maxSize, requestedPath);
|
|
370
|
+
}
|
|
371
|
+
chunks.push(buffer);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
if (!stream.destroyed) {
|
|
376
|
+
stream.destroy();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return Buffer.concat(chunks, totalSize);
|
|
355
380
|
}
|
|
356
381
|
async function headFile(handle, numLines, encoding = 'utf-8', maxBytesRead, signal) {
|
|
357
382
|
assertNotAborted(signal);
|
|
@@ -499,15 +524,24 @@ async function assertNotBinary(validPath, filePath, normalized) {
|
|
|
499
524
|
return;
|
|
500
525
|
throw new McpError(ErrorCode.E_INVALID_INPUT, `Binary file detected: ${filePath}. Refusing to read as text.`, filePath);
|
|
501
526
|
}
|
|
502
|
-
function
|
|
503
|
-
if (
|
|
504
|
-
|
|
527
|
+
function assertSizeWithinLimit(size, maxSize, filePath) {
|
|
528
|
+
if (size <= maxSize)
|
|
529
|
+
return;
|
|
530
|
+
throw new McpError(ErrorCode.E_TOO_LARGE, `File too large: ${size} bytes (max: ${maxSize} bytes). Use head parameter to preview the first N lines.`, filePath, { size, maxSize });
|
|
531
|
+
}
|
|
532
|
+
function requireReadOption(normalized, key, filePath) {
|
|
533
|
+
const value = normalized[key];
|
|
534
|
+
if (value !== undefined) {
|
|
535
|
+
return value;
|
|
505
536
|
}
|
|
506
|
-
|
|
537
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, `Missing ${key} option`, filePath);
|
|
507
538
|
}
|
|
508
|
-
function
|
|
539
|
+
async function executeHeadRead(context) {
|
|
540
|
+
const head = requireReadOption(context.normalized, 'head', context.filePath);
|
|
541
|
+
const readOptions = buildReadContentOptions(context.normalized);
|
|
542
|
+
const { content, truncated, linesRead, hasMoreLines } = await readHeadContent(context.handle, head, readOptions);
|
|
509
543
|
return {
|
|
510
|
-
path: validPath,
|
|
544
|
+
path: context.validPath,
|
|
511
545
|
content,
|
|
512
546
|
truncated,
|
|
513
547
|
readMode: 'head',
|
|
@@ -516,24 +550,27 @@ function buildHeadResult(validPath, content, truncated, head, linesRead, hasMore
|
|
|
516
550
|
hasMoreLines,
|
|
517
551
|
};
|
|
518
552
|
}
|
|
519
|
-
function
|
|
520
|
-
const
|
|
521
|
-
|
|
553
|
+
async function executeRangeRead(context) {
|
|
554
|
+
const startLine = requireReadOption(context.normalized, 'startLine', context.filePath);
|
|
555
|
+
const { endLine } = context.normalized;
|
|
556
|
+
const readOptions = buildReadContentOptions(context.normalized);
|
|
557
|
+
const { content, truncated, linesRead, hasMoreLines } = await readRangeContent(context.handle, startLine, endLine, readOptions);
|
|
558
|
+
return {
|
|
559
|
+
path: context.validPath,
|
|
522
560
|
content,
|
|
523
561
|
truncated,
|
|
524
562
|
readMode: 'range',
|
|
525
563
|
startLine,
|
|
564
|
+
...(endLine !== undefined ? { endLine } : {}),
|
|
526
565
|
linesRead,
|
|
527
566
|
hasMoreLines,
|
|
528
567
|
};
|
|
529
|
-
if (endLine !== undefined) {
|
|
530
|
-
result.endLine = endLine;
|
|
531
|
-
}
|
|
532
|
-
return result;
|
|
533
568
|
}
|
|
534
|
-
function
|
|
569
|
+
async function executeFullRead(context) {
|
|
570
|
+
assertSizeWithinLimit(context.stats.size, context.normalized.maxSize, context.filePath);
|
|
571
|
+
const { content, totalLines } = await readFullContent(context.handle, context.normalized.encoding, context.normalized.maxSize, context.filePath, context.normalized.signal);
|
|
535
572
|
return {
|
|
536
|
-
path: validPath,
|
|
573
|
+
path: context.validPath,
|
|
537
574
|
content,
|
|
538
575
|
truncated: false,
|
|
539
576
|
totalLines,
|
|
@@ -542,9 +579,12 @@ function buildFullResult(validPath, content, totalLines) {
|
|
|
542
579
|
hasMoreLines: false,
|
|
543
580
|
};
|
|
544
581
|
}
|
|
545
|
-
function
|
|
582
|
+
async function executeTailRead(context) {
|
|
583
|
+
const tail = requireReadOption(context.normalized, 'tail', context.filePath);
|
|
584
|
+
const readOptions = buildReadContentOptions(context.normalized);
|
|
585
|
+
const { content, truncated, linesRead, hasMoreLines } = await readTailContent(context.handle, tail, readOptions);
|
|
546
586
|
return {
|
|
547
|
-
path: validPath,
|
|
587
|
+
path: context.validPath,
|
|
548
588
|
content,
|
|
549
589
|
truncated,
|
|
550
590
|
readMode: 'tail',
|
|
@@ -553,51 +593,15 @@ function buildTailResult(validPath, content, truncated, tail, linesRead, hasMore
|
|
|
553
593
|
hasMoreLines,
|
|
554
594
|
};
|
|
555
595
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
return buildHeadResult(validPath, content, truncated, head, linesRead, hasMoreLines);
|
|
566
|
-
}
|
|
567
|
-
async function readRangeResult(handle, validPath, filePath, normalized) {
|
|
568
|
-
const { startLine, endLine } = normalized;
|
|
569
|
-
if (startLine === undefined) {
|
|
570
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Missing startLine option', filePath);
|
|
571
|
-
}
|
|
572
|
-
const readOptions = buildReadContentOptions(normalized);
|
|
573
|
-
const { content, truncated, linesRead, hasMoreLines } = await readRangeContent(handle, startLine, endLine, readOptions);
|
|
574
|
-
return buildRangeResult(validPath, content, truncated, startLine, endLine, linesRead, hasMoreLines);
|
|
575
|
-
}
|
|
576
|
-
async function readFullResult(handle, validPath, filePath, stats, normalized) {
|
|
577
|
-
assertSizeWithinLimit(stats.size, normalized.maxSize, filePath);
|
|
578
|
-
const { content, totalLines } = await readFullContent(handle, normalized.encoding, normalized.maxSize, filePath, normalized.signal);
|
|
579
|
-
return buildFullResult(validPath, content, totalLines);
|
|
580
|
-
}
|
|
581
|
-
async function readTailResult(handle, validPath, filePath, normalized) {
|
|
582
|
-
if (normalized.tail === undefined) {
|
|
583
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Missing tail option', filePath);
|
|
584
|
-
}
|
|
585
|
-
const readOptions = buildReadContentOptions(normalized);
|
|
586
|
-
const { content, truncated, linesRead, hasMoreLines } = await readTailContent(handle, normalized.tail, readOptions);
|
|
587
|
-
return buildTailResult(validPath, content, truncated, normalized.tail, linesRead, hasMoreLines);
|
|
588
|
-
}
|
|
589
|
-
async function readByMode(handle, validPath, filePath, stats, normalized) {
|
|
590
|
-
const mode = resolveReadMode(normalized);
|
|
591
|
-
if (mode === 'head') {
|
|
592
|
-
return readHeadResult(handle, validPath, filePath, normalized);
|
|
593
|
-
}
|
|
594
|
-
if (mode === 'tail') {
|
|
595
|
-
return readTailResult(handle, validPath, filePath, normalized);
|
|
596
|
-
}
|
|
597
|
-
if (mode === 'range') {
|
|
598
|
-
return readRangeResult(handle, validPath, filePath, normalized);
|
|
599
|
-
}
|
|
600
|
-
return readFullResult(handle, validPath, filePath, stats, normalized);
|
|
596
|
+
const READ_MODE_HANDLERS = {
|
|
597
|
+
head: executeHeadRead,
|
|
598
|
+
range: executeRangeRead,
|
|
599
|
+
full: executeFullRead,
|
|
600
|
+
tail: executeTailRead,
|
|
601
|
+
};
|
|
602
|
+
async function readByMode(context) {
|
|
603
|
+
const mode = resolveReadMode(context.normalized);
|
|
604
|
+
return READ_MODE_HANDLERS[mode](context);
|
|
601
605
|
}
|
|
602
606
|
function assertFileStats(filePath, stats) {
|
|
603
607
|
if (!stats.isFile()) {
|
|
@@ -605,19 +609,32 @@ function assertFileStats(filePath, stats) {
|
|
|
605
609
|
}
|
|
606
610
|
}
|
|
607
611
|
async function readFileWithStatsInternal(filePath, validPath, stats, normalized) {
|
|
608
|
-
|
|
609
|
-
assertAllowedFileAccess(filePath, validPath);
|
|
610
|
-
assertFileStats(filePath, stats);
|
|
611
|
-
if (normalized.skipBinary) {
|
|
612
|
-
await assertNotBinary(validPath, filePath, normalized);
|
|
613
|
-
}
|
|
614
|
-
assertNotAborted(normalized.signal);
|
|
615
|
-
const handle = await withAbort(fsp.open(validPath, 'r'), normalized.signal);
|
|
612
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
616
613
|
try {
|
|
617
|
-
|
|
614
|
+
assertNotAborted(normalized.signal);
|
|
615
|
+
assertAllowedFileAccess(filePath, validPath);
|
|
616
|
+
assertFileStats(filePath, stats);
|
|
617
|
+
if (normalized.skipBinary) {
|
|
618
|
+
await assertNotBinary(validPath, filePath, normalized);
|
|
619
|
+
}
|
|
620
|
+
assertNotAborted(normalized.signal);
|
|
621
|
+
const handle = __addDisposableResource(env_2, await openReadableFileHandle(validPath, normalized.signal), true);
|
|
622
|
+
return await readByMode({
|
|
623
|
+
handle,
|
|
624
|
+
validPath,
|
|
625
|
+
filePath,
|
|
626
|
+
stats,
|
|
627
|
+
normalized,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
catch (e_2) {
|
|
631
|
+
env_2.error = e_2;
|
|
632
|
+
env_2.hasError = true;
|
|
618
633
|
}
|
|
619
634
|
finally {
|
|
620
|
-
|
|
635
|
+
const result_2 = __disposeResources(env_2);
|
|
636
|
+
if (result_2)
|
|
637
|
+
await result_2;
|
|
621
638
|
}
|
|
622
639
|
}
|
|
623
640
|
export async function readFileWithStats(filePath, validPath, stats, options = {}) {
|
|
@@ -636,7 +653,7 @@ export async function atomicWriteFile(filePath, content, options = {}) {
|
|
|
636
653
|
const tempPath = `${filePath}.${randomUUID()}.tmp`;
|
|
637
654
|
try {
|
|
638
655
|
assertNotAborted(signal);
|
|
639
|
-
await
|
|
656
|
+
await fsp.writeFile(tempPath, content, { encoding, signal });
|
|
640
657
|
await withAbort(fsp.rename(tempPath, filePath), signal);
|
|
641
658
|
}
|
|
642
659
|
catch (error) {
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
2
|
+
export declare function debounce<Args extends unknown[]>(func: (...args: Args) => void, waitMs: number): {
|
|
3
|
+
(...args: Args): void;
|
|
4
|
+
cancel: () => void;
|
|
5
|
+
};
|
|
2
6
|
export declare function mergeOptions<T extends object>(defaults: T, overrides: Partial<T>): T;
|
|
3
7
|
export declare function omitOptionKeys<T extends object, K extends keyof T>(input: T, keys: readonly K[]): Omit<T, K>;
|
|
4
8
|
export declare function setIfDefined<T extends object, K extends keyof T>(target: T, key: K, value: T[K] | undefined): void;
|
package/dist/lib/utils.js
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
export function isRecord(value) {
|
|
3
3
|
return value !== null && typeof value === 'object';
|
|
4
4
|
}
|
|
5
|
+
// debounce
|
|
6
|
+
export function debounce(func, waitMs) {
|
|
7
|
+
let timeoutId;
|
|
8
|
+
const debounced = (...args) => {
|
|
9
|
+
if (timeoutId !== undefined) {
|
|
10
|
+
clearTimeout(timeoutId);
|
|
11
|
+
}
|
|
12
|
+
timeoutId = setTimeout(() => {
|
|
13
|
+
timeoutId = undefined;
|
|
14
|
+
func(...args);
|
|
15
|
+
}, waitMs);
|
|
16
|
+
// Unref if in Node environment to not block process exit
|
|
17
|
+
const nodeTimeout = timeoutId;
|
|
18
|
+
if (typeof nodeTimeout === 'object' &&
|
|
19
|
+
typeof nodeTimeout.unref === 'function') {
|
|
20
|
+
nodeTimeout.unref();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
debounced.cancel = () => {
|
|
24
|
+
if (timeoutId !== undefined) {
|
|
25
|
+
clearTimeout(timeoutId);
|
|
26
|
+
timeoutId = undefined;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
return debounced;
|
|
30
|
+
}
|
|
5
31
|
// option-utils.ts
|
|
6
32
|
export function mergeOptions(defaults, overrides) {
|
|
7
33
|
return { ...defaults, ...overrides };
|
|
@@ -91,8 +91,8 @@ function formatTaskSupportLabel(taskSupport) {
|
|
|
91
91
|
function formatAnnotationValue(value) {
|
|
92
92
|
return value ? 'true' : 'false';
|
|
93
93
|
}
|
|
94
|
-
function toJsonSchemaObject(schema) {
|
|
95
|
-
return z.toJSONSchema(schema);
|
|
94
|
+
function toJsonSchemaObject(schema, io = 'output') {
|
|
95
|
+
return z.toJSONSchema(schema, { io });
|
|
96
96
|
}
|
|
97
97
|
function summarizeSchemaType(schema) {
|
|
98
98
|
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
@@ -119,27 +119,31 @@ function buildSchemaFieldLines(label, schema) {
|
|
|
119
119
|
if (!schema) {
|
|
120
120
|
return [`<${label}>`, '- none', `</${label}>`];
|
|
121
121
|
}
|
|
122
|
-
const
|
|
122
|
+
const io = label === 'input_fields' ? 'input' : 'output';
|
|
123
|
+
const jsonSchema = toJsonSchemaObject(schema, io);
|
|
123
124
|
const properties = jsonSchema.properties ?? {};
|
|
124
125
|
const required = new Set(jsonSchema.required ?? []);
|
|
125
126
|
const fieldNames = Object.keys(properties);
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
const lines = [`<${label}>`];
|
|
128
|
+
if (typeof jsonSchema.description === 'string' &&
|
|
129
|
+
jsonSchema.description.length > 0) {
|
|
130
|
+
lines.push(`- Schema constraints: ${jsonSchema.description}`);
|
|
128
131
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
132
|
+
if (fieldNames.length === 0) {
|
|
133
|
+
lines.push('- object with no fields', `</${label}>`);
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
lines.push(...fieldNames.map((fieldName) => {
|
|
137
|
+
const fieldSchema = properties[fieldName] ?? {};
|
|
138
|
+
const type = summarizeSchemaType(fieldSchema);
|
|
139
|
+
const requiredLabel = required.has(fieldName) ? 'required' : 'optional';
|
|
140
|
+
const description = typeof fieldSchema.description === 'string' &&
|
|
141
|
+
fieldSchema.description.length > 0
|
|
142
|
+
? fieldSchema.description
|
|
143
|
+
: 'No description.';
|
|
144
|
+
return `- ${fieldName} (${type}, ${requiredLabel}): ${description}`;
|
|
145
|
+
}), `</${label}>`);
|
|
146
|
+
return lines;
|
|
143
147
|
}
|
|
144
148
|
function buildProtocolNotes(contract) {
|
|
145
149
|
const notes = [
|
package/dist/schemas.d.ts
CHANGED
|
@@ -63,8 +63,8 @@ export declare const SearchFilesInputSchema: z.ZodObject<{
|
|
|
63
63
|
sortBy: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
64
64
|
name: "name";
|
|
65
65
|
size: "size";
|
|
66
|
-
modified: "modified";
|
|
67
66
|
path: "path";
|
|
67
|
+
modified: "modified";
|
|
68
68
|
}>>>;
|
|
69
69
|
maxDepth: z.ZodOptional<z.ZodInt>;
|
|
70
70
|
cursor: z.ZodOptional<z.ZodString>;
|
package/dist/schemas.js
CHANGED
|
@@ -253,7 +253,8 @@ export const ReadFileInputSchema = z
|
|
|
253
253
|
}),
|
|
254
254
|
includeHash: defaultFalseBoolean('Include SHA-256 hash of full file content'),
|
|
255
255
|
})
|
|
256
|
-
.superRefine(validateReadRange)
|
|
256
|
+
.superRefine(validateReadRange)
|
|
257
|
+
.describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'. 'endLine' requires 'startLine'.");
|
|
257
258
|
export const ReadMultipleFilesInputSchema = z
|
|
258
259
|
.strictObject({
|
|
259
260
|
paths: z
|
|
@@ -268,7 +269,8 @@ export const ReadMultipleFilesInputSchema = z
|
|
|
268
269
|
endLine: 'End line (1-based, inclusive) per file. Requires startLine.',
|
|
269
270
|
}),
|
|
270
271
|
})
|
|
271
|
-
.superRefine(validateReadRange)
|
|
272
|
+
.superRefine(validateReadRange)
|
|
273
|
+
.describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'. 'endLine' requires 'startLine'.");
|
|
272
274
|
export const GetFileInfoInputSchema = z.strictObject({
|
|
273
275
|
path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
|
|
274
276
|
});
|
|
@@ -451,7 +453,8 @@ export const CreateDirectoryInputSchema = z
|
|
|
451
453
|
.refine((data) => data.path !== undefined || data.paths !== undefined, {
|
|
452
454
|
error: "Either 'path' or 'paths' must be provided",
|
|
453
455
|
path: ['path'],
|
|
454
|
-
})
|
|
456
|
+
})
|
|
457
|
+
.describe("Provide either 'path' or 'paths'.");
|
|
455
458
|
export const CreateDirectoryOutputSchema = z.strictObject({
|
|
456
459
|
ok: z.boolean(),
|
|
457
460
|
path: z.string().optional(),
|
|
@@ -510,7 +513,8 @@ export const MoveFileInputSchema = z
|
|
|
510
513
|
.refine((data) => (data.source ?? data.sources) !== undefined, {
|
|
511
514
|
error: "Either 'source' or 'sources' must be provided",
|
|
512
515
|
path: ['source'],
|
|
513
|
-
})
|
|
516
|
+
})
|
|
517
|
+
.describe("Provide either 'source' or 'sources'.");
|
|
514
518
|
export const MoveFileOutputSchema = z.strictObject({
|
|
515
519
|
ok: z.boolean(),
|
|
516
520
|
source: z.string().optional(),
|