@remcp/remcp 0.2.202 → 0.2.203
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 +1 -1
- package/src/cli/config.mjs +10 -0
- package/src/cli/npm-global.mjs +22 -2
- package/src/cli/service-inventory.mjs +5 -4
- package/src/cli/service.mjs +7 -1
- package/src/cli/update.mjs +25 -14
- package/src/cli.mjs +4 -4
package/package.json
CHANGED
package/src/cli/config.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// the server uses to recognise this computer across pairings.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
5
6
|
import { randomUUID } from 'node:crypto';
|
|
6
7
|
import { normalizeRuntime } from '../runtime.mjs';
|
|
7
8
|
|
|
@@ -78,6 +79,15 @@ export function writeJsonFile(file, value) {
|
|
|
78
79
|
fs.chmodSync(file, 0o600);
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
export function autoUpdateEnabled(config, platform = process.platform) {
|
|
83
|
+
// Linux often runs unrelated applications through the same global npm prefix. Never
|
|
84
|
+
// mutate that prefix implicitly: require `remcp auto-update on` for Linux agents.
|
|
85
|
+
// Keep the historical preference for existing explicit opt-ins and other platforms.
|
|
86
|
+
if (config?.autoUpdate === true) return true;
|
|
87
|
+
if (config?.autoUpdate === false) return false;
|
|
88
|
+
return platform !== 'linux';
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
export function flagEnabled(value) {
|
|
82
92
|
return value === undefined || value === null ? true : value !== false;
|
|
83
93
|
}
|
package/src/cli/npm-global.mjs
CHANGED
|
@@ -11,7 +11,26 @@ export function servicePlatform() {
|
|
|
11
11
|
return process.env.NODE_ENV === 'test' && process.env.REMCP_TEST_PLATFORM ? process.env.REMCP_TEST_PLATFORM : process.platform;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export function assertSafeReMcpNpmPrefix(value) {
|
|
15
|
+
const prefix = String(value || '').trim();
|
|
16
|
+
if (!path.isAbsolute(prefix)) throw new Error('ReMCP update refused: npm global prefix must be an absolute directory.');
|
|
17
|
+
const normalized = path.resolve(prefix);
|
|
18
|
+
const parts = normalized.split(path.sep).filter(Boolean);
|
|
19
|
+
// An inherited NPM_CONFIG_PREFIX aimed inside an already-installed package creates a
|
|
20
|
+
// second copy in its lib/node_modules. npm then reports success but systemd still starts
|
|
21
|
+
// the old agent, causing endless replacement attempts and offline device flapping.
|
|
22
|
+
for (let index = 0; index < parts.length - 2; index += 1) {
|
|
23
|
+
if (parts[index] === 'node_modules' && parts[index + 1] === '@remcp'
|
|
24
|
+
&& (parts[index + 2] === 'remcp' || parts[index + 2] === 'runtime')) {
|
|
25
|
+
throw new Error('ReMCP update refused: npm prefix points inside a ReMCP package. Repair the service launcher before retrying.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
export function globalPrefix() {
|
|
32
|
+
// Read-only diagnostics must work even when a legacy launcher exports a broken prefix.
|
|
33
|
+
// Validate only at installation/update write boundaries, not while discovering paths.
|
|
15
34
|
return output(npm.command, [...npm.args, 'prefix', '--global']);
|
|
16
35
|
}
|
|
17
36
|
|
|
@@ -34,6 +53,7 @@ function npmGlobalInstallArgs(specs, { preferOnline = false } = {}) {
|
|
|
34
53
|
}
|
|
35
54
|
|
|
36
55
|
export function npmGlobalInstall(...specs) {
|
|
56
|
+
assertSafeReMcpNpmPrefix(globalPrefix()); // Reject an unsafe prefix before npm writes anything.
|
|
37
57
|
run(npm.command, [...npm.args, ...npmGlobalInstallArgs(specs)]);
|
|
38
58
|
if (servicePlatform() === 'darwin' && specs.some(spec => /^@remcp\/runtime@[0-9]/.test(spec))) {
|
|
39
59
|
try {
|
|
@@ -198,7 +218,7 @@ function transactionalWindowsGlobalInstall({ resolved, prefix, packageNames, spe
|
|
|
198
218
|
// version, so updates revalidate metadata while retaining npm's cache for package bytes.
|
|
199
219
|
export function npmGlobalUpdate(packageNames, ...specs) {
|
|
200
220
|
const platform = servicePlatform();
|
|
201
|
-
const prefix = globalPrefix();
|
|
221
|
+
const prefix = assertSafeReMcpNpmPrefix(globalPrefix());
|
|
202
222
|
if (platform === 'darwin') {
|
|
203
223
|
transactionalMacGlobalInstall({ resolved:npm, prefix, packageNames, specs, preferOnline:true });
|
|
204
224
|
return;
|
|
@@ -213,7 +233,7 @@ export function npmGlobalUpdate(packageNames, ...specs) {
|
|
|
213
233
|
export function npmGlobalUpdateForNode(nodePath, packageNames, ...specs) {
|
|
214
234
|
const platform = servicePlatform();
|
|
215
235
|
const resolved = resolveNpm({ nodePath, home, platform });
|
|
216
|
-
const prefix = output(resolved.command, [...resolved.args, 'prefix', '--global']);
|
|
236
|
+
const prefix = assertSafeReMcpNpmPrefix(output(resolved.command, [...resolved.args, 'prefix', '--global']));
|
|
217
237
|
if (platform === 'darwin') {
|
|
218
238
|
transactionalMacGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline:true });
|
|
219
239
|
return;
|
|
@@ -189,10 +189,11 @@ export function rememberCurrentInstallation(config, { service = false } = {}) {
|
|
|
189
189
|
export function syncKnownInstallations(config, ...specs) {
|
|
190
190
|
const serviceInfo = serviceInstallationInfo(config);
|
|
191
191
|
const current = currentInstallationInfo(config);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
// Only the active service installation is mandatory. Other recorded Node manager prefixes
|
|
193
|
+
// can host unrelated user scripts and must not be mutated by an automatic agent update.
|
|
194
|
+
const candidates = serviceInfo.nodePath
|
|
195
|
+
? [{ nodePath:serviceInfo.nodePath, cliPath:serviceInfo.cliPath, service:true }]
|
|
196
|
+
: [];
|
|
196
197
|
const currentNode = (() => { try { return fs.realpathSync(current.nodePath); } catch { return current.nodePath; } })();
|
|
197
198
|
const seen = new Set();
|
|
198
199
|
const results = [];
|
package/src/cli/service.mjs
CHANGED
|
@@ -29,8 +29,14 @@ export function linuxServiceLauncherText(cliPath, nodePath) {
|
|
|
29
29
|
if (!path.isAbsolute(cliPath) || !path.isAbsolute(nodePath) || path.basename(path.dirname(cliPath)) !== 'bin') {
|
|
30
30
|
throw new Error('Linux service needs absolute CLI and Node paths in a global bin directory');
|
|
31
31
|
}
|
|
32
|
-
const prefix = path.dirname(path.dirname(cliPath));
|
|
33
32
|
const cliScript = resolvedCliScript(cliPath);
|
|
33
|
+
// A saved service CLI is often the resolved package entry rather than the global bin shim.
|
|
34
|
+
// Using dirname(dirname(cliPath)) for that case targets the package itself as npm prefix,
|
|
35
|
+
// nesting each update under @remcp/remcp/lib/node_modules while leaving the agent stale.
|
|
36
|
+
const packageEntrySuffix = path.join('lib', 'node_modules', ...PACKAGE_NAME.split('/'), 'bin', 'remcp.mjs');
|
|
37
|
+
const prefix = cliScript.endsWith(`${path.sep}${packageEntrySuffix}`)
|
|
38
|
+
? cliScript.slice(0, -(packageEntrySuffix.length + 1)) || path.sep
|
|
39
|
+
: path.dirname(path.dirname(cliPath));
|
|
34
40
|
const lines = [
|
|
35
41
|
'#!/bin/sh',
|
|
36
42
|
'NPM_CONFIG_PREFIX=' + shellQuote(prefix),
|
package/src/cli/update.mjs
CHANGED
|
@@ -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,
|
|
15
|
+
import { currentInstallationInfo, ensureMacCliCommand, ensureServiceIfRecorded, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
|
|
16
16
|
|
|
17
17
|
const UPDATE_DISCOVERY_TIMEOUT_MS = 5000;
|
|
18
18
|
|
|
@@ -188,19 +188,19 @@ export function updateAlreadyCurrent({ cfg, targets, currentInstall, serviceInst
|
|
|
188
188
|
if (String(serviceInstall?.runtimeVersion || '').trim() !== targetRuntimeVersion) return false;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
// converge it through syncKnownInstallations().
|
|
194
|
-
for (const item of Array.isArray(cfg?.installations) ? cfg.installations : []) {
|
|
195
|
-
const cliPath = String(item?.cliPath || '').trim();
|
|
196
|
-
if (!cliPath || !fs.existsSync(cliPath)) continue;
|
|
197
|
-
const actual = installationVersionsAtCliPath(cliPath, cfg.runtime.packageName);
|
|
198
|
-
if (String(actual.cliVersion || '').trim() !== targetClientVersion) return false;
|
|
199
|
-
if (String(actual.runtimeVersion || '').trim() !== targetRuntimeVersion) return false;
|
|
200
|
-
}
|
|
191
|
+
// Historic secondary installations may belong to other Node managers and applications.
|
|
192
|
+
// Never let one stale record force an automatic update of the active, healthy service.
|
|
201
193
|
return true;
|
|
202
194
|
}
|
|
203
195
|
|
|
196
|
+
export function verifyManagedServiceReleasePair(serviceInstall, clientSpec, runtimePackageName, runtimeSpec) {
|
|
197
|
+
const expectedClient = specVersion(PACKAGE_NAME, clientSpec);
|
|
198
|
+
const expectedRuntime = specVersion(runtimePackageName, runtimeSpec);
|
|
199
|
+
if (!expectedClient || !expectedRuntime || serviceInstall?.cliVersion !== expectedClient || serviceInstall?.runtimeVersion !== expectedRuntime) {
|
|
200
|
+
throw new Error(`ReMCP update refused to restart: managed service still runs client ${serviceInstall?.cliVersion || 'missing'} / runtime ${serviceInstall?.runtimeVersion || 'missing'}; expected ${expectedClient || 'invalid'} / ${expectedRuntime || 'invalid'}. Repair npm prefix and retry.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
204
|
export async function updateCommand(flags) {
|
|
205
205
|
let cfg = rememberCurrentInstallation(loadConfig());
|
|
206
206
|
const targets = await resolveUpdateTargets({ cfg, flags });
|
|
@@ -253,14 +253,25 @@ export async function updateCommand(flags) {
|
|
|
253
253
|
console.log(`Updating ReMCP to ${targets.clientSpec} with ${targets.runtimeSpec}…`);
|
|
254
254
|
npmGlobalUpdate([PACKAGE_NAME, cfg.runtime.packageName], targets.clientSpec, targets.runtimeSpec);
|
|
255
255
|
|
|
256
|
-
// Converge
|
|
257
|
-
//
|
|
258
|
-
// remain visible in status if they cannot be updated.
|
|
256
|
+
// Converge only the canonical service installation. Never update secondary nvm/Hermes/
|
|
257
|
+
// Homebrew installations implicitly: they may be managed by other applications.
|
|
259
258
|
const synced = syncKnownInstallations(cfg, targets.clientSpec, targets.runtimeSpec);
|
|
260
259
|
for (const item of synced) {
|
|
261
260
|
if (!item.ok) console.error(`ReMCP update: could not sync secondary installation at ${item.nodePath}: ${item.error}`);
|
|
262
261
|
}
|
|
263
262
|
|
|
263
|
+
// npm can report a successful install into a wrong global prefix. Before touching
|
|
264
|
+
// systemd/launchd, verify the exact release pair at the canonical service CLI path.
|
|
265
|
+
// Otherwise a stale launcher repeatedly restarts the old agent and loses device RPCs.
|
|
266
|
+
// Exact server-advertised or explicit release pairs must match the service on disk.
|
|
267
|
+
// A legacy manual `remcp update` against an untrusted/custom server may use @latest,
|
|
268
|
+
// for which no exact version is known until npm resolves it. Keep that manual path
|
|
269
|
+
// compatible; the automatic agent always supplies an exact version.
|
|
270
|
+
if (serviceExpected && specVersion(PACKAGE_NAME, targets.clientSpec)
|
|
271
|
+
&& specVersion(cfg.runtime.packageName, targets.runtimeSpec)) {
|
|
272
|
+
verifyManagedServiceReleasePair(serviceInstallationInfo(cfg), targets.clientSpec, cfg.runtime.packageName, targets.runtimeSpec);
|
|
273
|
+
}
|
|
274
|
+
|
|
264
275
|
// Persist the validated runtime pair before repairing the supervisor so a repair cannot be
|
|
265
276
|
// overwritten by an older in-memory config object.
|
|
266
277
|
if (targets.persistRuntime) {
|
package/src/cli.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { normalizeRuntime } from './runtime.mjs';
|
|
|
7
7
|
import { PACKAGE_NAME, VERSION } from './version.mjs';
|
|
8
8
|
import { probeFilesystemAccess } from './fs-access.mjs';
|
|
9
9
|
|
|
10
|
-
import { ensureMachineId, loadConfig, readJsonFile, saveConfig, setTelemetry, telemetryState, writeJsonFile } from './cli/config.mjs';
|
|
10
|
+
import { autoUpdateEnabled, ensureMachineId, loadConfig, readJsonFile, saveConfig, setTelemetry, telemetryState, writeJsonFile } from './cli/config.mjs';
|
|
11
11
|
import { assertRuntimeTrust, assertSecureServerUrl, 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';
|
|
@@ -144,7 +144,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
144
144
|
const telemetry = telemetryState();
|
|
145
145
|
await runAgent({
|
|
146
146
|
...cfg,
|
|
147
|
-
autoUpdate: cfg
|
|
147
|
+
autoUpdate: autoUpdateEnabled(cfg),
|
|
148
148
|
trustRuntime: cfg.trustRuntime === true
|
|
149
149
|
|| process.env.REMCP_TRUST_RUNTIME === '1'
|
|
150
150
|
|| new URL(cfg.serverUrl).origin === officialOrigin,
|
|
@@ -160,7 +160,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
160
160
|
const action = String(positional[0] || 'status').toLowerCase();
|
|
161
161
|
const cfg = loadConfig(false) || {};
|
|
162
162
|
if (action === 'status') {
|
|
163
|
-
console.log(JSON.stringify({ autoUpdate: cfg
|
|
163
|
+
console.log(JSON.stringify({ autoUpdate: autoUpdateEnabled(cfg), checkIntervalHours: 6 }, null, 2));
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
166
166
|
if (action === 'on' || action === 'enable') { saveConfig({ ...cfg, autoUpdate: true }); console.log('Auto-update enabled.'); return; }
|
|
@@ -301,7 +301,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
301
301
|
...(health.error ? { error:health.error } : {}),
|
|
302
302
|
},
|
|
303
303
|
update:{
|
|
304
|
-
auto:cfg
|
|
304
|
+
auto:autoUpdateEnabled(cfg),
|
|
305
305
|
targetClient,
|
|
306
306
|
targetRuntime,
|
|
307
307
|
multipleInstallations,
|