@remcp/runtime 0.2.24 → 0.2.26
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 +1 -1
- package/package.json +2 -2
- package/src/permissions.mjs +23 -0
- package/src/tools/files.mjs +37 -14
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ReMCP local runtime
|
|
2
2
|
|
|
3
|
-
`@remcp/runtime` is the local device runtime for [ReMCP](https://remcp.
|
|
3
|
+
`@remcp/runtime` is the local device runtime for [ReMCP](https://remcp.site). It is an MCP
|
|
4
4
|
server that runs on a computer you paired with ReMCP and executes the file, image, search, terminal,
|
|
5
5
|
and process tools that the hosted ReMCP MCP endpoint exposes to ChatGPT and Codex.
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remcp/runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.26",
|
|
4
4
|
"description": "First-party ReMCP local device runtime: file, search, terminal and process tools over MCP for computers paired with ReMCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"homepage": "https://remcp.
|
|
7
|
+
"homepage": "https://remcp.site",
|
|
8
8
|
"author": "Anton Baider",
|
|
9
9
|
"bin": {
|
|
10
10
|
"remcp-runtime": "src/index.mjs"
|
package/src/permissions.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import path from 'node:path';
|
|
|
9
9
|
// System Settings → Privacy & Security → Full Disk Access → + → the binary that runs the tools
|
|
10
10
|
// These helpers turn those errnos into that instruction. They never widen access — they explain it.
|
|
11
11
|
const MACOS_HOME_FOLDERS = Object.freeze(['Desktop', 'Documents', 'Downloads']);
|
|
12
|
+
const WINDOWS_HOME_FOLDERS = Object.freeze([['Desktop'], ['Documents'], ['Downloads'], ['OneDrive', 'Desktop'], ['OneDrive', 'Documents']]);
|
|
12
13
|
const PERMISSION_CODES = new Set(['EACCES', 'EPERM']);
|
|
13
14
|
|
|
14
15
|
// The protected location a path belongs to ("~/Desktop", "iCloud Drive", "an external volume"), or
|
|
@@ -34,6 +35,20 @@ export function macosProtectedLocation(target, { platform = process.platform, ho
|
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
// Windows 11 ships the same idea as TCC under a different name: Controlled folder access (Windows
|
|
39
|
+
// Security → Virus & threat protection → Ransomware protection) blocks Desktop, Documents and
|
|
40
|
+
// Downloads for every application that is not on its allow list, and the process sees EPERM/EACCES.
|
|
41
|
+
export function windowsProtectedLocation(target, { platform = process.platform, home = os.homedir() } = {}) {
|
|
42
|
+
if (platform !== 'win32') return null;
|
|
43
|
+
const value = typeof target === 'string' ? target.trim() : '';
|
|
44
|
+
if (!value || !home) return null;
|
|
45
|
+
const relative = path.win32.relative(path.win32.resolve(home), path.win32.resolve(value));
|
|
46
|
+
if (!relative || relative.startsWith('..') || path.win32.isAbsolute(relative)) return null;
|
|
47
|
+
const parts = relative.split(path.win32.sep);
|
|
48
|
+
const match = WINDOWS_HOME_FOLDERS.find(entry => entry.every((segment, index) => (parts[index] || '').toLowerCase() === segment.toLowerCase()));
|
|
49
|
+
return match ? `%USERPROFILE%\\${match.join('\\')}` : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
// What the person should do about this failure, or null when the error is not a filesystem permission
|
|
38
53
|
// problem this module has better words for.
|
|
39
54
|
export function filesystemErrorExplanation(error, options = {}) {
|
|
@@ -49,14 +64,22 @@ export function filesystemErrorExplanation(error, options = {}) {
|
|
|
49
64
|
const where = typeof target === 'string' && target.trim() ? target.trim() : 'that path';
|
|
50
65
|
if (code === 'ENOSPC') return 'The disk is full (ENOSPC). Free some space or write to another volume.';
|
|
51
66
|
if (code === 'EROFS') return `${where} is on a read-only file system (EROFS). Write somewhere else, or remount it read-write.`;
|
|
67
|
+
if (code === 'EBUSY') return `${where} is in use by another program (EBUSY). Close whatever holds it and try again.`;
|
|
52
68
|
if (!PERMISSION_CODES.has(code)) return null;
|
|
53
69
|
const protectedLocation = macosProtectedLocation(target, { platform, home });
|
|
54
70
|
if (protectedLocation) {
|
|
55
71
|
return `macOS protects ${protectedLocation} (${code}), and ReMCP runs as a background service, which cannot show the permission prompt. Grant it by hand: System Settings → Privacy & Security → Full Disk Access → + → ${execPath}, then restart the agent with \`remcp start\`. A folder outside Desktop, Documents, Downloads and iCloud Drive needs no new permission.`;
|
|
56
72
|
}
|
|
73
|
+
const windowsLocation = windowsProtectedLocation(target, { platform, home });
|
|
74
|
+
if (windowsLocation) {
|
|
75
|
+
return `Windows Controlled folder access is blocking ${windowsLocation} (${code}). Add the binary that runs the tools (${execPath}) under Windows Security → Virus & threat protection → Ransomware protection → Allow an app through Controlled folder access, then restart the agent with \`remcp start\`. A folder outside Desktop, Documents, Downloads and OneDrive needs no new permission.`;
|
|
76
|
+
}
|
|
57
77
|
if (platform === 'darwin') {
|
|
58
78
|
return `macOS denied access to ${where} (${code}). Check the ownership and permissions of that folder, or add ${execPath} to System Settings → Privacy & Security → Full Disk Access and restart the agent with \`remcp start\`.`;
|
|
59
79
|
}
|
|
80
|
+
if (platform === 'win32') {
|
|
81
|
+
return `Windows denied access to ${where} (${code}). Check whether the file is read-only, open in another program, or inside a Controlled folder access area, then allow ${execPath} through Windows Security (Controlled folder access) and restart the agent with \`remcp start\`.`;
|
|
82
|
+
}
|
|
60
83
|
return `The operating system denied access to ${where} (${code}). Check that this user owns the folder and its parents, or choose another path.`;
|
|
61
84
|
}
|
|
62
85
|
|
package/src/tools/files.mjs
CHANGED
|
@@ -14,9 +14,10 @@ import { countEvent, recordEvent } from '../telemetry.mjs';
|
|
|
14
14
|
import { clampInteger, decodeText, displayPath, fail, globToRegExp, image, looksBinary, multi, pageLines, resolveSafePath, splitLines, text } from '../util.mjs';
|
|
15
15
|
|
|
16
16
|
const MAX_INLINE_FILE_BYTES = 20 * 1024 * 1024;
|
|
17
|
-
// An image travels base64-encoded, which costs a third more bytes
|
|
18
|
-
// the
|
|
19
|
-
//
|
|
17
|
+
// An image travels base64-encoded, which costs a third more bytes. The agent's stdio transport holds
|
|
18
|
+
// 24 MiB and the relay's WebSocket frames hold 32 MiB, so 8 MiB of image is ~11 MiB on the wire with
|
|
19
|
+
// room to spare; anything larger goes through read_binary in chunks instead. Binary chunks are 4 MiB
|
|
20
|
+
// for the same reason: four times fewer round trips for the same file.
|
|
20
21
|
const MAX_IMAGE_BYTES = 4 * 1024 * 1024;
|
|
21
22
|
const MAX_BINARY_CHUNK_BYTES = 1024 * 1024;
|
|
22
23
|
const IMAGE_TYPES = new Map([
|
|
@@ -921,6 +922,26 @@ const SCREENSHOT_COMMANDS = [
|
|
|
921
922
|
{ command: 'screencapture', args: file => ['-x', file] },
|
|
922
923
|
];
|
|
923
924
|
|
|
925
|
+
// What to tell the person when no capture worked. The general "install a screenshot tool" line was
|
|
926
|
+
// wrong on a Mac, where `screencapture` ships with the system and fails only because TCC has not
|
|
927
|
+
// granted Screen Recording — the exact failure behind "the screenshot returned an error" reports.
|
|
928
|
+
function screenshotAdvice(attempts, { platform = process.platform, env = process.env, execPath = process.execPath } = {}) {
|
|
929
|
+
if (platform === 'darwin') {
|
|
930
|
+
return `macOS refused the screen capture (${attempts.join('; ') || 'no capture command ran'}). Grant Screen Recording to the binary that runs the tools — System Settings → Privacy & Security → Screen Recording → + → ${execPath} — then restart the agent with \`remcp start\`. macOS requires it for screencapture even when the file itself is writable.`;
|
|
931
|
+
}
|
|
932
|
+
if (platform === 'win32') {
|
|
933
|
+
return `Windows refused the screen capture (${attempts.join('; ') || 'no capture command ran'}). Screen capture needs an interactive desktop session: a machine where nobody is signed in, or a locked session, cannot be captured. Sign in on that computer and try again.`;
|
|
934
|
+
}
|
|
935
|
+
const wayland = /wayland/i.test(String(env.XDG_SESSION_TYPE || '')) || Boolean(env.WAYLAND_DISPLAY);
|
|
936
|
+
if (!env.DISPLAY && !wayland) {
|
|
937
|
+
return `This computer has no graphical session (no DISPLAY and no Wayland display), so there is nothing to capture — servers and containers usually have none.`;
|
|
938
|
+
}
|
|
939
|
+
if (wayland) {
|
|
940
|
+
return `Could not capture the screen on Wayland (${attempts.join('; ') || 'no capture command ran'}). Install \`grim\` (Wayland's capture tool) — X11 tools such as scrot or ImageMagick import cannot read a Wayland session.`;
|
|
941
|
+
}
|
|
942
|
+
return `Could not capture the screen. Install one of grim, gnome-screenshot, spectacle, scrot, or ImageMagick import (tried: ${attempts.join('; ') || 'none available'}).`;
|
|
943
|
+
}
|
|
944
|
+
|
|
924
945
|
function windowsScreenshotScript(file) {
|
|
925
946
|
return [
|
|
926
947
|
'Add-Type -AssemblyName System.Windows.Forms,System.Drawing',
|
|
@@ -949,19 +970,21 @@ export async function takeScreenshotTool(args) {
|
|
|
949
970
|
}
|
|
950
971
|
}
|
|
951
972
|
if (!await pathExists(file)) {
|
|
952
|
-
fail(
|
|
973
|
+
fail(screenshotAdvice(attempts));
|
|
953
974
|
}
|
|
954
975
|
const info = await stat(file);
|
|
955
|
-
if (info.size
|
|
956
|
-
await
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
976
|
+
if (info.size <= MAX_IMAGE_BYTES) {
|
|
977
|
+
const buffer = await readFile(file);
|
|
978
|
+
if (args.keep !== true) await rm(file, { force: true });
|
|
979
|
+
return multi([
|
|
980
|
+
{ type: 'text', text: `Screenshot of ${os.hostname()} (${info.size} bytes)${args.keep === true ? ` saved at ${displayPath(file)}` : ''}` },
|
|
981
|
+
image(buffer.toString('base64'), 'image/png'),
|
|
982
|
+
]);
|
|
983
|
+
}
|
|
984
|
+
// A big screen is not an error: the PNG stays on the computer and the model is told how to fetch it
|
|
985
|
+
// in chunks, which is the same path every other large file takes. Failing here used to lose the
|
|
986
|
+
// screenshot entirely.
|
|
987
|
+
return text(`Screenshot of ${os.hostname()} captured: ${info.size} bytes, above the ${MAX_IMAGE_BYTES}-byte inline limit, so it is saved at ${displayPath(file)} instead of being returned as an image. Fetch it with read_binary using chunks of up to ${MAX_BINARY_CHUNK_BYTES} bytes (offset_bytes and length_bytes), or ask for a smaller region.`);
|
|
965
988
|
}
|
|
966
989
|
|
|
967
990
|
export const fileToolHandlers = {
|