@remcp/remcp 0.2.192 → 0.2.194
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 +10 -0
- package/package.json +3 -2
- package/src/cli/connect.mjs +2 -0
- package/src/cli/pairing-qr.mjs +43 -0
- package/src/cli/service.mjs +3 -0
- package/src/cli.mjs +26 -3
package/README.md
CHANGED
|
@@ -16,6 +16,16 @@ it keeps the ReMCP runtime under `%LOCALAPPDATA%\ReMCP`. Generated one-time comm
|
|
|
16
16
|
runtime, and registers a user service (systemd on Linux, LaunchAgent on macOS, Scheduled Task on
|
|
17
17
|
Windows).
|
|
18
18
|
|
|
19
|
+
## Scan to pair with a phone
|
|
20
|
+
|
|
21
|
+
Run `remcp connect` in an interactive terminal to display a short-lived pairing code,
|
|
22
|
+
a link, and a compact QR that opens the same ReMCP approval page on your phone.
|
|
23
|
+
Sign in on your phone if needed, check the computer name, then explicitly approve.
|
|
24
|
+
The QR contains only the one-time code in the existing device URL: it does **not**
|
|
25
|
+
authorize the device on its own. The code expires and cannot be reused after approval.
|
|
26
|
+
If your terminal cannot display a QR, use the printed link or code. Add `--no-qr`
|
|
27
|
+
to suppress the QR; a custom non-HTTPS or cross-origin URL never gets one.
|
|
28
|
+
|
|
19
29
|
## Commands
|
|
20
30
|
|
|
21
31
|
```text
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remcp/remcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.194",
|
|
4
4
|
"description": "ReMCP device client: pair a computer with ReMCP and run the outbound-only agent that hosts the local MCP runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"check": "node --check bin/remcp.mjs && node --check src/agent-network-probe-client.mjs && node --check src/agent-relay-connection.mjs && node --check src/agent-responder.mjs && node --check src/agent-runtime-capabilities.mjs && node --check src/agent-update-monitor.mjs && node --check src/agent-update.mjs && node --check src/agent-utilities.mjs && node --check src/agent.mjs && node --check src/cli.mjs && node --check src/cli/config.mjs && node --check src/cli/connect.mjs && node --check src/cli/doctor.mjs && node --check src/cli/env.mjs && node --check src/cli/npm-global.mjs && node --check src/cli/service-inventory.mjs && node --check src/cli/service-macos.mjs && node --check src/cli/service-paths.mjs && node --check src/cli/service.mjs && node --check src/cli/shell.mjs && node --check src/cli/update.mjs && node --check src/cli/windows-supervisor.mjs && node --check src/fs-access.mjs && node --check src/npm.mjs && node --check src/runtime.mjs && node --check src/version.mjs",
|
|
21
|
+
"check": "node --check bin/remcp.mjs && node --check src/agent-network-probe-client.mjs && node --check src/agent-relay-connection.mjs && node --check src/agent-responder.mjs && node --check src/agent-runtime-capabilities.mjs && node --check src/agent-update-monitor.mjs && node --check src/agent-update.mjs && node --check src/agent-utilities.mjs && node --check src/agent.mjs && node --check src/cli.mjs && node --check src/cli/config.mjs && node --check src/cli/connect.mjs && node --check src/cli/doctor.mjs && node --check src/cli/env.mjs && node --check src/cli/npm-global.mjs && node --check src/cli/pairing-qr.mjs && node --check src/cli/service-inventory.mjs && node --check src/cli/service-macos.mjs && node --check src/cli/service-paths.mjs && node --check src/cli/service.mjs && node --check src/cli/shell.mjs && node --check src/cli/update.mjs && node --check src/cli/windows-supervisor.mjs && node --check src/fs-access.mjs && node --check src/npm.mjs && node --check src/runtime.mjs && node --check src/version.mjs",
|
|
22
22
|
"test": "node --test test/*.test.mjs"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@modelcontextprotocol/client": "^2.3.1",
|
|
26
|
+
"uqr": "0.1.3",
|
|
26
27
|
"ws": "^8.21.3"
|
|
27
28
|
},
|
|
28
29
|
"repository": {
|
package/src/cli/connect.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { spawn } from 'node:child_process';
|
|
|
7
7
|
import { ensureMachineId } from './config.mjs';
|
|
8
8
|
import { officialOrigin } from './env.mjs';
|
|
9
9
|
import { sleep } from './shell.mjs';
|
|
10
|
+
import { printPairingQr } from './pairing-qr.mjs';
|
|
10
11
|
|
|
11
12
|
// Keep the server-provided approval URL as data all the way to the OS. In particular, do not route it
|
|
12
13
|
// through `cmd /c start` on Windows: cmd reparses metacharacters such as `&`, so a custom pairing
|
|
@@ -78,6 +79,7 @@ export async function pairWithDeviceCode(server, flags) {
|
|
|
78
79
|
const approvalUrl = grant.verification_uri_complete || grant.verification_uri;
|
|
79
80
|
console.log(`Approve this computer in your browser: ${approvalUrl}`);
|
|
80
81
|
console.log(`Pairing code: ${grant.user_code} (expires in ${Math.max(1, Math.round(Number(grant.expires_in || 600) / 60))} minutes)`);
|
|
82
|
+
if (flags?.['no-qr'] !== true) printPairingQr({ server, approvalUrl, userCode: grant.user_code });
|
|
81
83
|
openInBrowser(approvalUrl);
|
|
82
84
|
console.log('Waiting for approval… (Ctrl+C to cancel)');
|
|
83
85
|
const deadline = Date.now() + (Number(grant.expires_in) || 600) * 1000;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { renderUnicodeCompact } from 'uqr';
|
|
2
|
+
|
|
3
|
+
const USER_CODE_PATTERN = /^[A-Z2-9]{4}-[A-Z2-9]{4}$/;
|
|
4
|
+
|
|
5
|
+
// A QR is a shortcut to the existing device review screen, not a login token.
|
|
6
|
+
// Only encode the same-origin HTTPS /device URL containing this exact short-lived code.
|
|
7
|
+
export function formatPairingQr({ server, approvalUrl, userCode, output = process.stdout, env = process.env }) {
|
|
8
|
+
if (!output?.isTTY || env.TERM === 'dumb' || env.NO_COLOR) return null;
|
|
9
|
+
if (!USER_CODE_PATTERN.test(String(userCode))) return null;
|
|
10
|
+
|
|
11
|
+
let origin;
|
|
12
|
+
let url;
|
|
13
|
+
try {
|
|
14
|
+
origin = new URL(server);
|
|
15
|
+
url = new URL(approvalUrl);
|
|
16
|
+
} catch { return null; }
|
|
17
|
+
if (origin.protocol !== 'https:' || url.protocol !== 'https:' || url.origin !== origin.origin ||
|
|
18
|
+
url.username || url.password || url.hash || url.pathname !== '/device' ||
|
|
19
|
+
url.searchParams.size !== 1 || url.searchParams.get('user_code') !== userCode) return null;
|
|
20
|
+
|
|
21
|
+
// Each pair of QR rows fits into one terminal line. Use explicit light/dark ANSI
|
|
22
|
+
// colors so the quiet zone remains scannable regardless of terminal theme.
|
|
23
|
+
let qr;
|
|
24
|
+
try { qr = renderUnicodeCompact(url.href, { ecc: 'M', border: 3 }); }
|
|
25
|
+
catch { return null; }
|
|
26
|
+
const columns = Number.isFinite(output.columns) && output.columns > 0 ? output.columns : 80;
|
|
27
|
+
const rows = qr.split('\n');
|
|
28
|
+
if (rows.some(row => [...row].length + 2 > columns)) return null;
|
|
29
|
+
// The library paints a dark lower half outside odd-sized matrices.
|
|
30
|
+
// Restore the white bottom quiet zone so phone cameras can detect it.
|
|
31
|
+
const last = rows.length - 1;
|
|
32
|
+
rows[last] = rows[last].replaceAll('▀', '█');
|
|
33
|
+
return rows.map(row => `\x1b[97;40m${row}\x1b[0m`).join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function printPairingQr({ server, approvalUrl, userCode, output = process.stdout, env = process.env }) {
|
|
37
|
+
const qr = formatPairingQr({ server, approvalUrl, userCode, output, env });
|
|
38
|
+
if (!qr) return false;
|
|
39
|
+
output.write('Scan with your phone to review this computer:\n');
|
|
40
|
+
output.write(qr + '\n');
|
|
41
|
+
output.write('Check the computer name before approving.\n');
|
|
42
|
+
return true;
|
|
43
|
+
}
|
package/src/cli/service.mjs
CHANGED
|
@@ -245,6 +245,9 @@ export function restartPersistentServiceIfInstalled(config, { deferLinux = false
|
|
|
245
245
|
if (platform === 'win32') {
|
|
246
246
|
const result = spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName], { stdio: 'ignore' });
|
|
247
247
|
if (result.status === 0) {
|
|
248
|
+
// /Run does not restart a task that is already running; stop the previous
|
|
249
|
+
// process first so a rotated credential is loaded by the replacement.
|
|
250
|
+
spawnSync('schtasks.exe', ['/End', '/TN', windowsTaskName], { stdio: 'ignore' });
|
|
248
251
|
run('schtasks.exe', ['/Run', '/TN', windowsTaskName]);
|
|
249
252
|
return windowsTaskName;
|
|
250
253
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { ensureMachineId, loadConfig, readJsonFile, saveConfig, setTelemetry, te
|
|
|
11
11
|
import { assertRuntimeTrust, assertSecureServerUrl, pairWithDeviceCode } from './cli/connect.mjs';
|
|
12
12
|
import { diagnoseLocalRuntime, installedVersion, runtimeAllowedRoots } from './cli/doctor.mjs';
|
|
13
13
|
import { configFile, npm, officialOrigin, runtimeConfigFile } from './cli/env.mjs';
|
|
14
|
-
import { currentInstallationInfo, ensureMacCliCommand, installPersistentAgent, persistentServiceState, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, uninstallPersistentService } from './cli/service.mjs';
|
|
14
|
+
import { currentInstallationInfo, ensureMacCliCommand, installPersistentAgent, persistentServiceState, servicePlatform, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, uninstallPersistentService } from './cli/service.mjs';
|
|
15
15
|
import { run } from './cli/shell.mjs';
|
|
16
16
|
import { updateCommand } from './cli/update.mjs';
|
|
17
17
|
function fetchConfiguredServer(url, options) {
|
|
@@ -34,7 +34,7 @@ function parse(argv) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function printHelp() {
|
|
37
|
-
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 godmode [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\n\nUnrestricted mode (remcp godmode on) lifts the access roots and the command guardrails for this\ncomputer only. The MCP configuration tool cannot enable it; protect remote terminal access.`);
|
|
37
|
+
console.log(`ReMCP ${VERSION}\n\nCommands:\n remcp connect [--no-qr]\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 godmode [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\n\nUnrestricted mode (remcp godmode on) lifts the access roots and the command guardrails for this\ncomputer only. The MCP configuration tool cannot enable it; protect remote terminal access.`);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export async function main(argv = process.argv.slice(2)) {
|
|
@@ -49,6 +49,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
49
49
|
// Like the desktop-app flow this mirrors: with no flags the command talks to the official
|
|
50
50
|
// server, prints a code, opens the browser to approve it, and pairs. `--code` keeps working for
|
|
51
51
|
// the workspace-generated command and for CI, and `--server` for self-hosted deployments.
|
|
52
|
+
const previous = loadConfig(false);
|
|
53
|
+
const alreadyPaired = Boolean(previous?.deviceId && previous?.deviceToken);
|
|
54
|
+
if (alreadyPaired && flags['re-pair'] !== true) {
|
|
55
|
+
throw new Error('This computer is already paired. Use `remcp doctor` or `remcp install` to repair it without changing credentials. To intentionally replace the pairing (possibly moving it to another account), add --re-pair.');
|
|
56
|
+
}
|
|
52
57
|
const server = assertSecureServerUrl(flags.server || officialOrigin, { allowInsecure: flags['allow-insecure-transport'] === true });
|
|
53
58
|
const code = String(flags.code || '').replace(/\s+/g, '').toUpperCase();
|
|
54
59
|
assertRuntimeTrust(server, flags);
|
|
@@ -60,7 +65,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
60
65
|
method: 'POST',
|
|
61
66
|
redirect: 'error',
|
|
62
67
|
headers: { 'content-type': 'application/json' },
|
|
63
|
-
body: JSON.stringify({ code, machineId: ensureMachineId(), name: String(flags.name || os.hostname()), hostname: os.hostname(), platform: process.platform, arch: process.arch }),
|
|
68
|
+
body: JSON.stringify({ code, machineId: ensureMachineId(), name: String(flags.name || os.hostname()), hostname: os.hostname(), platform: process.platform, arch: process.arch, rePair: flags['re-pair'] === true }),
|
|
64
69
|
|
|
65
70
|
});
|
|
66
71
|
if (!response.ok) throw new Error(`Pairing failed (${response.status}): ${await response.text()}`);
|
|
@@ -75,6 +80,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
75
80
|
console.log('This computer was paired to another ReMCP account. That device was revoked and this machine now belongs to the account you approved.');
|
|
76
81
|
}
|
|
77
82
|
const config = {
|
|
83
|
+
...(previous || {}),
|
|
78
84
|
serverUrl: server,
|
|
79
85
|
deviceId: paired.deviceId || paired.device_id,
|
|
80
86
|
deviceToken: paired.deviceToken || paired.device_token,
|
|
@@ -90,8 +96,25 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
90
96
|
saveConfig(config);
|
|
91
97
|
const accountEmail = String(paired.account?.email || '');
|
|
92
98
|
console.log(`Paired ${os.hostname()} with ${server}${accountEmail ? ` as ${accountEmail}` : ''}`);
|
|
99
|
+
// Credential rotation must reload the managed agent; a normal install of an already
|
|
100
|
+
// running systemd service does not restart it, and Windows /Run may keep the old task.
|
|
101
|
+
const managedBefore = alreadyPaired &&
|
|
102
|
+
(previous.serviceInstalled === true ||
|
|
103
|
+
(previous.serviceInstalled === undefined && persistentServiceState(previous).active));
|
|
93
104
|
if (flags.install) {
|
|
94
105
|
installPersistentAgent(config);
|
|
106
|
+
if (alreadyPaired && servicePlatform() !== 'darwin') {
|
|
107
|
+
restartPersistentServiceIfInstalled(config, { deferLinux: true });
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (managedBefore) {
|
|
112
|
+
const restarted = restartPersistentServiceIfInstalled(config, { deferLinux: true });
|
|
113
|
+
if (!restarted) installPersistentAgent(config);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (alreadyPaired) {
|
|
117
|
+
console.log('Pairing credentials replaced. Restart any manually managed ReMCP agent to use the new token.');
|
|
95
118
|
return;
|
|
96
119
|
}
|
|
97
120
|
// Only the command that asked for its own code keeps running: someone who ran `remcp connect` on
|