@remcp/runtime 0.2.49 → 0.2.51
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 +17 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/catalog.mjs +14 -13
- package/src/extended/browser.mjs +20 -6
- package/src/extended/catalog.mjs +99 -11
- package/src/extended/desktop-linux.mjs +15 -6
- package/src/extended/desktop.mjs +58 -16
- package/src/extended/documents.mjs +18 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.51 — 2026-09-20
|
|
6
|
+
|
|
7
|
+
- Linux AT-SPI text/focus operations reject explicit false results, `type_text method=accessibility` remains accessibility-only, and semantic browser text failures direct callers to `browser_action` rather than reporting an unverified paste as success.
|
|
8
|
+
- GNOME Wayland drag requires the consent-backed Remote Desktop portal instead of unreliable XTEST/xdotool delivery.
|
|
9
|
+
- PDF `read_document` falls back from the built-in parser to local `pdftotext` when embedded fonts prevent direct decoding, while preserving the existing no-text error when neither path extracts content.
|
|
10
|
+
- Lockstep patch release with the ReMCP client at `0.2.51`.
|
|
11
|
+
|
|
12
|
+
## 0.2.50 — 2026-09-20
|
|
13
|
+
|
|
14
|
+
- Lockstep release with the ReMCP client after the hosted MCP/plugin contract was hardened for current OpenAI/MCP Apps requirements.
|
|
15
|
+
- Computer-use tools retain the 83-tool runtime surface with AI-oriented descriptions, conditional JSON Schema validation, structured outputs, dynamic capability advertising, and semantic Accessibility/DOM-first routing.
|
|
16
|
+
- `browser_navigate` adds `action=new_tab` so CDP automation can create the first page target without a shell/browser-address-bar workaround.
|
|
17
|
+
- Live Linux verification covers Tesseract OCR targeting, XDG RemoteDesktop keyboard/pointer input, Unicode `type_text`, and the complete 39-tool computer-use smoke.
|
|
18
|
+
|
|
5
19
|
## 0.2.49 — 2026-09-20
|
|
6
20
|
|
|
7
21
|
- Terminal process tools now return schema-validated structured session state, output ranges, match results and termination facts alongside their human-readable text.
|
|
@@ -21,6 +35,9 @@
|
|
|
21
35
|
- Chrome DevTools Protocol endpoints are restricted to loopback addresses. New filesystem/document operations continue through the existing resolved-path allowlist, and launch/service/power operations reuse the runtime command policy.
|
|
22
36
|
- Fixed metadata output truncation once the JSON tool contract exceeded the operating-system pipe buffer by waiting for stdout writes to flush before exit.
|
|
23
37
|
- Terminal session tools now expose schema-validated structured `pid`, status, exit information, output ranges/matches, and session lists alongside their human-readable text, so hosted agents can chain process calls without parsing prose.
|
|
38
|
+
- Tool descriptions now state the intended routing boundary for exact-path vs glob reads, singular vs batch filesystem operations, full-screen vs targeted/semantic screenshots, and local runtime settings vs hosted device labels.
|
|
39
|
+
- Desktop action schemas and common guards now reject targetless `ui_action`/`window_action`, coordinate-less pointer moves, zero-motion scrolls, incomplete cross-backend `computer_action` targets, and connectivity tests without host/port before platform adapters can act. Empty `ui_find`/`browser_find` calls are redirected to their snapshot tools, and `wait_for_ui` requires an explicit state/condition instead of silently acting as a no-op.
|
|
40
|
+
- `take_screenshot` is correctly advertised as non-read-only/non-idempotent because kept or oversized captures can persist timestamped files; selector-scoped `browser_snapshot` screenshots restore the original page scroll position before returning so the inspection path remains read-only.
|
|
24
41
|
- No browser/document rendering stack was added; the runtime remains dependency-light and uses native OS facilities plus existing qpdf/poppler/zip helpers when available.
|
|
25
42
|
|
|
26
43
|
## 0.2.47 — 2026-09-19
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ For frontend QA, use semantic data to operate the page and rendered pixels to ve
|
|
|
63
63
|
`maxReadLines`, `maxBufferedLines`, and `maxOutputBytes`. Access roots, blocked commands,
|
|
64
64
|
the command guardrail, shell, write limit, runtime name, and unrestricted mode stay with the person
|
|
65
65
|
at the computer. DOCX/XLSX edits operate directly on OOXML, while structural PDF writes use existing
|
|
66
|
-
system tools such as qpdf/poppler when present; ReMCP still does not bundle Chromium, Puppeteer,
|
|
66
|
+
system tools such as qpdf/poppler when present. PDF text reading uses the built-in parser first and can fall back to local `pdftotext` for embedded-font PDFs; ReMCP still does not bundle Chromium, Puppeteer,
|
|
67
67
|
`sharp`, or `exceljs`.
|
|
68
68
|
|
|
69
69
|
`--print-tools` prints the exact JSON contract (schemas and annotations) the runtime advertises, and
|
|
@@ -76,7 +76,7 @@ and the user's request is the authorization. ReMCP adds no per-tool approval pro
|
|
|
76
76
|
and no dry-run detour unless you ask for one.
|
|
77
77
|
|
|
78
78
|
Operating-system security boundaries still apply. macOS can require Accessibility/Screen Recording grants,
|
|
79
|
-
and GNOME Wayland requires a one-time XDG RemoteDesktop consent before low-level keyboard/pointer input.
|
|
79
|
+
and GNOME Wayland requires a one-time XDG RemoteDesktop consent before low-level keyboard/pointer/drag input. XTEST/xdotool drag is deliberately not treated as reliable on GNOME Wayland.
|
|
80
80
|
ReMCP does not bypass those controls: after GNOME grants access, the portal restore token is stored locally
|
|
81
81
|
under the runtime config directory with mode `0600` and reused when the portal permits it. Semantic AT-SPI
|
|
82
82
|
UI actions and the XDG Screenshot portal remain separate from that low-level input permission.
|
package/package.json
CHANGED
package/src/catalog.mjs
CHANGED
|
@@ -109,7 +109,7 @@ export const toolDefinitions = [
|
|
|
109
109
|
{
|
|
110
110
|
name: 'read_files',
|
|
111
111
|
title: 'Read files by glob',
|
|
112
|
-
description: 'Read
|
|
112
|
+
description: 'Read files discovered by one glob under a directory, each section prefixed with its path and line count. Use this when the exact file list is not already known. If you already know the exact paths, use read_multiple_files; for one known path use read_file.',
|
|
113
113
|
inputSchema: {
|
|
114
114
|
type: 'object',
|
|
115
115
|
properties: {
|
|
@@ -127,7 +127,7 @@ export const toolDefinitions = [
|
|
|
127
127
|
{
|
|
128
128
|
name: 'read_multiple_files',
|
|
129
129
|
title: 'Read multiple files',
|
|
130
|
-
description: 'Read several text files in one call.
|
|
130
|
+
description: 'Read several explicitly named text files in one call. Use this when the exact paths are already known; each file is returned separately and one failure does not stop the others. Do not use this for glob discovery: use read_files instead. For one path use read_file.',
|
|
131
131
|
inputSchema: {
|
|
132
132
|
type: 'object',
|
|
133
133
|
properties: {
|
|
@@ -239,7 +239,7 @@ export const toolDefinitions = [
|
|
|
239
239
|
{
|
|
240
240
|
name: 'write_file',
|
|
241
241
|
title: 'Write file',
|
|
242
|
-
description: 'Create
|
|
242
|
+
description: 'Create or replace exactly one text file. Parent directories are created automatically; mode "append" adds to the end. Use write_files for two or more independent files, and write_binary for binary bytes.',
|
|
243
243
|
inputSchema: {
|
|
244
244
|
type: 'object',
|
|
245
245
|
properties: {
|
|
@@ -256,7 +256,7 @@ export const toolDefinitions = [
|
|
|
256
256
|
{
|
|
257
257
|
name: 'write_files',
|
|
258
258
|
title: 'Write multiple files',
|
|
259
|
-
description: 'Create or replace
|
|
259
|
+
description: 'Create or replace two or more text files in one batch, each with its own path, content, and optional mode. Use this for scaffolding or coherent multi-file writes; for one path use write_file.',
|
|
260
260
|
inputSchema: {
|
|
261
261
|
type: 'object',
|
|
262
262
|
properties: {
|
|
@@ -427,7 +427,7 @@ export const toolDefinitions = [
|
|
|
427
427
|
{
|
|
428
428
|
name: 'move_file',
|
|
429
429
|
title: 'Move or rename',
|
|
430
|
-
description: 'Move or rename
|
|
430
|
+
description: 'Move or rename exactly one file or directory from one source to one destination. Replaces an existing destination file by default; pass overwrite false to refuse instead. Use move_paths for two or more moves.',
|
|
431
431
|
inputSchema: {
|
|
432
432
|
type: 'object',
|
|
433
433
|
properties: {
|
|
@@ -444,7 +444,7 @@ export const toolDefinitions = [
|
|
|
444
444
|
{
|
|
445
445
|
name: 'copy_file',
|
|
446
446
|
title: 'Copy file',
|
|
447
|
-
description: 'Copy one file to a new path
|
|
447
|
+
description: 'Copy exactly one regular file to a new path. Directories are not copied recursively. Use copy_paths for directories or two or more copy operations; pass overwrite false to refuse an existing destination.',
|
|
448
448
|
inputSchema: {
|
|
449
449
|
type: 'object',
|
|
450
450
|
properties: {
|
|
@@ -461,7 +461,7 @@ export const toolDefinitions = [
|
|
|
461
461
|
{
|
|
462
462
|
name: 'copy_paths',
|
|
463
463
|
title: 'Copy paths',
|
|
464
|
-
description: 'Copy
|
|
464
|
+
description: 'Copy two or more paths, or copy a directory recursively, in one call. Each item has its own source and destination. For exactly one regular file use copy_file.',
|
|
465
465
|
inputSchema: {
|
|
466
466
|
type: 'object',
|
|
467
467
|
properties: {
|
|
@@ -489,7 +489,7 @@ export const toolDefinitions = [
|
|
|
489
489
|
{
|
|
490
490
|
name: 'move_paths',
|
|
491
491
|
title: 'Move paths',
|
|
492
|
-
description: 'Move or rename
|
|
492
|
+
description: 'Move or rename two or more files/directories in one batch, each with its own source and destination. Falls back to copy-and-delete across filesystems. For one source/destination pair use move_file.',
|
|
493
493
|
inputSchema: {
|
|
494
494
|
type: 'object',
|
|
495
495
|
properties: {
|
|
@@ -517,7 +517,7 @@ export const toolDefinitions = [
|
|
|
517
517
|
{
|
|
518
518
|
name: 'delete_path',
|
|
519
519
|
title: 'Delete path',
|
|
520
|
-
description: 'Delete
|
|
520
|
+
description: 'Delete exactly one file or directory. Directories are removed with their contents unless recursive is false; the filesystem root is refused. Use delete_paths for two or more targets, or move_to_trash when deletion should be reversible.',
|
|
521
521
|
inputSchema: {
|
|
522
522
|
type: 'object',
|
|
523
523
|
properties: {
|
|
@@ -533,7 +533,7 @@ export const toolDefinitions = [
|
|
|
533
533
|
{
|
|
534
534
|
name: 'delete_paths',
|
|
535
535
|
title: 'Delete paths',
|
|
536
|
-
description: 'Delete
|
|
536
|
+
description: 'Delete two or more files/directories in one batch and report each result. For one target use delete_path. Use move_to_trash instead when deletion should be reversible.',
|
|
537
537
|
inputSchema: {
|
|
538
538
|
type: 'object',
|
|
539
539
|
properties: {
|
|
@@ -582,7 +582,7 @@ export const toolDefinitions = [
|
|
|
582
582
|
{
|
|
583
583
|
name: 'take_screenshot',
|
|
584
584
|
title: 'Take screenshot',
|
|
585
|
-
description: 'Capture the
|
|
585
|
+
description: 'Capture the entire desktop only when full-screen pixels are actually needed. Prefer ui_snapshot or browser_snapshot for semantic inspection, and screenshot_region for one window, monitor, or rectangle. This tool uses the platform screenshot portal/utility and returns an image.',
|
|
586
586
|
inputSchema: {
|
|
587
587
|
type: 'object',
|
|
588
588
|
properties: {
|
|
@@ -591,7 +591,8 @@ export const toolDefinitions = [
|
|
|
591
591
|
},
|
|
592
592
|
additionalProperties: false,
|
|
593
593
|
},
|
|
594
|
-
|
|
594
|
+
// keep=true and oversized fallbacks persist a timestamped PNG, so a repeated call can add files.
|
|
595
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
595
596
|
handler: fileToolHandlers.take_screenshot,
|
|
596
597
|
},
|
|
597
598
|
{
|
|
@@ -813,7 +814,7 @@ export const toolDefinitions = [
|
|
|
813
814
|
{
|
|
814
815
|
name: 'set_config_value',
|
|
815
816
|
title: 'Change runtime setting',
|
|
816
|
-
description: 'Change one of this runtime’s own preferences on
|
|
817
|
+
description: 'Change one of this local runtime’s own preferences on the selected computer: telemetryEnabled, maxReadLines, maxBufferedLines or maxOutputBytes. This does not rename the paired device; use hosted rename_device for that label. Access roots, blocked commands, shell/guardrails and the write limit remain local-only.',
|
|
817
818
|
inputSchema: {
|
|
818
819
|
type: 'object',
|
|
819
820
|
properties: {
|
package/src/extended/browser.mjs
CHANGED
|
@@ -310,10 +310,12 @@ export async function browserSnapshot(args) {
|
|
|
310
310
|
|
|
311
311
|
await session.send('Page.enable');
|
|
312
312
|
const selector = optionalString(args.selector);
|
|
313
|
+
let restoreScroll = null;
|
|
313
314
|
if (selector) {
|
|
314
315
|
const bounds = await evaluate(session, `new Promise((resolve,reject) => {
|
|
315
316
|
const el=document.querySelector(${JSON.stringify(selector)});
|
|
316
317
|
if(!el){reject(new Error('Screenshot element not found'));return;}
|
|
318
|
+
const originalScrollX=scrollX, originalScrollY=scrollY;
|
|
317
319
|
const initial=el.getBoundingClientRect();
|
|
318
320
|
const desiredTop=Math.max(0, scrollY + initial.top - Math.max(0,(innerHeight-initial.height)/2));
|
|
319
321
|
window.scrollTo({top:desiredTop,left:scrollX,behavior:'instant'});
|
|
@@ -328,6 +330,8 @@ export async function browserSnapshot(args) {
|
|
|
328
330
|
viewport_height:innerHeight,
|
|
329
331
|
scroll_x:scrollX,
|
|
330
332
|
scroll_y:scrollY,
|
|
333
|
+
original_scroll_x:originalScrollX,
|
|
334
|
+
original_scroll_y:originalScrollY,
|
|
331
335
|
fully_visible:r.x>=0 && r.y>=0 && r.right<=innerWidth && r.bottom<=innerHeight
|
|
332
336
|
});
|
|
333
337
|
}));
|
|
@@ -335,7 +339,9 @@ export async function browserSnapshot(args) {
|
|
|
335
339
|
if (!bounds || !Number.isFinite(Number(bounds.width)) || !Number.isFinite(Number(bounds.height)) || bounds.width <= 0 || bounds.height <= 0) {
|
|
336
340
|
throw new Error('Screenshot element has invalid bounds');
|
|
337
341
|
}
|
|
338
|
-
|
|
342
|
+
restoreScroll = { x:Number(bounds.original_scroll_x || 0), y:Number(bounds.original_scroll_y || 0) };
|
|
343
|
+
const { original_scroll_x: _originalX, original_scroll_y: _originalY, ...reportedBounds } = bounds;
|
|
344
|
+
payload.screenshot_target = { selector, ...reportedBounds };
|
|
339
345
|
}
|
|
340
346
|
payload.screenshot = {
|
|
341
347
|
scope: 'viewport',
|
|
@@ -343,11 +349,18 @@ export async function browserSnapshot(args) {
|
|
|
343
349
|
height: await evaluate(session, 'innerHeight').catch(() => null),
|
|
344
350
|
};
|
|
345
351
|
const result = jsonResult(payload);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
352
|
+
let captured;
|
|
353
|
+
try {
|
|
354
|
+
captured = await session.send('Page.captureScreenshot', {
|
|
355
|
+
format: 'png',
|
|
356
|
+
fromSurface: true,
|
|
357
|
+
captureBeyondViewport: false,
|
|
358
|
+
});
|
|
359
|
+
} finally {
|
|
360
|
+
if (restoreScroll) {
|
|
361
|
+
await evaluate(session, `window.scrollTo({left:${restoreScroll.x},top:${restoreScroll.y},behavior:'instant'})`).catch(() => {});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
351
364
|
const data = String(captured?.data || '');
|
|
352
365
|
if (!data) throw new Error('CDP returned no screenshot data');
|
|
353
366
|
if (Buffer.byteLength(data, 'base64') > 8 * 1024 * 1024) throw new Error('Browser screenshot exceeds the 8 MiB inline image limit');
|
|
@@ -393,6 +406,7 @@ function findExpression(args) {
|
|
|
393
406
|
}
|
|
394
407
|
|
|
395
408
|
export async function browserFind(args) {
|
|
409
|
+
if (!(args.selector || args.text || args.role)) throw new Error('browser_find requires selector, text, or role; use browser_snapshot to enumerate page structure');
|
|
396
410
|
return withTarget(args, async (session, target) => {
|
|
397
411
|
const matches = await evaluate(session, findExpression(args));
|
|
398
412
|
return jsonResult({ target_id: target.id, count: Array.isArray(matches) ? matches.length : 0, matches: Array.isArray(matches) ? matches : [] });
|
package/src/extended/catalog.mjs
CHANGED
|
@@ -34,14 +34,14 @@ const AI_TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
34
34
|
window_action: 'Use this to focus, minimize, maximize, restore, move, resize or close one known top-level window. Select it by id/PID/app/title from list_windows; do not use pointer coordinates for window management when this semantic tool can express the action.',
|
|
35
35
|
launch_app: 'Use this to start a desktop application by executable/path or friendly application name without a shell. Prefer it over start_process for GUI application launch; use start_process for terminal commands, build tools and shell pipelines.',
|
|
36
36
|
ui_snapshot: 'Use this to inspect native desktop Accessibility/UI Automation semantics for the active app before clicking or typing. Prefer scope=active; use scope=desktop only for cross-application discovery. For a Chromium web page with CDP available, prefer browser_snapshot for page DOM semantics.',
|
|
37
|
-
ui_find: 'Use this after ui_snapshot when you need a small set of native UI elements by role/name/label/AutomationId. Prefer returned id or compact label for subsequent ui_action; refresh only when the UI has changed.',
|
|
37
|
+
ui_find: 'Use this after ui_snapshot when you need a small set of native UI elements by role/name/label/AutomationId. It requires an element criterion; use ui_snapshot, not an empty ui_find, to enumerate UI. Prefer returned id or compact label for subsequent ui_action; refresh only when the UI has changed.',
|
|
38
38
|
ui_action: 'Use this to invoke, focus, edit, select, toggle, expand/collapse, range-edit or scroll a native accessible UI element. Prefer this over pointer clicks because semantic actions survive layout changes; fall back to pointer only when accessibility cannot perform the requested action.',
|
|
39
|
-
type_text: 'Use this for reliable Unicode text entry into a native semantic target or focused control. Prefer method=auto so Accessibility
|
|
39
|
+
type_text: 'Use this for reliable Unicode text entry into a native semantic target or focused native control. Prefer method=auto so Accessibility is used when the target supports verified semantic editing. For Chromium page DOM, use browser_action instead of forcing desktop accessibility text input; auto fails safely when a semantic element rejects the write rather than claiming an unverified paste. Use method=keys only for deliberate physical key-by-key input, and keyboard for shortcuts/navigation/control keys.',
|
|
40
40
|
keyboard: 'Use this for shortcuts and individual key presses such as Ctrl/Cmd+C, Tab, Enter or Escape. Do not use it to enter normal prose when type_text is available. On Wayland auto uses an already-authorized portal; backend=portal explicitly requests consent.',
|
|
41
41
|
pointer: 'Use this only when an action truly requires screen coordinates or a semantic UI/browser action is unavailable. Prefer ui_action for native controls and browser_action for web controls; pointer is the coordinate fallback for move/click/button-down/button-up.',
|
|
42
|
-
drag_drop: 'Use this for a real drag gesture between two UI element ids or coordinates. Prefer element ids from ui_find/ui_snapshot because their current bounds are resolved at execution time; use coordinates only when no semantic endpoints exist.',
|
|
42
|
+
drag_drop: 'Use this for a real drag gesture between two UI element ids or coordinates. Prefer element ids from ui_find/ui_snapshot because their current bounds are resolved at execution time; use coordinates only when no semantic endpoints exist. On GNOME Wayland, real drag input requires the consent-backed Remote Desktop portal because XTEST/xdotool drag delivery is not reliable.',
|
|
43
43
|
scroll: 'Use this for native wheel scrolling of a desktop region or semantic UI target. Prefer browser_action scroll_into_view for a known web element and ui_action scroll_into_view for a known accessible desktop element.',
|
|
44
|
-
wait_for_ui: 'Use this after a desktop action to wait for
|
|
44
|
+
wait_for_ui: 'Use this after a desktop action to wait for an explicit native UI state or condition instead of sleeping or polling screenshots. Presence/absence waits need a semantic target; use state=changed for a broad tree change and set a bounded timeout.',
|
|
45
45
|
clipboard: 'Use this for explicit text clipboard read/write/clear operations. Do not use clipboard as a typing workaround directly; type_text already uses safe clipboard paste with restoration when needed.',
|
|
46
46
|
display_inventory: 'Use this before monitor-specific screenshots, cross-monitor geometry or coordinate work to obtain monitor bounds, scale and primary-display information.',
|
|
47
47
|
screenshot_region: 'Use this when pixels are required for visual verification but only one window, monitor or rectangle matters. Prefer this over a full-desktop screenshot; use ui_snapshot/browser_snapshot first when semantic structure is sufficient.',
|
|
@@ -50,8 +50,8 @@ const AI_TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
50
50
|
notification: 'Use this only when the user wants a visible native desktop notification on the paired computer. Do not use it as a substitute for replying in chat.',
|
|
51
51
|
browser_tabs: 'Use this first for CDP browser automation when the target page/tab is not already unambiguous. It lists debuggable Chromium pages from a loopback-only endpoint; use the returned target_id/title/url to scope later browser tools.',
|
|
52
52
|
browser_navigate: 'Use this to open a URL in a new debuggable tab or navigate a known Chromium page by URL, back, forward or reload. Prefer it over typing into the address bar; new_tab also bootstraps browser automation when no page target exists yet.',
|
|
53
|
-
browser_snapshot: 'Use this to inspect a web page structurally through CDP accessibility. Prefer it over desktop ui_snapshot for page content; add include_screenshot=true only when final pixel/layout verification is needed.',
|
|
54
|
-
browser_find: 'Use this to locate visible web elements by CSS selector, text or ARIA role and obtain reusable selectors/bounds. Prefer this before browser_action rather than guessing selectors or coordinates.',
|
|
53
|
+
browser_snapshot: 'Use this to inspect a web page structurally through CDP accessibility. Prefer it over desktop ui_snapshot for page content; add include_screenshot=true only when final pixel/layout verification is needed. A selector may be temporarily centered for capture, but the original page scroll position is restored before the tool returns.',
|
|
54
|
+
browser_find: 'Use this to locate visible web elements by CSS selector, text or ARIA role and obtain reusable selectors/bounds. It requires an element criterion; use browser_snapshot, not an empty browser_find, to enumerate page structure. Prefer this before browser_action rather than guessing selectors or coordinates.',
|
|
55
55
|
browser_action: 'Use this for deterministic DOM/CDP interaction with a known web element: click/focus/type/value/select/upload/key/scroll or viewport emulation. Prefer it over desktop pointer/ui_action for page content; use browser_evaluate only when the supported actions cannot express the task.',
|
|
56
56
|
browser_wait: 'Use this after browser navigation/action to wait for selector/text/URL/load/navigation/network-idle instead of fixed sleeps or screenshot polling. Use browser_evaluate separately for JavaScript predicates.',
|
|
57
57
|
browser_evaluate: 'Use this as the powerful browser escape hatch only when browser_snapshot/find/action/wait cannot express the required page operation or inspection. JavaScript executes in page context and page content is untrusted.',
|
|
@@ -63,7 +63,7 @@ const AI_TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
63
63
|
audio: 'Use this to read or change the default output volume/mute state. Use action=status for inspection; set_volume requires a 0–100 value.',
|
|
64
64
|
power_action: 'Use this only when the user explicitly asks to lock, sleep, restart or shut down the computer. It is intentionally guarded by device command policy because restart/shutdown can interrupt work.',
|
|
65
65
|
record_screen: 'Use this for a short bounded screen recording when motion/timing is necessary to diagnose a UI issue. Prefer screenshot_region for static visual verification because it is cheaper and easier to inspect.',
|
|
66
|
-
read_document: 'Use this to read structured content from PDF, DOCX, XLSX, TXT, Markdown, CSV, JSON or XML without opening a desktop app. Prefer it over shell extraction or launching Office when the goal is document content.',
|
|
66
|
+
read_document: 'Use this to read structured content from PDF, DOCX, XLSX, TXT, Markdown, CSV, JSON or XML without opening a desktop app. Prefer it over shell extraction or launching Office when the goal is document content. PDF reading uses the built-in parser first and may fall back to local pdftotext when embedded fonts prevent direct decoding.',
|
|
67
67
|
edit_spreadsheet: 'Use this for direct XLSX cell/range/formula edits without launching Excel. Prefer explicit cells/ranges and write to output when preserving the original matters.',
|
|
68
68
|
edit_document: 'Use this for structured DOCX paragraph edits without launching Word. It is best for replace/insert/delete paragraph operations; use native UI automation only when document formatting/layout interaction is required.',
|
|
69
69
|
pdf_action: 'Use this for PDF metadata/annotations inspection or structural merge/split/page extraction. For reading PDF text use read_document; supply an output/output_dir for write actions when preserving originals matters.',
|
|
@@ -244,7 +244,93 @@ function requiredAny(...groups) {
|
|
|
244
244
|
|
|
245
245
|
function applySchemaRules(name, schema) {
|
|
246
246
|
const next = structuredClone(schema);
|
|
247
|
-
if (name === '
|
|
247
|
+
if (name === 'computer_action') {
|
|
248
|
+
const semanticTarget = requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id'], ['selector'], ['browser_text']);
|
|
249
|
+
next.allOf = [
|
|
250
|
+
{
|
|
251
|
+
if:{ properties:{ action:{ const:'click' } }, required:['action'] },
|
|
252
|
+
then:{ anyOf:[...semanticTarget, { required:['ocr_text'] }, { required:['x','y'] }] },
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
if:{ properties:{ action:{ enum:['invoke','set_value','select','toggle','expand','collapse','scroll_into_view','set_range_value','add_to_selection','remove_from_selection'] } }, required:['action'] },
|
|
256
|
+
then:{ anyOf:semanticTarget },
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
if:{ properties:{ action:{ const:'focus' } }, required:['action'] },
|
|
260
|
+
then:{ anyOf:[...semanticTarget, ...requiredAny(['pid'], ['app'], ['window_title'], ['title'])] },
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
if:{ properties:{ action:{ enum:['set_value','set_range_value'] } }, required:['action'] },
|
|
264
|
+
then:{ required:['value'] },
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
if:{ properties:{ action:{ const:'multi_select' } }, required:['action'] },
|
|
268
|
+
then:{ required:['targets'] },
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
if:{ properties:{ action:{ const:'multi_edit' } }, required:['action'] },
|
|
272
|
+
then:{ required:['edits'] },
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
} else if (name === 'ui_find') {
|
|
276
|
+
next.anyOf = requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id']);
|
|
277
|
+
} else if (name === 'ui_action') {
|
|
278
|
+
next.allOf = [
|
|
279
|
+
{ anyOf:requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id']) },
|
|
280
|
+
{
|
|
281
|
+
if:{ properties:{ action:{ enum:['set_value','set_range_value'] } }, required:['action'] },
|
|
282
|
+
then:{ required:['value'] },
|
|
283
|
+
},
|
|
284
|
+
];
|
|
285
|
+
} else if (name === 'wait_for_ui') {
|
|
286
|
+
next.anyOf = requiredAny(['state'], ['condition']);
|
|
287
|
+
next.allOf = [
|
|
288
|
+
{
|
|
289
|
+
if:{ properties:{ condition:{ const:'text_exists' } }, required:['condition'] },
|
|
290
|
+
then:{ required:['text'] },
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
if:{ properties:{ condition:{ const:'active_window' } }, required:['condition'] },
|
|
294
|
+
then:{ anyOf:requiredAny(['text'], ['name'], ['window_title']) },
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
if:{ properties:{ condition:{ enum:['element_exists','element_enabled'] } }, required:['condition'] },
|
|
298
|
+
then:{ anyOf:requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id']) },
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
if:{ properties:{ state:{ enum:['present','absent'] } }, required:['state'] },
|
|
302
|
+
then:{ anyOf:requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id']) },
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
} else if (name === 'window_action') {
|
|
306
|
+
next.allOf = [
|
|
307
|
+
{ anyOf:requiredAny(['id'], ['pid'], ['app'], ['title']) },
|
|
308
|
+
{
|
|
309
|
+
if:{ properties:{ action:{ const:'move' } }, required:['action'] },
|
|
310
|
+
then:{ required:['x','y'] },
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
if:{ properties:{ action:{ const:'resize' } }, required:['action'] },
|
|
314
|
+
then:{ required:['width','height'] },
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
if:{ properties:{ action:{ const:'move_resize' } }, required:['action'] },
|
|
318
|
+
then:{ required:['x','y','width','height'] },
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
} else if (name === 'pointer') {
|
|
322
|
+
next.allOf = [{
|
|
323
|
+
if:{ properties:{ action:{ const:'move' } }, required:['action'] },
|
|
324
|
+
then:{ required:['x','y'] },
|
|
325
|
+
}];
|
|
326
|
+
} else if (name === 'scroll') {
|
|
327
|
+
next.anyOf = requiredAny(['delta_x'], ['delta_y'], ['delta'], ['direction']);
|
|
328
|
+
} else if (name === 'network') {
|
|
329
|
+
next.allOf = [{
|
|
330
|
+
if:{ properties:{ action:{ const:'test' } }, required:['action'] },
|
|
331
|
+
then:{ required:['host','port'] },
|
|
332
|
+
}];
|
|
333
|
+
} else if (name === 'launch_app') {
|
|
248
334
|
next.anyOf = requiredAny(['app'], ['path']);
|
|
249
335
|
} else if (name === 'keyboard') {
|
|
250
336
|
next.anyOf = requiredAny(['shortcut'], ['key'], ['keys']);
|
|
@@ -259,6 +345,8 @@ function applySchemaRules(name, schema) {
|
|
|
259
345
|
{ required:['x','y','width','height'] },
|
|
260
346
|
...requiredAny(['window_id'], ['pid'], ['app'], ['title'], ['monitor'], ['monitor_index']),
|
|
261
347
|
];
|
|
348
|
+
} else if (name === 'browser_find') {
|
|
349
|
+
next.anyOf = requiredAny(['selector'], ['text'], ['role']);
|
|
262
350
|
} else if (name === 'browser_navigate') {
|
|
263
351
|
next.allOf = [{
|
|
264
352
|
if:{ properties:{ action:{ enum:['url','new_tab'] } }, required:['action'] },
|
|
@@ -449,10 +537,10 @@ export const extendedToolDefinitions = [
|
|
|
449
537
|
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']),
|
|
450
538
|
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']),
|
|
451
539
|
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']),
|
|
452
|
-
define('type_text', 'Type text', 'Enter Unicode text into a semantic accessibility target or focused control. Auto prefers accessibility editing
|
|
540
|
+
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']),
|
|
453
541
|
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']),
|
|
454
542
|
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']),
|
|
455
|
-
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
|
|
543
|
+
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']),
|
|
456
544
|
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']),
|
|
457
545
|
define('wait_for_ui', 'Wait for UI', 'Wait on fresh native accessibility state without screenshot polling. Use state=present/absent/changed or condition=text_exists/active_window/element_exists/element_enabled/focused_element.', o({ state:e(['present','absent','changed'], 'changed captures the first matching semantic state and waits until it differs.'), condition:e(['text_exists','active_window','element_exists','element_enabled','focused_element']), text:s('Text used by text_exists or active_window.'), browser_dom:b('Use native browser document accessibility where supported.'), ...uiSelector, timeout_ms:n('Maximum wait, capped at 120000 ms.'), poll_ms:n('Polling interval, 50..5000 ms.') }), readOnlyLive, desktopHandlers.wait_for_ui, ['ui']),
|
|
458
546
|
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']),
|
|
@@ -464,7 +552,7 @@ export const extendedToolDefinitions = [
|
|
|
464
552
|
|
|
465
553
|
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']),
|
|
466
554
|
define('browser_navigate', 'Browser navigate', 'Open a new debuggable tab or navigate a selected browser page by URL, history back/forward, or reload through Chrome DevTools Protocol.', o({ ...browserTarget, action:e(['url','new_tab','back','forward','reload'], 'Default url when url is provided, otherwise reload. new_tab creates the first page target when needed.'), url:s('Destination URL for action=url or action=new_tab.'), wait:b('Wait for the page load event; default true.'), ignore_cache:b('Reload without cache when action=reload.') }), openMutatingNonDestructive, browserHandlers.browser_navigate, ['browser_cdp']),
|
|
467
|
-
define('browser_snapshot', 'Browser snapshot', 'Return the selected browser page accessibility tree through Chrome DevTools Protocol and, when requested, attach a real rendered viewport PNG for visual verification. A selector
|
|
555
|
+
define('browser_snapshot', 'Browser snapshot', 'Return the selected browser page accessibility tree through Chrome DevTools Protocol and, when requested, attach a real rendered viewport PNG for visual verification. A selector is temporarily recentered for capture, its exact viewport bounds are returned, and the prior scroll position is restored before return.', o({ ...browserTarget, max_nodes:n('Maximum AX nodes; default 1500.'), include_screenshot:b('Attach a real CDP-rendered viewport PNG screenshot; default false.'), selector:s('When include_screenshot=true, center this CSS-selected element before capture and report its viewport bounds.') }), readOnlyLive, browserHandlers.browser_snapshot, ['browser_cdp']),
|
|
468
556
|
define('browser_find', 'Find browser element', 'Find visible DOM elements by CSS selector, text, or ARIA role and return reusable selectors plus text, value, and bounding boxes.', o({ ...browserTarget, selector:s('CSS selector.'), text:s('Visible text substring.'), role:s('ARIA role.'), limit:n() }), readOnlyLive, browserHandlers.browser_find, ['browser_cdp']),
|
|
469
557
|
define('browser_action', 'Browser element action', 'Click, focus, type/set a value, select, scroll to, upload files, press a key, or set an exact responsive-test viewport in a debuggable browser page using DOM/CDP semantics.', o({ ...browserTarget, action:e(['click','focus','type','set_value','select','scroll_into_view','upload','press','set_viewport']), selector:s(), text:s(), value:s(), text_value:s(), option:s(), path:s(), paths:{type:'array',items:{type:'string'}}, key:s(), width:n('CSS viewport width for set_viewport.'), height:n('CSS viewport height for set_viewport.'), device_scale_factor:n('Device scale factor for set_viewport; default 1.'), mobile:b('Enable mobile emulation for set_viewport.') }, ['action']), openMutating, browserHandlers.browser_action, ['browser_cdp']),
|
|
470
558
|
define('browser_wait', 'Wait for browser', 'Wait for DOM state, page text, URL, completed loading, a navigation away from the current URL, or a short network-idle period in a debuggable browser page. Use browser_evaluate separately for JavaScript predicates or page-specific inspection.', o({ ...browserTarget, condition:e(['selector','text','url_contains','load','navigation','network_idle']), selector:s(), text:s(), value:s(), poll_ms:n(), idle_ms:n('Required zero-in-flight network quiet window for network_idle; default 500 ms.') }), readOnlyLive, browserHandlers.browser_wait, ['browser_cdp']),
|
|
@@ -586,10 +586,14 @@ if target is None:raise Exception("UI element not found")
|
|
|
586
586
|
if action in ("click","invoke","select","toggle"):
|
|
587
587
|
target.queryAction().doAction(0)
|
|
588
588
|
elif action=="focus":
|
|
589
|
-
target.queryComponent().grabFocus()
|
|
589
|
+
if target.queryComponent().grabFocus() is False: raise Exception("focus accessibility action returned false")
|
|
590
590
|
elif action=="set_value":
|
|
591
|
-
try:
|
|
592
|
-
|
|
591
|
+
try:
|
|
592
|
+
applied=target.queryEditableText().setTextContents(value)
|
|
593
|
+
if applied is False: raise Exception("editable text write returned false")
|
|
594
|
+
except Exception as editable_error:
|
|
595
|
+
try: target.queryValue().currentValue=value
|
|
596
|
+
except Exception as value_error: raise Exception("set_value failed: editable=%s; value=%s"%(editable_error,value_error))
|
|
593
597
|
elif action=="set_range_value":
|
|
594
598
|
target.queryValue().currentValue=float(value)
|
|
595
599
|
elif action in ("expand","collapse"):
|
|
@@ -620,7 +624,10 @@ print(json.dumps({"action":action,"name":target.name or "","role":target.getRole
|
|
|
620
624
|
try { payload = JSON.parse(result.stdout || ''); } catch {}
|
|
621
625
|
if (payload && !payload.error) return text(JSON.stringify(payload));
|
|
622
626
|
if (payload?.error) unavailable('Linux accessibility action', payload.error);
|
|
623
|
-
if (result.code !== 0)
|
|
627
|
+
if (result.code !== 0) {
|
|
628
|
+
const detail = String(result.stderr || result.stdout || 'AT-SPI action failed').trim().split(/\r?\n/).filter(Boolean).at(-1) || 'AT-SPI action failed';
|
|
629
|
+
unavailable('Linux accessibility action', detail);
|
|
630
|
+
}
|
|
624
631
|
return text(result.stdout.trim());
|
|
625
632
|
}
|
|
626
633
|
|
|
@@ -961,9 +968,11 @@ export async function dragDrop(args = {}) {
|
|
|
961
968
|
return jsonResult({ from:[args.from_x,args.from_y], to:[args.to_x,args.to_y], backend:'xdg-desktop-portal' });
|
|
962
969
|
} catch {}
|
|
963
970
|
}
|
|
964
|
-
if (backend
|
|
971
|
+
if (backend === 'x11') {
|
|
972
|
+
unavailable('Wayland drag and drop', 'XTEST/xdotool drag events are not reliably delivered through GNOME Wayland; use backend=portal');
|
|
973
|
+
}
|
|
965
974
|
if (backend === 'auto' && waylandPortalCandidate()) portalPermissionHint('Wayland drag and drop');
|
|
966
|
-
unavailable('Wayland drag and drop',
|
|
975
|
+
unavailable('Wayland drag and drop', 'no working consent-backed drag backend is available');
|
|
967
976
|
}
|
|
968
977
|
if (await runXdotool()) return jsonResult({ from:[args.from_x,args.from_y], to:[args.to_x,args.to_y], backend:'xdotool' });
|
|
969
978
|
unavailable('Drag and drop', 'install xdotool');
|
package/src/extended/desktop.mjs
CHANGED
|
@@ -273,7 +273,15 @@ export async function listWindows(args = {}) {
|
|
|
273
273
|
return { ...window, monitor: monitor?.name ?? null, monitor_index: monitor?.index ?? null };
|
|
274
274
|
}));
|
|
275
275
|
}
|
|
276
|
-
export async function windowAction(args = {}) {
|
|
276
|
+
export async function windowAction(args = {}) {
|
|
277
|
+
const hasTarget = Boolean(args.id || Number.isInteger(Number(args.pid)) || args.app || args.title);
|
|
278
|
+
if (!hasTarget) throw new Error('window_action requires id, pid, app, or title');
|
|
279
|
+
const action = optionalString(args.action);
|
|
280
|
+
if (action === 'move' && ![args.x,args.y].every(value => Number.isFinite(Number(value)))) throw new Error('window_action move requires x and y');
|
|
281
|
+
if (action === 'resize' && ![args.width,args.height].every(value => Number.isFinite(Number(value)))) throw new Error('window_action resize requires width and height');
|
|
282
|
+
if (action === 'move_resize' && ![args.x,args.y,args.width,args.height].every(value => Number.isFinite(Number(value)))) throw new Error('window_action move_resize requires x, y, width and height');
|
|
283
|
+
return adapter.windowAction(args);
|
|
284
|
+
}
|
|
277
285
|
export async function uiSnapshot(args = {}) {
|
|
278
286
|
const explicitlyScoped = args.pid != null || args.app || args.window_title || args.windowTitle;
|
|
279
287
|
const activeOnly = args.active_only != null
|
|
@@ -285,10 +293,18 @@ export async function uiSnapshot(args = {}) {
|
|
|
285
293
|
return jsonResult(enrichUiPayload(value, effective));
|
|
286
294
|
}
|
|
287
295
|
export async function uiAction(args = {}) {
|
|
288
|
-
|
|
296
|
+
const resolved = withResolvedUiLabel(args);
|
|
297
|
+
const hasTarget = Boolean(resolved.id || resolved.label != null || resolved.name || resolved.role || resolved.automation_id || resolved.automationId);
|
|
298
|
+
if (!hasTarget) throw new Error('ui_action requires id, label, name, role, or automation_id');
|
|
299
|
+
const action = optionalString(resolved.action);
|
|
300
|
+
if (['set_value','set_range_value'].includes(action) && resolved.value == null) throw new Error(`ui_action ${action} requires value`);
|
|
301
|
+
return adapter.uiAction(resolved);
|
|
289
302
|
}
|
|
290
303
|
export async function keyboard(args = {}) { return adapter.keyboard(args); }
|
|
291
|
-
export async function pointer(args = {}) {
|
|
304
|
+
export async function pointer(args = {}) {
|
|
305
|
+
if (args.action === 'move' && ![args.x,args.y].every(value => Number.isFinite(Number(value)))) throw new Error('pointer move requires x and y');
|
|
306
|
+
return adapter.pointer(args);
|
|
307
|
+
}
|
|
292
308
|
export async function clipboard(args = {}) { return adapter.clipboard(args); }
|
|
293
309
|
export async function displayInventory(args = {}) { return adapter.displayInventory(args); }
|
|
294
310
|
export async function cursorPosition(args = {}) { return typeof adapter.cursorPosition === 'function' ? adapter.cursorPosition(args) : jsonResult({ x:null, y:null }); }
|
|
@@ -334,6 +350,8 @@ export async function screenshotRegion(args = {}) {
|
|
|
334
350
|
export async function notification(args = {}) { return adapter.notification(args); }
|
|
335
351
|
export async function scroll(args = {}) {
|
|
336
352
|
const direction = optionalString(args.direction);
|
|
353
|
+
const hasDelta = [args.delta_x,args.delta_y,args.delta].some(value => value != null && Number.isFinite(Number(value)));
|
|
354
|
+
if (!direction && !hasDelta) throw new Error('scroll requires direction or a delta');
|
|
337
355
|
const times = clamp(args.wheel_times, 1, 1, 50);
|
|
338
356
|
let deltaX = Number(args.delta_x || 0);
|
|
339
357
|
let deltaY = Number(args.delta_y ?? args.delta ?? 0);
|
|
@@ -403,6 +421,8 @@ async function uiMatches(args = {}) {
|
|
|
403
421
|
}
|
|
404
422
|
|
|
405
423
|
export async function uiFind(args = {}) {
|
|
424
|
+
const hasSelector = Boolean(args.id || args.label != null || args.name || args.role || args.automation_id || args.automationId);
|
|
425
|
+
if (!hasSelector) throw new Error('ui_find requires id, label, name, role, or automation_id; use ui_snapshot to enumerate UI');
|
|
406
426
|
const nodes = await uiMatches(args);
|
|
407
427
|
return jsonResult({ count: nodes.length, nodes });
|
|
408
428
|
}
|
|
@@ -473,11 +493,17 @@ export async function waitForUi(args = {}) {
|
|
|
473
493
|
const timeoutMs = clamp(args.timeout_ms, 10_000, 100, 120_000);
|
|
474
494
|
const pollMs = clamp(args.poll_ms, 250, 50, 5000);
|
|
475
495
|
const rawCondition = optionalString(args.condition);
|
|
496
|
+
const rawState = optionalString(args.state);
|
|
497
|
+
if (!rawCondition && !rawState) throw new Error('wait_for_ui requires state or condition');
|
|
476
498
|
const aliases = { text:'text_exists', window:'active_window', element:'element_exists', enabled:'element_enabled', focused:'focused_element' };
|
|
477
499
|
const condition = rawCondition
|
|
478
500
|
? requireEnum(aliases[rawCondition] || rawCondition, 'condition', ['text_exists','active_window','element_exists','element_enabled','focused_element'])
|
|
479
501
|
: null;
|
|
480
|
-
const state = condition ? null : requireEnum(
|
|
502
|
+
const state = condition ? null : requireEnum(rawState, 'state', ['present', 'absent', 'changed']);
|
|
503
|
+
const hasSelector = Boolean(args.id || args.label != null || args.name || args.role || args.automation_id || args.automationId);
|
|
504
|
+
if (['element_exists','element_enabled'].includes(condition) && !hasSelector) throw new Error(`wait_for_ui condition=${condition} requires a semantic target`);
|
|
505
|
+
if (condition === 'active_window' && !(args.text || args.name || args.window_title || args.windowTitle)) throw new Error('wait_for_ui condition=active_window requires text, name, or window_title');
|
|
506
|
+
if (['present','absent'].includes(state) && !hasSelector) throw new Error(`wait_for_ui state=${state} requires a semantic target`);
|
|
481
507
|
const deadline = Date.now() + timeoutMs;
|
|
482
508
|
const startedAt = Date.now();
|
|
483
509
|
let attempts = 0;
|
|
@@ -541,24 +567,37 @@ export async function typeText(args = {}) {
|
|
|
541
567
|
const value = String(args.text ?? '');
|
|
542
568
|
const method = requireEnum(args.method || 'auto', 'method', ['auto','accessibility','clipboard','keys']);
|
|
543
569
|
const resolvedArgs = args.label != null ? withResolvedUiLabel(args) : args;
|
|
544
|
-
const
|
|
570
|
+
const hasElementSelector = Boolean(
|
|
545
571
|
optionalString(resolvedArgs.id) || optionalString(resolvedArgs.name) || optionalString(resolvedArgs.role) ||
|
|
546
|
-
optionalString(resolvedArgs.automation_id || resolvedArgs.automationId)
|
|
547
|
-
optionalString(resolvedArgs.window_title || resolvedArgs.windowTitle) || Number.isInteger(Number(resolvedArgs.pid))
|
|
572
|
+
optionalString(resolvedArgs.automation_id || resolvedArgs.automationId)
|
|
548
573
|
);
|
|
549
574
|
const clear = args.clear === true;
|
|
550
575
|
const pressEnter = args.press_enter === true;
|
|
551
576
|
const caret = requireEnum(args.caret_position || 'idle', 'caret_position', ['start','idle','end']);
|
|
552
577
|
|
|
553
|
-
if ((method === 'auto' || method === 'accessibility') &&
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
578
|
+
if ((method === 'auto' || method === 'accessibility') && hasElementSelector) {
|
|
579
|
+
try {
|
|
580
|
+
const result = await uiAction({ ...resolvedArgs, action:'set_value', value });
|
|
581
|
+
const raw = result.content?.[0]?.text || '';
|
|
582
|
+
let target = raw;
|
|
583
|
+
try { target = JSON.parse(raw); } catch {}
|
|
584
|
+
if (pressEnter) await keyboard({ ...resolvedArgs, key:'ENTER' });
|
|
585
|
+
return jsonResult({ length:value.length, method:'accessibility', target, clear:true, press_enter:pressEnter });
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (method === 'accessibility') throw error;
|
|
588
|
+
if (!clear && caret === 'idle' && typeof adapter.typeTextFocused === 'function') {
|
|
589
|
+
await uiAction({ ...resolvedArgs, action:'focus' });
|
|
590
|
+
const semantic = await adapter.typeTextFocused(value, resolvedArgs);
|
|
591
|
+
if (semantic) {
|
|
592
|
+
if (pressEnter) await keyboard({ ...resolvedArgs, key:'ENTER' });
|
|
593
|
+
return jsonResult({ length:value.length, method:'accessibility', target:semantic, press_enter:pressEnter });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
597
|
+
throw new Error(`Semantic text target rejected accessibility input. Refresh the UI target or use browser_action for browser page content. ${detail}`);
|
|
598
|
+
}
|
|
560
599
|
}
|
|
561
|
-
if ((method === 'auto' || method === 'accessibility') && typeof adapter.typeTextFocused === 'function' && !clear && caret === 'idle') {
|
|
600
|
+
if ((method === 'auto' || method === 'accessibility') && !hasElementSelector && typeof adapter.typeTextFocused === 'function' && !clear && caret === 'idle') {
|
|
562
601
|
const semantic = await adapter.typeTextFocused(value, resolvedArgs);
|
|
563
602
|
if (semantic) {
|
|
564
603
|
if (pressEnter) await keyboard({ ...resolvedArgs, key:'ENTER' });
|
|
@@ -566,8 +605,11 @@ export async function typeText(args = {}) {
|
|
|
566
605
|
}
|
|
567
606
|
if (method === 'accessibility') throw new Error('No focused editable accessibility element is available');
|
|
568
607
|
}
|
|
608
|
+
if (method === 'accessibility') {
|
|
609
|
+
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');
|
|
610
|
+
}
|
|
569
611
|
|
|
570
|
-
if (
|
|
612
|
+
if (hasElementSelector) await uiAction({ ...resolvedArgs, action:'focus' });
|
|
571
613
|
if (clear) {
|
|
572
614
|
await keyboard({ ...resolvedArgs, shortcut:process.platform === 'darwin' ? 'CMD+A' : 'CTRL+A' });
|
|
573
615
|
await keyboard({ ...resolvedArgs, key:'BACKSPACE' });
|
|
@@ -127,12 +127,29 @@ async function readXlsx(filePath, args) {
|
|
|
127
127
|
} finally { await removeTemp(dir); }
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
async function readPdfDocument(filePath, data) {
|
|
131
|
+
try {
|
|
132
|
+
return readPdfText(data);
|
|
133
|
+
} catch (builtinError) {
|
|
134
|
+
if (commandExists('pdftotext')) {
|
|
135
|
+
const extracted = await runFile('pdftotext', ['-enc','UTF-8',filePath,'-'], {
|
|
136
|
+
label:'PDF text extraction',
|
|
137
|
+
timeout:60_000,
|
|
138
|
+
allowFailure:true,
|
|
139
|
+
});
|
|
140
|
+
const fallbackText = String(extracted.stdout || '').replace(/\f/g, '\n').trim();
|
|
141
|
+
if (extracted.code === 0 && fallbackText) return fallbackText;
|
|
142
|
+
}
|
|
143
|
+
throw builtinError;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
130
147
|
export async function readDocument(args) {
|
|
131
148
|
const filePath = await resolveSafePath(args.path,'path');
|
|
132
149
|
const lower = filePath.toLowerCase();
|
|
133
150
|
const data = await readFile(filePath);
|
|
134
151
|
if (lower.endsWith('.docx')) return text(readDocxText(data));
|
|
135
|
-
if (lower.endsWith('.pdf')) return text(
|
|
152
|
+
if (lower.endsWith('.pdf')) return text(await readPdfDocument(filePath,data));
|
|
136
153
|
if (lower.endsWith('.xlsx')) return jsonResult(await readXlsx(filePath,args));
|
|
137
154
|
if (/\.(txt|md|csv|json|xml|yaml|yml)$/i.test(lower)) return text(data.toString('utf8'));
|
|
138
155
|
throw new Error('read_document supports PDF, DOCX, XLSX, TXT, Markdown, CSV, JSON, XML, YAML');
|