@remcp/runtime 0.2.156 → 0.2.158
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 +1 -1
- package/src/extended/browser-actions.mjs +84 -4
- package/src/extended/browser-endpoint.mjs +7 -4
- package/src/extended/browser-session.mjs +5 -20
- package/src/extended/browser-workflow.mjs +111 -0
- package/src/extended/catalog-schemas.mjs +1 -1
- package/src/extended/catalog.mjs +1 -1
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ import { clamp, commandExists, jsonResult, optionalString, requireEnum, spawnDet
|
|
|
12
12
|
import { assertBrowserRemoteEnabled, assertSafeEvaluationExpression } from './browser-endpoint.mjs';
|
|
13
13
|
import { callPageFunction, evaluate, withTarget } from './browser-session.mjs';
|
|
14
14
|
import { browserNavigate, browserSnapshot, browserTabs, findExpression } from './browser-read.mjs';
|
|
15
|
+
import { executeBrowserWorkflow, validateBrowserWorkflow } from './browser-workflow.mjs';
|
|
15
16
|
|
|
16
17
|
export async function browserFind(args) {
|
|
17
18
|
if (!(args.selector || args.text || args.role)) throw new Error('browser_find requires selector, text, or role; use browser_snapshot to enumerate page structure');
|
|
@@ -89,15 +90,81 @@ export const PAGE_ELEMENT_ACTION = `function(selector, needle, action, value) {
|
|
|
89
90
|
el.dispatchEvent(new Event('input', {bubbles:true}));
|
|
90
91
|
el.dispatchEvent(new Event('change', {bubbles:true}));
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
-
if (action === 'click'
|
|
93
|
+
let b = el.getBoundingClientRect();
|
|
94
|
+
if (action === 'click') {
|
|
95
|
+
// An inline element can span several text fragments. Its bounding box center
|
|
96
|
+
// can point to a completely unrelated input (or even a destructive button).
|
|
97
|
+
// Only return a point whose actual hit target belongs to this element.
|
|
98
|
+
const rectangles = Array.from(el.getClientRects());
|
|
99
|
+
const safe = rectangles.find(rect => {
|
|
100
|
+
if (!rect.width || !rect.height) return false;
|
|
101
|
+
const x = rect.x + rect.width / 2, y = rect.y + rect.height / 2;
|
|
102
|
+
const target = document.elementFromPoint(x, y);
|
|
103
|
+
return target && (target === el || el.contains(target));
|
|
104
|
+
});
|
|
105
|
+
if (!safe) throw new Error('Element has no unobstructed clickable fragment');
|
|
106
|
+
b = safe;
|
|
107
|
+
}
|
|
94
108
|
return {
|
|
95
109
|
tag: el.tagName.toLowerCase(),
|
|
96
|
-
|
|
110
|
+
// Never expose password or credential field contents in tool responses.
|
|
111
|
+
value: 'value' in el && !el.matches('input[type="password"],input[autocomplete="current-password"],input[autocomplete="new-password"],input[autocomplete="one-time-code"]') ?
|
|
112
|
+
String(el.value) : '',
|
|
97
113
|
x: b.x, y: b.y, width: b.width, height: b.height,
|
|
98
114
|
};
|
|
99
115
|
}`;
|
|
100
116
|
|
|
117
|
+
// Bounded form mutation: validate every target before changing any field.
|
|
118
|
+
// Values are intentionally excluded from the return object and tool logs.
|
|
119
|
+
export const PAGE_FILL_FORM = `function(fields) {
|
|
120
|
+
const targets = fields.map(field => {
|
|
121
|
+
const el = document.querySelector(field.selector);
|
|
122
|
+
if (!el || !el.getClientRects().length || el.disabled || el.readOnly ||
|
|
123
|
+
el.matches('input[type="file"],input[type="submit"],input[type="reset"],button')) {
|
|
124
|
+
throw new Error('Form field unavailable or not editable: ' + field.selector);
|
|
125
|
+
}
|
|
126
|
+
const tag = el.tagName.toLowerCase();
|
|
127
|
+
if (!['input','textarea','select'].includes(tag)) throw new Error('Unsupported form field: ' + field.selector);
|
|
128
|
+
if (tag === 'select' && !Array.from(el.options).some(option => option.value === String(field.value))) {
|
|
129
|
+
throw new Error('No matching select option: ' + field.selector);
|
|
130
|
+
}
|
|
131
|
+
return {el,tag,value:field.value};
|
|
132
|
+
});
|
|
133
|
+
const summary = [];
|
|
134
|
+
for (const {el,tag,value} of targets) {
|
|
135
|
+
const checkbox = tag === 'input' && ['checkbox','radio'].includes(el.type);
|
|
136
|
+
const prop = checkbox ? 'checked' : 'value';
|
|
137
|
+
let prototype = Object.getPrototypeOf(el);
|
|
138
|
+
let setter;
|
|
139
|
+
while (prototype && !setter) {
|
|
140
|
+
setter = Object.getOwnPropertyDescriptor(prototype, prop)?.set;
|
|
141
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
142
|
+
}
|
|
143
|
+
const next = checkbox ? Boolean(value) : String(value);
|
|
144
|
+
if (setter) setter.call(el, next); else el[prop] = next;
|
|
145
|
+
el.dispatchEvent(new Event('input', {bubbles:true}));
|
|
146
|
+
el.dispatchEvent(new Event('change', {bubbles:true}));
|
|
147
|
+
summary.push({tag, changed:true});
|
|
148
|
+
}
|
|
149
|
+
return {filled:summary.length, fields:summary};
|
|
150
|
+
}`;
|
|
151
|
+
|
|
152
|
+
export function validateFormFields(fields) {
|
|
153
|
+
if (!Array.isArray(fields) || fields.length < 1 || fields.length > 20) {
|
|
154
|
+
throw new Error('fill_form requires 1 to 20 fields');
|
|
155
|
+
}
|
|
156
|
+
return fields.map(field => {
|
|
157
|
+
const selector = typeof field?.selector === 'string' ? field.selector.trim() : '';
|
|
158
|
+
if (!selector || selector.length > 512 || !Object.hasOwn(field,'value') ||
|
|
159
|
+
!['string','boolean','number'].includes(typeof field.value) ||
|
|
160
|
+
(typeof field.value === 'number' && !Number.isFinite(field.value))) {
|
|
161
|
+
throw new Error('Each form field requires a CSS selector and a string/boolean/number value');
|
|
162
|
+
}
|
|
163
|
+
if (String(field.value).length > 16384) throw new Error('Form field value exceeds 16384 characters');
|
|
164
|
+
return {selector, value:field.value};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
101
168
|
export function browserActionLocatorText(args = {}, action = '') {
|
|
102
169
|
const explicit = optionalString(args.browser_text || args.match_text);
|
|
103
170
|
if (explicit) return explicit;
|
|
@@ -137,8 +204,21 @@ export function browserScrollDeltas(args = {}) {
|
|
|
137
204
|
}
|
|
138
205
|
|
|
139
206
|
export async function browserAction(args) {
|
|
140
|
-
const action = requireEnum(args.action, 'action', ['click', 'focus', 'type', 'set_value', 'select', 'scroll', 'scroll_into_view', 'upload', 'press', 'set_viewport']);
|
|
207
|
+
const action = requireEnum(args.action, 'action', ['click', 'focus', 'type', 'set_value', 'select', 'scroll', 'scroll_into_view', 'upload', 'press', 'set_viewport', 'fill_form', 'workflow']);
|
|
208
|
+
const formFields = action === 'fill_form' ? validateFormFields(args.fields) : null;
|
|
209
|
+
const workflowSteps = action === 'workflow' ?
|
|
210
|
+
validateBrowserWorkflow(args.steps, {validateFormFields}) : null;
|
|
141
211
|
return withTarget(args, async (session, target) => {
|
|
212
|
+
if (action === 'workflow') {
|
|
213
|
+
const result = await executeBrowserWorkflow(session, workflowSteps, {
|
|
214
|
+
pageElementAction:PAGE_ELEMENT_ACTION, pageFillForm:PAGE_FILL_FORM,
|
|
215
|
+
});
|
|
216
|
+
return jsonResult({target_id:target.id, action, result});
|
|
217
|
+
}
|
|
218
|
+
if (action === 'fill_form') {
|
|
219
|
+
const result = await callPageFunction(session, PAGE_FILL_FORM, [formFields]);
|
|
220
|
+
return jsonResult({target_id:target.id, action, result});
|
|
221
|
+
}
|
|
142
222
|
if (action === 'set_viewport') {
|
|
143
223
|
const width = clamp(args.width, 1280, 200, 8192);
|
|
144
224
|
const height = clamp(args.height, 720, 200, 8192);
|
|
@@ -386,11 +386,12 @@ export async function browserCapabilityAvailable(endpoint, timeoutMs = 500) {
|
|
|
386
386
|
// Validating every tab sequentially can require hundreds of serial DNS lookups,
|
|
387
387
|
// making browser_tabs exceed the relay's deadline on machines with many tabs.
|
|
388
388
|
// Keep DNS and security validation, but bound both the work and its concurrency.
|
|
389
|
-
export async function filterBrowserTargets(list, { validate = assertResolvedPageUrl } = {}) {
|
|
389
|
+
export async function filterBrowserTargets(list, { validate = assertResolvedPageUrl, targetId = null } = {}) {
|
|
390
390
|
if (!Array.isArray(list)) return [];
|
|
391
391
|
const candidates = [];
|
|
392
392
|
for (const item of list) {
|
|
393
393
|
if (!item || item.type !== 'page' || !item.webSocketDebuggerUrl) continue;
|
|
394
|
+
if (targetId && item.id !== targetId) continue;
|
|
394
395
|
candidates.push(item);
|
|
395
396
|
if (candidates.length >= 1000) break;
|
|
396
397
|
}
|
|
@@ -414,7 +415,7 @@ export async function filterBrowserTargets(list, { validate = assertResolvedPage
|
|
|
414
415
|
return targets;
|
|
415
416
|
}
|
|
416
417
|
|
|
417
|
-
export async function listBrowserTargets(endpoint) {
|
|
418
|
+
export async function listBrowserTargets(endpoint, { targetId = null } = {}) {
|
|
418
419
|
assertBrowserRemoteEnabled();
|
|
419
420
|
// Reading tabs and page semantics must not launch a separate Chromium/profile.
|
|
420
421
|
// Only an explicit browser_navigate(action=new_tab) can bootstrap an isolated browser.
|
|
@@ -431,7 +432,7 @@ export async function listBrowserTargets(endpoint) {
|
|
|
431
432
|
throw new Error(`${label} is unavailable at ${base.origin}. Read-only browser_tabs and browser_snapshot do not launch a browser. Use ui_snapshot and ui_find for the existing browser window; do not retry this CDP endpoint unchanged. Only browser_navigate(action=new_tab), when explicitly requested, may start a local isolated browser.`);
|
|
432
433
|
}
|
|
433
434
|
// Retain the current bounded concurrent DNS/security checks for up to 1000 tabs.
|
|
434
|
-
return filterBrowserTargets(list);
|
|
435
|
+
return filterBrowserTargets(list, { targetId });
|
|
435
436
|
}
|
|
436
437
|
|
|
437
438
|
export function validatedTargetWebSocketUrl(target, endpoint) {
|
|
@@ -449,8 +450,10 @@ export function validatedTargetWebSocketUrl(target, endpoint) {
|
|
|
449
450
|
}
|
|
450
451
|
|
|
451
452
|
export async function chooseTarget(args = {}) {
|
|
452
|
-
const targets = (await listBrowserTargets(args.endpoint)).filter(item => item.type === 'page' && item.webSocketDebuggerUrl);
|
|
453
453
|
const id = optionalString(args.target_id || args.targetId);
|
|
454
|
+
// Explicit target IDs are returned by browser_tabs. Never DNS-resolve every
|
|
455
|
+
// unrelated tab merely to act on one target: validate only its actual URL.
|
|
456
|
+
const targets = await listBrowserTargets(args.endpoint, { targetId:id });
|
|
454
457
|
const title = optionalString(args.title);
|
|
455
458
|
const urlContains = optionalString(args.url_contains || args.urlContains);
|
|
456
459
|
const matched = targets.find(item =>
|
|
@@ -390,24 +390,9 @@ export async function evaluate(session, expression, awaitPromise = true) {
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
export async function callPageFunction(session, functionDeclaration, args = [], awaitPromise = true) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (global.exceptionDetails || !global.result?.objectId) {
|
|
399
|
-
throw new Error(global.exceptionDetails?.exception?.description || global.exceptionDetails?.text || 'Could not access the page execution context');
|
|
400
|
-
}
|
|
401
|
-
const result = await session.send('Runtime.callFunctionOn', {
|
|
402
|
-
objectId: global.result.objectId,
|
|
403
|
-
functionDeclaration,
|
|
404
|
-
arguments: args.map(value => ({ value })),
|
|
405
|
-
returnByValue: true,
|
|
406
|
-
awaitPromise,
|
|
407
|
-
userGesture: true,
|
|
408
|
-
});
|
|
409
|
-
if (result.exceptionDetails) {
|
|
410
|
-
throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text || 'JavaScript function call failed');
|
|
411
|
-
}
|
|
412
|
-
return result.result?.value;
|
|
393
|
+
// These declarations are trusted internal browser actions. JSON arguments
|
|
394
|
+
// stay data, never raw page code. One CDP Runtime.evaluate call replaces
|
|
395
|
+
// Runtime.evaluate(globalThis) + Runtime.callFunctionOn (two serial RTTs).
|
|
396
|
+
const expression = '(' + functionDeclaration + ')(...' + JSON.stringify(args) + ')';
|
|
397
|
+
return evaluate(session, expression, awaitPromise);
|
|
413
398
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Execute deterministic bounded UI steps on one guarded CDP target/session.
|
|
2
|
+
// A workflow is explicitly requested by the caller after inspecting the page.
|
|
3
|
+
// No arbitrary JavaScript, cross-target access, file uploads or implicit submits.
|
|
4
|
+
import { assertResolvedPageUrl } from './browser-endpoint.mjs';
|
|
5
|
+
import { callPageFunction, evaluate } from './browser-session.mjs';
|
|
6
|
+
|
|
7
|
+
const ACTIONS = new Set(['fill_form','set_value','select','focus','click','wait_for']);
|
|
8
|
+
const CONDITIONS = new Set(['selector','text','url_contains','load']);
|
|
9
|
+
|
|
10
|
+
export function validateBrowserWorkflow(steps, { validateFormFields }) {
|
|
11
|
+
if (!Array.isArray(steps) || steps.length < 1 || steps.length > 12) {
|
|
12
|
+
throw new Error('browser workflow requires 1 to 12 steps');
|
|
13
|
+
}
|
|
14
|
+
return steps.map((raw, index) => {
|
|
15
|
+
const action = typeof raw?.action === 'string' ? raw.action : '';
|
|
16
|
+
if (!ACTIONS.has(action)) throw new Error('Unsupported browser workflow action at step ' + (index + 1));
|
|
17
|
+
if (action === 'fill_form') {
|
|
18
|
+
return { action, fields:validateFormFields(raw.fields) };
|
|
19
|
+
}
|
|
20
|
+
if (action === 'wait_for') {
|
|
21
|
+
const condition = String(raw.condition || 'selector');
|
|
22
|
+
if (!CONDITIONS.has(condition)) throw new Error('Unsupported browser wait condition at step ' + (index + 1));
|
|
23
|
+
const criterion = condition === 'selector' ? raw.selector : raw.text;
|
|
24
|
+
if (condition !== 'load' && (typeof criterion !== 'string' || !criterion.trim() || criterion.length > 512)) {
|
|
25
|
+
throw new Error('Browser workflow wait requires a bounded criterion at step ' + (index + 1));
|
|
26
|
+
}
|
|
27
|
+
const timeout_ms = raw.timeout_ms == null ? 1500 : Number(raw.timeout_ms);
|
|
28
|
+
if (!Number.isInteger(timeout_ms) || timeout_ms < 100 || timeout_ms > 4000) {
|
|
29
|
+
throw new Error('Browser workflow wait timeout must be 100-4000 ms');
|
|
30
|
+
}
|
|
31
|
+
return {action, condition, criterion:String(criterion || ''), timeout_ms};
|
|
32
|
+
}
|
|
33
|
+
if (typeof raw.selector !== 'string' || !raw.selector.trim() || raw.selector.length > 512) {
|
|
34
|
+
throw new Error('Browser workflow action requires a CSS selector at step ' + (index + 1));
|
|
35
|
+
}
|
|
36
|
+
if (['set_value','select'].includes(action) &&
|
|
37
|
+
(typeof raw.value !== 'string' && typeof raw.value !== 'number' && typeof raw.value !== 'boolean')) {
|
|
38
|
+
throw new Error('Browser workflow input requires a string/number/boolean value at step ' + (index + 1));
|
|
39
|
+
}
|
|
40
|
+
if (['set_value','select'].includes(action) && String(raw.value).length > 16384) {
|
|
41
|
+
throw new Error('Browser workflow input exceeds 16384 characters');
|
|
42
|
+
}
|
|
43
|
+
return {action, selector:raw.selector.trim(),
|
|
44
|
+
...(['set_value','select'].includes(action) ? {value:String(raw.value)} : {})};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function waitForStep(session, step, started) {
|
|
49
|
+
const deadline = Math.min(started + 12000, Date.now() + step.timeout_ms);
|
|
50
|
+
const expression = step.condition === 'selector' ?
|
|
51
|
+
'Boolean(document.querySelector('+JSON.stringify(step.criterion)+'))'
|
|
52
|
+
: step.condition === 'text' ?
|
|
53
|
+
'(document.body?.innerText||"").includes('+JSON.stringify(step.criterion)+')'
|
|
54
|
+
: step.condition === 'url_contains' ?
|
|
55
|
+
'location.href.includes('+JSON.stringify(step.criterion)+')'
|
|
56
|
+
: 'document.readyState === "complete"';
|
|
57
|
+
while (Date.now() < deadline) {
|
|
58
|
+
let state;
|
|
59
|
+
try {
|
|
60
|
+
// One CDP roundtrip validates the navigated page URL and the condition.
|
|
61
|
+
state = await evaluate(session, '({url:location.href,matched:Boolean(' + expression + ')})');
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Clicking a link can invalidate the JS execution context briefly.
|
|
64
|
+
if (!/execution context (was destroyed|not found)|cannot find context/i.test(String(error?.message || ''))) {
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
await new Promise(resolve=>setTimeout(resolve, 75));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Never accept a match on a private or unsafe page.
|
|
71
|
+
await assertResolvedPageUrl(state?.url);
|
|
72
|
+
if (state?.matched) return;
|
|
73
|
+
await new Promise(resolve=>setTimeout(resolve, 75));
|
|
74
|
+
}
|
|
75
|
+
throw new Error('Workflow wait timed out');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function executeBrowserWorkflow(session, steps, {
|
|
79
|
+
pageElementAction, pageFillForm,
|
|
80
|
+
}) {
|
|
81
|
+
const started = Date.now();
|
|
82
|
+
const completed = [];
|
|
83
|
+
for (let index = 0; index < steps.length; index++) {
|
|
84
|
+
const step = steps[index];
|
|
85
|
+
if (Date.now() - started > 12000) throw new Error('Workflow exceeded 12 second budget after '+completed.length+' steps');
|
|
86
|
+
try {
|
|
87
|
+
const currentUrl = await evaluate(session, 'location.href');
|
|
88
|
+
await assertResolvedPageUrl(currentUrl);
|
|
89
|
+
if (step.action === 'fill_form') {
|
|
90
|
+
await callPageFunction(session,pageFillForm,[step.fields]);
|
|
91
|
+
} else if (step.action === 'wait_for') {
|
|
92
|
+
await waitForStep(session,step,started);
|
|
93
|
+
} else if (step.action === 'click') {
|
|
94
|
+
const bounds=await callPageFunction(session,pageElementAction,[step.selector,null,'click','']);
|
|
95
|
+
const x=Number(bounds.x)+Number(bounds.width)/2;
|
|
96
|
+
const y=Number(bounds.y)+Number(bounds.height)/2;
|
|
97
|
+
await session.send('Input.dispatchMouseEvent',{type:'mouseMoved',x,y,button:'none'});
|
|
98
|
+
await session.send('Input.dispatchMouseEvent',{type:'mousePressed',x,y,button:'left',clickCount:1});
|
|
99
|
+
await session.send('Input.dispatchMouseEvent',{type:'mouseReleased',x,y,button:'left',clickCount:1});
|
|
100
|
+
} else {
|
|
101
|
+
await callPageFunction(session,pageElementAction,[step.selector,null,step.action,step.value??'']);
|
|
102
|
+
}
|
|
103
|
+
completed.push({index:index+1,action:step.action,ok:true});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Don't echo form values or page text in an error.
|
|
106
|
+
throw new Error('Browser workflow stopped at step '+(index+1)+' ('+step.action+'); '+
|
|
107
|
+
completed.length+' steps completed. '+(step.action==='wait_for'?'Condition not satisfied.':'Inspect the page before retrying.'));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {completed:completed.length,steps:completed,elapsed_ms:Date.now()-started};
|
|
111
|
+
}
|
|
@@ -38,7 +38,7 @@ export const AI_TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
38
38
|
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.',
|
|
39
39
|
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.',
|
|
40
40
|
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.',
|
|
41
|
-
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
|
|
41
|
+
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 action=fill_form with CSS selectors to edit up to 20 visible fields in one tool call without submitting. Use browser_evaluate only when supported actions cannot express the task.',
|
|
42
42
|
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.',
|
|
43
43
|
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.',
|
|
44
44
|
service: 'Use this for operating-system service inventory/status/start/stop/restart. Prefer status before a mutation when service state matters; use start_process for ordinary commands that are not service-manager operations.',
|
package/src/extended/catalog.mjs
CHANGED
|
@@ -165,7 +165,7 @@ export const extendedToolDefinitions = [
|
|
|
165
165
|
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']),
|
|
166
166
|
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.') }), readOnlyOpen, browserHandlers.browser_snapshot, ['browser_cdp']),
|
|
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
|
-
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']),
|
|
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
170
|
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']),
|
|
171
171
|
|