@remcp/runtime 0.2.131 → 0.2.132

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.132 — 2026-10-02
4
+
5
+ - `computer_action` now exposes `type`, `scroll`, `drag`, and `target=window` in its MCP schema while keeping specialist tools available when the backend is already known.
6
+ - Auto `type` uses accessibility-only semantic editing before CDP and never falls through to unverified physical text input merely because a native selector failed.
7
+ - Browser DOM typing separates `browser_text` locator semantics from the `text` payload; legacy locator-in-`text` calls remain compatible when `value`/`text_value` carries the payload.
8
+ - `browser_action scroll` performs bounded page or nearest-scrollable-ancestor scrolling with the same delta/direction vocabulary as native `scroll`; `computer_action scroll` can route to it automatically.
9
+ - Action-specific schemas validate type targets/text, scroll deltas/directions, drag endpoints, and explicit backend/target compatibility before handler execution. Window targets require a window selector; UI/coordinate scroll and drag require target-appropriate fields; browser/OCR drag is rejected before dispatch.
10
+ - macOS `service action=list` now uses explicit launchd domains so `scope=user` reads `gui/<uid>` and `scope=system` reads `system`; the old legacy `launchctl list` path could not honor system scope.
11
+ - Public macOS release CI exercises the system launchd domain in addition to document/network backends. Runtime tool names remain unchanged at 83.
12
+
3
13
  ## 0.2.131 — 2026-10-02
4
14
 
5
15
  - `network action=summary` is now bounded for Docker-heavy hosts: non-container interfaces are prioritized, at most eight interfaces and four addresses per interface are returned, and count/truncation metadata shows what was omitted.
package/README.md CHANGED
@@ -60,7 +60,9 @@ If custom widgets are explicitly re-enabled, hosted discovery expands to **15 de
60
60
  **5 app-only** UI helpers. Cached legacy clients can still call supported granular aliases without inflating
61
61
  discovery for new clients.
62
62
 
63
- `computer_snapshot` and `computer_action` keep OCR inside the compact high-level surface: when a local `tesseract` binary is available, snapshot can return bounded OCR text/boxes and click targeting can fall back through Accessibility → browser DOM/CDP → OCR → coordinates. OCR is optional and never adds another MCP tool or bundled OCR dependency.
63
+ `computer_snapshot` and `computer_action` keep OCR inside the compact high-level surface: when a local `tesseract` binary is available, snapshot can return bounded OCR text/boxes and click targeting can fall back through Accessibility → browser DOM/CDP → OCR → coordinates. `computer_action` also exposes backend-uncertain `type`, `scroll`, and `drag` plus explicit window targeting; it prefers semantic Accessibility, can route browser selectors through CDP, and keeps native coordinate/UI gestures as the fallback. When the backend is already known, the specialist UI/browser/type/scroll/drag/window tools keep their full controls. OCR is optional and never adds another MCP tool or bundled OCR dependency.
64
+
65
+ For browser typing, `browser_text` is the visible DOM locator while `text` is the Unicode payload. `browser_action(action="scroll")` accepts page/element deltas or direction/wheel steps and scrolls the page or nearest scrollable ancestor; `scroll_into_view` remains the separate semantic reveal operation.
64
66
 
65
67
  For frontend QA, use semantic data to operate the page and rendered pixels to verify it: `browser_action(action="set_viewport")` sets an exact responsive-test viewport, `browser_wait` waits for the real application state, and `browser_snapshot(include_screenshot=true, selector=...)` recenters the component, returns its bounds, and attaches the real CDP-rendered viewport PNG. This intentionally treats screenshots as the visual oracle without making screenshot-driven coordinate guessing the primary control path.
66
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.131",
3
+ "version": "0.2.132",
4
4
  "description": "First-party ReMCP local device runtime: filesystem, terminal, computer-use, browser, diagnostics and document tools over MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1045,6 +1045,47 @@ export async function browserFind(args) {
1045
1045
  });
1046
1046
  }
1047
1047
 
