@remcp/remcp 0.2.55 → 0.2.57

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.55",
3
+ "version": "0.2.57",
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
@@ -21,6 +21,10 @@ export { localRuntimeEntry, supervisorRestart, updateDecision } from './agent-up
21
21
 
22
22
  const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
23
23
  const UPDATE_CHECK_TIMEOUT_MS = 5000;
24
+ // A deployment can briefly leave one HA replica advertising the previous release. Recheck a few
25
+ // times after a stable connection instead of making that unlucky first answer stick for six hours.
26
+ const POST_CONNECT_UPDATE_RECHECK_DELAYS_MS = Object.freeze([60_000, 5 * 60_000, 15 * 60_000]);
27
+ const POST_CONNECT_UPDATE_RECHECK_JITTER_MS = 30_000;
24
28
  // A version that failed to install is retried after this cooldown instead of on every reconnect.
25
29
  const UPDATE_RETRY_COOLDOWN_MS = 30 * 60 * 1000;
26
30
  const METRICS_INTERVAL_MS = 60_000;
@@ -75,12 +79,22 @@ export async function runAgent(options) {
75
79
  const callTimeoutMs = Math.max(5_000, Number(options.rpcTimeoutMs || 120000) - CALL_TIMEOUT_MARGIN_MS);
76
80
  const telemetryEnabled = options.telemetryEnabled !== false;
77
81
  const persistState = typeof options.persistState === 'function' ? options.persistState : () => {};
82
+ const postConnectUpdateRecheckDelaysMs = (Array.isArray(options.updateRecheckDelaysMs)
83
+ ? options.updateRecheckDelaysMs
84
+ : POST_CONNECT_UPDATE_RECHECK_DELAYS_MS)
85
+ .map(Number)
86
+ .filter(delay => Number.isFinite(delay) && delay >= 0 && delay < UPDATE_CHECK_INTERVAL_MS)
87
+ .slice(0, 3);
88
+ const postConnectUpdateRecheckJitterMs = Number.isFinite(Number(options.updateRecheckJitterMs))
89
+ ? Math.max(0, Math.min(60_000, Number(options.updateRecheckJitterMs)))
90
+ : POST_CONNECT_UPDATE_RECHECK_JITTER_MS;
78
91
  let stopping = false;
79
92
  let revoked = false;
80
93
  const inFlight = new Map();
81
94
  let activeSocket;
82
95
  let reconnects = 0;
83
96
  let reconnectTimer = null;
97
+ let postConnectUpdateTimers = [];
84
98
  // Set when a replica asks this agent to move before it is replaced; the close handler reads it.
85
99
  let askedToReconnect = false;
86
100
  let pendingRequests = 0;
@@ -377,6 +391,7 @@ export async function runAgent(options) {
377
391
  reportInstallOnce();
378
392
  flushTelemetry();
379
393
  void checkForUpdate();
394
+ schedulePostConnectUpdateChecks();
380
395
  });
381
396
  ws.on('message', raw => {
382
397
  let message;
@@ -392,6 +407,7 @@ export async function runAgent(options) {
392
407
  }
393
408
  });
