@remcp/remcp 0.2.215 → 0.2.217

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.215",
3
+ "version": "0.2.217",
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-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",
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-update-lock.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": {
@@ -5,7 +5,9 @@ import { VERSION } from './version.mjs';
5
5
  import { fetchUpdateManifest } from './fetch-update-manifest.mjs';
6
6
  import { globalCliEntry, globalInstalledVersion, isNewer, restartToApplyUpdate, updateDecision, updateInvocationArgs } from './agent-update.mjs';
7
7
 
8
- export const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
8
+ // Poll releases while connected too: relying on reconnect alone can leave a
9
+ // healthy long-lived agent stale for six hours after deployment.
10
+ export const UPDATE_CHECK_INTERVAL_MS = 30 * 60 * 1000;
9
11
  const UPDATE_CHECK_TIMEOUT_MS = 12000;
10
12
  // A flapping relay must not trigger update discovery on every successful reconnect.
11
13
  const UPDATE_CHECK_RECONNECT_GAP_MS = 30000;
@@ -6,6 +6,7 @@ import { PACKAGE_NAME } from '../version.mjs';
6
6
  import { home, npm } from './env.mjs';
7
7
  import { buildVettedMacTemperatureAddon } from './mac-native-temperature.mjs';
8
8
  import { output, run } from './shell.mjs';
9
+ import { withWindowsUpdateLock } from './windows-update-lock.mjs';
9
10
 
