@remcp/remcp 0.2.4 → 0.2.6

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.4",
3
+ "version": "0.2.6",
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
@@ -2,7 +2,7 @@ import os from 'node:os';
2
2
  import { existsSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import process from 'node:process';
5
- import { spawnSync } from 'node:child_process';
5
+ import { spawn, spawnSync } from 'node:child_process';
6
6
  import WebSocket from 'ws';
7
7
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
8
8
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
@@ -10,6 +10,8 @@ import { normalizeRuntime } from './runtime.mjs';
10
10
  import { VERSION } from './version.mjs';
11
11
 
12
12
  const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
13
+ const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
14
+ const UPDATE_CHECK_TIMEOUT_MS = 5000;
13
15
  const METRICS_INTERVAL_MS = 60_000;
14
16
  const TELEMETRY_QUEUE_LIMIT = 500;
15
17
  const TELEMETRY_BATCH_LIMIT = 100;
@@ -35,6 +37,31 @@ function localRuntimeEntry(runtimeValue) {
35
37
  return candidate;
36
38
  }
37
39
 
40
+ function parseVersion(value) {
41
+ const match = String(value || '').match(/(\d+)\.(\d+)\.(\d+)/);
42
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
43
+ }
44
+
45
+ function isNewer(candidate, current) {
46
+ const a = parseVersion(candidate);
47
+ const b = parseVersion(current);
48
+ if (!a || !b) return false;
49
+ for (let index = 0; index < 3; index += 1) {
50
+ if (a[index] > b[index]) return true;
51
+ if (a[index] < b[index]) return false;
52
+ }
53
+ return false;
54
+ }
55
+
56
+ function globalCliEntry() {
57
+ const prefix = spawnSync(npmCommand, ['prefix', '--global'], { encoding: 'utf8' });
58
+ if (prefix.error || prefix.status !== 0) return null;
59
+ const base = String(prefix.stdout || '').trim();
60
+ return process.platform === 'win32'
61
+ ? path.join(base, 'remcp.cmd')
62
+ : path.join(base, 'bin', 'remcp');
63
+ }
64
+
38
65
  function jitter(ms) {
39
66
  return Math.round(ms * (0.75 + Math.random() * 0.5));
40
67
  }
@@ -220,6 +247,7 @@ export async function runAgent(options) {
220
247
  send({ type: 'metrics', metrics: deviceMetrics({ reconnects, pendingRequests, runtimeVersion, runtimeRestarts, runtimeDown }) });
221
248
  reportInstallOnce();
222
249
  flushTelemetry();
250
+ void checkForUpdate();
223
251
  });
