@remcp/remcp 0.2.224 → 0.2.226

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.224",
3
+ "version": "0.2.226",
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",
@@ -18,7 +18,7 @@
18
18
  "README.md"
19
19
  ],
20
20
  "scripts": {
21
- "check": "node --check bin/remcp.mjs && node --check src/agent-deploy-reconnect.mjs && node --check src/agent-network-probe-client.mjs && node --check src/agent-relay-connection.mjs && node --check src/agent-responder.mjs && node --check src/agent-runtime-capabilities.mjs && node --check src/agent-update-monitor.mjs && node --check src/agent-update.mjs && node --check src/agent-utilities.mjs && node --check src/agent.mjs && node --check src/cli.mjs && node --check src/cli/config.mjs && node --check src/cli/connect.mjs && node --check src/cli/doctor.mjs && node --check src/cli/env.mjs && node --check src/cli/installation-audit.mjs && node --check src/cli/mac-native-temperature.mjs && node --check src/cli/npm-global.mjs && node --check src/cli/pairing-qr.mjs && node --check src/cli/service-inventory.mjs && node --check src/cli/service-macos.mjs && node --check src/cli/service-paths.mjs && node --check src/cli/service.mjs && node --check src/cli/shell.mjs && node --check src/cli/update.mjs && node --check src/cli/windows-owned-job-source.mjs && node --check src/cli/windows-owned-job.mjs && node --check src/cli/windows-supervisor.mjs && node --check src/cli/windows-task-restart.mjs && node --check src/cli/windows-update-lock.mjs && node --check src/cli/windows-user-supervisor.mjs && node --check src/fetch-update-manifest.mjs && node --check src/fs-access.mjs && node --check src/npm.mjs && node --check src/runtime.mjs && node --check src/version.mjs",
21
+ "check": "node --check bin/remcp.mjs && node --check src/agent-deploy-reconnect.mjs && node --check src/agent-network-probe-client.mjs && node --check src/agent-relay-connection.mjs && node --check src/agent-responder.mjs && node --check src/agent-runtime-capabilities.mjs && node --check src/agent-update-monitor.mjs && node --check src/agent-update.mjs && node --check src/agent-utilities.mjs && node --check src/agent.mjs && node --check src/cli.mjs && node --check src/cli/config.mjs && node --check src/cli/connect.mjs && node --check src/cli/doctor.mjs && node --check src/cli/env.mjs && node --check src/cli/installation-audit.mjs && node --check src/cli/mac-native-temperature.mjs && node --check src/cli/npm-global.mjs && node --check src/cli/pairing-qr.mjs && node --check src/cli/service-inventory.mjs && node --check src/cli/service-macos.mjs && node --check src/cli/service-paths.mjs && node --check src/cli/service.mjs && node --check src/cli/shell.mjs && node --check src/cli/update.mjs && node --check src/cli/windows-owned-job-source.mjs && node --check src/cli/windows-owned-job.mjs && node --check src/cli/windows-supervisor.mjs && node --check src/cli/windows-update-lock.mjs && node --check src/cli/windows-user-supervisor.mjs && node --check src/fetch-update-manifest.mjs && node --check src/fs-access.mjs && node --check src/npm.mjs && node --check src/runtime.mjs && node --check src/version.mjs",
22
22
  "test": "node --test test/*.test.mjs"
23
23
  },
24
24
  "dependencies": {
@@ -15,11 +15,10 @@ export { macLaunchDomain, ensureMacCliCommand, installMacService } from './servi
15
15
  import { PACKAGE_NAME, VERSION } from '../version.mjs';
16
16
 
17
17
  import { saveConfig } from './config.mjs';
18
- import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macServiceFile, macServiceLabel, windowsServiceLauncherFile, windowsTaskScriptFile, windowsStartupScriptFile, windowsTaskName } from './env.mjs';
18
+ import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macServiceFile, macServiceLabel, windowsServiceLauncherFile, windowsStartupScriptFile, windowsTaskName } from './env.mjs';
19
19
  import { run } from './shell.mjs';
20
- import { windowsServiceLauncherText, windowsTaskReliabilityCommand, windowsTaskAbsenceCommand, windowsBatchUnsafeProfile, windowsTaskScriptText, windowsTaskScriptInstalled, windowsStartupScriptText, windowsStartupScriptInstalled } from './windows-supervisor.mjs';
20
+ import { windowsServiceLauncherText, windowsTaskAbsenceCommand, windowsStartupScriptText, windowsStartupScriptInstalled } from './windows-supervisor.mjs';
21
21
  import { ensureWindowsSupervisorKey, windowsSupervisorCommand } from './windows-user-supervisor.mjs';
22
- import { scheduleWindowsTaskRestart } from './windows-task-restart.mjs';
23
22
 
