@remcp/runtime 0.2.79 → 0.2.81
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 +14 -0
- package/package.json +3 -3
- package/src/extended/catalog.mjs +3 -3
- package/src/extended/desktop-linux.mjs +31 -0
- package/src/extended/desktop.mjs +20 -3
- package/src/extended/diagnostics.mjs +22 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.81 — 2026-09-21
|
|
6
|
+
|
|
7
|
+
- `record_screen` is advertised only when the current OS/session has a recorder backend the runtime can actually invoke.
|
|
8
|
+
- macOS/Windows require `ffmpeg`; Linux Wayland requires `wf-recorder + timeout`; Linux X11 requires `ffmpeg`.
|
|
9
|
+
- Availability errors are platform-specific, and regression coverage locks the capability matrix.
|
|
10
|
+
- Lockstep patch release with the ReMCP client/server at `0.2.81`.
|
|
11
|
+
|
|
12
|
+
## 0.2.80 — 2026-09-21
|
|
13
|
+
|
|
14
|
+
- Wayland `type_text` can replace text in an inactive explicitly targeted native window without compositor focus when exactly one editable accessibility control is present.
|
|
15
|
+
- Ambiguous or missing editables are never guessed; key/clipboard/Enter fallbacks retain verified window-focus requirements.
|
|
16
|
+
- Tool descriptions now explain the semantic window-target path so agents do not request unnecessary focus before accessibility replacement.
|
|
17
|
+
- Lockstep patch release with the ReMCP client/server at `0.2.80`.
|
|
18
|
+
|
|
5
19
|
## 0.2.79 — 2026-09-21
|
|
6
20
|
|
|
7
21
|
- 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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remcp/runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.81",
|
|
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/
|
|
35
|
+
"url": "git+https://github.com/getremcp/remcp.git",
|
|
36
36
|
"directory": "packages/runtime"
|
|
37
37
|
},
|
|
38
38
|
"bugs": {
|
|
39
|
-
"url": "https://github.com/
|
|
39
|
+
"url": "https://github.com/getremcp/remcp/issues"
|
|
40
40
|
},
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public"
|
package/src/extended/catalog.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { isWaylandSession } from '../screenshot-portal.mjs';
|
|
|
9
9
|
import { waylandPortalCandidate } from '../wayland-remote-desktop.mjs';
|
|
10
10
|
import { commandExists, runFile } from './common.mjs';
|
|
11
11
|
import { desktopHandlers } from './desktop.mjs';
|
|
12
|
-
import { diagnosticHandlers } from './diagnostics.mjs';
|
|
12
|
+
import { diagnosticHandlers, recordScreenAvailable } from './diagnostics.mjs';
|
|
13
13
|
import { documentHandlers } from './documents.mjs';
|
|
14
14
|
|
|
15
15
|
const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
@@ -538,7 +538,7 @@ 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
|
|
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
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']),
|
|
@@ -623,7 +623,7 @@ export async function capabilitySnapshot() {
|
|
|
623
623
|
apps: win || mac || commandExists('dpkg-query') || commandExists('rpm'),
|
|
624
624
|
audio: win || mac || commandExists('wpctl') || commandExists('pactl') || commandExists('amixer'),
|
|
625
625
|
power: win || mac || commandExists('systemctl') || commandExists('loginctl'),
|
|
626
|
-
record:
|
|
626
|
+
record: recordScreenAvailable({ platform:process.platform, wayland }),
|
|
627
627
|
ooxml: win || (commandExists('zip') && commandExists('unzip')),
|
|
628
628
|
};
|
|
629
629
|
}
|
|
@@ -757,6 +757,37 @@ export async function clipboard(args = {}) {
|
|
|
757
757
|
unavailable('Clipboard read', `${failures.join('; ') || 'install wl-clipboard or xclip'}`);
|
|
758
758
|
}
|
|
759
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
|
+
|
|
760
791
|
export async function typeTextFocused(value) {
|
|
761
792
|
if (!commandExists('python3')) return null;
|
|
762
793
|
const script = String.raw`import gi,json,sys
|
package/src/extended/desktop.mjs
CHANGED
|
@@ -644,11 +644,24 @@ export async function typeText(args = {}) {
|
|
|
644
644
|
optionalString(resolvedArgs.automation_id || resolvedArgs.automationId)
|
|
645
645
|
);
|
|
646
646
|
const explicitWindowTarget = explicitTypeTextWindowTarget(resolvedArgs, hasElementSelector);
|
|
647
|
-
if (explicitWindowTarget) await windowAction({ action:'focus', ...explicitWindowTarget });
|
|
648
647
|
const clear = args.clear === true;
|
|
649
648
|
const pressEnter = args.press_enter === true;
|
|
650
649
|
const caret = requireEnum(args.caret_position || 'idle', 'caret_position', ['start','idle','end']);
|
|
651
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
|
+
|
|
652
665
|
if ((method === 'auto' || method === 'accessibility') && hasElementSelector) {
|
|
653
666
|
try {
|
|
654
667
|
const result = await uiAction({ ...resolvedArgs, action:'set_value', value });
|
|
@@ -671,7 +684,10 @@ export async function typeText(args = {}) {
|
|
|
671
684
|
throw new Error(`Semantic text target rejected accessibility input. Refresh the UI target or use browser_action for browser page content. ${detail}`);
|
|
672
685
|
}
|
|
673
686
|
}
|
|
674
|
-
if (
|
|
687
|
+
if (
|
|
688
|
+
!explicitWindowTarget && (method === 'auto' || method === 'accessibility') && !hasElementSelector
|
|
689
|
+
&& typeof adapter.typeTextFocused === 'function' && !clear && caret === 'idle'
|
|
690
|
+
) {
|
|
675
691
|
const semantic = await adapter.typeTextFocused(value, resolvedArgs);
|
|
676
692
|
if (semantic) {
|
|
677
693
|
if (pressEnter) await keyboard({ ...resolvedArgs, key:'ENTER' });
|
|
@@ -680,9 +696,10 @@ export async function typeText(args = {}) {
|
|
|
680
696
|
if (method === 'accessibility') throw new Error('No focused editable accessibility element is available');
|
|
681
697
|
}
|
|
682
698
|
if (method === 'accessibility') {
|
|
683
|
-
throw new Error('Accessibility-only text input requires a semantic element target
|
|
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');
|
|
684
700
|
}
|
|
685
701
|
|
|
702
|
+
if (explicitWindowTarget) await windowAction({ action:'focus', ...explicitWindowTarget });
|
|
686
703
|
if (hasElementSelector) await uiAction({ ...resolvedArgs, action:'focus' });
|
|
687
704
|
if (clear) {
|
|
688
705
|
await keyboard({ ...resolvedArgs, shortcut:process.platform === 'darwin' ? 'CMD+A' : 'CTRL+A' });
|
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
import { stat } from 'node:fs/promises';
|
|
7
7
|
import { assertAllowedCommand } from '../policy.mjs';
|
|
8
|
+
import { isWaylandSession } from '../screenshot-portal.mjs';
|
|
8
9
|
import { resolveSafePath, text } from '../util.mjs';
|
|
9
10
|
import {
|
|
10
11
|
clamp,
|
|
@@ -243,6 +244,19 @@ export async function powerAction(args) {
|
|
|
243
244
|
return text(`${policy.note ? `${policy.note}\n` : ''}Power action ${action} requested.`);
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
export function recordScreenAvailable({
|
|
248
|
+
platform = process.platform,
|
|
249
|
+
wayland = platform === 'linux' ? isWaylandSession() : false,
|
|
250
|
+
commandExistsFn = commandExists,
|
|
251
|
+
} = {}) {
|
|
252
|
+
if (platform === 'linux') {
|
|
253
|
+
if (wayland) return commandExistsFn('wf-recorder') && commandExistsFn('timeout');
|
|
254
|
+
return commandExistsFn('ffmpeg');
|
|
255
|
+
}
|
|
256
|
+
if (platform === 'darwin' || platform === 'win32') return commandExistsFn('ffmpeg');
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
246
260
|
export async function recordScreen(args) {
|
|
247
261
|
const seconds = clamp(args.duration_seconds, 5, 1, 120);
|
|
248
262
|
const fps = clamp(args.fps, 15, 1, 60);
|
|
@@ -251,7 +265,14 @@ export async function recordScreen(args) {
|
|
|
251
265
|
const result = await runFile('timeout', ['--signal=INT', `${seconds}s`, 'wf-recorder', '-f',destination,'-r',String(fps),'-c','libx264'], { label:'screen recording', timeout:(seconds+10)*1000, allowFailure:true });
|
|
252
266
|
if (![0, 124, 130].includes(Number(result.code))) throw new Error(result.stderr.trim() || `wf-recorder exited ${result.code}`);
|
|
253
267
|
} else {
|
|
254
|
-
if (
|
|
268
|
+
if (process.platform === 'linux' && isWaylandSession()) {
|
|
269
|
+
unavailable('Screen recording', 'wf-recorder and timeout are required on Wayland');
|
|
270
|
+
}
|
|
271
|
+
if (!commandExists('ffmpeg')) {
|
|
272
|
+
if (process.platform === 'darwin') unavailable('Screen recording', 'ffmpeg is required on macOS');
|
|
273
|
+
if (process.platform === 'win32') unavailable('Screen recording', 'ffmpeg is required on Windows');
|
|
274
|
+
unavailable('Screen recording', 'ffmpeg is required on X11');
|
|
275
|
+
}
|
|
255
276
|
let argv;
|
|
256
277
|
if (process.platform === 'win32') argv=['-y','-f','gdigrab','-framerate',String(fps),'-i','desktop','-t',String(seconds),'-pix_fmt','yuv420p',destination];
|
|
257
278
|
else if (process.platform === 'darwin') argv=['-y','-f','avfoundation','-framerate',String(fps),'-i','1:none','-t',String(seconds),'-pix_fmt','yuv420p',destination];
|