@remcp/runtime 0.2.77 → 0.2.79
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 +7 -0
- package/package.json +1 -1
- package/src/extended/catalog.mjs +3 -3
- package/src/extended/desktop-linux.mjs +50 -7
- package/src/extended/desktop.mjs +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.79 — 2026-09-21
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- Native Wayland cursor telemetry fails closed instead of trusting stale XWayland `xdotool` coordinates; `computer_snapshot` keeps that capture non-fatal and structured.
|
|
9
|
+
- Regression coverage is headless-safe while still enforcing the snapshot normalization/error contract.
|
|
10
|
+
- Lockstep patch release with the ReMCP client/server at `0.2.79`.
|
|
11
|
+
|
|
5
12
|
## 0.2.77 — 2026-09-21
|
|
6
13
|
|
|
7
14
|
- Fixed `browser_action` typing so the documented `text` argument reaches CDP `Input.insertText` with Unicode intact; `value` and `text_value` remain compatible aliases.
|
package/package.json
CHANGED
package/src/extended/catalog.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
542
|
-
define('keyboard', 'Keyboard shortcut', 'Send a keyboard shortcut or key press to the active desktop application or an explicitly selected window. On Wayland
|
|
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, and explicit Wayland window targets are focused and verified before dispatch.', 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('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 target window id; the window is focused before auto/accessibility/clipboard/keys input and Wayland verifies focus before dispatch.'), title:s('Optional target window title substring; focused before input when no semantic element target is supplied.'), ...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
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
|
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
|
|
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' });
|
|
@@ -805,6 +843,7 @@ export async function keyboard(args = {}) {
|
|
|
805
843
|
return argv;
|
|
806
844
|
};
|
|
807
845
|
if (isWaylandSession()) {
|
|
846
|
+
await focusExplicitWaylandInputTarget(args);
|
|
808
847
|
if (backend !== 'portal' && commandExists('wtype')) {
|
|
809
848
|
const result = await runFile('wtype', wtypeArgs(), { label: 'keyboard', allowFailure: true, timeout: 5000 });
|
|
810
849
|
if (result.code === 0) return text(`Sent ${shortcut} via wtype.`);
|
|
@@ -842,6 +881,7 @@ export async function typeTextKeys(value, args = {}) {
|
|
|
842
881
|
const delay = clamp(args.delay_ms, 1, 0, 1000);
|
|
843
882
|
const backend = inputBackend(args);
|
|
844
883
|
if (isWaylandSession()) {
|
|
884
|
+
await focusExplicitWaylandInputTarget(args);
|
|
845
885
|
if (backend !== 'portal' && commandExists('wtype')) {
|
|
846
886
|
const result = await runFile('wtype', ['-d', String(delay), '--', String(value)], { label: 'type text', timeout: 30_000, allowFailure: true });
|
|
847
887
|
if (result.code === 0) return { ...result, backend: 'wtype' };
|
|
@@ -1063,6 +1103,9 @@ export async function scroll(args = {}) {
|
|
|
1063
1103
|
}
|
|
1064
1104
|
|
|
1065
1105
|
export async function cursorPosition() {
|
|
1106
|
+
if (isWaylandSession()) {
|
|
1107
|
+
unavailable('Cursor position', 'native Wayland does not expose an authoritative global cursor position; XWayland xdotool coordinates may be stale');
|
|
1108
|
+
}
|
|
1066
1109
|
if (!commandExists('xdotool')) unavailable('Cursor position', 'install xdotool');
|
|
1067
1110
|
const result = await runFile('xdotool', ['getmouselocation','--shell'], { label:'cursor position', allowFailure:true, timeout:2000 });
|
|
1068
1111
|
const x = Number(result.stdout.match(/^X=(-?\d+)$/m)?.[1]);
|
package/src/extended/desktop.mjs
CHANGED
|
@@ -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,6 +643,8 @@ 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);
|
|
647
|
+
if (explicitWindowTarget) await windowAction({ action:'focus', ...explicitWindowTarget });
|
|
629
648
|
const clear = args.clear === true;
|
|
630
649
|
const pressEnter = args.press_enter === true;
|
|
631
650
|
const caret = requireEnum(args.caret_position || 'idle', 'caret_position', ['start','idle','end']);
|