@remcp/runtime 0.2.191 → 0.2.193
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 +12 -0
- package/package.json +1 -1
- package/src/extended/browser-actions.mjs +21 -10
- package/src/extended/browser-read.mjs +42 -15
- package/src/extended/browser-session.mjs +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.193 — 2026-10-28
|
|
4
|
+
|
|
5
|
+
- Publish the unchanged 84-tool runtime interface in lockstep with the device pairing and agent update: the CLI requires the coordinated 0.2.193 release.
|
|
6
|
+
- Preserve browser, terminal, filesystem and authorization semantics from 0.2.192; the pairing security change belongs to the agent/server, not to a new runtime tool.
|
|
7
|
+
|
|
8
|
+
## 0.2.192 — 2026-10-28
|
|
9
|
+
|
|
10
|
+
- Accelerate `browser_find` role/text candidate filtering without omitting ARIA fallback semantics.
|
|
11
|
+
- Reduce redundant `browser_wait` Chrome reads and coalesce focus/clear for `browser_action` native text input.
|
|
12
|
+
- Reject CDP event waits and pending writes promptly when the debugging WebSocket disconnects.
|
|
13
|
+
- Preserve browser authorization, origin/DNS/SSRF checks, secure input events, credential redaction and the 84-tool contract.
|
|
14
|
+
|
|
3
15
|
## 0.2.191 — 2026-10-28
|
|
4
16
|
|
|
5
17
|
- Correct the catastrophic-command heuristic for shell wrappers that consume option values, including sudo, nice, ionice, and env split-string.
|
package/package.json
CHANGED
|
@@ -74,15 +74,15 @@ export const PAGE_ELEMENT_ACTION = `function(selector, needle, action, value) {
|
|
|
74
74
|
) || null;
|
|
75
75
|
}
|
|
76
76
|
if (!el) throw new Error('Element not found');
|
|
77
|
-
if (action === 'click' || action === 'type' || action === 'scroll_into_view') {
|
|
77
|
+
if (action === 'click' || action === 'type' || action === 'type_clear' || action === 'scroll_into_view') {
|
|
78
78
|
el.scrollIntoView({block:'center', inline:'center'});
|
|
79
79
|
}
|
|
80
|
-
if (action === 'focus' || action === 'type') el.focus();
|
|
80
|
+
if (action === 'focus' || action === 'type' || action === 'type_clear') el.focus();
|
|
81
81
|
if (action === 'select') {
|
|
82
82
|
el.value = value;
|
|
83
83
|
el.dispatchEvent(new Event('input', {bubbles:true}));
|
|
84
84
|
el.dispatchEvent(new Event('change', {bubbles:true}));
|
|
85
|
-
} else if (action === 'set_value') {
|
|
85
|
+
} else if (action === 'set_value' || action === 'type_clear') {
|
|
86
86
|
const proto = Object.getPrototypeOf(el);
|
|
87
87
|
const own = Object.getOwnPropertyDescriptor(proto, 'value');
|
|
88
88
|
const parent = Object.getPrototypeOf(proto);
|
|
@@ -307,8 +307,11 @@ export async function browserAction(args) {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
if (action === 'type') {
|
|
310
|
-
|
|
311
|
-
|
|
310
|
+
// Focus and clear must happen in that order, but they can share one
|
|
311
|
+
// guarded page evaluation. Keep native insertText and read-back CDP
|
|
312
|
+
// calls sequential so real keyboard input and verification are preserved.
|
|
313
|
+
const preparatoryAction = args.clear === true ? 'type_clear' : 'type';
|
|
314
|
+
const focused = await callPageFunction(session, PAGE_ELEMENT_ACTION, [selector, needle, preparatoryAction, '']);
|
|
312
315
|
await session.send('Input.insertText', { text:value });
|
|
313
316
|
const current = await callPageFunction(session, PAGE_ELEMENT_ACTION, [selector, needle, 'read_value', '']);
|
|
314
317
|
return jsonResult({ target_id:target.id, action, result:{ ...focused, value:current.value } });
|
|
@@ -333,7 +336,10 @@ export async function browserWait(args = {}) {
|
|
|
333
336
|
if (condition === 'expression') await assertSafeEvaluationExpression(wanted);
|
|
334
337
|
return withTarget(args, async (session, target) => {
|
|
335
338
|
const started = Date.now();
|
|
336
|
-
|
|
339
|
+
// withTarget has already verified this live URL before calling us.
|
|
340
|
+
// Avoid reading it again just to establish the navigation baseline.
|
|
341
|
+
const initialUrl = target.url || '';
|
|
342
|
+
let observedUrl = initialUrl || null;
|
|
337
343
|
const inflight = new Set();
|
|
338
344
|
let lastResourceCount = -1;
|
|
339
345
|
let stableSince = Date.now();
|
|
@@ -358,8 +364,9 @@ export async function browserWait(args = {}) {
|
|
|
358
364
|
while (Date.now() - started < timeoutMs) {
|
|
359
365
|
let matched = false;
|
|
360
366
|
if (condition === 'network_idle') {
|
|
361
|
-
const state = await evaluate(session, `({ready:document.readyState,resources:performance.getEntriesByType('resource').length})`).catch(() => null);
|
|
367
|
+
const state = await evaluate(session, `({url:location.href,ready:document.readyState,resources:performance.getEntriesByType('resource').length})`).catch(() => null);
|
|
362
368
|
if (state) {
|
|
369
|
+
if (typeof state.url === 'string') observedUrl = state.url;
|
|
363
370
|
if (state.resources !== lastResourceCount) {
|
|
364
371
|
lastResourceCount = state.resources;
|
|
365
372
|
stableSince = Date.now();
|
|
@@ -373,7 +380,11 @@ export async function browserWait(args = {}) {
|
|
|
373
380
|
: condition === 'load' ? `document.readyState === 'complete'`
|
|
374
381
|
: condition === 'navigation' ? `location.href !== ${JSON.stringify(initialUrl)}`
|
|
375
382
|
: `Boolean(${wanted})`;
|
|
376
|
-
|
|
383
|
+
// Capture the condition and its URL atomically in one CDP read.
|
|
384
|
+
// withTarget independently validates the final live URL afterward.
|
|
385
|
+
const state = await evaluate(session, `({url:location.href,matched:Boolean(${expression})})`);
|
|
386
|
+
if (typeof state?.url === 'string') observedUrl = state.url;
|
|
387
|
+
matched = Boolean(state?.matched);
|
|
377
388
|
}
|
|
378
389
|
if (matched) return jsonResult({
|
|
379
390
|
target_id: target.id,
|
|
@@ -381,7 +392,7 @@ export async function browserWait(args = {}) {
|
|
|
381
392
|
matched: true,
|
|
382
393
|
elapsed_ms: Date.now() - started,
|
|
383
394
|
...(condition === 'network_idle' ? { inflight: inflight.size, idle_ms: idleMs } : {}),
|
|
384
|
-
url:
|
|
395
|
+
url: observedUrl,
|
|
385
396
|
});
|
|
386
397
|
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
387
398
|
}
|
|
@@ -391,7 +402,7 @@ export async function browserWait(args = {}) {
|
|
|
391
402
|
matched: false,
|
|
392
403
|
elapsed_ms: Date.now() - started,
|
|
393
404
|
...(condition === 'network_idle' ? { inflight: inflight.size, idle_ms: idleMs } : {}),
|
|
394
|
-
url:
|
|
405
|
+
url: observedUrl,
|
|
395
406
|
});
|
|
396
407
|
} finally {
|
|
397
408
|
for (const unsubscribe of unsubscribers) unsubscribe();
|
|
@@ -284,7 +284,22 @@ export function findExpression(args) {
|
|
|
284
284
|
const selector = ${JSON.stringify(selector)};
|
|
285
285
|
const needle = ${JSON.stringify(needle)};
|
|
286
286
|
const role = ${JSON.stringify(role)};
|
|
287
|
-
const
|
|
287
|
+
const wanted = needle ? needle.toLowerCase() : '';
|
|
288
|
+
const compactWanted = wanted.replace(/\\s+/g, '');
|
|
289
|
+
const wantedRole = role ? role.toLowerCase() : '';
|
|
290
|
+
// Native roles have a bounded set of possible tags. Include all explicit
|
|
291
|
+
// ARIA roles too, so role='button' on a custom element remains discoverable.
|
|
292
|
+
// Unknown roles deliberately retain the full-document fallback.
|
|
293
|
+
const nativeRoleSelectors = {
|
|
294
|
+
button:'button,summary,input', link:'a[href]',
|
|
295
|
+
heading:'h1,h2,h3,h4,h5,h6', textbox:'textarea,input',
|
|
296
|
+
searchbox:'input', combobox:'select,input', listbox:'select',
|
|
297
|
+
checkbox:'input', radio:'input', slider:'input', spinbutton:'input',
|
|
298
|
+
option:'option', img:'img', progressbar:'progress', meter:'meter',
|
|
299
|
+
};
|
|
300
|
+
const sourceSelector = selector || (Object.hasOwn(nativeRoleSelectors,wantedRole)
|
|
301
|
+
? nativeRoleSelectors[wantedRole] + ',[role]' : '*');
|
|
302
|
+
const source = document.querySelectorAll(sourceSelector);
|
|
288
303
|
function cssPath(el) {
|
|
289
304
|
if (el.id) return '#' + CSS.escape(el.id);
|
|
290
305
|
const parts = [];
|
|
@@ -328,9 +343,8 @@ export function findExpression(args) {
|
|
|
328
343
|
return (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
329
344
|
}
|
|
330
345
|
function textQuality(value) {
|
|
331
|
-
if (!
|
|
346
|
+
if (!wanted) return 0;
|
|
332
347
|
const hay = value.toLowerCase();
|
|
333
|
-
const wanted = needle.toLowerCase();
|
|
334
348
|
if (hay === wanted) return 0;
|
|
335
349
|
if (hay.startsWith(wanted)) return 1;
|
|
336
350
|
return 2;
|
|
@@ -340,19 +354,32 @@ export function findExpression(args) {
|
|
|
340
354
|
for (let current = el.parentElement; current; current = current.parentElement) depth += 1;
|
|
341
355
|
return depth;
|
|
342
356
|
}
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const hay = visibleText(el);
|
|
357
|
+
const rankAll = Boolean(wanted && !selector);
|
|
358
|
+
const candidates = [];
|
|
359
|
+
for (const el of source) {
|
|
347
360
|
const r = semanticRole(el);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
361
|
+
if (wantedRole && r !== wantedRole) continue;
|
|
362
|
+
// Cheap raw text/label check before forcing layout via innerText,
|
|
363
|
+
// computed styles or bounding boxes. The final visibleText match is
|
|
364
|
+
// still required so hidden descendants do not become false positives.
|
|
365
|
+
if (wanted) {
|
|
366
|
+
const raw = String(el.textContent || '').toLowerCase();
|
|
367
|
+
const aria = String(el.getAttribute('aria-label') || '').toLowerCase();
|
|
368
|
+
// CSS layout can insert line breaks absent from textContent. Compare
|
|
369
|
+
// whitespace-free forms as a fallback to avoid missing those matches.
|
|
370
|
+
if (!raw.includes(wanted) && !aria.includes(wanted)
|
|
371
|
+
&& !raw.replace(/\\s+/g, '').includes(compactWanted)
|
|
372
|
+
&& !aria.replace(/\\s+/g, '').includes(compactWanted)) continue;
|
|
373
|
+
}
|
|
374
|
+
const style = getComputedStyle(el);
|
|
375
|
+
if (style.visibility === 'hidden' || style.display === 'none' || !el.getClientRects().length) continue;
|
|
376
|
+
const text = wanted ? visibleText(el) : '';
|
|
377
|
+
if (wanted && !text.toLowerCase().includes(wanted)) continue;
|
|
378
|
+
candidates.push({el,index:candidates.length,text,role:r,depth:rankAll ? depthOf(el) : 0});
|
|
379
|
+
// Text-only searches rank all matches to prefer exact interactive
|
|
380
|
+
// targets. Every other search uses DOM order and can stop at limit.
|
|
381
|
+
if (!rankAll && candidates.length >= ${limit}) break;
|
|
382
|
+
}
|
|
356
383
|
let selected = candidates;
|
|
357
384
|
if (needle && !selector) {
|
|
358
385
|
selected = [...candidates].sort((a, b) =>
|
|
@@ -115,11 +115,25 @@ export class CdpSession {
|
|
|
115
115
|
reject(new Error(`${method} timed out`));
|
|
116
116
|
}, timeoutMs);
|
|
117
117
|
this.pending.set(key, { resolve, reject, timer });
|
|
118
|
-
|
|
118
|
+
try {
|
|
119
|
+
this.socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// A failed synchronous WebSocket write must not retain a pending
|
|
122
|
+
// request (and its timer) for the rest of the CDP deadline.
|
|
123
|
+
this.pending.delete(key);
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
reject(error);
|
|
126
|
+
}
|
|
119
127
|
});
|
|
120
128
|
}
|
|
121
129
|
|
|
122
130
|
waitFor(method, timeoutMs = this.timeoutMs, signal = null) {
|
|
131
|
+
if (signal?.aborted) return Promise.reject(new Error(`${method} aborted`));
|
|
132
|
+
// Chrome may disconnect between the preceding command and subscription.
|
|
133
|
+
// Do not turn an already-dead socket into a full navigation timeout.
|
|
134
|
+
if (this.closed || !this.socket || this.socket.readyState !== 1) {
|
|
135
|
+
return Promise.reject(new Error('CDP websocket is not open'));
|
|
136
|
+
}
|
|
123
137
|
return new Promise((resolve, reject) => {
|
|
124
138
|
if (signal?.aborted) { reject(new Error(`${method} aborted`)); return; }
|
|
125
139
|
const listeners = this.waiters.get(method) || [];
|