@remcp/remcp 0.2.46 → 0.2.47

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.46",
3
+ "version": "0.2.47",
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/agent.mjs CHANGED
@@ -286,6 +286,15 @@ export async function runAgent(options) {
286
286
  // Service access is a user-controlled pause, not an outage. Poll slowly enough not to hammer
287
287
  // the relay, but cap recovery so an enable action becomes effective within seconds.
288
288
  scheduleReconnect(jitter(RECONNECT_BASE_MS));
289
+ return;
290
+ }
291
+ const status = Number(response.statusCode || 0);
292
+ const transientHandshakeFailure = status === 408 || status === 425 || status === 429 || (status >= 500 && status <= 599);
293
+ if (transientHandshakeFailure && !revoked) {
294
+ // A reverse proxy or relay deploy can refuse the HTTP upgrade before a WebSocket exists.
295
+ // With an unexpected-response listener the client does not reliably emit close afterwards,
296
+ // so this branch must schedule its own retry or a still-running agent can stay offline forever.
297
+ scheduleBackoffReconnect();
289
298
  }
290
299
  });
291
300
  activeSocket = ws;
package/src/cli/env.mjs CHANGED
@@ -11,6 +11,7 @@ export const configFile = path.join(configDir, 'config.json');
11
11
  export const runtimeConfigFile = path.join(configDir, 'runtime.json');
12
12
  export const machineIdFile = path.join(configDir, 'machine-id');
13
13
  export const linuxServiceFile = path.join(home, '.config', 'systemd', 'user', 'remcp-agent.service');
14
+ export const linuxServiceLauncherFile = path.join(configDir, 'remcp-agent-launcher');
14
15
  export const macServiceLabel = 'com.remcp.agent';
15
16
  export const macServiceFile = path.join(home, 'Library', 'LaunchAgents', `${macServiceLabel}.plist`);
16
17
  export const macLogFile = path.join(home, 'Library', 'Logs', 'remcp-agent.log');
@@ -8,7 +8,7 @@ import { spawnSync } from 'node:child_process';
8
8
  import { PACKAGE_NAME, VERSION } from '../version.mjs';
9
9
 
10
10
  import { saveConfig } from './config.mjs';
11
- import { configDir, home, linuxServiceFile, macLogFile, macServiceFile, macServiceLabel, npm, windowsTaskName } from './env.mjs';
11
+ import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macLogFile, macServiceFile, macServiceLabel, npm, windowsTaskName } from './env.mjs';
12
12
  import { output, run } from './shell.mjs';
13
13
 
14
14
  export function servicePlatform() {
@@ -32,12 +32,28 @@ export function quoteSystemd(value) {
32
32
  return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
33
33
  }
34
34
 
35
+ function resolvedCliScript(cliPath) {
36
+ return fs.existsSync(cliPath) ? fs.realpathSync(cliPath) : path.resolve(cliPath);
37
+ }
38
+
39
+ function writeLinuxServiceLauncher(cliPath = globalCliPath()) {
40
+ const cliScript = resolvedCliScript(cliPath);
41
+ const launcher = `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(cliScript)} \"$@\"\n`;
42
+ fs.mkdirSync(path.dirname(linuxServiceLauncherFile), { recursive: true, mode: 0o700 });
43
+ const temporary = `${linuxServiceLauncherFile}.${process.pid}.${Date.now()}.tmp`;
44
+ fs.writeFileSync(temporary, launcher, { mode: 0o700 });
45
+ fs.chmodSync(temporary, 0o700);
46
+ fs.renameSync(temporary, linuxServiceLauncherFile);
47
+ return linuxServiceLauncherFile;
48
+ }
49
+
35
50
  export function xmlEscape(value) {
36
51
  return String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&apos;');
37
52
  }
38
53
 
39
54
  export function installLinuxService(cliPath = globalCliPath()) {
40
- const unit = `[Unit]\nDescription=ReMCP device agent\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${quoteSystemd(cliPath)} start\nRestart=always\nRestartSec=3\nNoNewPrivileges=true\n\n[Install]\nWantedBy=default.target\n`;
55
+ const launcherFile = writeLinuxServiceLauncher(cliPath);
56
+ const unit = `[Unit]\nDescription=ReMCP device agent\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${quoteSystemd(launcherFile)} start\nRestart=always\nRestartSec=3\nNoNewPrivileges=true\n\n[Install]\nWantedBy=default.target\n`;
41
57
  fs.mkdirSync(path.dirname(linuxServiceFile), { recursive: true });
42
58
  fs.writeFileSync(linuxServiceFile, unit);
43
59
  run('systemctl', ['--user', 'daemon-reload']);
@@ -229,17 +245,16 @@ export function ensureServiceIfRecorded(config) {
229
245
  if (!fs.existsSync(linuxServiceFile)) {
230
246
  installLinuxService(cliPath);
231
247
  } else {
232
- // Node managers can move the global npm prefix between updates (nvm -> Hermes was observed in
233
- // production). An existing systemd unit then keeps launching the old CLI forever even though
234
- // npm successfully installed the new one. Repair the launcher in place before restarting it.
248
+ // Keep the systemd unit independent of nvm/Hermes/Homebrew prefixes. Only this small launcher
249
+ // changes when npm or Node moves, so every install converges on one supervisor entrypoint.
250
+ const launcherFile = writeLinuxServiceLauncher(cliPath);
235
251
  const unit = fs.readFileSync(linuxServiceFile, 'utf8');
236
- const expected = `ExecStart=${quoteSystemd(cliPath)} start`;
252
+ const expected = `ExecStart=${quoteSystemd(launcherFile)} start`;
237
253
  if (!unit.includes(expected)) {
238
254
  const repaired = /^ExecStart=/m.test(unit) ? unit.replace(/^ExecStart=.*$/m, expected) : '';
239
255
  if (!repaired) {
240
- // A unit that lost its ExecStart line (edited by hand, or written as `ExecStart = …`) cannot
241
- // be patched by substitution. Rewriting the whole unit is what keeps the machine out of the
242
- // "old CLI forever" loop, so fall back to a fresh install instead of giving up.
256
+ // A hand-edited unit with no launcher line is replaced wholesale; the stable launcher is
257
+ // still refreshed first so the replacement never points back at a retired Node manager.
243
258
  installLinuxService(cliPath);
244
259
  } else {
245
260
  fs.writeFileSync(linuxServiceFile, repaired);