24
23
  export function quoteSystemd(value) {
25
24
  return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
@@ -79,20 +78,6 @@ export function writeWindowsServiceLauncher(cliPath = globalCliPath(), nodePath
79
78
  return windowsServiceLauncherFile;
80
79
  }
81
80
 
82
- function writeWindowsTaskScript(launcherFile) {
83
- if (fs.existsSync(windowsTaskScriptFile) && !windowsTaskScriptInstalled(windowsTaskScriptFile, windowsServiceLauncherFile)) {
84
- throw new Error('The ReMCP scheduled task script is not owned by ReMCP; refusing to overwrite it');
85
- }
86
- const script = windowsTaskScriptText(launcherFile);
87
- fs.mkdirSync(configDir, { recursive:true, mode:0o700 });
88
- const temporary = `${windowsTaskScriptFile}.${process.pid}.${Date.now()}.tmp`;
89
- try {
90
- fs.writeFileSync(temporary, '\uFEFF' + script, { flag:'wx', encoding:'utf16le', mode:0o600 });
91
- fs.renameSync(temporary, windowsTaskScriptFile);
92
- } finally { try { fs.unlinkSync(temporary); } catch {} }
93
- return windowsTaskScriptFile;
94
- }
95
-
96
81
  function installWindowsUserStartup(cliPath, nodePath) {
97
82
  const existed = fs.existsSync(windowsStartupScriptFile);
98
83
  if (existed && !windowsStartupScriptInstalled(windowsStartupScriptFile)) {
@@ -140,64 +125,39 @@ function installWindowsUserStartup(cliPath, nodePath) {
140
125
  console.log('ReMCP uses this Windows account\'s Startup folder (no administrator rights required).');
141
126
  }
142
127
 
143
- function requireWindowsScheduledTaskAbsent() {
144
- // A failed schtasks /Query is ambiguous across Windows languages and ACLs.
145
- // Fail closed if COM cannot prove absence before choosing another startup route.
146
- const existing = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive',
128
+ function windowsScheduledTaskPresence() {
129
+ // Never interpret schtasks.exe failure as absence: it also reports access-denied.
130
+ // COM returns locale-independent PRESENT/ABSENT or throws on an uncertain result.
131
+ const query = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive',
147
132
  '-Command', windowsTaskAbsenceCommand(windowsTaskName)],
148
- { encoding:'utf8', windowsHide:true, timeout:10000, maxBuffer:4096, input:'' });
149
- if (existing.error || existing.status !== 0 || String(existing.stdout || '').trim() !== 'ABSENT') {
150
- throw new Error('Could not prove that the ReMCP scheduled task is absent; refusing to start a competing agent');
133
+ { encoding:'utf8', windowsHide:true, timeout:30000, maxBuffer:4096, input:'' });
134
+ const state = String(query.stdout || '').trim();
135
+ if (query.error || query.status !== 0 || !['ABSENT', 'PRESENT'].includes(state)) {
136
+ throw new Error('Could not verify the legacy ReMCP Scheduled Task; refusing an unsafe lifecycle change');
151
137
  }
138
+ return state;
152
139
  }
153
140
 
154
- export function installWindowsService(cliPath = globalCliPath(), { nodePath = process.execPath } = {}) {
155
- if (windowsStartupScriptInstalled(windowsStartupScriptFile)) {
156
- installWindowsUserStartup(cliPath, nodePath);
157
- return;
158
- }
159
- if (windowsBatchUnsafeProfile(cliPath, nodePath, windowsServiceLauncherFile)) {
160
- requireWindowsScheduledTaskAbsent();
161
- console.log('ReMCP: Unicode or shell-sensitive Windows profile; using the per-user Startup supervisor.');
162
- installWindowsUserStartup(cliPath, nodePath);
163
- return;
164
- }
165
- const launcherFile = writeWindowsServiceLauncher(cliPath, nodePath);
166
- const taskScript = writeWindowsTaskScript(launcherFile);
167
- // WScript has no console; its hidden CMD child remains attached until it exits.
168
- // /IT keeps desktop/UI automation in the signed-in user's session.
169
- const command = `wscript.exe //B //Nologo "${taskScript}"`;
170
- // Current-user interactive tasks need /IT. /RL LIMITED avoids requiring elevation.
171
- // An organization may still disable creation of all tasks by standard users.
172
- const created = spawnSync('schtasks.exe',
173
- ['/Create', '/TN', windowsTaskName, '/TR', command, '/SC', 'ONLOGON', '/RL', 'LIMITED', '/IT', '/F'],
174
- { encoding:'utf8', timeout:15000, maxBuffer:64 * 1024, windowsHide:true, input:'' });
175
- if (created.error || created.status !== 0) {
176
- requireWindowsScheduledTaskAbsent();
177
- console.warn('Windows Task Scheduler rejected task creation; switching to per-user Startup.');
178
- installWindowsUserStartup(cliPath, nodePath);
179
- return;
180
- }
181
- // Recovery tuning is optional; a policy denying Set-ScheduledTask must not invalidate
182
- // a successfully created and runnable interactive task.
183
- try {
184
- run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', windowsTaskReliabilityCommand(windowsTaskName)]);
185
- } catch (error) {
186
- console.warn(`ReMCP task recovery settings were not applied: ${error.message}`);
187
- }
188
- try {
189
- run('schtasks.exe', ['/Run', '/TN', windowsTaskName], { timeout:15000 });
190
- } catch (error) {
191
- // We just created this task. Remove it before registering another launcher, so
192
- // a delayed logon cannot start competing agents under two supervisors.
193
- const removed = spawnSync('schtasks.exe', ['/Delete', '/TN', windowsTaskName, '/F'],
194
- { stdio:'ignore', timeout:10000 });
195
- if (removed.status !== 0) throw error;
196
- console.warn('Windows Task Scheduler could not start ReMCP; using per-user Startup.');
197
- installWindowsUserStartup(cliPath, nodePath);
141
+ function requireWindowsScheduledTaskAbsent() {
142
+ if (windowsScheduledTaskPresence() !== 'ABSENT') {
143
+ throw new Error('A legacy ReMCP Scheduled Task exists; its child process tree cannot be safely stopped or migrated automatically');
198
144
  }
199
145
  }
200
146
 
147
+ // Called before any npm install/update so a legacy task cannot be partially
148
+ // replaced while its WScript/CMD/Node descendants are still running.
149
+ export function assertWindowsUpdateLifecycleSafe() {
150
+ if (servicePlatform() === 'win32') requireWindowsScheduledTaskAbsent();
151
+ }
152
+
153
+ export function installWindowsService(cliPath = globalCliPath(), { nodePath = process.execPath } = {}) {
154
+ // The old Task Scheduler launcher (WScript -> CMD -> Node) can orphan its
155
+ // descendants after /End. Do not create or touch it on new installations.
156
+ // An existing task must be migrated with verified process ownership first.
157
+ requireWindowsScheduledTaskAbsent();
158
+ installWindowsUserStartup(cliPath, nodePath);
159
+ }
160
+
201
161
  // `serviceInstalled` was added after background services already existed in the wild. Treat an
202
162
  // explicit false as the user's opt-out, an explicit true as the current marker, and only infer the
203
163
  // old intent from an OS service artifact when the marker is absent. This keeps legacy installs
@@ -382,20 +342,14 @@ export function restartPersistentServiceIfInstalled(config, { deferLinux = false
382
342
  return macServiceLabel;
383
343
  }
384
344
  if (platform === 'win32') {
345
+ // /End can leave Node descendants running after a legacy CMD task ends.
346
+ // Do not report a successful credential restart when ownership is unknown.
347
+ requireWindowsScheduledTaskAbsent();
385
348
  if (windowsStartupScriptInstalled(windowsStartupScriptFile)) {
386
349
  const state = windowsSupervisorCommand('restart');
387
350
  if (state.restartScheduled !== true) throw new Error('Windows user supervisor did not schedule a restart');
388
351
  return 'ReMCP user startup supervisor (restart scheduled)';
389
352
  }
390
- const result = spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName], { stdio: 'ignore' });
391
- if (result.status === 0) {
392
- if (deferWindows) return scheduleWindowsTaskRestart(canonicalServiceNodePath(config));
393
- // /Run does not restart a task that is already running; stop the previous
394
- // process first so a rotated credential is loaded by the replacement.
395
- spawnSync('schtasks.exe', ['/End', '/TN', windowsTaskName], { stdio: 'ignore' });
396
- run('schtasks.exe', ['/Run', '/TN', windowsTaskName]);
397
- return windowsTaskName;
398
- }
399
353
  }
400
354
  return null;
401
355
  }
@@ -412,6 +366,13 @@ export function uninstallPersistentService() {
412
366
  try { fs.unlinkSync(macServiceFile); } catch {}
413
367
  removeMacCliCommand();
414
368
  } else if (platform === 'win32') {
369
+ // Refuse before touching any artifact. Scheduler /End and /Delete cannot
370
+ // prove that its detached CMD/Node descendants exited.
371
+ requireWindowsScheduledTaskAbsent();
372
+ if (fs.existsSync(windowsStartupScriptFile) &&
373
+ !windowsStartupScriptInstalled(windowsStartupScriptFile)) {
374
+ throw new Error('The ReMCP Startup entry is not owned by ReMCP; refusing to remove it');
375
+ }
415
376
  if (windowsStartupScriptInstalled(windowsStartupScriptFile)) {
416
377
  // Removing Startup alone leaves the already spawned agent alive. Stop the
417
378
  // exact child owned by the authenticated singleton, then remove registration.
@@ -426,9 +387,7 @@ export function uninstallPersistentService() {
426
387
  }
427
388
  fs.unlinkSync(windowsStartupScriptFile);
428
389
  }
429
- spawnSync('schtasks.exe', ['/End', '/TN', windowsTaskName], { stdio: 'ignore' });
430
- spawnSync('schtasks.exe', ['/Delete', '/TN', windowsTaskName, '/F'], { stdio: 'ignore' });
431
- try { if (windowsTaskScriptInstalled(windowsTaskScriptFile, windowsServiceLauncherFile)) fs.unlinkSync(windowsTaskScriptFile); } catch {}
432
- try { fs.unlinkSync(windowsServiceLauncherFile); } catch {}
390
+ // Retain legacy launch files. Their presence alone is not proof of ownership,
391
+ // and an absent task provides no safe justification for deleting them.
433
392
  }
434
393
  }
@@ -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, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, servicePlatform, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
15
+ import { assertWindowsUpdateLifecycleSafe, currentInstallationInfo, ensureMacCliCommand, ensureServiceIfRecorded, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, servicePlatform, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
16
16
 
17
17
  const UPDATE_DISCOVERY_TIMEOUT_MS = 5000;
18
18
 
@@ -264,6 +264,11 @@ export async function updateCommand(flags) {
264
264
  return;
265
265
  }
266
266
 
267
+ // Legacy Scheduled Task /End can orphan an already running agent. Refuse the
268
+ // update before touching npm or package state until ownership is verifiable.
269
+ // An old task may survive an explicit service opt-out. Its ownership must be
270
+ // checked independently of that preference before replacing either package.
271
+ assertWindowsUpdateLifecycleSafe();
267
272
  const before = { cli: VERSION, runtime: installedVersion(cfg.runtime.packageName) };
268
273
  console.log(`Updating ReMCP to ${targets.clientSpec} with ${targets.runtimeSpec}…`);
269
274
  npmGlobalUpdate([PACKAGE_NAME, cfg.runtime.packageName], targets.clientSpec, targets.runtimeSpec);
@@ -15,6 +15,11 @@ const root = process.env.REMCP_CONFIG_DIR || path.join(os.homedir(), '.config',
15
15
  const keyFile = path.join(root, 'windows-startup-control.key');
16
16
  const actions = new Set(['status', 'stop', 'restart']);
17
17
 
18
+ // Startup readiness must cover the owned Job Object helper's 45-second PowerShell
19
+ // initialization budget plus process startup. The synchronous CLI needs extra
20
+ // time to collect the result from the independent control process.
21
+ export const WINDOWS_SUPERVISOR_READY_BUDGET_MS = 57000;
22
+
18
23
  export function windowsSupervisorPath(configDir = root, platform = process.platform) {
19
24
  const id = crypto.createHash('sha256').update(path.resolve(configDir)).digest('hex').slice(0, 32);
20
25
  return platform === 'win32' ? '\\\\.\\pipe\\remcp-user-' + id
@@ -74,7 +79,10 @@ export async function windowsSupervisorRequest(action, { timeoutMs = action ===
74
79
 
75
80
  // Synchronous callers in the installed CLI use a separate Node client.
76
81
  // Never pass device tokens or control keys in command-line arguments.
77
- export function windowsSupervisorCommand(action, { timeoutMs = action === 'stop' ? 20000 : 15000 } = {}) {
82
+ export function windowsSupervisorCommand(action, {
83
+ timeoutMs = action === 'stop' ? 20000 :
84
+ action === 'wait' ? WINDOWS_SUPERVISOR_READY_BUDGET_MS + 8000 : 15000,
85
+ } = {}) {
78
86
  if (!actions.has(action) && action !== 'wait') throw new Error('Invalid supervisor action');
79
87
  const proc = spawnSync(process.execPath, [currentFile, 'control', action], {
80
88
  encoding: 'utf8', windowsHide: true, timeout: timeoutMs, maxBuffer: 8192,
@@ -245,7 +253,7 @@ async function control(action) {
245
253
  throw new Error('Windows Startup supervisor has not exited after stop');
246
254
  }
247
255
  if (action !== 'wait') return windowsSupervisorRequest(action);
248
- const deadline = Date.now() + 11000;
256
+ const deadline = Date.now() + WINDOWS_SUPERVISOR_READY_BUDGET_MS;
249
257
  while (Date.now() < deadline) {
250
258
  try {
251
259
  const current = await windowsSupervisorRequest('status', { timeoutMs: 1000 });