@remcp/runtime 0.2.68 → 0.2.70

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/runtime",
3
- "version": "0.2.68",
3
+ "version": "0.2.70",
4
4
  "description": "First-party ReMCP local device runtime: filesystem, terminal, computer-use, browser, diagnostics and document tools over MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,10 +1,86 @@
1
1
  import process from 'node:process';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { existsSync, mkdirSync } from 'node:fs';
2
5
  import { resolveSafePath } from '../util.mjs';
3
- import { clamp, jsonResult, optionalString, requireEnum, unavailable } from './common.mjs';
6
+ import { clamp, commandExists, jsonResult, optionalString, requireEnum, spawnDetached, unavailable } from './common.mjs';
4
7
 
5
8
  const DEFAULT_ENDPOINT = 'http://127.0.0.1:9222';
6
9
  const MAX_DISCOVERY_BYTES = 8 * 1024 * 1024;
7
10
 
11
+ let browserLaunchPromise = null;
12
+
13
+ function browserExecutable() {
14
+ const configured = optionalString(process.env.REMCP_BROWSER_BINARY);
15
+ const candidates = configured ? [configured] : process.platform === 'darwin'
16
+ ? [
17
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
18
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
19
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
20
+ ]
21
+ : process.platform === 'win32'
22
+ ? [
23
+ path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'Google', 'Chrome', 'Application', 'chrome.exe'),
24
+ path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe'),
25
+ path.join(process.env.LOCALAPPDATA || '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
26
+ path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
27
+ ]
28
+ : ['/opt/google/chrome/chrome','/usr/bin/google-chrome','/usr/bin/google-chrome-stable','chromium','chromium-browser','microsoft-edge','microsoft-edge-stable'];
29
+ return candidates.find(candidate => path.isAbsolute(candidate) ? existsSync(candidate) : commandExists(candidate)) || null;
30
+ }
31
+
32
+ export function browserAutoLaunchAvailable() {
33
+ return Boolean(browserExecutable());
34
+ }
35
+
36
+ function browserDataDir() {
37
+ const configured = optionalString(process.env.REMCP_BROWSER_DATA_DIR);
38
+ if (configured) return configured;
39
+ if (process.platform === 'darwin') return path.join(os.homedir(), 'Library', 'Application Support', 'ReMCP', 'browser-cdp');
40
+ if (process.platform === 'win32') return path.join(process.env.LOCALAPPDATA || os.homedir(), 'ReMCP', 'browser-cdp');
41
+ return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'remcp', 'browser-cdp');
42
+ }
43
+
44
+ async function waitForBrowserEndpoint(timeoutMs = 5000) {
45
+ const deadline = Date.now() + timeoutMs;
46
+ do {
47
+ if (await browserCapabilityAvailable(undefined, 250)) return true;
48
+ await new Promise(resolve => setTimeout(resolve, 100));
49
+ } while (Date.now() < deadline);
50
+ return false;
51
+ }
52
+
53
+ async function ensureBrowserEndpoint(endpoint) {
54
+ if (endpoint || process.env.REMCP_CDP_URL) return;
55
+ if (await browserCapabilityAvailable(undefined, 250)) return;
56
+ if (!browserLaunchPromise) {
57
+ browserLaunchPromise = (async () => {
58
+ const executable = browserExecutable();
59
+ if (!executable) throw new Error('Browser control is unavailable: no supported Chrome, Edge, or Chromium executable was found');
60
+ const profile = browserDataDir();
61
+ mkdirSync(profile, { recursive:true, mode:0o700 });
62
+ const args = [
63
+ '--remote-debugging-address=127.0.0.1',
64
+ '--remote-debugging-port=9222',
65
+ `--user-data-dir=${profile}`,
66
+ '--no-first-run',
67
+ '--no-default-browser-check',
68
+ 'about:blank',
69
+ ];
70
+ if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) args.unshift('--headless=new');
71
+ spawnDetached(executable, args);
72
+ if (!await waitForBrowserEndpoint(5000)) throw new Error('ReMCP launched a browser but its local CDP endpoint did not become ready');
73
+ })().finally(() => { browserLaunchPromise = null; });
74
+ }
75
+ await browserLaunchPromise;
76
+ }
77
+
78
+ export async function browserControlAvailable(endpoint, timeoutMs = 500) {
79
+ if (await browserCapabilityAvailable(endpoint, timeoutMs)) return true;
80
+ if (endpoint || process.env.REMCP_CDP_URL) return false;
81
+ return browserAutoLaunchAvailable();
82
+ }
83
+
8
84
  function boundedText(value, max = 1000) {
9
85
  const text = String(value ?? '');
10
86
  return text.length <= max ? text : `${text.slice(0, max)}…`;
@@ -66,6 +142,7 @@ export async function browserCapabilityAvailable(endpoint, timeoutMs = 500) {
66
142
  }
67
143
 
68
144
  export async function listBrowserTargets(endpoint) {
145
+ await ensureBrowserEndpoint(endpoint);
69
146
  const list = await requestJson('/json/list', endpoint);
70
147
  return Array.isArray(list) ? list : [];
71
148
  }
@@ -242,6 +319,7 @@ export async function browserNavigate(args = {}) {
242
319
  const action = requireEnum(args.action || (url ? 'url' : 'reload'), 'action', ['url','new_tab','back','forward','reload']);
243
320
  if (['url','new_tab'].includes(action) && !url) throw new Error('url is required when action=url or new_tab');
244
321
  if (action === 'new_tab') {
322
+ await ensureBrowserEndpoint(args.endpoint);
245
323
  const created = await requestJson(`/json/new?${encodeURIComponent(url)}`, args.endpoint, { method:'PUT', timeout_ms:args.timeout_ms });
246
324
  return jsonResult({
247
325
  target_id:created?.id || null,
@@ -4,7 +4,7 @@ const JSON_OUTPUT_SCHEMA = {
4
4
  additionalProperties: true,
5
5
  description: 'Structured tool result. Array/scalar payloads are exposed under data for compatibility with MCP clients that require object-root output schemas.',
6
6
  };
7
- import { browserCapabilityAvailable, browserHandlers } from './browser.mjs';
7
+ import { browserControlAvailable, browserHandlers } from './browser.mjs';
8
8
  import { isWaylandSession } from '../screenshot-portal.mjs';
9
9
  import { waylandPortalCandidate } from '../wayland-remote-desktop.mjs';
10
10
  import { commandExists, runFile } from './common.mjs';
@@ -606,7 +606,7 @@ export async function capabilitySnapshot() {
606
606
  const wayland = linux && isWaylandSession();
607
607
  const portalInput = wayland && waylandPortalCandidate();
608
608
  const x11Input = linux && !wayland && (commandExists('wdotool') || commandExists('xdotool'));
609
- const browserCdp = await browserCapabilityAvailable(undefined, 500);
609
+ const browserCdp = await browserControlAvailable(undefined, 500);
610
610
  return {
611
611
  windows: win || mac || commandExists('wdotool') || commandExists('wmctrl') || (wayland && pyAtSpi),
612
612
  ui: win || mac || pyAtSpi,