hypha-debugger 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hypha-debugger.js +213 -91
- package/dist/hypha-debugger.js.map +1 -1
- package/dist/hypha-debugger.min.js +7 -7
- package/dist/hypha-debugger.min.js.map +1 -1
- package/dist/hypha-debugger.mjs +213 -91
- package/dist/hypha-debugger.mjs.map +1 -1
- package/dist/services/screenshot.d.ts +5 -4
- package/package.json +1 -1
package/dist/hypha-debugger.mjs
CHANGED
|
@@ -1968,27 +1968,95 @@ async function toJpeg(node, options = {}) {
|
|
|
1968
1968
|
|
|
1969
1969
|
/**
|
|
1970
1970
|
* Screenshot capture service using html-to-image.
|
|
1971
|
+
*
|
|
1972
|
+
* Images are downscaled before being returned so agents don't receive
|
|
1973
|
+
* multi-megabyte base64 payloads that can crash their context window.
|
|
1971
1974
|
*/
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1975
|
+
/**
|
|
1976
|
+
* Resize an image data URL via a canvas. Returns a new data URL at the
|
|
1977
|
+
* requested format/quality. Maintains aspect ratio: fits within
|
|
1978
|
+
* (maxWidth × maxHeight) without distortion.
|
|
1979
|
+
*/
|
|
1980
|
+
async function resizeDataUrl(dataUrl, maxWidth, maxHeight, format, quality) {
|
|
1981
|
+
return new Promise((resolve, reject) => {
|
|
1982
|
+
const img = new Image();
|
|
1983
|
+
img.onload = () => {
|
|
1984
|
+
const srcW = img.naturalWidth;
|
|
1985
|
+
const srcH = img.naturalHeight;
|
|
1986
|
+
// Compute scale to fit within bounds (but never upscale)
|
|
1987
|
+
const scale = Math.min(maxWidth / srcW, maxHeight / srcH, 1);
|
|
1988
|
+
const dstW = Math.max(1, Math.round(srcW * scale));
|
|
1989
|
+
const dstH = Math.max(1, Math.round(srcH * scale));
|
|
1990
|
+
const canvas = document.createElement("canvas");
|
|
1991
|
+
canvas.width = dstW;
|
|
1992
|
+
canvas.height = dstH;
|
|
1993
|
+
const ctx = canvas.getContext("2d");
|
|
1994
|
+
if (!ctx) {
|
|
1995
|
+
reject(new Error("Could not get 2D canvas context"));
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
// Fill white background for JPEG (no alpha support)
|
|
1999
|
+
if (format === "jpeg") {
|
|
2000
|
+
ctx.fillStyle = "#ffffff";
|
|
2001
|
+
ctx.fillRect(0, 0, dstW, dstH);
|
|
2002
|
+
}
|
|
2003
|
+
ctx.drawImage(img, 0, 0, dstW, dstH);
|
|
2004
|
+
const mime = format === "jpeg" ? "image/jpeg" : "image/png";
|
|
2005
|
+
const out = canvas.toDataURL(mime, quality);
|
|
2006
|
+
resolve({ dataUrl: out, width: dstW, height: dstH });
|
|
2007
|
+
};
|
|
2008
|
+
img.onerror = () => reject(new Error("Failed to load image for resizing"));
|
|
2009
|
+
img.src = dataUrl;
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
async function takeScreenshot(selector, format, quality, max_width, max_height, full_page) {
|
|
2013
|
+
// Agent-friendly defaults: JPEG, moderate quality, capped at 1024px,
|
|
2014
|
+
// viewport-only (not the entire scrollable page).
|
|
2015
|
+
const fmt = format ?? "jpeg";
|
|
2016
|
+
const qual = quality ?? 0.75;
|
|
2017
|
+
const maxW = max_width ?? 1024;
|
|
2018
|
+
const maxH = max_height ?? 1024;
|
|
2019
|
+
const capturePage = full_page ?? false;
|
|
2020
|
+
// Pick target:
|
|
2021
|
+
// - explicit selector → that element
|
|
2022
|
+
// - full_page=true → document.documentElement (the entire scrollable page)
|
|
2023
|
+
// - default → viewport-sized region (clipped to window size)
|
|
2024
|
+
let target;
|
|
2025
|
+
if (selector) {
|
|
2026
|
+
target = document.querySelector(selector);
|
|
2027
|
+
if (!target) {
|
|
2028
|
+
return { error: `No element found for selector: ${selector}` };
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
else if (capturePage) {
|
|
2032
|
+
target = document.documentElement;
|
|
2033
|
+
}
|
|
2034
|
+
else {
|
|
2035
|
+
target = document.body;
|
|
1979
2036
|
}
|
|
1980
2037
|
try {
|
|
1981
2038
|
const node = target;
|
|
2039
|
+
// For viewport-only captures, limit html-to-image's output size
|
|
2040
|
+
// to the viewport dimensions.
|
|
2041
|
+
const viewportW = window.innerWidth;
|
|
2042
|
+
const viewportH = window.innerHeight;
|
|
1982
2043
|
const captureOptions = {
|
|
1983
2044
|
quality: qual,
|
|
1984
|
-
pixelRatio:
|
|
2045
|
+
pixelRatio: 1, // always capture at 1x — we'll resize after
|
|
1985
2046
|
cacheBust: true,
|
|
1986
2047
|
skipAutoScale: true,
|
|
1987
|
-
// Filter out the debugger overlay itself
|
|
1988
2048
|
filter: (el) => {
|
|
1989
|
-
|
|
2049
|
+
// Exclude the debugger overlay and cursor from screenshots
|
|
2050
|
+
return (el.id !== "hypha-debugger-host" &&
|
|
2051
|
+
el.id !== "hypha-debugger-cursor" &&
|
|
2052
|
+
el.id !== "playwright-highlight-container");
|
|
1990
2053
|
},
|
|
1991
2054
|
};
|
|
2055
|
+
if (!selector && !capturePage) {
|
|
2056
|
+
// Viewport-only capture: constrain canvas to window size
|
|
2057
|
+
captureOptions.width = viewportW;
|
|
2058
|
+
captureOptions.height = viewportH;
|
|
2059
|
+
}
|
|
1992
2060
|
let dataUrl;
|
|
1993
2061
|
if (fmt === "jpeg") {
|
|
1994
2062
|
dataUrl = await toJpeg(node, captureOptions);
|
|
@@ -1996,26 +2064,15 @@ async function takeScreenshot(selector, format, quality, scale, max_width, max_h
|
|
|
1996
2064
|
else {
|
|
1997
2065
|
dataUrl = await toPng(node, captureOptions);
|
|
1998
2066
|
}
|
|
1999
|
-
//
|
|
2000
|
-
const
|
|
2001
|
-
|
|
2002
|
-
let height = Math.round(rect.height * scl);
|
|
2003
|
-
// Optionally resize if too large
|
|
2004
|
-
if (max_width && width > max_width) {
|
|
2005
|
-
const ratio = max_width / width;
|
|
2006
|
-
width = max_width;
|
|
2007
|
-
height = Math.round(height * ratio);
|
|
2008
|
-
}
|
|
2009
|
-
if (max_height && height > max_height) {
|
|
2010
|
-
const ratio = max_height / height;
|
|
2011
|
-
height = max_height;
|
|
2012
|
-
width = Math.round(width * ratio);
|
|
2013
|
-
}
|
|
2067
|
+
// Resize down to fit within (maxW × maxH) and re-encode
|
|
2068
|
+
const resized = await resizeDataUrl(dataUrl, maxW, maxH, fmt, qual);
|
|
2069
|
+
const sizeKb = Math.round((resized.dataUrl.length * 0.75) / 1024); // rough base64 → bytes
|
|
2014
2070
|
return {
|
|
2015
|
-
data: dataUrl,
|
|
2071
|
+
data: resized.dataUrl,
|
|
2016
2072
|
format: fmt,
|
|
2017
|
-
width,
|
|
2018
|
-
height,
|
|
2073
|
+
width: resized.width,
|
|
2074
|
+
height: resized.height,
|
|
2075
|
+
size_kb: sizeKb,
|
|
2019
2076
|
};
|
|
2020
2077
|
}
|
|
2021
2078
|
catch (err) {
|
|
@@ -2024,34 +2081,37 @@ async function takeScreenshot(selector, format, quality, scale, max_width, max_h
|
|
|
2024
2081
|
}
|
|
2025
2082
|
takeScreenshot.__schema__ = {
|
|
2026
2083
|
name: "takeScreenshot",
|
|
2027
|
-
description: "Capture a screenshot of the
|
|
2084
|
+
description: "Capture a screenshot of the current viewport, a specific element, or the full page. " +
|
|
2085
|
+
"Returns a base64-encoded data URL, downscaled to fit within max_width × max_height " +
|
|
2086
|
+
"(default 1024px) to keep the payload small enough for AI agents. Defaults to JPEG " +
|
|
2087
|
+
"format at 0.75 quality for reasonable file size.",
|
|
2028
2088
|
parameters: {
|
|
2029
2089
|
type: "object",
|
|
2030
2090
|
properties: {
|
|
2031
2091
|
selector: {
|
|
2032
2092
|
type: "string",
|
|
2033
|
-
description: "CSS selector of the element to capture. Omit to capture the
|
|
2093
|
+
description: "CSS selector of the element to capture. Omit to capture the viewport (or full page if full_page=true).",
|
|
2034
2094
|
},
|
|
2035
2095
|
format: {
|
|
2036
2096
|
type: "string",
|
|
2037
2097
|
enum: ["png", "jpeg"],
|
|
2038
|
-
description: 'Image format. Default: "png".',
|
|
2098
|
+
description: 'Image format. Default: "jpeg" (much smaller than PNG). Use "png" for sharp text.',
|
|
2039
2099
|
},
|
|
2040
2100
|
quality: {
|
|
2041
2101
|
type: "number",
|
|
2042
|
-
description: "
|
|
2043
|
-
},
|
|
2044
|
-
scale: {
|
|
2045
|
-
type: "number",
|
|
2046
|
-
description: "Pixel ratio / scale factor. Default: 1. Use 2 for retina.",
|
|
2102
|
+
description: "JPEG quality (0–1). Default: 0.75. Ignored for PNG. Lower = smaller payload.",
|
|
2047
2103
|
},
|
|
2048
2104
|
max_width: {
|
|
2049
2105
|
type: "number",
|
|
2050
|
-
description: "Maximum width in pixels. Image
|
|
2106
|
+
description: "Maximum output width in pixels. Default: 1024. Image is scaled down preserving aspect ratio.",
|
|
2051
2107
|
},
|
|
2052
2108
|
max_height: {
|
|
2053
2109
|
type: "number",
|
|
2054
|
-
description: "Maximum height in pixels. Image
|
|
2110
|
+
description: "Maximum output height in pixels. Default: 1024. Image is scaled down preserving aspect ratio.",
|
|
2111
|
+
},
|
|
2112
|
+
full_page: {
|
|
2113
|
+
type: "boolean",
|
|
2114
|
+
description: "If true, capture the entire scrollable page instead of just the viewport. Default: false.",
|
|
2055
2115
|
},
|
|
2056
2116
|
},
|
|
2057
2117
|
},
|
|
@@ -2060,12 +2120,56 @@ takeScreenshot.__schema__ = {
|
|
|
2060
2120
|
/**
|
|
2061
2121
|
* Arbitrary JavaScript execution service.
|
|
2062
2122
|
*/
|
|
2123
|
+
/**
|
|
2124
|
+
* Attempt to auto-return the last expression in a code block.
|
|
2125
|
+
* If the code doesn't contain an explicit `return`, we try to
|
|
2126
|
+
* add one to the last expression statement so the result is captured.
|
|
2127
|
+
*
|
|
2128
|
+
* Examples:
|
|
2129
|
+
* "document.title" → "return (document.title);"
|
|
2130
|
+
* "const x = 1; x + 2" → "const x = 1; return (x + 2);"
|
|
2131
|
+
* "const x = 1\nx + 2" → "const x = 1\nreturn (x + 2);"
|
|
2132
|
+
* "for(...) {}" → unchanged (control flow)
|
|
2133
|
+
* "return 42" → unchanged (explicit return)
|
|
2134
|
+
*/
|
|
2135
|
+
function autoReturn(code) {
|
|
2136
|
+
const trimmed = code.trim();
|
|
2137
|
+
// Already has a return statement? Leave it alone.
|
|
2138
|
+
if (/\breturn\b/.test(trimmed))
|
|
2139
|
+
return trimmed;
|
|
2140
|
+
// Split into statements: by newlines first, then by semicolons for
|
|
2141
|
+
// single-line multi-statement code like "const x = 1; x + 2"
|
|
2142
|
+
let lines = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
2143
|
+
// If there's only one line with semicolons, split on semicolons
|
|
2144
|
+
if (lines.length === 1 && lines[0].includes(";")) {
|
|
2145
|
+
lines = lines[0].split(";").map((s) => s.trim()).filter(Boolean);
|
|
2146
|
+
}
|
|
2147
|
+
if (lines.length === 0)
|
|
2148
|
+
return trimmed;
|
|
2149
|
+
const lastLine = lines[lines.length - 1];
|
|
2150
|
+
// Don't add return to control flow, declarations, or assignment-only statements
|
|
2151
|
+
if (/^(if|for|while|switch|try|class|function |const |let |var |import |export )/.test(lastLine)) {
|
|
2152
|
+
return trimmed;
|
|
2153
|
+
}
|
|
2154
|
+
// Replace last statement with return
|
|
2155
|
+
lines[lines.length - 1] = "return (" + lastLine.replace(/;$/, "") + ");";
|
|
2156
|
+
return lines.join(";\n");
|
|
2157
|
+
}
|
|
2063
2158
|
async function executeScript(code, timeout_ms) {
|
|
2064
2159
|
const timeoutMs = timeout_ms ?? 10000;
|
|
2065
2160
|
try {
|
|
2161
|
+
// Try with auto-return first, fall back to original code if syntax error
|
|
2162
|
+
let execCode = autoReturn(code);
|
|
2163
|
+
let fn;
|
|
2164
|
+
try {
|
|
2165
|
+
fn = new Function("return (async () => {" + execCode + "})()");
|
|
2166
|
+
}
|
|
2167
|
+
catch {
|
|
2168
|
+
// Auto-return broke the syntax — use original code
|
|
2169
|
+
fn = new Function("return (async () => {" + code + "})()");
|
|
2170
|
+
}
|
|
2066
2171
|
const result = await Promise.race([
|
|
2067
|
-
|
|
2068
|
-
new Function("return (async () => {" + code + "})()")(),
|
|
2172
|
+
fn(),
|
|
2069
2173
|
new Promise((_, reject) => setTimeout(() => reject(new Error("Execution timed out")), timeoutMs)),
|
|
2070
2174
|
]);
|
|
2071
2175
|
// Serialize the result safely
|
|
@@ -2080,6 +2184,7 @@ async function executeScript(code, timeout_ms) {
|
|
|
2080
2184
|
serialized = {
|
|
2081
2185
|
tag: result.tagName.toLowerCase(),
|
|
2082
2186
|
id: result.id,
|
|
2187
|
+
className: result.className,
|
|
2083
2188
|
text: (result.textContent ?? "").trim().slice(0, 500),
|
|
2084
2189
|
};
|
|
2085
2190
|
type = "HTMLElement";
|
|
@@ -2109,13 +2214,13 @@ async function executeScript(code, timeout_ms) {
|
|
|
2109
2214
|
}
|
|
2110
2215
|
executeScript.__schema__ = {
|
|
2111
2216
|
name: "executeScript",
|
|
2112
|
-
description:
|
|
2217
|
+
description: 'Execute arbitrary JavaScript code in the page context. Supports async/await. The last expression is auto-returned (no need for explicit "return"). Examples: "document.title", "document.querySelectorAll(\'a\').length", "await fetch(\'/api/data\').then(r => r.json())".',
|
|
2113
2218
|
parameters: {
|
|
2114
2219
|
type: "object",
|
|
2115
2220
|
properties: {
|
|
2116
2221
|
code: {
|
|
2117
2222
|
type: "string",
|
|
2118
|
-
description: 'JavaScript code to execute. The
|
|
2223
|
+
description: 'JavaScript code to execute. The last expression is automatically returned. Examples: "document.title", "document.querySelector(\'h1\').textContent".',
|
|
2119
2224
|
},
|
|
2120
2225
|
timeout_ms: {
|
|
2121
2226
|
type: "number",
|
|
@@ -2638,70 +2743,78 @@ function generateSkillMd(serviceFunctions, serviceUrl) {
|
|
|
2638
2743
|
"# Web Debugger Skill",
|
|
2639
2744
|
"",
|
|
2640
2745
|
"This skill allows you to remotely debug and interact with a web page through HTTP API endpoints.",
|
|
2746
|
+
"Pick the approach that fits your task — they can be combined freely.",
|
|
2641
2747
|
"",
|
|
2642
|
-
"##
|
|
2748
|
+
"## Approaches",
|
|
2643
2749
|
"",
|
|
2644
|
-
"
|
|
2750
|
+
"### execute_script — Run Arbitrary JavaScript",
|
|
2751
|
+
"",
|
|
2752
|
+
"The most versatile function. Use it to read/modify page state, call APIs, query the DOM,",
|
|
2753
|
+
"or do anything JavaScript can do. The last expression is auto-returned (no need for `return`).",
|
|
2645
2754
|
"",
|
|
2646
|
-
"### Step 1: Observe the page",
|
|
2647
2755
|
"```bash",
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
"Elements are detected via smart heuristics: CSS cursor, ARIA roles, event listeners, tag names.",
|
|
2652
|
-
"Visual highlight labels are overlaid on the page for each detected element.",
|
|
2756
|
+
`# Read page state`,
|
|
2757
|
+
`curl -X POST '{SERVICE_URL}/execute_script' \\`,
|
|
2758
|
+
` -H 'Content-Type: application/json' -d '{"code": "document.title"}'`,
|
|
2653
2759
|
"",
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
"
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2760
|
+
`# Query DOM`,
|
|
2761
|
+
`curl -X POST '{SERVICE_URL}/execute_script' \\`,
|
|
2762
|
+
` -H 'Content-Type: application/json' -d '{"code": "document.querySelector(\\\"h1\\\").textContent"}'`,
|
|
2763
|
+
"",
|
|
2764
|
+
`# Call an API`,
|
|
2765
|
+
`curl -X POST '{SERVICE_URL}/execute_script' \\`,
|
|
2766
|
+
` -H 'Content-Type: application/json' -d '{"code": "await fetch(\\\"/api/data\\\").then(r => r.json())"}'`,
|
|
2767
|
+
"",
|
|
2768
|
+
`# Modify the page`,
|
|
2769
|
+
`curl -X POST '{SERVICE_URL}/execute_script' \\`,
|
|
2770
|
+
` -H 'Content-Type: application/json' -d '{"code": "document.getElementById(\\\"name\\\").value = \\\"Alice\\\""}'`,
|
|
2661
2771
|
"```",
|
|
2662
2772
|
"",
|
|
2663
|
-
"###
|
|
2773
|
+
"### get_browser_state + Index-Based Interaction",
|
|
2774
|
+
"",
|
|
2775
|
+
"Best for UI interaction as a user would — clicking buttons, filling forms, selecting options.",
|
|
2776
|
+
"All interactive elements are detected and indexed as `[0]`, `[1]`, `[2]`, etc.",
|
|
2777
|
+
"",
|
|
2664
2778
|
"```bash",
|
|
2665
|
-
|
|
2779
|
+
`# Step 1: See all interactive elements`,
|
|
2780
|
+
`curl '{SERVICE_URL}/get_browser_state'`,
|
|
2781
|
+
"",
|
|
2782
|
+
`# Step 2: Act by index`,
|
|
2666
2783
|
`curl -X POST '{SERVICE_URL}/click_element_by_index' \\`,
|
|
2667
2784
|
` -H 'Content-Type: application/json' -d '{"index": 2}'`,
|
|
2668
2785
|
"",
|
|
2669
|
-
"# Type into an input (e.g. [1] Search):",
|
|
2670
2786
|
`curl -X POST '{SERVICE_URL}/input_text' \\`,
|
|
2671
2787
|
` -H 'Content-Type: application/json' -d '{"index": 1, "text": "hello world"}'`,
|
|
2672
2788
|
"",
|
|
2673
|
-
"# Select a dropdown option (e.g. [3] Language):",
|
|
2674
2789
|
`curl -X POST '{SERVICE_URL}/select_option' \\`,
|
|
2675
2790
|
` -H 'Content-Type: application/json' -d '{"index": 3, "option_text": "French"}'`,
|
|
2676
2791
|
"",
|
|
2677
|
-
"# Scroll down:",
|
|
2678
2792
|
`curl -X POST '{SERVICE_URL}/scroll' \\`,
|
|
2679
2793
|
` -H 'Content-Type: application/json' -d '{"direction": "down"}'`,
|
|
2680
2794
|
"",
|
|
2681
|
-
|
|
2682
|
-
`curl -X POST '{SERVICE_URL}/scroll' \\`,
|
|
2683
|
-
` -H 'Content-Type: application/json' -d '{"direction": "down", "index": 4}'`,
|
|
2684
|
-
"```",
|
|
2685
|
-
"",
|
|
2686
|
-
"### Step 3: Verify",
|
|
2687
|
-
"```bash",
|
|
2795
|
+
`# Step 3: Verify visually`,
|
|
2688
2796
|
`curl '{SERVICE_URL}/take_screenshot'`,
|
|
2689
2797
|
"```",
|
|
2690
2798
|
"",
|
|
2691
|
-
"###
|
|
2799
|
+
"### get_react_tree — Inspect React Components",
|
|
2800
|
+
"",
|
|
2801
|
+
"If the page uses React, inspect component names, props, state, and hooks:",
|
|
2692
2802
|
"```bash",
|
|
2693
|
-
`curl '{SERVICE_URL}/
|
|
2803
|
+
`curl '{SERVICE_URL}/get_react_tree'`,
|
|
2694
2804
|
"```",
|
|
2695
2805
|
"",
|
|
2696
|
-
"
|
|
2806
|
+
"### CSS Selector-Based Functions",
|
|
2697
2807
|
"",
|
|
2698
|
-
"
|
|
2808
|
+
"Use CSS selectors directly when you know the element:",
|
|
2699
2809
|
"```bash",
|
|
2700
2810
|
`curl -X POST '{SERVICE_URL}/click_element' \\`,
|
|
2701
2811
|
` -H 'Content-Type: application/json' -d '{"selector": "button.submit"}'`,
|
|
2702
2812
|
"",
|
|
2703
2813
|
`curl -X POST '{SERVICE_URL}/fill_input' \\`,
|
|
2704
2814
|
` -H 'Content-Type: application/json' -d '{"selector": "#email", "value": "user@example.com"}'`,
|
|
2815
|
+
"",
|
|
2816
|
+
`curl -X POST '{SERVICE_URL}/query_dom' \\`,
|
|
2817
|
+
` -H 'Content-Type: application/json' -d '{"selector": ".product-card"}'`,
|
|
2705
2818
|
"```",
|
|
2706
2819
|
"",
|
|
2707
2820
|
"## How to call functions",
|
|
@@ -2777,14 +2890,14 @@ function generateSkillMd(serviceFunctions, serviceUrl) {
|
|
|
2777
2890
|
const tips = [
|
|
2778
2891
|
"## Tips",
|
|
2779
2892
|
"",
|
|
2780
|
-
"-
|
|
2781
|
-
"-
|
|
2893
|
+
"- **`execute_script` is the most versatile** — use it for reading state, calling APIs, DOM queries, or anything not covered by other functions. The last expression is auto-returned.",
|
|
2894
|
+
"- **`get_browser_state` is the best way to see what's on the page** — it detects all interactive elements and shows them as indexed items.",
|
|
2782
2895
|
"- **After each action, call `get_browser_state` again** — element indices change when the DOM updates.",
|
|
2783
2896
|
"- **Use `take_screenshot`** to visually verify the page state. Call `remove_highlights` first for a clean view.",
|
|
2784
|
-
"- **Use `execute_script`** for anything not covered by the built-in functions — it runs arbitrary JavaScript.",
|
|
2785
2897
|
"- **Use `scroll`** with an element index to scroll inside a specific container (e.g. a chat window, sidebar).",
|
|
2786
2898
|
"- **Use `get_page_info` with `include_logs=true`** to check for JavaScript errors or debug output.",
|
|
2787
|
-
"- **Use `get_react_tree`** if the page uses React — it gives you component names, props, and state.",
|
|
2899
|
+
"- **Use `get_react_tree`** if the page uses React — it gives you component names, props, and state without needing DevTools.",
|
|
2900
|
+
"- **Use `navigate`** to go to other pages — same-origin navigation auto-reconnects the debugger.",
|
|
2788
2901
|
"- All POST endpoints accept JSON body with the parameter names as keys.",
|
|
2789
2902
|
"",
|
|
2790
2903
|
].join("\n");
|
|
@@ -2818,14 +2931,24 @@ function wrapFn(fn) {
|
|
|
2818
2931
|
// Create a wrapper that:
|
|
2819
2932
|
// 1. Has correct, unminified parameter names (for hypha-rpc getParamNames)
|
|
2820
2933
|
// 2. Detects when kwargs are passed as a single object and destructures them
|
|
2934
|
+
//
|
|
2935
|
+
// hypha-rpc HTTP handler passes kwargs as a single plain object, e.g.:
|
|
2936
|
+
// execute_script({code: "..."}) instead of execute_script("...")
|
|
2937
|
+
// get_react_tree({}) instead of get_react_tree()
|
|
2938
|
+
// We detect this and destructure, or discard empty objects.
|
|
2821
2939
|
const paramList = paramNames.join(", ");
|
|
2940
|
+
const firstParam = paramNames[0];
|
|
2822
2941
|
const wrapper = new Function("fn", "paramNames", `return async function(${paramList}) {
|
|
2823
2942
|
// Detect kwargs-as-object: single argument that is a plain object
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
var
|
|
2827
|
-
|
|
2828
|
-
if (
|
|
2943
|
+
if (arguments.length === 1 && ${firstParam} != null && typeof ${firstParam} === "object" && !Array.isArray(${firstParam}) && !(${firstParam} instanceof Date) && ${firstParam}.constructor === Object) {
|
|
2944
|
+
var _kw = ${firstParam};
|
|
2945
|
+
var _keys = Object.keys(_kw);
|
|
2946
|
+
// Empty object {} → call with no args (all defaults)
|
|
2947
|
+
if (_keys.length === 0) {
|
|
2948
|
+
return fn();
|
|
2949
|
+
}
|
|
2950
|
+
// Keys match schema params → destructure
|
|
2951
|
+
if (paramNames.indexOf(_keys[0]) !== -1) {
|
|
2829
2952
|
var _args = paramNames.map(function(n) { return _kw[n]; });
|
|
2830
2953
|
return fn.apply(null, _args);
|
|
2831
2954
|
}
|
|
@@ -5915,22 +6038,21 @@ class HyphaDebugger {
|
|
|
5915
6038
|
`# A debugger is attached to a live web page.`,
|
|
5916
6039
|
`# You can remotely inspect, interact with, and control this page via the HTTP API below.`,
|
|
5917
6040
|
`#`,
|
|
5918
|
-
`#
|
|
5919
|
-
`#
|
|
5920
|
-
`#
|
|
5921
|
-
`#
|
|
5922
|
-
|
|
5923
|
-
`#
|
|
5924
|
-
`# scroll_to, execute_script, navigate, get_react_tree, remove_highlights`,
|
|
6041
|
+
`# APPROACHES (pick what fits your task):`,
|
|
6042
|
+
`# - execute_script: Run arbitrary JS — fastest for reading/modifying page state, DOM queries, API calls`,
|
|
6043
|
+
`# - get_browser_state + click/input/select by index: Visual interaction with the page as a user would`,
|
|
6044
|
+
`# - get_html / query_dom: Read DOM structure with CSS selectors`,
|
|
6045
|
+
`# - get_react_tree: Inspect React component props, state, and hooks`,
|
|
6046
|
+
`# - take_screenshot: Visual verification of page state`,
|
|
5925
6047
|
`#`,
|
|
5926
|
-
`# POST endpoints accept JSON body with parameter names as keys.`,
|
|
6048
|
+
`# All POST endpoints accept JSON body with parameter names as keys.`,
|
|
5927
6049
|
``,
|
|
5928
6050
|
`SERVICE_URL="${serviceUrl}"`,
|
|
5929
6051
|
];
|
|
5930
6052
|
if (token) {
|
|
5931
6053
|
lines.push(`TOKEN="${token}"`);
|
|
5932
6054
|
}
|
|
5933
|
-
lines.push(``, `#
|
|
6055
|
+
lines.push(``, `# Execute JavaScript (most versatile — read state, call APIs, modify DOM):`, `curl -X POST "$SERVICE_URL/execute_script"${auth} -H "Content-Type: application/json" -d '{"code": "document.title"}'`, ``, `# Smart DOM analysis (indexed interactive elements for click/type/select):`, `curl "$SERVICE_URL/get_browser_state"${auth}`, ``, `# Interact by element index:`, `curl -X POST "$SERVICE_URL/click_element_by_index"${auth} -H "Content-Type: application/json" -d '{"index": 3}'`, `curl -X POST "$SERVICE_URL/input_text"${auth} -H "Content-Type: application/json" -d '{"index": 5, "text": "hello"}'`, ``, `# Screenshot + React inspection:`, `curl "$SERVICE_URL/take_screenshot"${auth}`, `curl "$SERVICE_URL/get_react_tree"${auth}`, ``, `# Navigate (auto-reconnects for same-origin):`, `curl -X POST "$SERVICE_URL/navigate"${auth} -H "Content-Type: application/json" -d '{"url": "/other-page"}'`, ``, `# Full API docs:`, `curl "$SERVICE_URL/get_skill_md"${auth}`);
|
|
5934
6056
|
return lines.join("\n");
|
|
5935
6057
|
}
|
|
5936
6058
|
/**
|