@remcp/remcp 0.1.3 → 0.2.4
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 +41 -113
- package/package.json +10 -24
- package/src/agent.mjs +215 -36
- package/src/cli.mjs +217 -43
- package/src/runtime.mjs +15 -0
- package/src/version.mjs +1 -3
- package/THIRD_PARTY_NOTICES.md +0 -7
- package/assets/remcp-icon.png +0 -0
- package/mcp.json +0 -9
- package/plugin.json +0 -37
- package/public/android-chrome-192x192.png +0 -0
- package/public/android-chrome-512x512.png +0 -0
- package/public/app.js +0 -96
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/art/hero-relay.webp +0 -0
- package/public/assets/art/relay-detail.webp +0 -0
- package/public/assets/icon-120.png +0 -0
- package/public/assets/icon-16.png +0 -0
- package/public/assets/icon-180.png +0 -0
- package/public/assets/icon-192.png +0 -0
- package/public/assets/icon-256.png +0 -0
- package/public/assets/icon-32.png +0 -0
- package/public/assets/icon-48.png +0 -0
- package/public/assets/icon-512.png +0 -0
- package/public/assets/og-remcp.png +0 -0
- package/public/authorize.html +0 -59
- package/public/copy.js +0 -40
- package/public/docs.html +0 -93
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon-48x48.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +0 -134
- package/public/oauth-app.js +0 -63
- package/public/privacy.html +0 -80
- package/public/remcp-darkmode-16x16.png +0 -0
- package/public/remcp-darkmode-192x192.png +0 -0
- package/public/remcp-darkmode-32x32.png +0 -0
- package/public/remcp-darkmode-48x48.png +0 -0
- package/public/remcp-darkmode-512x512.png +0 -0
- package/public/security.html +0 -70
- package/public/site.webmanifest +0 -23
- package/public/style.css +0 -261
- package/public/support.html +0 -75
- package/public/terms.html +0 -77
- package/skills/remcp-operator/SKILL.md +0 -36
- package/src/auth.mjs +0 -71
- package/src/config.mjs +0 -51
- package/src/db.mjs +0 -102
- package/src/mcp.mjs +0 -95
- package/src/oauth.mjs +0 -164
- package/src/relay.mjs +0 -97
- package/src/review-sandbox.mjs +0 -96
- package/src/server.mjs +0 -159
- package/src/tool-catalog.json +0 -796
- package/src/util.mjs +0 -24
package/src/cli.mjs
CHANGED
|
@@ -3,24 +3,34 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
6
7
|
import { runAgent } from './agent.mjs';
|
|
7
|
-
import {
|
|
8
|
+
import { normalizeRuntime } from './runtime.mjs';
|
|
9
|
+
import { PACKAGE_NAME, VERSION } from './version.mjs';
|
|
8
10
|
|
|
9
11
|
const home = os.homedir();
|
|
10
12
|
const configDir = process.env.REMCP_CONFIG_DIR || path.join(home, '.config', 'remcp');
|
|
11
13
|
const configFile = path.join(configDir, 'config.json');
|
|
12
|
-
const
|
|
14
|
+
const runtimeConfigFile = path.join(configDir, 'runtime.json');
|
|
15
|
+
const machineIdFile = path.join(configDir, 'machine-id');
|
|
16
|
+
const linuxServiceFile = path.join(home, '.config', 'systemd', 'user', 'remcp-agent.service');
|
|
17
|
+
const macServiceLabel = 'com.remcp.agent';
|
|
18
|
+
const macServiceFile = path.join(home, 'Library', 'LaunchAgents', `${macServiceLabel}.plist`);
|
|
19
|
+
const macLogFile = path.join(home, 'Library', 'Logs', 'remcp-agent.log');
|
|
20
|
+
const windowsTaskName = 'ReMCP Agent';
|
|
13
21
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
22
|
+
const officialOrigin = 'https://remcp.delio24.com';
|
|
14
23
|
|
|
15
24
|
function parse(argv) {
|
|
16
25
|
const [command = 'help', ...rest] = argv;
|
|
17
26
|
const flags = {};
|
|
27
|
+
const positional = [];
|
|
18
28
|
for (let i = 0; i < rest.length; i++) {
|
|
19
|
-
if (!rest[i].startsWith('--')) continue;
|
|
29
|
+
if (!rest[i].startsWith('--')) { positional.push(rest[i]); continue; }
|
|
20
30
|
const key = rest[i].slice(2);
|
|
21
31
|
flags[key] = rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[++i] : true;
|
|
22
32
|
}
|
|
23
|
-
return { command, flags };
|
|
33
|
+
return { command, flags, positional };
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
function run(command, args, options = {}) {
|
|
@@ -37,9 +47,14 @@ function output(command, args) {
|
|
|
37
47
|
return String(result.stdout || '').trim();
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
function loadConfig() {
|
|
41
|
-
if (!fs.existsSync(configFile))
|
|
42
|
-
|
|
50
|
+
function loadConfig(required = true) {
|
|
51
|
+
if (!fs.existsSync(configFile)) {
|
|
52
|
+
if (!required) return undefined;
|
|
53
|
+
throw new Error(`ReMCP is not paired. Generate a pairing command at ${officialOrigin}/app/connect`);
|
|
54
|
+
}
|
|
55
|
+
const value = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
56
|
+
value.runtime = normalizeRuntime(value.runtime);
|
|
57
|
+
return value;
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
function saveConfig(value) {
|
|
@@ -48,58 +63,178 @@ function saveConfig(value) {
|
|
|
48
63
|
fs.chmodSync(configFile, 0o600);
|
|
49
64
|
}
|
|
50
65
|
|
|
66
|
+
function readJsonFile(file) {
|
|
67
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return {}; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeJsonFile(file, value) {
|
|
71
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
72
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
|
|
73
|
+
fs.chmodSync(file, 0o600);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function flagEnabled(value) {
|
|
77
|
+
return value === undefined || value === null ? true : value !== false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// One switch for the whole machine: the client and the runtime it spawns must agree,
|
|
81
|
+
// otherwise the runtime would keep reporting after the user opted out.
|
|
82
|
+
function telemetryState() {
|
|
83
|
+
const client = readJsonFile(configFile);
|
|
84
|
+
const runtime = readJsonFile(runtimeConfigFile);
|
|
85
|
+
const clientEnabled = flagEnabled(client.telemetryEnabled);
|
|
86
|
+
const runtimeEnabled = flagEnabled(runtime.telemetryEnabled);
|
|
87
|
+
return {
|
|
88
|
+
enabled: clientEnabled && runtimeEnabled,
|
|
89
|
+
clientEnabled,
|
|
90
|
+
runtimeEnabled,
|
|
91
|
+
installReported: client.installReported === true,
|
|
92
|
+
configFile,
|
|
93
|
+
runtimeConfigFile,
|
|
94
|
+
transport: 'paired-agent-only',
|
|
95
|
+
endpoint: null,
|
|
96
|
+
collects: 'tool names, durations, outcomes, error classes, and device health samples',
|
|
97
|
+
neverCollects: 'file paths, file contents, command strings, tool arguments, and tool output',
|
|
98
|
+
thirdParty: false,
|
|
99
|
+
installPing: false,
|
|
100
|
+
remoteFeatureFlags: false,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function setTelemetry(enabled) {
|
|
105
|
+
const client = readJsonFile(configFile);
|
|
106
|
+
client.telemetryEnabled = enabled;
|
|
107
|
+
writeJsonFile(configFile, client);
|
|
108
|
+
const runtime = readJsonFile(runtimeConfigFile);
|
|
109
|
+
runtime.telemetryEnabled = enabled;
|
|
110
|
+
writeJsonFile(runtimeConfigFile, runtime);
|
|
111
|
+
return telemetryState();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ensureMachineId() {
|
|
115
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
116
|
+
try {
|
|
117
|
+
const existing = fs.readFileSync(machineIdFile, 'utf8').trim();
|
|
118
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(existing)) return existing;
|
|
119
|
+
} catch {}
|
|
120
|
+
const value = randomUUID();
|
|
121
|
+
fs.writeFileSync(machineIdFile, value + '\n', { mode: 0o600 });
|
|
122
|
+
fs.chmodSync(machineIdFile, 0o600);
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function servicePlatform() {
|
|
127
|
+
return process.env.NODE_ENV === 'test' && process.env.REMCP_TEST_PLATFORM ? process.env.REMCP_TEST_PLATFORM : process.platform;
|
|
128
|
+
}
|
|
129
|
+
|
|
51
130
|
function globalPrefix() {
|
|
52
131
|
return output(npmCommand, ['prefix', '--global']);
|
|
53
132
|
}
|
|
54
133
|
|
|
55
134
|
function globalCliPath() {
|
|
56
135
|
const prefix = globalPrefix();
|
|
57
|
-
return
|
|
136
|
+
return servicePlatform() === 'win32' ? path.join(prefix, 'remcp.cmd') : path.join(prefix, 'bin', 'remcp');
|
|
58
137
|
}
|
|
59
138
|
|
|
60
|
-
function npmGlobalInstall(
|
|
61
|
-
run(npmCommand, ['install', '--global',
|
|
139
|
+
function npmGlobalInstall(...specs) {
|
|
140
|
+
run(npmCommand, ['install', '--global', ...specs, '--no-audit', '--no-fund', '--loglevel=error']);
|
|
62
141
|
}
|
|
63
142
|
|
|
64
143
|
function quoteSystemd(value) {
|
|
65
144
|
return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
66
145
|
}
|
|
67
146
|
|
|
147
|
+
function xmlEscape(value) {
|
|
148
|
+
return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
149
|
+
}
|
|
150
|
+
|
|
68
151
|
function installLinuxService(cliPath = globalCliPath()) {
|
|
69
152
|
const unit = `[Unit]\nDescription=ReMCP device agent\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${quoteSystemd(cliPath)} start\nRestart=always\nRestartSec=3\nNoNewPrivileges=true\n\n[Install]\nWantedBy=default.target\n`;
|
|
70
|
-
fs.mkdirSync(path.dirname(
|
|
71
|
-
fs.writeFileSync(
|
|
153
|
+
fs.mkdirSync(path.dirname(linuxServiceFile), { recursive: true });
|
|
154
|
+
fs.writeFileSync(linuxServiceFile, unit);
|
|
72
155
|
run('systemctl', ['--user', 'daemon-reload']);
|
|
73
156
|
run('systemctl', ['--user', 'enable', '--now', 'remcp-agent.service']);
|
|
74
157
|
}
|
|
75
158
|
|
|
76
|
-
function
|
|
77
|
-
if (process.
|
|
78
|
-
|
|
79
|
-
npmGlobalInstall(`${PACKAGE_NAME}@${VERSION}`);
|
|
80
|
-
installLinuxService(globalCliPath());
|
|
81
|
-
console.log('ReMCP is installed as a user service. Future updates: remcp update');
|
|
159
|
+
function macLaunchDomain() {
|
|
160
|
+
if (typeof process.getuid !== 'function') throw new Error('Could not determine the current macOS user');
|
|
161
|
+
return `gui/${process.getuid()}`;
|
|
82
162
|
}
|
|
83
163
|
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
164
|
+
function installMacService(cliPath = globalCliPath()) {
|
|
165
|
+
const domain = macLaunchDomain();
|
|
166
|
+
const target = `${domain}/${macServiceLabel}`;
|
|
167
|
+
const cliScript = fs.realpathSync(cliPath);
|
|
168
|
+
fs.mkdirSync(path.dirname(macServiceFile), { recursive: true });
|
|
169
|
+
fs.mkdirSync(path.dirname(macLogFile), { recursive: true });
|
|
170
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>\n<key>Label</key><string>${macServiceLabel}</string>\n<key>ProgramArguments</key><array><string>${xmlEscape(process.execPath)}</string><string>${xmlEscape(cliScript)}</string><string>start</string></array>\n<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>\n<key>ProcessType</key><string>Background</string>\n<key>StandardOutPath</key><string>${xmlEscape(macLogFile)}</string>\n<key>StandardErrorPath</key><string>${xmlEscape(macLogFile)}</string>\n</dict></plist>\n`;
|
|
171
|
+
fs.writeFileSync(macServiceFile, plist, { mode: 0o600 });
|
|
172
|
+
spawnSync('launchctl', ['bootout', domain, macServiceFile], { stdio: 'ignore' });
|
|
173
|
+
run('launchctl', ['bootstrap', domain, macServiceFile]);
|
|
174
|
+
run('launchctl', ['enable', target]);
|
|
175
|
+
run('launchctl', ['kickstart', '-k', target]);
|
|
88
176
|
}
|
|
89
177
|
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
178
|
+
function installWindowsService(cliPath = globalCliPath()) {
|
|
179
|
+
const command = `"${cliPath}" start`;
|
|
180
|
+
run('schtasks.exe', ['/Create', '/TN', windowsTaskName, '/TR', command, '/SC', 'ONLOGON', '/RL', 'LIMITED', '/F']);
|
|
181
|
+
run('schtasks.exe', ['/Run', '/TN', windowsTaskName]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function installPersistentAgent(config) {
|
|
185
|
+
const platform = servicePlatform();
|
|
186
|
+
if (!['linux', 'darwin', 'win32'].includes(platform)) throw new Error(`Automatic background service installation is not supported on ${platform}`);
|
|
187
|
+
console.log(`Installing ReMCP ${VERSION}…`);
|
|
188
|
+
npmGlobalInstall(`${PACKAGE_NAME}@${VERSION}`, config.runtime.packageSpec);
|
|
189
|
+
const cliPath = globalCliPath();
|
|
190
|
+
if (platform === 'linux') installLinuxService(cliPath);
|
|
191
|
+
else if (platform === 'darwin') installMacService(cliPath);
|
|
192
|
+
else installWindowsService(cliPath);
|
|
193
|
+
console.log('ReMCP is installed as a background service. Future updates: remcp update');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function restartPersistentServiceIfInstalled() {
|
|
197
|
+
const platform = servicePlatform();
|
|
198
|
+
if (platform === 'linux' && fs.existsSync(linuxServiceFile)) {
|
|
199
|
+
run('systemctl', ['--user', 'daemon-reload']);
|
|
200
|
+
run('systemctl', ['--user', 'restart', 'remcp-agent.service']);
|
|
201
|
+
} else if (platform === 'darwin' && fs.existsSync(macServiceFile)) {
|
|
202
|
+
run('launchctl', ['kickstart', '-k', `${macLaunchDomain()}/${macServiceLabel}`]);
|
|
203
|
+
} else if (platform === 'win32') {
|
|
204
|
+
const result = spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName], { stdio: 'ignore' });
|
|
205
|
+
if (result.status === 0) run('schtasks.exe', ['/Run', '/TN', windowsTaskName]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function uninstallPersistentService() {
|
|
210
|
+
const platform = servicePlatform();
|
|
211
|
+
if (platform === 'linux') {
|
|
212
|
+
spawnSync('systemctl', ['--user', 'disable', '--now', 'remcp-agent.service'], { stdio: 'inherit' });
|
|
213
|
+
try { fs.unlinkSync(linuxServiceFile); } catch {}
|
|
214
|
+
spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'inherit' });
|
|
215
|
+
} else if (platform === 'darwin') {
|
|
216
|
+
const domain = macLaunchDomain();
|
|
217
|
+
spawnSync('launchctl', ['bootout', domain, macServiceFile], { stdio: 'ignore' });
|
|
218
|
+
try { fs.unlinkSync(macServiceFile); } catch {}
|
|
219
|
+
} else if (platform === 'win32') {
|
|
220
|
+
spawnSync('schtasks.exe', ['/End', '/TN', windowsTaskName], { stdio: 'ignore' });
|
|
221
|
+
spawnSync('schtasks.exe', ['/Delete', '/TN', windowsTaskName, '/F'], { stdio: 'ignore' });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function assertRuntimeTrust(server, flags) {
|
|
226
|
+
const origin = new URL(server).origin;
|
|
227
|
+
if (origin !== officialOrigin && !flags['trust-runtime']) {
|
|
228
|
+
throw new Error('Custom servers can provide local runtime metadata. Re-run with --trust-runtime only if you trust that server.');
|
|
229
|
+
}
|
|
95
230
|
}
|
|
96
231
|
|
|
97
232
|
function printHelp() {
|
|
98
|
-
console.log(`ReMCP ${VERSION}\n\nCommands:\n
|
|
233
|
+
console.log(`ReMCP ${VERSION}\n\nCommands:\n remcp start\n remcp status\n remcp doctor\n remcp update\n remcp install\n remcp uninstall\n remcp uninstall --purge\n remcp telemetry [status|on|off]\n remcp --version\n\nPairing commands are generated in the ReMCP workspace.\n\nUsage metrics are opt-out (tool names, timings, outcomes only, sent to your own ReMCP\naccount through the paired agent). Disable them at any time with: remcp telemetry off`);
|
|
99
234
|
}
|
|
100
235
|
|
|
101
236
|
export async function main(argv = process.argv.slice(2)) {
|
|
102
|
-
const { command, flags } = parse(argv);
|
|
237
|
+
const { command, flags, positional } = parse(argv);
|
|
103
238
|
|
|
104
239
|
if (command === '--version' || command === '-v' || command === 'version') {
|
|
105
240
|
console.log(VERSION);
|
|
@@ -108,52 +243,91 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
108
243
|
|
|
109
244
|
if (command === 'connect') {
|
|
110
245
|
const server = String(flags.server || '').replace(/\/$/, '');
|
|
111
|
-
const code = String(flags.code || '').toUpperCase();
|
|
246
|
+
const code = String(flags.code || '').replace(/\s+/g, '').toUpperCase();
|
|
112
247
|
if (!server || !code) throw new Error('--server and --code are required');
|
|
248
|
+
assertRuntimeTrust(server, flags);
|
|
113
249
|
const response = await fetch(`${server}/api/pair/claim`, {
|
|
114
250
|
method: 'POST',
|
|
115
251
|
headers: { 'content-type': 'application/json' },
|
|
116
|
-
body: JSON.stringify({ code, name: String(flags.name || os.hostname()), hostname: os.hostname(), platform: process.platform, arch: process.arch }),
|
|
252
|
+
body: JSON.stringify({ code, machineId: ensureMachineId(), name: String(flags.name || os.hostname()), hostname: os.hostname(), platform: process.platform, arch: process.arch }),
|
|
117
253
|
});
|
|
118
254
|
if (!response.ok) throw new Error(`Pairing failed (${response.status}): ${await response.text()}`);
|
|
119
255
|
const paired = await response.json();
|
|
120
|
-
|
|
256
|
+
const config = {
|
|
257
|
+
serverUrl: server,
|
|
258
|
+
deviceId: paired.deviceId,
|
|
259
|
+
deviceToken: paired.deviceToken,
|
|
260
|
+
deviceName: String(flags.name || os.hostname()),
|
|
261
|
+
runtime: normalizeRuntime(paired.runtime),
|
|
262
|
+
machineId: ensureMachineId(),
|
|
263
|
+
};
|
|
264
|
+
saveConfig(config);
|
|
121
265
|
console.log(`Paired ${os.hostname()} with ${server}`);
|
|
122
|
-
if (flags.install) installPersistentAgent();
|
|
266
|
+
if (flags.install) installPersistentAgent(config);
|
|
123
267
|
return;
|
|
124
268
|
}
|
|
125
269
|
|
|
126
270
|
if (command === 'start') {
|
|
127
|
-
|
|
271
|
+
const cfg = loadConfig();
|
|
272
|
+
const telemetry = telemetryState();
|
|
273
|
+
await runAgent({
|
|
274
|
+
...cfg,
|
|
275
|
+
telemetryEnabled: telemetry.enabled,
|
|
276
|
+
installReported: telemetry.installReported,
|
|
277
|
+
installSpec: `${PACKAGE_NAME}@${VERSION}`,
|
|
278
|
+
persistState: patch => saveConfig({ ...cfg, ...patch }),
|
|
279
|
+
});
|
|
128
280
|
return;
|
|
129
281
|
}
|
|
130
282
|
|
|
283
|
+
if (command === 'telemetry') {
|
|
284
|
+
const action = String(positional[0] || 'status').toLowerCase();
|
|
285
|
+
if (action === 'status') {
|
|
286
|
+
console.log(JSON.stringify(telemetryState(), null, 2));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (action === 'on' || action === 'enable') {
|
|
290
|
+
console.log(JSON.stringify(setTelemetry(true), null, 2));
|
|
291
|
+
restartPersistentServiceIfInstalled();
|
|
292
|
+
console.log('Usage metrics enabled and the agent restarted to apply it.');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (action === 'off' || action === 'disable') {
|
|
296
|
+
console.log(JSON.stringify(setTelemetry(false), null, 2));
|
|
297
|
+
restartPersistentServiceIfInstalled();
|
|
298
|
+
console.log('Usage metrics disabled and the agent restarted to apply it.');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
throw new Error('Usage: remcp telemetry [status|on|off]');
|
|
302
|
+
}
|
|
303
|
+
|
|
131
304
|
if (command === 'status' || command === 'doctor') {
|
|
132
305
|
const cfg = loadConfig();
|
|
133
306
|
const health = await fetch(`${cfg.serverUrl}/health?fresh=${Date.now()}`, { cache: 'no-store' }).then(r => r.json());
|
|
134
|
-
console.log(JSON.stringify({ configured: true, cliVersion: VERSION, deviceId: cfg.deviceId, deviceName: cfg.deviceName, server: cfg.serverUrl, serverHealth: health }, null, 2));
|
|
307
|
+
console.log(JSON.stringify({ configured: true, cliVersion: VERSION, deviceId: cfg.deviceId, deviceName: cfg.deviceName, server: cfg.serverUrl, runtime: cfg.runtime, telemetry: telemetryState(), serverHealth: health }, null, 2));
|
|
135
308
|
return;
|
|
136
309
|
}
|
|
137
310
|
|
|
138
311
|
if (command === 'install') {
|
|
139
|
-
loadConfig();
|
|
140
|
-
installPersistentAgent();
|
|
312
|
+
installPersistentAgent(loadConfig());
|
|
141
313
|
return;
|
|
142
314
|
}
|
|
143
315
|
|
|
144
316
|
if (command === 'update') {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
317
|
+
const cfg = loadConfig();
|
|
318
|
+
console.log('Updating ReMCP to the latest published version…');
|
|
319
|
+
npmGlobalInstall(`${PACKAGE_NAME}@latest`, cfg.runtime.packageSpec);
|
|
320
|
+
restartPersistentServiceIfInstalled();
|
|
148
321
|
console.log('ReMCP updated. Run `remcp --version` or `remcp status` to verify.');
|
|
149
322
|
return;
|
|
150
323
|
}
|
|
151
324
|
|
|
152
325
|
if (command === 'uninstall') {
|
|
153
|
-
|
|
326
|
+
uninstallPersistentService();
|
|
154
327
|
if (flags.purge) {
|
|
155
|
-
|
|
156
|
-
|
|
328
|
+
const cfg = loadConfig(false);
|
|
329
|
+
const specs = [PACKAGE_NAME, ...(cfg?.runtime?.packageName ? [cfg.runtime.packageName] : [])];
|
|
330
|
+
run(npmCommand, ['uninstall', '--global', ...specs, '--no-audit', '--no-fund', '--loglevel=error']);
|
|
157
331
|
}
|
|
158
332
|
return;
|
|
159
333
|
}
|
package/src/runtime.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
|
|
4
|
+
const PACKAGE_SPEC = /^[^\s]+$/;
|
|
5
|
+
|
|
6
|
+
export function normalizeRuntime(value) {
|
|
7
|
+
if (!value || value.kind !== 'npm') throw new Error('Pairing server did not provide a supported local runtime');
|
|
8
|
+
const packageName = String(value.packageName || '');
|
|
9
|
+
const packageSpec = String(value.packageSpec || '');
|
|
10
|
+
const entry = String(value.entry || '');
|
|
11
|
+
if (!PACKAGE_NAME.test(packageName)) throw new Error('Pairing server returned an invalid runtime package name');
|
|
12
|
+
if (!PACKAGE_SPEC.test(packageSpec) || !packageSpec.startsWith(`${packageName}@`)) throw new Error('Pairing server returned an invalid runtime package spec');
|
|
13
|
+
if (!entry || path.isAbsolute(entry) || entry.split(/[\\/]+/).includes('..')) throw new Error('Pairing server returned an invalid runtime entry');
|
|
14
|
+
return { kind: 'npm', packageName, packageSpec, entry };
|
|
15
|
+
}
|
package/src/version.mjs
CHANGED
|
@@ -2,7 +2,5 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
|
|
3
3
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
4
4
|
|
|
5
|
-
export const PACKAGE_NAME =
|
|
5
|
+
export const PACKAGE_NAME = '@remcp/remcp';
|
|
6
6
|
export const VERSION = pkg.version;
|
|
7
|
-
export const LOCAL_RUNTIME_NAME = '@wonderwhy-er/desktop-commander';
|
|
8
|
-
export const LOCAL_RUNTIME_SPEC = `${LOCAL_RUNTIME_NAME}@0.2.50`;
|
package/THIRD_PARTY_NOTICES.md
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# Third-party notices
|
|
2
|
-
|
|
3
|
-
ReMCP uses `@wonderwhy-er/desktop-commander` as its local device MCP backend. Desktop Commander is MIT licensed.
|
|
4
|
-
|
|
5
|
-
Copyright (c) 2024-2025 Eduard Ruzga and Desktop Commander Contributors.
|
|
6
|
-
|
|
7
|
-
The upstream project is available at https://github.com/wonderwhy-er/DesktopCommanderMCP. ReMCP does not use or depend on the proprietary Remote Desktop Commander cloud relay.
|
package/assets/remcp-icon.png
DELETED
|
Binary file
|
package/mcp.json
DELETED
package/plugin.json
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
-
"name": "remcp",
|
|
4
|
-
"version": "0.1.3",
|
|
5
|
-
"description": "Securely use files, directories, searches, terminal sessions, and processes on computers you have paired with ReMCP.",
|
|
6
|
-
"homepage": "https://remcp.delio24.com",
|
|
7
|
-
"repository": "https://github.com/antonbaider/remcp",
|
|
8
|
-
"license": "MIT",
|
|
9
|
-
"keywords": [
|
|
10
|
-
"mcp",
|
|
11
|
-
"remote-computer",
|
|
12
|
-
"developer-tools",
|
|
13
|
-
"files",
|
|
14
|
-
"terminal"
|
|
15
|
-
],
|
|
16
|
-
"extensions": {
|
|
17
|
-
"com.openai": {
|
|
18
|
-
"interface": {
|
|
19
|
-
"displayName": "ReMCP",
|
|
20
|
-
"shortDescription": "Use your paired computers through MCP.",
|
|
21
|
-
"longDescription": "ReMCP securely connects ChatGPT and Codex to computers you have explicitly paired. Inspect files and directories, search local content, and manage terminal sessions and processes through an authenticated remote MCP endpoint.",
|
|
22
|
-
"category": "Developer Tools",
|
|
23
|
-
"websiteURL": "https://remcp.delio24.com",
|
|
24
|
-
"privacyPolicyURL": "https://remcp.delio24.com/privacy",
|
|
25
|
-
"termsOfServiceURL": "https://remcp.delio24.com/terms",
|
|
26
|
-
"defaultPrompt": [
|
|
27
|
-
"Show my paired ReMCP computers and tell me which ones are online.",
|
|
28
|
-
"On my laptop, list the files in my project folder and summarize what is there without changing anything.",
|
|
29
|
-
"On my server, list the running processes and help me identify the one using the most memory."
|
|
30
|
-
],
|
|
31
|
-
"brandColor": "#4C50D8",
|
|
32
|
-
"composerIcon": "./assets/remcp-icon.png",
|
|
33
|
-
"logo": "./assets/remcp-icon.png"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
Binary file
|
|
Binary file
|
package/public/app.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.14.1/firebase-app.js';
|
|
2
|
-
import { getAuth, GoogleAuthProvider, GithubAuthProvider, onAuthStateChanged, signInWithPopup, signOut } from 'https://www.gstatic.com/firebasejs/10.14.1/firebase-auth.js';
|
|
3
|
-
|
|
4
|
-
const cfg = await fetch(`/api/config?fresh=${Date.now()}`, { cache: 'no-store' }).then(r => r.json());
|
|
5
|
-
const auth = getAuth(initializeApp(cfg.firebase));
|
|
6
|
-
const $ = selector => document.querySelector(selector);
|
|
7
|
-
const googleProvider = new GoogleAuthProvider();
|
|
8
|
-
const githubProvider = new GithubAuthProvider();
|
|
9
|
-
googleProvider.setCustomParameters({ prompt: 'select_account' });
|
|
10
|
-
|
|
11
|
-
const friendlyAuthError = error => {
|
|
12
|
-
const code = error?.code || '';
|
|
13
|
-
if (code === 'auth/popup-closed-by-user' || code === 'auth/cancelled-popup-request') return '';
|
|
14
|
-
if (code === 'auth/account-exists-with-different-credential') return 'This email is already linked to another sign-in provider.';
|
|
15
|
-
if (code === 'auth/operation-not-allowed') return 'This sign-in provider is not enabled.';
|
|
16
|
-
return error?.message || String(error);
|
|
17
|
-
};
|
|
18
|
-
const showError = error => { $('#auth-error').textContent = friendlyAuthError(error); };
|
|
19
|
-
|
|
20
|
-
async function signIn(button, provider, idleLabel) {
|
|
21
|
-
const label = button.querySelector('span');
|
|
22
|
-
button.disabled = true;
|
|
23
|
-
$('#auth-error').textContent = '';
|
|
24
|
-
label.textContent = 'Opening…';
|
|
25
|
-
try { await signInWithPopup(auth, provider); }
|
|
26
|
-
catch (error) { showError(error); }
|
|
27
|
-
finally { button.disabled = false; label.textContent = idleLabel; }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
$('#mcp-url').textContent = cfg.mcpUrl;
|
|
31
|
-
$('#google').onclick = () => signIn($('#google'), googleProvider, 'Continue with Google');
|
|
32
|
-
$('#github').onclick = () => signIn($('#github'), githubProvider, 'Continue with GitHub');
|
|
33
|
-
$('#logout').onclick = () => signOut(auth);
|
|
34
|
-
|
|
35
|
-
async function api(path, options = {}) {
|
|
36
|
-
const token = await auth.currentUser.getIdToken();
|
|
37
|
-
const response = await fetch(path, { ...options, headers: { ...(options.headers || {}), authorization: `Bearer ${token}`, 'content-type': 'application/json' } });
|
|
38
|
-
if (!response.ok) throw new Error(await response.text());
|
|
39
|
-
return response.status === 204 ? null : response.json();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function deviceRow(device) {
|
|
43
|
-
const row = document.createElement('div');
|
|
44
|
-
row.className = 'device';
|
|
45
|
-
const details = document.createElement('div');
|
|
46
|
-
const name = document.createElement('strong');
|
|
47
|
-
name.textContent = device.name || 'Unnamed device';
|
|
48
|
-
const meta = document.createElement('div');
|
|
49
|
-
meta.className = 'muted';
|
|
50
|
-
meta.textContent = `${device.platform || 'unknown'} · ${device.hostname || device.id}`;
|
|
51
|
-
const status = document.createElement('div');
|
|
52
|
-
status.className = device.online ? 'online' : 'offline';
|
|
53
|
-
status.textContent = device.online ? 'Online' : 'Offline';
|
|
54
|
-
details.append(name, meta, status);
|
|
55
|
-
const revoke = document.createElement('button');
|
|
56
|
-
revoke.className = 'danger';
|
|
57
|
-
revoke.type = 'button';
|
|
58
|
-
revoke.textContent = 'Revoke';
|
|
59
|
-
revoke.setAttribute('aria-label', `Revoke ${device.name || 'device'}`);
|
|
60
|
-
revoke.onclick = async () => {
|
|
61
|
-
if (!confirm(`Revoke ${device.name || 'this device'}? It will need to be paired again before reconnecting.`)) return;
|
|
62
|
-
await api(`/api/devices/${encodeURIComponent(device.id)}`, { method: 'DELETE' });
|
|
63
|
-
await refreshDevices();
|
|
64
|
-
};
|
|
65
|
-
row.append(details, revoke);
|
|
66
|
-
return row;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function refreshDevices() {
|
|
70
|
-
const data = await api('/api/me');
|
|
71
|
-
const root = $('#devices');
|
|
72
|
-
root.replaceChildren();
|
|
73
|
-
if (!data.devices.length) { root.textContent = 'No paired devices.'; return; }
|
|
74
|
-
for (const device of data.devices) root.append(deviceRow(device));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
$('#pair').onclick = async () => {
|
|
78
|
-
try {
|
|
79
|
-
const pairing = await api('/api/pair/create', { method: 'POST', body: '{}' });
|
|
80
|
-
$('#pairing').classList.remove('hidden');
|
|
81
|
-
$('#pair-command').textContent = pairing.command;
|
|
82
|
-
const expiry = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' }).format(new Date(pairing.expiresAt));
|
|
83
|
-
$('#pair-expiry').textContent = `One-time command · expires ${expiry}`;
|
|
84
|
-
} catch (error) { showError(error); }
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
onAuthStateChanged(auth, async user => {
|
|
88
|
-
const signedIn = Boolean(user);
|
|
89
|
-
$('#signed-out').classList.toggle('hidden', signedIn);
|
|
90
|
-
$('#signed-in').classList.toggle('hidden', !signedIn);
|
|
91
|
-
$('#dashboard').classList.toggle('hidden', !signedIn);
|
|
92
|
-
$('#auth-error').textContent = '';
|
|
93
|
-
if (!signedIn) return;
|
|
94
|
-
$('#user-label').textContent = user.email || user.displayName || user.uid;
|
|
95
|
-
try { await refreshDevices(); } catch (error) { showError(error); }
|
|
96
|
-
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|