1048
+ const PAGE_SCROLL_ACTION = `function(selector, needle, deltaX, deltaY) {
1049
+ let target = selector ? document.querySelector(selector) : null;
1050
+ if (!target && needle) {
1051
+ const lower = String(needle).toLowerCase();
1052
+ target = Array.from(document.querySelectorAll('*')).find(node =>
1053
+ String(node.innerText || node.textContent || '').trim().toLowerCase().includes(lower)
1054
+ ) || null;
1055
+ }
1056
+ if ((selector || needle) && !target) throw new Error('Element not found');
1057
+
1058
+ let scroller = document.scrollingElement || document.documentElement;
1059
+ if (target) {
1060
+ let current = target;
1061
+ while (current && current !== document.documentElement) {
1062
+ const style = getComputedStyle(current);
1063
+ const scrollX = /(auto|scroll|overlay)/.test(String(style.overflowX || '')) && current.scrollWidth > current.clientWidth;
1064
+ const scrollY = /(auto|scroll|overlay)/.test(String(style.overflowY || '')) && current.scrollHeight > current.clientHeight;
1065
+ if (scrollX || scrollY) {
1066
+ scroller = current;
1067
+ break;
1068
+ }
1069
+ current = current.parentElement;
1070
+ }
1071
+ }
1072
+
1073
+ const beforeX = Number(scroller.scrollLeft || 0);
1074
+ const beforeY = Number(scroller.scrollTop || 0);
1075
+ scroller.scrollLeft = beforeX + Number(deltaX || 0);
1076
+ scroller.scrollTop = beforeY + Number(deltaY || 0);
1077
+ return {
1078
+ target: target ? target.tagName.toLowerCase() : 'page',
1079
+ scroller: scroller === document.scrollingElement || scroller === document.documentElement ? 'page' : scroller.tagName.toLowerCase(),
1080
+ before_x: beforeX,
1081
+ before_y: beforeY,
1082
+ after_x: Number(scroller.scrollLeft || 0),
1083
+ after_y: Number(scroller.scrollTop || 0),
1084
+ delta_x: Number(deltaX || 0),
1085
+ delta_y: Number(deltaY || 0),
1086
+ };
1087
+ }`;
1088
+
1048
1089
  const PAGE_ELEMENT_ACTION = `function(selector, needle, action, value) {
1049
1090
  let el = selector ? document.querySelector(selector) : null;
1050
1091
  if (!el && needle) {
@@ -1081,14 +1122,46 @@ const PAGE_ELEMENT_ACTION = `function(selector, needle, action, value) {
1081
1122
  };
