@remcp/remcp 0.2.230 → 0.2.232
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-update-monitor.mjs +17 -4
- package/src/agent-update.mjs +4 -4
- package/src/cli/doctor.mjs +1 -1
- package/src/cli/service-inventory.mjs +2 -2
- package/src/cli/shell.mjs +2 -2
- package/src/cli/windows-cli-shim.mjs +62 -0
- package/src/cli.mjs +8 -0
- package/src/npm.mjs +4 -4
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@ const POST_CONNECT_UPDATE_RECHECK_DELAYS_MS = Object.freeze([60_000, 5 * 60_000,
|
|
|
15
15
|
const POST_CONNECT_UPDATE_RECHECK_JITTER_MS = 30_000;
|
|
16
16
|
const UPDATE_RETRY_COOLDOWN_MS = 30 * 60 * 1000;
|
|
17
17
|
|
|
18
|
-
export function createAgentUpdateMonitor({ options, serverUrl, queueEvent, getRuntimeState, onStopRequested, restartRuntime, isStopping, stop }) {
|
|
18
|
+
export function createAgentUpdateMonitor({ options, serverUrl, queueEvent, getRuntimeState, onStopRequested, restartRuntime, isStopping, stop, resolveCli = globalCliEntry }) {
|
|
19
19
|
const postConnectUpdateRecheckDelaysMs = (Array.isArray(options.updateRecheckDelaysMs)
|
|
20
20
|
? options.updateRecheckDelaysMs : POST_CONNECT_UPDATE_RECHECK_DELAYS_MS)
|
|
21
21
|
.map(Number).filter(delay => Number.isFinite(delay) && delay >= 0 && delay < UPDATE_CHECK_INTERVAL_MS).slice(0, 3);
|
|
@@ -37,6 +37,8 @@ export function createAgentUpdateMonitor({ options, serverUrl, queueEvent, getRu
|
|
|
37
37
|
let lastUpdateCheckError = '';
|
|
38
38
|
let lastUpdateCheckErrorAt = 0;
|
|
39
39
|
let lastDiscoveryAt = 0;
|
|
40
|
+
let lastAnnouncedVersion = '';
|
|
41
|
+
let lastAnnouncedAt = 0;
|
|
40
42
|
|
|
41
43
|
function clearPostConnectUpdateChecks() {
|
|
42
44
|
for (const timer of postConnectUpdateTimers) clearTimeout(timer);
|
|
@@ -86,10 +88,20 @@ export function createAgentUpdateMonitor({ options, serverUrl, queueEvent, getRu
|
|
|
86
88
|
const decision = updateDecision({ advertised, cliVersion: VERSION, runtimeVersion, runtimePackageName: options.runtime?.packageName, runtimeDown });
|
|
87
89
|
if (!decision.needed) return;
|
|
88
90
|
const target = decision.target;
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
+
const availabilityKey = `${target}|${decision.runtime}`;
|
|
92
|
+
const observedAt = Date.now();
|
|
93
|
+
const announce = availabilityKey !== lastAnnouncedVersion ||
|
|
94
|
+
observedAt - lastAnnouncedAt >= UPDATE_RETRY_COOLDOWN_MS;
|
|
95
|
+
if (announce) {
|
|
96
|
+
lastAnnouncedVersion = availabilityKey;
|
|
97
|
+
lastAnnouncedAt = observedAt;
|
|
98
|
+
// Version discovery is not installation success. The running version
|
|
99
|
+
// proves convergence only after the replacement agent reconnects.
|
|
100
|
+
queueEvent({ event: 'agent_update_available', at: observedAt, reason: decision.reason.slice(0, 32) });
|
|
101
|
+
}
|
|
102
|
+
const cli = resolveCli();
|
|
91
103
|
if (!cli || !existsSync(cli)) {
|
|
92
|
-
console.error(`ReMCP ${target} is available; run: remcp update`);
|
|
104
|
+
if (announce) console.error(`ReMCP ${target} is available; run: remcp update`);
|
|
93
105
|
return;
|
|
94
106
|
}
|
|
95
107
|
// A version that already failed to install is retried only after a cooldown, so a broken
|
|
@@ -105,6 +117,7 @@ export function createAgentUpdateMonitor({ options, serverUrl, queueEvent, getRu
|
|
|
105
117
|
const updateArgs = updateInvocationArgs(decision, options.trustRuntime === true);
|
|
106
118
|
const child = spawn(process.execPath, [cli, ...updateArgs], {
|
|
107
119
|
detached: true,
|
|
120
|
+
windowsHide: true,
|
|
108
121
|
// The updater's own output is the only record of why an install failed, so it is piped back
|
|
109
122
|
// into this agent's log instead of being discarded.
|
|
110
123
|
stdio: ['ignore', 'pipe', 'pipe'],
|
package/src/agent-update.mjs
CHANGED
|
@@ -24,7 +24,7 @@ function packageLocalNodeModules() {
|
|
|
24
24
|
const REPLACEMENT_HANDOVER_TIMEOUT_MS = 20_000;
|
|
25
25
|
|
|
26
26
|
function globalNodeModules() {
|
|
27
|
-
const result = spawnSync(npm.command, [...npm.args, 'root', '--global'], { encoding: 'utf8' });
|
|
27
|
+
const result = spawnSync(npm.command, [...npm.args, 'root', '--global'], { encoding: 'utf8', windowsHide: true });
|
|
28
28
|
if (result.error || result.status !== 0) throw new Error('Could not locate the global npm modules directory');
|
|
29
29
|
return String(result.stdout || '').trim();
|
|
30
30
|
}
|
|
@@ -142,7 +142,7 @@ export async function restartToApplyUpdate(cli, stopAgent, markStopping, onRunti
|
|
|
142
142
|
}
|
|
143
143
|
// Handing over to a version that cannot start would take the machine offline with nobody left to
|
|
144
144
|
// retry. The new CLI has to answer `--version` before this process steps aside.
|
|
145
|
-
const probe = spawnSync(process.execPath, [cli, '--version'], { encoding: 'utf8', timeout: 30000 });
|
|
145
|
+
const probe = spawnSync(process.execPath, [cli, '--version'], { encoding: 'utf8', timeout: 30000, windowsHide: true });
|
|
146
146
|
const reported = String(probe.stdout || '').trim();
|
|
147
147
|
if (probe.error || probe.status !== 0 || !/^\d+\.\d+\.\d+/.test(reported)) {
|
|
148
148
|
console.error(`The installed ReMCP ${installed || 'update'} did not run (${probe.error?.message || `exit ${probe.status}`}${reported ? `: ${reported}` : ''}). Keeping ${VERSION} running; retry with: remcp update`);
|
|
@@ -150,7 +150,7 @@ export async function restartToApplyUpdate(cli, stopAgent, markStopping, onRunti
|
|
|
150
150
|
}
|
|
151
151
|
console.log(`ReMCP ${installed || 'a newer version'} installed and verified (${reported}); restarting to apply it.`);
|
|
152
152
|
if (!supervisorRestart()) {
|
|
153
|
-
spawn(process.execPath, [cli, 'start'], { detached: true, stdio: 'ignore', env: { ...process.env } }).unref();
|
|
153
|
+
spawn(process.execPath, [cli, 'start'], { detached: true, windowsHide: true, stdio: 'ignore', env: { ...process.env } }).unref();
|
|
154
154
|
// Stepping aside is only safe once the replacement really holds the device: the relay closes
|
|
155
155
|
// this socket with 1012 ('replaced') the moment another agent takes the machine over, and that
|
|
156
156
|
// close is what stops this process. Without the wait, a replacement that cannot start left the
|
|
@@ -221,7 +221,7 @@ export function globalCliEntry({ packageLocal = true } = {}) {
|
|
|
221
221
|
// CLI entry and avoid asking npm which global prefix happens to be active in a minimal service
|
|
222
222
|
// environment (FNM/NVM/Hermes/Homebrew can all disagree with the shell).
|
|
223
223
|
if (packageLocal && existsSync(PACKAGE_CLI_ENTRY)) return PACKAGE_CLI_ENTRY;
|
|
224
|
-
const prefix = spawnSync(npm.command, [...npm.args, 'prefix', '--global'], { encoding:'utf8' });
|
|
224
|
+
const prefix = spawnSync(npm.command, [...npm.args, 'prefix', '--global'], { encoding:'utf8', windowsHide:true });
|
|
225
225
|
if (prefix.error || prefix.status !== 0) return null;
|
|
226
226
|
const base = String(prefix.stdout || '').trim();
|
|
227
227
|
return process.platform === 'win32'
|
package/src/cli/doctor.mjs
CHANGED
|
@@ -108,7 +108,7 @@ export function runtimeAllowedRoots() {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
export function installedVersion(packageName) {
|
|
111
|
-
const prefix = spawnSync(npm.command, [...npm.args, 'prefix', '--global'], { encoding: 'utf8' });
|
|
111
|
+
const prefix = spawnSync(npm.command, [...npm.args, 'prefix', '--global'], { encoding: 'utf8', windowsHide: true });
|
|
112
112
|
if (prefix.error || prefix.status !== 0) return null;
|
|
113
113
|
const manifest = path.join(String(prefix.stdout || '').trim(), 'lib', 'node_modules', ...packageName.split('/'), 'package.json');
|
|
114
114
|
try { return JSON.parse(fs.readFileSync(manifest, 'utf8')).version || null; } catch { return null; }
|
|
@@ -16,7 +16,7 @@ export function persistentServiceExpected(config) {
|
|
|
16
16
|
if (platform === 'darwin') return fs.existsSync(macServiceFile);
|
|
17
17
|
if (platform === 'win32') {
|
|
18
18
|
return windowsStartupScriptInstalled(windowsStartupScriptFile) ||
|
|
19
|
-
spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName], { stdio: 'ignore' }).status === 0;
|
|
19
|
+
spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName], { stdio: 'ignore', windowsHide: true }).status === 0;
|
|
20
20
|
}
|
|
21
21
|
return false;
|
|
22
22
|
}
|
|
@@ -36,7 +36,7 @@ export function persistentServiceState(config) {
|
|
|
36
36
|
return { expected, installed, active, manager:'launchd', name:macServiceLabel };
|
|
37
37
|
}
|
|
38
38
|
if (platform === 'win32') {
|
|
39
|
-
const query = spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName, '/FO', 'LIST'], { encoding:'utf8' });
|
|
39
|
+
const query = spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName, '/FO', 'LIST'], { encoding:'utf8', windowsHide:true });
|
|
40
40
|
const installed = query.status === 0;
|
|
41
41
|
const outputText = String(query.stdout || '');
|
|
42
42
|
const active = installed && /(?:Status|Состояние):\s*Running/i.test(outputText);
|
package/src/cli/shell.mjs
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
|
|
5
5
|
export function run(command, args, options = {}) {
|
|
6
|
-
const result = spawnSync(command, args, { stdio: 'inherit', ...options });
|
|
6
|
+
const result = spawnSync(command, args, { stdio: 'inherit', ...options, windowsHide: true });
|
|
7
7
|
if (result.error) throw result.error;
|
|
8
8
|
if (result.status !== 0) throw new Error(`${command} failed with exit code ${result.status}`);
|
|
9
9
|
return result;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function output(command, args) {
|
|
13
|
-
const result = spawnSync(command, args, { encoding: 'utf8' });
|
|
13
|
+
const result = spawnSync(command, args, { encoding: 'utf8', windowsHide: true });
|
|
14
14
|
if (result.error) throw result.error;
|
|
15
15
|
if (result.status !== 0) throw new Error(`${command} failed with exit code ${result.status}`);
|
|
16
16
|
return String(result.stdout || '').trim();
|
|
@@ -71,3 +71,65 @@ export function preferWindowsCmdShim(prefix) {
|
|
|
71
71
|
fs.unlinkSync(powershell);
|
|
72
72
|
return true;
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
// An older auto-updater can install a newer client without running that newer
|
|
77
|
+
// client's post-npm shim cleanup. Repair the canonical npm-owned shadow at
|
|
78
|
+
// agent startup so a Windows account using Restricted PowerShell remains usable.
|
|
79
|
+
// Do not touch arbitrary prefixes, custom scripts, links or nonstandard shims.
|
|
80
|
+
export const WINDOWS_NPM_CMD_SHIM = String.raw`@ECHO off
|
|
81
|
+
GOTO start
|
|
82
|
+
:find_dp0
|
|
83
|
+
SET dp0=%~dp0
|
|
84
|
+
EXIT /b
|
|
85
|
+
:start
|
|
86
|
+
SETLOCAL
|
|
87
|
+
CALL :find_dp0
|
|
88
|
+
|
|
89
|
+
IF EXIST "%dp0%\node.exe" (
|
|
90
|
+
SET "_prog=%dp0%\node.exe"
|
|
91
|
+
) ELSE (
|
|
92
|
+
SET "_prog=node"
|
|
93
|
+
SET PATHEXT=%PATHEXT:;.JS;=;%
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\node_modules\@remcp\remcp\bin\remcp.mjs" %*
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
function sameCanonicalFile(actual, expected) {
|
|
100
|
+
if (!actual || !expected) return false;
|
|
101
|
+
const same = (a, b) => path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
|
|
102
|
+
try {
|
|
103
|
+
return same(actual, expected) && same(fs.realpathSync(actual), expected)
|
|
104
|
+
&& same(fs.realpathSync(expected), expected);
|
|
105
|
+
} catch { return false; }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function repairManagedWindowsPowerShellShimOnStart({
|
|
109
|
+
platform = process.platform,
|
|
110
|
+
localAppData = process.env.LOCALAPPDATA,
|
|
111
|
+
nodePath = process.execPath,
|
|
112
|
+
cliEntry = process.argv[1],
|
|
113
|
+
} = {}) {
|
|
114
|
+
if (platform !== 'win32' || !localAppData || !path.isAbsolute(localAppData)) return false;
|
|
115
|
+
const prefix = path.join(localAppData, 'ReMCP', 'runtime');
|
|
116
|
+
const expectedNode = path.join(prefix, 'node.exe');
|
|
117
|
+
const expectedCli = path.join(prefix, 'node_modules', '@remcp', 'remcp', 'bin', 'remcp.mjs');
|
|
118
|
+
if (!sameCanonicalFile(nodePath, expectedNode) || !sameCanonicalFile(cliEntry, expectedCli)) return false;
|
|
119
|
+
|
|
120
|
+
const directory = fs.lstatSync(prefix);
|
|
121
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
122
|
+
throw new Error('Refusing Windows launcher self-repair outside a real managed runtime directory');
|
|
123
|
+
}
|
|
124
|
+
if (!assertWindowsNpmPowerShellShimOwned(prefix)) return false;
|
|
125
|
+
const cmdPath = path.join(prefix, 'remcp.cmd');
|
|
126
|
+
const cmdStat = fs.lstatSync(cmdPath);
|
|
127
|
+
if (!cmdStat.isFile() || cmdStat.isSymbolicLink()) {
|
|
128
|
+
throw new Error('Refusing Windows launcher self-repair without a trusted batch shim');
|
|
129
|
+
}
|
|
130
|
+
const cmdBody = fs.readFileSync(cmdPath, 'utf8');
|
|
131
|
+
if (normalizedShim(cmdBody) !== normalizedShim(WINDOWS_NPM_CMD_SHIM)) {
|
|
132
|
+
throw new Error('Refusing Windows launcher self-repair for an unrecognized batch shim');
|
|
133
|
+
}
|
|
134
|
+
return preferWindowsCmdShim(prefix);
|
|
135
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { auditKnownInstallations } from './cli/installation-audit.mjs';
|
|
|
17
17
|
import { scheduleMacTemperatureRepair } from './cli/mac-native-temperature.mjs';
|
|
18
18
|
import { guardSupervisorDisconnect } from './cli/supervised-agent-lifecycle.mjs';
|
|
19
19
|
import { repairMacTemperatureAfterStart } from './cli/npm-global.mjs';
|
|
20
|
+
import { repairManagedWindowsPowerShellShimOnStart } from './cli/windows-cli-shim.mjs';
|
|
20
21
|
import { run } from './cli/shell.mjs';
|
|
21
22
|
import { updateCommand } from './cli/update.mjs';
|
|
22
23
|
function fetchConfiguredServer(url, options) {
|
|
@@ -165,6 +166,13 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
165
166
|
|| Boolean(process.env.INVOCATION_ID)
|
|
166
167
|
|| String(process.env.XPC_SERVICE_NAME || '').includes('com.remcp.agent');
|
|
167
168
|
const cfg = rememberCurrentInstallation(loadConfig(), { service:supervised });
|
|
169
|
+
if (process.platform === 'win32') {
|
|
170
|
+
try { repairManagedWindowsPowerShellShimOnStart(); }
|
|
171
|
+
catch (error) {
|
|
172
|
+
// Never take a paired device offline because an old launcher is custom or unreadable.
|
|
173
|
+
console.error(`ReMCP Windows PowerShell launcher self-repair skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
168
176
|
// A service can start from an absolute fnm/nvm Node path even when an interactive macOS shell
|
|
169
177
|
// cannot resolve the npm-global `remcp` binary. Refresh the stable user shim on every start so
|
|
170
178
|
// an auto-update repairs the next Terminal session without requiring a manual PATH edit.
|
package/src/npm.mjs
CHANGED
|
@@ -62,13 +62,13 @@ export function resolveNpm({ nodePath = process.execPath, home = os.homedir(), p
|
|
|
62
62
|
|
|
63
63
|
// The version npm itself reports, or null when it cannot be executed at all.
|
|
64
64
|
export function npmVersion(resolved = resolveNpm()) {
|
|
65
|
-
const result = spawnSync(resolved.command, [...resolved.args, '--version'], { encoding: 'utf8', timeout: 15000 });
|
|
65
|
+
const result = spawnSync(resolved.command, [...resolved.args, '--version'], { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
66
66
|
if (result.error || result.status !== 0) {
|
|
67
67
|
// A login shell may still find npm (nvm and Homebrew write their PATH into the profile).
|
|
68
|
-
const shell = process.platform === 'win32' ? null : spawnSync('/bin/sh', ['-lc', 'command -v npm'], { encoding: 'utf8', timeout: 15000 });
|
|
68
|
+
const shell = process.platform === 'win32' ? null : spawnSync('/bin/sh', ['-lc', 'command -v npm'], { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
69
69
|
const found = String(shell?.stdout || '').trim().split('\n').pop();
|
|
70
70
|
if (found && existsSync(found)) {
|
|
71
|
-
const retry = spawnSync(found, ['--version'], { encoding: 'utf8', timeout: 15000 });
|
|
71
|
+
const retry = spawnSync(found, ['--version'], { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
72
72
|
if (!retry.error && retry.status === 0) return { version: String(retry.stdout).trim(), source: found };
|
|
73
73
|
}
|
|
74
74
|
return null;
|
|
@@ -79,5 +79,5 @@ export function npmVersion(resolved = resolveNpm()) {
|
|
|
79
79
|
// Runs npm with the resolved command, so callers never depend on PATH.
|
|
80
80
|
export function npmRun(args, { encoding = 'utf8', stdio = 'inherit' } = {}) {
|
|
81
81
|
const resolved = resolveNpm();
|
|
82
|
-
return spawnSync(resolved.command, [...resolved.args, ...args], { encoding, stdio });
|
|
82
|
+
return spawnSync(resolved.command, [...resolved.args, ...args], { encoding, stdio, windowsHide: true });
|
|
83
83
|
}
|