@remcp/runtime 0.2.209 → 0.2.211

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.209",
3
+ "version": "0.2.211",
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",
@@ -5,6 +5,7 @@ const PERMISSION_CODES = new Set(['EACCES', 'EPERM']);
5
5
  const MISSING_CODES = new Set(['ENOENT', 'ENOTDIR']);
6
6
  const TIMEOUT_CODES = new Set(['ETIMEDOUT', 'ETIME']);
7
7
  const CONNECTION_CODES = new Set(['ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH']);
8
+ const STORAGE_CODES = new Set(['ENOSPC', 'EDQUOT']);
8
9
 
9
10
  export function classifyRuntimeToolError(error, toolName = '') {
10
11
  const code = typeof error?.code === 'string' ? error.code.toUpperCase() : '';
@@ -12,6 +13,8 @@ export function classifyRuntimeToolError(error, toolName = '') {
12
13
  const browserTool = typeof toolName === 'string' && toolName.startsWith('browser_');
13
14
  if (PERMISSION_CODES.has(code)) return 'permission';
14
15
  if (MISSING_CODES.has(code)) return 'not_found';
16
+ if (STORAGE_CODES.has(code)) return 'resource_exhausted';
17
+ if (code === 'EBUSY') return 'busy';
15
18
  if (TIMEOUT_CODES.has(code)) return 'timeout';
16
19
  if (browserTool && CONNECTION_CODES.has(code)) return 'browser_unavailable';
17
20
  if (browserTool && code === 'BROWSER_TARGET_DISAPPEARED') return 'browser_target_stale';
@@ -37,6 +40,7 @@ export function classifyRuntimeToolError(error, toolName = '') {
37
40
  }
38
41
  }
39
42
  if (/permission denied|access is denied|not authorized|not authorised|accessibility permission/.test(message)) return 'permission';
43
+ if (/\b(?:file|path|source) not found\b|\bis not a directory\b/.test(message)) return 'not_found';
40
44
  if (/timed out|timeout after|time limit exceeded/.test(message)) return 'timeout';
41
45
  if (/element not found|no element matches|no matching ui window|target not found|not found in the current/.test(message)) return 'not_found';
42
46
  if (/requires .*|must be (?:a |an |positive |between )|invalid (?:argument|selector|field|parameter)/.test(message)) return 'invalid_input';
@@ -19,15 +19,17 @@ export function macosProtectedLocation(target, { platform = process.platform, ho
19
19
  if (platform !== 'darwin') return null;
20
20
  const value = typeof target === 'string' ? target.trim() : '';
21
21
  if (!value) return null;
22
- const resolved = path.resolve(value);
23
- if (resolved === '/Volumes' || resolved.startsWith(`/Volumes${path.sep}`)) return 'an external or network volume';
22
+ // Evaluate macOS paths with POSIX semantics even when diagnostics/tests run on
23
+ // Windows. Using the host path module turns /Users/... into a Windows path.
24
+ const resolved = path.posix.resolve(value);
25
+ if (resolved === '/Volumes' || resolved.startsWith('/Volumes/')) return 'an external or network volume';
24
26
  // The kernel reports the absolute path, so a folder can be recognised even when it belongs to
25
27
  // another account: every /Users/<name>/Desktop is somebody's Desktop, and it is protected for the
26
28
  // process that is asking, whatever this runtime's own home directory is.
27
- const segments = resolved.split(path.sep);
28
- const relative = home ? path.relative(path.resolve(home), resolved) : '';
29
- const insideHome = relative && !relative.startsWith('..') && !path.isAbsolute(relative);
30
- const candidate = insideHome ? relative.split(path.sep) : segments[0] === '' && segments[1] === 'Users' ? segments.slice(3) : null;
29
+ const segments = resolved.split(path.posix.sep);
30
+ const relative = home ? path.posix.relative(path.posix.resolve(home), resolved) : '';
31
+ const insideHome = relative && !relative.startsWith('..') && !path.posix.isAbsolute(relative);
32
+ const candidate = insideHome ? relative.split(path.posix.sep) : segments[0] === '' && segments[1] === 'Users' ? segments.slice(3) : null;
31
33
  if (!candidate?.length) return null;
32
34
  const [first, second] = candidate;
33
35
  if (MACOS_HOME_FOLDERS.includes(first)) return `~/${first}`;
@@ -63,6 +65,9 @@ export function filesystemErrorExplanation(error, options = {}) {
63
65
  const code = typeof error?.code === 'string' ? error.code : '';
64
66
  const where = typeof target === 'string' && target.trim() ? target.trim() : 'that path';
65
67
  if (code === 'ENOSPC') return 'The disk is full (ENOSPC). Free some space or write to another volume.';
68
+ if (code === 'EDQUOT') return 'The disk or temporary-file quota is exhausted (EDQUOT). Free quota space or select another writable volume.';
69
+ if (code === 'ENOENT') return `The file or folder does not exist (ENOENT): ${where}. Check its name and drive letter.`;
70
+ if (code === 'ENOTDIR') return `A component of ${where} is a file instead of a directory (ENOTDIR). Check the parent path.`;
66
71
  if (code === 'EROFS') return `${where} is on a read-only file system (EROFS). Write somewhere else, or remount it read-write.`;
67
72
  if (code === 'EBUSY') return `${where} is in use by another program (EBUSY). Close whatever holds it and try again.`;
68
73
  if (!PERMISSION_CODES.has(code)) return null;
@@ -399,7 +399,11 @@ export async function moveFileTool(args) {
399
399
  export async function copyFileTool(args) {
400
400
  const source = await resolveSafePath(args.source, 'source');
401
401
  const destination = await resolveSafePath(args.destination, 'destination');
402
- if (source === destination) fail('source and destination are the same path');
402
+ // Windows paths normally refer to the same file irrespective of case.
403
+ // Refuse to overwrite the still-open source via a differently cased name.
404
+ if (source === destination || (process.platform === 'win32' && source.toLowerCase() === destination.toLowerCase())) {
405
+ fail('source and destination are the same path');
406
+ }
403
407
  const overwrite = args.overwrite !== false;
404
408
  let info;
405
409
  await withExistingParent(source, async anchoredSource => {
@@ -309,6 +309,14 @@ export async function openPathForInfo(absolute) {
309
309
  }
310
310
  if (!canTryFile) throw directoryError;
311
311
  }
312
+ // Metadata on Windows must not require opening a file for reading. Another
313
+ // process may deny FILE_SHARE_READ while lstat still succeeds (e.g. Office).
314
+ // lstat also does not traverse a final symbolic link or junction.
315
+ if (process.platform === 'win32') {
316
+ const info = await lstat(absolute);
317
+ if (info.isSymbolicLink()) fail('Path is a symbolic link');
318
+ if (info.isFile()) return { info, close: async () => {} };
319
+ }
312
320
  const opened = await openEntryAtPath(absolute, READ_NOFOLLOW_NONBLOCK);
313
321
  try {
314
322
  return { info: await opened.handle.stat(), close: opened.close };