@remcp/runtime 0.2.78 → 0.2.80

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/CHANGELOG.md CHANGED
@@ -2,11 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- ## 0.2.78 — 2026-09-21
5
+ ## 0.2.80 — 2026-09-21
6
6
 
7
- - Native Wayland cursor telemetry no longer trusts stale XWayland `xdotool` coordinates; unavailable global cursor position is reported explicitly instead.
8
- - `computer_snapshot` degrades that capture to `cursor: {x:null,y:null}` with a non-fatal cursor error while preserving the rest of the snapshot.
9
- - Lockstep patch release with the ReMCP client/server at `0.2.78`.
7
+ - Wayland `type_text` can replace text in an inactive explicitly targeted native window without compositor focus when exactly one editable accessibility control is present.
8
+ - Ambiguous or missing editables are never guessed; key/clipboard/Enter fallbacks retain verified window-focus requirements.
9
+ - Tool descriptions now explain the semantic window-target path so agents do not request unnecessary focus before accessibility replacement.
10
+ - Lockstep patch release with the ReMCP client/server at `0.2.80`.
11
+
12
+ ## 0.2.79 — 2026-09-21
13
+
14
+ - Explicit Wayland keyboard and key-by-key text input now focuses and verifies the requested target window before dispatch, with bounded direct window cycling and verified fallbacks.
15
+ - Native Wayland cursor telemetry fails closed instead of trusting stale XWayland `xdotool` coordinates; `computer_snapshot` keeps that capture non-fatal and structured.
16
+ - Regression coverage is headless-safe while still enforcing the snapshot normalization/error contract.
17
+ - Lockstep patch release with the ReMCP client/server at `0.2.79`.
10
18
 
11
19
  ## 0.2.77 — 2026-09-21
12
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.78",
3
+ "version": "0.2.80",
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",
@@ -32,11 +32,11 @@
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
35
- "url": "git+https://github.com/antonbaider/remcp.git",
35
+ "url": "git+https://github.com/getremcp/remcp.git",
36
36
  "directory": "packages/runtime"
37
37
  },
38
38
  "bugs": {
39
- "url": "https://github.com/antonbaider/remcp/issues"
39
+ "url": "https://github.com/getremcp/remcp/issues"
40
40
  },
