@remcp/runtime 0.2.178 → 0.2.179

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.178",
3
+ "version": "0.2.179",
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",
@@ -4,6 +4,7 @@ import { jsonResult, clamp, requireEnum } from './common.mjs';
4
4
  import { withTarget } from './browser-session.mjs';
5
5
  import { inspectNativeBrowserAudit } from './browser-native-audit.mjs';
6
6
  import { inspectDomGeometry } from './browser-geometry.mjs';
7
+ import { inspectElementListeners } from './browser-listeners.mjs';
7
8
 
8
9
  const METRIC_NAMES = new Set([
9
10
  'Timestamp','Documents','Frames','JSEventListeners','Nodes','LayoutCount',
@@ -32,7 +33,7 @@ export function normalizeDevtoolsMetrics(entries) {
32
33
  .filter(item => METRIC_NAMES.has(item?.name) && Number.isFinite(item?.value))
33
34
  .map(item => [item.name, item.value]));
34
35
  }
35
- export async function sampleBrowserDevtools(session, action, durationMs, maxEvents) {
36
+ export async function sampleBrowserDevtools(session, action, durationMs, maxEvents, selector) {
36
37
  const events = [];
37
38
  const counts = {};
38
39
  const requests = new Map();
@@ -43,6 +44,7 @@ export async function sampleBrowserDevtools(session, action, durationMs, maxEven
43
44
  }
44
45
  if (action === 'layout' || action === 'accessibility') return inspectNativeBrowserAudit(session, action);
45
46
  if (action === 'geometry') return inspectDomGeometry(session);
47
+ if (action === 'listeners') return inspectElementListeners(session,selector);
46
48
  if (action === 'performance') {
47
49
  await session.send('Performance.enable');
48
50
  try {
@@ -103,14 +105,16 @@ export async function sampleBrowserDevtools(session, action, durationMs, maxEven
103
105
  }
104
106
  export function buildDevtoolsReport(target, action, result) {
105
107
  return { target_id:target.id,
106
- ...(action === 'geometry' ? {} : {url:safeDevtoolsUrl(target.url)}),
108
+ ...(['geometry','listeners'].includes(action) ? {} : {url:safeDevtoolsUrl(target.url)}),
107
109
  ...result };
108
110
  }
109
111
  export async function browserDevtools(args = {}) {
110
- const action = requireEnum(args.action, 'action', ['console','network','performance','layout','accessibility','geometry']);
112
+ const action = requireEnum(args.action, 'action', ['console','network','performance','layout','accessibility','geometry','listeners']);
111
113
  const durationMs = clamp(args.sample_ms, 1500, 250, 5000);
112
114
  const maxEvents = clamp(args.max_events, 50, 1, 100);
115
+ if (action === 'listeners' && (typeof args.target_id !== 'string' || !args.target_id.trim()))
116
+ throw new TypeError('Listener diagnostics require an explicitly selected target_id');
113
117
  return withTarget(args, async (session,target) =>
114
118
  jsonResult(buildDevtoolsReport(target, action,
115
- await sampleBrowserDevtools(session,action,durationMs,maxEvents))));
119
+ await sampleBrowserDevtools(session,action,durationMs,maxEvents,args.selector))));
116
120
  }
@@ -0,0 +1,65 @@
1
+ // Native CDP event-listener audit for one explicitly selected DOM element.
2
+ // CDP returns handler references, source positions and custom listener names:
3
+ // NONE of that may leave the paired device. Never evaluate page JavaScript.
4
+ const TYPES = new Set([
5
+ 'click','dblclick','auxclick','contextmenu','input','change','beforeinput',
6
+ 'submit','reset','keydown','keyup','keypress','focus','blur','focusin','focusout',
7
+ 'mousedown','mouseup','mousemove','mouseenter','mouseleave',
8
+ 'pointerdown','pointerup','pointermove','pointerenter','pointerleave',
9
+ 'touchstart','touchend','touchmove','wheel','scroll','resize','load','error',
10
+ 'drag','dragstart','dragend','dragover','drop','animationend','transitionend',
11
+ ]);
12
+ const MAX_LISTENERS = 200;
13
+ const MAX_TYPES = 24;
14
+ function validSelector(value) {
15
+ if (typeof value !== 'string') throw new TypeError('A CSS selector is required for the event listener audit');
16
+ const selector = value.trim();
17
+ if (!selector || selector.length > 120 || /[\u0000-\u001f\u007f<>]/.test(selector)) {
18
+ throw new TypeError('A CSS selector between 1 and 120 safe characters is required');
19
+ }
20
+ return selector;
21
+ }
22
+ export function summarizeElementListeners(raw) {
23
+ const listeners = Array.isArray(raw) ? raw : [];
24
+ const sample = listeners.slice(0,MAX_LISTENERS);
25
+ const types = Object.create(null);
26
+ let passive = 0, once = 0, capturing = 0;
27
+ for (const listener of sample) {
28
+ if (!listener || typeof listener !== 'object') continue;
29
+ const eventType = TYPES.has(listener.type) ? listener.type : 'other';
30
+ types[eventType] = (types[eventType] || 0) + 1;
31
+ if (listener.passive === true) passive++;
32
+ if (listener.once === true) once++;
33
+ if (listener.useCapture === true) capturing++;
34
+ }
35
+ const byType = Object.entries(types).sort((a,b)=>b[1]-a[1] || a[0].localeCompare(b[0])).slice(0,MAX_TYPES);
36
+ return {
37
+ sampled_listeners:sample.length,
38
+ listener_count:Math.min(listeners.length,100000),
39
+ truncated:listeners.length>MAX_LISTENERS,
40
+ by_type:Object.fromEntries(byType),
41
+ passive_count:passive, once_count:once, capture_count:capturing,
42
+ repeated_event_types:byType.filter(([,count])=>count>1).length,
43
+ note:'Native CDP listener counts on the single selected node. Repeated event types are not proof of duplicate handlers. Delegated ancestor listeners are not included; handler code, locations, custom event names and page contents are never returned.',
44
+ };
45
+ }
46
+ export async function inspectElementListeners(session,selectorValue) {
47
+ const selector=validSelector(selectorValue);
48
+ // getDocument(depth:0) is enough to get the root without enumerating contents.
49
+ const {root} = await session.send('DOM.getDocument',{depth:0,pierce:false});
50
+ if (!Number.isSafeInteger(root?.nodeId) || root.nodeId<=0) throw new Error('CDP document root unavailable');
51
+ let objectId;
52
+ try {
53
+ const selected = await session.send('DOM.querySelector',{nodeId:root.nodeId,selector});
54
+ if (!Number.isSafeInteger(selected?.nodeId) || selected.nodeId<=0)
55
+ return {action:'listeners',found:false,listeners:summarizeElementListeners([])};
56
+ const resolved=await session.send('DOM.resolveNode',{nodeId:selected.nodeId});
57
+ objectId=resolved?.object?.objectId;
58
+ if (typeof objectId!=='string' || !objectId || objectId.length>512) throw new Error('Selected DOM node cannot be resolved');
59
+ const result=await session.send('DOMDebugger.getEventListeners',{objectId,depth:0,pierce:false});
60
+ return {action:'listeners',found:true,listeners:summarizeElementListeners(result.listeners)};
61
+ } finally {
62
+ if (objectId) await session.send('Runtime.releaseObject',{objectId}).catch(()=>{});
63
+ await session.send('DOM.disable').catch(()=>{});
64
+ }
65
+ }
@@ -167,7 +167,7 @@ export const extendedToolDefinitions = [
167
167
  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() }), readOnlyOpen, browserHandlers.browser_find, ['browser_cdp']),
