@remcp/runtime 0.2.128 → 0.2.130
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 +13 -0
- package/README.md +7 -1
- package/package.json +1 -1
- package/src/extended/catalog.mjs +35 -17
- package/src/extended/common.mjs +9 -0
- package/src/extended/desktop-linux.mjs +3 -2
- package/src/extended/desktop.mjs +15 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.130 — 2026-10-02
|
|
4
|
+
|
|
5
|
+
- `computer_snapshot` now skips desktop-only probes on headless Linux and returns stable empty/unavailable desktop fields without false error noise. The shared `desktopSessionAvailable()` helper keeps this behavior aligned with live capability discovery.
|
|
6
|
+
- Linux `screenshot_region` advertising now uses the same backend resolver as execution: `grim` is Wayland-only, portal capture requires a Wayland portal candidate plus `ffmpeg`, GNOME capture requires `gnome-screenshot` + `ffmpeg`, and X11 uses ImageMagick `import`.
|
|
7
|
+
- Wayland + `ffmpeg` alone no longer exposes a screenshot tool that can only fail at runtime. Tool names and MCP schemas remain unchanged.
|
|
8
|
+
|
|
9
|
+
## 0.2.129 — 2026-10-02
|
|
10
|
+
|
|
11
|
+
- Linux Desktop capability discovery now requires an active `DISPLAY` or `WAYLAND_DISPLAY`, preventing headless hosts from advertising desktop actions just because helper binaries are installed.
|
|
12
|
+
- `launch_app` requires `desktop_session`; `open_path` and `reveal_path` require `desktop_open`, which additionally checks `xdg-open` on Linux.
|
|
13
|
+
- AT-SPI/Wayland portal probes are skipped entirely on headless Linux. Browser/CDP and non-desktop capability groups remain independent.
|
|
14
|
+
- Tool names and MCP schemas remain unchanged.
|
|
15
|
+
|
|
3
16
|
## 0.2.128 — 2026-10-02
|
|
4
17
|
|
|
5
18
|
- Linux `power_action` is advertised only when both `loginctl` and `systemctl` exist, matching the full lock/sleep/restart/shutdown schema instead of exposing a partially unusable tool.
|
package/README.md
CHANGED
|
@@ -77,7 +77,13 @@ system tools such as qpdf/poppler when present. PDF text reading uses the built-
|
|
|
77
77
|
A running device can advertise a smaller capability-aware subset; the runtime declares
|
|
78
78
|
`tools.listChanged`, emits `notifications/tools/list_changed` when dynamic capabilities change, and the
|
|
79
79
|
agent forwards the refreshed tool names to the hosted relay. Browser/CDP tools are the primary dynamic
|
|
80
|
-
example: they are enabled only while the runtime can use a supported loopback browser endpoint.
|
|
80
|
+
example: they are enabled only while the runtime can use a supported loopback browser endpoint. On Linux,
|
|
81
|
+
Desktop capabilities additionally require an active graphical session (`DISPLAY` or `WAYLAND_DISPLAY`):
|
|
82
|
+
headless hosts do not advertise window/UI/input/clipboard/display/screenshot/launch/open/reveal/notification
|
|
83
|
+
operations merely because desktop helper binaries happen to be installed. The universal `computer_snapshot`
|
|
84
|
+
remains available on those hosts but skips unavailable desktop probes instead of reporting synthetic backend
|
|
85
|
+
errors. On Linux, `screenshot_region` is advertised only when the current session has a real matching capture
|
|
86
|
+
backend (Wayland grim/GNOME/portal path or X11 ImageMagick), not merely because `ffmpeg` is installed.
|
|
81
87
|
|
|
82
88
|
The generated hosted reference covers the current **10 hosted production definitions**, all model-visible
|
|
83
89
|
and routing to the 83 granular runtime operations described above; the optional widget-enabled surface is 15.
|
package/package.json
CHANGED
package/src/extended/catalog.mjs
CHANGED
|
@@ -7,8 +7,9 @@ const JSON_OUTPUT_SCHEMA = {
|
|
|
7
7
|
import { browserControlAvailable, browserHandlers, browserRemoteEnabled } from './browser.mjs';
|
|
8
8
|
import { isWaylandSession } from '../screenshot-portal.mjs';
|
|
9
9
|
import { waylandPortalCandidate } from '../wayland-remote-desktop.mjs';
|
|
10
|
-
import { commandExists, runFile } from './common.mjs';
|
|
10
|
+
import { commandExists, desktopSessionAvailable, runFile } from './common.mjs';
|
|
11
11
|
import { desktopHandlers } from './desktop.mjs';
|
|
12
|
+
import { regionScreenshotBackends } from './desktop-linux.mjs';
|
|
12
13
|
import { diagnosticHandlers, gnomeScreencastSupported, recordScreenAvailable } from './diagnostics.mjs';
|
|
13
14
|
import { documentHandlers } from './documents.mjs';
|
|
14
15
|
|
|
@@ -534,7 +535,7 @@ export const extendedToolDefinitions = [
|
|
|
534
535
|
),
|
|
535
536
|
define('list_windows', 'List windows', 'List visible top-level desktop windows with stable-enough identifiers, PID, application name, title, bounds, and the monitor containing the largest part of each window when display geometry is available.', o({ include_monitor:b('Enrich each window with monitor and monitor_index; default true.') }), readOnlyLive, desktopHandlers.list_windows, ['windows']),
|
|
536
537
|
define('window_action', 'Window action', 'Focus, minimize, maximize, restore, move, resize, or close a top-level desktop window selected by id, pid, application name, or title. Native GNOME Wayland focus uses AT-SPI; compositor window-manager actions use the consent-backed XDG RemoteDesktop input path.', o({ action:e(['focus','minimize','maximize','restore','move','resize','move_resize','close']), ...windowSelector, x:n(), y:n(), width:n(), height:n(), ...inputBackendSelector }, ['action']), mutating, desktopHandlers.window_action, ['windows']),
|
|
537
|
-
define('launch_app', 'Launch application', 'Launch a desktop application or executable without invoking a shell. On Windows, executable/path launch falls back to Start Menu/UWP app discovery by friendly name. cwd is confined by the ReMCP filesystem policy and is only accepted when the platform can guarantee the child working directory. Optionally wait until a matching top-level window exists.', o({ app:s('Application name, friendly Start Menu name, or executable path.'), path:s('Alias for app.'), args:{ type:'array', items:{type:'string'} }, cwd:s('Optional permitted working directory for direct executable launch.'), wait_for_window:s('Optional application/title substring that must appear after launch.'), wait_timeout_ms:n('Maximum wait for wait_for_window; default 10000, maximum 120000.') }), openMutatingNonDestructive, desktopHandlers.launch_app),
|
|
538
|
+
define('launch_app', 'Launch application', 'Launch a desktop application or executable without invoking a shell. On Windows, executable/path launch falls back to Start Menu/UWP app discovery by friendly name. cwd is confined by the ReMCP filesystem policy and is only accepted when the platform can guarantee the child working directory. Optionally wait until a matching top-level window exists.', o({ app:s('Application name, friendly Start Menu name, or executable path.'), path:s('Alias for app.'), args:{ type:'array', items:{type:'string'} }, cwd:s('Optional permitted working directory for direct executable launch.'), wait_for_window:s('Optional application/title substring that must appear after launch.'), wait_timeout_ms:n('Maximum wait for wait_for_window; default 10000, maximum 120000.') }), openMutatingNonDestructive, desktopHandlers.launch_app, ['desktop_session']),
|
|
538
539
|
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
540
|
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
541
|
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']),
|
|
@@ -547,8 +548,8 @@ export const extendedToolDefinitions = [
|
|
|
547
548
|
define('clipboard', 'Clipboard', 'Read, write, or clear the system text clipboard using native operating-system clipboard facilities.', o({ action:e(['read','write','clear']), text:s('Text to write.') }, ['action']), mutating, desktopHandlers.clipboard, ['clipboard']),
|
|
548
549
|
define('display_inventory', 'Display inventory', 'List connected displays/monitors and their geometry and primary-display status using native display tools.', o(), readOnlyLive, desktopHandlers.display_inventory, ['displays']),
|
|
549
550
|
define('screenshot_region', 'Screenshot region', 'Capture a desktop rectangle, a selected top-level window, or a selected monitor and return it as an inline PNG. This avoids full-desktop screenshots when only one UI area matters.', o({ x:n(), y:n(), width:n(), height:n(), window_id:s('Window id returned by list_windows.'), pid:n(), app:s(), title:s(), monitor:s('Monitor name substring, or "primary".'), monitor_index:n('Zero-based monitor index from display_inventory.'), padding:n('Optional pixels added around the resolved rectangle; 0..200.') }), readOnlyLive, desktopHandlers.screenshot_region, ['screen_region']),
|
|
550
|
-
define('open_path', 'Open path', 'Open a permitted local file or directory in its system-associated desktop application. Filesystem confinement is checked before opening.', o({ path:s('File or directory to open.') }, ['path']), additive, desktopHandlers.open_path),
|
|
551
|
-
define('reveal_path', 'Reveal path', 'Reveal a permitted local file or directory in Finder, Explorer, or the Linux file manager after applying ReMCP filesystem confinement.', o({ path:s('File or directory to reveal.') }, ['path']), additive, desktopHandlers.reveal_path),
|
|
551
|
+
define('open_path', 'Open path', 'Open a permitted local file or directory in its system-associated desktop application. Filesystem confinement is checked before opening.', o({ path:s('File or directory to open.') }, ['path']), additive, desktopHandlers.open_path, ['desktop_open']),
|
|
552
|
+
define('reveal_path', 'Reveal path', 'Reveal a permitted local file or directory in Finder, Explorer, or the Linux file manager after applying ReMCP filesystem confinement.', o({ path:s('File or directory to reveal.') }, ['path']), additive, desktopHandlers.reveal_path, ['desktop_open']),
|
|
552
553
|
define('notification', 'Desktop notification', 'Show a native desktop notification or user-visible popup on the paired computer.', o({ title:s(), message:s('Notification message.'), timeout_seconds:n() }, ['message']), additive, desktopHandlers.notification, ['notifications']),
|
|
553
554
|
|
|
554
555
|
define('browser_tabs', 'Browser tabs', 'List debuggable Chrome, Edge, or Chromium page targets from a loopback-only Chrome DevTools Protocol endpoint.', o({ ...browserTarget }), readOnlyLive, browserHandlers.browser_tabs, ['browser_cdp']),
|
|
@@ -598,6 +599,19 @@ async function linuxHasPyAtSpi() {
|
|
|
598
599
|
return pyAtSpiCache.value;
|
|
599
600
|
}
|
|
600
601
|
|
|
602
|
+
export function desktopShellCapabilitySnapshot({
|
|
603
|
+
platform = process.platform,
|
|
604
|
+
env = process.env,
|
|
605
|
+
commandExistsFn = commandExists,
|
|
606
|
+
} = {}) {
|
|
607
|
+
const nativeDesktop = platform === 'win32' || platform === 'darwin';
|
|
608
|
+
const desktopSession = desktopSessionAvailable({ platform, env });
|
|
609
|
+
return {
|
|
610
|
+
desktop_session:desktopSession,
|
|
611
|
+
desktop_open:desktopSession && (nativeDesktop || commandExistsFn('xdg-open')),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
601
615
|
export function powerCapabilitySnapshot({
|
|
602
616
|
platform = process.platform,
|
|
603
617
|
commandExistsFn = commandExists,
|
|
@@ -626,23 +640,27 @@ export async function capabilitySnapshot() {
|
|
|
626
640
|
const win = process.platform === 'win32';
|
|
627
641
|
const mac = process.platform === 'darwin';
|
|
628
642
|
const linux = process.platform === 'linux';
|
|
629
|
-
const
|
|
630
|
-
const
|
|
643
|
+
const desktopShellCapabilities = desktopShellCapabilitySnapshot();
|
|
644
|
+
const desktopSession = desktopShellCapabilities.desktop_session;
|
|
645
|
+
const pyAtSpi = linux && desktopSession ? await linuxHasPyAtSpi() : false;
|
|
646
|
+
const wayland = linux && desktopSession && isWaylandSession();
|
|
631
647
|
const gnomeRecord = wayland ? await gnomeScreencastSupported() : false;
|
|
632
|
-
const
|
|
633
|
-
const
|
|
648
|
+
const portalCandidate = wayland && waylandPortalCandidate();
|
|
649
|
+
const portalInput = portalCandidate;
|
|
650
|
+
const x11Input = linux && desktopSession && !wayland && (commandExists('wdotool') || commandExists('xdotool'));
|
|
634
651
|
const browserCdp = browserRemoteEnabled() && await browserControlAvailable(undefined, 500);
|
|
635
652
|
const documentCapabilities = documentCapabilitySnapshot();
|
|
636
653
|
return {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
654
|
+
...desktopShellCapabilities,
|
|
655
|
+
windows: win || mac || (desktopSession && (commandExists('wdotool') || commandExists('wmctrl') || (wayland && pyAtSpi))),
|
|
656
|
+
ui: win || mac || (desktopSession && pyAtSpi),
|
|
657
|
+
input: win || mac || (desktopSession && (portalInput || x11Input || (wayland && commandExists('wtype')))),
|
|
658
|
+
pointer: win || mac || (desktopSession && (portalInput || x11Input)),
|
|
659
|
+
drag: win || mac || (desktopSession && (portalInput || x11Input)),
|
|
660
|
+
clipboard: win || mac || (desktopSession && (commandExists('wl-paste') || commandExists('xclip') || commandExists('xsel'))),
|
|
661
|
+
displays: win || mac || (desktopSession && ((wayland && linux) || commandExists('wlr-randr') || commandExists('xrandr'))),
|
|
662
|
+
screen_region: win || mac || (desktopSession && regionScreenshotBackends({ wayland, portalCandidate }).length > 0),
|
|
663
|
+
notifications: win || mac || (desktopSession && commandExists('notify-send')),
|
|
646
664
|
browser_cdp: browserCdp,
|
|
647
665
|
browser_evaluate: browserCdp && (process.env.NODE_ENV !== 'production' || ['1', 'true', 'yes', 'on'].includes(String(process.env.REMCP_BROWSER_ALLOW_EVALUATE || '').toLowerCase())),
|
|
648
666
|
services: win || mac || commandExists('systemctl'),
|
package/src/extended/common.mjs
CHANGED
|
@@ -21,6 +21,15 @@ export function optionalString(value) {
|
|
|
21
21
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export function desktopSessionAvailable({
|
|
25
|
+
platform = process.platform,
|
|
26
|
+
env = process.env,
|
|
27
|
+
} = {}) {
|
|
28
|
+
if (platform === 'win32' || platform === 'darwin') return true;
|
|
29
|
+
if (platform !== 'linux') return false;
|
|
30
|
+
return Boolean(String(env?.DISPLAY || '').trim() || String(env?.WAYLAND_DISPLAY || '').trim());
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
export function requireEnum(value, field, allowed) {
|
|
25
34
|
const normalized = String(value ?? '');
|
|
26
35
|
if (!allowed.includes(normalized)) fail(`${field} must be one of: ${allowed.join(', ')}`);
|
|
@@ -1487,12 +1487,13 @@ export function regionScreenshotBackends({
|
|
|
1487
1487
|
gnomeScreenshot = commandExists('gnome-screenshot'),
|
|
1488
1488
|
ffmpeg = commandExists('ffmpeg'),
|
|
1489
1489
|
imagemagick = commandExists('import'),
|
|
1490
|
+
portalCandidate = waylandPortalCandidate(),
|
|
1490
1491
|
} = {}) {
|
|
1491
1492
|
const backends = [];
|
|
1492
|
-
if (grim) backends.push('grim');
|
|
1493
|
+
if (wayland && grim) backends.push('grim');
|
|
1493
1494
|
if (wayland) {
|
|
1494
1495
|
if (gnomeScreenshot && ffmpeg) backends.push('gnome-screenshot');
|
|
1495
|
-
if (ffmpeg) backends.push('portal');
|
|
1496
|
+
if (ffmpeg && portalCandidate) backends.push('portal');
|
|
1496
1497
|
} else if (imagemagick) {
|
|
1497
1498
|
backends.push('imagemagick');
|
|
1498
1499
|
}
|
package/src/extended/desktop.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { fileToolHandlers } from '../tools/files.mjs';
|
|
|
4
4
|
import { liveConfig, runtimeConfig } from '../config.mjs';
|
|
5
5
|
import { assertAllowedCommand } from '../policy.mjs';
|
|
6
6
|
import { multi, resolveSafePath, text } from '../util.mjs';
|
|
7
|
-
import { clamp, jsonResult, optionalString, requireEnum } from './common.mjs';
|
|
7
|
+
import { clamp, desktopSessionAvailable, jsonResult, optionalString, requireEnum } from './common.mjs';
|
|
8
8
|
import * as linux from './desktop-linux.mjs';
|
|
9
9
|
import * as macos from './desktop-macos.mjs';
|
|
10
10
|
import * as windows from './desktop-windows.mjs';
|
|
@@ -1026,12 +1026,14 @@ function computerScreenshotTask(args = {}) {
|
|
|
1026
1026
|
}
|
|
1027
1027
|
|
|
1028
1028
|
export async function computerSnapshot(args = {}) {
|
|
1029
|
-
const
|
|
1029
|
+
const desktopAvailable = desktopSessionAvailable();
|
|
1030
|
+
const screenshotRequested = args.include_screenshot !== false || args.include_ocr === true;
|
|
1031
|
+
const captureScreenshot = desktopAvailable && screenshotRequested;
|
|
1030
1032
|
const includeUi = args.include_ui !== false;
|
|
1031
1033
|
const [windowsResult, displaysResult, uiResult, clipboardResult, cursorResult, shotResult] = await Promise.allSettled([
|
|
1032
|
-
listWindows(),
|
|
1033
|
-
displayInventory(),
|
|
1034
|
-
includeUi
|
|
1034
|
+
desktopAvailable ? listWindows() : Promise.resolve(null),
|
|
1035
|
+
desktopAvailable ? displayInventory() : Promise.resolve(null),
|
|
1036
|
+
includeUi && desktopAvailable
|
|
1035
1037
|
? uiSnapshot({
|
|
1036
1038
|
max_nodes:args.max_ui_nodes || 800,
|
|
1037
1039
|
max_depth:args.max_ui_depth || 12,
|
|
@@ -1040,8 +1042,8 @@ export async function computerSnapshot(args = {}) {
|
|
|
1040
1042
|
...(args.ui_browser_dom === true ? { browser_dom:true } : {}),
|
|
1041
1043
|
})
|
|
1042
1044
|
: Promise.resolve(null),
|
|
1043
|
-
clipboard({ action:'read' }),
|
|
1044
|
-
cursorPosition(),
|
|
1045
|
+
desktopAvailable ? clipboard({ action:'read' }) : Promise.resolve(null),
|
|
1046
|
+
desktopAvailable ? cursorPosition() : Promise.resolve(null),
|
|
1045
1047
|
captureScreenshot ? computerScreenshotTask(args) : Promise.resolve(null),
|
|
1046
1048
|
]);
|
|
1047
1049
|
const parse = (result, fallback = null) => {
|
|
@@ -1069,7 +1071,7 @@ export async function computerSnapshot(args = {}) {
|
|
|
1069
1071
|
if (includeUi && parsedUi !== ui && parsedUi != null) {
|
|
1070
1072
|
shapeErrors.push({ source:'ui', message:'accessibility snapshot returned an invalid result shape' });
|
|
1071
1073
|
}
|
|
1072
|
-
const clipboardOk = clipboardResult.status === 'fulfilled' && clipboardResult.value?.isError !== true;
|
|
1074
|
+
const clipboardOk = desktopAvailable && clipboardResult.status === 'fulfilled' && clipboardResult.value?.isError !== true && Boolean(clipboardResult.value);
|
|
1073
1075
|
const clipText = clipboardOk ? clipboardResult.value.content?.[0]?.text || '' : '';
|
|
1074
1076
|
const uiNodes = Array.isArray(ui?.nodes) ? ui.nodes : Array.isArray(ui) ? ui : [];
|
|
1075
1077
|
const focusedNodes = uiNodes.filter(node => node.focused === true);
|
|
@@ -1152,8 +1154,9 @@ export async function computerSnapshot(args = {}) {
|
|
|
1152
1154
|
}
|
|
1153
1155
|
}
|
|
1154
1156
|
|
|
1155
|
-
const
|
|
1156
|
-
|| (args.include_ocr !== false &&
|
|
1157
|
+
const ocrRequested = args.include_ocr === true
|
|
1158
|
+
|| (desktopAvailable && args.include_ocr !== false && screenshotRequested && uiNodes.length === 0 && !browser);
|
|
1159
|
+
const shouldOcr = desktopAvailable && ocrRequested;
|
|
1157
1160
|
let ocr = null;
|
|
1158
1161
|
let ocrCaptureBounds = null;
|
|
1159
1162
|
let ocrShotResult = shotResult;
|
|
@@ -1193,14 +1196,14 @@ export async function computerSnapshot(args = {}) {
|
|
|
1193
1196
|
...(browser ? { browser } : {}),
|
|
1194
1197
|
...(ocr ? { ocr } : {}),
|
|
1195
1198
|
fallback_chain: {
|
|
1196
|
-
accessibility: { requested:includeUi, available:includeUi && uiResult.status === 'fulfilled', nodes:uiNodes.length },
|
|
1199
|
+
accessibility: { requested:includeUi, available:includeUi && desktopAvailable && uiResult.status === 'fulfilled' && Boolean(uiResult.value), nodes:uiNodes.length },
|
|
1197
1200
|
native_browser_dom: {
|
|
1198
1201
|
requested:includeUi && args.ui_browser_dom === true,
|
|
1199
1202
|
available:Boolean(includeUi && ui?.browser_dom?.available),
|
|
1200
1203
|
provider:ui?.browser_dom?.provider || null,
|
|
1201
1204
|
},
|
|
1202
1205
|
browser_dom: { requested:browserRequested, available:Boolean(browser) },
|
|
1203
|
-
ocr: { requested:
|
|
1206
|
+
ocr: { requested:ocrRequested, available:Boolean(ocr?.available), backend:ocr?.backend || null },
|
|
1204
1207
|
vision_screenshot: { available:shotResult.status === 'fulfilled' && Boolean(shotResult.value?.content?.some(part => part.type === 'image')) },
|
|
1205
1208
|
},
|
|
1206
1209
|
visual: {
|