@remcp/remcp 0.2.26 → 0.2.28

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.26",
3
+ "version": "0.2.28",
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
@@ -531,10 +531,21 @@ export async function runAgent(options) {
531
531
  child.stderr?.on('data', forward);
532
532
  child.on('exit', code => {
533
533
  updateInFlight = false;
534
- if (code !== 0) {
534
+ // The updater installs the packages and then restarts the service this agent runs in, so it is
535
+ // routinely killed by that restart and exits with a signal (`code` is null). That is success,
536
+ // not failure: the installed version is the proof. Without this check the agent kept
537
+ // reinstalling every few seconds — each cycle taking the device offline and making every tool
538
+ // call in that window fail in 1-3 ms ("Device is offline").
539
+ const targetVersion = String(target).split('@').pop();
540
+ const installedNow = globalInstalledVersion();
541
+ const installedTarget = installedNow && targetVersion && !isNewer(targetVersion, installedNow);
542
+ if (code !== 0 && !installedTarget) {
535
543
  console.error(`remcp update exited with ${code}; keeping ${VERSION} and retrying after the cooldown.`);
536
544
  return;
537
545
  }
546
+ if (code !== 0) {
547
+ console.error(`remcp update was terminated (${code}) after installing ${installedNow}; applying it.`);
548
+ }
538
549
  void restartToApplyUpdate(cli, stop, () => { stopping = true; }, async () => {
539
550
  // The packages are installed now: clear the failure, reset the backoff and start again.
540
551
  runtimeRestartDelay = RUNTIME_RESTART_BASE_MS;
package/src/cli.mjs CHANGED
@@ -170,7 +170,10 @@ function macLaunchDomain() {
170
170
  function installMacService(cliPath = globalCliPath()) {
171
171
  const domain = macLaunchDomain();
172
172
  const target = `${domain}/${macServiceLabel}`;
173
- const cliScript = fs.realpathSync(cliPath);
173
+ // launchd wants an absolute path; a symlinked prefix that npm has not materialised yet (or a path
174
+ // that is about to be replaced by the next install) must not abort the repair — a stale plist is
175
+ // exactly the loop this function exists to break.
176
+ const cliScript = fs.existsSync(cliPath) ? fs.realpathSync(cliPath) : path.resolve(cliPath);
174
177
  fs.mkdirSync(path.dirname(macServiceFile), { recursive: true });
175
178
  fs.mkdirSync(path.dirname(macLogFile), { recursive: true });
176
179
  const plist = `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>\n<key>Label</key><string>${macServiceLabel}</string>\n<key>ProgramArguments</key><array><string>${xmlEscape(process.execPath)}</string><string>${xmlEscape(cliScript)}</string><string>start</string></array>\n<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>\n<key>ProcessType</key><string>Background</string>\n<key>StandardOutPath</key><string>${xmlEscape(macLogFile)}</string>\n<key>StandardErrorPath</key><string>${xmlEscape(macLogFile)}</string>\n</dict></plist>\n`;
@@ -255,9 +258,35 @@ function ensureServiceIfRecorded(config) {
255
258
  try {
256
259
  const cliPath = globalCliPath();
257
260
  const platform = servicePlatform();
258
- if (platform === 'linux') { if (!fs.existsSync(linuxServiceFile)) installLinuxService(cliPath); }
259
- else if (platform === 'darwin') { if (!fs.existsSync(macServiceFile)) installMacService(cliPath); }
260
- else if (platform === 'win32') installWindowsService(cliPath);
261
+ if (platform === 'linux') {
262
+ if (!fs.existsSync(linuxServiceFile)) {
263
+ installLinuxService(cliPath);
264
+ } else {
265
+ // Node managers can move the global npm prefix between updates (nvm -> Hermes was observed in
266
+ // production). An existing systemd unit then keeps launching the old CLI forever even though
267
+ // npm successfully installed the new one. Repair the launcher in place before restarting it.
268
+ const unit = fs.readFileSync(linuxServiceFile, 'utf8');
269
+ const expected = `ExecStart=${quoteSystemd(cliPath)} start`;
270
+ if (!unit.includes(expected)) {
271
+ const repaired = /^ExecStart=/m.test(unit) ? unit.replace(/^ExecStart=.*$/m, expected) : '';
272
+ if (!repaired) {
273
+ // A unit that lost its ExecStart line (edited by hand, or written as `ExecStart = …`) cannot
274
+ // be patched by substitution. Rewriting the whole unit is what keeps the machine out of the
275
+ // "old CLI forever" loop, so fall back to a fresh install instead of giving up.
276
+ installLinuxService(cliPath);
277
+ } else {
278
+ fs.writeFileSync(linuxServiceFile, repaired);
279
+ run('systemctl', ['--user', 'daemon-reload']);
280
+ }
281
+ }
282
+ }
283
+ } else if (platform === 'darwin') {
284
+ // launchd bakes the interpreter and the CLI path into the plist, so a Node manager that moves
285
+ // its global prefix leaves the agent launching a file that no longer exists — the same loop the
286
+ // Linux unit above is repaired for. Reinstalling is idempotent (bootout, bootstrap, enable,
287
+ // kickstart) and is what the Windows task already does on every update.
288
+ installMacService(cliPath);
289
+ } else if (platform === 'win32') installWindowsService(cliPath);
261
290
  return true;
262
291
  } catch (error) {
263
292
  console.error(`Could not ensure the background service: ${error instanceof Error ? error.message : String(error)}`);
@@ -646,9 +675,11 @@ export async function main(argv = process.argv.slice(2)) {
646
675
  return;
647
676
  }
648
677
  const before = { cli: VERSION, runtime: installedVersion(cfg.runtime.packageName) };
649
- ensureServiceIfRecorded(cfg);
650
678
  console.log(`Updating ReMCP to the latest published version (${runtimeSpec})…`);
651
679
  npmGlobalInstall(`${PACKAGE_NAME}@latest`, runtimeSpec);
680
+ // The npm prefix can change across Node-manager upgrades. Repair an existing persistent-service
681
+ // launcher only after the install, when globalCliPath() points at the CLI we just installed.
682
+ ensureServiceIfRecorded(cfg);
652
683
  // Only a validated spec is persisted, so a failed update cannot leave the install unable to start.
653
684
  if (requested && runtimeSpec !== cfg.runtime.packageSpec) saveConfig({ ...cfg, runtime: { ...cfg.runtime, packageSpec: runtimeSpec } });
654
685
  const after = { cli: installedVersion(PACKAGE_NAME), runtime: installedVersion(cfg.runtime.packageName) };