@jobshimo/browser-link 0.22.0 → 0.23.1

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.
Files changed (72) hide show
  1. package/README.md +5 -0
  2. package/dist/agent-instructions/content.js +11 -0
  3. package/dist/agent-instructions/content.js.map +1 -1
  4. package/dist/bridge/ws-bridge.js +13 -0
  5. package/dist/bridge/ws-bridge.js.map +1 -1
  6. package/dist/cdp/client.d.ts +35 -0
  7. package/dist/cdp/client.js +149 -0
  8. package/dist/cdp/client.js.map +1 -0
  9. package/dist/cdp/flow.d.ts +178 -0
  10. package/dist/cdp/flow.js +217 -0
  11. package/dist/cdp/flow.js.map +1 -0
  12. package/dist/cdp/gate.d.ts +31 -0
  13. package/dist/cdp/gate.js +35 -0
  14. package/dist/cdp/gate.js.map +1 -0
  15. package/dist/cdp/grant.d.ts +45 -0
  16. package/dist/cdp/grant.js +81 -0
  17. package/dist/cdp/grant.js.map +1 -0
  18. package/dist/cdp/inpage/builders.d.ts +165 -0
  19. package/dist/cdp/inpage/builders.js +501 -0
  20. package/dist/cdp/inpage/builders.js.map +1 -0
  21. package/dist/cdp/inpage/deep-query.d.ts +57 -0
  22. package/dist/cdp/inpage/deep-query.js +325 -0
  23. package/dist/cdp/inpage/deep-query.js.map +1 -0
  24. package/dist/cdp/inpage/dom-helpers.d.ts +27 -0
  25. package/dist/cdp/inpage/dom-helpers.js +147 -0
  26. package/dist/cdp/inpage/dom-helpers.js.map +1 -0
  27. package/dist/cdp/keymap.d.ts +83 -0
  28. package/dist/cdp/keymap.js +205 -0
  29. package/dist/cdp/keymap.js.map +1 -0
  30. package/dist/cdp/settle.d.ts +38 -0
  31. package/dist/cdp/settle.js +76 -0
  32. package/dist/cdp/settle.js.map +1 -0
  33. package/dist/cdp/support.d.ts +26 -0
  34. package/dist/cdp/support.js +58 -0
  35. package/dist/cdp/support.js.map +1 -0
  36. package/dist/cdp/targets.d.ts +54 -0
  37. package/dist/cdp/targets.js +176 -0
  38. package/dist/cdp/targets.js.map +1 -0
  39. package/dist/cdp/transport.d.ts +31 -0
  40. package/dist/cdp/transport.js +531 -0
  41. package/dist/cdp/transport.js.map +1 -0
  42. package/dist/cli.js +50 -0
  43. package/dist/cli.js.map +1 -1
  44. package/dist/commands/about.js +13 -0
  45. package/dist/commands/about.js.map +1 -1
  46. package/dist/commands/cdp.d.ts +28 -0
  47. package/dist/commands/cdp.js +172 -0
  48. package/dist/commands/cdp.js.map +1 -0
  49. package/dist/commands/config.d.ts +92 -0
  50. package/dist/commands/config.js +173 -12
  51. package/dist/commands/config.js.map +1 -1
  52. package/dist/config.d.ts +70 -0
  53. package/dist/config.js +94 -1
  54. package/dist/config.js.map +1 -1
  55. package/dist/extension/background.js +58 -5
  56. package/dist/extension/background.js.map +1 -1
  57. package/dist/extension/inpage/recorder.js +25 -0
  58. package/dist/extension/inpage/recorder.js.map +1 -1
  59. package/dist/extension/manifest.json +1 -1
  60. package/dist/extension/recording.d.ts +39 -3
  61. package/dist/extension/recording.js +57 -3
  62. package/dist/extension/recording.js.map +1 -1
  63. package/dist/server.js +9 -0
  64. package/dist/server.js.map +1 -1
  65. package/dist/tools/browser-definitions.js +4 -3
  66. package/dist/tools/browser-definitions.js.map +1 -1
  67. package/dist/tools/browser-dispatch.d.ts +31 -0
  68. package/dist/tools/browser-dispatch.js +88 -40
  69. package/dist/tools/browser-dispatch.js.map +1 -1
  70. package/dist/tools/server-instructions.js +19 -0
  71. package/dist/tools/server-instructions.js.map +1 -1
  72. package/package.json +1 -1
