@remcp/runtime 0.2.177 → 0.2.178
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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
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
|
+
import { inspectDomGeometry } from './browser-geometry.mjs';
|
|
6
7
|
|
|
7
8
|
const METRIC_NAMES = new Set([
|
|
8
9
|
'Timestamp','Documents','Frames','JSEventListeners','Nodes','LayoutCount',
|
|
@@ -41,6 +42,7 @@ export async function sampleBrowserDevtools(session, action, durationMs, maxEven
|
|
|
41
42
|
if (events.length < maxEvents) events.push(event);
|
|
42
43
|
}
|
|
43
44
|
if (action === 'layout' || action === 'accessibility') return inspectNativeBrowserAudit(session, action);
|
|
45
|
+
if (action === 'geometry') return inspectDomGeometry(session);
|
|
44
46
|
if (action === 'performance') {
|
|
45
47
|
await session.send('Performance.enable');
|
|
46
48
|
try {
|
|
@@ -99,11 +101,16 @@ export async function sampleBrowserDevtools(session, action, durationMs, maxEven
|
|
|
99
101
|
return { action, sampled_ms:durationMs, counts, events, truncated:Object.values(counts).reduce((a,b) => a+b,0) > maxEvents,
|
|
100
102
|
note:'Live events only; DevTools does not backfill events recorded before sampling. Request/response contents and headers are excluded.' };
|
|
101
103
|
}
|
|
104
|
+
export function buildDevtoolsReport(target, action, result) {
|
|
105
|
+
return { target_id:target.id,
|
|
106
|
+
...(action === 'geometry' ? {} : {url:safeDevtoolsUrl(target.url)}),
|
|
107
|
+
...result };
|
|
108
|
+
}
|
|
102
109
|
export async function browserDevtools(args = {}) {
|
|
103
|
-
const action = requireEnum(args.action, 'action', ['console','network','performance','layout','accessibility']);
|
|
110
|
+
const action = requireEnum(args.action, 'action', ['console','network','performance','layout','accessibility','geometry']);
|
|
104
111
|
const durationMs = clamp(args.sample_ms, 1500, 250, 5000);
|
|
105
112
|
const maxEvents = clamp(args.max_events, 50, 1, 100);
|
|
106
113
|
return withTarget(args, async (session,target) =>
|
|
107
|
-
jsonResult(
|
|
108
|
-
|
|
114
|
+
jsonResult(buildDevtoolsReport(target, action,
|
|
115
|
+
await sampleBrowserDevtools(session,action,durationMs,maxEvents))));
|
|
109
116
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Local-only Chrome DOMSnapshot geometry aggregation.
|
|
2
|
+
// DOMSnapshot can contain page text and attributes: NEVER serialize, log or return it.
|
|
3
|
+
// The result contains counts and bounded geometry of tag types, not DOM content.
|
|
4
|
+
const ALLOWED_TAGS = new Set([
|
|
5
|
+
'A','ARTICLE','ASIDE','BLOCKQUOTE','BUTTON','CANVAS','CODE','DD','DETAILS','DIV',
|
|
6
|
+
'DL','DT','FIELDSET','FIGCAPTION','FIGURE','FOOTER','FORM','H1','H2','H3','H4',
|
|
7
|
+
'H5','H6','HEADER','IFRAME','IMG','INPUT','LABEL','LI','MAIN','NAV','OL','OPTION',
|
|
8
|
+
'P','PICTURE','PRE','SECTION','SELECT','SPAN','SVG','TABLE','TBODY','TD',
|
|
9
|
+
'TEXTAREA','TH','THEAD','TR','UL','VIDEO',
|
|
10
|
+
]);
|
|
11
|
+
const MAX_LAYOUT_NODES = 12000;
|
|
12
|
+
const MAX_CANDIDATES = 8;
|
|
13
|
+
const EPSILON = 1;
|
|
14
|
+
function decimal(value) {
|
|
15
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
16
|
+
? Math.max(-10000000, Math.min(10000000, Math.round(value * 100) / 100))
|
|
17
|
+
: null;
|
|
18
|
+
}
|
|
19
|
+
function typeForNode(doc, strings, nodeIndex) {
|
|
20
|
+
const idx = doc?.nodes?.nodeName?.[nodeIndex];
|
|
21
|
+
if (!Number.isSafeInteger(idx) || !Array.isArray(strings)) return '';
|
|
22
|
+
const str = strings[idx];
|
|
23
|
+
if (typeof str !== 'string' || str.length > 20) return '';
|
|
24
|
+
const upper = str.toUpperCase();
|
|
25
|
+
return ALLOWED_TAGS.has(upper) ? upper.toLowerCase() : '';
|
|
26
|
+
}
|
|
27
|
+
export function summarizeDomGeometry(snapshot, pageMetrics, { maxNodes = MAX_LAYOUT_NODES } = {}) {
|
|
28
|
+
const rawDocs = Array.isArray(snapshot?.documents) ? snapshot.documents : [];
|
|
29
|
+
const doc = rawDocs[0] || {};
|
|
30
|
+
const indices = Array.isArray(doc?.layout?.nodeIndex) ? doc.layout.nodeIndex : [];
|
|
31
|
+
const bounds = Array.isArray(doc?.layout?.bounds) ? doc.layout.bounds : [];
|
|
32
|
+
const width = decimal(pageMetrics?.cssLayoutViewport?.clientWidth);
|
|
33
|
+
const scrollX = decimal(pageMetrics?.cssLayoutViewport?.pageX) ?? 0;
|
|
34
|
+
if (width == null || width <= 0) throw new Error('Chrome did not report a valid layout viewport width');
|
|
35
|
+
const left = scrollX, right = scrollX + width;
|
|
36
|
+
const count = Math.min(MAX_LAYOUT_NODES, Math.max(0,Math.floor(Number(maxNodes) || MAX_LAYOUT_NODES)),indices.length,bounds.length);
|
|
37
|
+
let eligible = 0, candidateCount = 0, leftOverflowCount = 0, rightOverflowCount = 0;
|
|
38
|
+
let biggestOverflow = 0;
|
|
39
|
+
const byTag = Object.create(null);
|
|
40
|
+
const top = [];
|
|
41
|
+
for (let i = 0; i < count; i++) {
|
|
42
|
+
const tag = typeForNode(doc,snapshot?.strings,indices[i]);
|
|
43
|
+
if (!tag) continue;
|
|
44
|
+
const box = bounds[i];
|
|
45
|
+
if (!Array.isArray(box) || box.length < 4) continue;
|
|
46
|
+
const [x,y,w,h] = box.map(decimal);
|
|
47
|
+
if ([x,y,w,h].some(v => v == null) || w <= 0 || h <= 0) continue;
|
|
48
|
+
eligible++;
|
|
49
|
+
const extraLeft = Math.max(0,left-x);
|
|
50
|
+
const extraRight = Math.max(0,x+w-right);
|
|
51
|
+
if (extraLeft <= EPSILON && extraRight <= EPSILON) continue;
|
|
52
|
+
candidateCount++;
|
|
53
|
+
if (extraLeft > EPSILON) leftOverflowCount++;
|
|
54
|
+
if (extraRight > EPSILON) rightOverflowCount++;
|
|
55
|
+
const overflow = Math.ceil(Math.max(extraLeft,extraRight));
|
|
56
|
+
biggestOverflow = Math.max(biggestOverflow,overflow);
|
|
57
|
+
byTag[tag] = (byTag[tag] || 0) + 1;
|
|
58
|
+
// Never send selectors, attributes, text, sources, titles or URLs.
|
|
59
|
+
const candidate = { tag, side:extraLeft > EPSILON && extraRight > EPSILON ? 'both' :
|
|
60
|
+
extraLeft > EPSILON ? 'left' : 'right', overflow_px:overflow,
|
|
61
|
+
bounds: { x, y, width:w, height:h } };
|
|
62
|
+
if (top.length < MAX_CANDIDATES) top.push(candidate);
|
|
63
|
+
else {
|
|
64
|
+
let smallest = 0;
|
|
65
|
+
for (let j=1; j<top.length; j++) if (top[j].overflow_px < top[smallest].overflow_px) smallest = j;
|
|
66
|
+
if (candidate.overflow_px > top[smallest].overflow_px) top[smallest] = candidate;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
top.sort((a,b)=>b.overflow_px-a.overflow_px);
|
|
70
|
+
return {
|
|
71
|
+
viewport_width_px:width,
|
|
72
|
+
sampled_layout_nodes:count,
|
|
73
|
+
eligible_elements:eligible,
|
|
74
|
+
candidate_count:candidateCount,
|
|
75
|
+
left_overflow_count:leftOverflowCount,
|
|
76
|
+
right_overflow_count:rightOverflowCount,
|
|
77
|
+
maximum_overflow_px:biggestOverflow,
|
|
78
|
+
by_tag:Object.fromEntries(Object.entries(byTag).sort((a,b)=>b[1]-a[1]).slice(0,20)),
|
|
79
|
+
candidates:top,
|
|
80
|
+
frames_omitted:Math.max(0,rawDocs.length-1),
|
|
81
|
+
truncated:count<indices.length || count<bounds.length,
|
|
82
|
+
note:'Only numeric geometry and allowlisted HTML tag types leave the device. Candidate bounds may be intentional (for example carousels or offscreen animations); this is not proof of a layout bug. Only the main frame is inspected.',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function inspectDomGeometry(session) {
|
|
86
|
+
// Fetch just enough geometry for local assessment. The CDP transport enforces
|
|
87
|
+
// its own 16 MiB message bound; snapshot text never leaves this process.
|
|
88
|
+
const [metrics,snapshot] = await Promise.all([
|
|
89
|
+
session.send('Page.getLayoutMetrics'),
|
|
90
|
+
session.send('DOMSnapshot.captureSnapshot',{
|
|
91
|
+
computedStyles:[],includePaintOrder:false,includeDOMRects:false,
|
|
92
|
+
}),
|
|
93
|
+
]);
|
|
94
|
+
return {action:'geometry',geometry:summarizeDomGeometry(snapshot,metrics)};
|
|
95
|
+
}
|
package/src/extended/catalog.mjs
CHANGED
|
@@ -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
|
|
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']),
|
|
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']),
|