@remcp/remcp 0.2.55 → 0.2.56

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.55",
3
+ "version": "0.2.56",
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",
@@ -158,6 +158,100 @@ function shellQuote(value) {
158
158
  return `'${String(value).replaceAll("'", "'\\''")}'`;
159
159
  }
160
160
 
161
+ const remcpPathBlockStart = '# >>> ReMCP CLI PATH >>>';
162
+ const remcpPathBlockEnd = '# <<< ReMCP CLI PATH <<<';
163
+ const remcpCliShimMarker = '# ReMCP managed CLI shim';
164
+
165
+ function macCliShimPath() {
166
+ return path.join(home, '.local', 'bin', 'remcp');
167
+ }
168
+
169
+ function interactiveShellProfile() {
170
+ const shell = String(process.env.REMCP_TEST_SHELL || process.env.SHELL || os.userInfo().shell || '/bin/zsh');
171
+ const name = path.basename(shell);
172
+ if (name === 'zsh') return path.join(home, '.zshrc');
173
+ if (name === 'bash') return path.join(home, '.bash_profile');
174
+ return '';
175
+ }
176
+
177
+ function upsertManagedPathBlock(file) {
178
+ if (!file) return false;
179
+ const block = [
180
+ remcpPathBlockStart,
181
+ 'case ":$PATH:" in',
182
+ ' *":$HOME/.local/bin:"*) ;;',
183
+ ' *) export PATH="$HOME/.local/bin:$PATH" ;;',
184
+ 'esac',
185
+ remcpPathBlockEnd,
186
+ ].join('\n');
187
+ let current = '';
188
+ try { current = fs.readFileSync(file, 'utf8'); } catch {}
189
+ const start = current.indexOf(remcpPathBlockStart);
190
+ const end = start >= 0 ? current.indexOf(remcpPathBlockEnd, start + remcpPathBlockStart.length) : -1;
191
+ let next;
192
+ if (start >= 0 && end >= 0) {
193
+ next = current.slice(0, start) + block + current.slice(end + remcpPathBlockEnd.length);
194
+ } else {
195
+ const prefix = current && !current.endsWith('\n') ? current + '\n' : current;
196
+ next = prefix + (prefix ? '\n' : '') + block + '\n';
197
+ }
198
+ if (next === current) return false;
199
+ fs.writeFileSync(file, next, { mode:0o600 });
200
+ return true;
201
+ }
202
+
203
+ function removeManagedPathBlock(file) {
204
+ if (!file) return;
205
+ let current = '';
206
+ try { current = fs.readFileSync(file, 'utf8'); } catch { return; }
207
+ const start = current.indexOf(remcpPathBlockStart);
208
+ const end = start >= 0 ? current.indexOf(remcpPathBlockEnd, start + remcpPathBlockStart.length) : -1;
209
+ if (start < 0 || end < 0) return;
210
+ let before = current.slice(0, start);
211
+ let after = current.slice(end + remcpPathBlockEnd.length);
212
+ if (before.endsWith('\n') && after.startsWith('\n')) after = after.slice(1);
213
+ const next = before + after;
214
+ fs.writeFileSync(file, next, { mode:0o600 });
215
+ }
216
+
217
+ export function ensureMacCliCommand(config = {}, { cliPath = '', nodePath = '' } = {}) {
218
+ if (servicePlatform() !== 'darwin') return null;
219
+ const effectiveCli = cliPath || (persistentServiceExpected(config) ? canonicalServiceCliPath(config) : globalCliPath());
220
+ const effectiveNode = nodePath || (persistentServiceExpected(config) ? canonicalServiceNodePath(config) : process.execPath);
221
+ const cliScript = resolvedCliScript(effectiveCli);
222
+ const shim = macCliShimPath();
223
+ try {
224
+ const existing = fs.readFileSync(shim, 'utf8');
225
+ if (!existing.includes(remcpCliShimMarker)) {
226
+ console.error(`ReMCP did not replace ${shim} because it is not a ReMCP-managed command.`);
227
+ return null;
228
+ }
229
+ } catch (error) {
230
+ if (error?.code && error.code !== 'ENOENT') {
231
+ console.error(`ReMCP could not inspect ${shim}: ${error instanceof Error ? error.message : String(error)}`);
232
+ return null;
233
+ }
234
+ }
235
+ const launcher = `#!/bin/sh\n${remcpCliShimMarker}\nexec ${shellQuote(effectiveNode)} ${shellQuote(cliScript)} "$@"\n`;
236
+ fs.mkdirSync(path.dirname(shim), { recursive:true, mode:0o755 });
237
+ const temporary = `${shim}.${process.pid}.${Date.now()}.tmp`;
238
+ fs.writeFileSync(temporary, launcher, { mode:0o755 });
239
+ fs.chmodSync(temporary, 0o755);
240
+ fs.renameSync(temporary, shim);
241
+ upsertManagedPathBlock(interactiveShellProfile());
242
+ return shim;
243
+ }
244
+
245
+ function removeMacCliCommand() {
246
+ if (servicePlatform() !== 'darwin') return;
247
+ const shim = macCliShimPath();
248
+ try {
249
+ const existing = fs.readFileSync(shim, 'utf8');
250
+ if (existing.includes(remcpCliShimMarker)) fs.unlinkSync(shim);
251
+ } catch {}
252
+ removeManagedPathBlock(interactiveShellProfile());
253
+ }
254
+
161
255
  function macServicePlist(cliPath, nodePath = process.execPath) {
162
256
  // launchd wants an absolute path; a symlinked prefix that npm has not materialised yet (or a path
163
257
  // that is about to be replaced by the next install) must not abort the repair — a stale plist is
@@ -486,8 +580,10 @@ export function installPersistentAgent(config) {
486
580
  const cliScript = resolvedCliScript(cliPath);
487
581
  const nodePath = process.execPath;
488
582
  if (platform === 'linux') installLinuxService(cliPath, nodePath);
489
- else if (platform === 'darwin') installMacService(cliPath, { nodePath });
490
- else installWindowsService(cliPath);
583
+ else if (platform === 'darwin') {
584
+ installMacService(cliPath, { nodePath });
585
+ ensureMacCliCommand(config, { cliPath:cliScript, nodePath });
586
+ } else installWindowsService(cliPath);
491
587
  configurePostInstallAccess();
492
588
  saveConfig({ ...config, serviceInstalled: true, serviceCliPath: cliScript, serviceNodePath: nodePath });
493
589
  console.log('ReMCP is installed as a background service. Future updates: remcp update');
@@ -530,6 +626,7 @@ export function ensureServiceIfRecorded(config) {
530
626
  // installMacService either leaves an unchanged loaded job alone, bootstraps an unloaded job, or
531
627
  // hands a changed launcher to an independent transient launchd helper.
532
628
  const state = installMacService(cliPath, { restart: false, nodePath });
629
+ ensureMacCliCommand(config, { cliPath, nodePath });
533
630
  if (state === 'reload-scheduled') restartScheduled = macServiceLabel;
534
631
  } else if (platform === 'win32') installWindowsService(cliPath);
535
632
  // Upgrade the legacy inferred state only after the supervisor repair succeeded. A failed repair
@@ -586,6 +683,7 @@ export function uninstallPersistentService() {
586
683
  const domain = macLaunchDomain();
587
684
  spawnSync('launchctl', ['bootout', domain, macServiceFile], { stdio: 'ignore' });
588
685
  try { fs.unlinkSync(macServiceFile); } catch {}
686
+ removeMacCliCommand();
589
687
  } else if (platform === 'win32') {
590
688
  spawnSync('schtasks.exe', ['/End', '/TN', windowsTaskName], { stdio: 'ignore' });
591
689
  spawnSync('schtasks.exe', ['/Delete', '/TN', windowsTaskName, '/F'], { stdio: 'ignore' });
@@ -11,7 +11,7 @@ import { PACKAGE_NAME, VERSION } from '../version.mjs';
11
11
  import { loadConfig, saveConfig } from './config.mjs';
12
12
  import { installedVersion } from './doctor.mjs';
13
13
  import { linuxServiceFile, macServiceFile, officialOrigin } from './env.mjs';
14
- import { currentInstallationInfo, ensureServiceIfRecorded, installationVersionsAtCliPath, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
14
+ import { currentInstallationInfo, ensureMacCliCommand, ensureServiceIfRecorded, installationVersionsAtCliPath, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
15
15
 
16
16
  const UPDATE_DISCOVERY_TIMEOUT_MS = 5000;
17
17
 
@@ -232,6 +232,9 @@ export async function updateCommand(flags) {
232
232
  }
233
233
 
234
234
  if (alreadyCurrent) {
235
+ // `npx @remcp/remcp@latest update` is the recovery path when a Node-manager prefix was never
236
+ // exported into the user's shell. Even with no package delta, repair the stable macOS command.
237
+ ensureMacCliCommand(cfg);
235
238
  if (targets.persistRuntime) {
236
239
  saveConfig({ ...cfg, runtime:{ ...cfg.runtime, packageSpec:targets.runtimeSpec } });
237
240
  }
package/src/cli.mjs CHANGED
@@ -11,7 +11,7 @@ import { ensureMachineId, loadConfig, readJsonFile, saveConfig, setTelemetry, te
11
11
  import { assertRuntimeTrust, 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';
14
- import { currentInstallationInfo, installPersistentAgent, persistentServiceState, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, uninstallPersistentService } from './cli/service.mjs';
14
+ import { currentInstallationInfo, ensureMacCliCommand, installPersistentAgent, persistentServiceState, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, uninstallPersistentService } from './cli/service.mjs';
15
15
  import { run } from './cli/shell.mjs';
16
16
  import { updateCommand } from './cli/update.mjs';
17
17
  function parse(argv) {
@@ -97,6 +97,10 @@ export async function main(argv = process.argv.slice(2)) {
97
97
  || Boolean(process.env.INVOCATION_ID)
98
98
  || String(process.env.XPC_SERVICE_NAME || '').includes('com.remcp.agent');
99
99
  const cfg = rememberCurrentInstallation(loadConfig(), { service:supervised });
100
+ // A service can start from an absolute fnm/nvm Node path even when an interactive macOS shell
101
+ // cannot resolve the npm-global `remcp` binary. Refresh the stable user shim on every start so
102
+ // an auto-update repairs the next Terminal session without requiring a manual PATH edit.
103
+ ensureMacCliCommand(cfg);
100
104
  const service = persistentServiceState(cfg);
101
105
  if (!supervised && service.active) {
102
106
  console.log(`ReMCP is already running as a background service (${service.name}). Use \`remcp status\` to inspect it or \`remcp doctor\` to diagnose it.`);