@remcp/remcp 0.2.45 → 0.2.47
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/agent.mjs +9 -0
- package/src/cli/env.mjs +1 -0
- package/src/cli/service.mjs +56 -21
package/package.json
CHANGED
package/src/agent.mjs
CHANGED
|
@@ -286,6 +286,15 @@ export async function runAgent(options) {
|
|
|
286
286
|
// Service access is a user-controlled pause, not an outage. Poll slowly enough not to hammer
|
|
287
287
|
// the relay, but cap recovery so an enable action becomes effective within seconds.
|
|
288
288
|
scheduleReconnect(jitter(RECONNECT_BASE_MS));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const status = Number(response.statusCode || 0);
|
|
292
|
+
const transientHandshakeFailure = status === 408 || status === 425 || status === 429 || (status >= 500 && status <= 599);
|
|
293
|
+
if (transientHandshakeFailure && !revoked) {
|
|
294
|
+
// A reverse proxy or relay deploy can refuse the HTTP upgrade before a WebSocket exists.
|
|
295
|
+
// With an unexpected-response listener the client does not reliably emit close afterwards,
|
|
296
|
+
// so this branch must schedule its own retry or a still-running agent can stay offline forever.
|
|
297
|
+
scheduleBackoffReconnect();
|
|
289
298
|
}
|
|
290
299
|
});
|
|
291
300
|
activeSocket = ws;
|
package/src/cli/env.mjs
CHANGED
|
@@ -11,6 +11,7 @@ export const configFile = path.join(configDir, 'config.json');
|
|
|
11
11
|
export const runtimeConfigFile = path.join(configDir, 'runtime.json');
|
|
12
12
|
export const machineIdFile = path.join(configDir, 'machine-id');
|
|
13
13
|
export const linuxServiceFile = path.join(home, '.config', 'systemd', 'user', 'remcp-agent.service');
|
|
14
|
+
export const linuxServiceLauncherFile = path.join(configDir, 'remcp-agent-launcher');
|
|
14
15
|
export const macServiceLabel = 'com.remcp.agent';
|
|
15
16
|
export const macServiceFile = path.join(home, 'Library', 'LaunchAgents', `${macServiceLabel}.plist`);
|
|
16
17
|
export const macLogFile = path.join(home, 'Library', 'Logs', 'remcp-agent.log');
|
package/src/cli/service.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
8
8
|
import { PACKAGE_NAME, VERSION } from '../version.mjs';
|
|
9
9
|
|
|
10
10
|
import { saveConfig } from './config.mjs';
|
|
11
|
-
import { configDir, home, linuxServiceFile, macLogFile, macServiceFile, macServiceLabel, npm, windowsTaskName } from './env.mjs';
|
|
11
|
+
import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macLogFile, macServiceFile, macServiceLabel, npm, windowsTaskName } from './env.mjs';
|
|
12
12
|
import { output, run } from './shell.mjs';
|
|
13
13
|
|
|
14
14
|
export function servicePlatform() {
|
|
@@ -32,12 +32,28 @@ export function quoteSystemd(value) {
|
|
|
32
32
|
return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function resolvedCliScript(cliPath) {
|
|
36
|
+
return fs.existsSync(cliPath) ? fs.realpathSync(cliPath) : path.resolve(cliPath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeLinuxServiceLauncher(cliPath = globalCliPath()) {
|
|
40
|
+
const cliScript = resolvedCliScript(cliPath);
|
|
41
|
+
const launcher = `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(cliScript)} \"$@\"\n`;
|
|
42
|
+
fs.mkdirSync(path.dirname(linuxServiceLauncherFile), { recursive: true, mode: 0o700 });
|
|
43
|
+
const temporary = `${linuxServiceLauncherFile}.${process.pid}.${Date.now()}.tmp`;
|
|
44
|
+
fs.writeFileSync(temporary, launcher, { mode: 0o700 });
|
|
45
|
+
fs.chmodSync(temporary, 0o700);
|
|
46
|
+
fs.renameSync(temporary, linuxServiceLauncherFile);
|
|
47
|
+
return linuxServiceLauncherFile;
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
export function xmlEscape(value) {
|
|
36
51
|
return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
export function installLinuxService(cliPath = globalCliPath()) {
|
|
40
|
-
const
|
|
55
|
+
const launcherFile = writeLinuxServiceLauncher(cliPath);
|
|
56
|
+
const unit = `[Unit]\nDescription=ReMCP device agent\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${quoteSystemd(launcherFile)} start\nRestart=always\nRestartSec=3\nNoNewPrivileges=true\n\n[Install]\nWantedBy=default.target\n`;
|
|
41
57
|
fs.mkdirSync(path.dirname(linuxServiceFile), { recursive: true });
|
|
42
58
|
fs.writeFileSync(linuxServiceFile, unit);
|
|
43
59
|
run('systemctl', ['--user', 'daemon-reload']);
|
|
@@ -65,23 +81,41 @@ function macJobLoaded(target) {
|
|
|
65
81
|
return spawnSync('launchctl', ['print', target], { stdio: 'ignore' }).status === 0;
|
|
66
82
|
}
|
|
67
83
|
|
|
68
|
-
function
|
|
84
|
+
function submitMacHelper(kind, lines) {
|
|
69
85
|
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
70
|
-
const
|
|
71
|
-
const
|
|
86
|
+
const nonce = `${process.pid}.${Date.now()}`;
|
|
87
|
+
const helperFile = path.join(configDir, `launchd-${kind}-${nonce}.sh`);
|
|
88
|
+
const helperLabel = `${macServiceLabel}.${kind}.${nonce}`;
|
|
72
89
|
const script = [
|
|
73
90
|
'#!/bin/sh',
|
|
74
91
|
'sleep 1',
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`
|
|
78
|
-
`launchctl kickstart -k ${shellQuote(target)}`,
|
|
92
|
+
'status=0',
|
|
93
|
+
...lines.map(line => `${line} || status=$?`),
|
|
94
|
+
`if [ "$status" -ne 0 ]; then echo "ReMCP launchd ${kind} helper failed with status $status" >> ${shellQuote(macLogFile)}; fi`,
|
|
79
95
|
`rm -f ${shellQuote(helperFile)}`,
|
|
96
|
+
`launchctl remove ${shellQuote(helperLabel)} >/dev/null 2>&1 || true`,
|
|
97
|
+
'exit 0',
|
|
80
98
|
'',
|
|
81
99
|
].join('\n');
|
|
82
100
|
fs.writeFileSync(helperFile, script, { mode: 0o700 });
|
|
83
101
|
fs.chmodSync(helperFile, 0o700);
|
|
84
102
|
run('launchctl', ['submit', '-l', helperLabel, '--', '/bin/sh', helperFile]);
|
|
103
|
+
return helperLabel;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scheduleMacServiceReload(domain, target) {
|
|
107
|
+
return submitMacHelper('reload', [
|
|
108
|
+
`launchctl bootout ${shellQuote(target)} >/dev/null 2>&1 || true`,
|
|
109
|
+
`launchctl bootstrap ${shellQuote(domain)} ${shellQuote(macServiceFile)}`,
|
|
110
|
+
`launchctl enable ${shellQuote(target)}`,
|
|
111
|
+
`launchctl kickstart -k ${shellQuote(target)}`,
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function scheduleMacServiceRestart(target) {
|
|
116
|
+
return submitMacHelper('restart', [
|
|
117
|
+
`launchctl kickstart -k ${shellQuote(target)}`,
|
|
118
|
+
]);
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
export function installMacService(cliPath = globalCliPath(), { restart = true } = {}) {
|
|
@@ -211,17 +245,16 @@ export function ensureServiceIfRecorded(config) {
|
|
|
211
245
|
if (!fs.existsSync(linuxServiceFile)) {
|
|
212
246
|
installLinuxService(cliPath);
|
|
213
247
|
} else {
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
248
|
+
// Keep the systemd unit independent of nvm/Hermes/Homebrew prefixes. Only this small launcher
|
|
249
|
+
// changes when npm or Node moves, so every install converges on one supervisor entrypoint.
|
|
250
|
+
const launcherFile = writeLinuxServiceLauncher(cliPath);
|
|
217
251
|
const unit = fs.readFileSync(linuxServiceFile, 'utf8');
|
|
218
|
-
const expected = `ExecStart=${quoteSystemd(
|
|
252
|
+
const expected = `ExecStart=${quoteSystemd(launcherFile)} start`;
|
|
219
253
|
if (!unit.includes(expected)) {
|
|
220
254
|
const repaired = /^ExecStart=/m.test(unit) ? unit.replace(/^ExecStart=.*$/m, expected) : '';
|
|
221
255
|
if (!repaired) {
|
|
222
|
-
// A unit
|
|
223
|
-
//
|
|
224
|
-
// "old CLI forever" loop, so fall back to a fresh install instead of giving up.
|
|
256
|
+
// A hand-edited unit with no launcher line is replaced wholesale; the stable launcher is
|
|
257
|
+
// still refreshed first so the replacement never points back at a retired Node manager.
|
|
225
258
|
installLinuxService(cliPath);
|
|
226
259
|
} else {
|
|
227
260
|
fs.writeFileSync(linuxServiceFile, repaired);
|
|
@@ -255,12 +288,14 @@ export function restartPersistentServiceIfInstalled(config) {
|
|
|
255
288
|
return 'remcp-agent.service';
|
|
256
289
|
}
|
|
257
290
|
if (platform === 'darwin' && fs.existsSync(macServiceFile)) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
291
|
+
const target = `${macLaunchDomain()}/${macServiceLabel}`;
|
|
292
|
+
if (macJobLoaded(target)) {
|
|
293
|
+
// Never kill a loaded LaunchAgent inline: the caller may itself be a child of that job.
|
|
294
|
+
// A transient helper survives the handoff and performs the restart after this CLI returns.
|
|
295
|
+
scheduleMacServiceRestart(target);
|
|
296
|
+
} else {
|
|
261
297
|
// A plist can survive while launchd has no loaded job (older installs, logout/login cleanup,
|
|
262
|
-
// manual bootout, or a failed previous update). Re-register
|
|
263
|
-
// opaque launchctl 113 error. installMacService is idempotent and rewrites stale Node paths too.
|
|
298
|
+
// manual bootout, or a failed previous update). Re-register it directly.
|
|
264
299
|
installMacService(globalCliPath());
|
|
265
300
|
}
|
|
266
301
|
return macServiceLabel;
|