168
168
  define('browser_action', 'Browser element action', 'Click, focus, type/set a value, fill up to 20 form fields in one guarded call, 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','fill_form','workflow']), selector:s('CSS selector for the target element; prefer the reusable selector returned by browser_find.'), steps:{type:'array',minItems:1,maxItems:12,description:'Workflow on one page: explicit CSS actions and bounded waits, max 12 seconds. Never silently submits; click must be explicit.',items:{type:'object',additionalProperties:false,properties:{action:e(['fill_form','set_value','select','focus','click','wait_for']),selector:s('CSS selector for action or wait_for.'),value:{type:['string','number','boolean']},fields:{type:'array',description:'CSS-selected field edits within the workflow step; no implicit form submission.',minItems:1,maxItems:20,items:{type:'object',additionalProperties:false,properties:{selector:s(),value:{type:['string','number','boolean']}},required:['selector','value']}},condition:e(['selector','text','url_contains','load']),text:s('Text or URL fragment for wait condition.'),timeout_ms:n('Wait timeout 100..4000 ms; default 1500.')},required:['action']}}, fields:{type:'array',minItems:1,maxItems:20,items:{type:'object',additionalProperties:false,properties:{selector:s('CSS selector.'),value:{type:['string','boolean','number'],description:'New field value; not returned in results.'}},required:['selector','value']},description:'fill_form: 1–20 CSS form fields in one CDP call. Does not submit.'}, 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']),
169
169
  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.') }), readOnlyOpen, browserHandlers.browser_wait, ['browser_cdp']),
170
- define('browser_devtools', 'Browser DevTools diagnostics', 'Use this to inspect Chrome Console/Network live events, Performance, Layout, Accessibility or screenshot-free DOMSnapshot Geometry via native CDP. Network headers, bodies and cookies are excluded; geometry returns bounded numeric summaries without page text.', o({ ...browserTarget, action:e(['console','network','performance','layout','accessibility','geometry'],'Diagnostics mode. Layout measures CSS viewport, accessibility returns anonymized roles and unnamed controls, geometry audits DOMSnapshot bounds without page contents.'), sample_ms:n('Live event capture period 250–5000 ms; default 1500. Ignored for performance, layout, accessibility and geometry.'), max_events:n('Maximum 1–100 events retained; default 50.') }, ['action']), readOnlyOpen, browserHandlers.browser_devtools, ['browser_cdp']),
170
+ define('browser_devtools', 'Browser DevTools diagnostics', 'Use this to inspect Chrome Console/Network live events, Performance, Layout, Accessibility or screenshot-free DOMSnapshot Geometry via native CDP. Network headers, bodies and cookies are excluded; geometry returns bounded numeric summaries without page text.', o({ ...browserTarget, action:e(['console','network','performance','layout','accessibility','geometry','listeners'],'Diagnostics mode. Layout measures CSS viewport, accessibility returns anonymized roles and unnamed controls, geometry audits DOMSnapshot bounds without page contents.'), sample_ms:n('Live event capture period 250–5000 ms; default 1500. Ignored for performance, layout, accessibility, geometry and listeners.'), selector:s('Required CSS selector for action=listeners; only the first matching element is inspected, with no script source or DOM text returned.'), max_events:n('Maximum 1–100 events retained; default 50.') }, ['action']), readOnlyOpen, browserHandlers.browser_devtools, ['browser_cdp']),
171
171
  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']),
172
172
 
173
173
  define('service', 'Service control', 'List, inspect, start, stop, or restart operating-system services using Service Control Manager, launchctl, or systemd. Mutating actions pass through ReMCP command policy.', o({ action:e(['list','status','start','stop','restart']), name:s('Service/unit/launchd label.'), scope:e(['user','system']) }, ['action']), mutating, diagnosticHandlers.service, ['services']),