hypha-debugger 0.1.0

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,2374 @@
1
+ import * as hyphaRpc from 'hypha-rpc';
2
+
3
+ /**
4
+ * Scoped CSS for the debug overlay, injected into Shadow DOM.
5
+ */
6
+ const overlayStyles = `
7
+ :host {
8
+ all: initial;
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ }
11
+
12
+ * {
13
+ box-sizing: border-box;
14
+ margin: 0;
15
+ padding: 0;
16
+ }
17
+
18
+ .debugger-icon {
19
+ width: 40px;
20
+ height: 40px;
21
+ border-radius: 50%;
22
+ background: #4a90d9;
23
+ color: white;
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ cursor: pointer;
28
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
29
+ transition: transform 0.15s, background 0.15s;
30
+ user-select: none;
31
+ font-size: 18px;
32
+ line-height: 1;
33
+ }
34
+
35
+ .debugger-icon:hover {
36
+ transform: scale(1.1);
37
+ background: #357abd;
38
+ }
39
+
40
+ .debugger-icon.connected {
41
+ background: #27ae60;
42
+ }
43
+
44
+ .debugger-icon.error {
45
+ background: #e74c3c;
46
+ }
47
+
48
+ .debugger-panel {
49
+ display: none;
50
+ width: 320px;
51
+ max-height: 420px;
52
+ background: #1e1e2e;
53
+ color: #cdd6f4;
54
+ border-radius: 8px;
55
+ border: 1px solid #45475a;
56
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
57
+ overflow: hidden;
58
+ flex-direction: column;
59
+ position: absolute;
60
+ bottom: 50px;
61
+ right: 0;
62
+ font-size: 13px;
63
+ }
64
+
65
+ .debugger-panel.open {
66
+ display: flex;
67
+ }
68
+
69
+ .panel-header {
70
+ background: #313244;
71
+ padding: 8px 12px;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: space-between;
75
+ cursor: default;
76
+ border-bottom: 1px solid #45475a;
77
+ font-weight: 600;
78
+ font-size: 12px;
79
+ letter-spacing: 0.3px;
80
+ text-transform: uppercase;
81
+ color: #a6adc8;
82
+ }
83
+
84
+ .panel-header .title {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 6px;
88
+ }
89
+
90
+ .panel-header .close-btn {
91
+ cursor: pointer;
92
+ color: #6c7086;
93
+ font-size: 16px;
94
+ line-height: 1;
95
+ padding: 2px 4px;
96
+ border-radius: 3px;
97
+ }
98
+
99
+ .panel-header .close-btn:hover {
100
+ color: #cdd6f4;
101
+ background: #45475a;
102
+ }
103
+
104
+ .status-dot {
105
+ width: 8px;
106
+ height: 8px;
107
+ border-radius: 50%;
108
+ display: inline-block;
109
+ background: #6c7086;
110
+ }
111
+
112
+ .status-dot.connected {
113
+ background: #a6e3a1;
114
+ }
115
+
116
+ .status-dot.error {
117
+ background: #f38ba8;
118
+ }
119
+
120
+ .panel-body {
121
+ padding: 10px 12px;
122
+ overflow-y: auto;
123
+ flex: 1;
124
+ max-height: 340px;
125
+ }
126
+
127
+ .info-row {
128
+ display: flex;
129
+ justify-content: space-between;
130
+ padding: 4px 0;
131
+ border-bottom: 1px solid #313244;
132
+ font-size: 12px;
133
+ }
134
+
135
+ .info-row .label {
136
+ color: #6c7086;
137
+ }
138
+
139
+ .info-row .value {
140
+ color: #cdd6f4;
141
+ text-align: right;
142
+ word-break: break-all;
143
+ max-width: 180px;
144
+ font-family: 'SF Mono', Monaco, Consolas, monospace;
145
+ font-size: 11px;
146
+ }
147
+
148
+ .url-section {
149
+ margin-top: 8px;
150
+ padding-top: 6px;
151
+ border-top: 1px solid #313244;
152
+ }
153
+
154
+ .url-label {
155
+ font-size: 11px;
156
+ color: #6c7086;
157
+ text-transform: uppercase;
158
+ letter-spacing: 0.5px;
159
+ margin-bottom: 3px;
160
+ }
161
+
162
+ .url-row {
163
+ display: flex;
164
+ align-items: center;
165
+ gap: 4px;
166
+ background: #313244;
167
+ border-radius: 3px;
168
+ padding: 4px 6px;
169
+ }
170
+
171
+ .url-text {
172
+ flex: 1;
173
+ font-family: 'SF Mono', Monaco, Consolas, monospace;
174
+ font-size: 10px;
175
+ color: #89b4fa;
176
+ word-break: break-all;
177
+ overflow: hidden;
178
+ display: -webkit-box;
179
+ -webkit-line-clamp: 2;
180
+ -webkit-box-orient: vertical;
181
+ }
182
+
183
+ .copy-btn {
184
+ background: #45475a;
185
+ color: #cdd6f4;
186
+ border: none;
187
+ border-radius: 3px;
188
+ padding: 2px 8px;
189
+ font-size: 10px;
190
+ cursor: pointer;
191
+ white-space: nowrap;
192
+ flex-shrink: 0;
193
+ }
194
+
195
+ .copy-btn:hover {
196
+ background: #585b70;
197
+ }
198
+
199
+ .log-section {
200
+ margin-top: 8px;
201
+ }
202
+
203
+ .log-title {
204
+ font-size: 11px;
205
+ color: #6c7086;
206
+ text-transform: uppercase;
207
+ letter-spacing: 0.5px;
208
+ margin-bottom: 4px;
209
+ }
210
+
211
+ .log-entry {
212
+ font-family: 'SF Mono', Monaco, Consolas, monospace;
213
+ font-size: 11px;
214
+ padding: 3px 6px;
215
+ border-radius: 3px;
216
+ background: #313244;
217
+ margin-bottom: 2px;
218
+ color: #a6adc8;
219
+ word-break: break-all;
220
+ }
221
+
222
+ .log-entry.call {
223
+ border-left: 2px solid #89b4fa;
224
+ }
225
+
226
+ .log-entry.result {
227
+ border-left: 2px solid #a6e3a1;
228
+ }
229
+
230
+ .log-entry.error {
231
+ border-left: 2px solid #f38ba8;
232
+ color: #f38ba8;
233
+ }
234
+ `;
235
+
236
+ /**
237
+ * Floating debug overlay built with Shadow DOM for style isolation.
238
+ */
239
+ class DebugOverlay {
240
+ constructor(options) {
241
+ this.isOpen = false;
242
+ this.isDragging = false;
243
+ this.dragOffset = { x: 0, y: 0 };
244
+ this.maxLogEntries = 50;
245
+ // Create host container
246
+ this.host = document.createElement("div");
247
+ this.host.id = "hypha-debugger-host";
248
+ Object.assign(this.host.style, {
249
+ position: "fixed",
250
+ bottom: "20px",
251
+ right: "20px",
252
+ zIndex: "2147483647",
253
+ display: "flex",
254
+ flexDirection: "column",
255
+ alignItems: "flex-end",
256
+ });
257
+ if (options?.position) {
258
+ this.host.style.bottom = "auto";
259
+ this.host.style.right = "auto";
260
+ this.host.style.left = options.position.x + "px";
261
+ this.host.style.top = options.position.y + "px";
262
+ }
263
+ // Attach shadow DOM
264
+ this.shadow = this.host.attachShadow({ mode: "open" });
265
+ // Inject styles
266
+ const style = document.createElement("style");
267
+ style.textContent = overlayStyles;
268
+ this.shadow.appendChild(style);
269
+ // Build UI
270
+ const wrapper = document.createElement("div");
271
+ wrapper.style.position = "relative";
272
+ wrapper.style.display = "flex";
273
+ wrapper.style.flexDirection = "column";
274
+ wrapper.style.alignItems = "flex-end";
275
+ // Panel (hidden by default)
276
+ this.panel = document.createElement("div");
277
+ this.panel.className = "debugger-panel";
278
+ const header = document.createElement("div");
279
+ header.className = "panel-header";
280
+ const titleSpan = document.createElement("span");
281
+ titleSpan.className = "title";
282
+ this.statusDot = document.createElement("span");
283
+ this.statusDot.className = "status-dot";
284
+ titleSpan.appendChild(this.statusDot);
285
+ titleSpan.appendChild(document.createTextNode(" Hypha Debugger"));
286
+ const closeBtn = document.createElement("span");
287
+ closeBtn.className = "close-btn";
288
+ closeBtn.textContent = "\u00d7";
289
+ closeBtn.addEventListener("click", () => this.toggle());
290
+ header.appendChild(titleSpan);
291
+ header.appendChild(closeBtn);
292
+ const body = document.createElement("div");
293
+ body.className = "panel-body";
294
+ this.infoBody = document.createElement("div");
295
+ body.appendChild(this.infoBody);
296
+ const logSection = document.createElement("div");
297
+ logSection.className = "log-section";
298
+ const logTitle = document.createElement("div");
299
+ logTitle.className = "log-title";
300
+ logTitle.textContent = "Remote Operations";
301
+ this.logContainer = document.createElement("div");
302
+ logSection.appendChild(logTitle);
303
+ logSection.appendChild(this.logContainer);
304
+ body.appendChild(logSection);
305
+ this.panel.appendChild(header);
306
+ this.panel.appendChild(body);
307
+ // Floating icon
308
+ this.icon = document.createElement("div");
309
+ this.icon.className = "debugger-icon";
310
+ this.icon.textContent = "\u{1f41b}";
311
+ this.icon.addEventListener("click", (e) => {
312
+ if (!this.isDragging)
313
+ this.toggle();
314
+ });
315
+ wrapper.appendChild(this.panel);
316
+ wrapper.appendChild(this.icon);
317
+ this.shadow.appendChild(wrapper);
318
+ // Mount to page
319
+ document.documentElement.appendChild(this.host);
320
+ // Dragging
321
+ this.setupDrag();
322
+ }
323
+ setupDrag() {
324
+ let startX = 0;
325
+ let startY = 0;
326
+ let moved = false;
327
+ this.icon.addEventListener("mousedown", (e) => {
328
+ startX = e.clientX;
329
+ startY = e.clientY;
330
+ moved = false;
331
+ const rect = this.host.getBoundingClientRect();
332
+ this.dragOffset.x = e.clientX - rect.left;
333
+ this.dragOffset.y = e.clientY - rect.top;
334
+ e.preventDefault();
335
+ });
336
+ document.addEventListener("mousemove", (e) => {
337
+ if (startX === 0 && startY === 0)
338
+ return;
339
+ const dx = Math.abs(e.clientX - startX);
340
+ const dy = Math.abs(e.clientY - startY);
341
+ if (dx > 3 || dy > 3) {
342
+ moved = true;
343
+ this.isDragging = true;
344
+ this.host.style.left = e.clientX - this.dragOffset.x + "px";
345
+ this.host.style.top = e.clientY - this.dragOffset.y + "px";
346
+ this.host.style.right = "auto";
347
+ this.host.style.bottom = "auto";
348
+ }
349
+ });
350
+ document.addEventListener("mouseup", () => {
351
+ startX = 0;
352
+ startY = 0;
353
+ if (moved) {
354
+ // Delay clearing isDragging so click handler doesn't fire
355
+ setTimeout(() => {
356
+ this.isDragging = false;
357
+ }, 50);
358
+ }
359
+ });
360
+ }
361
+ toggle() {
362
+ this.isOpen = !this.isOpen;
363
+ this.panel.classList.toggle("open", this.isOpen);
364
+ }
365
+ setStatus(status) {
366
+ this.statusDot.className = "status-dot";
367
+ this.icon.className = "debugger-icon";
368
+ if (status === "connected") {
369
+ this.statusDot.classList.add("connected");
370
+ this.icon.classList.add("connected");
371
+ }
372
+ else if (status === "error") {
373
+ this.statusDot.classList.add("error");
374
+ this.icon.classList.add("error");
375
+ }
376
+ }
377
+ setInfo(info) {
378
+ this.infoBody.innerHTML = "";
379
+ for (const [label, value] of Object.entries(info)) {
380
+ const row = document.createElement("div");
381
+ row.className = "info-row";
382
+ const labelSpan = document.createElement("span");
383
+ labelSpan.className = "label";
384
+ labelSpan.textContent = label;
385
+ const valueSpan = document.createElement("span");
386
+ valueSpan.className = "value";
387
+ valueSpan.textContent = value;
388
+ valueSpan.title = value;
389
+ row.appendChild(labelSpan);
390
+ row.appendChild(valueSpan);
391
+ this.infoBody.appendChild(row);
392
+ }
393
+ }
394
+ /** Show the service URL with a copy button. */
395
+ setServiceUrl(url, token) {
396
+ const section = document.createElement("div");
397
+ section.className = "url-section";
398
+ const urlLabel = document.createElement("div");
399
+ urlLabel.className = "url-label";
400
+ urlLabel.textContent = "Service URL";
401
+ section.appendChild(urlLabel);
402
+ const urlRow = document.createElement("div");
403
+ urlRow.className = "url-row";
404
+ const urlText = document.createElement("span");
405
+ urlText.className = "url-text";
406
+ urlText.textContent = url;
407
+ urlText.title = url;
408
+ const copyBtn = document.createElement("button");
409
+ copyBtn.className = "copy-btn";
410
+ copyBtn.textContent = "Copy";
411
+ copyBtn.addEventListener("click", () => {
412
+ navigator.clipboard.writeText(url).then(() => {
413
+ copyBtn.textContent = "Copied!";
414
+ setTimeout(() => { copyBtn.textContent = "Copy"; }, 1500);
415
+ });
416
+ });
417
+ urlRow.appendChild(urlText);
418
+ urlRow.appendChild(copyBtn);
419
+ section.appendChild(urlRow);
420
+ const tokenLabel = document.createElement("div");
421
+ tokenLabel.className = "url-label";
422
+ tokenLabel.textContent = "Token";
423
+ tokenLabel.style.marginTop = "6px";
424
+ section.appendChild(tokenLabel);
425
+ const tokenRow = document.createElement("div");
426
+ tokenRow.className = "url-row";
427
+ const tokenText = document.createElement("span");
428
+ tokenText.className = "url-text";
429
+ tokenText.textContent = token.slice(0, 20) + "...";
430
+ tokenText.title = token;
431
+ const copyTokenBtn = document.createElement("button");
432
+ copyTokenBtn.className = "copy-btn";
433
+ copyTokenBtn.textContent = "Copy";
434
+ copyTokenBtn.addEventListener("click", () => {
435
+ navigator.clipboard.writeText(token).then(() => {
436
+ copyTokenBtn.textContent = "Copied!";
437
+ setTimeout(() => { copyTokenBtn.textContent = "Copy"; }, 1500);
438
+ });
439
+ });
440
+ tokenRow.appendChild(tokenText);
441
+ tokenRow.appendChild(copyTokenBtn);
442
+ section.appendChild(tokenRow);
443
+ this.infoBody.appendChild(section);
444
+ }
445
+ addLog(message, type = "call") {
446
+ const entry = document.createElement("div");
447
+ entry.className = `log-entry ${type}`;
448
+ entry.textContent = message;
449
+ this.logContainer.appendChild(entry);
450
+ // Trim old entries
451
+ while (this.logContainer.children.length > this.maxLogEntries) {
452
+ this.logContainer.removeChild(this.logContainer.firstChild);
453
+ }
454
+ // Auto-scroll
455
+ const body = this.panel.querySelector(".panel-body");
456
+ if (body)
457
+ body.scrollTop = body.scrollHeight;
458
+ }
459
+ destroy() {
460
+ this.host.remove();
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Environment detection and page metadata collection.
466
+ */
467
+ function detectFrameworks() {
468
+ const frameworks = [];
469
+ const w = window;
470
+ // React
471
+ if (w.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers?.size > 0) {
472
+ frameworks.push("react");
473
+ }
474
+ else {
475
+ const root = document.querySelector("#root, #app, [data-reactroot]");
476
+ if (root) {
477
+ const hasReactFiber = Object.keys(root).some((k) => k.startsWith("__reactFiber$") ||
478
+ k.startsWith("__reactInternalInstance$"));
479
+ if (hasReactFiber)
480
+ frameworks.push("react");
481
+ }
482
+ }
483
+ // Vue
484
+ if (w.__VUE__ || w.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
485
+ frameworks.push("vue");
486
+ }
487
+ else {
488
+ const el = document.querySelector("[data-v-app], #app");
489
+ if (el && el.__vue_app__)
490
+ frameworks.push("vue");
491
+ }
492
+ // Angular
493
+ if (w.ng || document.querySelector("[ng-version]")) {
494
+ frameworks.push("angular");
495
+ }
496
+ // Svelte
497
+ if (document.querySelector("[class*='svelte-']")) {
498
+ frameworks.push("svelte");
499
+ }
500
+ // Next.js
501
+ if (w.__NEXT_DATA__) {
502
+ frameworks.push("nextjs");
503
+ }
504
+ return frameworks;
505
+ }
506
+ function collectPageInfo() {
507
+ const perf = performance.getEntriesByType("navigation")[0];
508
+ return {
509
+ url: window.location.href,
510
+ title: document.title,
511
+ viewport: {
512
+ width: window.innerWidth,
513
+ height: window.innerHeight,
514
+ },
515
+ document_size: {
516
+ width: document.documentElement.scrollWidth,
517
+ height: document.documentElement.scrollHeight,
518
+ },
519
+ user_agent: navigator.userAgent,
520
+ timestamp: new Date().toISOString(),
521
+ cookies_enabled: navigator.cookieEnabled,
522
+ language: navigator.language,
523
+ platform: navigator.platform,
524
+ online: navigator.onLine,
525
+ performance: {
526
+ load_time_ms: perf ? Math.round(perf.loadEventEnd - perf.startTime) : null,
527
+ dom_content_loaded_ms: perf
528
+ ? Math.round(perf.domContentLoadedEventEnd - perf.startTime)
529
+ : null,
530
+ },
531
+ frameworks: detectFrameworks(),
532
+ };
533
+ }
534
+
535
+ /**
536
+ * Page information service.
537
+ */
538
+ function getPageInfo() {
539
+ return collectPageInfo();
540
+ }
541
+ getPageInfo.__schema__ = {
542
+ name: "getPageInfo",
543
+ description: "Get information about the current web page including URL, title, viewport size, detected frameworks, and performance timing.",
544
+ parameters: {
545
+ type: "object",
546
+ properties: {},
547
+ },
548
+ };
549
+ function getConsoleLogs(options) {
550
+ const logs = window.__HYPHA_DEBUGGER__?.consoleLogs ?? [];
551
+ const level = options?.level;
552
+ const limit = options?.limit ?? 100;
553
+ let filtered = level ? logs.filter((l) => l.level === level) : logs;
554
+ return filtered.slice(-limit);
555
+ }
556
+ getConsoleLogs.__schema__ = {
557
+ name: "getConsoleLogs",
558
+ description: "Retrieve captured console output (log, warn, error, info).",
559
+ parameters: {
560
+ type: "object",
561
+ properties: {
562
+ level: {
563
+ type: "string",
564
+ description: 'Filter by log level: "log", "warn", "error", "info". Omit for all levels.',
565
+ enum: ["log", "warn", "error", "info"],
566
+ },
567
+ limit: {
568
+ type: "number",
569
+ description: "Maximum number of log entries to return (most recent). Default: 100.",
570
+ },
571
+ },
572
+ },
573
+ };
574
+ /** Install console interceptor to capture logs. */
575
+ function installConsoleCapture(maxEntries = 500) {
576
+ var _a;
577
+ const store = ((_a = window).__HYPHA_DEBUGGER__ ?? (_a.__HYPHA_DEBUGGER__ = {}));
578
+ if (store.consoleInstalled)
579
+ return;
580
+ store.consoleLogs = [];
581
+ store.consoleInstalled = true;
582
+ const levels = ["log", "warn", "error", "info"];
583
+ for (const level of levels) {
584
+ const original = console[level].bind(console);
585
+ console[level] = (...args) => {
586
+ const logs = store.consoleLogs;
587
+ logs.push({
588
+ level,
589
+ message: args
590
+ .map((a) => {
591
+ try {
592
+ return typeof a === "string" ? a : JSON.stringify(a);
593
+ }
594
+ catch {
595
+ return String(a);
596
+ }
597
+ })
598
+ .join(" "),
599
+ timestamp: new Date().toISOString(),
600
+ });
601
+ if (logs.length > maxEntries) {
602
+ logs.splice(0, logs.length - maxEntries);
603
+ }
604
+ original(...args);
605
+ };
606
+ }
607
+ }
608
+
609
+ /**
610
+ * DOM query and manipulation service.
611
+ */
612
+ function elementToInfo(el) {
613
+ const rect = el.getBoundingClientRect();
614
+ const attrs = {};
615
+ for (const attr of Array.from(el.attributes)) {
616
+ attrs[attr.name] = attr.value;
617
+ }
618
+ return {
619
+ tag: el.tagName.toLowerCase(),
620
+ id: el.id,
621
+ classes: Array.from(el.classList),
622
+ text: (el.textContent ?? "").trim().slice(0, 500),
623
+ attributes: attrs,
624
+ bounds: {
625
+ x: Math.round(rect.x),
626
+ y: Math.round(rect.y),
627
+ width: Math.round(rect.width),
628
+ height: Math.round(rect.height),
629
+ },
630
+ visible: rect.width > 0 &&
631
+ rect.height > 0 &&
632
+ getComputedStyle(el).visibility !== "hidden" &&
633
+ getComputedStyle(el).display !== "none",
634
+ children_count: el.children.length,
635
+ };
636
+ }
637
+ function queryDom(selector, options) {
638
+ const limit = options?.limit ?? 20;
639
+ const elements = document.querySelectorAll(selector);
640
+ const results = [];
641
+ for (let i = 0; i < Math.min(elements.length, limit); i++) {
642
+ results.push(elementToInfo(elements[i]));
643
+ }
644
+ return results;
645
+ }
646
+ queryDom.__schema__ = {
647
+ name: "queryDom",
648
+ description: "Query DOM elements by CSS selector. Returns tag, id, classes, text content, attributes, bounding rect, and visibility for each matching element.",
649
+ parameters: {
650
+ type: "object",
651
+ properties: {
652
+ selector: {
653
+ type: "string",
654
+ description: 'CSS selector to query, e.g. "button.primary", "#app > div".',
655
+ },
656
+ limit: {
657
+ type: "number",
658
+ description: "Maximum number of elements to return. Default: 20.",
659
+ },
660
+ },
661
+ required: ["selector"],
662
+ },
663
+ };
664
+ function clickElement(selector) {
665
+ const el = document.querySelector(selector);
666
+ if (!el) {
667
+ return { success: false, message: `No element found for selector: ${selector}` };
668
+ }
669
+ const rect = el.getBoundingClientRect();
670
+ el.dispatchEvent(new MouseEvent("click", {
671
+ bubbles: true,
672
+ cancelable: true,
673
+ view: window,
674
+ clientX: rect.left + rect.width / 2,
675
+ clientY: rect.top + rect.height / 2,
676
+ }));
677
+ return { success: true, message: `Clicked element: ${selector}` };
678
+ }
679
+ clickElement.__schema__ = {
680
+ name: "clickElement",
681
+ description: "Click a DOM element matching the CSS selector.",
682
+ parameters: {
683
+ type: "object",
684
+ properties: {
685
+ selector: {
686
+ type: "string",
687
+ description: "CSS selector of the element to click.",
688
+ },
689
+ },
690
+ required: ["selector"],
691
+ },
692
+ };
693
+ function fillInput(selector, value) {
694
+ const el = document.querySelector(selector);
695
+ if (!el) {
696
+ return { success: false, message: `No element found for selector: ${selector}` };
697
+ }
698
+ const tag = el.tagName.toLowerCase();
699
+ if (tag === "input" || tag === "textarea") {
700
+ const inputEl = el;
701
+ // Use native setter to bypass React controlled component interception
702
+ const proto = tag === "textarea"
703
+ ? HTMLTextAreaElement.prototype
704
+ : HTMLInputElement.prototype;
705
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
706
+ if (nativeSetter) {
707
+ nativeSetter.call(inputEl, value);
708
+ }
709
+ else {
710
+ inputEl.value = value;
711
+ }
712
+ inputEl.dispatchEvent(new Event("input", { bubbles: true }));
713
+ inputEl.dispatchEvent(new Event("change", { bubbles: true }));
714
+ return { success: true, message: `Filled ${selector} with value` };
715
+ }
716
+ if (tag === "select") {
717
+ const selectEl = el;
718
+ const nativeSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value")?.set;
719
+ if (nativeSetter) {
720
+ nativeSetter.call(selectEl, value);
721
+ }
722
+ else {
723
+ selectEl.value = value;
724
+ }
725
+ selectEl.dispatchEvent(new Event("change", { bubbles: true }));
726
+ return { success: true, message: `Set ${selector} to ${value}` };
727
+ }
728
+ return { success: false, message: `Element ${selector} is not an input/textarea/select` };
729
+ }
730
+ fillInput.__schema__ = {
731
+ name: "fillInput",
732
+ description: "Set the value of an input, textarea, or select element. Works with React controlled components.",
733
+ parameters: {
734
+ type: "object",
735
+ properties: {
736
+ selector: {
737
+ type: "string",
738
+ description: "CSS selector of the input element.",
739
+ },
740
+ value: {
741
+ type: "string",
742
+ description: "The value to set.",
743
+ },
744
+ },
745
+ required: ["selector", "value"],
746
+ },
747
+ };
748
+ function scrollTo(target) {
749
+ if (typeof target === "string") {
750
+ const el = document.querySelector(target);
751
+ if (!el) {
752
+ return { success: false, message: `No element found for selector: ${target}` };
753
+ }
754
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
755
+ return { success: true, message: `Scrolled to ${target}` };
756
+ }
757
+ window.scrollTo({ left: target.x, top: target.y, behavior: "smooth" });
758
+ return { success: true, message: `Scrolled to (${target.x}, ${target.y})` };
759
+ }
760
+ scrollTo.__schema__ = {
761
+ name: "scrollTo",
762
+ description: "Scroll to a DOM element (by CSS selector) or to an absolute position {x, y}.",
763
+ parameters: {
764
+ type: "object",
765
+ properties: {
766
+ target: {
767
+ description: 'CSS selector string to scroll to an element, or an object {x, y} for absolute scroll position.',
768
+ },
769
+ },
770
+ required: ["target"],
771
+ },
772
+ };
773
+ function getComputedStyles(selector, properties) {
774
+ const el = document.querySelector(selector);
775
+ if (!el) {
776
+ return { error: `No element found for selector: ${selector}` };
777
+ }
778
+ const computed = getComputedStyle(el);
779
+ const result = {};
780
+ const props = properties ??
781
+ [
782
+ "display",
783
+ "position",
784
+ "width",
785
+ "height",
786
+ "color",
787
+ "background-color",
788
+ "font-size",
789
+ "font-family",
790
+ "margin",
791
+ "padding",
792
+ "border",
793
+ "opacity",
794
+ "visibility",
795
+ "overflow",
796
+ "z-index",
797
+ ];
798
+ for (const prop of props) {
799
+ result[prop] = computed.getPropertyValue(prop);
800
+ }
801
+ return result;
802
+ }
803
+ getComputedStyles.__schema__ = {
804
+ name: "getComputedStyles",
805
+ description: "Get computed CSS styles for an element.",
806
+ parameters: {
807
+ type: "object",
808
+ properties: {
809
+ selector: {
810
+ type: "string",
811
+ description: "CSS selector of the element.",
812
+ },
813
+ properties: {
814
+ type: "array",
815
+ items: { type: "string" },
816
+ description: 'CSS property names to retrieve, e.g. ["color", "font-size"]. Omit for common defaults.',
817
+ },
818
+ },
819
+ required: ["selector"],
820
+ },
821
+ };
822
+ function getElementBounds(selector) {
823
+ const el = document.querySelector(selector);
824
+ if (!el) {
825
+ return { error: `No element found for selector: ${selector}` };
826
+ }
827
+ const rect = el.getBoundingClientRect();
828
+ return {
829
+ bounds: {
830
+ x: Math.round(rect.x),
831
+ y: Math.round(rect.y),
832
+ width: Math.round(rect.width),
833
+ height: Math.round(rect.height),
834
+ },
835
+ visible: rect.width > 0 &&
836
+ rect.height > 0 &&
837
+ getComputedStyle(el).visibility !== "hidden" &&
838
+ getComputedStyle(el).display !== "none",
839
+ };
840
+ }
841
+ getElementBounds.__schema__ = {
842
+ name: "getElementBounds",
843
+ description: "Get the bounding rectangle and visibility of a DOM element.",
844
+ parameters: {
845
+ type: "object",
846
+ properties: {
847
+ selector: {
848
+ type: "string",
849
+ description: "CSS selector of the element.",
850
+ },
851
+ },
852
+ required: ["selector"],
853
+ },
854
+ };
855
+
856
+ function resolveUrl(url, baseUrl) {
857
+ // url is absolute already
858
+ if (url.match(/^[a-z]+:\/\//i)) {
859
+ return url;
860
+ }
861
+ // url is absolute already, without protocol
862
+ if (url.match(/^\/\//)) {
863
+ return window.location.protocol + url;
864
+ }
865
+ // dataURI, mailto:, tel:, etc.
866
+ if (url.match(/^[a-z]+:/i)) {
867
+ return url;
868
+ }
869
+ const doc = document.implementation.createHTMLDocument();
870
+ const base = doc.createElement('base');
871
+ const a = doc.createElement('a');
872
+ doc.head.appendChild(base);
873
+ doc.body.appendChild(a);
874
+ if (baseUrl) {
875
+ base.href = baseUrl;
876
+ }
877
+ a.href = url;
878
+ return a.href;
879
+ }
880
+ const uuid = (() => {
881
+ // generate uuid for className of pseudo elements.
882
+ // We should not use GUIDs, otherwise pseudo elements sometimes cannot be captured.
883
+ let counter = 0;
884
+ // ref: http://stackoverflow.com/a/6248722/2519373
885
+ const random = () =>
886
+ // eslint-disable-next-line no-bitwise
887
+ `0000${((Math.random() * 36 ** 4) << 0).toString(36)}`.slice(-4);
888
+ return () => {
889
+ counter += 1;
890
+ return `u${random()}${counter}`;
891
+ };
892
+ })();
893
+ function toArray(arrayLike) {
894
+ const arr = [];
895
+ for (let i = 0, l = arrayLike.length; i < l; i++) {
896
+ arr.push(arrayLike[i]);
897
+ }
898
+ return arr;
899
+ }
900
+ let styleProps = null;
901
+ function getStyleProperties(options = {}) {
902
+ if (styleProps) {
903
+ return styleProps;
904
+ }
905
+ if (options.includeStyleProperties) {
906
+ styleProps = options.includeStyleProperties;
907
+ return styleProps;
908
+ }
909
+ styleProps = toArray(window.getComputedStyle(document.documentElement));
910
+ return styleProps;
911
+ }
912
+ function px(node, styleProperty) {
913
+ const win = node.ownerDocument.defaultView || window;
914
+ const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
915
+ return val ? parseFloat(val.replace('px', '')) : 0;
916
+ }
917
+ function getNodeWidth(node) {
918
+ const leftBorder = px(node, 'border-left-width');
919
+ const rightBorder = px(node, 'border-right-width');
920
+ return node.clientWidth + leftBorder + rightBorder;
921
+ }
922
+ function getNodeHeight(node) {
923
+ const topBorder = px(node, 'border-top-width');
924
+ const bottomBorder = px(node, 'border-bottom-width');
925
+ return node.clientHeight + topBorder + bottomBorder;
926
+ }
927
+ function getImageSize(targetNode, options = {}) {
928
+ const width = options.width || getNodeWidth(targetNode);
929
+ const height = options.height || getNodeHeight(targetNode);
930
+ return { width, height };
931
+ }
932
+ function getPixelRatio() {
933
+ let ratio;
934
+ let FINAL_PROCESS;
935
+ try {
936
+ FINAL_PROCESS = process;
937
+ }
938
+ catch (e) {
939
+ // pass
940
+ }
941
+ const val = FINAL_PROCESS && FINAL_PROCESS.env
942
+ ? FINAL_PROCESS.env.devicePixelRatio
943
+ : null;
944
+ if (val) {
945
+ ratio = parseInt(val, 10);
946
+ if (Number.isNaN(ratio)) {
947
+ ratio = 1;
948
+ }
949
+ }
950
+ return ratio || window.devicePixelRatio || 1;
951
+ }
952
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
953
+ const canvasDimensionLimit = 16384;
954
+ function checkCanvasDimensions(canvas) {
955
+ if (canvas.width > canvasDimensionLimit ||
956
+ canvas.height > canvasDimensionLimit) {
957
+ if (canvas.width > canvasDimensionLimit &&
958
+ canvas.height > canvasDimensionLimit) {
959
+ if (canvas.width > canvas.height) {
960
+ canvas.height *= canvasDimensionLimit / canvas.width;
961
+ canvas.width = canvasDimensionLimit;
962
+ }
963
+ else {
964
+ canvas.width *= canvasDimensionLimit / canvas.height;
965
+ canvas.height = canvasDimensionLimit;
966
+ }
967
+ }
968
+ else if (canvas.width > canvasDimensionLimit) {
969
+ canvas.height *= canvasDimensionLimit / canvas.width;
970
+ canvas.width = canvasDimensionLimit;
971
+ }
972
+ else {
973
+ canvas.width *= canvasDimensionLimit / canvas.height;
974
+ canvas.height = canvasDimensionLimit;
975
+ }
976
+ }
977
+ }
978
+ function createImage(url) {
979
+ return new Promise((resolve, reject) => {
980
+ const img = new Image();
981
+ img.onload = () => {
982
+ img.decode().then(() => {
983
+ requestAnimationFrame(() => resolve(img));
984
+ });
985
+ };
986
+ img.onerror = reject;
987
+ img.crossOrigin = 'anonymous';
988
+ img.decoding = 'async';
989
+ img.src = url;
990
+ });
991
+ }
992
+ async function svgToDataURL(svg) {
993
+ return Promise.resolve()
994
+ .then(() => new XMLSerializer().serializeToString(svg))
995
+ .then(encodeURIComponent)
996
+ .then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
997
+ }
998
+ async function nodeToDataURL(node, width, height) {
999
+ const xmlns = 'http://www.w3.org/2000/svg';
1000
+ const svg = document.createElementNS(xmlns, 'svg');
1001
+ const foreignObject = document.createElementNS(xmlns, 'foreignObject');
1002
+ svg.setAttribute('width', `${width}`);
1003
+ svg.setAttribute('height', `${height}`);
1004
+ svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
1005
+ foreignObject.setAttribute('width', '100%');
1006
+ foreignObject.setAttribute('height', '100%');
1007
+ foreignObject.setAttribute('x', '0');
1008
+ foreignObject.setAttribute('y', '0');
1009
+ foreignObject.setAttribute('externalResourcesRequired', 'true');
1010
+ svg.appendChild(foreignObject);
1011
+ foreignObject.appendChild(node);
1012
+ return svgToDataURL(svg);
1013
+ }
1014
+ const isInstanceOfElement = (node, instance) => {
1015
+ if (node instanceof instance)
1016
+ return true;
1017
+ const nodePrototype = Object.getPrototypeOf(node);
1018
+ if (nodePrototype === null)
1019
+ return false;
1020
+ return (nodePrototype.constructor.name === instance.name ||
1021
+ isInstanceOfElement(nodePrototype, instance));
1022
+ };
1023
+
1024
+ function formatCSSText(style) {
1025
+ const content = style.getPropertyValue('content');
1026
+ return `${style.cssText} content: '${content.replace(/'|"/g, '')}';`;
1027
+ }
1028
+ function formatCSSProperties(style, options) {
1029
+ return getStyleProperties(options)
1030
+ .map((name) => {
1031
+ const value = style.getPropertyValue(name);
1032
+ const priority = style.getPropertyPriority(name);
1033
+ return `${name}: ${value}${priority ? ' !important' : ''};`;
1034
+ })
1035
+ .join(' ');
1036
+ }
1037
+ function getPseudoElementStyle(className, pseudo, style, options) {
1038
+ const selector = `.${className}:${pseudo}`;
1039
+ const cssText = style.cssText
1040
+ ? formatCSSText(style)
1041
+ : formatCSSProperties(style, options);
1042
+ return document.createTextNode(`${selector}{${cssText}}`);
1043
+ }
1044
+ function clonePseudoElement(nativeNode, clonedNode, pseudo, options) {
1045
+ const style = window.getComputedStyle(nativeNode, pseudo);
1046
+ const content = style.getPropertyValue('content');
1047
+ if (content === '' || content === 'none') {
1048
+ return;
1049
+ }
1050
+ const className = uuid();
1051
+ try {
1052
+ clonedNode.className = `${clonedNode.className} ${className}`;
1053
+ }
1054
+ catch (err) {
1055
+ return;
1056
+ }
1057
+ const styleElement = document.createElement('style');
1058
+ styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options));
1059
+ clonedNode.appendChild(styleElement);
1060
+ }
1061
+ function clonePseudoElements(nativeNode, clonedNode, options) {
1062
+ clonePseudoElement(nativeNode, clonedNode, ':before', options);
1063
+ clonePseudoElement(nativeNode, clonedNode, ':after', options);
1064
+ }
1065
+
1066
+ const WOFF = 'application/font-woff';
1067
+ const JPEG = 'image/jpeg';
1068
+ const mimes = {
1069
+ woff: WOFF,
1070
+ woff2: WOFF,
1071
+ ttf: 'application/font-truetype',
1072
+ eot: 'application/vnd.ms-fontobject',
1073
+ png: 'image/png',
1074
+ jpg: JPEG,
1075
+ jpeg: JPEG,
1076
+ gif: 'image/gif',
1077
+ tiff: 'image/tiff',
1078
+ svg: 'image/svg+xml',
1079
+ webp: 'image/webp',
1080
+ };
1081
+ function getExtension(url) {
1082
+ const match = /\.([^./]*?)$/g.exec(url);
1083
+ return match ? match[1] : '';
1084
+ }
1085
+ function getMimeType(url) {
1086
+ const extension = getExtension(url).toLowerCase();
1087
+ return mimes[extension] || '';
1088
+ }
1089
+
1090
+ function getContentFromDataUrl(dataURL) {
1091
+ return dataURL.split(/,/)[1];
1092
+ }
1093
+ function isDataUrl(url) {
1094
+ return url.search(/^(data:)/) !== -1;
1095
+ }
1096
+ function makeDataUrl(content, mimeType) {
1097
+ return `data:${mimeType};base64,${content}`;
1098
+ }
1099
+ async function fetchAsDataURL(url, init, process) {
1100
+ const res = await fetch(url, init);
1101
+ if (res.status === 404) {
1102
+ throw new Error(`Resource "${res.url}" not found`);
1103
+ }
1104
+ const blob = await res.blob();
1105
+ return new Promise((resolve, reject) => {
1106
+ const reader = new FileReader();
1107
+ reader.onerror = reject;
1108
+ reader.onloadend = () => {
1109
+ try {
1110
+ resolve(process({ res, result: reader.result }));
1111
+ }
1112
+ catch (error) {
1113
+ reject(error);
1114
+ }
1115
+ };
1116
+ reader.readAsDataURL(blob);
1117
+ });
1118
+ }
1119
+ const cache = {};
1120
+ function getCacheKey(url, contentType, includeQueryParams) {
1121
+ let key = url.replace(/\?.*/, '');
1122
+ if (includeQueryParams) {
1123
+ key = url;
1124
+ }
1125
+ // font resource
1126
+ if (/ttf|otf|eot|woff2?/i.test(key)) {
1127
+ key = key.replace(/.*\//, '');
1128
+ }
1129
+ return contentType ? `[${contentType}]${key}` : key;
1130
+ }
1131
+ async function resourceToDataURL(resourceUrl, contentType, options) {
1132
+ const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);
1133
+ if (cache[cacheKey] != null) {
1134
+ return cache[cacheKey];
1135
+ }
1136
+ // ref: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
1137
+ if (options.cacheBust) {
1138
+ // eslint-disable-next-line no-param-reassign
1139
+ resourceUrl += (/\?/.test(resourceUrl) ? '&' : '?') + new Date().getTime();
1140
+ }
1141
+ let dataURL;
1142
+ try {
1143
+ const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {
1144
+ if (!contentType) {
1145
+ // eslint-disable-next-line no-param-reassign
1146
+ contentType = res.headers.get('Content-Type') || '';
1147
+ }
1148
+ return getContentFromDataUrl(result);
1149
+ });
1150
+ dataURL = makeDataUrl(content, contentType);
1151
+ }
1152
+ catch (error) {
1153
+ dataURL = options.imagePlaceholder || '';
1154
+ let msg = `Failed to fetch resource: ${resourceUrl}`;
1155
+ if (error) {
1156
+ msg = typeof error === 'string' ? error : error.message;
1157
+ }
1158
+ if (msg) {
1159
+ console.warn(msg);
1160
+ }
1161
+ }
1162
+ cache[cacheKey] = dataURL;
1163
+ return dataURL;
1164
+ }
1165
+
1166
+ async function cloneCanvasElement(canvas) {
1167
+ const dataURL = canvas.toDataURL();
1168
+ if (dataURL === 'data:,') {
1169
+ return canvas.cloneNode(false);
1170
+ }
1171
+ return createImage(dataURL);
1172
+ }
1173
+ async function cloneVideoElement(video, options) {
1174
+ if (video.currentSrc) {
1175
+ const canvas = document.createElement('canvas');
1176
+ const ctx = canvas.getContext('2d');
1177
+ canvas.width = video.clientWidth;
1178
+ canvas.height = video.clientHeight;
1179
+ ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
1180
+ const dataURL = canvas.toDataURL();
1181
+ return createImage(dataURL);
1182
+ }
1183
+ const poster = video.poster;
1184
+ const contentType = getMimeType(poster);
1185
+ const dataURL = await resourceToDataURL(poster, contentType, options);
1186
+ return createImage(dataURL);
1187
+ }
1188
+ async function cloneIFrameElement(iframe, options) {
1189
+ var _a;
1190
+ try {
1191
+ if ((_a = iframe === null || iframe === void 0 ? void 0 : iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.body) {
1192
+ return (await cloneNode(iframe.contentDocument.body, options, true));
1193
+ }
1194
+ }
1195
+ catch (_b) {
1196
+ // Failed to clone iframe
1197
+ }
1198
+ return iframe.cloneNode(false);
1199
+ }
1200
+ async function cloneSingleNode(node, options) {
1201
+ if (isInstanceOfElement(node, HTMLCanvasElement)) {
1202
+ return cloneCanvasElement(node);
1203
+ }
1204
+ if (isInstanceOfElement(node, HTMLVideoElement)) {
1205
+ return cloneVideoElement(node, options);
1206
+ }
1207
+ if (isInstanceOfElement(node, HTMLIFrameElement)) {
1208
+ return cloneIFrameElement(node, options);
1209
+ }
1210
+ return node.cloneNode(isSVGElement(node));
1211
+ }
1212
+ const isSlotElement = (node) => node.tagName != null && node.tagName.toUpperCase() === 'SLOT';
1213
+ const isSVGElement = (node) => node.tagName != null && node.tagName.toUpperCase() === 'SVG';
1214
+ async function cloneChildren(nativeNode, clonedNode, options) {
1215
+ var _a, _b;
1216
+ if (isSVGElement(clonedNode)) {
1217
+ return clonedNode;
1218
+ }
1219
+ let children = [];
1220
+ if (isSlotElement(nativeNode) && nativeNode.assignedNodes) {
1221
+ children = toArray(nativeNode.assignedNodes());
1222
+ }
1223
+ else if (isInstanceOfElement(nativeNode, HTMLIFrameElement) &&
1224
+ ((_a = nativeNode.contentDocument) === null || _a === void 0 ? void 0 : _a.body)) {
1225
+ children = toArray(nativeNode.contentDocument.body.childNodes);
1226
+ }
1227
+ else {
1228
+ children = toArray(((_b = nativeNode.shadowRoot) !== null && _b !== void 0 ? _b : nativeNode).childNodes);
1229
+ }
1230
+ if (children.length === 0 ||
1231
+ isInstanceOfElement(nativeNode, HTMLVideoElement)) {
1232
+ return clonedNode;
1233
+ }
1234
+ await children.reduce((deferred, child) => deferred
1235
+ .then(() => cloneNode(child, options))
1236
+ .then((clonedChild) => {
1237
+ if (clonedChild) {
1238
+ clonedNode.appendChild(clonedChild);
1239
+ }
1240
+ }), Promise.resolve());
1241
+ return clonedNode;
1242
+ }
1243
+ function cloneCSSStyle(nativeNode, clonedNode, options) {
1244
+ const targetStyle = clonedNode.style;
1245
+ if (!targetStyle) {
1246
+ return;
1247
+ }
1248
+ const sourceStyle = window.getComputedStyle(nativeNode);
1249
+ if (sourceStyle.cssText) {
1250
+ targetStyle.cssText = sourceStyle.cssText;
1251
+ targetStyle.transformOrigin = sourceStyle.transformOrigin;
1252
+ }
1253
+ else {
1254
+ getStyleProperties(options).forEach((name) => {
1255
+ let value = sourceStyle.getPropertyValue(name);
1256
+ if (name === 'font-size' && value.endsWith('px')) {
1257
+ const reducedFont = Math.floor(parseFloat(value.substring(0, value.length - 2))) - 0.1;
1258
+ value = `${reducedFont}px`;
1259
+ }
1260
+ if (isInstanceOfElement(nativeNode, HTMLIFrameElement) &&
1261
+ name === 'display' &&
1262
+ value === 'inline') {
1263
+ value = 'block';
1264
+ }
1265
+ if (name === 'd' && clonedNode.getAttribute('d')) {
1266
+ value = `path(${clonedNode.getAttribute('d')})`;
1267
+ }
1268
+ targetStyle.setProperty(name, value, sourceStyle.getPropertyPriority(name));
1269
+ });
1270
+ }
1271
+ }
1272
+ function cloneInputValue(nativeNode, clonedNode) {
1273
+ if (isInstanceOfElement(nativeNode, HTMLTextAreaElement)) {
1274
+ clonedNode.innerHTML = nativeNode.value;
1275
+ }
1276
+ if (isInstanceOfElement(nativeNode, HTMLInputElement)) {
1277
+ clonedNode.setAttribute('value', nativeNode.value);
1278
+ }
1279
+ }
1280
+ function cloneSelectValue(nativeNode, clonedNode) {
1281
+ if (isInstanceOfElement(nativeNode, HTMLSelectElement)) {
1282
+ const clonedSelect = clonedNode;
1283
+ const selectedOption = Array.from(clonedSelect.children).find((child) => nativeNode.value === child.getAttribute('value'));
1284
+ if (selectedOption) {
1285
+ selectedOption.setAttribute('selected', '');
1286
+ }
1287
+ }
1288
+ }
1289
+ function decorate(nativeNode, clonedNode, options) {
1290
+ if (isInstanceOfElement(clonedNode, Element)) {
1291
+ cloneCSSStyle(nativeNode, clonedNode, options);
1292
+ clonePseudoElements(nativeNode, clonedNode, options);
1293
+ cloneInputValue(nativeNode, clonedNode);
1294
+ cloneSelectValue(nativeNode, clonedNode);
1295
+ }
1296
+ return clonedNode;
1297
+ }
1298
+ async function ensureSVGSymbols(clone, options) {
1299
+ const uses = clone.querySelectorAll ? clone.querySelectorAll('use') : [];
1300
+ if (uses.length === 0) {
1301
+ return clone;
1302
+ }
1303
+ const processedDefs = {};
1304
+ for (let i = 0; i < uses.length; i++) {
1305
+ const use = uses[i];
1306
+ const id = use.getAttribute('xlink:href');
1307
+ if (id) {
1308
+ const exist = clone.querySelector(id);
1309
+ const definition = document.querySelector(id);
1310
+ if (!exist && definition && !processedDefs[id]) {
1311
+ // eslint-disable-next-line no-await-in-loop
1312
+ processedDefs[id] = (await cloneNode(definition, options, true));
1313
+ }
1314
+ }
1315
+ }
1316
+ const nodes = Object.values(processedDefs);
1317
+ if (nodes.length) {
1318
+ const ns = 'http://www.w3.org/1999/xhtml';
1319
+ const svg = document.createElementNS(ns, 'svg');
1320
+ svg.setAttribute('xmlns', ns);
1321
+ svg.style.position = 'absolute';
1322
+ svg.style.width = '0';
1323
+ svg.style.height = '0';
1324
+ svg.style.overflow = 'hidden';
1325
+ svg.style.display = 'none';
1326
+ const defs = document.createElementNS(ns, 'defs');
1327
+ svg.appendChild(defs);
1328
+ for (let i = 0; i < nodes.length; i++) {
1329
+ defs.appendChild(nodes[i]);
1330
+ }
1331
+ clone.appendChild(svg);
1332
+ }
1333
+ return clone;
1334
+ }
1335
+ async function cloneNode(node, options, isRoot) {
1336
+ if (!isRoot && options.filter && !options.filter(node)) {
1337
+ return null;
1338
+ }
1339
+ return Promise.resolve(node)
1340
+ .then((clonedNode) => cloneSingleNode(clonedNode, options))
1341
+ .then((clonedNode) => cloneChildren(node, clonedNode, options))
1342
+ .then((clonedNode) => decorate(node, clonedNode, options))
1343
+ .then((clonedNode) => ensureSVGSymbols(clonedNode, options));
1344
+ }
1345
+
1346
+ const URL_REGEX = /url\((['"]?)([^'"]+?)\1\)/g;
1347
+ const URL_WITH_FORMAT_REGEX = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g;
1348
+ const FONT_SRC_REGEX = /src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g;
1349
+ function toRegex(url) {
1350
+ // eslint-disable-next-line no-useless-escape
1351
+ const escaped = url.replace(/([.*+?^${}()|\[\]\/\\])/g, '\\$1');
1352
+ return new RegExp(`(url\\(['"]?)(${escaped})(['"]?\\))`, 'g');
1353
+ }
1354
+ function parseURLs(cssText) {
1355
+ const urls = [];
1356
+ cssText.replace(URL_REGEX, (raw, quotation, url) => {
1357
+ urls.push(url);
1358
+ return raw;
1359
+ });
1360
+ return urls.filter((url) => !isDataUrl(url));
1361
+ }
1362
+ async function embed(cssText, resourceURL, baseURL, options, getContentFromUrl) {
1363
+ try {
1364
+ const resolvedURL = baseURL ? resolveUrl(resourceURL, baseURL) : resourceURL;
1365
+ const contentType = getMimeType(resourceURL);
1366
+ let dataURL;
1367
+ if (getContentFromUrl) ;
1368
+ else {
1369
+ dataURL = await resourceToDataURL(resolvedURL, contentType, options);
1370
+ }
1371
+ return cssText.replace(toRegex(resourceURL), `$1${dataURL}$3`);
1372
+ }
1373
+ catch (error) {
1374
+ // pass
1375
+ }
1376
+ return cssText;
1377
+ }
1378
+ function filterPreferredFontFormat(str, { preferredFontFormat }) {
1379
+ return !preferredFontFormat
1380
+ ? str
1381
+ : str.replace(FONT_SRC_REGEX, (match) => {
1382
+ // eslint-disable-next-line no-constant-condition
1383
+ while (true) {
1384
+ const [src, , format] = URL_WITH_FORMAT_REGEX.exec(match) || [];
1385
+ if (!format) {
1386
+ return '';
1387
+ }
1388
+ if (format === preferredFontFormat) {
1389
+ return `src: ${src};`;
1390
+ }
1391
+ }
1392
+ });
1393
+ }
1394
+ function shouldEmbed(url) {
1395
+ return url.search(URL_REGEX) !== -1;
1396
+ }
1397
+ async function embedResources(cssText, baseUrl, options) {
1398
+ if (!shouldEmbed(cssText)) {
1399
+ return cssText;
1400
+ }
1401
+ const filteredCSSText = filterPreferredFontFormat(cssText, options);
1402
+ const urls = parseURLs(filteredCSSText);
1403
+ return urls.reduce((deferred, url) => deferred.then((css) => embed(css, url, baseUrl, options)), Promise.resolve(filteredCSSText));
1404
+ }
1405
+
1406
+ async function embedProp(propName, node, options) {
1407
+ var _a;
1408
+ const propValue = (_a = node.style) === null || _a === void 0 ? void 0 : _a.getPropertyValue(propName);
1409
+ if (propValue) {
1410
+ const cssString = await embedResources(propValue, null, options);
1411
+ node.style.setProperty(propName, cssString, node.style.getPropertyPriority(propName));
1412
+ return true;
1413
+ }
1414
+ return false;
1415
+ }
1416
+ async function embedBackground(clonedNode, options) {
1417
+ (await embedProp('background', clonedNode, options)) ||
1418
+ (await embedProp('background-image', clonedNode, options));
1419
+ (await embedProp('mask', clonedNode, options)) ||
1420
+ (await embedProp('-webkit-mask', clonedNode, options)) ||
1421
+ (await embedProp('mask-image', clonedNode, options)) ||
1422
+ (await embedProp('-webkit-mask-image', clonedNode, options));
1423
+ }
1424
+ async function embedImageNode(clonedNode, options) {
1425
+ const isImageElement = isInstanceOfElement(clonedNode, HTMLImageElement);
1426
+ if (!(isImageElement && !isDataUrl(clonedNode.src)) &&
1427
+ !(isInstanceOfElement(clonedNode, SVGImageElement) &&
1428
+ !isDataUrl(clonedNode.href.baseVal))) {
1429
+ return;
1430
+ }
1431
+ const url = isImageElement ? clonedNode.src : clonedNode.href.baseVal;
1432
+ const dataURL = await resourceToDataURL(url, getMimeType(url), options);
1433
+ await new Promise((resolve, reject) => {
1434
+ clonedNode.onload = resolve;
1435
+ clonedNode.onerror = options.onImageErrorHandler
1436
+ ? (...attributes) => {
1437
+ try {
1438
+ resolve(options.onImageErrorHandler(...attributes));
1439
+ }
1440
+ catch (error) {
1441
+ reject(error);
1442
+ }
1443
+ }
1444
+ : reject;
1445
+ const image = clonedNode;
1446
+ if (image.decode) {
1447
+ image.decode = resolve;
1448
+ }
1449
+ if (image.loading === 'lazy') {
1450
+ image.loading = 'eager';
1451
+ }
1452
+ if (isImageElement) {
1453
+ clonedNode.srcset = '';
1454
+ clonedNode.src = dataURL;
1455
+ }
1456
+ else {
1457
+ clonedNode.href.baseVal = dataURL;
1458
+ }
1459
+ });
1460
+ }
1461
+ async function embedChildren(clonedNode, options) {
1462
+ const children = toArray(clonedNode.childNodes);
1463
+ const deferreds = children.map((child) => embedImages(child, options));
1464
+ await Promise.all(deferreds).then(() => clonedNode);
1465
+ }
1466
+ async function embedImages(clonedNode, options) {
1467
+ if (isInstanceOfElement(clonedNode, Element)) {
1468
+ await embedBackground(clonedNode, options);
1469
+ await embedImageNode(clonedNode, options);
1470
+ await embedChildren(clonedNode, options);
1471
+ }
1472
+ }
1473
+
1474
+ function applyStyle(node, options) {
1475
+ const { style } = node;
1476
+ if (options.backgroundColor) {
1477
+ style.backgroundColor = options.backgroundColor;
1478
+ }
1479
+ if (options.width) {
1480
+ style.width = `${options.width}px`;
1481
+ }
1482
+ if (options.height) {
1483
+ style.height = `${options.height}px`;
1484
+ }
1485
+ const manual = options.style;
1486
+ if (manual != null) {
1487
+ Object.keys(manual).forEach((key) => {
1488
+ style[key] = manual[key];
1489
+ });
1490
+ }
1491
+ return node;
1492
+ }
1493
+
1494
+ const cssFetchCache = {};
1495
+ async function fetchCSS(url) {
1496
+ let cache = cssFetchCache[url];
1497
+ if (cache != null) {
1498
+ return cache;
1499
+ }
1500
+ const res = await fetch(url);
1501
+ const cssText = await res.text();
1502
+ cache = { url, cssText };
1503
+ cssFetchCache[url] = cache;
1504
+ return cache;
1505
+ }
1506
+ async function embedFonts(data, options) {
1507
+ let cssText = data.cssText;
1508
+ const regexUrl = /url\(["']?([^"')]+)["']?\)/g;
1509
+ const fontLocs = cssText.match(/url\([^)]+\)/g) || [];
1510
+ const loadFonts = fontLocs.map(async (loc) => {
1511
+ let url = loc.replace(regexUrl, '$1');
1512
+ if (!url.startsWith('https://')) {
1513
+ url = new URL(url, data.url).href;
1514
+ }
1515
+ return fetchAsDataURL(url, options.fetchRequestInit, ({ result }) => {
1516
+ cssText = cssText.replace(loc, `url(${result})`);
1517
+ return [loc, result];
1518
+ });
1519
+ });
1520
+ return Promise.all(loadFonts).then(() => cssText);
1521
+ }
1522
+ function parseCSS(source) {
1523
+ if (source == null) {
1524
+ return [];
1525
+ }
1526
+ const result = [];
1527
+ const commentsRegex = /(\/\*[\s\S]*?\*\/)/gi;
1528
+ // strip out comments
1529
+ let cssText = source.replace(commentsRegex, '');
1530
+ // eslint-disable-next-line prefer-regex-literals
1531
+ const keyframesRegex = new RegExp('((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})', 'gi');
1532
+ // eslint-disable-next-line no-constant-condition
1533
+ while (true) {
1534
+ const matches = keyframesRegex.exec(cssText);
1535
+ if (matches === null) {
1536
+ break;
1537
+ }
1538
+ result.push(matches[0]);
1539
+ }
1540
+ cssText = cssText.replace(keyframesRegex, '');
1541
+ const importRegex = /@import[\s\S]*?url\([^)]*\)[\s\S]*?;/gi;
1542
+ // to match css & media queries together
1543
+ const combinedCSSRegex = '((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]' +
1544
+ '*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})';
1545
+ // unified regex
1546
+ const unifiedRegex = new RegExp(combinedCSSRegex, 'gi');
1547
+ // eslint-disable-next-line no-constant-condition
1548
+ while (true) {
1549
+ let matches = importRegex.exec(cssText);
1550
+ if (matches === null) {
1551
+ matches = unifiedRegex.exec(cssText);
1552
+ if (matches === null) {
1553
+ break;
1554
+ }
1555
+ else {
1556
+ importRegex.lastIndex = unifiedRegex.lastIndex;
1557
+ }
1558
+ }
1559
+ else {
1560
+ unifiedRegex.lastIndex = importRegex.lastIndex;
1561
+ }
1562
+ result.push(matches[0]);
1563
+ }
1564
+ return result;
1565
+ }
1566
+ async function getCSSRules(styleSheets, options) {
1567
+ const ret = [];
1568
+ const deferreds = [];
1569
+ // First loop inlines imports
1570
+ styleSheets.forEach((sheet) => {
1571
+ if ('cssRules' in sheet) {
1572
+ try {
1573
+ toArray(sheet.cssRules || []).forEach((item, index) => {
1574
+ if (item.type === CSSRule.IMPORT_RULE) {
1575
+ let importIndex = index + 1;
1576
+ const url = item.href;
1577
+ const deferred = fetchCSS(url)
1578
+ .then((metadata) => embedFonts(metadata, options))
1579
+ .then((cssText) => parseCSS(cssText).forEach((rule) => {
1580
+ try {
1581
+ sheet.insertRule(rule, rule.startsWith('@import')
1582
+ ? (importIndex += 1)
1583
+ : sheet.cssRules.length);
1584
+ }
1585
+ catch (error) {
1586
+ console.error('Error inserting rule from remote css', {
1587
+ rule,
1588
+ error,
1589
+ });
1590
+ }
1591
+ }))
1592
+ .catch((e) => {
1593
+ console.error('Error loading remote css', e.toString());
1594
+ });
1595
+ deferreds.push(deferred);
1596
+ }
1597
+ });
1598
+ }
1599
+ catch (e) {
1600
+ const inline = styleSheets.find((a) => a.href == null) || document.styleSheets[0];
1601
+ if (sheet.href != null) {
1602
+ deferreds.push(fetchCSS(sheet.href)
1603
+ .then((metadata) => embedFonts(metadata, options))
1604
+ .then((cssText) => parseCSS(cssText).forEach((rule) => {
1605
+ inline.insertRule(rule, inline.cssRules.length);
1606
+ }))
1607
+ .catch((err) => {
1608
+ console.error('Error loading remote stylesheet', err);
1609
+ }));
1610
+ }
1611
+ console.error('Error inlining remote css file', e);
1612
+ }
1613
+ }
1614
+ });
1615
+ return Promise.all(deferreds).then(() => {
1616
+ // Second loop parses rules
1617
+ styleSheets.forEach((sheet) => {
1618
+ if ('cssRules' in sheet) {
1619
+ try {
1620
+ toArray(sheet.cssRules || []).forEach((item) => {
1621
+ ret.push(item);
1622
+ });
1623
+ }
1624
+ catch (e) {
1625
+ console.error(`Error while reading CSS rules from ${sheet.href}`, e);
1626
+ }
1627
+ }
1628
+ });
1629
+ return ret;
1630
+ });
1631
+ }
1632
+ function getWebFontRules(cssRules) {
1633
+ return cssRules
1634
+ .filter((rule) => rule.type === CSSRule.FONT_FACE_RULE)
1635
+ .filter((rule) => shouldEmbed(rule.style.getPropertyValue('src')));
1636
+ }
1637
+ async function parseWebFontRules(node, options) {
1638
+ if (node.ownerDocument == null) {
1639
+ throw new Error('Provided element is not within a Document');
1640
+ }
1641
+ const styleSheets = toArray(node.ownerDocument.styleSheets);
1642
+ const cssRules = await getCSSRules(styleSheets, options);
1643
+ return getWebFontRules(cssRules);
1644
+ }
1645
+ function normalizeFontFamily(font) {
1646
+ return font.trim().replace(/["']/g, '');
1647
+ }
1648
+ function getUsedFonts(node) {
1649
+ const fonts = new Set();
1650
+ function traverse(node) {
1651
+ const fontFamily = node.style.fontFamily || getComputedStyle(node).fontFamily;
1652
+ fontFamily.split(',').forEach((font) => {
1653
+ fonts.add(normalizeFontFamily(font));
1654
+ });
1655
+ Array.from(node.children).forEach((child) => {
1656
+ if (child instanceof HTMLElement) {
1657
+ traverse(child);
1658
+ }
1659
+ });
1660
+ }
1661
+ traverse(node);
1662
+ return fonts;
1663
+ }
1664
+ async function getWebFontCSS(node, options) {
1665
+ const rules = await parseWebFontRules(node, options);
1666
+ const usedFonts = getUsedFonts(node);
1667
+ const cssTexts = await Promise.all(rules
1668
+ .filter((rule) => usedFonts.has(normalizeFontFamily(rule.style.fontFamily)))
1669
+ .map((rule) => {
1670
+ const baseUrl = rule.parentStyleSheet
1671
+ ? rule.parentStyleSheet.href
1672
+ : null;
1673
+ return embedResources(rule.cssText, baseUrl, options);
1674
+ }));
1675
+ return cssTexts.join('\n');
1676
+ }
1677
+ async function embedWebFonts(clonedNode, options) {
1678
+ const cssText = options.fontEmbedCSS != null
1679
+ ? options.fontEmbedCSS
1680
+ : options.skipFonts
1681
+ ? null
1682
+ : await getWebFontCSS(clonedNode, options);
1683
+ if (cssText) {
1684
+ const styleNode = document.createElement('style');
1685
+ const sytleContent = document.createTextNode(cssText);
1686
+ styleNode.appendChild(sytleContent);
1687
+ if (clonedNode.firstChild) {
1688
+ clonedNode.insertBefore(styleNode, clonedNode.firstChild);
1689
+ }
1690
+ else {
1691
+ clonedNode.appendChild(styleNode);
1692
+ }
1693
+ }
1694
+ }
1695
+
1696
+ async function toSvg(node, options = {}) {
1697
+ const { width, height } = getImageSize(node, options);
1698
+ const clonedNode = (await cloneNode(node, options, true));
1699
+ await embedWebFonts(clonedNode, options);
1700
+ await embedImages(clonedNode, options);
1701
+ applyStyle(clonedNode, options);
1702
+ const datauri = await nodeToDataURL(clonedNode, width, height);
1703
+ return datauri;
1704
+ }
1705
+ async function toCanvas(node, options = {}) {
1706
+ const { width, height } = getImageSize(node, options);
1707
+ const svg = await toSvg(node, options);
1708
+ const img = await createImage(svg);
1709
+ const canvas = document.createElement('canvas');
1710
+ const context = canvas.getContext('2d');
1711
+ const ratio = options.pixelRatio || getPixelRatio();
1712
+ const canvasWidth = options.canvasWidth || width;
1713
+ const canvasHeight = options.canvasHeight || height;
1714
+ canvas.width = canvasWidth * ratio;
1715
+ canvas.height = canvasHeight * ratio;
1716
+ if (!options.skipAutoScale) {
1717
+ checkCanvasDimensions(canvas);
1718
+ }
1719
+ canvas.style.width = `${canvasWidth}`;
1720
+ canvas.style.height = `${canvasHeight}`;
1721
+ if (options.backgroundColor) {
1722
+ context.fillStyle = options.backgroundColor;
1723
+ context.fillRect(0, 0, canvas.width, canvas.height);
1724
+ }
1725
+ context.drawImage(img, 0, 0, canvas.width, canvas.height);
1726
+ return canvas;
1727
+ }
1728
+ async function toPng(node, options = {}) {
1729
+ const canvas = await toCanvas(node, options);
1730
+ return canvas.toDataURL();
1731
+ }
1732
+ async function toJpeg(node, options = {}) {
1733
+ const canvas = await toCanvas(node, options);
1734
+ return canvas.toDataURL('image/jpeg', options.quality || 1);
1735
+ }
1736
+
1737
+ /**
1738
+ * Screenshot capture service using html-to-image.
1739
+ */
1740
+ async function takeScreenshot(options) {
1741
+ const selector = options?.selector;
1742
+ const format = options?.format ?? "png";
1743
+ const quality = options?.quality ?? 0.92;
1744
+ const scale = options?.scale ?? 1;
1745
+ const target = selector ? document.querySelector(selector) : document.body;
1746
+ if (!target) {
1747
+ return { error: `No element found for selector: ${selector}` };
1748
+ }
1749
+ try {
1750
+ const node = target;
1751
+ const captureOptions = {
1752
+ quality,
1753
+ pixelRatio: scale,
1754
+ cacheBust: true,
1755
+ skipAutoScale: true,
1756
+ // Filter out the debugger overlay itself
1757
+ filter: (el) => {
1758
+ return el.id !== "hypha-debugger-host";
1759
+ },
1760
+ };
1761
+ let dataUrl;
1762
+ if (format === "jpeg") {
1763
+ dataUrl = await toJpeg(node, captureOptions);
1764
+ }
1765
+ else {
1766
+ dataUrl = await toPng(node, captureOptions);
1767
+ }
1768
+ // Get dimensions
1769
+ const rect = node.getBoundingClientRect();
1770
+ let width = Math.round(rect.width * scale);
1771
+ let height = Math.round(rect.height * scale);
1772
+ // Optionally resize if too large
1773
+ const maxW = options?.max_width;
1774
+ const maxH = options?.max_height;
1775
+ if (maxW && width > maxW) {
1776
+ const ratio = maxW / width;
1777
+ width = maxW;
1778
+ height = Math.round(height * ratio);
1779
+ }
1780
+ if (maxH && height > maxH) {
1781
+ const ratio = maxH / height;
1782
+ height = maxH;
1783
+ width = Math.round(width * ratio);
1784
+ }
1785
+ return {
1786
+ data: dataUrl,
1787
+ format,
1788
+ width,
1789
+ height,
1790
+ };
1791
+ }
1792
+ catch (err) {
1793
+ return { error: `Screenshot failed: ${err.message ?? err}` };
1794
+ }
1795
+ }
1796
+ takeScreenshot.__schema__ = {
1797
+ name: "takeScreenshot",
1798
+ description: "Capture a screenshot of the entire page or a specific element. Returns a base64-encoded data URL.",
1799
+ parameters: {
1800
+ type: "object",
1801
+ properties: {
1802
+ selector: {
1803
+ type: "string",
1804
+ description: "CSS selector of the element to capture. Omit to capture the entire page body.",
1805
+ },
1806
+ format: {
1807
+ type: "string",
1808
+ enum: ["png", "jpeg"],
1809
+ description: 'Image format. Default: "png".',
1810
+ },
1811
+ quality: {
1812
+ type: "number",
1813
+ description: "Image quality (0-1) for JPEG. Default: 0.92.",
1814
+ },
1815
+ scale: {
1816
+ type: "number",
1817
+ description: "Pixel ratio / scale factor. Default: 1. Use 2 for retina.",
1818
+ },
1819
+ max_width: {
1820
+ type: "number",
1821
+ description: "Maximum width in pixels. Image will be scaled down if larger.",
1822
+ },
1823
+ max_height: {
1824
+ type: "number",
1825
+ description: "Maximum height in pixels. Image will be scaled down if larger.",
1826
+ },
1827
+ },
1828
+ },
1829
+ };
1830
+
1831
+ /**
1832
+ * Arbitrary JavaScript execution service.
1833
+ */
1834
+ async function executeScript(code, options) {
1835
+ const timeoutMs = options?.timeout_ms ?? 10000;
1836
+ try {
1837
+ const result = await Promise.race([
1838
+ // Use async Function to allow top-level await in the code
1839
+ new Function("return (async () => {" + code + "})()")(),
1840
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Execution timed out")), timeoutMs)),
1841
+ ]);
1842
+ // Serialize the result safely
1843
+ let serialized;
1844
+ let type = typeof result;
1845
+ try {
1846
+ if (result === undefined) {
1847
+ serialized = null;
1848
+ type = "undefined";
1849
+ }
1850
+ else if (result instanceof HTMLElement) {
1851
+ serialized = {
1852
+ tag: result.tagName.toLowerCase(),
1853
+ id: result.id,
1854
+ text: (result.textContent ?? "").trim().slice(0, 500),
1855
+ };
1856
+ type = "HTMLElement";
1857
+ }
1858
+ else if (result instanceof NodeList || result instanceof HTMLCollection) {
1859
+ serialized = Array.from(result).map((el) => ({
1860
+ tag: el.tagName?.toLowerCase(),
1861
+ id: el.id,
1862
+ text: (el.textContent ?? "").trim().slice(0, 200),
1863
+ }));
1864
+ type = "NodeList";
1865
+ }
1866
+ else {
1867
+ // Try JSON serialization, fall back to string
1868
+ serialized = JSON.parse(JSON.stringify(result));
1869
+ }
1870
+ }
1871
+ catch {
1872
+ serialized = String(result);
1873
+ type = "string (serialized)";
1874
+ }
1875
+ return { result: serialized, type };
1876
+ }
1877
+ catch (err) {
1878
+ return { error: `Execution error: ${err.message ?? err}` };
1879
+ }
1880
+ }
1881
+ executeScript.__schema__ = {
1882
+ name: "executeScript",
1883
+ description: "Execute arbitrary JavaScript code in the page context. Supports async/await. Returns the result of the last expression.",
1884
+ parameters: {
1885
+ type: "object",
1886
+ properties: {
1887
+ code: {
1888
+ type: "string",
1889
+ description: 'JavaScript code to execute. The result of the last expression is returned. Example: "return document.title"',
1890
+ },
1891
+ timeout_ms: {
1892
+ type: "number",
1893
+ description: "Maximum execution time in milliseconds. Default: 10000.",
1894
+ },
1895
+ },
1896
+ required: ["code"],
1897
+ },
1898
+ };
1899
+
1900
+ /**
1901
+ * Navigation service.
1902
+ */
1903
+ function navigate(url) {
1904
+ try {
1905
+ window.location.href = url;
1906
+ return { success: true, message: `Navigating to ${url}` };
1907
+ }
1908
+ catch (err) {
1909
+ return { success: false, message: `Navigation failed: ${err.message ?? err}` };
1910
+ }
1911
+ }
1912
+ navigate.__schema__ = {
1913
+ name: "navigate",
1914
+ description: "Navigate the browser to a new URL.",
1915
+ parameters: {
1916
+ type: "object",
1917
+ properties: {
1918
+ url: {
1919
+ type: "string",
1920
+ description: "The URL to navigate to.",
1921
+ },
1922
+ },
1923
+ required: ["url"],
1924
+ },
1925
+ };
1926
+ function goBack() {
1927
+ try {
1928
+ window.history.back();
1929
+ return { success: true, message: "Navigated back" };
1930
+ }
1931
+ catch (err) {
1932
+ return { success: false, message: `Back navigation failed: ${err.message ?? err}` };
1933
+ }
1934
+ }
1935
+ goBack.__schema__ = {
1936
+ name: "goBack",
1937
+ description: "Navigate back in browser history.",
1938
+ parameters: {
1939
+ type: "object",
1940
+ properties: {},
1941
+ },
1942
+ };
1943
+ function goForward() {
1944
+ try {
1945
+ window.history.forward();
1946
+ return { success: true, message: "Navigated forward" };
1947
+ }
1948
+ catch (err) {
1949
+ return {
1950
+ success: false,
1951
+ message: `Forward navigation failed: ${err.message ?? err}`,
1952
+ };
1953
+ }
1954
+ }
1955
+ goForward.__schema__ = {
1956
+ name: "goForward",
1957
+ description: "Navigate forward in browser history.",
1958
+ parameters: {
1959
+ type: "object",
1960
+ properties: {},
1961
+ },
1962
+ };
1963
+ function reload() {
1964
+ try {
1965
+ window.location.reload();
1966
+ return { success: true, message: "Reloading page" };
1967
+ }
1968
+ catch (err) {
1969
+ return { success: false, message: `Reload failed: ${err.message ?? err}` };
1970
+ }
1971
+ }
1972
+ reload.__schema__ = {
1973
+ name: "reload",
1974
+ description: "Reload the current page.",
1975
+ parameters: {
1976
+ type: "object",
1977
+ properties: {},
1978
+ },
1979
+ };
1980
+
1981
+ /**
1982
+ * React component tree inspection service.
1983
+ */
1984
+ function getFiberFromDOM(node) {
1985
+ const keys = Object.keys(node);
1986
+ const fiberKey = keys.find((k) => k.startsWith("__reactFiber$") ||
1987
+ k.startsWith("__reactInternalInstance$"));
1988
+ return fiberKey ? node[fiberKey] : null;
1989
+ }
1990
+ function getComponentName(fiber) {
1991
+ const { type } = fiber;
1992
+ if (!type)
1993
+ return "(unknown)";
1994
+ if (typeof type === "string")
1995
+ return type; // DOM element
1996
+ if (typeof type === "function") {
1997
+ return type.displayName || type.name || "Anonymous";
1998
+ }
1999
+ if (typeof type === "object") {
2000
+ if (type.displayName)
2001
+ return type.displayName;
2002
+ if (type.render)
2003
+ return type.render.displayName || type.render.name || "ForwardRef";
2004
+ if (type.type)
2005
+ return getComponentName({ type: type.type });
2006
+ // React.memo
2007
+ if (type.$$typeof?.toString() === "Symbol(react.memo)") {
2008
+ return `Memo(${getComponentName({ type: type.type })})`;
2009
+ }
2010
+ }
2011
+ return "(unknown)";
2012
+ }
2013
+ function getFiberType(fiber) {
2014
+ // Fiber tag values: 0=FunctionComponent, 1=ClassComponent, 5=HostComponent
2015
+ if (fiber.tag === 0 || fiber.tag === 11 || fiber.tag === 14 || fiber.tag === 15)
2016
+ return "function";
2017
+ if (fiber.tag === 1)
2018
+ return "class";
2019
+ if (fiber.tag === 5 || fiber.tag === 6)
2020
+ return "host";
2021
+ return "other";
2022
+ }
2023
+ function safeSerialize(obj, depth = 0, maxDepth = 2) {
2024
+ if (depth > maxDepth)
2025
+ return "[max depth]";
2026
+ if (obj === null || obj === undefined)
2027
+ return obj;
2028
+ if (typeof obj === "function")
2029
+ return `[Function: ${obj.name || "anonymous"}]`;
2030
+ if (typeof obj !== "object")
2031
+ return obj;
2032
+ if (obj instanceof HTMLElement)
2033
+ return `[${obj.tagName.toLowerCase()}#${obj.id}]`;
2034
+ if (Array.isArray(obj)) {
2035
+ return obj.slice(0, 10).map((v) => safeSerialize(v, depth + 1, maxDepth));
2036
+ }
2037
+ const result = {};
2038
+ const keys = Object.keys(obj).slice(0, 20);
2039
+ for (const key of keys) {
2040
+ if (key.startsWith("_") || key.startsWith("$$"))
2041
+ continue;
2042
+ try {
2043
+ result[key] = safeSerialize(obj[key], depth + 1, maxDepth);
2044
+ }
2045
+ catch {
2046
+ result[key] = "[unserializable]";
2047
+ }
2048
+ }
2049
+ return result;
2050
+ }
2051
+ function extractState(fiber) {
2052
+ if (fiber.tag === 1 && fiber.stateNode) {
2053
+ // Class component
2054
+ return safeSerialize(fiber.stateNode.state);
2055
+ }
2056
+ if (fiber.tag === 0 || fiber.tag === 11 || fiber.tag === 15) {
2057
+ // Function component - hooks linked list
2058
+ const states = [];
2059
+ let hook = fiber.memoizedState;
2060
+ let i = 0;
2061
+ while (hook && i < 20) {
2062
+ if (hook.queue !== null && hook.queue !== undefined) {
2063
+ states.push(safeSerialize(hook.memoizedState));
2064
+ }
2065
+ hook = hook.next;
2066
+ i++;
2067
+ }
2068
+ return states.length > 0 ? states : null;
2069
+ }
2070
+ return null;
2071
+ }
2072
+ function fiberToInfo(fiber, depth, maxDepth) {
2073
+ const fiberType = getFiberType(fiber);
2074
+ // Skip host elements unless at the top level
2075
+ const isComponent = fiberType === "function" || fiberType === "class";
2076
+ const info = {
2077
+ name: getComponentName(fiber),
2078
+ type: fiberType,
2079
+ props: isComponent ? safeSerialize(fiber.memoizedProps) : {},
2080
+ state: isComponent ? extractState(fiber) : null,
2081
+ key: fiber.key,
2082
+ children: [],
2083
+ };
2084
+ if (depth < maxDepth) {
2085
+ let child = fiber.child;
2086
+ while (child) {
2087
+ const childInfo = fiberToInfo(child, depth + 1, maxDepth);
2088
+ if (childInfo) {
2089
+ // For host elements, only include if they have component children
2090
+ if (childInfo.type !== "host" || childInfo.children.length > 0) {
2091
+ info.children.push(childInfo);
2092
+ }
2093
+ }
2094
+ child = child.sibling;
2095
+ }
2096
+ }
2097
+ return info;
2098
+ }
2099
+ function getReactTree(options) {
2100
+ const selector = options?.selector ?? "#root";
2101
+ const maxDepth = options?.max_depth ?? 5;
2102
+ const rootEl = document.querySelector(selector);
2103
+ if (!rootEl) {
2104
+ return { error: `No element found for selector: ${selector}` };
2105
+ }
2106
+ const fiber = getFiberFromDOM(rootEl);
2107
+ if (!fiber) {
2108
+ return {
2109
+ error: `No React fiber found on element "${selector}". Is this a React app?`,
2110
+ };
2111
+ }
2112
+ // Walk up to find the root component fiber (skip HostRoot)
2113
+ let current = fiber;
2114
+ while (current && current.tag === 3) {
2115
+ // tag 3 = HostRoot
2116
+ current = current.child;
2117
+ }
2118
+ if (!current) {
2119
+ return { error: "Could not find root React component fiber." };
2120
+ }
2121
+ const tree = fiberToInfo(current, 0, maxDepth);
2122
+ if (!tree) {
2123
+ return { error: "Could not build React component tree." };
2124
+ }
2125
+ return tree;
2126
+ }
2127
+ getReactTree.__schema__ = {
2128
+ name: "getReactTree",
2129
+ description: "Inspect the React component tree starting from a DOM element. Returns component names, props, state (including hooks), and children hierarchy.",
2130
+ parameters: {
2131
+ type: "object",
2132
+ properties: {
2133
+ selector: {
2134
+ type: "string",
2135
+ description: 'CSS selector of the React root element. Default: "#root".',
2136
+ },
2137
+ max_depth: {
2138
+ type: "number",
2139
+ description: "Maximum depth to traverse the component tree. Default: 5.",
2140
+ },
2141
+ },
2142
+ },
2143
+ };
2144
+
2145
+ /**
2146
+ * Core debugger class: connects to Hypha and registers the debug service.
2147
+ */
2148
+ class HyphaDebugger {
2149
+ constructor(config) {
2150
+ this.overlay = null;
2151
+ this.server = null;
2152
+ this.serviceInfo = null;
2153
+ this.config = {
2154
+ server_url: config.server_url,
2155
+ workspace: config.workspace ?? "",
2156
+ token: config.token ?? "",
2157
+ service_id: config.service_id ?? "web-debugger",
2158
+ service_name: config.service_name ?? "Web Debugger",
2159
+ show_ui: config.show_ui ?? true,
2160
+ visibility: config.visibility ?? "public",
2161
+ };
2162
+ }
2163
+ async start() {
2164
+ // Install console capture early
2165
+ installConsoleCapture();
2166
+ // Guard against double-injection
2167
+ const w = window;
2168
+ if (w.__HYPHA_DEBUGGER__?.instance) {
2169
+ console.warn("[hypha-debugger] Already running, returning existing session.");
2170
+ return w.__HYPHA_DEBUGGER__.session;
2171
+ }
2172
+ // Show UI if enabled
2173
+ if (this.config.show_ui) {
2174
+ this.overlay = new DebugOverlay();
2175
+ this.overlay.setStatus("disconnected");
2176
+ this.overlay.setInfo({ Status: "Connecting..." });
2177
+ }
2178
+ try {
2179
+ // Get the connectToServer function
2180
+ const connect = this.getConnectToServer();
2181
+ // Connect to Hypha server
2182
+ const connectConfig = {
2183
+ server_url: this.config.server_url,
2184
+ };
2185
+ if (this.config.workspace)
2186
+ connectConfig.workspace = this.config.workspace;
2187
+ if (this.config.token)
2188
+ connectConfig.token = this.config.token;
2189
+ this.server = await connect(connectConfig);
2190
+ // Wrap service functions with logging
2191
+ const wrapFn = (fn, name) => {
2192
+ const wrapped = async (...args) => {
2193
+ this.overlay?.addLog(`${name}(${this.summarizeArgs(args)})`, "call");
2194
+ try {
2195
+ const result = await fn(...args);
2196
+ const hasError = result && typeof result === "object" && "error" in result;
2197
+ if (hasError) {
2198
+ this.overlay?.addLog(`${name}: ${result.error}`, "error");
2199
+ }
2200
+ else {
2201
+ this.overlay?.addLog(`${name} -> OK`, "result");
2202
+ }
2203
+ return result;
2204
+ }
2205
+ catch (err) {
2206
+ this.overlay?.addLog(`${name}: ${err.message}`, "error");
2207
+ throw err;
2208
+ }
2209
+ };
2210
+ // Preserve schema
2211
+ if (fn.__schema__) {
2212
+ wrapped.__schema__ = fn.__schema__;
2213
+ }
2214
+ return wrapped;
2215
+ };
2216
+ // Register debug service
2217
+ const service = {
2218
+ id: this.config.service_id,
2219
+ name: this.config.service_name,
2220
+ type: "debugger",
2221
+ description: "Remote web page debugger. Allows inspecting DOM, taking screenshots, executing JavaScript, and interacting with the page.",
2222
+ config: {
2223
+ visibility: this.config.visibility,
2224
+ },
2225
+ // Service functions
2226
+ get_page_info: wrapFn(getPageInfo, "get_page_info"),
2227
+ get_console_logs: wrapFn(getConsoleLogs, "get_console_logs"),
2228
+ query_dom: wrapFn(queryDom, "query_dom"),
2229
+ click_element: wrapFn(clickElement, "click_element"),
2230
+ fill_input: wrapFn(fillInput, "fill_input"),
2231
+ scroll_to: wrapFn(scrollTo, "scroll_to"),
2232
+ get_computed_styles: wrapFn(getComputedStyles, "get_computed_styles"),
2233
+ get_element_bounds: wrapFn(getElementBounds, "get_element_bounds"),
2234
+ take_screenshot: wrapFn(takeScreenshot, "take_screenshot"),
2235
+ execute_script: wrapFn(executeScript, "execute_script"),
2236
+ navigate: wrapFn(navigate, "navigate"),
2237
+ go_back: wrapFn(goBack, "go_back"),
2238
+ go_forward: wrapFn(goForward, "go_forward"),
2239
+ reload: wrapFn(reload, "reload"),
2240
+ get_react_tree: wrapFn(getReactTree, "get_react_tree"),
2241
+ };
2242
+ this.serviceInfo = await this.server.registerService(service);
2243
+ // Update UI
2244
+ const workspace = this.server.config.workspace;
2245
+ const fullServiceId = this.serviceInfo.id ?? this.config.service_id;
2246
+ // Generate a token for remote HTTP access
2247
+ const sessionToken = await this.server.generateToken();
2248
+ // Build the HTTP service URL
2249
+ const serviceUrl = this.buildServiceUrl(fullServiceId);
2250
+ if (this.overlay) {
2251
+ this.overlay.setStatus("connected");
2252
+ this.overlay.setInfo({
2253
+ Status: "Connected",
2254
+ Server: this.config.server_url,
2255
+ });
2256
+ this.overlay.setServiceUrl(serviceUrl, sessionToken);
2257
+ this.overlay.addLog("Service registered", "result");
2258
+ }
2259
+ console.log(`[hypha-debugger] Connected to ${this.config.server_url}`);
2260
+ console.log(`[hypha-debugger] Service ID: ${fullServiceId}`);
2261
+ console.log(`[hypha-debugger] Service URL: ${serviceUrl}`);
2262
+ console.log(`[hypha-debugger] Token: ${sessionToken}`);
2263
+ console.log(`[hypha-debugger] Test it:\n curl '${serviceUrl}/get_page_info' -H 'Authorization: Bearer ${sessionToken}'`);
2264
+ // Build session object
2265
+ const session = {
2266
+ service_id: fullServiceId,
2267
+ workspace,
2268
+ server: this.server,
2269
+ service_url: serviceUrl,
2270
+ token: sessionToken,
2271
+ destroy: () => this.destroy(),
2272
+ };
2273
+ // Store globally
2274
+ w.__HYPHA_DEBUGGER__ = w.__HYPHA_DEBUGGER__ ?? {};
2275
+ w.__HYPHA_DEBUGGER__.instance = this;
2276
+ w.__HYPHA_DEBUGGER__.session = session;
2277
+ return session;
2278
+ }
2279
+ catch (err) {
2280
+ console.error("[hypha-debugger] Failed to start:", err);
2281
+ if (this.overlay) {
2282
+ this.overlay.setStatus("error");
2283
+ this.overlay.setInfo({
2284
+ Status: "Error",
2285
+ Error: err.message ?? String(err),
2286
+ });
2287
+ }
2288
+ throw err;
2289
+ }
2290
+ }
2291
+ async destroy() {
2292
+ try {
2293
+ if (this.serviceInfo && this.server) {
2294
+ await this.server.unregisterService(this.serviceInfo.id);
2295
+ }
2296
+ }
2297
+ catch {
2298
+ // Ignore unregister errors on cleanup
2299
+ }
2300
+ this.overlay?.destroy();
2301
+ this.overlay = null;
2302
+ const w = window;
2303
+ if (w.__HYPHA_DEBUGGER__) {
2304
+ delete w.__HYPHA_DEBUGGER__.instance;
2305
+ delete w.__HYPHA_DEBUGGER__.session;
2306
+ }
2307
+ }
2308
+ buildServiceUrl(serviceId) {
2309
+ const base = this.config.server_url.replace(/\/+$/, "");
2310
+ // serviceId format: "workspace/clientId:svcName"
2311
+ const slashIdx = serviceId.indexOf("/");
2312
+ if (slashIdx !== -1) {
2313
+ const workspace = serviceId.substring(0, slashIdx);
2314
+ const svcPart = serviceId.substring(slashIdx + 1);
2315
+ return `${base}/${workspace}/services/${svcPart}`;
2316
+ }
2317
+ return `${base}/services/${serviceId}`;
2318
+ }
2319
+ getConnectToServer() {
2320
+ // Prefer the static import; fall back to global for UMD/script-tag usage
2321
+ const connect = hyphaRpc.connectToServer;
2322
+ if (connect)
2323
+ return connect;
2324
+ const w = window;
2325
+ if (w.hyphaWebsocketClient?.connectToServer) {
2326
+ return w.hyphaWebsocketClient.connectToServer;
2327
+ }
2328
+ throw new Error("hypha-rpc not found. Install it via npm or include the script tag: " +
2329
+ '<script src="https://cdn.jsdelivr.net/npm/hypha-rpc@0.20.97/dist/hypha-rpc-websocket.min.js"></script>');
2330
+ }
2331
+ summarizeArgs(args) {
2332
+ if (args.length === 0)
2333
+ return "";
2334
+ return args
2335
+ .map((a) => {
2336
+ if (typeof a === "string")
2337
+ return a.length > 40 ? a.slice(0, 40) + "..." : a;
2338
+ if (typeof a === "object" && a !== null)
2339
+ return "{...}";
2340
+ return String(a);
2341
+ })
2342
+ .join(", ");
2343
+ }
2344
+ }
2345
+
2346
+ /**
2347
+ * hypha-debugger - Injectable debugger for web pages, powered by Hypha RPC.
2348
+ *
2349
+ * Usage:
2350
+ * import { startDebugger } from 'hypha-debugger';
2351
+ * const session = await startDebugger({ server_url: 'https://hypha.aicell.io' });
2352
+ */
2353
+ /**
2354
+ * Start the Hypha debugger. Connects to a Hypha server and registers
2355
+ * a debug service that remote clients can use to inspect and interact
2356
+ * with this web page.
2357
+ *
2358
+ * @param config - Configuration for the debugger.
2359
+ * @returns A session object with service_id, workspace, and destroy().
2360
+ *
2361
+ * @example
2362
+ * ```js
2363
+ * import { startDebugger } from 'hypha-debugger';
2364
+ * const session = await startDebugger({ server_url: 'https://hypha.aicell.io' });
2365
+ * console.log(`Service: ${session.service_id}, Workspace: ${session.workspace}`);
2366
+ * ```
2367
+ */
2368
+ async function startDebugger(config) {
2369
+ const debugger_ = new HyphaDebugger(config);
2370
+ return debugger_.start();
2371
+ }
2372
+
2373
+ export { HyphaDebugger, startDebugger };
2374
+ //# sourceMappingURL=hypha-debugger.mjs.map