@remcp/runtime 0.2.216 → 0.2.218
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/package.json
CHANGED
|
@@ -302,7 +302,7 @@ export function buildWindowsClipboardReadPowerShell() {
|
|
|
302
302
|
export function decodeWindowsClipboardRead(stdout = '') {
|
|
303
303
|
const encoded = String(stdout).trim();
|
|
304
304
|
if (!encoded) return '';
|
|
305
|
-
return Buffer.from(encoded, 'base64').toString('utf8')
|
|
305
|
+
return Buffer.from(encoded, 'base64').toString('utf8');
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
export async function clipboard(args = {}) {
|
|
@@ -17,7 +17,7 @@ import { clampInteger, decodeText, displayPath, fail, fitsOutput, globToRegExp,
|
|
|
17
17
|
|
|
18
18
|
import { IMAGE_TYPES, MAX_IMAGE_BYTES, MAX_INLINE_FILE_BYTES, READ_NOFOLLOW_NONBLOCK, detectEol, openDirectoryPath, openEntryAtPath, openRegularFile, readRegularBuffer } from './files-secure-paths.mjs';
|
|
19
19
|
|
|
20
|
-
export async function collectTree(root, { maxFiles = 500, skip = [], signal = null } = {}) {
|
|
20
|
+
export async function collectTree(root, { maxFiles = 500, skip = [], signal = null, acceptFile = null } = {}) {
|
|
21
21
|
const found = [];
|
|
22
22
|
const denied = [];
|
|
23
23
|
const skipName = name => skip.some(entry => (entry.endsWith('*') ? name.startsWith(entry.slice(0, -1)) : name === entry));
|
|
@@ -52,7 +52,7 @@ export async function collectTree(root, { maxFiles = 500, skip = [], signal = nu
|
|
|
52
52
|
try {
|
|
53
53
|
opened = await openEntryAtPath(childAnchor, READ_NOFOLLOW_NONBLOCK);
|
|
54
54
|
const info = await opened.handle.stat();
|
|
55
|
-
if (info.isFile()) found.push(childLogical);
|
|
55
|
+
if (info.isFile() && (!acceptFile || await acceptFile(childLogical, info))) found.push(childLogical);
|
|
56
56
|
} catch {
|
|
57
57
|
denied.push(childLogical);
|
|
58
58
|
} finally {
|
|
@@ -234,12 +234,43 @@ export async function replaceInFilesTool(args, extra = {}) {
|
|
|
234
234
|
fail(`pattern is not a valid regular expression (${error instanceof Error ? error.message : String(error)})`);
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
|
-
const info = await getPathInfo(root).catch(() => fail(`Path not found: ${displayPath(root)}`));
|
|
238
|
-
const walk = info.isFile() ? { files: [root], denied: [] } : await collectTree(root, { maxFiles, skip: ['.git', 'node_modules', '.remcp-trash*'], signal: extra.signal });
|
|
239
|
-
const files = await confineAll(walk.files);
|
|
240
237
|
const glob = filePattern ? globToRegExp(filePattern) : null;
|
|
241
238
|
const changed = [];
|
|
242
239
|
let scanned = 0;
|
|
240
|
+
const match = original => {
|
|
241
|
+
if (isRegex) matcher.lastIndex = 0;
|
|
242
|
+
const count = isRegex ? (original.match(matcher) || []).length : original.split(pattern).length - 1;
|
|
243
|
+
if (!count) return null;
|
|
244
|
+
if (isRegex) matcher.lastIndex = 0;
|
|
245
|
+
const updated = isRegex ? original.replace(matcher, replacement) : original.split(pattern).join(replacement);
|
|
246
|
+
return updated === original ? null : { count, updated };
|
|
247
|
+
};
|
|
248
|
+
// Select actual candidates before writing, so maxFiles limits changed files
|
|
249
|
+
// rather than unrelated directory entries, and cancellation during discovery
|
|
250
|
+
// still cannot leave a partially edited tree.
|
|
251
|
+
const acceptFile = async (file, info) => {
|
|
252
|
+
throwIfCancelled(extra.signal);
|
|
253
|
+
if (info.size > MAX_INLINE_FILE_BYTES || (glob && !glob.test(path.basename(file)))) return false;
|
|
254
|
+
let opened;
|
|
255
|
+
try {
|
|
256
|
+
const safe = await resolveSafePath(file);
|
|
257
|
+
opened = await openRegularFile(safe, dryRun ? READ_NOFOLLOW : READWRITE_NOFOLLOW);
|
|
258
|
+
if (opened.info.size > MAX_INLINE_FILE_BYTES) return false;
|
|
259
|
+
const buffer = await opened.handle.readFile();
|
|
260
|
+
const decoded = decodeText(buffer);
|
|
261
|
+
if (decoded.encoding === 'utf8' && looksBinary(buffer)) return false;
|
|
262
|
+
scanned += 1;
|
|
263
|
+
return Boolean(match(decoded.text));
|
|
264
|
+
} catch {
|
|
265
|
+
throwIfCancelled(extra.signal);
|
|
266
|
+
return false;
|
|
267
|
+
} finally {
|
|
268
|
+
if (opened) await opened.close().catch(() => {});
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const info = await getPathInfo(root).catch(() => fail(`Path not found: ${displayPath(root)}`));
|
|
272
|
+
const walk = info.isFile() ? { files: [root], denied: [] } : await collectTree(root, { maxFiles, skip: ['.git', 'node_modules', '.remcp-trash*'], signal: extra.signal, acceptFile });
|
|
273
|
+
const files = await confineAll(walk.files);
|
|
243
274
|
for (const file of files) {
|
|
244
275
|
throwIfCancelled(extra.signal);
|
|
245
276
|
if (changed.length >= maxFiles) break;
|
|
@@ -253,13 +284,11 @@ export async function replaceInFilesTool(args, extra = {}) {
|
|
|
253
284
|
const buffer = await handle.readFile();
|
|
254
285
|
const decoded = decodeText(buffer);
|
|
255
286
|
if (decoded.encoding === 'utf8' && looksBinary(buffer)) continue;
|
|
256
|
-
scanned += 1;
|
|
287
|
+
if (info.isFile()) scanned += 1;
|
|
257
288
|
const original = decoded.text;
|
|
258
|
-
const
|
|
259
|
-
if (!
|
|
260
|
-
|
|
261
|
-
const updated = isRegex ? original.replace(matcher, replacement) : original.split(pattern).join(replacement);
|
|
262
|
-
if (updated === original) continue;
|
|
289
|
+
const result = match(original);
|
|
290
|
+
if (!result) continue;
|
|
291
|
+
const { count, updated } = result;
|
|
263
292
|
assertWritableSize(updated);
|
|
264
293
|
if (!dryRun) await overwriteOpenFile(handle, updated);
|
|
265
294
|
const stats = diffStats(original, updated);
|