394
409
  ws.on('close', code => {
410
+ clearPostConnectUpdateChecks();
395
411
  for (const controller of inFlight.values()) controller.abort();
396
412
  if (stopping) return;
397
413
  // 1013 ('reconnect') is what a replica sends before it is replaced by a deployment. The machine
@@ -436,6 +452,28 @@ export async function runAgent(options) {
436
452
  let lastAttemptAt = 0;
437
453
  let lastUpdateCheckError = '';
438
454
  let lastUpdateCheckErrorAt = 0;
455
+
456
+ function clearPostConnectUpdateChecks() {
457
+ for (const timer of postConnectUpdateTimers) clearTimeout(timer);
458
+ postConnectUpdateTimers = [];
459
+ }
460
+
461
+ function schedulePostConnectUpdateChecks() {
462
+ clearPostConnectUpdateChecks();
463
+ if (options.autoUpdate === false) return;
464
+ for (const delay of postConnectUpdateRecheckDelaysMs) {
465
+ const jitter = postConnectUpdateRecheckJitterMs > 0
466
+ ? Math.floor(Math.random() * (postConnectUpdateRecheckJitterMs + 1))
467
+ : 0;
468
+ const timer = setTimeout(() => {
469
+ postConnectUpdateTimers = postConnectUpdateTimers.filter(candidate => candidate !== timer);
470
+ void checkForUpdate();
471
+ }, delay + jitter);
472
+ timer.unref?.();
473
+ postConnectUpdateTimers.push(timer);
474
+ }
475
+ }
476
+
439
477
  async function checkForUpdate() {
440
478
  if (options.autoUpdate === false) return;
441
479
  if (updateInFlight) return;
@@ -550,6 +588,7 @@ export async function runAgent(options) {
550
588
  runtimeToolsRetryTimer = null;
551
589
  clearTimeout(reconnectTimer);
552
590
  reconnectTimer = null;
591
+ clearPostConnectUpdateChecks();
553
592
  if (telemetryTimer) clearInterval(telemetryTimer);
554
593
  clearInterval(telemetryFlushTimer);
555
594
  clearInterval(updateTimer);
@@ -158,6 +158,100 @@ function shellQuote(value) {
158
158
  return `'${String(value).replaceAll("'", "'\\''")}'`;
159
159
  }
160
160
 
161
+ const remcpPathBlockStart = '# >>> ReMCP CLI PATH >>>';
162
+ const remcpPathBlockEnd = '# <<< ReMCP CLI PATH <<<';
163
+ const remcpCliShimMarker = '# ReMCP managed CLI shim';
164
+
165
+ function macCliShimPath() {
166
+ return path.join(home, '.local', 'bin', 'remcp');
167
+ }
168
+
169
+ function interactiveShellProfile() {
170
+ const shell = String(process.env.REMCP_TEST_SHELL || process.env.SHELL || os.userInfo().shell || '/bin/zsh');
171
+ const name = path.basename(shell);
172
+ if (name === 'zsh') return path.join(home, '.zshrc');
173
+ if (name === 'bash') return path.join(home, '.bash_profile');
174
+ return '';
175
+ }
176
+
177
+ function upsertManagedPathBlock(file) {
178
+ if (!file) return false;
179
+ const block = [
180
+ remcpPathBlockStart,
181
+ 'case ":$PATH:" in',
182
+ ' *":$HOME/.local/bin:"*) ;;',
183
+ ' *) export PATH="$HOME/.local/bin:$PATH" ;;',
184
+ 'esac',
185
+ remcpPathBlockEnd,
186
+ ].join('\n');
187
+ let current = '';
188
+ try { current = fs.readFileSync(file, 'utf8'); } catch {}
189
+ const start = current.indexOf(remcpPathBlockStart);
190
+ const end = start >= 0 ? current.indexOf(remcpPathBlockEnd, start + remcpPathBlockStart.length) : -1;
191
+ let next;
192
+ if (start >= 0 && end >= 0) {
193
+ next = current.slice(0, start) + block + current.slice(end + remcpPathBlockEnd.length);
194
+ } else {
195
+ const prefix = current && !current.endsWith('\n') ? current + '\n' : current;
196
+ next = prefix + (prefix ? '\n' : '') + block + '\n';
197
+ }
198
+ if (next === current) return false;
199
+ fs.writeFileSync(file, next, { mode:0o600 });
200
+ return true;
201
+ }
202
+
203
+ function removeManagedPathBlock(file) {
204
+ if (!file) return;
205
+ let current = '';
206
+ try { current = fs.readFileSync(file, 'utf8'); } catch { return; }
207
+ const start = current.indexOf(remcpPathBlockStart);
208
+ const end = start >= 0 ? current.indexOf(remcpPathBlockEnd, start + remcpPathBlockStart.length) : -1;
209
+ if (start < 0 || end < 0) return;
210
+ let before = current.slice(0, start);
211
+ let after = current.slice(end + remcpPathBlockEnd.length);
212
+ if (before.endsWith('\n') && after.startsWith('\n')) after = after.slice(1);
213
+ const next = before + after;
214
+ fs.writeFileSync(file, next, { mode:0o600 });
215
+ }
216
+
217
+ export function ensureMacCliCommand(config = {}, { cliPath = '', nodePath = '' } = {}) {
218
+ if (servicePlatform() !== 'darwin') return null;
219
+ const effectiveCli = cliPath || (persistentServiceExpected(config) ? canonicalServiceCliPath(config) : globalCliPath());
220
+ const effectiveNode = nodePath || (persistentServiceExpected(config) ? canonicalServiceNodePath(config) : process.execPath);
221
+ const cliScript = resolvedCliScript(effectiveCli);
222
+ const shim = macCliShimPath();
223
+ try {
224
+ const existing = fs.readFileSync(shim, 'utf8');
225
+ if (!existing.includes(remcpCliShimMarker)) {
226
+ console.error(`ReMCP did not replace ${shim} because it is not a ReMCP-managed command.`);
227
+ return null;
228
+ }
229
+ } catch (error) {
230
+ if (error?.code && error.code !== 'ENOENT') {
231
+ console.error(`ReMCP could not inspect ${shim}: ${error instanceof Error ? error.message : String(error)}`);
232
+ return null;
233
+ }
234
+ }
235
+ const launcher = `#!/bin/sh\n${remcpCliShimMarker}\nexec ${shellQuote(effectiveNode)} ${shellQuote(cliScript)} "$@"\n`;
236
+ fs.mkdirSync(path.dirname(shim), { recursive:true, mode:0o755 });
237
+ const temporary = `${shim}.${process.pid}.${Date.now()}.tmp`;
238
+ fs.writeFileSync(temporary, launcher, { mode:0o755 });
239
+ fs.chmodSync(temporary, 0o755);
240
+ fs.renameSync(temporary, shim);
241
+ upsertManagedPathBlock(interactiveShellProfile());
242
+ return shim;
243
+ }
244
+
245
+ function removeMacCliCommand() {
246
+ if (servicePlatform() !== 'darwin') return;
247
+ const shim = macCliShimPath();
248
+ try {
249
+ const existing = fs.readFileSync(shim, 'utf8');
250
+ if (existing.includes(remcpCliShimMarker)) fs.unlinkSync(shim);
251
+ } catch {}
252
+ removeManagedPathBlock(interactiveShellProfile());
253
+ }
254
+
161
255
  function macServicePlist(cliPath, nodePath = process.execPath) {
162
256
  // launchd wants an absolute path; a symlinked prefix that npm has not materialised yet (or a path
163
257
  // that is about to be replaced by the next install) must not abort the repair — a stale plist is
@@ -486,8 +580,10 @@ export function installPersistentAgent(config) {
486
580
  const cliScript = resolvedCliScript(cliPath);
487
581
  const nodePath = process.execPath;
488
582
  if (platform === 'linux') installLinuxService(cliPath, nodePath);
489
- else if (platform === 'darwin') installMacService(cliPath, { nodePath });
490
- else installWindowsService(cliPath);
583
+ else if (platform === 'darwin') {
584
+ installMacService(cliPath, { nodePath });
585
+ ensureMacCliCommand(config, { cliPath:cliScript, nodePath });
586
+ } else installWindowsService(cliPath);
491
587
  configurePostInstallAccess();
492
588
  saveConfig({ ...config, serviceInstalled: true, serviceCliPath: cliScript, serviceNodePath: nodePath });
493
589
  console.log('ReMCP is installed as a background service. Future updates: remcp update');
@@ -530,6 +626,7 @@ export function ensureServiceIfRecorded(config) {
530
626
  // installMacService either leaves an unchanged loaded job alone, bootstraps an unloaded job, or
531
627
  // hands a changed launcher to an independent transient launchd helper.
532
628
  const state = installMacService(cliPath, { restart: false, nodePath });
629
+ ensureMacCliCommand(config, { cliPath, nodePath });
533
630
  if (state === 'reload-scheduled') restartScheduled = macServiceLabel;
534
631
  } else if (platform === 'win32') installWindowsService(cliPath);
535
632
  // Upgrade the legacy inferred state only after the supervisor repair succeeded. A failed repair
@@ -586,6 +683,7 @@ export function uninstallPersistentService() {
586
683
  const domain = macLaunchDomain();
587
684
  spawnSync('launchctl', ['bootout', domain, macServiceFile], { stdio: 'ignore' });
588
685
  try { fs.unlinkSync(macServiceFile); } catch {}
686
+ removeMacCliCommand();
589
687
  } else if (platform === 'win32') {
590
688
  spawnSync('schtasks.exe', ['/End', '/TN', windowsTaskName], { stdio: 'ignore' });
591
689
  spawnSync('schtasks.exe', ['/Delete', '/TN', windowsTaskName, '/F'], { stdio: 'ignore' });
@@ -11,7 +11,7 @@ import { PACKAGE_NAME, VERSION } from '../version.mjs';
11
11
  import { loadConfig, saveConfig } from './config.mjs';
12
12
  import { installedVersion } from './doctor.mjs';
13
13
  import { linuxServiceFile, macServiceFile, officialOrigin } from './env.mjs';
14
- import { currentInstallationInfo, ensureServiceIfRecorded, installationVersionsAtCliPath, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
14
+ import { currentInstallationInfo, ensureMacCliCommand, ensureServiceIfRecorded, installationVersionsAtCliPath, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
15
15
 
16
16
  const UPDATE_DISCOVERY_TIMEOUT_MS = 5000;
17
17
 
@@ -232,6 +232,9 @@ export async function updateCommand(flags) {
232
232
  }
233
233
 
234
234
  if (alreadyCurrent) {
235
+ // `npx @remcp/remcp@latest update` is the recovery path when a Node-manager prefix was never
236
+ // exported into the user's shell. Even with no package delta, repair the stable macOS command.
237
+ ensureMacCliCommand(cfg);
235
238
  if (targets.persistRuntime) {
236
239
  saveConfig({ ...cfg, runtime:{ ...cfg.runtime, packageSpec:targets.runtimeSpec } });
237
240
  }
package/src/cli.mjs CHANGED
@@ -11,7 +11,7 @@ import { ensureMachineId, loadConfig, readJsonFile, saveConfig, setTelemetry, te
11
11
  import { assertRuntimeTrust, pairWithDeviceCode } from './cli/connect.mjs';
12
12
  import { diagnoseLocalRuntime, installedVersion, runtimeAllowedRoots } from './cli/doctor.mjs';
13
13
  import { configFile, npm, officialOrigin, runtimeConfigFile } from './cli/env.mjs';
14
- import { currentInstallationInfo, installPersistentAgent, persistentServiceState, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, uninstallPersistentService } from './cli/service.mjs';
14
+ import { currentInstallationInfo, ensureMacCliCommand, installPersistentAgent, persistentServiceState, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, uninstallPersistentService } from './cli/service.mjs';
15
15
  import { run } from './cli/shell.mjs';
16
16
  import { updateCommand } from './cli/update.mjs';
17
17
  function parse(argv) {
@@ -97,6 +97,10 @@ export async function main(argv = process.argv.slice(2)) {
97
97
  || Boolean(process.env.INVOCATION_ID)
98
98
  || String(process.env.XPC_SERVICE_NAME || '').includes('com.remcp.agent');
99
99
  const cfg = rememberCurrentInstallation(loadConfig(), { service:supervised });
100
+ // A service can start from an absolute fnm/nvm Node path even when an interactive macOS shell
101
+ // cannot resolve the npm-global `remcp` binary. Refresh the stable user shim on every start so
102
+ // an auto-update repairs the next Terminal session without requiring a manual PATH edit.
103
+ ensureMacCliCommand(cfg);
100
104
  const service = persistentServiceState(cfg);
101
105
  if (!supervised && service.active) {
102
106
  console.log(`ReMCP is already running as a background service (${service.name}). Use \`remcp status\` to inspect it or \`remcp doctor\` to diagnose it.`);