@youtyan/browser-pilot 0.7.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.
@@ -0,0 +1,2107 @@
1
+ // apps/extension/src/content/element-utils.ts
2
+ var INTERACTABLE_SELECTORS = "input,select,textarea,button,a,[role='button'],[role='link'],[role='tab'],[role='menuitem'],[contenteditable='true']";
3
+ function isSensitiveInput(el) {
4
+ if (el.tagName !== "INPUT")
5
+ return false;
6
+ const type = el.getAttribute("type")?.toLowerCase();
7
+ return type === "password" || type === "hidden";
8
+ }
9
+ function isVisible(el) {
10
+ const rect = el.getBoundingClientRect();
11
+ if (rect.width <= 0 || rect.height <= 0)
12
+ return false;
13
+ const style = getComputedStyle(el);
14
+ if (style.display === "none")
15
+ return false;
16
+ if (style.visibility === "hidden" || style.visibility === "collapse")
17
+ return false;
18
+ if (style.opacity === "0")
19
+ return false;
20
+ let parent = el.parentElement;
21
+ while (parent) {
22
+ const ps = getComputedStyle(parent);
23
+ if (ps.display === "none")
24
+ return false;
25
+ if (ps.visibility === "hidden" || ps.visibility === "collapse")
26
+ return false;
27
+ if (ps.opacity === "0")
28
+ return false;
29
+ parent = parent.parentElement;
30
+ }
31
+ return true;
32
+ }
33
+ function isInteractable(el) {
34
+ return el.matches(INTERACTABLE_SELECTORS);
35
+ }
36
+ function matchesText(el, query) {
37
+ const lower = query.toLowerCase();
38
+ const text = el.textContent?.trim().toLowerCase() ?? "";
39
+ if (text.includes(lower))
40
+ return true;
41
+ const ariaLabel = el.getAttribute("aria-label");
42
+ if (ariaLabel && ariaLabel.toLowerCase().includes(lower))
43
+ return true;
44
+ const title = el.getAttribute("title");
45
+ if (title && title.toLowerCase().includes(lower))
46
+ return true;
47
+ if (el.tagName.toLowerCase() === "a") {
48
+ const href = el.href;
49
+ if (href && href.toLowerCase().includes(lower))
50
+ return true;
51
+ } else if (el.getAttribute("role") === "link") {
52
+ const href = el.getAttribute("href");
53
+ if (href && href.toLowerCase().includes(lower))
54
+ return true;
55
+ }
56
+ return false;
57
+ }
58
+ function matchesFilters(el, params) {
59
+ if (params.tag && el.tagName.toLowerCase() !== params.tag.toLowerCase())
60
+ return false;
61
+ if (params.text) {
62
+ if (!matchesText(el, params.text))
63
+ return false;
64
+ }
65
+ if (params.role) {
66
+ const elRole = el.getAttribute("role") ?? "";
67
+ if (elRole.toLowerCase() !== params.role.toLowerCase())
68
+ return false;
69
+ }
70
+ if (params.attribute) {
71
+ const attrVal = el.getAttribute(params.attribute);
72
+ if (attrVal === null)
73
+ return false;
74
+ if (params.attributeValue) {
75
+ if (!attrVal.toLowerCase().includes(params.attributeValue.toLowerCase()))
76
+ return false;
77
+ }
78
+ }
79
+ if (params.visibleOnly !== false && !isVisible(el))
80
+ return false;
81
+ if (params.interactableOnly && !isInteractable(el))
82
+ return false;
83
+ return true;
84
+ }
85
+ function collectByNearText(nearText, maxDepth, params) {
86
+ const results = [];
87
+ const seen = new Set;
88
+ const lowerNear = nearText.toLowerCase();
89
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
90
+ acceptNode(node) {
91
+ return node.textContent?.toLowerCase().includes(lowerNear) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
92
+ }
93
+ });
94
+ const textNodes = [];
95
+ for (let tNode = walker.nextNode();tNode !== null; tNode = walker.nextNode()) {
96
+ textNodes.push(tNode);
97
+ }
98
+ const implicitInteractable = !params.tag && !params.role && !params.selector;
99
+ for (const textNode of textNodes) {
100
+ let ancestor = textNode.parentElement;
101
+ for (let d = 0;d < maxDepth && ancestor && ancestor !== document.body; d++) {
102
+ ancestor = ancestor.parentElement;
103
+ }
104
+ if (!ancestor)
105
+ continue;
106
+ for (const desc of ancestor.querySelectorAll("*")) {
107
+ const htmlDesc = desc;
108
+ if (seen.has(htmlDesc))
109
+ continue;
110
+ const filterParams = {
111
+ ...params,
112
+ nearText: undefined,
113
+ interactableOnly: params.interactableOnly ?? implicitInteractable
114
+ };
115
+ if (matchesFilters(htmlDesc, filterParams)) {
116
+ seen.add(htmlDesc);
117
+ results.push(htmlDesc);
118
+ }
119
+ }
120
+ }
121
+ return results;
122
+ }
123
+ function findMatchingElements(params) {
124
+ if (!params)
125
+ return [];
126
+ if (params.nearText) {
127
+ const maxDepth = params.nearMaxDepth ?? 3;
128
+ return collectByNearText(params.nearText, maxDepth, params);
129
+ }
130
+ const hasRichFilters = params.tag || params.role || params.attribute || params.interactableOnly;
131
+ if (params.scanAll || hasRichFilters) {
132
+ let baseElements;
133
+ if (params.selector) {
134
+ try {
135
+ baseElements = document.querySelectorAll(params.selector);
136
+ } catch {
137
+ return [];
138
+ }
139
+ } else {
140
+ baseElements = document.querySelectorAll("*");
141
+ }
142
+ const candidates = [];
143
+ for (const el of baseElements) {
144
+ if (matchesFilters(el, params))
145
+ candidates.push(el);
146
+ }
147
+ return candidates;
148
+ }
149
+ const targets = [];
150
+ if (params.selector) {
151
+ const els = document.querySelectorAll(params.selector);
152
+ for (const el of els) {
153
+ const htmlEl = el;
154
+ if (!isVisible(htmlEl))
155
+ continue;
156
+ if (params.text) {
157
+ if (matchesText(htmlEl, params.text)) {
158
+ targets.push(htmlEl);
159
+ }
160
+ } else {
161
+ targets.push(htmlEl);
162
+ }
163
+ }
164
+ } else if (params.text) {
165
+ const clickableSelectors = ["button", "a", '[role="button"]', '[role="link"]', ".btn"];
166
+ for (const sel of clickableSelectors) {
167
+ const els = document.querySelectorAll(sel);
168
+ for (const el of els) {
169
+ const htmlEl = el;
170
+ if (!isVisible(htmlEl))
171
+ continue;
172
+ if (matchesText(htmlEl, params.text)) {
173
+ targets.push(htmlEl);
174
+ }
175
+ }
176
+ if (targets.length > 0)
177
+ break;
178
+ }
179
+ }
180
+ return targets;
181
+ }
182
+
183
+ // apps/extension/src/content/wait.ts
184
+ var LOADING_SELECTORS = [
185
+ '[class*="loading"]',
186
+ '[class*="Loading"]',
187
+ ".spinner",
188
+ '[class*="spinner"]',
189
+ '[role="progressbar"]'
190
+ ];
191
+ function hasVisibleLoading() {
192
+ return LOADING_SELECTORS.some((sel) => {
193
+ const els = document.querySelectorAll(sel);
194
+ return Array.from(els).some((el) => isVisible(el));
195
+ });
196
+ }
197
+ function waitForContent(timeoutMs = 5000) {
198
+ return new Promise((resolve) => {
199
+ const start = Date.now();
200
+ function check() {
201
+ if (!hasVisibleLoading() || Date.now() - start > timeoutMs) {
202
+ resolve();
203
+ return;
204
+ }
205
+ setTimeout(check, 100);
206
+ }
207
+ check();
208
+ });
209
+ }
210
+
211
+ // apps/extension/src/content/get-page.ts
212
+ function getPage() {
213
+ const clone = document.body.cloneNode(true);
214
+ for (const hlEl of clone.querySelectorAll("[data-mcp-annotate]")) {
215
+ hlEl.remove();
216
+ }
217
+ const origInputs = document.body.querySelectorAll("input, textarea");
218
+ const cloneInputs = clone.querySelectorAll("input, textarea");
219
+ origInputs.forEach((orig, i) => {
220
+ const origEl = orig;
221
+ const cloneEl = cloneInputs[i];
222
+ if (!cloneEl) {
223
+ return;
224
+ }
225
+ if (isSensitiveInput(cloneEl)) {
226
+ cloneEl.removeAttribute("value");
227
+ return;
228
+ }
229
+ if (origEl.value) {
230
+ cloneEl.setAttribute("value", origEl.value);
231
+ }
232
+ });
233
+ const origSelects = document.body.querySelectorAll("select");
234
+ const cloneSelects = clone.querySelectorAll("select");
235
+ origSelects.forEach((orig, i) => {
236
+ const origEl = orig;
237
+ const cloneEl = cloneSelects[i];
238
+ if (cloneEl) {
239
+ const selectedIdx = origEl.selectedIndex;
240
+ const opts = cloneEl.querySelectorAll("option");
241
+ opts.forEach((opt, j) => {
242
+ if (j === selectedIdx)
243
+ opt.setAttribute("selected", "");
244
+ else
245
+ opt.removeAttribute("selected");
246
+ });
247
+ }
248
+ });
249
+ return {
250
+ html: clone.innerHTML,
251
+ url: window.location.href,
252
+ title: document.title
253
+ };
254
+ }
255
+
256
+ // apps/extension/src/content/diagnose.ts
257
+ function diagnoseElement(params) {
258
+ const allMatches = findMatchingElementsUnfiltered(params);
259
+ if (allMatches.length === 0) {
260
+ const candidates = findCandidates(params);
261
+ return {
262
+ reason: "not_found",
263
+ ...candidates.length > 0 ? { candidates } : {}
264
+ };
265
+ }
266
+ const index = (params.nth ?? 1) - 1;
267
+ if (index < 0 || index >= allMatches.length) {
268
+ return {
269
+ reason: "not_found",
270
+ details: { element: `nth=${params.nth} but only ${allMatches.length} match(es)` }
271
+ };
272
+ }
273
+ const el = allMatches[index];
274
+ const visibilityIssue = diagnoseVisibility(el);
275
+ if (visibilityIssue) {
276
+ return {
277
+ reason: "invisible",
278
+ details: {
279
+ element: el.tagName.toLowerCase(),
280
+ ...visibilityIssue
281
+ }
282
+ };
283
+ }
284
+ const disabledIssue = diagnoseDisabled(el);
285
+ if (disabledIssue) {
286
+ return {
287
+ reason: "disabled",
288
+ details: disabledIssue
289
+ };
290
+ }
291
+ const coverageIssue = diagnoseCoverage(el);
292
+ if (coverageIssue) {
293
+ return {
294
+ reason: "covered",
295
+ details: coverageIssue
296
+ };
297
+ }
298
+ return { reason: "ok" };
299
+ }
300
+ function findMatchingElementsUnfiltered(params) {
301
+ if (params.nearText) {
302
+ const maxDepth = params.nearMaxDepth ?? 3;
303
+ return collectByNearTextUnfiltered(params.nearText, maxDepth, params);
304
+ }
305
+ const hasRichFilters = params.tag || params.role || params.attribute;
306
+ if (params.scanAll || hasRichFilters) {
307
+ let baseElements;
308
+ if (params.selector) {
309
+ try {
310
+ baseElements = document.querySelectorAll(params.selector);
311
+ } catch {
312
+ return [];
313
+ }
314
+ } else {
315
+ baseElements = document.querySelectorAll("*");
316
+ }
317
+ return applyNonVisibilityFilters(Array.from(baseElements), params);
318
+ }
319
+ if (params.selector) {
320
+ try {
321
+ const els = document.querySelectorAll(params.selector);
322
+ const filtered = applyNonVisibilityFilters(Array.from(els), params);
323
+ return filtered;
324
+ } catch {
325
+ return [];
326
+ }
327
+ }
328
+ if (params.text) {
329
+ const clickableSelectors = ["button", "a", '[role="button"]', '[role="link"]', ".btn"];
330
+ for (const sel of clickableSelectors) {
331
+ const els = document.querySelectorAll(sel);
332
+ const matches = [];
333
+ for (const el of els) {
334
+ const htmlEl = el;
335
+ if (matchesText(htmlEl, params.text))
336
+ matches.push(htmlEl);
337
+ }
338
+ if (matches.length > 0)
339
+ return matches;
340
+ }
341
+ }
342
+ return [];
343
+ }
344
+ function applyNonVisibilityFilters(elements, params) {
345
+ return elements.filter((el) => {
346
+ if (params.tag && el.tagName.toLowerCase() !== params.tag.toLowerCase())
347
+ return false;
348
+ if (params.text && !matchesText(el, params.text))
349
+ return false;
350
+ if (params.role) {
351
+ const elRole = el.getAttribute("role") ?? "";
352
+ if (elRole.toLowerCase() !== params.role.toLowerCase())
353
+ return false;
354
+ }
355
+ if (params.attribute) {
356
+ const attrVal = el.getAttribute(params.attribute);
357
+ if (attrVal === null)
358
+ return false;
359
+ if (params.attributeValue) {
360
+ if (!attrVal.toLowerCase().includes(params.attributeValue.toLowerCase()))
361
+ return false;
362
+ }
363
+ }
364
+ if (params.interactableOnly && !isInteractable(el))
365
+ return false;
366
+ return true;
367
+ });
368
+ }
369
+ function collectByNearTextUnfiltered(nearText, maxDepth, params) {
370
+ const results = [];
371
+ const seen = new Set;
372
+ const lowerNear = nearText.toLowerCase();
373
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
374
+ acceptNode(node) {
375
+ return node.textContent?.toLowerCase().includes(lowerNear) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
376
+ }
377
+ });
378
+ const textNodes = [];
379
+ for (let tNode = walker.nextNode();tNode !== null; tNode = walker.nextNode()) {
380
+ textNodes.push(tNode);
381
+ }
382
+ for (const textNode of textNodes) {
383
+ let ancestor = textNode.parentElement;
384
+ for (let d = 0;d < maxDepth && ancestor && ancestor !== document.body; d++) {
385
+ ancestor = ancestor.parentElement;
386
+ }
387
+ if (!ancestor)
388
+ continue;
389
+ for (const desc of ancestor.querySelectorAll("*")) {
390
+ const htmlDesc = desc;
391
+ if (seen.has(htmlDesc))
392
+ continue;
393
+ const matchesFiltersNoVis = applyNonVisibilityFilters([htmlDesc], {
394
+ ...params,
395
+ nearText: undefined,
396
+ interactableOnly: params.interactableOnly ?? (!params.tag && !params.role && !params.selector)
397
+ });
398
+ if (matchesFiltersNoVis.length > 0) {
399
+ seen.add(htmlDesc);
400
+ results.push(htmlDesc);
401
+ }
402
+ }
403
+ }
404
+ return results;
405
+ }
406
+ function diagnoseVisibility(el) {
407
+ const rect = el.getBoundingClientRect();
408
+ if (rect.width <= 0 || rect.height <= 0) {
409
+ return { visibility: "zero-size" };
410
+ }
411
+ const style = getComputedStyle(el);
412
+ if (style.display === "none")
413
+ return { visibility: "display:none" };
414
+ if (style.visibility === "hidden" || style.visibility === "collapse")
415
+ return { visibility: "visibility:hidden" };
416
+ if (style.opacity === "0")
417
+ return { visibility: "opacity:0" };
418
+ let parent = el.parentElement;
419
+ while (parent) {
420
+ const ps = getComputedStyle(parent);
421
+ if (ps.display === "none") {
422
+ return { visibility: "display:none", ancestor: describeElement(parent) };
423
+ }
424
+ if (ps.visibility === "hidden" || ps.visibility === "collapse") {
425
+ return { visibility: "visibility:hidden", ancestor: describeElement(parent) };
426
+ }
427
+ if (ps.opacity === "0") {
428
+ return { visibility: "opacity:0", ancestor: describeElement(parent) };
429
+ }
430
+ parent = parent.parentElement;
431
+ }
432
+ return null;
433
+ }
434
+ function diagnoseDisabled(el) {
435
+ if (el.disabled === true || el.getAttribute("disabled") !== null) {
436
+ return { disabledBy: "disabled" };
437
+ }
438
+ if (el.getAttribute("aria-disabled") === "true") {
439
+ return { disabledBy: "aria-disabled" };
440
+ }
441
+ const style = getComputedStyle(el);
442
+ if (style.pointerEvents === "none") {
443
+ return { disabledBy: "pointer-events:none" };
444
+ }
445
+ if (el.hasAttribute("inert") || el.closest("[inert]")) {
446
+ return { disabledBy: "inert" };
447
+ }
448
+ return null;
449
+ }
450
+ function diagnoseCoverage(el) {
451
+ const rect = el.getBoundingClientRect();
452
+ if (rect.width <= 0 || rect.height <= 0)
453
+ return null;
454
+ const centerX = rect.left + rect.width / 2;
455
+ const centerY = rect.top + rect.height / 2;
456
+ const topEl = document.elementFromPoint(centerX, centerY);
457
+ if (!topEl)
458
+ return null;
459
+ if (topEl !== el && !el.contains(topEl)) {
460
+ return {
461
+ coveredBy: describeElement(topEl)
462
+ };
463
+ }
464
+ return null;
465
+ }
466
+ function findCandidates(params) {
467
+ const candidates = [];
468
+ const clickable = document.querySelectorAll("button, a, [role='button'], [role='link'], input[type='submit']");
469
+ if (params.text) {
470
+ const lower = params.text.toLowerCase();
471
+ for (const el of clickable) {
472
+ if (candidates.length >= 3)
473
+ break;
474
+ const htmlEl = el;
475
+ const text = htmlEl.textContent?.trim() ?? "";
476
+ const label = htmlEl.getAttribute("aria-label") ?? "";
477
+ const combined = `${text} ${label}`.toLowerCase();
478
+ const queryWords = lower.split(/\s+/).filter((w) => w.length >= 2);
479
+ const hasOverlap = queryWords.some((w) => combined.includes(w));
480
+ if (hasOverlap && (text || label)) {
481
+ candidates.push({
482
+ tag: htmlEl.tagName.toLowerCase(),
483
+ text: (text || label).slice(0, 50),
484
+ selector: generateSimpleSelector(htmlEl)
485
+ });
486
+ }
487
+ }
488
+ }
489
+ if (candidates.length === 0) {
490
+ for (const el of clickable) {
491
+ if (candidates.length >= 3)
492
+ break;
493
+ const htmlEl = el;
494
+ const text = htmlEl.textContent?.trim() ?? "";
495
+ if (text || htmlEl.getAttribute("aria-label")) {
496
+ candidates.push({
497
+ tag: htmlEl.tagName.toLowerCase(),
498
+ text: text.slice(0, 50) || htmlEl.getAttribute("aria-label")?.slice(0, 50) || "",
499
+ selector: generateSimpleSelector(htmlEl)
500
+ });
501
+ }
502
+ }
503
+ }
504
+ return candidates;
505
+ }
506
+ function describeElement(el) {
507
+ const tag = el.tagName.toLowerCase();
508
+ const id = el.id ? `#${el.id}` : "";
509
+ const cls = el.className && typeof el.className === "string" ? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".") : "";
510
+ return `${tag}${id}${cls}`;
511
+ }
512
+ function generateSimpleSelector(el) {
513
+ if (el.id)
514
+ return `#${el.id}`;
515
+ const tag = el.tagName.toLowerCase();
516
+ const cls = el.className && typeof el.className === "string" ? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".") : "";
517
+ return `${tag}${cls}`;
518
+ }
519
+
520
+ // apps/extension/src/content/click-element.ts
521
+ function waitAfterClick(waitSelector, beforeCount, timeoutMs) {
522
+ return new Promise((resolve) => {
523
+ const start = Date.now();
524
+ function check() {
525
+ const hasLoading = hasVisibleLoading();
526
+ let ready;
527
+ if (waitSelector) {
528
+ try {
529
+ const afterCount = document.querySelectorAll(waitSelector).length;
530
+ ready = !hasLoading && afterCount !== beforeCount;
531
+ } catch {
532
+ ready = !hasLoading;
533
+ }
534
+ } else {
535
+ ready = !hasLoading;
536
+ }
537
+ if (ready || Date.now() - start > timeoutMs) {
538
+ resolve();
539
+ return;
540
+ }
541
+ setTimeout(check, 100);
542
+ }
543
+ setTimeout(check, 200);
544
+ });
545
+ }
546
+ function checkPostcondition(params, remainingMs) {
547
+ if (!params?.expectText && !params?.expectSelector && !params?.expectUrlContains) {
548
+ return Promise.resolve(null);
549
+ }
550
+ const effectiveTimeout = Math.max(remainingMs, 0);
551
+ return new Promise((resolve) => {
552
+ const start = Date.now();
553
+ function check() {
554
+ if (params.expectText) {
555
+ const bodyText = document.body?.innerText ?? "";
556
+ if (bodyText.includes(params.expectText)) {
557
+ resolve(null);
558
+ return;
559
+ }
560
+ if (Date.now() - start >= effectiveTimeout) {
561
+ resolve({ type: "text", expected: params.expectText, actual: bodyText.slice(0, 200) });
562
+ return;
563
+ }
564
+ } else if (params.expectSelector) {
565
+ try {
566
+ if (document.querySelector(params.expectSelector)) {
567
+ resolve(null);
568
+ return;
569
+ }
570
+ } catch {}
571
+ if (Date.now() - start >= effectiveTimeout) {
572
+ resolve({ type: "selector", expected: params.expectSelector, actual: "not found" });
573
+ return;
574
+ }
575
+ } else if (params.expectUrlContains) {
576
+ if (window.location.href.includes(params.expectUrlContains)) {
577
+ resolve(null);
578
+ return;
579
+ }
580
+ if (Date.now() - start >= effectiveTimeout) {
581
+ resolve({ type: "url", expected: params.expectUrlContains, actual: window.location.href });
582
+ return;
583
+ }
584
+ }
585
+ setTimeout(check, 100);
586
+ }
587
+ check();
588
+ });
589
+ }
590
+ async function clickElement(params) {
591
+ const targets = findMatchingElements(params);
592
+ if (targets.length === 0) {
593
+ const diag = diagnoseElement(params ?? {});
594
+ const diagnosis = diag.reason !== "ok" ? { reason: diag.reason, details: diag.details, candidates: diag.candidates } : undefined;
595
+ return { clicked: false, count: 0, ...diagnosis ? { diagnosis } : {} };
596
+ }
597
+ const timeout = params?.timeout ?? 5000;
598
+ if (params?.cdpClick) {
599
+ const index2 = (params?.nth ?? 1) - 1;
600
+ if (index2 < 0 || index2 >= targets.length) {
601
+ return { clicked: false, count: 0 };
602
+ }
603
+ const el = targets[index2];
604
+ el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
605
+ await new Promise((r) => setTimeout(r, 50));
606
+ const rect = el.getBoundingClientRect();
607
+ if (rect.width === 0 && rect.height === 0) {
608
+ return { clicked: false, count: 0 };
609
+ }
610
+ const x = Math.round(rect.left + rect.width / 2);
611
+ const y = Math.round(rect.top + rect.height / 2);
612
+ return { clicked: false, cdpTargets: [{ x, y }] };
613
+ }
614
+ function getBeforeCount() {
615
+ if (!params?.waitSelector)
616
+ return 0;
617
+ try {
618
+ return document.querySelectorAll(params.waitSelector).length;
619
+ } catch {
620
+ return 0;
621
+ }
622
+ }
623
+ if (params?.all) {
624
+ let clickedCount = 0;
625
+ const deadline = Date.now() + timeout;
626
+ for (const target of targets) {
627
+ const beforeCount2 = getBeforeCount();
628
+ target.click();
629
+ clickedCount++;
630
+ const perElementTimeout = Math.min(Math.max(deadline - Date.now(), 0), 2000);
631
+ await waitAfterClick(params?.waitSelector, beforeCount2, perElementTimeout);
632
+ if (Date.now() >= deadline)
633
+ break;
634
+ }
635
+ const remaining2 = deadline - Date.now();
636
+ const expectDetails2 = await checkPostcondition(params, remaining2);
637
+ if (expectDetails2) {
638
+ return { clicked: true, count: clickedCount, expectMet: false, expectDetails: expectDetails2 };
639
+ }
640
+ if (params?.expectText || params?.expectSelector || params?.expectUrlContains) {
641
+ return { clicked: true, count: clickedCount, expectMet: true };
642
+ }
643
+ return { clicked: true, count: clickedCount };
644
+ }
645
+ const index = (params?.nth ?? 1) - 1;
646
+ if (index < 0 || index >= targets.length) {
647
+ return { clicked: false, count: 0 };
648
+ }
649
+ const clickStart = Date.now();
650
+ const beforeCount = getBeforeCount();
651
+ targets[index].click();
652
+ await waitAfterClick(params?.waitSelector, beforeCount, timeout);
653
+ const remaining = timeout - (Date.now() - clickStart);
654
+ const expectDetails = await checkPostcondition(params, remaining);
655
+ if (expectDetails) {
656
+ return { clicked: true, count: 1, expectMet: false, expectDetails };
657
+ }
658
+ if (params?.expectText || params?.expectSelector || params?.expectUrlContains) {
659
+ return { clicked: true, count: 1, expectMet: true };
660
+ }
661
+ return { clicked: true, count: 1 };
662
+ }
663
+
664
+ // apps/extension/src/content/action-element.ts
665
+ function actionElement(params) {
666
+ const actionType = params?.action;
667
+ if (!actionType) {
668
+ return { dispatched: false, action: "none", count: 0 };
669
+ }
670
+ if (actionType === "drag") {
671
+ return dispatchDrag(params);
672
+ }
673
+ const targets = findMatchingElements(params);
674
+ if (targets.length === 0) {
675
+ return { dispatched: false, action: actionType ?? "none", count: 0 };
676
+ }
677
+ const elementsToAct = params?.all ? targets : [targets[0]];
678
+ for (const el of elementsToAct) {
679
+ if (actionType === "hover") {
680
+ el.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true, cancelable: true }));
681
+ el.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, cancelable: true }));
682
+ el.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true }));
683
+ } else if (actionType === "dblclick") {
684
+ el.dispatchEvent(new MouseEvent("dblclick", { bubbles: true, cancelable: true }));
685
+ } else if (actionType === "contextmenu") {
686
+ el.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true, cancelable: true }));
687
+ } else if (actionType === "mousedown") {
688
+ el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
689
+ } else if (actionType === "mouseup") {
690
+ el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
691
+ } else if (actionType === "focus") {
692
+ el.focus();
693
+ el.dispatchEvent(new FocusEvent("focus", { bubbles: true }));
694
+ } else if (actionType === "blur") {
695
+ el.blur();
696
+ el.dispatchEvent(new FocusEvent("blur", { bubbles: true }));
697
+ } else {
698
+ el.dispatchEvent(new Event(actionType, { bubbles: true, cancelable: true }));
699
+ }
700
+ }
701
+ return { dispatched: true, action: actionType, count: elementsToAct.length };
702
+ }
703
+ function dispatchDrag(params) {
704
+ const sources = findMatchingElements(params);
705
+ if (sources.length === 0) {
706
+ return { dispatched: false, action: "drag", count: 0 };
707
+ }
708
+ const source = sources[0];
709
+ const targetSelector = params?.targetSelector;
710
+ if (!targetSelector) {
711
+ return { dispatched: false, action: "drag", count: 0 };
712
+ }
713
+ const target = document.querySelector(targetSelector);
714
+ if (!target) {
715
+ return { dispatched: false, action: "drag", count: 0 };
716
+ }
717
+ const dt = new DataTransfer;
718
+ const sourceRect = source.getBoundingClientRect();
719
+ const targetRect = target.getBoundingClientRect();
720
+ const srcX = sourceRect.left + sourceRect.width / 2;
721
+ const srcY = sourceRect.top + sourceRect.height / 2;
722
+ const tgtX = targetRect.left + targetRect.width / 2;
723
+ const tgtY = targetRect.top + targetRect.height / 2;
724
+ source.dispatchEvent(new DragEvent("dragstart", {
725
+ bubbles: true,
726
+ cancelable: true,
727
+ dataTransfer: dt,
728
+ clientX: srcX,
729
+ clientY: srcY
730
+ }));
731
+ source.dispatchEvent(new DragEvent("drag", {
732
+ bubbles: true,
733
+ cancelable: true,
734
+ dataTransfer: dt,
735
+ clientX: srcX,
736
+ clientY: srcY
737
+ }));
738
+ target.dispatchEvent(new DragEvent("dragenter", {
739
+ bubbles: true,
740
+ cancelable: true,
741
+ dataTransfer: dt,
742
+ clientX: tgtX,
743
+ clientY: tgtY
744
+ }));
745
+ target.dispatchEvent(new DragEvent("dragover", {
746
+ bubbles: true,
747
+ cancelable: true,
748
+ dataTransfer: dt,
749
+ clientX: tgtX,
750
+ clientY: tgtY
751
+ }));
752
+ target.dispatchEvent(new DragEvent("drop", {
753
+ bubbles: true,
754
+ cancelable: true,
755
+ dataTransfer: dt,
756
+ clientX: tgtX,
757
+ clientY: tgtY
758
+ }));
759
+ source.dispatchEvent(new DragEvent("dragend", {
760
+ bubbles: true,
761
+ cancelable: true,
762
+ dataTransfer: dt,
763
+ clientX: tgtX,
764
+ clientY: tgtY
765
+ }));
766
+ return { dispatched: true, action: "drag", count: 1 };
767
+ }
768
+
769
+ // apps/extension/src/content/set-value.ts
770
+ function setValue(params) {
771
+ if (!params?.selector || params.value === undefined) {
772
+ return { set: false };
773
+ }
774
+ const el = document.querySelector(params.selector);
775
+ if (!el) {
776
+ const diag = diagnoseElement({ selector: params.selector });
777
+ const diagnosis = diag.reason !== "ok" ? { reason: diag.reason, details: diag.details, candidates: diag.candidates } : undefined;
778
+ return { set: false, ...diagnosis ? { diagnosis } : {} };
779
+ }
780
+ if (el.isContentEditable) {
781
+ el.focus();
782
+ if (params.value === "") {
783
+ if (el.textContent) {
784
+ document.execCommand("selectAll", false);
785
+ document.execCommand("delete", false);
786
+ }
787
+ if (params?.verify) {
788
+ const actual = el.innerText ?? "";
789
+ if (actual !== "") {
790
+ return { set: true, verified: false, verifyExpected: "", verifyActual: actual.slice(0, 200) };
791
+ }
792
+ return { set: true, verified: true };
793
+ }
794
+ return { set: true };
795
+ }
796
+ if (params.value.includes(`
797
+ `)) {
798
+ if (el.textContent) {
799
+ document.execCommand("selectAll", false);
800
+ document.execCommand("delete", false);
801
+ }
802
+ const dt = new DataTransfer;
803
+ dt.setData("text/plain", params.value);
804
+ const pasteEvent = new ClipboardEvent("paste", {
805
+ clipboardData: dt,
806
+ bubbles: true,
807
+ cancelable: true
808
+ });
809
+ el.dispatchEvent(pasteEvent);
810
+ if (!pasteEvent.defaultPrevented) {
811
+ const lines = params.value.split(`
812
+ `);
813
+ for (let i = 0;i < lines.length; i++) {
814
+ if (i === 0) {
815
+ document.execCommand("insertText", false, lines[i]);
816
+ } else {
817
+ document.execCommand("insertLineBreak", false);
818
+ if (lines[i]) {
819
+ document.execCommand("insertText", false, lines[i]);
820
+ }
821
+ }
822
+ }
823
+ }
824
+ } else {
825
+ if (el.textContent) {
826
+ document.execCommand("selectAll", false);
827
+ }
828
+ document.execCommand("insertText", false, params.value);
829
+ }
830
+ if (params?.verify) {
831
+ const actual = (el.innerText ?? "").replace(/\n{2,}/g, `
832
+ `).trim();
833
+ const expected = params.value.trim();
834
+ if (actual !== expected) {
835
+ return { set: true, verified: false, verifyExpected: params.value, verifyActual: actual.slice(0, 200) };
836
+ }
837
+ return { set: true, verified: true };
838
+ }
839
+ return { set: true };
840
+ }
841
+ const inputEl = el;
842
+ const nativeSetter = Object.getOwnPropertyDescriptor(inputEl instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : inputEl instanceof HTMLSelectElement ? HTMLSelectElement.prototype : HTMLInputElement.prototype, "value")?.set;
843
+ if (nativeSetter) {
844
+ nativeSetter.call(inputEl, params.value);
845
+ } else {
846
+ inputEl.value = params.value;
847
+ }
848
+ inputEl.dispatchEvent(new Event("input", { bubbles: true }));
849
+ inputEl.dispatchEvent(new Event("change", { bubbles: true }));
850
+ if (params?.verify) {
851
+ const actual = inputEl.value;
852
+ if (actual !== params.value) {
853
+ return { set: true, verified: false, verifyExpected: params.value, verifyActual: actual };
854
+ }
855
+ return { set: true, verified: true };
856
+ }
857
+ return { set: true };
858
+ }
859
+
860
+ // apps/extension/src/content/paste-file.ts
861
+ function pasteFile(params) {
862
+ if (!params?.base64 || !params?.mimeType) {
863
+ return { pasted: false };
864
+ }
865
+ const binaryStr = atob(params.base64);
866
+ const bytes = new Uint8Array(binaryStr.length);
867
+ for (let i = 0;i < binaryStr.length; i++) {
868
+ bytes[i] = binaryStr.charCodeAt(i);
869
+ }
870
+ const blob = new Blob([bytes], { type: params.mimeType });
871
+ const file = new File([blob], params.fileName ?? "pasted-file", { type: params.mimeType });
872
+ const dt = new DataTransfer;
873
+ dt.items.add(file);
874
+ const target = params.selector ? document.querySelector(params.selector) : document.activeElement;
875
+ if (!target) {
876
+ return { pasted: false };
877
+ }
878
+ target.focus();
879
+ const event = new ClipboardEvent("paste", {
880
+ clipboardData: dt,
881
+ bubbles: true,
882
+ cancelable: true
883
+ });
884
+ target.dispatchEvent(event);
885
+ return { pasted: true };
886
+ }
887
+
888
+ // apps/extension/src/content/query-selector.ts
889
+ function queryElements(params) {
890
+ if (!params?.selector) {
891
+ return { count: 0, elements: [] };
892
+ }
893
+ const limit = params.limit ?? 20;
894
+ let els;
895
+ try {
896
+ els = document.querySelectorAll(params.selector);
897
+ } catch {
898
+ return { count: 0, elements: [] };
899
+ }
900
+ const elements = [];
901
+ for (const el of els) {
902
+ if (elements.length >= limit)
903
+ break;
904
+ const htmlEl = el;
905
+ let outerHTML;
906
+ if (isSensitiveInput(htmlEl)) {
907
+ const clone = htmlEl.cloneNode(false);
908
+ clone.removeAttribute("value");
909
+ outerHTML = clone.outerHTML.slice(0, 200);
910
+ } else {
911
+ outerHTML = htmlEl.outerHTML.slice(0, 200);
912
+ }
913
+ elements.push({
914
+ tag: htmlEl.tagName.toLowerCase(),
915
+ text: (htmlEl.textContent?.trim() ?? "").slice(0, 100),
916
+ visible: isVisible(htmlEl),
917
+ outerHTML
918
+ });
919
+ }
920
+ return { count: els.length, elements };
921
+ }
922
+
923
+ // apps/extension/src/content/find-elements.ts
924
+ var KEY_ATTRIBUTES = ["id", "name", "class", "type", "placeholder", "aria-label", "role", "data-testid", "value", "href"];
925
+ function generateSelector(el) {
926
+ if (el.id) {
927
+ const sel = `#${CSS.escape(el.id)}`;
928
+ if (document.querySelectorAll(sel).length === 1)
929
+ return sel;
930
+ }
931
+ const testId = el.getAttribute("data-testid");
932
+ if (testId) {
933
+ const sel = `[data-testid="${CSS.escape(testId)}"]`;
934
+ if (document.querySelectorAll(sel).length === 1)
935
+ return sel;
936
+ }
937
+ const name = el.getAttribute("name");
938
+ if (name) {
939
+ const tag = el.tagName.toLowerCase();
940
+ const sel = `${tag}[name="${CSS.escape(name)}"]`;
941
+ if (document.querySelectorAll(sel).length === 1)
942
+ return sel;
943
+ }
944
+ const ariaLabel = el.getAttribute("aria-label");
945
+ if (ariaLabel) {
946
+ const tag = el.tagName.toLowerCase();
947
+ const sel = `${tag}[aria-label="${CSS.escape(ariaLabel)}"]`;
948
+ if (document.querySelectorAll(sel).length === 1)
949
+ return sel;
950
+ }
951
+ const placeholder = el.getAttribute("placeholder");
952
+ if (placeholder) {
953
+ const tag = el.tagName.toLowerCase();
954
+ const sel = `${tag}[placeholder="${CSS.escape(placeholder)}"]`;
955
+ if (document.querySelectorAll(sel).length === 1)
956
+ return sel;
957
+ }
958
+ const parts = [];
959
+ let current = el;
960
+ for (let depth = 0;depth < 3 && current && current !== document.body; depth++) {
961
+ const parent = current.parentElement;
962
+ if (!parent)
963
+ break;
964
+ const tag = current.tagName.toLowerCase();
965
+ const siblings = parent.querySelectorAll(`:scope > ${tag}`);
966
+ if (siblings.length === 1) {
967
+ parts.unshift(tag);
968
+ } else {
969
+ const idx = Array.from(siblings).indexOf(current) + 1;
970
+ parts.unshift(`${tag}:nth-of-type(${idx})`);
971
+ }
972
+ const candidate = parts.join(" > ");
973
+ if (document.querySelectorAll(candidate).length === 1)
974
+ return candidate;
975
+ current = parent;
976
+ }
977
+ return parts.join(" > ") || el.tagName.toLowerCase();
978
+ }
979
+ function getElementLabel(el) {
980
+ const ariaLabel = el.getAttribute("aria-label");
981
+ if (ariaLabel)
982
+ return ariaLabel;
983
+ const labelledBy = el.getAttribute("aria-labelledby");
984
+ if (labelledBy) {
985
+ const labelEl = document.getElementById(labelledBy);
986
+ if (labelEl)
987
+ return labelEl.textContent?.trim() ?? "";
988
+ }
989
+ if (el.id) {
990
+ const label = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
991
+ if (label)
992
+ return label.textContent?.trim() ?? "";
993
+ }
994
+ const ancestorLabel = el.closest("label");
995
+ if (ancestorLabel) {
996
+ const clone = ancestorLabel.cloneNode(true);
997
+ for (const inp of clone.querySelectorAll("input, select, textarea"))
998
+ inp.remove();
999
+ const text = clone.textContent?.trim() ?? "";
1000
+ if (text)
1001
+ return text;
1002
+ }
1003
+ const ph = el.getAttribute("placeholder");
1004
+ if (ph)
1005
+ return ph;
1006
+ return "";
1007
+ }
1008
+ function extractKeyAttributes(el) {
1009
+ const attrs = {};
1010
+ const sensitive = isSensitiveInput(el);
1011
+ for (const key of KEY_ATTRIBUTES) {
1012
+ if (sensitive && key === "value")
1013
+ continue;
1014
+ const val = el.getAttribute(key);
1015
+ if (val !== null && val !== "")
1016
+ attrs[key] = val.slice(0, 200);
1017
+ }
1018
+ return attrs;
1019
+ }
1020
+ function sanitizeOuterHTML(el) {
1021
+ if (isSensitiveInput(el)) {
1022
+ const clone = el.cloneNode(false);
1023
+ clone.removeAttribute("value");
1024
+ return clone.outerHTML.slice(0, 200);
1025
+ }
1026
+ return el.outerHTML.slice(0, 200);
1027
+ }
1028
+ function buildElementInfo(el) {
1029
+ return {
1030
+ generatedSelector: generateSelector(el),
1031
+ tag: el.tagName.toLowerCase(),
1032
+ text: (el.textContent?.trim() ?? "").slice(0, 100),
1033
+ visible: isVisible(el),
1034
+ attributes: extractKeyAttributes(el),
1035
+ outerHTML: sanitizeOuterHTML(el),
1036
+ label: getElementLabel(el)
1037
+ };
1038
+ }
1039
+ function findElements(params) {
1040
+ if (!params)
1041
+ return { count: 0, elements: [] };
1042
+ const limit = params.limit ?? 20;
1043
+ const candidates = findMatchingElements({ ...params, scanAll: true });
1044
+ return { count: candidates.length, elements: candidates.slice(0, limit).map(buildElementInfo) };
1045
+ }
1046
+
1047
+ // apps/extension/src/content/wait-for.ts
1048
+ function getInterval(elapsed) {
1049
+ if (elapsed < 2000)
1050
+ return 100;
1051
+ if (elapsed < 5000)
1052
+ return 200;
1053
+ return 500;
1054
+ }
1055
+ function waitFor(params) {
1056
+ const selector = params?.selector;
1057
+ if (!selector)
1058
+ return Promise.resolve({ matched: false, timeElapsed: 0 });
1059
+ const condition = params?.condition ?? "appear";
1060
+ const timeoutMs = params?.timeout ?? 1e4;
1061
+ const start = Date.now();
1062
+ function getVisibleCount(sel) {
1063
+ let els;
1064
+ try {
1065
+ els = document.querySelectorAll(sel);
1066
+ } catch {
1067
+ return { total: 0, visible: 0 };
1068
+ }
1069
+ let visible = 0;
1070
+ for (const el of els) {
1071
+ if (isVisible(el))
1072
+ visible++;
1073
+ }
1074
+ return { total: els.length, visible };
1075
+ }
1076
+ const initial = getVisibleCount(selector);
1077
+ let stableCount = 0;
1078
+ const STABLE_THRESHOLD = 2;
1079
+ return new Promise((resolve) => {
1080
+ function check() {
1081
+ const elapsed = Date.now() - start;
1082
+ const current = getVisibleCount(selector);
1083
+ if (elapsed > timeoutMs) {
1084
+ resolve({ matched: false, timeElapsed: Date.now() - start });
1085
+ return;
1086
+ }
1087
+ let met = false;
1088
+ if (condition === "appear") {
1089
+ met = current.visible > 0;
1090
+ } else if (condition === "disappear") {
1091
+ met = current.total === 0 || current.visible === 0;
1092
+ } else if (condition === "count_change") {
1093
+ const changed = current.visible !== initial.visible;
1094
+ if (changed) {
1095
+ stableCount++;
1096
+ if (stableCount < STABLE_THRESHOLD) {
1097
+ met = false;
1098
+ } else {
1099
+ met = true;
1100
+ }
1101
+ } else {
1102
+ stableCount = 0;
1103
+ met = false;
1104
+ }
1105
+ }
1106
+ if (met) {
1107
+ resolve({ matched: true, timeElapsed: Date.now() - start });
1108
+ return;
1109
+ }
1110
+ setTimeout(check, getInterval(elapsed));
1111
+ }
1112
+ check();
1113
+ });
1114
+ }
1115
+
1116
+ // apps/extension/src/content/scroll.ts
1117
+ function scrollPage(params) {
1118
+ if (params?.intoView) {
1119
+ try {
1120
+ const el = document.querySelector(params.intoView);
1121
+ if (el) {
1122
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
1123
+ return { scrolled: true, scrollX: window.scrollX, scrollY: window.scrollY };
1124
+ }
1125
+ } catch {}
1126
+ return { scrolled: false, scrollX: window.scrollX, scrollY: window.scrollY };
1127
+ }
1128
+ if (params?.scrollTo === "top") {
1129
+ window.scrollTo(0, 0);
1130
+ return { scrolled: true, scrollX: 0, scrollY: 0 };
1131
+ }
1132
+ if (params?.scrollTo === "bottom") {
1133
+ window.scrollTo(0, document.body.scrollHeight);
1134
+ return { scrolled: true, scrollX: window.scrollX, scrollY: document.body.scrollHeight };
1135
+ }
1136
+ const amount = params?.amount ?? window.innerHeight;
1137
+ const dir = params?.direction ?? "down";
1138
+ const dx = dir === "left" ? -amount : dir === "right" ? amount : 0;
1139
+ const dy = dir === "up" ? -amount : dir === "down" ? amount : 0;
1140
+ const target = params?.selector ? document.querySelector(params.selector) : null;
1141
+ if (target) {
1142
+ target.scrollBy(dx, dy);
1143
+ } else {
1144
+ window.scrollBy(dx, dy);
1145
+ }
1146
+ return { scrolled: true, scrollX: window.scrollX, scrollY: window.scrollY };
1147
+ }
1148
+
1149
+ // apps/extension/src/content/press-key.ts
1150
+ function pressKey(params) {
1151
+ if (!params?.key)
1152
+ return { dispatched: false, count: 0 };
1153
+ const repeatCount = params.repeat ?? 1;
1154
+ const mods = params.modifiers ?? [];
1155
+ const target = params.selector ? document.querySelector(params.selector) : document.activeElement;
1156
+ if (!target)
1157
+ return { dispatched: false, count: 0 };
1158
+ for (let i = 0;i < repeatCount; i++) {
1159
+ const eventInit = {
1160
+ key: params.key,
1161
+ code: params.key,
1162
+ bubbles: true,
1163
+ cancelable: true,
1164
+ ctrlKey: mods.includes("Control") || mods.includes("ctrlKey"),
1165
+ shiftKey: mods.includes("Shift") || mods.includes("shiftKey"),
1166
+ altKey: mods.includes("Alt") || mods.includes("altKey"),
1167
+ metaKey: mods.includes("Meta") || mods.includes("metaKey")
1168
+ };
1169
+ target.dispatchEvent(new KeyboardEvent("keydown", eventInit));
1170
+ target.dispatchEvent(new KeyboardEvent("keyup", eventInit));
1171
+ }
1172
+ return { dispatched: true, count: repeatCount };
1173
+ }
1174
+
1175
+ // apps/extension/src/content/get-attributes.ts
1176
+ function getAttributes(params) {
1177
+ if (!params?.selector)
1178
+ return { count: 0, elements: [] };
1179
+ const limit = params.limit ?? 20;
1180
+ const attrNames = params.attributes;
1181
+ let els;
1182
+ try {
1183
+ els = document.querySelectorAll(params.selector);
1184
+ } catch {
1185
+ return { count: 0, elements: [] };
1186
+ }
1187
+ const elements = [];
1188
+ for (const el of els) {
1189
+ if (elements.length >= limit)
1190
+ break;
1191
+ const htmlEl = el;
1192
+ const sensitive = isSensitiveInput(htmlEl);
1193
+ const attrs = {};
1194
+ if (attrNames && attrNames.length > 0) {
1195
+ for (const name of attrNames) {
1196
+ if (sensitive && name === "value")
1197
+ continue;
1198
+ const val = htmlEl.getAttribute(name);
1199
+ if (val !== null)
1200
+ attrs[name] = val;
1201
+ }
1202
+ } else {
1203
+ for (const attr of htmlEl.attributes) {
1204
+ if (sensitive && attr.name === "value")
1205
+ continue;
1206
+ attrs[attr.name] = attr.value;
1207
+ }
1208
+ }
1209
+ elements.push({ tag: htmlEl.tagName.toLowerCase(), attributes: attrs });
1210
+ }
1211
+ return { count: els.length, elements };
1212
+ }
1213
+
1214
+ // apps/extension/src/content/get-bounds.ts
1215
+ function getBounds(params) {
1216
+ const selector = params?.selector;
1217
+ if (!selector)
1218
+ return { bounds: [] };
1219
+ const all = params?.all ?? false;
1220
+ const els = all ? Array.from(document.querySelectorAll(selector)) : (() => {
1221
+ const el = document.querySelector(selector);
1222
+ return el ? [el] : [];
1223
+ })();
1224
+ const sX = window.scrollX;
1225
+ const sY = window.scrollY;
1226
+ const bounds = els.map((el, i) => {
1227
+ const rect = el.getBoundingClientRect();
1228
+ const vw = window.innerWidth;
1229
+ const vh = window.innerHeight;
1230
+ const visible = rect.bottom > 0 && rect.top < vh && rect.right > 0 && rect.left < vw && rect.width > 0 && rect.height > 0;
1231
+ return {
1232
+ index: i,
1233
+ selector,
1234
+ tag: el.tagName,
1235
+ text: (el.textContent ?? "").trim().slice(0, 100),
1236
+ x: Math.round(rect.x),
1237
+ y: Math.round(rect.y),
1238
+ width: Math.round(rect.width),
1239
+ height: Math.round(rect.height),
1240
+ scrollX: Math.round(sX),
1241
+ scrollY: Math.round(sY),
1242
+ pageX: Math.round(rect.x + sX),
1243
+ pageY: Math.round(rect.y + sY),
1244
+ visible
1245
+ };
1246
+ });
1247
+ return { bounds };
1248
+ }
1249
+
1250
+ // apps/extension/src/content/annotate.ts
1251
+ var annotationMap = new Map;
1252
+ var nextAnnotationId = 1;
1253
+ function applyAnnotations(params) {
1254
+ const items = params?.annotations ?? [];
1255
+ const results = [];
1256
+ for (const item of items) {
1257
+ const id = nextAnnotationId++;
1258
+ let element;
1259
+ switch (item.type) {
1260
+ case "rect":
1261
+ element = createRect(item);
1262
+ break;
1263
+ case "arrow":
1264
+ element = createArrow(id, item);
1265
+ break;
1266
+ case "text":
1267
+ element = createText(item);
1268
+ break;
1269
+ case "mosaic":
1270
+ element = createMosaic(item);
1271
+ break;
1272
+ case "badge":
1273
+ element = createBadge(item);
1274
+ break;
1275
+ default:
1276
+ continue;
1277
+ }
1278
+ element.setAttribute("data-mcp-annotate", String(id));
1279
+ document.body.appendChild(element);
1280
+ annotationMap.set(id, { id, type: item.type, element });
1281
+ results.push({ id, type: item.type });
1282
+ }
1283
+ return { annotations: results };
1284
+ }
1285
+ function clearAnnotations(params) {
1286
+ const ids = params?.ids;
1287
+ if (ids && ids.length > 0) {
1288
+ let cleared = 0;
1289
+ for (const id of ids) {
1290
+ const entry = annotationMap.get(id);
1291
+ if (entry) {
1292
+ entry.element.remove();
1293
+ annotationMap.delete(id);
1294
+ cleared++;
1295
+ }
1296
+ }
1297
+ return { cleared };
1298
+ }
1299
+ const count = annotationMap.size;
1300
+ for (const entry of annotationMap.values()) {
1301
+ entry.element.remove();
1302
+ }
1303
+ annotationMap.clear();
1304
+ return { cleared: count };
1305
+ }
1306
+ function createRect(item) {
1307
+ const div = document.createElement("div");
1308
+ const color = item.color ?? "red";
1309
+ const lineWidth = item.lineWidth ?? 3;
1310
+ Object.assign(div.style, {
1311
+ position: "absolute",
1312
+ left: `${item.x ?? 0}px`,
1313
+ top: `${item.y ?? 0}px`,
1314
+ width: `${item.width ?? 0}px`,
1315
+ height: `${item.height ?? 0}px`,
1316
+ border: `${lineWidth}px solid ${color}`,
1317
+ boxSizing: "border-box",
1318
+ pointerEvents: "none",
1319
+ zIndex: "2147483646"
1320
+ });
1321
+ return div;
1322
+ }
1323
+ function createArrow(id, item) {
1324
+ const fromX = item.fromX ?? 0;
1325
+ const fromY = item.fromY ?? 0;
1326
+ const toX = item.toX ?? 0;
1327
+ const toY = item.toY ?? 0;
1328
+ const color = item.color ?? "red";
1329
+ const lineWidth = item.lineWidth ?? 3;
1330
+ const pad = 20;
1331
+ const minX = Math.min(fromX, toX) - pad;
1332
+ const minY = Math.min(fromY, toY) - pad;
1333
+ const maxX = Math.max(fromX, toX) + pad;
1334
+ const maxY = Math.max(fromY, toY) + pad;
1335
+ const ns = "http://www.w3.org/2000/svg";
1336
+ const svg = document.createElementNS(ns, "svg");
1337
+ svg.setAttribute("xmlns", ns);
1338
+ svg.style.position = "absolute";
1339
+ svg.style.left = `${minX}px`;
1340
+ svg.style.top = `${minY}px`;
1341
+ svg.style.width = `${maxX - minX}px`;
1342
+ svg.style.height = `${maxY - minY}px`;
1343
+ svg.style.overflow = "visible";
1344
+ svg.style.pointerEvents = "none";
1345
+ svg.style.zIndex = "2147483646";
1346
+ const defs = document.createElementNS(ns, "defs");
1347
+ const marker = document.createElementNS(ns, "marker");
1348
+ const markerId = `mcp-arrow-${id}`;
1349
+ marker.setAttribute("id", markerId);
1350
+ marker.setAttribute("markerWidth", "10");
1351
+ marker.setAttribute("markerHeight", "7");
1352
+ marker.setAttribute("refX", "10");
1353
+ marker.setAttribute("refY", "3.5");
1354
+ marker.setAttribute("orient", "auto");
1355
+ const polygon = document.createElementNS(ns, "polygon");
1356
+ polygon.setAttribute("points", "0 0, 10 3.5, 0 7");
1357
+ polygon.setAttribute("fill", color);
1358
+ marker.appendChild(polygon);
1359
+ defs.appendChild(marker);
1360
+ svg.appendChild(defs);
1361
+ const line = document.createElementNS(ns, "line");
1362
+ line.setAttribute("x1", String(fromX - minX));
1363
+ line.setAttribute("y1", String(fromY - minY));
1364
+ line.setAttribute("x2", String(toX - minX));
1365
+ line.setAttribute("y2", String(toY - minY));
1366
+ line.setAttribute("stroke", color);
1367
+ line.setAttribute("stroke-width", String(lineWidth));
1368
+ line.setAttribute("marker-end", `url(#${markerId})`);
1369
+ svg.appendChild(line);
1370
+ return svg;
1371
+ }
1372
+ function createText(item) {
1373
+ const div = document.createElement("div");
1374
+ const color = item.color ?? "white";
1375
+ const fontSize = item.fontSize ?? 16;
1376
+ const bg = item.backgroundColor ?? "rgba(0,0,0,0.8)";
1377
+ Object.assign(div.style, {
1378
+ position: "absolute",
1379
+ left: `${item.x ?? 0}px`,
1380
+ top: `${item.y ?? 0}px`,
1381
+ color,
1382
+ fontSize: `${fontSize}px`,
1383
+ fontFamily: "Arial, Helvetica, sans-serif",
1384
+ fontWeight: "bold",
1385
+ backgroundColor: bg,
1386
+ padding: "4px 8px",
1387
+ borderRadius: "4px",
1388
+ whiteSpace: "nowrap",
1389
+ pointerEvents: "none",
1390
+ zIndex: "2147483646",
1391
+ lineHeight: "1.4"
1392
+ });
1393
+ div.textContent = item.text ?? "";
1394
+ return div;
1395
+ }
1396
+ function createMosaic(item) {
1397
+ const div = document.createElement("div");
1398
+ const blur = item.blur ?? 10;
1399
+ Object.assign(div.style, {
1400
+ position: "absolute",
1401
+ left: `${item.x ?? 0}px`,
1402
+ top: `${item.y ?? 0}px`,
1403
+ width: `${item.width ?? 0}px`,
1404
+ height: `${item.height ?? 0}px`,
1405
+ backdropFilter: `blur(${blur}px)`,
1406
+ WebkitBackdropFilter: `blur(${blur}px)`,
1407
+ pointerEvents: "none",
1408
+ zIndex: "2147483646"
1409
+ });
1410
+ return div;
1411
+ }
1412
+ function createBadge(item) {
1413
+ const div = document.createElement("div");
1414
+ const color = item.color ?? "red";
1415
+ Object.assign(div.style, {
1416
+ position: "absolute",
1417
+ left: `${(item.x ?? 0) - 12}px`,
1418
+ top: `${(item.y ?? 0) - 12}px`,
1419
+ width: "24px",
1420
+ height: "24px",
1421
+ borderRadius: "50%",
1422
+ backgroundColor: color,
1423
+ color: "white",
1424
+ fontSize: "12px",
1425
+ fontWeight: "bold",
1426
+ fontFamily: "Arial, sans-serif",
1427
+ display: "flex",
1428
+ alignItems: "center",
1429
+ justifyContent: "center",
1430
+ zIndex: "2147483646",
1431
+ boxShadow: "0 2px 4px rgba(0,0,0,0.3)",
1432
+ pointerEvents: "none"
1433
+ });
1434
+ div.textContent = String(item.number ?? 0);
1435
+ return div;
1436
+ }
1437
+
1438
+ // apps/extension/src/content/extract.ts
1439
+ var CURRENCY_RE = /^[\s]*[¥$€£₹₩₫₱₿][\s]*[\d,.]+|[\d,.]+[\s]*[¥$€£₹₩₫₱₿]|[\d,.]+\s*(USD|EUR|GBP|JPY|CNY|KRW|BRL|INR|kr|R\$)/i;
1440
+ var RATING_ATTR_RE = /^(data-)?rat(e|ing)|score|stars?$/i;
1441
+ var ALL_SEMANTIC_TYPES = ["title", "price", "url", "image", "rating", "description", "date", "author"];
1442
+ function extractData(params) {
1443
+ const { multiple = true } = params;
1444
+ let { fields } = params;
1445
+ const autoInfer = !fields || Object.keys(fields).length === 0;
1446
+ const effectiveRoot = autoInfer && !params.root ? "auto" : params.root;
1447
+ const extractionMeta = {
1448
+ mode: autoInfer ? "auto" : "explicit",
1449
+ rootResolution: "explicit",
1450
+ fieldResolution: autoInfer ? "inferred" : "explicit"
1451
+ };
1452
+ let roots;
1453
+ if (effectiveRoot === "auto") {
1454
+ const discoveryFields = autoInfer ? Object.fromEntries(ALL_SEMANTIC_TYPES.map((s) => [s, { semantic: s }])) : fields;
1455
+ roots = discoverRepeatingRoots(discoveryFields);
1456
+ if (roots.length > 0) {
1457
+ extractionMeta.rootResolution = "discovered";
1458
+ const first = roots[0];
1459
+ const tag = first.tagName.toLowerCase();
1460
+ const cls = first.className ? `.${first.className.trim().split(/\s+/).join(".")}` : "";
1461
+ extractionMeta.discoveredRoot = `${tag}${cls}`;
1462
+ }
1463
+ if (roots.length === 0 && autoInfer) {
1464
+ roots = [document.body];
1465
+ extractionMeta.rootResolution = "body_fallback";
1466
+ }
1467
+ } else if (effectiveRoot) {
1468
+ try {
1469
+ const all = document.querySelectorAll(effectiveRoot);
1470
+ roots = Array.from(all);
1471
+ } catch {
1472
+ return { items: [], total: 0, errors: [`Invalid root selector: "${effectiveRoot}"`] };
1473
+ }
1474
+ } else {
1475
+ roots = [document.body];
1476
+ }
1477
+ if (!multiple && roots.length > 0) {
1478
+ roots = [roots[0]];
1479
+ }
1480
+ if (autoInfer && roots.length > 0) {
1481
+ const inferredFields = {};
1482
+ const sample = roots[0];
1483
+ for (const semantic of ALL_SEMANTIC_TYPES) {
1484
+ const resolved = resolveSemanticField(sample, semantic, {});
1485
+ if (resolved.value !== null && resolved.value !== false) {
1486
+ inferredFields[semantic] = { semantic };
1487
+ }
1488
+ }
1489
+ fields = inferredFields;
1490
+ if (Object.keys(inferredFields).length === 0 && roots[0] !== document.body) {
1491
+ roots = [document.body];
1492
+ extractionMeta.rootResolution = "body_fallback";
1493
+ const bodyFields = {};
1494
+ for (const semantic of ALL_SEMANTIC_TYPES) {
1495
+ const resolved = resolveSemanticField(document.body, semantic, {});
1496
+ if (resolved.value !== null && resolved.value !== false) {
1497
+ bodyFields[semantic] = { semantic };
1498
+ }
1499
+ }
1500
+ fields = bodyFields;
1501
+ }
1502
+ const resolvedCount = Object.keys(fields).length;
1503
+ extractionMeta.resolvedFieldCount = resolvedCount;
1504
+ extractionMeta.fieldCoverage = resolvedCount / ALL_SEMANTIC_TYPES.length;
1505
+ }
1506
+ if (!fields || Object.keys(fields).length === 0) {
1507
+ return { items: [], total: 0, extractionMeta };
1508
+ }
1509
+ const items = [];
1510
+ const errors = [];
1511
+ const meta = {};
1512
+ let hasSemantic = false;
1513
+ for (const spec of Object.values(fields)) {
1514
+ if (spec.semantic)
1515
+ hasSemantic = true;
1516
+ }
1517
+ for (let i = 0;i < roots.length; i++) {
1518
+ const root = roots[i];
1519
+ const item = {};
1520
+ for (const [fieldName, spec] of Object.entries(fields)) {
1521
+ let value;
1522
+ if (spec.selector) {
1523
+ value = extractField(root, spec);
1524
+ if (i === 0) {
1525
+ meta[fieldName] = { matchedBy: spec.semantic ? `selector (semantic:${spec.semantic} available)` : "selector", resolvedSelector: spec.selector };
1526
+ }
1527
+ } else if (spec.semantic) {
1528
+ const resolved = resolveSemanticField(root, spec.semantic, spec);
1529
+ value = resolved.value;
1530
+ if (i === 0) {
1531
+ meta[fieldName] = { matchedBy: `semantic:${spec.semantic}`, resolvedSelector: resolved.selector };
1532
+ }
1533
+ } else {
1534
+ value = null;
1535
+ }
1536
+ item[fieldName] = value;
1537
+ const isMissing = value === null || spec.type === "exists" && value === false;
1538
+ if (spec.required && isMissing) {
1539
+ const target = spec.selector ?? spec.semantic ?? "unknown";
1540
+ errors.push(`Required field "${fieldName}" not found in root[${i}] (${target})`);
1541
+ }
1542
+ }
1543
+ items.push(item);
1544
+ }
1545
+ return {
1546
+ items,
1547
+ total: items.length,
1548
+ ...errors.length > 0 ? { errors } : {},
1549
+ ...hasSemantic ? { meta } : {},
1550
+ extractionMeta
1551
+ };
1552
+ }
1553
+ function extractField(root, spec) {
1554
+ const type = spec.type ?? "text";
1555
+ let el;
1556
+ try {
1557
+ el = root.querySelector(spec.selector);
1558
+ } catch {
1559
+ return null;
1560
+ }
1561
+ if (!el) {
1562
+ return type === "exists" ? false : null;
1563
+ }
1564
+ return extractValue(el, type, spec.attr);
1565
+ }
1566
+ function extractValue(el, type, attr) {
1567
+ switch (type) {
1568
+ case "text":
1569
+ return (el.textContent ?? "").trim() || null;
1570
+ case "attr":
1571
+ if (!attr)
1572
+ return null;
1573
+ return el.getAttribute(attr);
1574
+ case "html": {
1575
+ const clone = el.cloneNode(true);
1576
+ for (const input of clone.querySelectorAll("input")) {
1577
+ if (isSensitiveInput(input)) {
1578
+ input.removeAttribute("value");
1579
+ }
1580
+ }
1581
+ return clone.innerHTML;
1582
+ }
1583
+ case "exists":
1584
+ return true;
1585
+ default:
1586
+ return null;
1587
+ }
1588
+ }
1589
+ function contentScopedQuery(root, selector) {
1590
+ if (root !== document.body) {
1591
+ return root.querySelector(selector);
1592
+ }
1593
+ for (const scope of ["main", "[role='main']", "article", "#content", "#dp-container", ".content"]) {
1594
+ const container = root.querySelector(scope);
1595
+ if (container) {
1596
+ const el = container.querySelector(selector);
1597
+ if (el)
1598
+ return el;
1599
+ }
1600
+ }
1601
+ return root.querySelector(selector);
1602
+ }
1603
+ function contentScopedQueryAll(root, selector) {
1604
+ if (root !== document.body) {
1605
+ return Array.from(root.querySelectorAll(selector));
1606
+ }
1607
+ for (const scope of ["main", "[role='main']", "article", "#content", "#dp-container", ".content"]) {
1608
+ const container = root.querySelector(scope);
1609
+ if (container) {
1610
+ const els = Array.from(container.querySelectorAll(selector));
1611
+ if (els.length > 0)
1612
+ return els;
1613
+ }
1614
+ }
1615
+ return Array.from(root.querySelectorAll(selector));
1616
+ }
1617
+ function resolveSemanticField(root, semantic, _spec) {
1618
+ switch (semantic) {
1619
+ case "title":
1620
+ return resolveTitle(root);
1621
+ case "price":
1622
+ return resolvePrice(root);
1623
+ case "url":
1624
+ return resolveUrl(root);
1625
+ case "image":
1626
+ return resolveImage(root);
1627
+ case "rating":
1628
+ return resolveRating(root);
1629
+ case "description":
1630
+ return resolveDescription(root);
1631
+ case "date":
1632
+ return resolveByAttrOrClass(root, ["date", "time", "published", "created"], "time");
1633
+ case "author":
1634
+ return resolveByAttrOrClass(root, ["author", "byline", "writer"], undefined);
1635
+ default:
1636
+ return { value: null };
1637
+ }
1638
+ }
1639
+ function isInsideTemplate(el) {
1640
+ let parent = el.parentElement;
1641
+ while (parent) {
1642
+ if (parent.tagName === "TEMPLATE")
1643
+ return true;
1644
+ parent = parent.parentElement;
1645
+ }
1646
+ return false;
1647
+ }
1648
+ function isHiddenElement(el) {
1649
+ const htmlEl = el;
1650
+ if (htmlEl.offsetParent === null && htmlEl.tagName !== "BODY" && htmlEl.tagName !== "HTML") {
1651
+ const style = getComputedStyle(htmlEl);
1652
+ if (style.display === "none" || style.visibility === "hidden")
1653
+ return true;
1654
+ if (style.position !== "fixed" && style.position !== "sticky")
1655
+ return true;
1656
+ }
1657
+ if (el.getAttribute("aria-hidden") === "true")
1658
+ return true;
1659
+ return false;
1660
+ }
1661
+ function querySelectorVisible(root, selector) {
1662
+ const candidates = root.querySelectorAll(selector);
1663
+ for (const el of candidates) {
1664
+ if (isInsideTemplate(el))
1665
+ continue;
1666
+ if (isHiddenElement(el))
1667
+ continue;
1668
+ return el;
1669
+ }
1670
+ return null;
1671
+ }
1672
+ function isMeaningfulTitle(text) {
1673
+ if (text.length <= 2)
1674
+ return false;
1675
+ if (/^\d+\.?$/.test(text))
1676
+ return false;
1677
+ if (/^[#\-\*•→►▶]+$/.test(text))
1678
+ return false;
1679
+ return true;
1680
+ }
1681
+ function resolveTitle(root) {
1682
+ for (const sel of ["h1", "h2", "h3", "h4"]) {
1683
+ const el = querySelectorVisible(root, sel);
1684
+ if (el) {
1685
+ const text = (el.textContent ?? "").trim();
1686
+ if (isMeaningfulTitle(text))
1687
+ return { value: text, selector: sel };
1688
+ }
1689
+ }
1690
+ const itemprop = root.querySelector('[itemprop="name"]');
1691
+ if (itemprop) {
1692
+ const text = (itemprop.textContent ?? "").trim();
1693
+ if (isMeaningfulTitle(text))
1694
+ return { value: text, selector: '[itemprop="name"]' };
1695
+ }
1696
+ const links = root.querySelectorAll("a[href]");
1697
+ for (const link of links) {
1698
+ if (isInsideTemplate(link) || isHiddenElement(link))
1699
+ continue;
1700
+ const href = link.getAttribute("href") ?? "";
1701
+ if (href.startsWith("vote?") || href.startsWith("javascript:") || href === "#")
1702
+ continue;
1703
+ const text = (link.textContent ?? "").trim();
1704
+ if (isMeaningfulTitle(text) && text.length > 5) {
1705
+ return { value: text, selector: "a[href]" };
1706
+ }
1707
+ }
1708
+ for (const cls of ["title", "name", "heading"]) {
1709
+ const el = root.querySelector(`[class*="${cls}"]`);
1710
+ if (el) {
1711
+ const text = (el.textContent ?? "").trim();
1712
+ if (isMeaningfulTitle(text))
1713
+ return { value: text, selector: `[class*="${cls}"]` };
1714
+ }
1715
+ }
1716
+ return { value: null };
1717
+ }
1718
+ function resolvePrice(root) {
1719
+ const itemprop = root.querySelector('[itemprop="price"]');
1720
+ if (itemprop) {
1721
+ const text = (itemprop.textContent ?? "").trim();
1722
+ if (text)
1723
+ return { value: text, selector: '[itemprop="price"]' };
1724
+ }
1725
+ for (const cls of ["price", "cost", "amount"]) {
1726
+ const el = root.querySelector(`[class*="${cls}"]`);
1727
+ if (el) {
1728
+ const text = (el.textContent ?? "").trim();
1729
+ if (text)
1730
+ return { value: text, selector: `[class*="${cls}"]` };
1731
+ }
1732
+ }
1733
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1734
+ for (let node = walker.nextNode();node; node = walker.nextNode()) {
1735
+ const text = (node.textContent ?? "").trim();
1736
+ if (text && CURRENCY_RE.test(text)) {
1737
+ const parent = node.parentElement;
1738
+ if (parent && parent !== root) {
1739
+ return { value: text, selector: describeElement2(parent) };
1740
+ }
1741
+ }
1742
+ }
1743
+ return { value: null };
1744
+ }
1745
+ var SKIP_URL_RE = /^(vote\?|javascript:|#$|mailto:|tel:)/;
1746
+ function resolveUrl(root) {
1747
+ const links = root.querySelectorAll("a[href]");
1748
+ if (links.length === 0)
1749
+ return { value: null };
1750
+ let best = null;
1751
+ for (const link of links) {
1752
+ const href = link.getAttribute("href") ?? "";
1753
+ if (SKIP_URL_RE.test(href))
1754
+ continue;
1755
+ const text = (link.textContent ?? "").trim();
1756
+ let score = 0;
1757
+ if (href.startsWith("http"))
1758
+ score += 10;
1759
+ score += Math.min(text.length, 50) / 5;
1760
+ if (text.length > 3)
1761
+ score += 5;
1762
+ if (!best || score > best.score) {
1763
+ best = { el: link, score };
1764
+ }
1765
+ }
1766
+ if (best) {
1767
+ return { value: best.el.getAttribute("href"), selector: "a[href]" };
1768
+ }
1769
+ return { value: null };
1770
+ }
1771
+ function resolveImage(root) {
1772
+ const imgs = contentScopedQueryAll(root, "img[src]");
1773
+ let bestWithAlt = null;
1774
+ let bestWithoutAlt = null;
1775
+ for (const img of imgs) {
1776
+ const src = img.getAttribute("src");
1777
+ if (!src)
1778
+ continue;
1779
+ if (src.startsWith("data:") && src.length < 200)
1780
+ continue;
1781
+ const w = parseInt(img.getAttribute("width") ?? "0", 10);
1782
+ const h = parseInt(img.getAttribute("height") ?? "0", 10);
1783
+ if (w > 0 && h > 0 && (w < 50 || h < 50))
1784
+ continue;
1785
+ if (src.includes("sprite") || src.includes("pixel") || src.includes("spacer"))
1786
+ continue;
1787
+ const hasAlt = !!img.getAttribute("alt");
1788
+ if (hasAlt && !bestWithAlt) {
1789
+ bestWithAlt = img;
1790
+ } else if (!hasAlt && !bestWithoutAlt) {
1791
+ bestWithoutAlt = img;
1792
+ }
1793
+ }
1794
+ const chosen = bestWithAlt ?? bestWithoutAlt;
1795
+ if (chosen) {
1796
+ return { value: chosen.getAttribute("src"), selector: "img[src]" };
1797
+ }
1798
+ return { value: null };
1799
+ }
1800
+ function resolveRating(root) {
1801
+ for (const el of root.querySelectorAll("*")) {
1802
+ for (const attr of el.getAttributeNames()) {
1803
+ if (RATING_ATTR_RE.test(attr)) {
1804
+ const val = el.getAttribute(attr);
1805
+ if (val && /[\d.]+/.test(val)) {
1806
+ return { value: val, selector: `[${attr}]` };
1807
+ }
1808
+ }
1809
+ }
1810
+ }
1811
+ const itemprop = root.querySelector('[itemprop="ratingValue"]');
1812
+ if (itemprop) {
1813
+ const text = (itemprop.textContent ?? "").trim();
1814
+ if (text)
1815
+ return { value: text, selector: '[itemprop="ratingValue"]' };
1816
+ }
1817
+ for (const cls of ["rating", "score", "stars"]) {
1818
+ const el = root.querySelector(`[class*="${cls}"]`);
1819
+ if (el) {
1820
+ const text = (el.textContent ?? "").trim();
1821
+ if (text)
1822
+ return { value: text, selector: `[class*="${cls}"]` };
1823
+ }
1824
+ }
1825
+ return { value: null };
1826
+ }
1827
+ function resolveDescription(root) {
1828
+ const itemprop = contentScopedQuery(root, '[itemprop="description"]');
1829
+ if (itemprop) {
1830
+ const text = (itemprop.textContent ?? "").trim();
1831
+ if (text)
1832
+ return { value: text, selector: '[itemprop="description"]' };
1833
+ }
1834
+ const p = contentScopedQuery(root, "p");
1835
+ if (p) {
1836
+ const text = (p.textContent ?? "").trim();
1837
+ if (text && text.length > 20)
1838
+ return { value: text, selector: "p" };
1839
+ }
1840
+ for (const cls of ["description", "desc", "summary", "excerpt"]) {
1841
+ const el = contentScopedQuery(root, `[class*="${cls}"]`);
1842
+ if (el) {
1843
+ const text = (el.textContent ?? "").trim();
1844
+ if (text)
1845
+ return { value: text, selector: `[class*="${cls}"]` };
1846
+ }
1847
+ }
1848
+ return { value: null };
1849
+ }
1850
+ function resolveByAttrOrClass(root, keywords, tagHint) {
1851
+ if (tagHint) {
1852
+ const el = root.querySelector(tagHint);
1853
+ if (el) {
1854
+ const datetime = el.getAttribute("datetime");
1855
+ if (datetime)
1856
+ return { value: datetime, selector: tagHint };
1857
+ const text = (el.textContent ?? "").trim();
1858
+ if (text)
1859
+ return { value: text, selector: tagHint };
1860
+ }
1861
+ }
1862
+ for (const kw of keywords) {
1863
+ const el = root.querySelector(`[itemprop="${kw}"]`);
1864
+ if (el) {
1865
+ const text = (el.textContent ?? "").trim();
1866
+ if (text)
1867
+ return { value: text, selector: `[itemprop="${kw}"]` };
1868
+ }
1869
+ }
1870
+ for (const kw of keywords) {
1871
+ const el = root.querySelector(`[class*="${kw}"]`);
1872
+ if (el) {
1873
+ const text = (el.textContent ?? "").trim();
1874
+ if (text)
1875
+ return { value: text, selector: `[class*="${kw}"]` };
1876
+ }
1877
+ }
1878
+ return { value: null };
1879
+ }
1880
+ function describeElement2(el) {
1881
+ const tag = el.tagName.toLowerCase();
1882
+ const id = el.id ? `#${el.id}` : "";
1883
+ const cls = el.className && typeof el.className === "string" ? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".") : "";
1884
+ return `${tag}${id}${cls}`;
1885
+ }
1886
+ var MIN_REPEATING = 2;
1887
+ function discoverRepeatingRoots(fields) {
1888
+ const candidates = [];
1889
+ const articles = document.querySelectorAll("article");
1890
+ if (articles.length >= MIN_REPEATING) {
1891
+ const articleScore = scoreCandidate(Array.from(articles), fields);
1892
+ candidates.push({
1893
+ elements: Array.from(articles),
1894
+ score: articleScore > 0 ? articleScore * 2 : 0
1895
+ });
1896
+ }
1897
+ for (const list of document.querySelectorAll("ul, ol")) {
1898
+ const lis = Array.from(list.children).filter((c) => c.tagName === "LI");
1899
+ if (lis.length >= MIN_REPEATING) {
1900
+ candidates.push({
1901
+ elements: lis,
1902
+ score: scoreCandidate(lis, fields)
1903
+ });
1904
+ }
1905
+ }
1906
+ const scanned = new Set;
1907
+ const containers = [document.body, ...Array.from(document.querySelectorAll("main, section, div, [role='main'], [role='list']"))];
1908
+ for (const container of containers) {
1909
+ if (scanned.has(container))
1910
+ continue;
1911
+ scanned.add(container);
1912
+ const signatureGroups = new Map;
1913
+ for (const child of container.children) {
1914
+ const sig = elementSignature(child);
1915
+ if (!sig)
1916
+ continue;
1917
+ if (!signatureGroups.has(sig))
1918
+ signatureGroups.set(sig, []);
1919
+ signatureGroups.get(sig).push(child);
1920
+ }
1921
+ for (const group of signatureGroups.values()) {
1922
+ if (group.length >= MIN_REPEATING) {
1923
+ candidates.push({
1924
+ elements: group,
1925
+ score: scoreCandidate(group, fields)
1926
+ });
1927
+ }
1928
+ }
1929
+ }
1930
+ for (const table of document.querySelectorAll("table")) {
1931
+ const trGroups = new Map;
1932
+ for (const tr of table.querySelectorAll("tr")) {
1933
+ const cls = tr.className && typeof tr.className === "string" ? tr.className.trim() : "";
1934
+ if (!cls)
1935
+ continue;
1936
+ if (!trGroups.has(cls))
1937
+ trGroups.set(cls, []);
1938
+ trGroups.get(cls).push(tr);
1939
+ }
1940
+ for (const group of trGroups.values()) {
1941
+ if (group.length >= MIN_REPEATING) {
1942
+ candidates.push({
1943
+ elements: group,
1944
+ score: scoreCandidate(group, fields)
1945
+ });
1946
+ }
1947
+ }
1948
+ }
1949
+ const customTagCounts = new Map;
1950
+ for (const el of document.querySelectorAll("*")) {
1951
+ const tag = el.tagName.toLowerCase();
1952
+ if (tag.includes("-") && !tag.startsWith("x-") || tag.startsWith("shreddit-") || tag.startsWith("yt-")) {
1953
+ if (!customTagCounts.has(tag))
1954
+ customTagCounts.set(tag, []);
1955
+ customTagCounts.get(tag).push(el);
1956
+ }
1957
+ }
1958
+ for (const [, group] of customTagCounts) {
1959
+ if (group.length >= MIN_REPEATING) {
1960
+ candidates.push({
1961
+ elements: group,
1962
+ score: scoreCandidate(group, fields)
1963
+ });
1964
+ }
1965
+ }
1966
+ if (candidates.length === 0)
1967
+ return [];
1968
+ candidates.sort((a, b) => b.score - a.score);
1969
+ return candidates[0].elements;
1970
+ }
1971
+ function scoreCandidate(elements, fields) {
1972
+ const count = elements.length;
1973
+ const semanticFields = Object.values(fields).filter((f) => f.semantic);
1974
+ if (semanticFields.length === 0)
1975
+ return count;
1976
+ const sample = elements[0];
1977
+ let filled = 0;
1978
+ for (const spec of semanticFields) {
1979
+ if (spec.semantic) {
1980
+ const resolved = resolveSemanticField(sample, spec.semantic, spec);
1981
+ if (resolved.value !== null && resolved.value !== false)
1982
+ filled++;
1983
+ }
1984
+ }
1985
+ const coverage = filled / semanticFields.length;
1986
+ const sample0 = elements[0];
1987
+ const inNav = !!sample0.closest("nav, header, footer, [role='navigation'], [role='banner'], [role='contentinfo']");
1988
+ const navPenalty = inNav ? 0.3 : 1;
1989
+ const inMain = !!sample0.closest("main, [role='main'], article, section, #content, .content");
1990
+ const mainBonus = inMain ? 1.5 : 1;
1991
+ if (coverage === 0)
1992
+ return 0;
1993
+ if (elements.length >= 3) {
1994
+ const sampleTexts = elements.slice(0, Math.min(5, elements.length)).map((el) => (el.textContent ?? "").trim().slice(0, 50));
1995
+ const allSame = sampleTexts.every((t) => t === sampleTexts[0]);
1996
+ if (allSame)
1997
+ return 0;
1998
+ }
1999
+ return (coverage * 10 + Math.min(count, 20)) * navPenalty * mainBonus;
2000
+ }
2001
+ function elementSignature(el) {
2002
+ const tag = el.tagName.toLowerCase();
2003
+ if (tag === "script" || tag === "style" || tag === "link" || tag === "meta")
2004
+ return null;
2005
+ const cls = el.className && typeof el.className === "string" ? el.className.trim().split(/\s+/).sort().join(" ") : "";
2006
+ return `${tag}:${cls}`;
2007
+ }
2008
+
2009
+ // apps/extension/src/content.ts
2010
+ var CONTENT_SCRIPT_VERSION = 37;
2011
+ if (window.__mcp_listener) {
2012
+ try {
2013
+ chrome.runtime.onMessage.removeListener(window.__mcp_listener);
2014
+ } catch {}
2015
+ }
2016
+ var mcpListener = (request, _sender, sendResponse) => {
2017
+ if (!chrome.runtime?.id) {
2018
+ return false;
2019
+ }
2020
+ if (!request || !request.action) {
2021
+ sendResponse({ error: "Invalid request: missing action" });
2022
+ return true;
2023
+ }
2024
+ handleAction(request.action, request.params).then((result) => sendResponse(result)).catch((e) => {
2025
+ const msg = e instanceof Error ? e.message : String(e);
2026
+ sendResponse({ error: msg });
2027
+ });
2028
+ return true;
2029
+ };
2030
+ window.__mcp_listener = mcpListener;
2031
+ window.__mcp_content_version = CONTENT_SCRIPT_VERSION;
2032
+ chrome.runtime.onMessage.addListener(mcpListener);
2033
+ for (const el of document.querySelectorAll("[data-mcp-annotate]")) {
2034
+ el.remove();
2035
+ }
2036
+ async function handleAction(action, params) {
2037
+ const SKIP_WAIT = new Set(["click_element", "wait_for", "get_bounds", "annotate", "clear_annotations", "diagnose_interaction"]);
2038
+ if (!SKIP_WAIT.has(action)) {
2039
+ await waitForContent();
2040
+ }
2041
+ switch (action) {
2042
+ case "get_page":
2043
+ return getPage();
2044
+ case "click_element":
2045
+ return clickElement(params);
2046
+ case "action_element":
2047
+ return actionElement(params);
2048
+ case "set_value":
2049
+ return setValue(params);
2050
+ case "paste_file":
2051
+ return pasteFile(params);
2052
+ case "query_selector":
2053
+ return queryElements(params);
2054
+ case "find_elements":
2055
+ return findElements(params);
2056
+ case "wait_for":
2057
+ return waitFor(params);
2058
+ case "scroll":
2059
+ return scrollPage(params);
2060
+ case "press_key":
2061
+ return pressKey(params);
2062
+ case "get_attributes":
2063
+ return getAttributes(params);
2064
+ case "get_bounds":
2065
+ return getBounds(params);
2066
+ case "annotate":
2067
+ return applyAnnotations(params);
2068
+ case "clear_annotations":
2069
+ return clearAnnotations(params);
2070
+ case "extract_data": {
2071
+ const fields = {};
2072
+ if (params && typeof params === "object" && "fields" in params) {
2073
+ const rawFields = params.fields;
2074
+ if (rawFields && typeof rawFields === "object") {
2075
+ for (const [k, v] of Object.entries(rawFields)) {
2076
+ if (v && typeof v === "object") {
2077
+ const spec = v;
2078
+ fields[k] = {
2079
+ ...spec.selector ? { selector: String(spec.selector) } : {},
2080
+ ...spec.semantic ? { semantic: String(spec.semantic) } : {},
2081
+ ...spec.type ? { type: String(spec.type) } : {},
2082
+ ...spec.attr ? { attr: String(spec.attr) } : {},
2083
+ ...spec.required ? { required: true } : {}
2084
+ };
2085
+ }
2086
+ }
2087
+ }
2088
+ }
2089
+ return extractData({
2090
+ root: params?.selector,
2091
+ multiple: params?.all !== false,
2092
+ fields
2093
+ });
2094
+ }
2095
+ case "diagnose_interaction": {
2096
+ const diag = diagnoseElement(params ?? {});
2097
+ return {
2098
+ canInteract: diag.reason === "ok",
2099
+ reason: diag.reason,
2100
+ ...diag.details ? { details: diag.details } : {},
2101
+ ...diag.candidates ? { candidates: diag.candidates } : {}
2102
+ };
2103
+ }
2104
+ default:
2105
+ return { error: `Unknown action: ${action}` };
2106
+ }
2107
+ }