@remcp/remcp 0.2.43 → 0.2.44
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 +45 -2
- package/src/cli/service.mjs +30 -3
- package/src/cli/update.mjs +1 -1
- package/src/cli.mjs +7 -4
package/package.json
CHANGED
package/src/cli/config.mjs
CHANGED
|
@@ -7,21 +7,64 @@ import { normalizeRuntime } from '../runtime.mjs';
|
|
|
7
7
|
|
|
8
8
|
import { configDir, configFile, machineIdFile, officialOrigin, runtimeConfigFile } from './env.mjs';
|
|
9
9
|
|
|
10
|
+
export const CONFIG_SCHEMA_VERSION = 1;
|
|
11
|
+
const legacyOfficialOrigins = new Set(['https://remcp.delio24.com']);
|
|
12
|
+
|
|
13
|
+
function migrateConfig(value) {
|
|
14
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('ReMCP config must be a JSON object');
|
|
15
|
+
const declared = Number(value.configSchemaVersion || 0);
|
|
16
|
+
// A newer client may have added fields this client does not understand. Preserve that document
|
|
17
|
+
// byte-for-meaning and only use the known fields below; never downgrade its schema marker.
|
|
18
|
+
if (Number.isSafeInteger(declared) && declared > CONFIG_SCHEMA_VERSION) return { value: { ...value }, changed: false };
|
|
19
|
+
|
|
20
|
+
let next = { ...value };
|
|
21
|
+
let changed = false;
|
|
22
|
+
if (declared < 1) {
|
|
23
|
+
try {
|
|
24
|
+
const server = new URL(String(next.serverUrl || ''));
|
|
25
|
+
if (legacyOfficialOrigins.has(server.origin)) {
|
|
26
|
+
next.serverUrl = officialOrigin;
|
|
27
|
+
if (next.trustRuntime === undefined) next.trustRuntime = true;
|
|
28
|
+
changed = true;
|
|
29
|
+
}
|
|
30
|
+
} catch {}
|
|
31
|
+
if (next.configSchemaVersion !== CONFIG_SCHEMA_VERSION) {
|
|
32
|
+
next.configSchemaVersion = CONFIG_SCHEMA_VERSION;
|
|
33
|
+
changed = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { value: next, changed };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function configForWrite(value) {
|
|
40
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('ReMCP config must be a JSON object');
|
|
41
|
+
const declared = Number(value.configSchemaVersion || 0);
|
|
42
|
+
return {
|
|
43
|
+
...value,
|
|
44
|
+
configSchemaVersion: Number.isSafeInteger(declared) && declared > CONFIG_SCHEMA_VERSION
|
|
45
|
+
? declared
|
|
46
|
+
: CONFIG_SCHEMA_VERSION,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
10
50
|
export function loadConfig(required = true) {
|
|
11
51
|
if (!fs.existsSync(configFile)) {
|
|
12
52
|
if (!required) return undefined;
|
|
13
53
|
throw new Error(`ReMCP is not paired. Generate a pairing command at ${officialOrigin}/app/connect`);
|
|
14
54
|
}
|
|
15
|
-
const
|
|
55
|
+
const migrated = migrateConfig(JSON.parse(fs.readFileSync(configFile, 'utf8')));
|
|
56
|
+
const value = migrated.value;
|
|
16
57
|
// A configuration that only carries preferences (for example after `remcp auto-update off`
|
|
17
58
|
// before pairing) has no runtime yet; it must not fail as if it were corrupt.
|
|
18
59
|
if (value.runtime !== undefined) value.runtime = normalizeRuntime(value.runtime);
|
|
60
|
+
if (migrated.changed) saveConfig(value);
|
|
19
61
|
return value;
|
|
20
62
|
}
|
|
21
63
|
|
|
22
64
|
export function saveConfig(value) {
|
|
65
|
+
const persisted = configForWrite(value);
|
|
23
66
|
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
24
|
-
fs.writeFileSync(configFile, JSON.stringify(
|
|
67
|
+
fs.writeFileSync(configFile, JSON.stringify(persisted, null, 2) + '\n', { mode: 0o600 });
|
|
25
68
|
fs.chmodSync(configFile, 0o600);
|
|
26
69
|
}
|
|
27
70
|
|
package/src/cli/service.mjs
CHANGED
|
@@ -72,6 +72,22 @@ export function installWindowsService(cliPath = globalCliPath()) {
|
|
|
72
72
|
run('schtasks.exe', ['/Run', '/TN', windowsTaskName]);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// `serviceInstalled` was added after background services already existed in the wild. Treat an
|
|
76
|
+
// explicit false as the user's opt-out, an explicit true as the current marker, and only infer the
|
|
77
|
+
// old intent from an OS service artifact when the marker is absent. This keeps legacy installs
|
|
78
|
+
// repairable without resurrecting a service that a newer client explicitly disabled.
|
|
79
|
+
export function persistentServiceExpected(config) {
|
|
80
|
+
if (config?.serviceInstalled === false) return false;
|
|
81
|
+
if (config?.serviceInstalled === true) return true;
|
|
82
|
+
const platform = servicePlatform();
|
|
83
|
+
if (platform === 'linux') return fs.existsSync(linuxServiceFile);
|
|
84
|
+
if (platform === 'darwin') return fs.existsSync(macServiceFile);
|
|
85
|
+
if (platform === 'win32') {
|
|
86
|
+
return spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName], { stdio: 'ignore' }).status === 0;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
75
91
|
export function configurePostInstallAccess() {
|
|
76
92
|
const platform = servicePlatform();
|
|
77
93
|
try {
|
|
@@ -136,7 +152,7 @@ export function installPersistentAgent(config) {
|
|
|
136
152
|
// (a failed install, a cleaned LaunchAgents directory, a re-imaged user), the next update recreates
|
|
137
153
|
// it instead of leaving a hand-over to a process nobody supervises.
|
|
138
154
|
export function ensureServiceIfRecorded(config) {
|
|
139
|
-
if (config
|
|
155
|
+
if (!persistentServiceExpected(config)) return false;
|
|
140
156
|
try {
|
|
141
157
|
const cliPath = globalCliPath();
|
|
142
158
|
const platform = servicePlatform();
|
|
@@ -169,6 +185,9 @@ export function ensureServiceIfRecorded(config) {
|
|
|
169
185
|
// kickstart) and is what the Windows task already does on every update.
|
|
170
186
|
installMacService(cliPath);
|
|
171
187
|
} else if (platform === 'win32') installWindowsService(cliPath);
|
|
188
|
+
// Upgrade the legacy inferred state only after the supervisor repair succeeded. A failed repair
|
|
189
|
+
// must not turn a stale artifact into a permanent "managed service" declaration.
|
|
190
|
+
if (config?.serviceInstalled !== true) saveConfig({ ...config, serviceInstalled: true });
|
|
172
191
|
return true;
|
|
173
192
|
} catch (error) {
|
|
174
193
|
console.error(`Could not ensure the background service: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -176,7 +195,8 @@ export function ensureServiceIfRecorded(config) {
|
|
|
176
195
|
}
|
|
177
196
|
}
|
|
178
197
|
|
|
179
|
-
export function restartPersistentServiceIfInstalled() {
|
|
198
|
+
export function restartPersistentServiceIfInstalled(config) {
|
|
199
|
+
if (!persistentServiceExpected(config)) return null;
|
|
180
200
|
const platform = servicePlatform();
|
|
181
201
|
if (platform === 'linux' && fs.existsSync(linuxServiceFile)) {
|
|
182
202
|
run('systemctl', ['--user', 'daemon-reload']);
|
|
@@ -184,7 +204,14 @@ export function restartPersistentServiceIfInstalled() {
|
|
|
184
204
|
return 'remcp-agent.service';
|
|
185
205
|
}
|
|
186
206
|
if (platform === 'darwin' && fs.existsSync(macServiceFile)) {
|
|
187
|
-
|
|
207
|
+
try {
|
|
208
|
+
run('launchctl', ['kickstart', '-k', `${macLaunchDomain()}/${macServiceLabel}`]);
|
|
209
|
+
} catch {
|
|
210
|
+
// A plist can survive while launchd has no loaded job (older installs, logout/login cleanup,
|
|
211
|
+
// manual bootout, or a failed previous update). Re-register the job instead of surfacing the
|
|
212
|
+
// opaque launchctl 113 error. installMacService is idempotent and rewrites stale Node paths too.
|
|
213
|
+
installMacService(globalCliPath());
|
|
214
|
+
}
|
|
188
215
|
return macServiceLabel;
|
|
189
216
|
}
|
|
190
217
|
if (platform === 'win32') {
|
package/src/cli/update.mjs
CHANGED
|
@@ -200,7 +200,7 @@ export async function updateCommand(flags) {
|
|
|
200
200
|
// Only a validated spec is persisted, so a failed update cannot leave the install unable to start.
|
|
201
201
|
if (targets.persistRuntime) saveConfig({ ...cfg, runtime: { ...cfg.runtime, packageSpec: targets.runtimeSpec } });
|
|
202
202
|
const after = { cli: installedVersion(PACKAGE_NAME), runtime: installedVersion(cfg.runtime.packageName) };
|
|
203
|
-
const restarted = restartPersistentServiceIfInstalled();
|
|
203
|
+
const restarted = restartPersistentServiceIfInstalled(cfg);
|
|
204
204
|
if (restarted) {
|
|
205
205
|
console.log(`ReMCP updated and ${restarted} restarted (client ${before.cli} → ${after.cli ?? '?'}, runtime ${before.runtime ?? '?'} → ${after.runtime ?? '?'}).`);
|
|
206
206
|
return;
|
package/src/cli.mjs
CHANGED
|
@@ -128,13 +128,13 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
128
128
|
}
|
|
129
129
|
if (action === 'on' || action === 'enable') {
|
|
130
130
|
console.log(JSON.stringify(setTelemetry(true), null, 2));
|
|
131
|
-
restartPersistentServiceIfInstalled();
|
|
131
|
+
restartPersistentServiceIfInstalled(loadConfig(false));
|
|
132
132
|
console.log('Usage metrics enabled and the agent restarted to apply it.');
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
135
|
if (action === 'off' || action === 'disable') {
|
|
136
136
|
console.log(JSON.stringify(setTelemetry(false), null, 2));
|
|
137
|
-
restartPersistentServiceIfInstalled();
|
|
137
|
+
restartPersistentServiceIfInstalled(loadConfig(false));
|
|
138
138
|
console.log('Usage metrics disabled and the agent restarted to apply it.');
|
|
139
139
|
return;
|
|
140
140
|
}
|
|
@@ -162,7 +162,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
162
162
|
if (!['on', 'off', 'enable', 'disable'].includes(action)) throw new Error('Usage: remcp godmode [status|on|off]');
|
|
163
163
|
const enabled = action === 'on' || action === 'enable';
|
|
164
164
|
writeJsonFile(runtimeConfigFile, { ...readJsonFile(runtimeConfigFile), unrestricted: enabled });
|
|
165
|
-
const restarted = restartPersistentServiceIfInstalled();
|
|
165
|
+
const restarted = restartPersistentServiceIfInstalled(loadConfig(false));
|
|
166
166
|
if (enabled) {
|
|
167
167
|
console.error('Unrestricted mode is ON for this computer: every path and every command is allowed, including sudo. Anything the model is asked to do -- and anything a prompt injection asks it to do -- can now change this machine. Turn it off with `remcp godmode off` when you are done.');
|
|
168
168
|
} else {
|
|
@@ -213,9 +213,12 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
if (command === 'uninstall') {
|
|
216
|
+
const cfg = loadConfig(false);
|
|
216
217
|
uninstallPersistentService();
|
|
218
|
+
// Keep the pairing unless --purge was requested, but remember that the owner explicitly removed
|
|
219
|
+
// the supervisor. Future updates must not infer an old service artifact and recreate it.
|
|
220
|
+
if (cfg) saveConfig({ ...cfg, serviceInstalled: false });
|
|
217
221
|
if (flags.purge) {
|
|
218
|
-
const cfg = loadConfig(false);
|
|
219
222
|
const specs = [PACKAGE_NAME, ...(cfg?.runtime?.packageName ? [cfg.runtime.packageName] : [])];
|
|
220
223
|
run(npm.command, [...npm.args, 'uninstall', '--global', ...specs, '--no-audit', '--no-fund', '--loglevel=error']);
|
|
221
224
|
}
|