10
11
  export function servicePlatform() {
11
12
  return process.env.NODE_ENV === 'test' && process.env.REMCP_TEST_PLATFORM ? process.env.REMCP_TEST_PLATFORM : process.platform;
@@ -53,8 +54,10 @@ function npmGlobalInstallArgs(specs, { preferOnline = false } = {}) {
53
54
  }
54
55
 
55
56
  export function npmGlobalInstall(...specs) {
56
- assertSafeReMcpNpmPrefix(globalPrefix()); // Reject an unsafe prefix before npm writes anything.
57
- run(npm.command, [...npm.args, ...npmGlobalInstallArgs(specs)]);
57
+ const prefix = assertSafeReMcpNpmPrefix(globalPrefix()); // Reject an unsafe prefix before npm writes anything.
58
+ const install = () => run(npm.command, [...npm.args, ...npmGlobalInstallArgs(specs)]);
59
+ if (servicePlatform() === 'win32') withWindowsUpdateLock(prefix, install);
60
+ else install();
58
61
  if (servicePlatform() === 'darwin' && specs.some(spec => /^@remcp\/runtime@[0-9]/.test(spec))) {
59
62
  try {
60
63
  buildVettedMacTemperatureAddon({
@@ -241,7 +244,7 @@ export function npmGlobalUpdate(packageNames, ...specs) {
241
244
  return;
242
245
  }
243
246
  if (platform === 'win32') {
244
- transactionalWindowsGlobalInstall({ resolved:npm, prefix, packageNames, specs, preferOnline:true });
247
+ withWindowsUpdateLock(prefix, () => transactionalWindowsGlobalInstall({ resolved:npm, prefix, packageNames, specs, preferOnline:true }));
245
248
  return;
246
249
  }
247
250
  run(npm.command, [...npm.args, ...npmGlobalInstallArgs(specs, { preferOnline:true })]);
@@ -256,7 +259,7 @@ export function npmGlobalUpdateForNode(nodePath, packageNames, ...specs) {
256
259
  return;
257
260
  }
258
261
  if (platform === 'win32') {
259
- transactionalWindowsGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline:true });
262
+ withWindowsUpdateLock(prefix, () => transactionalWindowsGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline:true }));
260
263
  return;
261
264
  }
262
265
  run(resolved.command, [...resolved.args, ...npmGlobalInstallArgs(specs, { preferOnline:true })]);
@@ -12,7 +12,7 @@ import { loadConfig, saveConfig } from './config.mjs';
12
12
  import { assertSecureServerUrl } from './connect.mjs';
13
13
  import { installedVersion } from './doctor.mjs';
14
14
  import { linuxServiceFile, macServiceFile, officialOrigin } from './env.mjs';
15
- import { currentInstallationInfo, ensureMacCliCommand, ensureServiceIfRecorded, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
15
+ import { currentInstallationInfo, ensureMacCliCommand, ensureServiceIfRecorded, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, servicePlatform, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
16
16
 
17
17
  const UPDATE_DISCOVERY_TIMEOUT_MS = 5000;
18
18
 
@@ -201,6 +201,14 @@ export function verifyManagedServiceReleasePair(serviceInstall, clientSpec, runt
201
201
  }
202
202
  }
203
203
 
204
+ // A pinned automatic update can be a no-op on disk while an older Windows agent
205
+ // still owns the Scheduled Task. Keep manual unpinned no-ops side-effect free.
206
+ export function shouldApplyAlreadyInstalledWindowsRelease({ platform, serviceExpected, flags }) {
207
+ return platform === 'win32' && serviceExpected === true &&
208
+ typeof flags?.client === 'string' && Boolean(flags.client.trim()) &&
209
+ typeof flags?.runtime === 'string' && Boolean(flags.runtime.trim());
210
+ }
211
+
204
212
  export async function updateCommand(flags) {
205
213
  let cfg = rememberCurrentInstallation(loadConfig());
206
214
  const targets = await resolveUpdateTargets({ cfg, flags });
@@ -246,6 +254,13 @@ export async function updateCommand(flags) {
246
254
  }
247
255
  const currentRuntimeVersion = String(currentInstall.runtimeVersion || specVersion(cfg.runtime.packageName, targets.runtimeSpec) || '?');
248
256
  console.log(`ReMCP is already up to date (client ${currentInstall.cliVersion || VERSION}, runtime ${currentRuntimeVersion}).`);
257
+ if (shouldApplyAlreadyInstalledWindowsRelease({ platform: servicePlatform(), serviceExpected, flags })) {
258
+ // This CLI may be newer than its still-running parent agent. The canonical
259
+ // service files are correct, but the existing Windows task must hand over.
260
+ const restarted = restartPersistentServiceIfInstalled(cfg, { deferWindows: true });
261
+ if (!restarted) throw new Error('ReMCP Windows task could not schedule a restart for the installed release');
262
+ console.log(`ReMCP installed release will be applied: ${restarted}.`);
263
+ }
249
264
  return;
250
265
  }
251
266
 
@@ -284,7 +299,12 @@ export async function updateCommand(flags) {
284
299
  // A changed macOS plist is reloaded by an independent launchd helper. Do not schedule a second
285
300
  // helper against the same job: two one-second handoffs can race bootout/bootstrap with kickstart
286
301
  // and leave the device offline even though npm installed the new release successfully.
287
- const restarted = restartAlreadyScheduled || restartPersistentServiceIfInstalled(cfg);
302
+ // On Windows the updater may be a descendant of the task being restarted.
303
+ // An inline /End kills the updater before its /Run can execute, leaving an
304
+ // older process connected after the new release was installed on disk.
305
+ // Use the already-tested independent handoff helper for every update.
306
+ const restarted = restartAlreadyScheduled ||
307
+ restartPersistentServiceIfInstalled(cfg, { deferWindows: true });
288
308
  if (restarted) {
289
309
  console.log(`ReMCP updated and ${restarted} restarted (client ${before.cli} → ${after.cli ?? '?'}, runtime ${before.runtime ?? '?'} → ${after.runtime ?? '?'}).`);
290
310
  return;
@@ -0,0 +1,77 @@
1
+ // Serializes writes to one Windows npm global prefix across manual and agent updates.
2
+ // The lock exists outside the npm-managed package/shim trees and is held through rollback.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import crypto from 'node:crypto';
6
+ import process from 'node:process';
7
+
8
+ const LOCK_NAME = '.remcp-update-lock';
9
+ const OWNER_NAME = 'owner.json';
10
+
11
+ function lockOwnerFile(directory) {
12
+ return path.join(directory, OWNER_NAME);
13
+ }
14
+
15
+ function ownerIsLive(directory) {
16
+ try {
17
+ const owner = JSON.parse(fs.readFileSync(lockOwnerFile(directory), 'utf8'));
18
+ if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) return true;
19
+ process.kill(owner.pid, 0);
20
+ return true;
21
+ } catch (error) {
22
+ // On Windows and Unix, only ESRCH is reliable evidence the owner has exited.
23
+ // Permission errors and damaged/incomplete owner metadata must fail closed.
24
+ return error?.code !== 'ESRCH';
25
+ }
26
+ }
27
+
28
+ function removeOwnedLock(directory, token) {
29
+ let owner;
30
+ try {
31
+ if (!fs.lstatSync(directory).isDirectory()) return;
32
+ owner = JSON.parse(fs.readFileSync(lockOwnerFile(directory), 'utf8'));
33
+ } catch { return; }
34
+ if (owner.token !== token || owner.pid !== process.pid) return;
35
+ try {
36
+ fs.unlinkSync(lockOwnerFile(directory));
37
+ fs.rmdirSync(directory);
38
+ } catch (error) {
39
+ // Never force-remove unexpected files created by another process.
40
+ if (error?.code !== 'ENOENT') console.error('ReMCP update lock cleanup failed:', error?.message);
41
+ }
42
+ }
43
+
44
+ export function withWindowsUpdateLock(prefix, operation) {
45
+ if (!path.isAbsolute(prefix)) throw new Error('Windows update lock requires an absolute npm prefix.');
46
+ if (typeof operation !== 'function') throw new TypeError('Windows update operation is required.');
47
+ fs.mkdirSync(prefix, { recursive:true });
48
+ const directory = path.join(prefix, LOCK_NAME);
49
+ try {
50
+ fs.mkdirSync(directory, { mode:0o700 });
51
+ } catch (error) {
52
+ if (error?.code !== 'EEXIST') throw error;
53
+ const stale = !ownerIsLive(directory);
54
+ throw new Error(stale
55
+ ? 'ReMCP previous Windows update exited without releasing its lock; repair the stale update lock before retrying.'
56
+ : 'ReMCP update already in progress for this Windows npm prefix; retry after it finishes.');
57
+ }
58
+
59
+ const token = crypto.randomUUID();
60
+ try {
61
+ fs.writeFileSync(lockOwnerFile(directory), JSON.stringify({
62
+ pid:process.pid, token, startedAt:Date.now(),
63
+ }), { flag:'wx', mode:0o600 });
64
+ } catch (error) {
65
+ try { fs.rmdirSync(directory); } catch {}
66
+ throw error;
67
+ }
68
+
69
+ const onExit = () => removeOwnedLock(directory, token);
70
+ process.once('exit', onExit);
71
+ try {
72
+ return operation();
73
+ } finally {
74
+ process.removeListener('exit', onExit);
75
+ removeOwnedLock(directory, token);
76
+ }
77
+ }