@j0hanz/filesystem-mcp 1.9.1 → 1.10.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 +10 -10
- package/dist/completions.js +140 -115
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/file-operations/metadata.d.ts +5 -1
- package/dist/lib/file-operations/metadata.js +14 -1
- package/dist/lib/file-operations/search.d.ts +2 -0
- package/dist/lib/file-operations/search.js +59 -27
- package/dist/lib/fs-helpers.d.ts +3 -1
- package/dist/lib/fs-helpers.js +63 -0
- package/dist/lib/paths.js +79 -53
- package/dist/lib/resource-store.d.ts +2 -0
- package/dist/lib/resource-store.js +58 -17
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +51 -0
- package/dist/resources/generated-instructions.js +36 -9
- package/dist/resources/tool-catalog.js +30 -7
- package/dist/resources/tool-info.d.ts +4 -0
- package/dist/resources/tool-info.js +21 -3
- package/dist/resources/workflows.js +17 -5
- package/dist/schemas.d.ts +36 -1
- package/dist/schemas.js +73 -3
- package/dist/server/bootstrap.js +85 -65
- package/dist/tools/apply-patch.js +135 -31
- package/dist/tools/calculate-hash.js +13 -8
- package/dist/tools/create-directory.js +14 -3
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/diff-files.js +26 -8
- package/dist/tools/edit-file.js +11 -8
- package/dist/tools/list-directory.js +1 -6
- package/dist/tools/move-file.js +39 -7
- package/dist/tools/read-multiple.js +9 -1
- package/dist/tools/read.js +38 -6
- package/dist/tools/replace-in-files.js +72 -25
- package/dist/tools/roots.js +1 -0
- package/dist/tools/search-content.js +76 -48
- package/dist/tools/search-files.js +6 -7
- package/dist/tools/shared.d.ts +2 -1
- package/dist/tools/shared.js +36 -20
- package/dist/tools/stat-many.js +1 -1
- package/dist/tools/stat.js +4 -0
- package/dist/tools/task-support.js +4 -12
- package/dist/tools/tree.js +4 -0
- package/dist/tools/write-file.js +4 -2
- package/package.json +17 -8
package/dist/tools/read.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { DEFAULT_SEARCH_TIMEOUT_MS, MAX_TEXT_FILE_SIZE, } from '../lib/constants.js';
|
|
3
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
5
|
import { readFile } from '../lib/fs-helpers.js';
|
|
@@ -17,6 +18,7 @@ export const READ_FILE_TOOL = {
|
|
|
17
18
|
nuances: [
|
|
18
19
|
'Large content is externalized to `filesystem-mcp://result/{id}` and preview is returned inline.',
|
|
19
20
|
],
|
|
21
|
+
taskSupport: 'optional',
|
|
20
22
|
};
|
|
21
23
|
async function handleReadFile(args, signal, resourceStore) {
|
|
22
24
|
const options = {
|
|
@@ -27,6 +29,9 @@ async function handleReadFile(args, signal, resourceStore) {
|
|
|
27
29
|
if (args.head !== undefined) {
|
|
28
30
|
options.head = args.head;
|
|
29
31
|
}
|
|
32
|
+
if (args.tail !== undefined) {
|
|
33
|
+
options.tail = args.tail;
|
|
34
|
+
}
|
|
30
35
|
if (args.startLine !== undefined) {
|
|
31
36
|
options.startLine = args.startLine;
|
|
32
37
|
}
|
|
@@ -46,11 +51,18 @@ async function handleReadFile(args, signal, resourceStore) {
|
|
|
46
51
|
? { totalLines: result.totalLines }
|
|
47
52
|
: {}),
|
|
48
53
|
...(result.head !== undefined ? { head: result.head } : {}),
|
|
54
|
+
...(result.tail !== undefined ? { tail: result.tail } : {}),
|
|
49
55
|
...(result.startLine !== undefined ? { startLine: result.startLine } : {}),
|
|
50
56
|
...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
|
|
57
|
+
...(result.linesRead !== undefined ? { linesRead: result.linesRead } : {}),
|
|
51
58
|
...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
|
|
52
59
|
};
|
|
53
60
|
const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name: `read:${path.basename(args.path)}`, mimeType: 'text/plain' });
|
|
61
|
+
if (args.includeHash) {
|
|
62
|
+
structured.contentHash = createHash('sha256')
|
|
63
|
+
.update(result.content, 'utf-8')
|
|
64
|
+
.digest('hex');
|
|
65
|
+
}
|
|
54
66
|
if (!externalized) {
|
|
55
67
|
return buildToolResponse(result.content, structured);
|
|
56
68
|
}
|
|
@@ -72,6 +84,7 @@ async function handleReadFile(args, signal, resourceStore) {
|
|
|
72
84
|
name: entry.name,
|
|
73
85
|
mimeType: entry.mimeType,
|
|
74
86
|
description: 'Full file contents',
|
|
87
|
+
expiresAt: entry.expiresAt,
|
|
75
88
|
}),
|
|
76
89
|
]);
|
|
77
90
|
}
|
|
@@ -90,8 +103,12 @@ export function registerReadFileTool(server, options = {}) {
|
|
|
90
103
|
const name = path.basename(args.path);
|
|
91
104
|
if (args.startLine !== undefined) {
|
|
92
105
|
const end = args.endLine ?? '…';
|
|
93
|
-
return `🕮 read: ${name} [${args.startLine}
|
|
106
|
+
return `🕮 read: ${name} [lines ${args.startLine}–${end}]`;
|
|
94
107
|
}
|
|
108
|
+
if (args.head !== undefined)
|
|
109
|
+
return `🕮 read: ${name} [head ${args.head}]`;
|
|
110
|
+
if (args.tail !== undefined)
|
|
111
|
+
return `🕮 read: ${name} [tail ${args.tail}]`;
|
|
95
112
|
return `🕮 read: ${name}`;
|
|
96
113
|
},
|
|
97
114
|
completionMessage: (args, result) => {
|
|
@@ -101,11 +118,26 @@ export function registerReadFileTool(server, options = {}) {
|
|
|
101
118
|
const sc = result.structuredContent;
|
|
102
119
|
if (!sc.ok)
|
|
103
120
|
return `🕮 read: ${name} • failed`;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
121
|
+
const lines = sc.linesRead ?? sc.totalLines;
|
|
122
|
+
if (sc.startLine !== undefined) {
|
|
123
|
+
const end = sc.linesRead
|
|
124
|
+
? sc.startLine + sc.linesRead - 1
|
|
125
|
+
: (sc.endLine ?? '…');
|
|
126
|
+
return `🕮 read: ${name} • lines ${sc.startLine}–${end}`;
|
|
127
|
+
}
|
|
128
|
+
if (sc.head !== undefined) {
|
|
129
|
+
return sc.hasMoreLines
|
|
130
|
+
? `🕮 read: ${name} • first ${lines ?? sc.head} lines`
|
|
131
|
+
: `🕮 read: ${name} • ${lines ?? sc.head} lines`;
|
|
132
|
+
}
|
|
133
|
+
if (sc.tail !== undefined) {
|
|
134
|
+
return sc.hasMoreLines
|
|
135
|
+
? `🕮 read: ${name} • last ${lines ?? sc.tail} lines`
|
|
136
|
+
: `🕮 read: ${name} • ${lines ?? sc.tail} lines`;
|
|
137
|
+
}
|
|
138
|
+
if (sc.truncated)
|
|
139
|
+
return `🕮 read: ${name} • truncated [${String(lines)} lines]`;
|
|
140
|
+
return `🕮 read: ${name} • ${String(lines)} lines`;
|
|
109
141
|
},
|
|
110
142
|
});
|
|
111
143
|
const validatedHandler = withValidatedArgs(ReadFileInputSchema, wrappedHandler);
|
|
@@ -34,6 +34,7 @@ const MAX_FAILURES = 20;
|
|
|
34
34
|
const REPLACE_CONCURRENCY = Math.min(PARALLEL_CONCURRENCY, 8);
|
|
35
35
|
const MAX_CHANGED_FILES = 100;
|
|
36
36
|
const MAX_DIFF_SIZE = 20 * 1024; // 20KB limit for diff output
|
|
37
|
+
const DIFF_APPEND_BUFFER = 1024;
|
|
37
38
|
function recordFailure(failures, failure) {
|
|
38
39
|
if (failures.length >= MAX_FAILURES)
|
|
39
40
|
return;
|
|
@@ -47,9 +48,10 @@ function recordChangedFile(summary, filePath, matchCount) {
|
|
|
47
48
|
}
|
|
48
49
|
summary.changedFilesTruncated = true;
|
|
49
50
|
}
|
|
50
|
-
function createRegexMatcher(pattern) {
|
|
51
|
+
function createRegexMatcher(pattern, caseSensitive) {
|
|
52
|
+
const flags = caseSensitive ? 'g' : 'gi';
|
|
51
53
|
try {
|
|
52
|
-
return new RE2(pattern,
|
|
54
|
+
return new RE2(pattern, flags);
|
|
53
55
|
}
|
|
54
56
|
catch (error) {
|
|
55
57
|
throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
|
|
@@ -73,7 +75,12 @@ function createRegexReplacementMatcher(regex) {
|
|
|
73
75
|
};
|
|
74
76
|
return { count, replace };
|
|
75
77
|
}
|
|
76
|
-
function createLiteralReplacementMatcher(searchPattern) {
|
|
78
|
+
function createLiteralReplacementMatcher(searchPattern, caseSensitive) {
|
|
79
|
+
if (!caseSensitive) {
|
|
80
|
+
const escaped = searchPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
81
|
+
const regex = new RE2(escaped, 'gi');
|
|
82
|
+
return createRegexReplacementMatcher(regex);
|
|
83
|
+
}
|
|
77
84
|
const count = (content) => {
|
|
78
85
|
let matchCount = 0;
|
|
79
86
|
let pos = content.indexOf(searchPattern);
|
|
@@ -90,7 +97,8 @@ function createLiteralReplacementMatcher(searchPattern) {
|
|
|
90
97
|
function formatFileTooLargeError(filePath, size, maxFileSize) {
|
|
91
98
|
return `File too large: ${filePath} (${size} bytes > ${maxFileSize} bytes)`;
|
|
92
99
|
}
|
|
93
|
-
async function processEntry(entryPath,
|
|
100
|
+
async function processEntry(entryPath, context) {
|
|
101
|
+
const { options, replacement, matcher, maxFileSize, signal, summary } = context;
|
|
94
102
|
let validPath;
|
|
95
103
|
try {
|
|
96
104
|
validPath = await validatePathForWrite(entryPath, signal);
|
|
@@ -123,14 +131,12 @@ async function processEntry(entryPath, options, replacement, matcher, maxFileSiz
|
|
|
123
131
|
summary.filesChanged++;
|
|
124
132
|
recordChangedFile(summary, validPath, matchCount);
|
|
125
133
|
const newContent = matcher.replace(content, replacement);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
+
maybeAppendPatchDiff(summary, {
|
|
135
|
+
filePath: validPath,
|
|
136
|
+
originalContent: content,
|
|
137
|
+
updatedContent: newContent,
|
|
138
|
+
includeDiff: options.dryRun || options.returnDiff,
|
|
139
|
+
});
|
|
134
140
|
if (!options.dryRun) {
|
|
135
141
|
await atomicWriteFile(validPath, newContent, {
|
|
136
142
|
encoding: 'utf-8',
|
|
@@ -147,9 +153,27 @@ async function processEntry(entryPath, options, replacement, matcher, maxFileSiz
|
|
|
147
153
|
});
|
|
148
154
|
}
|
|
149
155
|
}
|
|
156
|
+
function maybeAppendPatchDiff(summary, params) {
|
|
157
|
+
if (!params.includeDiff)
|
|
158
|
+
return;
|
|
159
|
+
if (summary.diff.length >= MAX_DIFF_SIZE) {
|
|
160
|
+
summary.diffTruncated = true;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const patch = createTwoFilesPatch(path.basename(params.filePath), path.basename(params.filePath), params.originalContent, params.updatedContent, 'Original', 'Modified');
|
|
164
|
+
if (summary.diff.length + patch.length <=
|
|
165
|
+
MAX_DIFF_SIZE + DIFF_APPEND_BUFFER) {
|
|
166
|
+
summary.diff += patch;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
summary.diffTruncated = true;
|
|
170
|
+
}
|
|
150
171
|
async function processEntriesConcurrently(entries, options) {
|
|
151
172
|
const pending = new Set();
|
|
152
|
-
const
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
const { signal, concurrency, maxEntries, onEntry, runEntry } = options;
|
|
175
|
+
let dispatched = 0;
|
|
176
|
+
let stoppedByLimit = false;
|
|
153
177
|
const waitForSlot = async () => {
|
|
154
178
|
if (pending.size < concurrency)
|
|
155
179
|
return;
|
|
@@ -158,8 +182,16 @@ async function processEntriesConcurrently(entries, options) {
|
|
|
158
182
|
for await (const entry of entries) {
|
|
159
183
|
if (signal?.aborted)
|
|
160
184
|
break;
|
|
185
|
+
if (maxEntries !== undefined && dispatched >= maxEntries) {
|
|
186
|
+
stoppedByLimit = true;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
if (seen.has(entry.path))
|
|
190
|
+
continue;
|
|
191
|
+
seen.add(entry.path);
|
|
161
192
|
await waitForSlot();
|
|
162
193
|
onEntry();
|
|
194
|
+
dispatched++;
|
|
163
195
|
const task = runEntry(entry.path);
|
|
164
196
|
pending.add(task);
|
|
165
197
|
void task.finally(() => {
|
|
@@ -169,6 +201,7 @@ async function processEntriesConcurrently(entries, options) {
|
|
|
169
201
|
if (pending.size > 0) {
|
|
170
202
|
await Promise.allSettled([...pending]);
|
|
171
203
|
}
|
|
204
|
+
return { stoppedByLimit };
|
|
172
205
|
}
|
|
173
206
|
function createReplaceSummary(root) {
|
|
174
207
|
return {
|
|
@@ -181,6 +214,7 @@ function createReplaceSummary(root) {
|
|
|
181
214
|
changedFiles: [],
|
|
182
215
|
changedFilesTruncated: false,
|
|
183
216
|
diff: '',
|
|
217
|
+
diffTruncated: false,
|
|
184
218
|
};
|
|
185
219
|
}
|
|
186
220
|
async function resolveSearchRoot(pathValue, signal) {
|
|
@@ -194,14 +228,14 @@ function createReplacementRegex(args) {
|
|
|
194
228
|
if (!safeRegex(args.searchPattern)) {
|
|
195
229
|
throw new McpError(ErrorCode.E_INVALID_INPUT, `Unsafe regex pattern: ${args.searchPattern}`);
|
|
196
230
|
}
|
|
197
|
-
return createRegexMatcher(args.searchPattern);
|
|
231
|
+
return createRegexMatcher(args.searchPattern, args.caseSensitive);
|
|
198
232
|
}
|
|
199
233
|
function createReplacementMatcher(args) {
|
|
200
234
|
const regex = createReplacementRegex(args);
|
|
201
235
|
if (regex) {
|
|
202
236
|
return createRegexReplacementMatcher(regex);
|
|
203
237
|
}
|
|
204
|
-
return createLiteralReplacementMatcher(args.searchPattern);
|
|
238
|
+
return createLiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
|
|
205
239
|
}
|
|
206
240
|
export async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
207
241
|
const maxFileSize = MAX_TEXT_FILE_SIZE;
|
|
@@ -220,9 +254,10 @@ export async function handleSearchAndReplace(args, signal, onProgress = () => {
|
|
|
220
254
|
suppressErrors: true,
|
|
221
255
|
});
|
|
222
256
|
const summary = createReplaceSummary(root);
|
|
223
|
-
await processEntriesConcurrently(entries, {
|
|
257
|
+
const { stoppedByLimit } = await processEntriesConcurrently(entries, {
|
|
224
258
|
signal,
|
|
225
259
|
concurrency: REPLACE_CONCURRENCY,
|
|
260
|
+
...(args.maxFiles !== undefined ? { maxEntries: args.maxFiles } : {}),
|
|
226
261
|
onEntry: () => {
|
|
227
262
|
summary.processedFiles++;
|
|
228
263
|
reportPeriodicProgress(onProgress, summary.processedFiles, {
|
|
@@ -230,10 +265,20 @@ export async function handleSearchAndReplace(args, signal, onProgress = () => {
|
|
|
230
265
|
});
|
|
231
266
|
},
|
|
232
267
|
runEntry: async (entryPath) => processEntry(entryPath, {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
268
|
+
options: {
|
|
269
|
+
dryRun: args.dryRun,
|
|
270
|
+
returnDiff: args.returnDiff ?? false,
|
|
271
|
+
},
|
|
272
|
+
replacement: args.replacement,
|
|
273
|
+
matcher,
|
|
274
|
+
maxFileSize,
|
|
275
|
+
signal,
|
|
276
|
+
summary,
|
|
277
|
+
}),
|
|
236
278
|
});
|
|
279
|
+
if (stoppedByLimit) {
|
|
280
|
+
summary.stoppedReason = 'maxFiles';
|
|
281
|
+
}
|
|
237
282
|
reportPeriodicProgress(onProgress, summary.processedFiles, {
|
|
238
283
|
throttleModulo: 25,
|
|
239
284
|
force: true,
|
|
@@ -253,6 +298,10 @@ export async function handleSearchAndReplace(args, signal, onProgress = () => {
|
|
|
253
298
|
...((args.dryRun || args.returnDiff) && summary.diff
|
|
254
299
|
? { diff: summary.diff }
|
|
255
300
|
: {}),
|
|
301
|
+
...(summary.diffTruncated ? { diffTruncated: true } : {}),
|
|
302
|
+
...(summary.stoppedReason
|
|
303
|
+
? { stoppedReason: summary.stoppedReason }
|
|
304
|
+
: {}),
|
|
256
305
|
dryRun: args.dryRun,
|
|
257
306
|
});
|
|
258
307
|
}
|
|
@@ -265,12 +314,12 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
265
314
|
run: async (signal) => {
|
|
266
315
|
const dryLabel = args.dryRun ? ' [dry run]' : '';
|
|
267
316
|
const context = `"${args.searchPattern}" in ${args.filePattern}${dryLabel}`;
|
|
268
|
-
const progress = createToolProgressSession(extra, `🛠
|
|
317
|
+
const progress = createToolProgressSession(extra, `🛠 replace: ${context}`);
|
|
269
318
|
const progressWithMessage = ({ current, total, }) => {
|
|
270
319
|
progress.update({
|
|
271
320
|
current,
|
|
272
321
|
...(total !== undefined ? { total } : {}),
|
|
273
|
-
message: `🛠
|
|
322
|
+
message: `🛠 replace: ${args.searchPattern} [${current} files]`,
|
|
274
323
|
});
|
|
275
324
|
};
|
|
276
325
|
try {
|
|
@@ -282,13 +331,11 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
282
331
|
let endSuffix = `${sc.matches ?? 0} ${matchWord} in ${sc.filesChanged ?? 0} ${fileWord}`;
|
|
283
332
|
if (sc.failedFiles)
|
|
284
333
|
endSuffix += `, ${sc.failedFiles} failed`;
|
|
285
|
-
|
|
286
|
-
endSuffix += ' [dry run]';
|
|
287
|
-
progress.complete(`🛠 search_and_replace: ${context} • ${endSuffix}`, finalCurrent);
|
|
334
|
+
progress.complete(`🛠 replace: ${context} • ${endSuffix}`, finalCurrent);
|
|
288
335
|
return result;
|
|
289
336
|
}
|
|
290
337
|
catch (error) {
|
|
291
|
-
progress.fail(`🛠
|
|
338
|
+
progress.fail(`🛠 replace: ${context} • failed`);
|
|
292
339
|
throw error;
|
|
293
340
|
}
|
|
294
341
|
},
|
package/dist/tools/roots.js
CHANGED
|
@@ -12,6 +12,7 @@ export const LIST_ALLOWED_DIRECTORIES_TOOL = {
|
|
|
12
12
|
outputSchema: ListAllowedDirectoriesOutputSchema,
|
|
13
13
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
14
14
|
nuances: ['Returns absolute paths of all allowed directories.'],
|
|
15
|
+
taskSupport: 'optional',
|
|
15
16
|
};
|
|
16
17
|
function buildTextRoots(dirs) {
|
|
17
18
|
if (dirs.length === 0) {
|
|
@@ -34,36 +34,62 @@ function assertValidRegexPattern(pattern) {
|
|
|
34
34
|
throw new McpError(ErrorCode.E_INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
function findColumnOffset(content, pattern, isRegex, caseSensitive) {
|
|
38
|
+
try {
|
|
39
|
+
if (isRegex) {
|
|
40
|
+
const flags = caseSensitive ? '' : 'i';
|
|
41
|
+
const regex = new RE2(pattern, flags);
|
|
42
|
+
const match = regex.exec(content);
|
|
43
|
+
return match ? match.index : undefined;
|
|
44
|
+
}
|
|
45
|
+
if (caseSensitive) {
|
|
46
|
+
const idx = content.indexOf(pattern);
|
|
47
|
+
return idx >= 0 ? idx : undefined;
|
|
48
|
+
}
|
|
49
|
+
const lowerContent = content.toLowerCase();
|
|
50
|
+
const lowerPattern = pattern.toLowerCase();
|
|
51
|
+
const idx = lowerContent.indexOf(lowerPattern);
|
|
52
|
+
return idx >= 0 ? idx : undefined;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
37
58
|
function buildSearchTextResult(result, normalizedMatches) {
|
|
38
59
|
const { summary } = result;
|
|
39
60
|
if (normalizedMatches.length === 0)
|
|
40
61
|
return 'No matches';
|
|
41
62
|
let truncatedReason;
|
|
42
63
|
if (summary.truncated) {
|
|
43
|
-
|
|
44
|
-
truncatedReason = 'timeout';
|
|
45
|
-
}
|
|
46
|
-
else if (summary.stoppedReason === 'maxFiles') {
|
|
47
|
-
truncatedReason = `max files (${summary.filesScanned})`;
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
truncatedReason = `max results (${summary.matches})`;
|
|
51
|
-
}
|
|
64
|
+
truncatedReason = resolveTruncatedReason(summary);
|
|
52
65
|
}
|
|
53
66
|
const summaryOptions = {
|
|
54
67
|
truncated: summary.truncated,
|
|
55
68
|
...(truncatedReason ? { truncatedReason } : {}),
|
|
56
69
|
};
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
return (buildMatchListText(`Found ${normalizedMatches.length}:`, normalizedMatches) + formatOperationSummary(summaryOptions));
|
|
71
|
+
}
|
|
72
|
+
function resolveTruncatedReason(summary) {
|
|
73
|
+
if (summary.stoppedReason === 'timeout')
|
|
74
|
+
return 'timeout';
|
|
75
|
+
if (summary.stoppedReason === 'maxFiles') {
|
|
76
|
+
return `max files (${summary.filesScanned})`;
|
|
77
|
+
}
|
|
78
|
+
return `max results (${summary.matches})`;
|
|
79
|
+
}
|
|
80
|
+
function buildMatchListText(heading, matches) {
|
|
81
|
+
const lines = [heading];
|
|
82
|
+
for (const match of matches) {
|
|
59
83
|
lines.push(formatSearchMatchLine(match));
|
|
60
84
|
}
|
|
61
|
-
return joinLines(lines)
|
|
85
|
+
return joinLines(lines);
|
|
62
86
|
}
|
|
63
|
-
function buildSearchMatchPayload(match) {
|
|
87
|
+
function buildSearchMatchPayload(match, context) {
|
|
88
|
+
const column = findColumnOffset(match.content, context.pattern, context.isRegex, context.caseSensitive);
|
|
64
89
|
return {
|
|
65
90
|
file: match.relativeFile,
|
|
66
91
|
line: match.line,
|
|
92
|
+
...(column !== undefined ? { column } : {}),
|
|
67
93
|
content: match.content,
|
|
68
94
|
matchCount: match.matchCount,
|
|
69
95
|
...(match.contextBefore ? { contextBefore: [...match.contextBefore] } : {}),
|
|
@@ -74,9 +100,9 @@ function formatSearchMatchLine(match) {
|
|
|
74
100
|
const lineNum = String(match.line).padStart(4);
|
|
75
101
|
return ` ${match.relativeFile}:${lineNum}: ${match.content}`;
|
|
76
102
|
}
|
|
77
|
-
function buildStructuredSearchResult(result, normalizedMatches, options) {
|
|
103
|
+
function buildStructuredSearchResult(result, normalizedMatches, options, context) {
|
|
78
104
|
const { summary } = result;
|
|
79
|
-
const matches = normalizedMatches.map((match) => buildSearchMatchPayload(match));
|
|
105
|
+
const matches = normalizedMatches.map((match) => buildSearchMatchPayload(match, context));
|
|
80
106
|
return {
|
|
81
107
|
ok: true,
|
|
82
108
|
patternType: options.patternType,
|
|
@@ -141,6 +167,7 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
141
167
|
contextLines: args.contextLines,
|
|
142
168
|
maxResults: args.maxResults,
|
|
143
169
|
isLiteral: !args.isRegex,
|
|
170
|
+
multiline: args.multiline,
|
|
144
171
|
};
|
|
145
172
|
if (signal) {
|
|
146
173
|
options.signal = signal;
|
|
@@ -159,16 +186,21 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
159
186
|
throw error;
|
|
160
187
|
}
|
|
161
188
|
const normalizedMatches = normalizeSearchMatches(result);
|
|
189
|
+
const searchContext = {
|
|
190
|
+
pattern: args.pattern,
|
|
191
|
+
isRegex: args.isRegex,
|
|
192
|
+
caseSensitive: args.caseSensitive,
|
|
193
|
+
};
|
|
162
194
|
const structuredFull = buildStructuredSearchResult(result, normalizedMatches, {
|
|
163
195
|
patternType,
|
|
164
196
|
caseSensitive: args.caseSensitive,
|
|
165
|
-
});
|
|
197
|
+
}, searchContext);
|
|
166
198
|
const needsExternalize = normalizedMatches.length > MAX_INLINE_MATCHES;
|
|
167
199
|
if (!resourceStore || !needsExternalize) {
|
|
168
200
|
return buildToolResponse(buildSearchTextResult(result, normalizedMatches), structuredFull);
|
|
169
201
|
}
|
|
170
202
|
const previewMatches = normalizedMatches.slice(0, MAX_INLINE_MATCHES);
|
|
171
|
-
const previewPayload = previewMatches.map((match) => buildSearchMatchPayload(match));
|
|
203
|
+
const previewPayload = previewMatches.map((match) => buildSearchMatchPayload(match, searchContext));
|
|
172
204
|
const previewStructured = {
|
|
173
205
|
...structuredFull,
|
|
174
206
|
matches: previewPayload,
|
|
@@ -181,19 +213,14 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
181
213
|
text: JSON.stringify(structuredFull),
|
|
182
214
|
});
|
|
183
215
|
previewStructured.resourceUri = entry.uri;
|
|
184
|
-
const
|
|
185
|
-
`Found ${normalizedMatches.length} (showing first ${MAX_INLINE_MATCHES}):`,
|
|
186
|
-
];
|
|
187
|
-
for (const match of previewMatches) {
|
|
188
|
-
textLines.push(formatSearchMatchLine(match));
|
|
189
|
-
}
|
|
190
|
-
const text = joinLines(textLines);
|
|
216
|
+
const text = buildMatchListText(`Found ${normalizedMatches.length} (showing first ${MAX_INLINE_MATCHES}):`, previewMatches);
|
|
191
217
|
return buildToolResponse(text, previewStructured, [
|
|
192
218
|
buildResourceLink({
|
|
193
219
|
uri: entry.uri,
|
|
194
220
|
name: entry.name,
|
|
195
221
|
mimeType: entry.mimeType,
|
|
196
222
|
description: 'Full grep results as JSON (structuredContent)',
|
|
223
|
+
expiresAt: entry.expiresAt,
|
|
197
224
|
}),
|
|
198
225
|
]);
|
|
199
226
|
}
|
|
@@ -205,13 +232,12 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
205
232
|
run: async (signal) => {
|
|
206
233
|
const scope = args.filePattern;
|
|
207
234
|
const { pattern } = args;
|
|
208
|
-
const progress = createToolProgressSession(extra, `🔎︎ grep: ${pattern}
|
|
235
|
+
const progress = createToolProgressSession(extra, `🔎︎ grep: ${pattern}`);
|
|
209
236
|
const progressWithMessage = ({ current, total, }) => {
|
|
210
|
-
const fileWord = current === 1 ? 'file' : 'files';
|
|
211
237
|
progress.update({
|
|
212
238
|
current,
|
|
213
239
|
...(total !== undefined ? { total } : {}),
|
|
214
|
-
message: `🔎︎ grep: ${pattern} [${current}
|
|
240
|
+
message: `🔎︎ grep: ${pattern} [${current} files]`,
|
|
215
241
|
});
|
|
216
242
|
};
|
|
217
243
|
try {
|
|
@@ -220,37 +246,39 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
220
246
|
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
221
247
|
const filesMatched = sc.ok ? (sc.filesMatched ?? 0) : 0;
|
|
222
248
|
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const fileInfo = filesMatched > 0
|
|
230
|
-
? ` in ${filesMatched} ${filesMatched === 1 ? 'file' : 'files'}`
|
|
231
|
-
: '';
|
|
232
|
-
suffix = `${count} ${matchWord}${fileInfo}`;
|
|
233
|
-
if (stoppedReason === 'timeout') {
|
|
234
|
-
suffix += ' [stopped — timeout]';
|
|
235
|
-
}
|
|
236
|
-
else if (stoppedReason === 'maxResults') {
|
|
237
|
-
suffix += ' [truncated — max results]';
|
|
238
|
-
}
|
|
239
|
-
else if (stoppedReason === 'maxFiles') {
|
|
240
|
-
suffix += ' [truncated — max files]';
|
|
241
|
-
}
|
|
242
|
-
}
|
|
249
|
+
const suffix = buildCompletionSuffix({
|
|
250
|
+
count,
|
|
251
|
+
filesMatched,
|
|
252
|
+
scope,
|
|
253
|
+
stoppedReason,
|
|
254
|
+
});
|
|
243
255
|
const finalCurrent = resolveFinalProgressCurrent(progress, (sc.filesScanned ?? 0) + 1);
|
|
244
256
|
progress.complete(`🔎︎ grep: ${pattern} • ${suffix}`, finalCurrent);
|
|
245
257
|
return result;
|
|
246
258
|
}
|
|
247
259
|
catch (error) {
|
|
248
|
-
progress.fail(`🔎︎ grep: ${pattern}
|
|
260
|
+
progress.fail(`🔎︎ grep: ${pattern} • failed`);
|
|
249
261
|
throw error;
|
|
250
262
|
}
|
|
251
263
|
},
|
|
252
264
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? '.'),
|
|
253
265
|
});
|
|
266
|
+
function buildCompletionSuffix(params) {
|
|
267
|
+
if (params.count === 0) {
|
|
268
|
+
return `No matches in ${params.scope}`;
|
|
269
|
+
}
|
|
270
|
+
const matchWord = params.count === 1 ? 'match' : 'matches';
|
|
271
|
+
const fileWord = params.filesMatched === 1 ? 'file' : 'files';
|
|
272
|
+
const reasonLabels = {
|
|
273
|
+
timeout: 'timeout',
|
|
274
|
+
maxResults: 'max results',
|
|
275
|
+
maxFiles: 'max files',
|
|
276
|
+
};
|
|
277
|
+
const reasonSuffix = params.stoppedReason !== undefined
|
|
278
|
+
? ` [${reasonLabels[params.stoppedReason]}]`
|
|
279
|
+
: '';
|
|
280
|
+
return `${params.count} ${matchWord} in ${params.filesMatched} ${fileWord}${reasonSuffix}`;
|
|
281
|
+
}
|
|
254
282
|
const { isInitialized } = options;
|
|
255
283
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
256
284
|
guard: isInitialized,
|
|
@@ -113,17 +113,16 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
113
113
|
let progressCursor = 0;
|
|
114
114
|
notifyProgress(extra, {
|
|
115
115
|
current: 0,
|
|
116
|
-
message: `🔎︎ find: ${
|
|
116
|
+
message: `🔎︎ find: ${pattern}`,
|
|
117
117
|
});
|
|
118
118
|
const baseReporter = createProgressReporter(extra);
|
|
119
119
|
const progressWithMessage = ({ current, total, }) => {
|
|
120
120
|
if (current > progressCursor)
|
|
121
121
|
progressCursor = current;
|
|
122
|
-
const fileWord = current === 1 ? 'file' : 'files';
|
|
123
122
|
baseReporter({
|
|
124
123
|
current,
|
|
125
124
|
...(total !== undefined ? { total } : {}),
|
|
126
|
-
message: `🔎︎ find: ${pattern} [${current}
|
|
125
|
+
message: `🔎︎ find: ${pattern} [${current} files]`,
|
|
127
126
|
});
|
|
128
127
|
};
|
|
129
128
|
try {
|
|
@@ -133,18 +132,18 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
133
132
|
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
134
133
|
let suffix;
|
|
135
134
|
if (count === 0) {
|
|
136
|
-
suffix =
|
|
135
|
+
suffix = 'No matches';
|
|
137
136
|
}
|
|
138
137
|
else {
|
|
139
138
|
suffix = `${count} ${count === 1 ? 'match' : 'matches'}`;
|
|
140
139
|
if (stoppedReason === 'timeout') {
|
|
141
|
-
suffix += ' [
|
|
140
|
+
suffix += ' [timeout]';
|
|
142
141
|
}
|
|
143
142
|
else if (stoppedReason === 'maxResults') {
|
|
144
|
-
suffix += ' [
|
|
143
|
+
suffix += ' [max results]';
|
|
145
144
|
}
|
|
146
145
|
else if (stoppedReason === 'maxFiles') {
|
|
147
|
-
suffix += ' [
|
|
146
|
+
suffix += ' [max files]';
|
|
148
147
|
}
|
|
149
148
|
}
|
|
150
149
|
const finalCurrent = Math.max((sc.filesScanned ?? 0) + 1, progressCursor + 1);
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export declare function buildResourceLink(params: {
|
|
|
37
37
|
name: string;
|
|
38
38
|
mimeType?: string;
|
|
39
39
|
description?: string;
|
|
40
|
+
expiresAt?: string;
|
|
40
41
|
}): ContentBlock;
|
|
41
42
|
export declare function buildToolResponse<T>(text: string, structuredContent: T, extraContent?: ContentBlock[]): {
|
|
42
43
|
content: ContentBlock[];
|
|
@@ -128,7 +129,7 @@ export interface BatchProgressCallbacks {
|
|
|
128
129
|
progress: ToolProgressSession;
|
|
129
130
|
onItemComplete: () => void;
|
|
130
131
|
}
|
|
131
|
-
export declare function createToolProgressSession(extra: ToolExtra, startMessage: string): ToolProgressSession;
|
|
132
|
+
export declare function createToolProgressSession(extra: ToolExtra, startMessage: string, initialTotal?: number): ToolProgressSession;
|
|
132
133
|
export declare function createBatchProgressCallbacks(extra: ToolExtra, params: {
|
|
133
134
|
toolLabel: string;
|
|
134
135
|
context: string;
|