1082
1123
  }`;
1083
1124
 
1125
+ export function browserActionLocatorText(args = {}, action = '') {
1126
+ const explicit = optionalString(args.browser_text || args.match_text);
1127
+ if (explicit) return explicit;
1128
+ if (action === 'type') {
1129
+ // Legacy type calls used text as the locator and value/text_value as the payload.
1130
+ if (!optionalString(args.selector) && (args.value != null || args.text_value != null)) return optionalString(args.text);
1131
+ return null;
1132
+ }
1133
+ return optionalString(args.text);
1134
+ }
1135
+
1084
1136
  export function browserActionInputValue(args = {}, action = '') {
1085
- // text is the public field models naturally use for type, while set_value/select historically
1086
- // used value/text_value. Keep all aliases compatible but give each action its semantic field.
1087
- return String(action === 'type' ? (args.text ?? args.text_value ?? args.value ?? '') : (args.value ?? args.text_value ?? args.text ?? ''));
1137
+ if (action === 'type') {
1138
+ if (!optionalString(args.selector) && !optionalString(args.browser_text || args.match_text) && (args.value != null || args.text_value != null)) {
1139
+ return String(args.text_value ?? args.value ?? '');
1140
+ }
1141
+ return String(args.text ?? args.text_value ?? args.value ?? '');
1142
+ }
1143
+ return String(args.value ?? args.text_value ?? args.text ?? '');
1144
+ }
1145
+
1146
+ export function browserScrollDeltas(args = {}) {
1147
+ const direction = optionalString(args.direction);
1148
+ const hasDelta = [args.delta_x,args.delta_y,args.delta].some(value => value != null && Number.isFinite(Number(value)));
1149
+ if (!direction && !hasDelta) return null;
1150
+ const times = clamp(args.wheel_times, 1, 1, 50);
1151
+ let deltaX = Number(args.delta_x || 0);
1152
+ let deltaY = Number(args.delta_y ?? args.delta ?? 0);
1153
+ if (direction) {
1154
+ const normalized = requireEnum(direction, 'direction', ['up','down','left','right']);
1155
+ if (normalized === 'up') deltaY = -120 * times;
1156
+ if (normalized === 'down') deltaY = 120 * times;
1157
+ if (normalized === 'left') deltaX = -120 * times;
1158
+ if (normalized === 'right') deltaX = 120 * times;
1159
+ }
1160
+ return { delta_x:deltaX, delta_y:deltaY };
1088
1161
  }
1089
1162
 
1090
1163
  export async function browserAction(args) {
1091
- const action = requireEnum(args.action, 'action', ['click', 'focus', 'type', 'set_value', 'select', 'scroll_into_view', 'upload', 'press', 'set_viewport']);
1164
+ const action = requireEnum(args.action, 'action', ['click', 'focus', 'type', 'set_value', 'select', 'scroll', 'scroll_into_view', 'upload', 'press', 'set_viewport']);
1092
1165
  return withTarget(args, async (session, target) => {
1093
1166
  if (action === 'set_viewport') {
1094
1167
  const width = clamp(args.width, 1280, 200, 8192);
@@ -1126,11 +1199,22 @@ export async function browserAction(args) {
1126
1199
  return jsonResult({ target_id: target.id, action, key });
1127
1200
  }
1128
1201
  const selector = optionalString(args.selector);
1129
- const needle = optionalString(args.text);
1130
- if (!selector && !needle) throw new Error('selector or text is required');
1202
+ const needle = browserActionLocatorText(args, action);
1131
1203
  const value = browserActionInputValue(args, action);
1132
1204
  const option = String(args.option ?? value);
1133
1205
 
1206
+ if (action === 'scroll') {
1207
+ const deltas = browserScrollDeltas(args);
1208
+ if (!deltas) throw new Error('browser_action scroll requires direction or a delta');
1209
+ return jsonResult({
1210
+ target_id: target.id,
1211
+ action,
1212
+ result: await callPageFunction(session, PAGE_SCROLL_ACTION, [selector, needle, deltas.delta_x, deltas.delta_y]),
1213
+ });
1214
+ }
1215
+
1216
+ if (!selector && !needle) throw new Error('selector or browser_text/text locator is required');
1217
+
1134
1218
  if (action === 'click') {
1135
1219
  const rect = await callPageFunction(session, PAGE_ELEMENT_ACTION, [selector, needle, 'click', '']);
1136
1220
  const x = Number(rect.x) + Number(rect.width) / 2;
@@ -1143,6 +1227,7 @@ export async function browserAction(args) {
1143
1227
 
1144
1228
  if (action === 'type') {
1145
1229
  const focused = await callPageFunction(session, PAGE_ELEMENT_ACTION, [selector, needle, 'type', '']);
1230
+ if (args.clear === true) await callPageFunction(session, PAGE_ELEMENT_ACTION, [selector, needle, 'set_value', '']);
1146
1231
  await session.send('Input.insertText', { text:value });
1147
1232
  const current = await callPageFunction(session, PAGE_ELEMENT_ACTION, [selector, needle, 'read_value', '']);
1148
1233
  return jsonResult({ target_id:target.id, action, result:{ ...focused, value:current.value } });
@@ -30,7 +30,7 @@ const o = (properties = {}, required = []) => ({ type: 'object', properties, ...
30
30
 
31
31
  const AI_TOOL_DESCRIPTIONS = Object.freeze({
32
32
  computer_snapshot: 'Use this for the first look at an unfamiliar desktop state when you need one compact bundle of windows, semantic UI, displays, clipboard metadata and optional pixels/browser/OCR. Prefer it over repeated screenshots; narrow later with ui_find, browser_find or screenshot_region.',
33
- computer_action: 'Use this only when one semantic click/edit/focus/selection action must work across an uncertain target backend and may need Accessibility → browser DOM/CDP → OCR → coordinate fallback, or when multi_select/multi_edit avoids many identical native UI calls. Do not use it when the backend or operation is already known: use ui_action for native controls, browser_action for page DOM, type_text for prose, keyboard for shortcuts, pointer for raw pointer gestures, drag_drop or scroll for those gestures, window_action for windows, and the dedicated clipboard/launch/open/reveal/notification tools for those jobs.',
33
+ computer_action: 'Use this only when one semantic click/type/invoke/select/focus/scroll/drag action must work across an uncertain target backend and may need Accessibility → browser DOM/CDP → OCR → coordinate fallback, or when multi_select/multi_edit avoids many identical native UI calls. When the backend is already known, prefer the specialist tool (ui_action, browser_action, type_text, drag_drop, scroll or window_action) for its full controls. Keep keyboard, clipboard, launch/open/reveal/notification and raw pointer gestures in their dedicated tools.',
34
34
  list_windows: 'Use this to discover top-level desktop windows or obtain a stable window id/PID/bounds before window_action, screenshot_region or targeted input. Use ui_snapshot instead when you need controls inside a window; prefer list_windows over screenshot-based window guessing.',
35
35
  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. Use ui_action for controls inside the window; do not use pointer coordinates for window management when this semantic tool can express the action.',
36
36
  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. If the user wants to open a specific file or directory in its associated application, use open_path instead; use start_process for terminal commands, build tools and shell pipelines.',
@@ -247,8 +247,18 @@ function requiredAny(...groups) {
247
247
  function applySchemaRules(name, schema) {
248
248
  const next = structuredClone(schema);
249
249
  if (name === 'computer_action') {
250
- const semanticTarget = requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id'], ['selector'], ['browser_text']);
250
+ const nativeTarget = requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id']);
251
+ const browserElementTarget = requiredAny(['selector'], ['browser_text'], ['name']);
252
+ const windowTarget = requiredAny(['window_id'], ['pid'], ['app'], ['window_title'], ['title']);
253
+ const semanticTarget = [...nativeTarget, ...browserElementTarget];
251
254
  next.allOf = [
255
+ {
256
+ if:{ properties:{ target:{ const:'window' } }, required:['target'] },
257
+ then:{
258
+ properties:{ action:{ enum:['type','focus','scroll','drag'] } },
259
+ anyOf:windowTarget,
260
+ },
261
+ },
252
262
  {
253
263
  if:{ properties:{ action:{ const:'click' } }, required:['action'] },
254
264
  then:{ anyOf:[...semanticTarget, { required:['ocr_text'] }, { required:['x','y'] }] },
@@ -259,7 +269,60 @@ function applySchemaRules(name, schema) {
259
269
  },
260
270
  {
261
271
  if:{ properties:{ action:{ const:'focus' } }, required:['action'] },
262
- then:{ anyOf:[...semanticTarget, ...requiredAny(['pid'], ['app'], ['window_title'], ['title'])] },
272
+ then:{ anyOf:[...semanticTarget, ...requiredAny(['window_id'], ['pid'], ['app'], ['window_title'], ['title'])] },
273
+ },
274
+ {
275
+ if:{ properties:{ action:{ const:'type' } }, required:['action'] },
276
+ then:{
277
+ required:['text'],
278
+ properties:{ target:{ enum:['auto','ui','ui_element','browser','window'] } },
279
+ allOf:[
280
+ { anyOf:[...semanticTarget, ...windowTarget] },
281
+ {
282
+ if:{ properties:{ target:{ enum:['ui','ui_element'] } }, required:['target'] },
283
+ then:{ anyOf:nativeTarget },
284
+ },
285
+ {
286
+ if:{ properties:{ target:{ const:'browser' } }, required:['target'] },
287
+ then:{ anyOf:browserElementTarget },
288
+ },
289
+ ],
290
+ },
291
+ },
292
+ {
293
+ if:{ properties:{ action:{ const:'scroll' } }, required:['action'] },
294
+ then:{
295
+ properties:{ target:{ enum:['auto','ui','ui_element','browser','coordinates','window'] } },
296
+ allOf:[
297
+ { anyOf:requiredAny(['delta_x'], ['delta_y'], ['delta'], ['direction']) },
298
+ {
299
+ if:{ properties:{ target:{ enum:['ui','ui_element'] } }, required:['target'] },
300
+ then:{ anyOf:nativeTarget },
301
+ },
302
+ {
303
+ if:{ properties:{ target:{ const:'coordinates' } }, required:['target'] },
304
+ then:{ required:['x','y'] },
305
+ },
306
+ ],
307
+ },
308
+ },
309
+ {
310
+ if:{ properties:{ action:{ const:'drag' } }, required:['action'] },
311
+ then:{
312
+ properties:{ target:{ enum:['auto','ui','ui_element','coordinates','window'] } },
313
+ allOf:[
314
+ { anyOf:requiredAny(['from_id'], ['from_x','from_y']) },
315
+ { anyOf:requiredAny(['to_id'], ['to_x','to_y']) },
316
+ {
317
+ if:{ properties:{ target:{ enum:['ui','ui_element'] } }, required:['target'] },
318
+ then:{ required:['from_id','to_id'] },
319
+ },
320
+ {
321
+ if:{ properties:{ target:{ const:'coordinates' } }, required:['target'] },
322
+ then:{ required:['from_x','from_y','to_x','to_y'] },
323
+ },
324
+ ],
325
+ },
263
326
  },
264
327
  {
265
328
  if:{ properties:{ action:{ enum:['set_value','set_range_value'] } }, required:['action'] },
@@ -357,8 +420,19 @@ function applySchemaRules(name, schema) {
357
420
  } else if (name === 'browser_action') {
358
421
  next.allOf = [
359
422
  {
360
- if:{ properties:{ action:{ enum:['click','focus','type','set_value','select','scroll_into_view'] } }, required:['action'] },
361
- then:{ anyOf:requiredAny(['selector'], ['text']) },
423
+ if:{ properties:{ action:{ enum:['click','focus','set_value','select','scroll_into_view'] } }, required:['action'] },
424
+ then:{ anyOf:requiredAny(['selector'], ['text'], ['browser_text']) },
425
+ },
426
+ {
427
+ if:{ properties:{ action:{ const:'type' } }, required:['action'] },
428
+ then:{
429
+ required:['text'],
430
+ anyOf:requiredAny(['selector'], ['browser_text'], ['value'], ['text_value']),
431
+ },
432
+ },
433
+ {
434
+ if:{ properties:{ action:{ const:'scroll' } }, required:['action'] },
435
+ then:{ anyOf:requiredAny(['delta_x'], ['delta_y'], ['delta'], ['direction']) },
362
436
  },
363
437
  {
364
438
  if:{ properties:{ action:{ const:'upload' } }, required:['action'] },
@@ -477,22 +551,38 @@ export const extendedToolDefinitions = [
477
551
  'Perform one semantic cross-backend action when the target may require Accessibility → browser DOM/CDP → OCR → coordinate fallback.',
478
552
  o({
479
553
  action:e([
480
- 'click','invoke','focus','set_value','select','toggle','expand','collapse',
554
+ 'click','type','invoke','focus','select','scroll','drag','set_value','toggle','expand','collapse',
481
555
  'scroll_into_view','set_range_value','add_to_selection','remove_from_selection',
482
556
  'multi_select','multi_edit',
483
557
  ]),
484
558
  target:e(
485
- ['auto','ui','ui_element','browser','ocr','coordinates'],
486
- 'Optional target type. auto tries native accessibility first, then browser DOM/CDP, OCR text when requested, then coordinates for click.',
559
+ ['auto','ui','ui_element','browser','ocr','coordinates','window'],
560
+ 'Optional target type. auto prefers native accessibility, then browser DOM/CDP, then OCR/coordinates where supported. Use window for explicit top-level window focus/text/scroll targeting.',
487
561
  ),
488
562
  ...uiSelector,
489
- title:s('Optional browser page title substring when target=browser.'),
490
- text:s('Text used by multi_edit or as a compatibility value for a semantic edit.'),
563
+ title:s('Optional browser page title substring or top-level window title when target=window.'),
564
+ window_id:s('Top-level window id returned by list_windows when target=window.'),
565
+ text:s('Unicode text to enter for action=type, or text used by multi_edit.'),
566
+ clear:b('For action=type, clear the current semantic/browser value before inserting text when supported.'),
491
567
  value:s('New value for set_value, set_range_value, or select.'),
492
- x:n('Virtual-desktop X coordinate used only by the final click fallback.'),
493
- y:n('Virtual-desktop Y coordinate used only by the final click fallback.'),
568
+ x:n('Virtual-desktop X coordinate used by click/scroll coordinate targeting.'),
569
+ y:n('Virtual-desktop Y coordinate used by click/scroll coordinate targeting.'),
494
570
  selector:s('CSS selector when the fallback target is a browser element.'),
495
- browser_text:s('Visible browser text used to locate a DOM fallback target.'),
571
+ browser_text:s('Visible browser text used only to locate a DOM target; for action=type, text remains the value to enter.'),
572
+ delta_x:n('Horizontal scroll delta; positive means right.'),
573
+ delta_y:n('Vertical scroll delta; positive means down.'),
574
+ delta:n('Alias for delta_y.'),
575
+ direction:e(['up','down','left','right']),
576
+ wheel_times:n('Number of 120-unit wheel steps for direction; 1..50.'),
577
+ from_x:n('Drag source X coordinate.'),
578
+ from_y:n('Drag source Y coordinate.'),
579
+ to_x:n('Drag destination X coordinate.'),
580
+ to_y:n('Drag destination Y coordinate.'),
581
+ from_id:s('Source semantic UI element id for action=drag.'),
582
+ to_id:s('Destination semantic UI element id for action=drag.'),
583
+ button:e(['left','right','middle']),
584
+ hold_ms:n('Delay after pointer down before moving during action=drag.'),
585
+ duration_ms:n('Drag movement duration.'),
496
586
  ocr_text:s('Visible text to find in rendered pixels for the OCR click fallback.'),
497
587
  ocr_region:{
498
588
  type:'array',
@@ -556,7 +646,7 @@ export const extendedToolDefinitions = [
556
646
  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']),
557
647
  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']),
558
648
  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']),
559
- 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('CSS selector for the target element; prefer the reusable selector returned by browser_find.'), text:s('For action=type, Unicode text to insert. For element actions without selector, visible text may identify the target.'), value:s('Value for action=set_value; also accepted as a compatibility fallback for action=type.'), text_value:s('Compatibility alias for the type/set_value text value.'), 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']),
649
+ define('browser_action', 'Browser element action', 'Click, focus, type/set a value, select, scroll a page or scrollable element, scroll an element into view, 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','scroll_into_view','upload','press','set_viewport']), selector:s('CSS selector for the target element; prefer the reusable selector returned by browser_find.'), browser_text:s('Visible text used only to locate a DOM element; use this instead of overloading text for action=type.'), text:s('For action=type, Unicode text to insert. For non-type element actions without selector/browser_text, visible text may identify the target.'), clear:b('For action=type, clear the current value before inserting text.'), value:s('Value for action=set_value; also accepted as a legacy type payload when text is used as the locator.'), text_value:s('Compatibility alias for the type/set_value text value.'), option:s(), delta_x:n('Horizontal scroll delta; positive means right.'), delta_y:n('Vertical scroll 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 for direction; 1..50.'), 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']),
560
650
  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']),
561
651
  define('browser_evaluate', 'Evaluate browser JavaScript', 'Evaluate JavaScript in a selected debuggable browser page and return its serializable value. This is a powerful escape hatch for page-specific automation.', o({ ...browserTarget, expression:s('JavaScript expression.'), await_promise:b('Await a returned Promise; default true.') }, ['expression']), openMutating, browserHandlers.browser_evaluate, ['browser_evaluate']),
562
652
 
@@ -1263,6 +1263,7 @@ function browserComputerAction(args, action) {
1263
1263
  set_value:'set_value',
1264
1264
  select:'select',
1265
1265
  type:'type',
1266
+ scroll:'scroll',
1266
1267
  }[action];
1267
1268
  if (!mapped) throw new Error(`Browser target does not support computer_action action: ${action}`);
1268
1269
  const visibleText = optionalString(args.browser_text)
@@ -1276,10 +1277,17 @@ function browserComputerAction(args, action) {
1276
1277
  timeout_ms: args.timeout_ms,
1277
1278
  action: mapped,
1278
1279
  selector: args.selector,
1279
- ...(visibleText ? { text: visibleText } : {}),
1280
+ ...(visibleText ? { browser_text: visibleText } : {}),
1280
1281
  ...(args.value != null ? { value: args.value } : {}),
1281
1282
  ...(args.option != null ? { option: args.option } : {}),
1282
- ...(action === 'type' && args.text != null ? { value: args.text } : {}),
1283
+ ...(action === 'type' && args.text != null ? { text: args.text, clear:args.clear === true } : {}),
1284
+ ...(action === 'scroll' ? {
1285
+ delta_x:args.delta_x,
1286
+ delta_y:args.delta_y,
1287
+ delta:args.delta,
1288
+ direction:args.direction,
1289
+ wheel_times:args.wheel_times,
1290
+ } : {}),
1283
1291
  });
1284
1292
  }
1285
1293
 
@@ -1362,21 +1370,84 @@ export async function computerAction(args = {}) {
1362
1370
  if (action === 'ui') return uiAction(nested);
1363
1371
  if (action === 'pointer') return pointer(nested);
1364
1372
  if (action === 'window') return windowAction(nested);
1365
- if (action === 'type') return target === 'browser' ? browserComputerAction(args, 'type') : typeText(args);
1373
+
1374
+ const windowTarget = () => ({
1375
+ ...args,
1376
+ ...(args.window_id ? { id:args.window_id } : {}),
1377
+ ...(args.window_title && !args.title ? { title:args.window_title } : {}),
1378
+ });
1379
+
1380
+ if (action === 'type') {
1381
+ if (target === 'browser') return browserComputerAction(args, 'type');
1382
+ if (target === 'ocr' || target === 'coordinates') {
1383
+ throw new Error('computer_action type requires a native UI, browser, or window target');
1384
+ }
1385
+ if (target === 'window') return typeText(windowTarget());
1386
+ if (!automatic) return typeText({ ...args, method:args.method || 'accessibility' });
1387
+
1388
+ const failures = [];
1389
+ if (hasUi) {
1390
+ try { return await typeText({ ...args, method:'accessibility' }); }
1391
+ catch (error) { failures.push(`accessibility: ${String(error?.message || error).slice(0, 300)}`); }
1392
+ }
1393
+ if (hasBrowser) {
1394
+ if (await browserCapabilityAvailable(args.endpoint, 300)) {
1395
+ try { return await browserComputerAction(args, 'type'); }
1396
+ catch (error) { failures.push(`browser DOM: ${String(error?.message || error).slice(0, 300)}`); }
1397
+ } else {
1398
+ failures.push('browser DOM: no local CDP endpoint is available');
1399
+ }
1400
+ }
1401
+ if (args.window_id || args.pid != null || args.app || args.window_title || args.title) {
1402
+ try { return await typeText(windowTarget()); }
1403
+ catch (error) { failures.push(`window: ${String(error?.message || error).slice(0, 300)}`); }
1404
+ }
1405
+ if (failures.length) throw new Error(`computer_action type targeting failed: ${failures.join('; ')}`);
1406
+ throw new Error('computer_action type requires a native UI, browser, or window target');
1407
+ }
1408
+
1366
1409
  if (action === 'keyboard') {
1367
1410
  if (target === 'browser' && args.key && !args.shortcut && !Array.isArray(args.keys)) {
1368
1411
  return browserAction({ ...args, action:'press' });
1369
1412
  }
1370
1413
  return keyboard(args);
1371
1414
  }
1415
+
1372
1416
  if (action === 'scroll') {
1373
- if (target === 'browser' && (args.selector || args.browser_text)) {
1374
- return browserAction({ ...args, action:'scroll_into_view', text:args.browser_text });
1417
+ if (target === 'browser') return browserComputerAction(args, 'scroll');
1418
+ if (target === 'ocr') throw new Error('computer_action scroll does not support OCR targets; use browser/UI/window/coordinates');
1419
+
1420
+ if (automatic) {
1421
+ let uiError = null;
1422
+ if (hasUi) {
1423
+ try { return await scroll(args); }
1424
+ catch (error) { uiError = error; }
1425
+ }
1426
+ if (hasBrowser) {
1427
+ if (await browserCapabilityAvailable(args.endpoint, 300)) return browserComputerAction(args, 'scroll');
1428
+ if (uiError) throw new Error(`computer_action scroll targeting failed: accessibility: ${String(uiError?.message || uiError).slice(0, 300)}; browser DOM: no local CDP endpoint is available`);
1429
+ throw new Error('computer_action scroll browser target is unavailable because no local CDP endpoint is available');
1430
+ }
1431
+ if (args.window_id || args.pid != null || args.app || args.window_title || args.title) {
1432
+ await windowAction({ ...windowTarget(), action:'focus' });
1433
+ }
1434
+ return scroll(args);
1435
+ }
1436
+
1437
+ if (target === 'window') {
1438
+ await windowAction({ ...windowTarget(), action:'focus' });
1439
+ return scroll(args);
1375
1440
  }
1376
- if (args.id || args.name || args.role || args.automation_id) await uiAction({ ...args, action:'focus' }).catch(() => null);
1377
1441
  return scroll(args);
1378
1442
  }
1379
- if (action === 'drag') return dragDrop(args);
1443
+
1444
+ if (action === 'drag') {
1445
+ if (target === 'browser' || target === 'ocr') {
1446
+ throw new Error('computer_action drag currently requires native UI element ids or desktop coordinates; browser/OCR drag is not advertised');
1447
+ }
1448
+ if (target === 'window') await windowAction({ ...windowTarget(), action:'focus' });
1449
+ return dragDrop(args);
1450
+ }
1380
1451
  if (action === 'clipboard') return clipboard(nested);
1381
1452
  if (action === 'launch_app') return launchApp(args);
1382
1453
  if (action === 'open_path') return openPath(args);
@@ -1423,7 +1494,7 @@ export async function computerAction(args = {}) {
1423
1494
  }
1424
1495
  return uiAction({ ...args, action:'focus' });
1425
1496
  }
1426
- return windowAction({ ...args, action:'focus' });
1497
+ return windowAction({ ...windowTarget(), action:'focus' });
1427
1498
  }
1428
1499
 
1429
1500
  if (['minimize','maximize','restore','move_resize','resize','close'].includes(action)) {
@@ -32,6 +32,11 @@ function durationSpec(value) {
32
32
  return raw;
33
33
  }
34
34
 
35
+ export function macosLaunchctlDomain(scope = 'user', uid = process.getuid?.() ?? 0) {
36
+ const normalized = requireEnum(scope, 'scope', ['user','system']);
37
+ return normalized === 'system' ? 'system' : `gui/${uid}`;
38
+ }
39
+
35
40
  export async function serviceTool(args) {
36
41
  const action = requireEnum(args.action || 'list', 'action', ['list','status','start','stop','restart']);
37
42
  const name = optionalString(args.name);
@@ -55,8 +60,8 @@ export async function serviceTool(args) {
55
60
  }
56
61
 
57
62
  if (process.platform === 'darwin') {
58
- if (action === 'list') return text((await runFile('/bin/launchctl', ['list'], { label: 'launchctl list' })).stdout);
59
- const domain = scope === 'system' ? 'system' : `gui/${process.getuid?.() ?? 0}`;
63
+ const domain = macosLaunchctlDomain(scope);
64
+ if (action === 'list') return text((await runFile('/bin/launchctl', ['print', domain], { label: `launchctl print ${domain}` })).stdout);
60
65
  const target = `${domain}/${name}`;
61
66
  if (action === 'status') {
62
67
  const result = await runFile('/bin/launchctl', ['print', target], { label: 'launchctl print', allowFailure: true });