@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.
Files changed (56) hide show
  1. package/README.md +41 -113
  2. package/package.json +10 -24
  3. package/src/agent.mjs +215 -36
  4. package/src/cli.mjs +217 -43
  5. package/src/runtime.mjs +15 -0
  6. package/src/version.mjs +1 -3
  7. package/THIRD_PARTY_NOTICES.md +0 -7
  8. package/assets/remcp-icon.png +0 -0
  9. package/mcp.json +0 -9
  10. package/plugin.json +0 -37
  11. package/public/android-chrome-192x192.png +0 -0
  12. package/public/android-chrome-512x512.png +0 -0
  13. package/public/app.js +0 -96
  14. package/public/apple-touch-icon.png +0 -0
  15. package/public/assets/art/hero-relay.webp +0 -0
  16. package/public/assets/art/relay-detail.webp +0 -0
  17. package/public/assets/icon-120.png +0 -0
  18. package/public/assets/icon-16.png +0 -0
  19. package/public/assets/icon-180.png +0 -0
  20. package/public/assets/icon-192.png +0 -0
  21. package/public/assets/icon-256.png +0 -0
  22. package/public/assets/icon-32.png +0 -0
  23. package/public/assets/icon-48.png +0 -0
  24. package/public/assets/icon-512.png +0 -0
  25. package/public/assets/og-remcp.png +0 -0
  26. package/public/authorize.html +0 -59
  27. package/public/copy.js +0 -40
  28. package/public/docs.html +0 -93
  29. package/public/favicon-16x16.png +0 -0
  30. package/public/favicon-32x32.png +0 -0
  31. package/public/favicon-48x48.png +0 -0
  32. package/public/favicon.ico +0 -0
  33. package/public/index.html +0 -134
  34. package/public/oauth-app.js +0 -63
  35. package/public/privacy.html +0 -80
  36. package/public/remcp-darkmode-16x16.png +0 -0
  37. package/public/remcp-darkmode-192x192.png +0 -0
  38. package/public/remcp-darkmode-32x32.png +0 -0
  39. package/public/remcp-darkmode-48x48.png +0 -0
  40. package/public/remcp-darkmode-512x512.png +0 -0
  41. package/public/security.html +0 -70
  42. package/public/site.webmanifest +0 -23
  43. package/public/style.css +0 -261
  44. package/public/support.html +0 -75
  45. package/public/terms.html +0 -77
  46. package/skills/remcp-operator/SKILL.md +0 -36
  47. package/src/auth.mjs +0 -71
  48. package/src/config.mjs +0 -51
  49. package/src/db.mjs +0 -102
  50. package/src/mcp.mjs +0 -95
  51. package/src/oauth.mjs +0 -164
  52. package/src/relay.mjs +0 -97
  53. package/src/review-sandbox.mjs +0 -96
  54. package/src/server.mjs +0 -159
  55. package/src/tool-catalog.json +0 -796
  56. 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 { LOCAL_RUNTIME_NAME, LOCAL_RUNTIME_SPEC, PACKAGE_NAME, VERSION } from './version.mjs';
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 serviceFile = path.join(home, '.config', 'systemd', 'user', 'remcp-agent.service');
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)) throw new Error(`ReMCP is not paired. Run: npx --yes ${PACKAGE_NAME}@latest connect --server https://remcp.delio24.com --code CODE --install`);
42
- return JSON.parse(fs.readFileSync(configFile, 'utf8'));
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 process.platform === 'win32' ? path.join(prefix, 'remcp.cmd') : path.join(prefix, 'bin', 'remcp');
136
+ return servicePlatform() === 'win32' ? path.join(prefix, 'remcp.cmd') : path.join(prefix, 'bin', 'remcp');
58
137
  }
59
138
 
60
- function npmGlobalInstall(packageSpec) {
61
- run(npmCommand, ['install', '--global', packageSpec, LOCAL_RUNTIME_SPEC, '--no-audit', '--no-fund', '--loglevel=error']);
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('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&apos;');
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(serviceFile), { recursive: true });
71
- fs.writeFileSync(serviceFile, unit);
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 installPersistentAgent() {
77
- if (process.platform !== 'linux') throw new Error('Automatic background service installation currently supports Linux');
78
- console.log(`Installing ${PACKAGE_NAME}@${VERSION} and the local device runtime…`);
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 restartLinuxServiceIfInstalled() {
85
- if (process.platform !== 'linux' || !fs.existsSync(serviceFile)) return;
86
- run('systemctl', ['--user', 'daemon-reload']);
87
- run('systemctl', ['--user', 'restart', 'remcp-agent.service']);
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 uninstallLinuxService() {
91
- if (process.platform !== 'linux') return;
92
- spawnSync('systemctl', ['--user', 'disable', '--now', 'remcp-agent.service'], { stdio: 'inherit' });
93
- try { fs.unlinkSync(serviceFile); } catch {}
94
- spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'inherit' });
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 npx --yes ${PACKAGE_NAME}@latest connect --server https://remcp.delio24.com --code ABC12345 --install\n remcp start\n remcp status\n remcp doctor\n remcp update\n remcp install\n remcp uninstall\n remcp uninstall --purge\n remcp --version`);
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
- saveConfig({ serverUrl: server, deviceId: paired.deviceId, deviceToken: paired.deviceToken, deviceName: String(flags.name || os.hostname()) });
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
- await runAgent(loadConfig());
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
- console.log(`Updating ${PACKAGE_NAME} to the latest published version…`);
146
- npmGlobalInstall(`${PACKAGE_NAME}@latest`);
147
- restartLinuxServiceIfInstalled();
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
- uninstallLinuxService();
326
+ uninstallPersistentService();
154
327
  if (flags.purge) {
155
- console.log('Removing global ReMCP packages…');
156
- run(npmCommand, ['uninstall', '--global', PACKAGE_NAME, LOCAL_RUNTIME_NAME, '--no-audit', '--no-fund', '--loglevel=error']);
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
  }
@@ -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 = pkg.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`;
@@ -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.
Binary file
package/mcp.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
- "mcpServers": {
4
- "remcp": {
5
- "type": "streamable-http",
6
- "url": "https://remcp.delio24.com/mcp"
7
- }
8
- }
9
- }
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