41
41
  "publishConfig": {
42
42
  "access": "public"
@@ -455,7 +455,7 @@ const inputBackendSelector = {
455
455
  backend: e(['auto','x11','portal'], 'Input backend preference. auto uses proven local backends and already-authorized portal input; portal explicitly requests XDG RemoteDesktop consent on Wayland; x11 forces XWayland/xdotool only for operations where read-back testing shows it is reliable. GNOME Wayland keyboard/text deliberately reject x11.'),
456
456
  };
457
457
  const inputWindowSelector = {
458
- window_id: s('Optional target window id returned by list_windows. Useful for deterministic XWayland keyboard input on Wayland.'),
458
+ window_id: s('Optional target window id returned by list_windows. On Wayland the target is focused and verified before keyboard/text input is dispatched.'),
459
459
  pid: n('Optional target process id.'),
460
460
  app: s('Optional target application/process name substring.'),
461
461
  title: s('Optional target window title substring.'),
@@ -538,8 +538,8 @@ export const extendedToolDefinitions = [
538
538
  define('ui_snapshot', 'UI accessibility snapshot', 'Read the active desktop accessibility/UI Automation tree with compact labels, semantic hierarchy, bounds, state and suggested actions. Default scope is the active application/window; scope=desktop requests the full desktop tree. Explicit pid/app/window_title scopes override the active default. browser_dom asks the OS accessibility provider for the web-document subtree when available.', o({ scope:e(['active','desktop'],'Default active. Use desktop only when cross-application discovery is required.'), pid:n('Optional owning process id scope.'), app:s('Optional application/process name substring scope.'), window_title:s('Optional top-level accessibility window/frame title scope.'), browser_dom:b('Prefer the native browser document accessibility subtree instead of browser chrome when supported.'), semantic_tree:b('Include compact semantic hierarchy text; default true.'), max_nodes:n('Maximum nodes; default 500, maximum 5000.'), max_depth:n('Maximum traversal depth; default 8, maximum 32.') }), readOnlyLive, desktopHandlers.ui_snapshot, ['ui']),
539
539
  define('ui_find', 'Find UI elements', 'Find native accessible desktop elements by label/id/name/role/AutomationId. Reuses the latest five-second semantic snapshot for fast repeated targeting; refresh=true forces a new OS accessibility capture.', o({ ...uiSelector, refresh:b('Force a fresh accessibility capture instead of the short-lived snapshot cache.'), limit:n('Maximum matches; default 20.'), max_nodes:n(), max_depth:n() }), readOnlyLive, desktopHandlers.ui_find, ['ui']),
540
540
  define('ui_action', 'Act on UI element', 'Act on a native accessibility/UI Automation element by id, label or semantic selector. Supports invoke/focus/value/toggle/selection, expand-collapse, range values and scroll-into-view before falling back to coordinates.', o({ action:e(['click','invoke','focus','set_value','select','toggle','expand','collapse','scroll_into_view','set_range_value','add_to_selection','remove_from_selection']), ...uiSelector, value:{description:'New text or numeric range value.'} }, ['action']), openMutating, desktopHandlers.ui_action, ['ui']),
541
- define('type_text', 'Type text', 'Enter Unicode text into a native semantic accessibility target or focused native control. Auto prefers verified accessibility editing; explicit semantic targets fail safely when the platform rejects the write instead of claiming an unverified paste. Use browser_action for Chromium page DOM. Clipboard/keys fallbacks remain available for focused/window-level input when appropriate.', o({ text:s('Text to enter.'), method:e(['auto','accessibility','clipboard','keys']), clear:b('Clear existing text before input when keyboard/paste fallback is used.'), caret_position:e(['start','idle','end']), press_enter:b('Press Enter after typing.'), delay_ms:n('Per-key delay for method=keys.'), ...uiSelector, window_id:s('Optional XWayland target window id for clipboard/keys methods.'), title:s('Optional target window title substring for clipboard/keys methods.'), ...inputBackendSelector }, ['text']), openMutating, desktopHandlers.type_text, ['input']),
542
- define('keyboard', 'Keyboard shortcut', 'Send a keyboard shortcut or key press to the active desktop application or an explicitly selected window. On Wayland deterministic XWayland input should use window_id/pid/app/title; backend=portal explicitly requests the consent-backed XDG RemoteDesktop path.', o({ shortcut:s(), key:s(), keys:{ type:'array', items:{type:'string'} }, ...inputWindowSelector, ...inputBackendSelector }), openMutating, desktopHandlers.keyboard, ['input']),
541
+ define('type_text', 'Type text', 'Enter Unicode text into a native semantic accessibility target or focused native control. Auto prefers verified accessibility editing; with clear=true and an explicit window target it can replace the value of the single editable accessibility control in that window without compositor focus. Explicit semantic targets fail safely when the platform rejects the write instead of claiming an unverified paste. Use browser_action for Chromium page DOM. Clipboard/keys/Enter fallbacks still require verified window focus on Wayland.', o({ text:s('Text to enter.'), method:e(['auto','accessibility','clipboard','keys'], 'Default auto prefers verified accessibility editing and falls back to clipboard paste. keys sends physical key events and therefore follows the active keyboard layout.'), clear:b('Replace existing text for semantic/window-scoped accessibility input, or clear it before keyboard/paste fallback.'), caret_position:e(['start','idle','end']), press_enter:b('Press Enter after typing; physical Enter delivery requires verified window focus.'), delay_ms:n('Per-key delay for method=keys.'), ...uiSelector, window_id:s('Optional target window id. With auto/accessibility + clear=true, a unique editable control may be updated semantically without focus; keyboard/clipboard fallbacks verify focus before dispatch.'), title:s('Optional target window title substring. With auto/accessibility + clear=true, a unique editable control may be updated semantically without compositor focus.'), ...inputBackendSelector }, ['text']), openMutating, desktopHandlers.type_text, ['input']),
542
+ define('keyboard', 'Keyboard shortcut', 'Send a keyboard shortcut or key press to the active desktop application or an explicitly selected window. On Wayland, window_id/pid/app/title targets are focused and verified before input is dispatched; backend=portal explicitly requests the consent-backed XDG RemoteDesktop path.', o({ shortcut:s(), key:s(), keys:{ type:'array', items:{type:'string'} }, ...inputWindowSelector, ...inputBackendSelector }), openMutating, desktopHandlers.keyboard, ['input']),
543
543
  define('pointer', 'Pointer action', 'Move the pointer or perform click, double-click, right-click, mouse-down, or mouse-up at desktop screen coordinates. Wayland supports XWayland fallback and an explicitly consented XDG RemoteDesktop backend.', o({ action:e(['move','click','double_click','right_click','down','up']), x:n(), y:n(), button:e(['left','right','middle']), ...inputBackendSelector }, ['action']), openMutating, desktopHandlers.pointer, ['pointer']),
544
544
  define('drag_drop', 'Drag and drop', 'Drag between desktop coordinates or semantic UI element IDs returned by ui_snapshot/ui_find. Element IDs are resolved to current accessibility bounds before input. GNOME Wayland requires the consent-backed Remote Desktop portal for real drag delivery; unreliable XTEST/xdotool drag is rejected.', o({ from_x:n(), from_y:n(), to_x:n(), to_y:n(), from_id:s('Source UI element id; use instead of from_x/from_y.'), to_id:s('Destination UI element id; use instead of to_x/to_y.'), button:e(['left','right','middle']), hold_ms:n(), duration_ms:n(), ...inputBackendSelector }), openMutating, desktopHandlers.drag_drop, ['drag']),
545
545
  define('scroll', 'Scroll', 'Scroll vertically or horizontally using native wheel input. Target a semantic UI element/label or x/y first so the wheel is delivered to the intended region; direction+wheel_times is a convenience over normalized deltas.', o({ delta_x:n('Horizontal delta; positive means right.'), delta_y:n('Vertical delta; positive means down.'), delta:n('Alias for delta_y.'), direction:e(['up','down','left','right']), wheel_times:n('Number of 120-unit wheel steps when direction is used; 1..50.'), x:n(), y:n(), ...uiSelector, ...inputBackendSelector }), mutatingNonDestructive, desktopHandlers.scroll, ['pointer']),
@@ -252,6 +252,36 @@ async function focusInputWindow(windowId) {
252
252
  if (result.code === 0) await new Promise(resolve => setTimeout(resolve, 40));
253
253
  }
254
254
 
255
+ function explicitInputWindowSelector(args = {}) {
256
+ const dedicatedWindowId = optionalString(args.window_id || args.windowId);
257
+ const dedicatedTitle = optionalString(args.title);
258
+ const hasSemanticElementTarget = Boolean(
259
+ optionalString(args.id)
260
+ || optionalString(args.name)
261
+ || optionalString(args.role)
262
+ || optionalString(args.automation_id || args.automationId)
263
+ );
264
+ if (!dedicatedWindowId && !dedicatedTitle && hasSemanticElementTarget) return null;
265
+ const pid = Number.isInteger(Number(args.pid)) ? Number(args.pid) : null;
266
+ const app = optionalString(args.app);
267
+ const scopedTitle = optionalString(args.window_title || args.windowTitle);
268
+ if (!dedicatedWindowId && !dedicatedTitle && pid == null && !app && !scopedTitle) return null;
269
+ return {
270
+ ...(dedicatedWindowId ? { id:dedicatedWindowId } : {}),
271
+ ...(pid != null ? { pid } : {}),
272
+ ...(app ? { app } : {}),
273
+ ...((dedicatedTitle || scopedTitle) ? { title:dedicatedTitle || scopedTitle } : {}),
274
+ };
275
+ }
276
+
277
+ async function focusExplicitWaylandInputTarget(args = {}) {
278
+ const selector = explicitInputWindowSelector(args);
279
+ if (!selector) return null;
280
+ const row = await windowMatch(selector);
281
+ await windowAction({ action:'focus', id:row.id, backend:inputBackend(args) });
282
+ return row;
283
+ }
284
+
255
285
  async function nativeWaylandWindowAction(row, action, args) {
256
286
  const backend = inputBackend(args);
257
287
  if (backend === 'x11') unavailable('Native Wayland window action', 'use backend=portal for compositor-managed windows');
@@ -350,23 +380,31 @@ export async function windowAction(args) {
350
380
  if (focusState.exact) return text('Window action focus already matched the target window.');
351
381
 
352
382
  const rows = JSON.parse((await listWindows()).content?.[0]?.text || '[]');
353
- const appCycles = Math.min(30, Math.max(4, new Set(rows.map(item => Number(item.pid) || String(item.app || ''))).size + 2));
383
+ const windowCycles = Math.min(60, Math.max(8, rows.length + 4));
384
+ for (let index = 0; index < windowCycles; index += 1) {
385
+ await portalShortcut('ALT+ESC', portalOptions);
386
+ await new Promise(resolve => setTimeout(resolve, 140));
387
+ focusState = await targetAccessibilityFocus(row);
388
+ if (focusState.exact) return text('Window action focus completed via verified Wayland Alt+Esc.');
389
+ }
390
+
391
+ const appCycles = Math.min(12, Math.max(3, new Set(rows.map(item => Number(item.pid) || String(item.app || ''))).size + 1));
354
392
  for (let index = 0; index < appCycles; index += 1) {
355
393
  await portalShortcut('ALT+TAB', portalOptions);
356
- await new Promise(resolve => setTimeout(resolve, 120));
394
+ await new Promise(resolve => setTimeout(resolve, 160));
357
395
  focusState = await targetAccessibilityFocus(row);
358
- if (focusState.exact) return text('Window action focus completed via verified Wayland Alt+Tab.');
396
+ if (focusState.exact) return text('Window action focus completed via verified Wayland Alt+Tab fallback.');
359
397
  if (focusState.active) {
360
398
  const sameAppWindows = rows.filter(item => Number(item.pid) === Number(row.pid)).length;
361
- for (let windowIndex = 0; windowIndex < Math.min(12, Math.max(2, sameAppWindows + 2)); windowIndex += 1) {
399
+ for (let windowIndex = 0; windowIndex < Math.min(8, Math.max(2, sameAppWindows + 1)); windowIndex += 1) {
362
400
  await portalShortcut('ALT+`', portalOptions);
363
- await new Promise(resolve => setTimeout(resolve, 100));
401
+ await new Promise(resolve => setTimeout(resolve, 120));
364
402
  focusState = await targetAccessibilityFocus(row);
365
- if (focusState.exact) return text('Window action focus completed via verified Wayland window cycling.');
403
+ if (focusState.exact) return text('Window action focus completed via verified Wayland same-app fallback.');
366
404
  }
367
405
  }
368
406
  }
369
- throw new Error(`Could not verify focus for ${row.title || row.app || row.id} after cycling Wayland applications`);
407
+ throw new Error(`Could not verify focus for ${row.title || row.app || row.id} after bounded Wayland window cycling`);
370
408
  }
371
409
  if (!commandExists('wmctrl')) unavailable('Window focus', 'install wmctrl');
372
410
  await runFile('wmctrl', ['-ia', id], { label: 'window focus' });
@@ -719,6 +757,37 @@ export async function clipboard(args = {}) {
719
757
  unavailable('Clipboard read', `${failures.join('; ') || 'install wl-clipboard or xclip'}`);
720
758
  }
721
759
 
760
+ export async function typeTextWindowTarget(value, args = {}) {
761
+ const row = await windowMatch(args);
762
+ if (!row?.pid) return null;
763
+ const snapshot = await uiSnapshot({
764
+ pid:row.pid,
765
+ window_title:row.title || undefined,
766
+ max_nodes:2200,
767
+ max_depth:24,
768
+ });
769
+ let payload = null;
770
+ try { payload = JSON.parse(snapshot.content?.[0]?.text || '{}'); } catch {}
771
+ const nodes = Array.isArray(payload) ? payload : payload?.nodes || [];
772
+ const editableRoles = /(^|\b)(text|entry|textbox|edit|text field|search box|password text)(\b|$)/i;
773
+ const candidates = nodes.filter(node => node?.id && editableRoles.test(String(node.role || '')));
774
+ if (candidates.length !== 1) return null;
775
+
776
+ const target = candidates[0];
777
+ const result = await uiAction({ action:'set_value', id:target.id, value:String(value) });
778
+ let action = result.content?.[0]?.text || '';
779
+ try { action = JSON.parse(action); } catch {}
780
+ return {
781
+ id:target.id,
782
+ pid:row.pid,
783
+ app:row.app || target.app || '',
784
+ window:row.title || target.window || '',
785
+ role:target.role || '',
786
+ name:target.name || '',
787
+ action,
788
+ };
789
+ }
790
+
722
791
  export async function typeTextFocused(value) {
723
792
  if (!commandExists('python3')) return null;
724
793
  const script = String.raw`import gi,json,sys
@@ -805,6 +874,7 @@ export async function keyboard(args = {}) {
805
874
  return argv;
806
875
  };
807
876
  if (isWaylandSession()) {
877
+ await focusExplicitWaylandInputTarget(args);
808
878
  if (backend !== 'portal' && commandExists('wtype')) {
809
879
  const result = await runFile('wtype', wtypeArgs(), { label: 'keyboard', allowFailure: true, timeout: 5000 });
810
880
  if (result.code === 0) return text(`Sent ${shortcut} via wtype.`);
@@ -842,6 +912,7 @@ export async function typeTextKeys(value, args = {}) {
842
912
  const delay = clamp(args.delay_ms, 1, 0, 1000);
843
913
  const backend = inputBackend(args);
844
914
  if (isWaylandSession()) {
915
+ await focusExplicitWaylandInputTarget(args);
845
916
  if (backend !== 'portal' && commandExists('wtype')) {
846
917
  const result = await runFile('wtype', ['-d', String(delay), '--', String(value)], { label: 'type text', timeout: 30_000, allowFailure: true });
847
918
  if (result.code === 0) return { ...result, backend: 'wtype' };
@@ -618,6 +618,23 @@ export async function waitForUi(args = {}) {
618
618
  return jsonResult({ state:'timeout', wanted:state, matched:nodes.length, attempts, elapsed_ms:Date.now() - startedAt, nodes, ...(state === 'changed' ? { before:baseline } : {}) });
619
619
  }
620
620
 
621
+ function explicitTypeTextWindowTarget(args = {}, hasElementSelector = false) {
622
+ const windowId = optionalString(args.window_id || args.windowId);
623
+ const title = optionalString(args.title);
624
+ if (!windowId && !title && hasElementSelector) return null;
625
+ const pid = Number.isInteger(Number(args.pid)) ? Number(args.pid) : null;
626
+ const app = optionalString(args.app);
627
+ const scopedTitle = optionalString(args.window_title || args.windowTitle);
628
+ if (!windowId && !title && pid == null && !app && !scopedTitle) return null;
629
+ return {
630
+ ...(windowId ? { id:windowId } : {}),
631
+ ...(pid != null ? { pid } : {}),
632
+ ...(app ? { app } : {}),
633
+ ...((title || scopedTitle) ? { title:title || scopedTitle } : {}),
634
+ ...(args.backend ? { backend:args.backend } : {}),
635
+ };
636
+ }
637
+
621
638
  export async function typeText(args = {}) {
622
639
  const value = String(args.text ?? '');
623
640
  const method = requireEnum(args.method || 'auto', 'method', ['auto','accessibility','clipboard','keys']);
@@ -626,10 +643,25 @@ export async function typeText(args = {}) {
626
643
  optionalString(resolvedArgs.id) || optionalString(resolvedArgs.name) || optionalString(resolvedArgs.role) ||
627
644
  optionalString(resolvedArgs.automation_id || resolvedArgs.automationId)
628
645
  );
646
+ const explicitWindowTarget = explicitTypeTextWindowTarget(resolvedArgs, hasElementSelector);
629
647
  const clear = args.clear === true;
630
648
  const pressEnter = args.press_enter === true;
631
649
  const caret = requireEnum(args.caret_position || 'idle', 'caret_position', ['start','idle','end']);
632
650
 
651
+ if (
652
+ explicitWindowTarget && !hasElementSelector && clear && !pressEnter
653
+ && (method === 'auto' || method === 'accessibility')
654
+ && typeof adapter.typeTextWindowTarget === 'function'
655
+ ) {
656
+ const semantic = await adapter.typeTextWindowTarget(value, resolvedArgs);
657
+ if (semantic) {
658
+ return jsonResult({ length:value.length, method:'accessibility', target:semantic, clear:true, press_enter:false });
659
+ }
660
+ if (method === 'accessibility') {
661
+ throw new Error('Accessibility-only text input needs exactly one editable control in the targeted window when no element selector is provided');
662
+ }
663
+ }
664
+
633
665
  if ((method === 'auto' || method === 'accessibility') && hasElementSelector) {
634
666
  try {
635
667
  const result = await uiAction({ ...resolvedArgs, action:'set_value', value });
@@ -652,7 +684,10 @@ export async function typeText(args = {}) {
652
684
  throw new Error(`Semantic text target rejected accessibility input. Refresh the UI target or use browser_action for browser page content. ${detail}`);
653
685
  }
654
686
  }
655
- if ((method === 'auto' || method === 'accessibility') && !hasElementSelector && typeof adapter.typeTextFocused === 'function' && !clear && caret === 'idle') {
687
+ if (
688
+ !explicitWindowTarget && (method === 'auto' || method === 'accessibility') && !hasElementSelector
689
+ && typeof adapter.typeTextFocused === 'function' && !clear && caret === 'idle'
690
+ ) {
656
691
  const semantic = await adapter.typeTextFocused(value, resolvedArgs);
657
692
  if (semantic) {
658
693
  if (pressEnter) await keyboard({ ...resolvedArgs, key:'ENTER' });
@@ -661,9 +696,10 @@ export async function typeText(args = {}) {
661
696
  if (method === 'accessibility') throw new Error('No focused editable accessibility element is available');
662
697
  }
663
698
  if (method === 'accessibility') {
664
- throw new Error('Accessibility-only text input requires a semantic element target for replacement, or a focused editable control with clear=false and caret_position=idle');
699
+ throw new Error('Accessibility-only text input requires a semantic element target, one editable control in the targeted window with clear=true, or a focused editable control with clear=false and caret_position=idle');
665
700
  }
666
701
 
702
+ if (explicitWindowTarget) await windowAction({ action:'focus', ...explicitWindowTarget });
667
703
  if (hasElementSelector) await uiAction({ ...resolvedArgs, action:'focus' });
668
704
  if (clear) {
669
705
  await keyboard({ ...resolvedArgs, shortcut:process.platform === 'darwin' ? 'CMD+A' : 'CTRL+A' });