@remcp/remcp 0.2.116 → 0.2.118

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
@@ -9,10 +9,12 @@ remcp --version
9
9
  remcp status
10
10
  ```
11
11
 
12
- Pairing commands are generated in the workspace at <https://remcp.site/app/connect>. The
13
- generated command runs `remcp connect --server … --code … --install`, which stores a per-device
14
- credential under `~/.config/remcp/`, installs the runtime from npm, and registers a user service
15
- (systemd on Linux, LaunchAgent on macOS, Scheduled Task on Windows).
12
+ Pairing commands are generated in the workspace at <https://remcp.site/app/connect>. On Windows,
13
+ the browser shows a PowerShell bootstrap that needs no preinstalled system Node.js/npm/npx/winget;
14
+ it keeps the ReMCP runtime under `%LOCALAPPDATA%\ReMCP`. Generated one-time commands ultimately run
15
+ `remcp connect --server … --code … --install`, which stores a per-device credential, installs the
16
+ runtime, and registers a user service (systemd on Linux, LaunchAgent on macOS, Scheduled Task on
17
+ Windows).
16
18
 
17
19
  ## Commands
18
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/remcp",
3
- "version": "0.2.116",
3
+ "version": "0.2.118",
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",
package/src/cli/env.mjs CHANGED
@@ -16,6 +16,7 @@ export const macServiceLabel = 'com.remcp.agent';
16
16
  export const macServiceFile = path.join(home, 'Library', 'LaunchAgents', `${macServiceLabel}.plist`);
17
17
  export const macLogFile = path.join(home, 'Library', 'Logs', 'remcp-agent.log');
18
18
  export const windowsTaskName = 'ReMCP Agent';
19
+ export const windowsServiceLauncherFile = path.join(configDir, 'remcp-agent.cmd');
19
20
  // How npm is invoked is resolved from the running node when possible: a background service has a
20
21
  // minimal PATH, which is why auto-update used to find no npm on macOS. See src/npm.mjs.
21
22
  export const npm = resolveNpm();
@@ -9,7 +9,7 @@ import { resolveNpm } from '../npm.mjs';
9
9
  import { PACKAGE_NAME, VERSION } from '../version.mjs';
10
10
 
11
11
  import { saveConfig } from './config.mjs';
12
- import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macLogFile, macServiceFile, macServiceLabel, npm, windowsTaskName } from './env.mjs';
12
+ import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macLogFile, macServiceFile, macServiceLabel, npm, windowsServiceLauncherFile, windowsTaskName } from './env.mjs';
13
13
  import { output, run } from './shell.mjs';
14
14
 
15
15
  export function servicePlatform() {
@@ -51,13 +51,30 @@ function pathEntryExists(file) {
51
51
  }
52
52
  }
53
53
 
54
- function globalPackagePath(prefix, packageName) {
54
+ function globalPackagePath(prefix, packageName, platform = servicePlatform()) {
55
55
  const name = String(packageName || '').trim();
56
56
  const parts = name.split('/');
57
57
  const validUnscoped = parts.length === 1 && parts[0] && !parts[0].startsWith('@') && parts[0] !== '.' && parts[0] !== '..';
58
58
  const validScoped = parts.length === 2 && /^@[^/]+$/.test(parts[0]) && parts[1] && parts[1] !== '.' && parts[1] !== '..';
59
59
  if (!validUnscoped && !validScoped) throw new Error(`Invalid npm package name for update: ${name || '(empty)'}`);
60
- return path.join(prefix, 'lib', 'node_modules', ...parts);
60
+ const modulesRoot = platform === 'win32'
61
+ ? path.join(prefix, 'node_modules')
62
+ : path.join(prefix, 'lib', 'node_modules');
63
+ return path.join(modulesRoot, ...parts);
64
+ }
65
+
66
+ function managedWindowsRuntimePrefix() {
67
+ return path.resolve(
68
+ process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'),
69
+ 'ReMCP',
70
+ 'runtime',
71
+ );
72
+ }
73
+
74
+ function isManagedWindowsRuntimePrefix(prefix) {
75
+ const actual = path.resolve(String(prefix || ''));
76
+ const expected = managedWindowsRuntimePrefix();
77
+ return actual.toLowerCase() === expected.toLowerCase();
61
78
  }
62
79
 
63
80
  function transactionalMacGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline = false }) {
@@ -111,26 +128,93 @@ function transactionalMacGlobalInstall({ resolved, prefix, packageNames, specs,
111
128
  }
112
129
  }
113
130
 
131
+ function transactionalWindowsGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline = false }) {
132
+ prefix = String(prefix || '').trim();
133
+ if (!path.isAbsolute(prefix)) throw new Error(`npm global prefix must be absolute for a Windows update: ${prefix || '(empty)'}`);
134
+ if (!isManagedWindowsRuntimePrefix(prefix)) {
135
+ run(resolved.command, [...resolved.args, ...npmGlobalInstallArgs(specs, { preferOnline })]);
136
+ return;
137
+ }
138
+
139
+ const backupRoot = path.join(prefix, `.remcp-update-backup-${process.pid}-${Date.now()}`);
140
+ const moved = [];
141
+ const moveAside = (source, label) => {
142
+ if (!pathEntryExists(source)) return;
143
+ const backup = path.join(backupRoot, label);
144
+ fs.renameSync(source, backup);
145
+ moved.push({ source, backup });
146
+ };
147
+ const restore = originalError => {
148
+ let rollbackError = null;
149
+ for (const item of [...moved].reverse()) {
150
+ try {
151
+ if (pathEntryExists(item.source)) fs.rmSync(item.source, { recursive:true, force:true });
152
+ fs.mkdirSync(path.dirname(item.source), { recursive:true });
153
+ fs.renameSync(item.backup, item.source);
154
+ } catch (error) {
155
+ rollbackError ??= error;
156
+ }
157
+ }
158
+ try { fs.rmSync(backupRoot, { recursive:true, force:true }); } catch (error) { rollbackError ??= error; }
159
+ if (rollbackError) {
160
+ throw new Error(
161
+ `ReMCP update failed and the previous Windows installation could not be fully restored: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`,
162
+ { cause:originalError },
163
+ );
164
+ }
165
+ };
166
+
167
+ fs.mkdirSync(prefix, { recursive:true });
168
+ fs.mkdirSync(backupRoot, { recursive:false, mode:0o700 });
169
+ try {
170
+ const uniqueNames = [...new Set(packageNames.map(name => String(name || '').trim()).filter(Boolean))];
171
+ uniqueNames.forEach((name, index) => moveAside(globalPackagePath(prefix, name, 'win32'), `package-${index}`));
172
+ if (uniqueNames.includes(PACKAGE_NAME)) {
173
+ for (const shim of ['remcp', 'remcp.cmd', 'remcp.ps1']) moveAside(path.join(prefix, shim), `bin-${shim}`);
174
+ }
175
+ run(resolved.command, [...resolved.args, ...npmGlobalInstallArgs(specs, { preferOnline })]);
176
+ } catch (error) {
177
+ restore(error);
178
+ throw error;
179
+ }
180
+
181
+ try {
182
+ fs.rmSync(backupRoot, { recursive:true, force:true });
183
+ } catch (error) {
184
+ console.error(`ReMCP update: could not remove Windows rollback backup ${backupRoot}: ${error instanceof Error ? error.message : String(error)}`);
185
+ }
186
+ }
187
+
114
188
  // Release updates use exact versions that may have been published only moments ago. npm can keep a
115
189
  // cached pre-publication packument long enough to answer ETARGET after the registry already serves the
116
190
  // version, so updates revalidate metadata while retaining npm's cache for package bytes.
117
191
  export function npmGlobalUpdate(packageNames, ...specs) {
118
- if (servicePlatform() !== 'darwin') {
119
- run(npm.command, [...npm.args, ...npmGlobalInstallArgs(specs, { preferOnline:true })]);
192
+ const platform = servicePlatform();
193
+ const prefix = globalPrefix();
194
+ if (platform === 'darwin') {
195
+ transactionalMacGlobalInstall({ resolved:npm, prefix, packageNames, specs, preferOnline:true });
120
196
  return;
121
197
  }
122
- const prefix = globalPrefix();
123
- transactionalMacGlobalInstall({ resolved:npm, prefix, packageNames, specs, preferOnline:true });
198
+ if (platform === 'win32') {
199
+ transactionalWindowsGlobalInstall({ resolved:npm, prefix, packageNames, specs, preferOnline:true });
200
+ return;
201
+ }
202
+ run(npm.command, [...npm.args, ...npmGlobalInstallArgs(specs, { preferOnline:true })]);
124
203
  }
125
204
 
126
205
  export function npmGlobalUpdateForNode(nodePath, packageNames, ...specs) {
127
- const resolved = resolveNpm({ nodePath, home, platform:servicePlatform() });
128
- if (servicePlatform() !== 'darwin') {
129
- run(resolved.command, [...resolved.args, ...npmGlobalInstallArgs(specs, { preferOnline:true })]);
206
+ const platform = servicePlatform();
207
+ const resolved = resolveNpm({ nodePath, home, platform });
208
+ const prefix = output(resolved.command, [...resolved.args, 'prefix', '--global']);
209
+ if (platform === 'darwin') {
210
+ transactionalMacGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline:true });
130
211
  return;
131
212
  }
132
- const prefix = output(resolved.command, [...resolved.args, 'prefix', '--global']);
133
- transactionalMacGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline:true });
213
+ if (platform === 'win32') {
214
+ transactionalWindowsGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline:true });
215
+ return;
216
+ }
217
+ run(resolved.command, [...resolved.args, ...npmGlobalInstallArgs(specs, { preferOnline:true })]);
134
218
  }
135
219
 
136
220
  export function quoteSystemd(value) {
@@ -350,8 +434,32 @@ export function installMacService(cliPath = globalCliPath(), { restart = true, n
350
434
  return 'bootstrapped';
351
435
  }
352
436
 
353
- export function installWindowsService(cliPath = globalCliPath()) {
354
- const command = `"${cliPath}" start --service`;
437
+ function windowsBatchValue(value) {
438
+ return String(value).replaceAll('%', '%%');
439
+ }
440
+
441
+ export function writeWindowsServiceLauncher(cliPath = globalCliPath(), nodePath = process.execPath) {
442
+ const prefix = path.dirname(cliPath);
443
+ const nodeDir = path.dirname(nodePath);
444
+ const launcher = [
445
+ '@echo off',
446
+ 'setlocal DisableDelayedExpansion',
447
+ `set "NPM_CONFIG_PREFIX=${windowsBatchValue(prefix)}"`,
448
+ `set "PATH=${windowsBatchValue(nodeDir)};%PATH%"`,
449
+ `call "${windowsBatchValue(cliPath)}" start --service`,
450
+ 'exit /b %ERRORLEVEL%',
451
+ '',
452
+ ].join('\r\n');
453
+ fs.mkdirSync(configDir, { recursive:true, mode:0o700 });
454
+ const temporary = `${windowsServiceLauncherFile}.${process.pid}.${Date.now()}.tmp`;
455
+ fs.writeFileSync(temporary, launcher, { mode:0o700 });
456
+ fs.renameSync(temporary, windowsServiceLauncherFile);
457
+ return windowsServiceLauncherFile;
458
+ }
459
+
460
+ export function installWindowsService(cliPath = globalCliPath(), { nodePath = process.execPath } = {}) {
461
+ const launcherFile = writeWindowsServiceLauncher(cliPath, nodePath);
462
+ const command = `"${launcherFile}"`;
355
463
  run('schtasks.exe', ['/Create', '/TN', windowsTaskName, '/TR', command, '/SC', 'ONLOGON', '/RL', 'HIGHEST', '/F']);
356
464
  run('schtasks.exe', ['/Run', '/TN', windowsTaskName]);
357
465
  }
@@ -450,13 +558,24 @@ function packageVersionBesideCli(cliPath, packageName) {
450
558
  return versionFromPackageJson(path.join(nodeModules, ...String(packageName).split('/'), 'package.json'));
451
559
  }
452
560
 
561
+ function packageVersionForCli(cliPath, packageName) {
562
+ const beside = packageVersionBesideCli(cliPath, packageName);
563
+ if (beside || servicePlatform() !== 'win32' || !cliPath || !packageName) return beside;
564
+ // npm's Windows global executable is a .cmd shim in the prefix root, not a symlink into
565
+ // node_modules. realpath() therefore cannot reveal the package directory like it does on Unix.
566
+ // Resolve the package from the same prefix layout npm uses on Windows.
567
+ if (path.basename(cliPath).toLowerCase() !== 'remcp.cmd') return null;
568
+ const prefix = path.dirname(cliPath);
569
+ return versionFromPackageJson(path.join(prefix, 'node_modules', ...String(packageName).split('/'), 'package.json'));
570
+ }
571
+
453
572
  export function installationVersionsAtCliPath(cliPath, runtimePackageName) {
454
573
  if (!cliPath) return { cliVersion:null, runtimeVersion:null, cliPath:null };
455
574
  let resolvedCliPath = cliPath;
456
575
  try { if (fs.existsSync(cliPath)) resolvedCliPath = fs.realpathSync(cliPath); } catch {}
457
576
  return {
458
- cliVersion:packageVersionBesideCli(resolvedCliPath, PACKAGE_NAME),
459
- runtimeVersion:packageVersionBesideCli(resolvedCliPath, runtimePackageName),
577
+ cliVersion:packageVersionForCli(resolvedCliPath, PACKAGE_NAME),
578
+ runtimeVersion:packageVersionForCli(resolvedCliPath, runtimePackageName),
460
579
  cliPath:resolvedCliPath,
461
580
  };
462
581
  }
@@ -468,8 +587,8 @@ export function serviceInstallationInfo(config) {
468
587
  let resolvedCliPath = cliPath;
469
588
  try { if (cliPath && fs.existsSync(cliPath)) resolvedCliPath = fs.realpathSync(cliPath); } catch {}
470
589
  return {
471
- cliVersion:packageVersionBesideCli(resolvedCliPath, PACKAGE_NAME),
472
- runtimeVersion:packageVersionBesideCli(resolvedCliPath, config?.runtime?.packageName),
590
+ cliVersion:packageVersionForCli(resolvedCliPath, PACKAGE_NAME),
591
+ runtimeVersion:packageVersionForCli(resolvedCliPath, config?.runtime?.packageName),
473
592
  cliPath:resolvedCliPath || cliPath,
474
593
  nodePath,
475
594
  };
@@ -480,7 +599,7 @@ export function currentInstallationInfo(config) {
480
599
  try { if (cliPath && fs.existsSync(cliPath)) cliPath = fs.realpathSync(cliPath); } catch {}
481
600
  return {
482
601
  cliVersion:VERSION,
483
- runtimeVersion:config?.runtime?.packageName ? packageVersionBesideCli(cliPath, config.runtime.packageName) : null,
602
+ runtimeVersion:config?.runtime?.packageName ? packageVersionForCli(cliPath, config.runtime.packageName) : null,
484
603
  cliPath:cliPath || null,
485
604
  nodePath:process.execPath,
486
605
  };
@@ -599,7 +718,7 @@ export function installPersistentAgent(config) {
599
718
  else if (platform === 'darwin') {
600
719
  installMacService(cliPath, { nodePath });
601
720
  ensureMacCliCommand(config, { cliPath:cliScript, nodePath });
602
- } else installWindowsService(cliPath);
721
+ } else installWindowsService(cliPath, { nodePath });
603
722
  configurePostInstallAccess();
604
723
  saveConfig({ ...config, serviceInstalled: true, serviceCliPath: cliScript, serviceNodePath: nodePath });
605
724
  console.log('ReMCP is installed as a background service. Future updates: remcp update');
@@ -663,7 +782,7 @@ export function ensureServiceIfRecorded(config) {
663
782
  const state = installMacService(cliPath, { restart: false, nodePath });
664
783
  ensureMacCliCommand(config, { cliPath, nodePath });
665
784
  if (state === 'reload-scheduled') restartScheduled = macServiceLabel;
666
- } else if (platform === 'win32') installWindowsService(cliPath);
785
+ } else if (platform === 'win32') installWindowsService(cliPath, { nodePath });
667
786
  // Upgrade the legacy inferred state only after the supervisor repair succeeded. A failed repair
668
787
  // must not turn a stale artifact into a permanent "managed service" declaration.
669
788
  const cliScript = resolvedCliScript(cliPath);
@@ -722,5 +841,6 @@ export function uninstallPersistentService() {
722
841
  } else if (platform === 'win32') {
723
842
  spawnSync('schtasks.exe', ['/End', '/TN', windowsTaskName], { stdio: 'ignore' });
724
843
  spawnSync('schtasks.exe', ['/Delete', '/TN', windowsTaskName, '/F'], { stdio: 'ignore' });
844
+ try { fs.unlinkSync(windowsServiceLauncherFile); } catch {}
725
845
  }
726
846
  }