@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 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.192",
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": {
@@ -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
+ }
@@ -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