@remcp/runtime 0.2.217 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.217",
3
+ "version": "0.2.218",
4
4
  "description": "First-party ReMCP local device runtime: filesystem, terminal, computer-use, browser, diagnostics and document tools over MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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').replace(/\r?\n$/, '');
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 count = isRegex ? (original.match(matcher) || []).length : original.split(pattern).length - 1;
259
- if (!count) continue;
260
- if (isRegex) matcher.lastIndex = 0;
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);