@remcp/runtime 0.2.49 → 0.2.50
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 +10 -0
- package/package.json +1 -1
- package/src/catalog.mjs +14 -13
- package/src/extended/browser.mjs +20 -6
- package/src/extended/catalog.mjs +94 -6
- package/src/extended/desktop.mjs +30 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.50 — 2026-09-20
|
|
6
|
+
|
|
7
|
+
- Lockstep release with the ReMCP client after the hosted MCP/plugin contract was hardened for current OpenAI/MCP Apps requirements.
|
|
8
|
+
- 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.
|
|
9
|
+
- `browser_navigate` adds `action=new_tab` so CDP automation can create the first page target without a shell/browser-address-bar workaround.
|
|
10
|
+
- Live Linux verification covers Tesseract OCR targeting, XDG RemoteDesktop keyboard/pointer input, Unicode `type_text`, and the complete 39-tool computer-use smoke.
|
|
11
|
+
|
|
5
12
|
## 0.2.49 — 2026-09-20
|
|
6
13
|
|
|
7
14
|
- Terminal process tools now return schema-validated structured session state, output ranges, match results and termination facts alongside their human-readable text.
|
|
@@ -21,6 +28,9 @@
|
|
|
21
28
|
- 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
29
|
- 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
30
|
- 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.
|
|
31
|
+
- 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.
|
|
32
|
+
- 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.
|
|
33
|
+
- `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
34
|
- 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
35
|
|
|
26
36
|
## 0.2.47 — 2026-09-19
|
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
39
|
type_text: 'Use this for reliable Unicode text entry into a native semantic target or focused control. Prefer method=auto so Accessibility or clipboard restoration preserves the requested characters regardless of keyboard layout. Use method=keys only for deliberate physical key-by-key input; printable output then follows the current keyboard layout on the computer. Use keyboard for shortcuts, navigation keys and 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
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.',
|
|
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.',
|
|
@@ -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'] },
|
|
@@ -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']),
|
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;
|