@remcp/remcp 0.2.231 → 0.2.232

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.231",
3
+ "version": "0.2.232",
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",
@@ -117,6 +117,7 @@ export function createAgentUpdateMonitor({ options, serverUrl, queueEvent, getRu
117
117
  const updateArgs = updateInvocationArgs(decision, options.trustRuntime === true);
118
118
  const child = spawn(process.execPath, [cli, ...updateArgs], {
119
119
  detached: true,
120
+ windowsHide: true,
120
121
  // The updater's own output is the only record of why an install failed, so it is piped back
121
122
  // into this agent's log instead of being discarded.
122
123
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -24,7 +24,7 @@ function packageLocalNodeModules() {
24
24
  const REPLACEMENT_HANDOVER_TIMEOUT_MS = 20_000;
25
25
 
26
26
  function globalNodeModules() {
27
- const result = spawnSync(npm.command, [...npm.args, 'root', '--global'], { encoding: 'utf8' });
27
+ const result = spawnSync(npm.command, [...npm.args, 'root', '--global'], { encoding: 'utf8', windowsHide: true });
28
28
  if (result.error || result.status !== 0) throw new Error('Could not locate the global npm modules directory');
29
29
  return String(result.stdout || '').trim();
30
30
  }
@@ -142,7 +142,7 @@ export async function restartToApplyUpdate(cli, stopAgent, markStopping, onRunti
142
142
  }
143
143
  // Handing over to a version that cannot start would take the machine offline with nobody left to
144
144
  // retry. The new CLI has to answer `--version` before this process steps aside.
145
- const probe = spawnSync(process.execPath, [cli, '--version'], { encoding: 'utf8', timeout: 30000 });
145
+ const probe = spawnSync(process.execPath, [cli, '--version'], { encoding: 'utf8', timeout: 30000, windowsHide: true });
146
146
  const reported = String(probe.stdout || '').trim();
147
147
  if (probe.error || probe.status !== 0 || !/^\d+\.\d+\.\d+/.test(reported)) {
148
148
  console.error(`The installed ReMCP ${installed || 'update'} did not run (${probe.error?.message || `exit ${probe.status}`}${reported ? `: ${reported}` : ''}). Keeping ${VERSION} running; retry with: remcp update`);
@@ -150,7 +150,7 @@ export async function restartToApplyUpdate(cli, stopAgent, markStopping, onRunti
150
150
  }
151
151
  console.log(`ReMCP ${installed || 'a newer version'} installed and verified (${reported}); restarting to apply it.`);
152
152
  if (!supervisorRestart()) {
153
- spawn(process.execPath, [cli, 'start'], { detached: true, stdio: 'ignore', env: { ...process.env } }).unref();
153
+ spawn(process.execPath, [cli, 'start'], { detached: true, windowsHide: true, stdio: 'ignore', env: { ...process.env } }).unref();
154
154
  // Stepping aside is only safe once the replacement really holds the device: the relay closes
155
155
  // this socket with 1012 ('replaced') the moment another agent takes the machine over, and that
156
156
  // close is what stops this process. Without the wait, a replacement that cannot start left the
@@ -221,7 +221,7 @@ export function globalCliEntry({ packageLocal = true } = {}) {
221
221
  // CLI entry and avoid asking npm which global prefix happens to be active in a minimal service
222
222
  // environment (FNM/NVM/Hermes/Homebrew can all disagree with the shell).
223
223
  if (packageLocal && existsSync(PACKAGE_CLI_ENTRY)) return PACKAGE_CLI_ENTRY;
224
- const prefix = spawnSync(npm.command, [...npm.args, 'prefix', '--global'], { encoding:'utf8' });
224
+ const prefix = spawnSync(npm.command, [...npm.args, 'prefix', '--global'], { encoding:'utf8', windowsHide:true });
225
225
  if (prefix.error || prefix.status !== 0) return null;
226
226
  const base = String(prefix.stdout || '').trim();
227
227
  return process.platform === 'win32'
@@ -108,7 +108,7 @@ export function runtimeAllowedRoots() {
108
108
  }
109
109
 
110
110
  export function installedVersion(packageName) {
111
- const prefix = spawnSync(npm.command, [...npm.args, 'prefix', '--global'], { encoding: 'utf8' });
111
+ const prefix = spawnSync(npm.command, [...npm.args, 'prefix', '--global'], { encoding: 'utf8', windowsHide: true });
112
112
  if (prefix.error || prefix.status !== 0) return null;
113
113
  const manifest = path.join(String(prefix.stdout || '').trim(), 'lib', 'node_modules', ...packageName.split('/'), 'package.json');
114
114
  try { return JSON.parse(fs.readFileSync(manifest, 'utf8')).version || null; } catch { return null; }
@@ -16,7 +16,7 @@ export function persistentServiceExpected(config) {
16
16
  if (platform === 'darwin') return fs.existsSync(macServiceFile);
17
17
  if (platform === 'win32') {
18
18
  return windowsStartupScriptInstalled(windowsStartupScriptFile) ||
19
- spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName], { stdio: 'ignore' }).status === 0;
19
+ spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName], { stdio: 'ignore', windowsHide: true }).status === 0;
20
20
  }
21
21
  return false;
22
22
  }
@@ -36,7 +36,7 @@ export function persistentServiceState(config) {
36
36
  return { expected, installed, active, manager:'launchd', name:macServiceLabel };
37
37
  }
38
38
  if (platform === 'win32') {
39
- const query = spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName, '/FO', 'LIST'], { encoding:'utf8' });
39
+ const query = spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName, '/FO', 'LIST'], { encoding:'utf8', windowsHide:true });
40
40
  const installed = query.status === 0;
41
41
  const outputText = String(query.stdout || '');
42
42
  const active = installed && /(?:Status|Состояние):\s*Running/i.test(outputText);
package/src/cli/shell.mjs CHANGED
@@ -3,14 +3,14 @@
3
3
  import { spawnSync } from 'node:child_process';
4
4
 
5
5
  export function run(command, args, options = {}) {
6
- const result = spawnSync(command, args, { stdio: 'inherit', ...options });
6
+ const result = spawnSync(command, args, { stdio: 'inherit', ...options, windowsHide: true });
7
7
  if (result.error) throw result.error;
8
8
  if (result.status !== 0) throw new Error(`${command} failed with exit code ${result.status}`);
9
9
  return result;
10
10
  }
11
11
 
12
12
  export function output(command, args) {
13
- const result = spawnSync(command, args, { encoding: 'utf8' });
13
+ const result = spawnSync(command, args, { encoding: 'utf8', windowsHide: true });
14
14
  if (result.error) throw result.error;
15
15
  if (result.status !== 0) throw new Error(`${command} failed with exit code ${result.status}`);
16
16
  return String(result.stdout || '').trim();
@@ -71,3 +71,65 @@ export function preferWindowsCmdShim(prefix) {
71
71
  fs.unlinkSync(powershell);
72
72
  return true;
73
73
  }
74
+
75
+
76
+ // An older auto-updater can install a newer client without running that newer
77
+ // client's post-npm shim cleanup. Repair the canonical npm-owned shadow at
78
+ // agent startup so a Windows account using Restricted PowerShell remains usable.
79
+ // Do not touch arbitrary prefixes, custom scripts, links or nonstandard shims.
80
+ export const WINDOWS_NPM_CMD_SHIM = String.raw`@ECHO off
81
+ GOTO start
82
+ :find_dp0
83
+ SET dp0=%~dp0
84
+ EXIT /b
85
+ :start
86
+ SETLOCAL
87
+ CALL :find_dp0
88
+
89
+ IF EXIST "%dp0%\node.exe" (
90
+ SET "_prog=%dp0%\node.exe"
91
+ ) ELSE (
92
+ SET "_prog=node"
93
+ SET PATHEXT=%PATHEXT:;.JS;=;%
94
+ )
95
+
96
+ endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\node_modules\@remcp\remcp\bin\remcp.mjs" %*
97
+ `;
98
+
99
+ function sameCanonicalFile(actual, expected) {
100
+ if (!actual || !expected) return false;
101
+ const same = (a, b) => path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
102
+ try {
103
+ return same(actual, expected) && same(fs.realpathSync(actual), expected)
104
+ && same(fs.realpathSync(expected), expected);
105
+ } catch { return false; }
106
+ }
107
+
108
+ export function repairManagedWindowsPowerShellShimOnStart({
109
+ platform = process.platform,
110
+ localAppData = process.env.LOCALAPPDATA,
111
+ nodePath = process.execPath,
112
+ cliEntry = process.argv[1],
113
+ } = {}) {
114
+ if (platform !== 'win32' || !localAppData || !path.isAbsolute(localAppData)) return false;
115
+ const prefix = path.join(localAppData, 'ReMCP', 'runtime');
116
+ const expectedNode = path.join(prefix, 'node.exe');
117
+ const expectedCli = path.join(prefix, 'node_modules', '@remcp', 'remcp', 'bin', 'remcp.mjs');
118
+ if (!sameCanonicalFile(nodePath, expectedNode) || !sameCanonicalFile(cliEntry, expectedCli)) return false;
119
+
120
+ const directory = fs.lstatSync(prefix);
121
+ if (!directory.isDirectory() || directory.isSymbolicLink()) {
122
+ throw new Error('Refusing Windows launcher self-repair outside a real managed runtime directory');
123
+ }
124
+ if (!assertWindowsNpmPowerShellShimOwned(prefix)) return false;
125
+ const cmdPath = path.join(prefix, 'remcp.cmd');
126
+ const cmdStat = fs.lstatSync(cmdPath);
127
+ if (!cmdStat.isFile() || cmdStat.isSymbolicLink()) {
128
+ throw new Error('Refusing Windows launcher self-repair without a trusted batch shim');
129
+ }
130
+ const cmdBody = fs.readFileSync(cmdPath, 'utf8');
131
+ if (normalizedShim(cmdBody) !== normalizedShim(WINDOWS_NPM_CMD_SHIM)) {
132
+ throw new Error('Refusing Windows launcher self-repair for an unrecognized batch shim');
133
+ }
134
+ return preferWindowsCmdShim(prefix);
135
+ }
package/src/cli.mjs CHANGED
@@ -17,6 +17,7 @@ import { auditKnownInstallations } from './cli/installation-audit.mjs';
17
17
  import { scheduleMacTemperatureRepair } from './cli/mac-native-temperature.mjs';
18
18
  import { guardSupervisorDisconnect } from './cli/supervised-agent-lifecycle.mjs';
19
19
  import { repairMacTemperatureAfterStart } from './cli/npm-global.mjs';
20
+ import { repairManagedWindowsPowerShellShimOnStart } from './cli/windows-cli-shim.mjs';
20
21
  import { run } from './cli/shell.mjs';
21
22
  import { updateCommand } from './cli/update.mjs';
22
23
  function fetchConfiguredServer(url, options) {
@@ -165,6 +166,13 @@ export async function main(argv = process.argv.slice(2)) {
165
166
  || Boolean(process.env.INVOCATION_ID)
166
167
  || String(process.env.XPC_SERVICE_NAME || '').includes('com.remcp.agent');
167
168
  const cfg = rememberCurrentInstallation(loadConfig(), { service:supervised });
169
+ if (process.platform === 'win32') {
170
+ try { repairManagedWindowsPowerShellShimOnStart(); }
171
+ catch (error) {
172
+ // Never take a paired device offline because an old launcher is custom or unreadable.
173
+ console.error(`ReMCP Windows PowerShell launcher self-repair skipped: ${error instanceof Error ? error.message : String(error)}`);
174
+ }
175
+ }
168
176
  // A service can start from an absolute fnm/nvm Node path even when an interactive macOS shell
169
177
  // cannot resolve the npm-global `remcp` binary. Refresh the stable user shim on every start so
170
178
  // an auto-update repairs the next Terminal session without requiring a manual PATH edit.
package/src/npm.mjs CHANGED
@@ -62,13 +62,13 @@ export function resolveNpm({ nodePath = process.execPath, home = os.homedir(), p
62
62
 
63
63
  // The version npm itself reports, or null when it cannot be executed at all.
64
64
  export function npmVersion(resolved = resolveNpm()) {
65
- const result = spawnSync(resolved.command, [...resolved.args, '--version'], { encoding: 'utf8', timeout: 15000 });
65
+ const result = spawnSync(resolved.command, [...resolved.args, '--version'], { encoding: 'utf8', timeout: 15000, windowsHide: true });
66
66
  if (result.error || result.status !== 0) {
67
67
  // A login shell may still find npm (nvm and Homebrew write their PATH into the profile).
68
- const shell = process.platform === 'win32' ? null : spawnSync('/bin/sh', ['-lc', 'command -v npm'], { encoding: 'utf8', timeout: 15000 });
68
+ const shell = process.platform === 'win32' ? null : spawnSync('/bin/sh', ['-lc', 'command -v npm'], { encoding: 'utf8', timeout: 15000, windowsHide: true });
69
69
  const found = String(shell?.stdout || '').trim().split('\n').pop();
70
70
  if (found && existsSync(found)) {
71
- const retry = spawnSync(found, ['--version'], { encoding: 'utf8', timeout: 15000 });
71
+ const retry = spawnSync(found, ['--version'], { encoding: 'utf8', timeout: 15000, windowsHide: true });
72
72
  if (!retry.error && retry.status === 0) return { version: String(retry.stdout).trim(), source: found };
73
73
  }
74
74
  return null;
@@ -79,5 +79,5 @@ export function npmVersion(resolved = resolveNpm()) {
79
79
  // Runs npm with the resolved command, so callers never depend on PATH.
80
80
  export function npmRun(args, { encoding = 'utf8', stdio = 'inherit' } = {}) {
81
81
  const resolved = resolveNpm();
82
- return spawnSync(resolved.command, [...resolved.args, ...args], { encoding, stdio });
82
+ return spawnSync(resolved.command, [...resolved.args, ...args], { encoding, stdio, windowsHide: true });
83
83
  }