224
252
  ws.on('message', raw => {
225
253
  let message;
@@ -241,6 +269,38 @@ export async function runAgent(options) {
241
269
  ws.on('error', error => console.error(`ReMCP relay: ${error.message}`));
242
270
  }
243
271
 
272
+ // Auto-update: the server publishes the versions an agent should be running. A newer
273
+ // release is installed in the background and the service restart picks it up; a failed
274
+ // or skipped update leaves the current version running, so an old agent keeps working.
275
+ async function checkForUpdate() {
276
+ if (options.autoUpdate === false) return;
277
+ try {
278
+ const response = await fetch(`${serverUrl}/api/agent/version`, { signal: AbortSignal.timeout(UPDATE_CHECK_TIMEOUT_MS) });
279
+ if (!response.ok) return;
280
+ const advertised = await response.json();
281
+ const minimum = advertised.minimum;
282
+ if (minimum && isNewer(minimum, VERSION)) {
283
+ console.error(`ReMCP ${VERSION} is older than the minimum supported agent ${minimum}; update with: remcp update`);
284
+ }
285
+ if (!isNewer(advertised.cli, VERSION)) return;
286
+ queueEvent({ event: 'agent_update', at: Date.now(), reason: String(advertised.cli).slice(0, 32), success: true });
287
+ const cli = globalCliEntry();
288
+ if (!cli || !existsSync(cli)) {
289
+ console.error(`ReMCP ${advertised.cli} is available; run: remcp update`);
290
+ return;
291
+ }
292
+ console.log(`Updating ReMCP to ${advertised.cli}${advertised.runtime ? ` with ${advertised.runtime}` : ''}…`);
293
+ const child = spawn(process.execPath, [cli, 'update', ...(advertised.runtime ? ['--runtime', advertised.runtime] : [])], {
294
+ detached: true,
295
+ stdio: 'ignore',
296
+ env: { ...process.env },
297
+ });
298
+ child.unref();
299
+ } catch {
300
+ // Offline, DNS failure, older server without the endpoint: keep running as-is.
301
+ }
302
+ }
303
+
244
304
  telemetryTimer = setInterval(() => {
245
305
  if (activeSocket?.readyState === 1) {
246
306
  send({ type: 'metrics', metrics: deviceMetrics({ reconnects, pendingRequests, runtimeVersion, runtimeRestarts, runtimeDown, queueDepth: telemetryQueue.length }) });
@@ -250,12 +310,15 @@ export async function runAgent(options) {
250
310
  telemetryTimer.unref?.();
251
311
  const telemetryFlushTimer = setInterval(flushTelemetry, TELEMETRY_SEND_INTERVAL_MS);
252
312
  telemetryFlushTimer.unref?.();
313
+ const updateTimer = setInterval(() => void checkForUpdate(), UPDATE_CHECK_INTERVAL_MS);
314
+ updateTimer.unref?.();
253
315
 
254
316
  async function stop() {
255
317
  if (stopping) return;
256
318
  stopping = true;
257
319
  if (telemetryTimer) clearInterval(telemetryTimer);
258
320
  clearInterval(telemetryFlushTimer);
321
+ clearInterval(updateTimer);
259
322
  try { activeSocket?.close(); } catch {}
260
323
  try { await mcp?.close(); } catch {}
261
324
  try { await transport?.close(); } catch {}
package/src/cli.mjs CHANGED
@@ -272,6 +272,7 @@ export async function main(argv = process.argv.slice(2)) {
272
272
  const telemetry = telemetryState();
273
273
  await runAgent({
274
274
  ...cfg,
275
+ autoUpdate: cfg.autoUpdate !== false,
275
276
  telemetryEnabled: telemetry.enabled,
276
277
  installReported: telemetry.installReported,
277
278
  installSpec: `${PACKAGE_NAME}@${VERSION}`,
@@ -280,6 +281,18 @@ export async function main(argv = process.argv.slice(2)) {
280
281
  return;
281
282
  }
282
283
 
284
+ if (command === 'auto-update') {
285
+ const action = String(positional[0] || 'status').toLowerCase();
286
+ const cfg = loadConfig(false) || {};
287
+ if (action === 'status') {
288
+ console.log(JSON.stringify({ autoUpdate: cfg.autoUpdate !== false, checkIntervalHours: 6 }, null, 2));
289
+ return;
290
+ }
291
+ if (action === 'on' || action === 'enable') { saveConfig({ ...cfg, autoUpdate: true }); console.log('Auto-update enabled.'); return; }
292
+ if (action === 'off' || action === 'disable') { saveConfig({ ...cfg, autoUpdate: false }); console.log('Auto-update disabled. Run `remcp update` yourself when you want a new version.'); return; }
293
+ throw new Error('Usage: remcp auto-update [status|on|off]');
294
+ }
295
+
283
296
  if (command === 'telemetry') {
284
297
  const action = String(positional[0] || 'status').toLowerCase();
285
298
  if (action === 'status') {
@@ -315,8 +328,20 @@ export async function main(argv = process.argv.slice(2)) {
315
328
 
316
329
  if (command === 'update') {
317
330
  const cfg = loadConfig();
318
- console.log('Updating ReMCP to the latest published version…');
319
- npmGlobalInstall(`${PACKAGE_NAME}@latest`, cfg.runtime.packageSpec);
331
+ // The server advertises which runtime version it expects; an agent that is updating
332
+ // itself passes it through so client and runtime move together.
333
+ const requested = typeof flags.runtime === 'string' ? flags.runtime.trim() : '';
334
+ if (requested && (!/^@?[a-z0-9._-]+(\/[a-z0-9._-]+)?@\S+$/i.test(requested) || requested.includes(' '))) {
335
+ throw new Error('--runtime must look like @scope/package@1.2.3');
336
+ }
337
+ const runtimeSpec = requested || cfg.runtime.packageSpec;
338
+ if (flags.check) {
339
+ console.log(JSON.stringify({ current: VERSION, runtime: cfg.runtime.packageSpec, available: `${PACKAGE_NAME}@latest` }, null, 2));
340
+ return;
341
+ }
342
+ console.log(`Updating ReMCP to the latest published version (${runtimeSpec})…`);
343
+ npmGlobalInstall(`${PACKAGE_NAME}@latest`, runtimeSpec);
344
+ if (requested) saveConfig({ ...cfg, runtime: { ...cfg.runtime, packageSpec: requested } });
320
345
  restartPersistentServiceIfInstalled();
321
346
  console.log('ReMCP updated. Run `remcp --version` or `remcp status` to verify.');
322
347
  return;