@remcp/remcp 0.2.212 → 0.2.214

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/remcp",
3
- "version": "0.2.212",
3
+ "version": "0.2.214",
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,7 +18,7 @@
18
18
  "README.md"
19
19
  ],
20
20
  "scripts": {
21
- "check": "node --check bin/remcp.mjs && node --check src/agent-deploy-reconnect.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/installation-audit.mjs && node --check src/cli/mac-native-temperature.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/cli/windows-user-supervisor.mjs && node --check src/fetch-update-manifest.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-deploy-reconnect.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/installation-audit.mjs && node --check src/cli/mac-native-temperature.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/cli/windows-task-restart.mjs && node --check src/cli/windows-user-supervisor.mjs && node --check src/fetch-update-manifest.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": {
@@ -9,14 +9,13 @@ import { officialOrigin } from './env.mjs';
9
9
  import { sleep } from './shell.mjs';
10
10
  import { printPairingQr } from './pairing-qr.mjs';
11
11
 
12
- // Keep the server-provided approval URL as one argument all the way to the OS.
13
- // Windows Explorer can treat a URL as a shell path and open Documents instead.
14
- // FileProtocolHandler dispatches to the default browser without a cmd/PowerShell shell,
15
- // where a URL query containing '&' could otherwise be interpreted as a command.
12
+ // Keep the server-provided approval URL as data all the way to the OS. In particular, do not route it
13
+ // through `cmd /c start` on Windows: cmd reparses metacharacters such as `&`, so a custom pairing
14
+ // server could otherwise turn a verification URL into a local shell command.
16
15
  export function browserLaunchCommand(url, platform = process.platform) {
17
16
  const target = String(url);
18
17
  if (platform === 'darwin') return { command: 'open', args: [target] };
19
- if (platform === 'win32') return { command: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', target] };
18
+ if (platform === 'win32') return { command: 'explorer.exe', args: [target] };
20
19
  return { command: 'xdg-open', args: [target] };
21
20
  }
22
21
 
package/src/cli/env.mjs CHANGED
@@ -17,6 +17,7 @@ export const macServiceFile = path.join(home, 'Library', 'LaunchAgents', `${macS
17
17
  export const macLogFile = path.join(home, 'Library', 'Logs', 'remcp-agent.log');
18
18
  export const windowsTaskName = 'ReMCP Agent';
19
19
  export const windowsServiceLauncherFile = path.join(configDir, 'remcp-agent.cmd');
20
+ export const windowsTaskScriptFile = path.join(configDir, 'remcp-agent.vbs');
20
21
  // Task Scheduler may refuse to create a task for a standard Windows account.
21
22
  // This per-user Startup folder runs without administrator privileges.
22
23
  export const windowsStartupScriptFile = path.join(
@@ -72,6 +72,19 @@ function inferServiceIdentity() {
72
72
 
73
73
  export function canonicalServiceCliPath(config) {
74
74
  const configured = String(config?.serviceCliPath || '').trim();
75
+ if (servicePlatform() === 'win32' && configured) {
76
+ // Saved serviceCliPath is the resolved npm package entry (remcp.mjs), not
77
+ // the Windows executable shim. CMD `call remcp.mjs` opens the Windows
78
+ // "Choose an app" dialog instead of starting Node, and dirname(entry) is
79
+ // an invalid NPM_CONFIG_PREFIX. Recover the sibling npm global shim.
80
+ const suffix = '/node_modules/@remcp/remcp/bin/remcp.mjs';
81
+ if (configured.replaceAll('\\', '/').toLowerCase().endsWith(suffix)) {
82
+ const prefix = configured.slice(0, -suffix.length);
83
+ const shim = path.join(prefix, 'remcp.cmd');
84
+ if (!fs.existsSync(shim)) throw new Error('Windows ReMCP npm shim is missing; refusing a .mjs Scheduled Task');
85
+ return shim;
86
+ }
87
+ }
75
88
  if (configured && fs.existsSync(configured)) return configured;
76
89
  const inferred = inferServiceIdentity().cliPath;
77
90
  if (inferred && fs.existsSync(inferred)) return inferred;
@@ -15,10 +15,11 @@ export { macLaunchDomain, ensureMacCliCommand, installMacService } from './servi
15
15
  import { PACKAGE_NAME, VERSION } from '../version.mjs';
16
16
 
17
17
  import { saveConfig } from './config.mjs';
18
- import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macServiceFile, macServiceLabel, windowsServiceLauncherFile, windowsStartupScriptFile, windowsTaskName } from './env.mjs';
18
+ import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macServiceFile, macServiceLabel, windowsServiceLauncherFile, windowsTaskScriptFile, windowsStartupScriptFile, windowsTaskName } from './env.mjs';
19
19
  import { run } from './shell.mjs';
20
- import { windowsServiceLauncherText, windowsTaskReliabilityCommand, windowsTaskAbsenceCommand, windowsBatchUnsafeProfile, windowsStartupScriptText, windowsStartupScriptInstalled } from './windows-supervisor.mjs';
20
+ import { windowsServiceLauncherText, windowsTaskReliabilityCommand, windowsTaskAbsenceCommand, windowsBatchUnsafeProfile, windowsTaskScriptText, windowsTaskScriptInstalled, windowsStartupScriptText, windowsStartupScriptInstalled } from './windows-supervisor.mjs';
21
21
  import { ensureWindowsSupervisorKey, windowsSupervisorCommand } from './windows-user-supervisor.mjs';
22
+ import { scheduleWindowsTaskRestart } from './windows-task-restart.mjs';
22
23
 
23
24
  export function quoteSystemd(value) {
24
25
  return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
@@ -78,6 +79,20 @@ export function writeWindowsServiceLauncher(cliPath = globalCliPath(), nodePath
78
79
  return windowsServiceLauncherFile;
79
80
  }
80
81
 
82
+ function writeWindowsTaskScript(launcherFile) {
83
+ if (fs.existsSync(windowsTaskScriptFile) && !windowsTaskScriptInstalled(windowsTaskScriptFile, windowsServiceLauncherFile)) {
84
+ throw new Error('The ReMCP scheduled task script is not owned by ReMCP; refusing to overwrite it');
85
+ }
86
+ const script = windowsTaskScriptText(launcherFile);
87
+ fs.mkdirSync(configDir, { recursive:true, mode:0o700 });
88
+ const temporary = `${windowsTaskScriptFile}.${process.pid}.${Date.now()}.tmp`;
89
+ try {
90
+ fs.writeFileSync(temporary, '\uFEFF' + script, { flag:'wx', encoding:'utf16le', mode:0o600 });
91
+ fs.renameSync(temporary, windowsTaskScriptFile);
92
+ } finally { try { fs.unlinkSync(temporary); } catch {} }
93
+ return windowsTaskScriptFile;
94
+ }
95
+
81
96
  function installWindowsUserStartup(cliPath, nodePath) {
82
97
  const existed = fs.existsSync(windowsStartupScriptFile);
83
98
  if (existed && !windowsStartupScriptInstalled(windowsStartupScriptFile)) {
@@ -147,19 +162,11 @@ export function installWindowsService(cliPath = globalCliPath(), { nodePath = pr
147
162
  installWindowsUserStartup(cliPath, nodePath);
148
163
  return;
149
164
  }
150
- // Task Scheduler runs an /IT .cmd inside a *visible* terminal window.
151
- // For fresh installations prefer our existing hidden WSH + Node supervisor.
152
- // Keep already-registered scheduled tasks intact until an explicit migration;
153
- // never start two supervisors for the same machine.
154
- const previousTask = spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName],
155
- { stdio:'ignore', windowsHide:true, timeout:10000 });
156
- if (previousTask.status !== 0) {
157
- requireWindowsScheduledTaskAbsent();
158
- installWindowsUserStartup(cliPath, nodePath);
159
- return;
160
- }
161
165
  const launcherFile = writeWindowsServiceLauncher(cliPath, nodePath);
162
- const command = `"${launcherFile}"`;
166
+ const taskScript = writeWindowsTaskScript(launcherFile);
167
+ // WScript has no console; its hidden CMD child remains attached until it exits.
168
+ // /IT keeps desktop/UI automation in the signed-in user's session.
169
+ const command = `wscript.exe //B //Nologo "${taskScript}"`;
163
170
  // Current-user interactive tasks need /IT. /RL LIMITED avoids requiring elevation.
164
171
  // An organization may still disable creation of all tasks by standard users.
165
172
  const created = spawnSync('schtasks.exe',
@@ -347,7 +354,7 @@ export function deferredLinuxAgentRestartArgs(pid) {
347
354
  'systemctl', '--user', 'restart', 'remcp-agent.service'];
348
355
  }
349
356
 
350
- export function restartPersistentServiceIfInstalled(config, { deferLinux = false } = {}) {
357
+ export function restartPersistentServiceIfInstalled(config, { deferLinux = false, deferWindows = false } = {}) {
351
358
  if (!persistentServiceExpected(config)) return null;
352
359
  const platform = servicePlatform();
353
360
  if (platform === 'linux' && fs.existsSync(linuxServiceFile)) {
@@ -382,6 +389,7 @@ export function restartPersistentServiceIfInstalled(config, { deferLinux = false
382
389
  }
383
390
  const result = spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName], { stdio: 'ignore' });
384
391
  if (result.status === 0) {
392
+ if (deferWindows) return scheduleWindowsTaskRestart(canonicalServiceNodePath(config));
385
393
  // /Run does not restart a task that is already running; stop the previous
386
394
  // process first so a rotated credential is loaded by the replacement.
387
395
  spawnSync('schtasks.exe', ['/End', '/TN', windowsTaskName], { stdio: 'ignore' });
@@ -420,6 +428,7 @@ export function uninstallPersistentService() {
420
428
  }
421
429
  spawnSync('schtasks.exe', ['/End', '/TN', windowsTaskName], { stdio: 'ignore' });
422
430
  spawnSync('schtasks.exe', ['/Delete', '/TN', windowsTaskName, '/F'], { stdio: 'ignore' });
431
+ try { if (windowsTaskScriptInstalled(windowsTaskScriptFile, windowsServiceLauncherFile)) fs.unlinkSync(windowsTaskScriptFile); } catch {}
423
432
  try { fs.unlinkSync(windowsServiceLauncherFile); } catch {}
424
433
  }
425
434
  }
@@ -24,6 +24,12 @@ export function windowsBatchUnsafeProfile(...paths) {
24
24
  }
25
25
 
26
26
  export function windowsServiceLauncherText(cliPath, nodePath, { localAppData = process.env.LOCALAPPDATA } = {}) {
27
+ // CMD's `call` executes batch shims, not the underlying .mjs source. A saved
28
+ // package entry is valid for `node.exe remcp.mjs`, but invalid as a Task
29
+ // Scheduler command and must never be written into a persistent launcher.
30
+ if (/\.mjs$/i.test(String(cliPath))) {
31
+ throw new Error('Windows Scheduled Task requires the ReMCP .cmd shim, not a .mjs module');
32
+ }
27
33
  // Tests can simulate win32 from a POSIX runner with injected temporary paths.
28
34
  // Native Windows paths always use win32 parsing; POSIX-shaped fixtures keep their roots.
29
35
  const dirname = file => /^[A-Za-z]:[\\/]/.test(file) || file.includes('\\')
@@ -65,6 +71,44 @@ export function windowsServiceLauncherText(cliPath, nodePath, { localAppData = p
65
71
  ].join('\r\n');
66
72
  }
67
73
 
74
+ export const WINDOWS_TASK_MARKER = "' ReMCP managed scheduled task (do not edit)";
75
+
76
+ // WScript is a GUI subsystem executable: it starts the existing CMD supervisor
77
+ // with SW_HIDE, while waiting for it so Task Scheduler still owns its lifetime.
78
+ // A Task Scheduler "Hidden" setting only hides the task in its UI, not the console.
79
+ export function windowsTaskScriptText(launcherPath) {
80
+ if (typeof launcherPath !== 'string' || !launcherPath ||
81
+ /["\0\r\n]/.test(launcherPath) ||
82
+ !(path.isAbsolute(launcherPath) || path.win32.isAbsolute(launcherPath))) {
83
+ throw new Error('Invalid ReMCP Windows task launcher');
84
+ }
85
+ return [
86
+ WINDOWS_TASK_MARKER,
87
+ 'Set remcpShell = CreateObject("WScript.Shell")',
88
+ `remcpShell.Run Chr(34) & "${launcherPath}" & Chr(34), 0, True`,
89
+ '',
90
+ ].join('\r\n');
91
+ }
92
+
93
+ export function windowsTaskScriptInstalled(file, expectedLauncherPath = null) {
94
+ try {
95
+ const text = fs.readFileSync(file, 'utf16le');
96
+ const lines = text.replace(/^\uFEFF/, '').split('\r\n');
97
+ if (!text.startsWith('\uFEFF') || lines.length !== 4 ||
98
+ lines[0] !== WINDOWS_TASK_MARKER ||
99
+ lines[1] !== 'Set remcpShell = CreateObject("WScript.Shell")' ||
100
+ lines[3] !== '') return false;
101
+ const match = lines[2].match(/^remcpShell\.Run Chr\(34\) & "([^"]+)" & Chr\(34\), 0, True$/);
102
+ if (!match) return false;
103
+ const launcher = match[1];
104
+ const absolute = path.isAbsolute(launcher) || path.win32.isAbsolute(launcher);
105
+ const expected = expectedLauncherPath === null ||
106
+ path.win32.normalize(launcher).toLowerCase() === path.win32.normalize(expectedLauncherPath).toLowerCase();
107
+ return absolute && expected &&
108
+ path.posix.basename(launcher.replaceAll('\\', '/')) === 'remcp-agent.cmd';
109
+ } catch { return false; }
110
+ }
111
+
68
112
  export const WINDOWS_STARTUP_MARKER = "' ReMCP managed user startup (do not edit)";
69
113
 
70
114
  // Start the existing private launcher in the interactive user's session without displaying
@@ -0,0 +1,53 @@
1
+ // ReMCP's confirmed remote God Mode change must acknowledge its command BEFORE
2
+ // killing the agent's own Task Scheduler process tree. Launch a fixed-purpose
3
+ // helper outside the CLI process; never interpolate a user-provided task or shell.
4
+ import { spawn, spawnSync } from 'node:child_process';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import process from 'node:process';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { windowsTaskName } from './env.mjs';
10
+
11
+ const helperPath = fileURLToPath(import.meta.url);
12
+ const options = { windowsHide:true, encoding:'utf8', timeout:15000, maxBuffer:32768 };
13
+
14
+ export function scheduleWindowsTaskRestart(nodePath = process.execPath) {
15
+ if (!path.isAbsolute(nodePath) || !fs.existsSync(nodePath)) {
16
+ throw new Error('Windows deferred restart requires the existing Node interpreter');
17
+ }
18
+ const child = spawn(nodePath, [helperPath, '--relaunch'], {
19
+ detached:true, windowsHide:true, stdio:'ignore',
20
+ });
21
+ if (!Number.isSafeInteger(child.pid) || child.pid <= 0) {
22
+ throw new Error('Could not schedule independent Windows ReMCP restart');
23
+ }
24
+ child.unref();
25
+ return 'ReMCP Agent (restart scheduled)';
26
+ }
27
+
28
+ export async function executeWindowsTaskRestart({
29
+ pause = ms => new Promise(resolve => setTimeout(resolve,ms)),
30
+ invoke = spawnSync,
31
+ } = {}) {
32
+ await pause(2500);
33
+ // Do not run a second agent if Windows refused to stop the owned task.
34
+ const stopped = invoke('schtasks.exe', ['/End','/TN',windowsTaskName], options);
35
+ if (stopped.error || stopped.status !== 0) {
36
+ throw new Error('Windows ReMCP task could not be stopped safely');
37
+ }
38
+ const started = invoke('schtasks.exe', ['/Run','/TN',windowsTaskName], options);
39
+ if (started.error || started.status !== 0) {
40
+ throw new Error('Windows ReMCP task could not be started after stop');
41
+ }
42
+ }
43
+
44
+ if (process.argv[1] && path.resolve(process.argv[1]) === helperPath) {
45
+ if (process.argv[2] !== '--relaunch' || process.argv.length !== 3 || process.platform !== 'win32') {
46
+ process.exitCode = 2;
47
+ } else {
48
+ executeWindowsTaskRestart().catch(error => {
49
+ console.error(error.message);
50
+ process.exitCode = 1;
51
+ });
52
+ }
53
+ }
package/src/cli.mjs CHANGED
@@ -271,11 +271,12 @@ export async function main(argv = process.argv.slice(2)) {
271
271
  if (!['on', 'off', 'enable', 'disable'].includes(action)) throw new Error('Usage: remcp godmode [status|on|off]');
272
272
  const enabled = action === 'on' || action === 'enable';
273
273
  writeJsonFile(runtimeConfigFile, { ...readJsonFile(runtimeConfigFile), unrestricted: enabled });
274
- // The authenticated Workspace supplies this explicit flag to allow the
275
- // remote command to finish before its agent schedules a Linux restart.
276
- // Local CLI and normal update operations retain synchronous semantics.
274
+ // The authenticated Workspace supplies this explicit flag so the remote
275
+ // command can acknowledge a policy change before Linux/Windows kills its
276
+ // own agent session. Local CLI operations retain synchronous semantics.
277
277
  const restarted = restartPersistentServiceIfInstalled(loadConfig(false),
278
- { deferLinux: flags['defer-restart'] === true });
278
+ { deferLinux: flags['defer-restart'] === true,
279
+ deferWindows: flags['defer-restart'] === true });
279
280
  if (enabled) {
280
281
  console.error('Unrestricted mode is ON for this computer: every path and every command is allowed, including sudo. Anything the model is asked to do -- and anything a prompt injection asks it to do -- can now change this machine. Turn it off with `remcp godmode off` when you are done.');
281
282
  } else {