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