@@ -0,0 +1,501 @@
1
+ /**
2
+ * Independent copy of `packages/extension/src/inpage/builders.ts` — see
3
+ * `./deep-query.ts`'s header comment for why this is a verbatim duplicate
4
+ * rather than a cross-package import.
5
+ *
6
+ * DELIBERATELY A SUBSET: only the builders `cdp/transport.ts` needs for
7
+ * cdp-direct's v1 supported tool set (snapshot, find, click, type, press,
8
+ * state, plus the shared settle wait) are copied. `buildDragProbeJs` /
9
+ * `DragProbeOpts` are NOT copied — `browser.drag` is explicitly out of
10
+ * v1 scope for cdp-direct (see the README's cdp-direct tool-support table
11
+ * and `cdp/transport.ts`'s `CDP_TOOL_SUPPORT`). If drag is ever added to
12
+ * cdp-direct, copy `buildDragProbeJs` here at that time.
13
+ *
14
+ * Builders that assemble the CDP `Runtime.evaluate` expression strings for
15
+ * `snapshot`, `find`, `click`, `type`, `press` and the shared post-action
16
+ * `settle` wait. The exact same string the extension sends over CDP. Because
17
+ * this file is a SUBSET it is not byte-compared wholesale; instead the two
18
+ * security-critical pieces it interpolates — `DEEP_QUERY_JS` and
19
+ * `DOM_HELPERS_JS` — are asserted byte-identical to their extension siblings
20
+ * by `cdp/drift.test.ts`, and `cdp/inpage/builders.test.ts` proves this copy
21
+ * embeds exactly those shared helpers.
22
+ */
23
+ import { DEEP_QUERY_JS } from './deep-query.js';
24
+ import { DOM_HELPERS_JS } from './dom-helpers.js';
25
+ export function buildSnapshotJs(opts = {}) {
26
+ const optsJson = JSON.stringify({
27
+ withinSelector: typeof opts.within_selector === 'string' ? opts.within_selector : null,
28
+ onlyInteractive: opts.only_interactive === true,
29
+ exclude: Array.isArray(opts.exclude) ? opts.exclude : [],
30
+ maxInteractive: typeof opts.max_interactive === 'number' && opts.max_interactive > 0
31
+ ? Math.min(opts.max_interactive, 500)
32
+ : 120,
33
+ });
34
+ return `
35
+ (() => {
36
+ ${DEEP_QUERY_JS}
37
+ ${DOM_HELPERS_JS}
38
+ const opts = ${optsJson};
39
+ let root = document;
40
+ let notice = '';
41
+ if (opts.withinSelector) {
42
+ const sub = deepQueryFirst(opts.withinSelector);
43
+ if (!sub) {
44
+ return {
45
+ title: document.title,
46
+ url: location.href,
47
+ interactive: [],
48
+ notice: 'within_selector did not match any element',
49
+ };
50
+ }
51
+ root = sub;
52
+ }
53
+ const excludeSet = new Set((opts.exclude || []).map(s => String(s).toLowerCase()));
54
+ function inExcludedLandmark(el) {
55
+ // Composed-tree climb (same pattern as composedContains): walking via
56
+ // parentElement alone would stop dead at a shadow boundary — a button
57
+ // inside a web component slotted into <nav> would escape exclusion.
58
+ // parentNode reaches the ShadowRoot; jumping to .host continues the
59
+ // climb in the host document.
60
+ if (excludeSet.size === 0) return false;
61
+ let cur = el.parentNode;
62
+ let guard = 0;
63
+ while (cur && guard < 1000) {
64
+ guard++;
65
+ if (cur === root) return false;
66
+ if (cur.nodeType === 1) {
67
+ const tag = cur.tagName ? cur.tagName.toLowerCase() : '';
68
+ if (excludeSet.has(tag)) return true;
69
+ if (cur === document.body) return false;
70
+ cur = cur.parentNode;
71
+ continue;
72
+ }
73
+ if (cur.host) {
74
+ cur = cur.host;
75
+ continue;
76
+ }
77
+ break;
78
+ }
79
+ return false;
80
+ }
81
+ const sel = 'a[href], button, input, select, textarea, [role=button], [role=link], [role=checkbox], [role=tab], [role=menuitem], [contenteditable=true]';
82
+ const interactive = [];
83
+ deepQueryAll(sel, root).forEach((el) => {
84
+ if (!isVisible(el)) return;
85
+ if (inExcludedLandmark(el)) return;
86
+ const tag = el.tagName.toLowerCase();
87
+ const role = el.getAttribute('role') || tag;
88
+ const selInfo = genSelectorInfo(el);
89
+ const entry = { tag, role, selector: selInfo.selector };
90
+ if (selInfo.ambiguous) entry.ambiguous = true;
91
+ const txt = shortText(el);
92
+ if (txt) entry.text = txt;
93
+ if ('value' in el && el.value) entry.value = String(el.value);
94
+ const placeholder = el.getAttribute('placeholder');
95
+ if (placeholder) entry.placeholder = placeholder;
96
+ const aria_label = el.getAttribute('aria-label');
97
+ if (aria_label) entry.aria_label = aria_label;
98
+ const name = el.getAttribute('name');
99
+ if (name) entry.name = name;
100
+ const type = el.getAttribute('type');
101
+ if (type) entry.type = type;
102
+ const href = el.getAttribute('href');
103
+ if (href) entry.href = href;
104
+ if ('disabled' in el && el.disabled) entry.disabled = true;
105
+ const frame = frameSelectorFor(el);
106
+ if (frame) entry.frame = frame;
107
+ interactive.push(entry);
108
+ });
109
+ const result = {
110
+ title: document.title,
111
+ url: location.href,
112
+ interactive: interactive.slice(0, opts.maxInteractive),
113
+ };
114
+ if (!opts.onlyInteractive) {
115
+ const headings = [];
116
+ deepQueryAll('h1, h2, h3', root).forEach((h) => {
117
+ if (!isVisible(h)) return;
118
+ if (inExcludedLandmark(h)) return;
119
+ const t = shortText(h);
120
+ if (t) headings.push({ level: h.tagName, text: t });
121
+ });
122
+ result.headings = headings.slice(0, 30);
123
+ const textRoot = (root === document) ? (document.body || document) : root;
124
+ const visibleText = (textRoot && textRoot.innerText) ? textRoot.innerText.slice(0, 4000) : '';
125
+ if (visibleText) result.text = visibleText;
126
+ }
127
+ if (notice) result.notice = notice;
128
+ return result;
129
+ })()
130
+ `;
131
+ }
132
+ export function buildFindJs(opts) {
133
+ const optsJson = JSON.stringify({
134
+ text: opts.text,
135
+ role: typeof opts.role === 'string' && opts.role.length > 0 ? opts.role : null,
136
+ exact: opts.exact === true,
137
+ candidateLimit: 5,
138
+ nearMissLimit: 3,
139
+ });
140
+ return `
141
+ (() => {
142
+ ${DEEP_QUERY_JS}
143
+ ${DOM_HELPERS_JS}
144
+ const opts = ${optsJson};
145
+ const needle = opts.text.toLowerCase();
146
+ const needleTokens = needle.split(/[^a-z0-9]+/).filter(Boolean);
147
+ const ROLE_SELECTORS = {
148
+ button: 'button, [role="button"], input[type="button"], input[type="submit"], input[type="reset"]',
149
+ link: 'a[href], [role="link"]',
150
+ textbox: 'input[type="text"], input[type="email"], input[type="password"], input[type="search"], input[type="url"], input[type="tel"], input:not([type]), textarea, [role="textbox"], [contenteditable="true"]',
151
+ checkbox: 'input[type="checkbox"], [role="checkbox"]',
152
+ tab: '[role="tab"]',
153
+ menuitem: '[role="menuitem"]',
154
+ };
155
+ const BROAD_SELECTOR = 'button, a, input, textarea, select, [role], [onclick], [contenteditable="true"], [tabindex]';
156
+ const selectorSet = opts.role && ROLE_SELECTORS[opts.role]
157
+ ? ROLE_SELECTORS[opts.role]
158
+ : BROAD_SELECTOR;
159
+ const all = deepQueryAll(selectorSet);
160
+ const matches = [];
161
+ for (const el of all) {
162
+ if (!isVisible(el)) continue;
163
+ const text = accessibleText(el).trim();
164
+ if (text.length === 0) continue;
165
+ const lower = text.toLowerCase();
166
+ const ok = opts.exact ? lower === needle : lower.includes(needle);
167
+ if (ok) matches.push(el);
168
+ }
169
+ if (matches.length === 0) {
170
+ // Near-miss ranking: containment beats token overlap; anything scoring
171
+ // 0 is dropped so the suggestion list never pads itself with noise.
172
+ function snippetOf(el) {
173
+ const t = accessibleText(el).trim();
174
+ return t.length > 60 ? t.slice(0, 60) + '...' : t;
175
+ }
176
+ function scoreText(text) {
177
+ const lower = text.toLowerCase();
178
+ const contains = lower.includes(needle);
179
+ const candTokens = lower.split(/[^a-z0-9]+/).filter(Boolean);
180
+ let overlap = 0;
181
+ for (let i = 0; i < needleTokens.length; i++) {
182
+ if (candTokens.indexOf(needleTokens[i]) !== -1) overlap++;
183
+ }
184
+ if (!contains && overlap === 0) return -1;
185
+ return (contains ? 1000 : 0) + overlap * 10;
186
+ }
187
+ function rankCandidates(elements) {
188
+ const scored = [];
189
+ for (const el of elements) {
190
+ if (!isVisible(el)) continue;
191
+ const text = accessibleText(el).trim();
192
+ if (text.length === 0) continue;
193
+ const score = scoreText(text);
194
+ if (score < 0) continue;
195
+ scored.push({ el: el, score: score });
196
+ }
197
+ scored.sort((a, b) => b.score - a.score);
198
+ return scored.slice(0, opts.nearMissLimit).map((s) => s.el);
199
+ }
200
+ function toNearMiss(el) {
201
+ const entry = { text: snippetOf(el), selector: genSelector(el) };
202
+ const roleAttr = el.getAttribute('role');
203
+ if (roleAttr) entry.role = roleAttr;
204
+ return entry;
205
+ }
206
+ function describeForError(el) {
207
+ const tag = el.tagName.toLowerCase();
208
+ if (el.hasAttribute('onclick')) return '<' + tag + ' onclick>';
209
+ const role = el.getAttribute('role');
210
+ if (role) return '<' + tag + ' role="' + role + '">';
211
+ return '<' + tag + '>';
212
+ }
213
+ const result = { matched: false, reason: 'not-found' };
214
+ if (opts.role) {
215
+ // Role narrowed the scan — check whether the text exists OUTSIDE the
216
+ // role filter so the error can name the exclusion explicitly instead
217
+ // of reporting a bare not-found.
218
+ const broadAll = deepQueryAll(BROAD_SELECTOR);
219
+ const broadTextMatches = [];
220
+ for (const el of broadAll) {
221
+ if (!isVisible(el)) continue;
222
+ const text = accessibleText(el).trim();
223
+ if (text.length === 0) continue;
224
+ if (text.toLowerCase().includes(needle)) broadTextMatches.push(el);
225
+ }
226
+ if (broadTextMatches.length > 0) {
227
+ const ranked = rankCandidates(broadTextMatches);
228
+ const closest = ranked[0];
229
+ result.error = 'text matched ' + broadTextMatches.length + ' element' +
230
+ (broadTextMatches.length === 1 ? '' : 's') +
231
+ ' but none with role "' + opts.role + '"' +
232
+ (closest ? ' — closest: ' + describeForError(closest) + ' "' + snippetOf(closest) + '"' : '');
233
+ result.near_misses = ranked.map(toNearMiss);
234
+ return result;
235
+ }
236
+ const ranked = rankCandidates(broadAll);
237
+ if (ranked.length > 0) result.near_misses = ranked.map(toNearMiss);
238
+ return result;
239
+ }
240
+ const ranked = rankCandidates(all);
241
+ if (ranked.length > 0) result.near_misses = ranked.map(toNearMiss);
242
+ return result;
243
+ }
244
+ if (matches.length > 1) {
245
+ return {
246
+ matched: false,
247
+ reason: 'multiple-matches',
248
+ candidates: matches.slice(0, opts.candidateLimit).map((el) => ({
249
+ selector: genSelector(el),
250
+ text: shortText(el),
251
+ tag: el.tagName.toLowerCase(),
252
+ })),
253
+ };
254
+ }
255
+ const el = matches[0];
256
+ const center = viewportCenterOf(el);
257
+ const selInfo = genSelectorInfo(el);
258
+ const result = {
259
+ matched: true,
260
+ selector: selInfo.selector,
261
+ coords: {
262
+ x: Math.round(center.x),
263
+ y: Math.round(center.y),
264
+ },
265
+ tag: el.tagName.toLowerCase(),
266
+ text: shortText(el),
267
+ };
268
+ if (selInfo.ambiguous) result.ambiguous = true;
269
+ const frame = frameSelectorFor(el);
270
+ if (frame) result.frame = frame;
271
+ return result;
272
+ })()
273
+ `;
274
+ }
275
+ export function buildClickResolveJs(opts) {
276
+ const optsJson = JSON.stringify({ selector: opts.selector, force: opts.force });
277
+ return `
278
+ (() => {
279
+ ${DEEP_QUERY_JS}
280
+ const opts = ${optsJson};
281
+ try {
282
+ document.querySelector(opts.selector);
283
+ } catch (e) {
284
+ return { ok: false, reason: 'invalid-selector', error: e && e.message ? e.message : String(e) };
285
+ }
286
+ const el = deepQueryFirst(opts.selector);
287
+ if (!el) return { ok: false, reason: 'not-found' };
288
+ const chain = frameElementChain(el);
289
+ for (const fe of chain) {
290
+ if (fe.scrollIntoView) fe.scrollIntoView({ block: 'center', inline: 'center' });
291
+ }
292
+ if (el.scrollIntoView) el.scrollIntoView({ block: 'center', inline: 'center' });
293
+ const center = viewportCenterOf(el);
294
+ const tag = el.tagName.toLowerCase();
295
+ if (!opts.force) {
296
+ const occlusion = checkOcclusion(el, center.localX, center.localY);
297
+ if (!occlusion.allowed) {
298
+ return { ok: false, reason: 'occluded', blocker: occlusion.blocker };
299
+ }
300
+ }
301
+ return { ok: true, x: center.x, y: center.y, tag };
302
+ })()
303
+ `;
304
+ }
305
+ export function buildTypeResolveJs(opts) {
306
+ const optsJson = JSON.stringify({ selector: opts.selector });
307
+ return `
308
+ (() => {
309
+ ${DEEP_QUERY_JS}
310
+ const opts = ${optsJson};
311
+ try {
312
+ document.querySelector(opts.selector);
313
+ } catch (e) {
314
+ return { ok: false, reason: 'invalid-selector', error: e && e.message ? e.message : String(e) };
315
+ }
316
+ const el = deepQueryFirst(opts.selector);
317
+ if (!el) return { ok: false, reason: 'not-found' };
318
+ el.focus();
319
+ ${opts.clear ? "if ('value' in el) { el.value = ''; el.dispatchEvent(new Event('input', { bubbles: true })); }" : ''}
320
+ return { ok: true };
321
+ })()`;
322
+ }
323
+ export function buildFocusJs(opts) {
324
+ const optsJson = JSON.stringify({ selector: opts.selector });
325
+ return `
326
+ (() => {
327
+ ${DEEP_QUERY_JS}
328
+ const opts = ${optsJson};
329
+ const el = deepQueryFirst(opts.selector);
330
+ if (!el) return false;
331
+ el.focus();
332
+ return true;
333
+ })()`;
334
+ }
335
+ export function buildSettleJs(opts) {
336
+ const optsJson = JSON.stringify({
337
+ settleMs: Math.max(0, opts.settle_ms),
338
+ timeoutMs: Math.max(0, opts.settle_timeout_ms),
339
+ });
340
+ return `
341
+ (() => {
342
+ const opts = ${optsJson};
343
+ const startUrl = location.href;
344
+ const startActiveEl = document.activeElement;
345
+ const state = { mutationCount: 0, lastMutationAt: Date.now() };
346
+ const observer = new MutationObserver((mutations) => {
347
+ state.mutationCount += mutations.length;
348
+ state.lastMutationAt = Date.now();
349
+ });
350
+ observer.observe(document, {
351
+ subtree: true,
352
+ childList: true,
353
+ attributes: true,
354
+ characterData: true,
355
+ });
356
+ const start = Date.now();
357
+ return new Promise((resolve) => {
358
+ function finish(settled) {
359
+ observer.disconnect();
360
+ const result = {
361
+ settled: settled,
362
+ duration_ms: Date.now() - start,
363
+ mutation_count: state.mutationCount,
364
+ };
365
+ if (location.href !== startUrl) result.url_changed = location.href;
366
+ if (document.activeElement !== startActiveEl) result.focus_moved = true;
367
+ resolve(result);
368
+ }
369
+ if (opts.settleMs <= 0) {
370
+ finish(true);
371
+ return;
372
+ }
373
+ (function check() {
374
+ const now = Date.now();
375
+ const quietFor = now - state.lastMutationAt;
376
+ if (quietFor >= opts.settleMs) {
377
+ finish(true);
378
+ return;
379
+ }
380
+ if (now - start >= opts.timeoutMs) {
381
+ finish(false);
382
+ return;
383
+ }
384
+ setTimeout(check, Math.min(opts.settleMs - quietFor, 50));
385
+ })();
386
+ });
387
+ })()`;
388
+ }
389
+ /**
390
+ * Build the `browser.state` expression: a compact orientation snapshot —
391
+ * current url/title, the deep-resolved focused element, visible dialog-role
392
+ * elements, scroll position, and viewport size. Cheaper than a full
393
+ * `browser.snapshot` when the agent only needs "where am I right now".
394
+ *
395
+ * `focused` descends through `document.activeElement` past shadow-root and
396
+ * same-origin-iframe boundaries to the real innermost focused element (a
397
+ * shadow host or an <iframe> being "active" at one level is not the actual
398
+ * focus target the agent cares about). Its `selector` goes through the same
399
+ * `genSelectorInfo` uniqueness check `snapshot`/`find` use, so it carries
400
+ * `ambiguous: true` under the same structurally-identical-twins condition.
401
+ *
402
+ * `dialogs` matches visible `[role=dialog]`, `[role=alertdialog]` and open
403
+ * `<dialog>` elements found via the same deep walk, with a best-effort
404
+ * `label` resolved from aria-label / aria-labelledby / the first heading
405
+ * inside the dialog.
406
+ *
407
+ * Every optional field is omitted when there is nothing to report: no
408
+ * `focused` beyond `<body>`, no `dialogs` when none are open, and no
409
+ * `scroll` when the page is at its default (0,0) position — token-lean by
410
+ * construction, matching the omit-falsy convention the other builders use.
411
+ */
412
+ export function buildStateJs() {
413
+ return `
414
+ (() => {
415
+ ${DEEP_QUERY_JS}
416
+ ${DOM_HELPERS_JS}
417
+ function deepActiveElement() {
418
+ let el = document.activeElement;
419
+ let guard = 0;
420
+ while (el && guard < 20) {
421
+ guard++;
422
+ if (el.shadowRoot && el.shadowRoot.activeElement) {
423
+ el = el.shadowRoot.activeElement;
424
+ continue;
425
+ }
426
+ if (el.tagName === 'IFRAME') {
427
+ let innerDoc = null;
428
+ try {
429
+ innerDoc = el.contentDocument;
430
+ } catch (_) {
431
+ innerDoc = null;
432
+ }
433
+ if (innerDoc && innerDoc.activeElement && innerDoc.activeElement !== innerDoc.body) {
434
+ el = innerDoc.activeElement;
435
+ continue;
436
+ }
437
+ }
438
+ break;
439
+ }
440
+ return el;
441
+ }
442
+ function dialogLabel(el) {
443
+ const aria = el.getAttribute('aria-label');
444
+ if (aria) return aria;
445
+ const labelledby = el.getAttribute('aria-labelledby');
446
+ if (labelledby) {
447
+ const ids = labelledby.split(/\\s+/);
448
+ const parts = [];
449
+ for (let i = 0; i < ids.length; i++) {
450
+ if (!ids[i]) continue;
451
+ const target = deepQueryFirst('#' + CSS.escape(ids[i]));
452
+ if (target) {
453
+ const t = shortText(target);
454
+ if (t) parts.push(t);
455
+ }
456
+ }
457
+ const joined = parts.join(' ').trim();
458
+ if (joined) return joined;
459
+ }
460
+ const heading = deepQueryFirst('h1, h2, h3', el);
461
+ if (heading) {
462
+ const t = shortText(heading);
463
+ if (t) return t;
464
+ }
465
+ return '';
466
+ }
467
+
468
+ const result = {
469
+ url: location.href,
470
+ title: document.title,
471
+ viewport: { w: window.innerWidth, h: window.innerHeight },
472
+ };
473
+
474
+ const active = deepActiveElement();
475
+ if (active && active !== document.body && active.tagName) {
476
+ const selInfo = genSelectorInfo(active);
477
+ const focused = { selector: selInfo.selector, tag: active.tagName.toLowerCase() };
478
+ if (selInfo.ambiguous) focused.ambiguous = true;
479
+ result.focused = focused;
480
+ }
481
+
482
+ const dialogs = [];
483
+ deepQueryAll('[role="dialog"], [role="alertdialog"], dialog[open]').forEach((el) => {
484
+ if (!isVisible(el)) return;
485
+ const role = el.getAttribute('role') || 'dialog';
486
+ const entry = { selector: genSelector(el), role: role };
487
+ const label = dialogLabel(el);
488
+ if (label) entry.label = label;
489
+ dialogs.push(entry);
490
+ });
491
+ if (dialogs.length > 0) result.dialogs = dialogs;
492
+
493
+ const scrollX = Math.round(window.scrollX);
494
+ const scrollY = Math.round(window.scrollY);
495
+ if (scrollX !== 0 || scrollY !== 0) result.scroll = { x: scrollX, y: scrollY };
496
+
497
+ return result;
498
+ })()
499
+ `;
500
+ }
501
+ //# sourceMappingURL=builders.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builders.js","sourceRoot":"","sources":["../../../src/cdp/inpage/builders.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Independent copy of `packages/extension/src/inpage/deep-query.ts` — used
3
+ * by the cdp-direct transport (`../transport.ts`) to build the exact same
4
+ * `Runtime.evaluate` expressions the extension's `background.ts` sends,
5
+ * over a raw CDP-over-WS connection instead of `chrome.debugger`.
6
+ *
7
+ * Duplicated rather than imported for the same reason `config.ts` and
8
+ * `messages.ts` keep independent copies of extension-side constants/types:
9
+ * `@jobshimo/browser-link` (this package) publishes to npm standalone and
10
+ * must not carry a workspace dependency on `@browser-link/extension` (a
11
+ * private, unpublished package with `chrome.*` types in its own tsconfig).
12
+ * This file is copied VERBATIM — it has zero `chrome.*` references (it only
13
+ * builds a JS-source string that runs inside the TARGET PAGE, never the
14
+ * extension/server process), so there is nothing to adapt. If the source
15
+ * file changes, re-copy it here — the exported `DEEP_QUERY_JS` body is
16
+ * asserted byte-identical to the sibling at
17
+ * `packages/extension/src/inpage/deep-query.ts` by `cdp/drift.test.ts`,
18
+ * which fails the build if the two ever diverge (a security fix to one copy
19
+ * must reach the other).
20
+ *
21
+ * In-page JS source injected into CDP `Runtime.evaluate` expressions. This
22
+ * is the "deep search" layer that lets `snapshot`, `find`, `click` and
23
+ * `type` see past two boundaries plain `document.querySelector` /
24
+ * `document.querySelectorAll` cannot cross:
25
+ *
26
+ * - OPEN Shadow DOM roots (`element.attachShadow({ mode: 'open' })`),
27
+ * walked recursively (shadow roots nested inside shadow roots).
28
+ * - Same-origin `<iframe>` documents, walked recursively (iframes nested
29
+ * inside iframes, shadow roots nested inside iframes and vice versa).
30
+ *
31
+ * Exported as a raw string, not real TypeScript — it runs inside the
32
+ * TARGET PAGE's JS context via CDP `Runtime.evaluate`, not inside this
33
+ * process. Keep the syntax plain (`var`/`function`, no optional chaining
34
+ * assumptions) since it must run unmodified in whatever JS engine the
35
+ * connected tab uses.
36
+ *
37
+ * Known limitations (also documented on the MCP tool descriptions):
38
+ * - CLOSED shadow roots (`{ mode: 'closed' }`) are unreachable from page
39
+ * world JS — `element.shadowRoot` is `null` for them by design. There
40
+ * is no CDP-level workaround from `Runtime.evaluate`.
41
+ * - CROSS-ORIGIN iframes are unreachable — `contentDocument` throws or
42
+ * returns null under the same-origin policy. Access is wrapped in
43
+ * try/catch so a cross-origin frame is silently skipped rather than
44
+ * failing the whole query.
45
+ * - A plain CSS selector cannot distinguish between two elements that
46
+ * produce byte-identical generated selectors in two different roots
47
+ * (e.g. two structurally-identical Shadow DOM component instances).
48
+ * `deepQueryFirst` returns the first match in traversal order
49
+ * (the start root itself, then nested shadow roots, then nested
50
+ * same-origin iframe documents, depth-first). Extremely rare in
51
+ * practice — real content differs — but worth knowing when a page
52
+ * repeats the exact same component markup in multiple places. When
53
+ * selector generation cannot avoid this (see `genSelectorInfo` in
54
+ * `dom-helpers.ts`), snapshot/find mark the affected entry with
55
+ * `ambiguous: true` so agents know not to cache that selector.
56
+ */
57
+ export declare const DEEP_QUERY_JS = "\n function collectSearchRoots(start) {\n // start: a Document, Element or ShadowRoot to scope the search to.\n // Returns [start, ...every open shadow root and same-origin iframe\n // document reachable inside start's subtree, recursively]. The caller\n // runs .querySelectorAll()/.querySelector() on each entry directly \u2014\n // each entry's own querySelectorAll does NOT pierce into nested shadow\n // roots or iframes on its own, which is why we enumerate them here.\n var roots = [start];\n function walk(scope, depth) {\n // Depth guard, same pattern as the other recursive helpers here:\n // pathological nesting (or a cycle introduced by a broken mock)\n // must degrade to a truncated result, never a stack overflow.\n if (depth > 20) return;\n var all;\n try {\n all = scope.querySelectorAll('*');\n } catch (_) {\n return;\n }\n for (var i = 0; i < all.length; i++) {\n var el = all[i];\n if (el.shadowRoot) {\n roots.push(el.shadowRoot);\n walk(el.shadowRoot, depth + 1);\n }\n if (el.tagName === 'IFRAME') {\n var doc = null;\n try {\n doc = el.contentDocument;\n } catch (_) {\n doc = null;\n }\n if (doc) {\n roots.push(doc);\n walk(doc, depth + 1);\n }\n }\n }\n }\n walk(start, 0);\n return roots;\n }\n\n function deepWalkAll(callback, start) {\n // Tree-walk primitive: invoke callback(el) for every element reachable\n // from start (default: document) across the top document, every open\n // shadow root, and every same-origin iframe document.\n var root = start || document;\n var roots = collectSearchRoots(root);\n for (var r = 0; r < roots.length; r++) {\n var all;\n try {\n all = roots[r].querySelectorAll('*');\n } catch (_) {\n continue;\n }\n for (var i = 0; i < all.length; i++) callback(all[i]);\n }\n }\n\n function deepQueryAll(selector, start) {\n var root = start || document;\n var roots = collectSearchRoots(root);\n var out = [];\n for (var r = 0; r < roots.length; r++) {\n var found;\n try {\n found = roots[r].querySelectorAll(selector);\n } catch (_) {\n continue;\n }\n for (var i = 0; i < found.length; i++) out.push(found[i]);\n }\n return out;\n }\n\n function deepQueryFirst(selector, start) {\n var root = start || document;\n var roots = collectSearchRoots(root);\n for (var r = 0; r < roots.length; r++) {\n var el = null;\n try {\n el = roots[r].querySelector(selector);\n } catch (_) {\n el = null;\n }\n if (el) return el;\n }\n return null;\n }\n\n function frameElementChain(el) {\n // Walk up from el's own window to the top window, collecting the\n // <iframe> element that hosts each intermediate document. Outermost\n // frame first. Same-origin only \u2014 frameElement throws/returns null\n // across an origin boundary, at which point the chain simply stops\n // (the coordinate math below degrades gracefully rather than throwing).\n var chain = [];\n var doc = el.ownerDocument;\n var win = doc && doc.defaultView;\n if (!win) return chain;\n var topWin;\n try {\n topWin = win.top;\n } catch (_) {\n topWin = win;\n }\n var guard = 0;\n while (win && win !== topWin && guard < 20) {\n guard++;\n var fe = null;\n try {\n fe = win.frameElement;\n } catch (_) {\n fe = null;\n }\n if (!fe) break;\n chain.unshift(fe);\n win = fe.ownerDocument && fe.ownerDocument.defaultView;\n }\n return chain;\n }\n\n function frameViewportOriginOffset(fe) {\n // The embedded document's viewport origin sits at the frame element's\n // CONTENT box, but getBoundingClientRect() returns the BORDER box \u2014\n // border and padding must be added on top of rect.left/rect.top or\n // every coordinate inside a styled iframe lands short. clientLeft/\n // clientTop are the border widths; padding comes from computed style\n // (read through the frame's own view \u2014 same-origin, so reachable).\n var borderX = fe.clientLeft || 0;\n var borderY = fe.clientTop || 0;\n var padX = 0;\n var padY = 0;\n try {\n var view = (fe.ownerDocument && fe.ownerDocument.defaultView) || window;\n var style = view.getComputedStyle(fe);\n padX = parseFloat(style.paddingLeft) || 0;\n padY = parseFloat(style.paddingTop) || 0;\n } catch (_) {}\n return { x: borderX + padX, y: borderY + padY };\n }\n\n function clickRectOf(el) {\n // For a line-wrapped INLINE element (a link spanning two lines), the\n // center of getBoundingClientRect() can land in the unpainted gap\n // between the line boxes \u2014 elementFromPoint would report whatever sits\n // behind, and a dispatched click would miss. The first client rect is\n // always a painted fragment, so its center is a safe click point.\n // Block-level elements have exactly one client rect, identical to the\n // bounding rect \u2014 no behavior change for them.\n var rects = null;\n try {\n rects = el.getClientRects ? el.getClientRects() : null;\n } catch (_) {\n rects = null;\n }\n if (rects && rects.length > 0) return rects[0];\n return el.getBoundingClientRect();\n }\n\n function viewportCenterOf(el) {\n // Returns the element's click point in two coordinate spaces:\n // - localX/localY: relative to el's OWN document viewport (what\n // that document's elementFromPoint expects).\n // - x/y: relative to the TOP-LEVEL viewport (what CDP\n // Input.dispatchMouseEvent expects) \u2014 localX/localY plus the\n // accumulated content-box origin of every ancestor <iframe>\n // element (border-box rect + border + padding). Shadow DOM\n // boundaries need no offset: they do not introduce a new\n // coordinate space.\n // Both spaces derive from the SAME point (first-client-rect center),\n // so the occlusion hit-test and the CDP dispatch always agree.\n var rect = clickRectOf(el);\n var localX = rect.left + rect.width / 2;\n var localY = rect.top + rect.height / 2;\n var chain = frameElementChain(el);\n var offsetX = 0;\n var offsetY = 0;\n for (var i = 0; i < chain.length; i++) {\n var frameRect = chain[i].getBoundingClientRect();\n var origin = frameViewportOriginOffset(chain[i]);\n offsetX += frameRect.left + origin.x;\n offsetY += frameRect.top + origin.y;\n }\n return { x: offsetX + localX, y: offsetY + localY, localX: localX, localY: localY };\n }\n\n function composedContains(ancestor, node) {\n // Node.contains() does not cross shadow boundaries. This walks the\n // COMPOSED tree: when climbing hits a ShadowRoot (no parentNode), jump\n // to its host and keep climbing.\n var cur = node;\n var guard = 0;\n while (cur && guard < 1000) {\n guard++;\n if (cur === ancestor) return true;\n if (cur.parentNode) {\n cur = cur.parentNode;\n continue;\n }\n if (cur.host) {\n cur = cur.host;\n continue;\n }\n break;\n }\n return false;\n }\n\n function describeElement(el) {\n if (!el || !el.tagName) return 'an unknown element';\n var desc = el.tagName.toLowerCase();\n if (el.id) desc += '#' + el.id;\n if (el.classList && el.classList.length) {\n var classes = [];\n for (var i = 0; i < el.classList.length && i < 2; i++) classes.push(el.classList[i]);\n if (classes.length) desc += '.' + classes.join('.');\n }\n return desc;\n }\n\n function hitTestDeep(root, x, y) {\n var node = null;\n try {\n node = root && root.elementFromPoint ? root.elementFromPoint(x, y) : null;\n } catch (_) {\n node = null;\n }\n // The hit element may itself host a nested open shadow root whose\n // content visually covers the point \u2014 descend until elementFromPoint\n // stops finding a deeper shadow tree.\n var guard = 0;\n while (node && node.shadowRoot && guard < 20) {\n guard++;\n var inner = null;\n try {\n inner = node.shadowRoot.elementFromPoint ? node.shadowRoot.elementFromPoint(x, y) : null;\n } catch (_) {\n inner = null;\n }\n if (!inner || inner === node) break;\n node = inner;\n }\n return node;\n }\n\n function checkOcclusion(target, localX, localY) {\n // Hit-test starting from the element's OWN root (its closest shadow\n // root, or the document that owns it when not in shadow DOM) using\n // LOCAL coordinates \u2014 the same coordinate space that root's own\n // elementFromPoint expects (an iframe document's viewport starts at\n // its own top-left, not the top-level page's).\n var root = target.getRootNode ? target.getRootNode() : document;\n var hit = hitTestDeep(root, localX, localY);\n if (!hit) {\n // Nothing hit-tested at all (e.g. elementFromPoint unsupported in\n // this environment, or the point is outside the rendered area).\n // Fail open rather than block a click we cannot actually verify.\n return { allowed: true };\n }\n if (hit === target || composedContains(target, hit) || composedContains(hit, target)) {\n return { allowed: true };\n }\n return { allowed: false, blocker: describeElement(hit) };\n }\n";