hypha-debugger 0.1.4 → 0.1.6

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.
@@ -2127,20 +2127,248 @@ executeScript.__schema__ = {
2127
2127
  };
2128
2128
 
2129
2129
  /**
2130
- * Navigation service.
2130
+ * Navigation service with auto-reconnect support.
2131
+ *
2132
+ * For agent-triggered reload() and same-origin navigate(), we use a "soft"
2133
+ * approach: fetch the target HTML, inject the debugger <script> tag, then
2134
+ * replace the document via document.write(). The injected script auto-starts
2135
+ * from sessionStorage config, so the debugger reconnects with the same
2136
+ * workspace and service ID. The agent's URL stays stable.
2137
+ *
2138
+ * For cross-origin navigate(), goBack(), and goForward(), we fall back to
2139
+ * normal navigation. The debugger config is saved to sessionStorage so
2140
+ * re-clicking the bookmarklet reconnects seamlessly.
2141
+ */
2142
+ const STORAGE_KEY$1 = "__hypha_debugger_config__";
2143
+ /** Read the saved script URL from sessionStorage, or fall back to CDN. */
2144
+ function getScriptUrl() {
2145
+ try {
2146
+ const raw = sessionStorage.getItem(STORAGE_KEY$1);
2147
+ if (raw) {
2148
+ const config = JSON.parse(raw);
2149
+ if (config.script_url)
2150
+ return config.script_url;
2151
+ }
2152
+ }
2153
+ catch {
2154
+ // ignore
2155
+ }
2156
+ return "https://cdn.jsdelivr.net/npm/hypha-debugger/dist/hypha-debugger.min.js";
2157
+ }
2158
+ /**
2159
+ * Inject the debugger loader script into HTML before </body> (or append).
2160
+ */
2161
+ function injectLoader(html, scriptUrl) {
2162
+ const loader = `<script src="${scriptUrl}"><\/script>`;
2163
+ if (html.includes("</body>")) {
2164
+ return html.replace("</body>", loader + "\n</body>");
2165
+ }
2166
+ if (html.includes("</html>")) {
2167
+ return html.replace("</html>", loader + "\n</html>");
2168
+ }
2169
+ return html + "\n" + loader;
2170
+ }
2171
+ /**
2172
+ * Perform a soft page replacement: fetch HTML, inject debugger script,
2173
+ * replace the document via document.write(). If the fetch or write fails,
2174
+ * falls back to hard navigation.
2175
+ */
2176
+ function softReplace(url, pushState) {
2177
+ const scriptUrl = getScriptUrl();
2178
+ fetch(url, { credentials: "same-origin", cache: "reload" })
2179
+ .then((response) => {
2180
+ if (!response.ok)
2181
+ throw new Error(`HTTP ${response.status}`);
2182
+ const contentType = response.headers.get("content-type") ?? "";
2183
+ if (!contentType.includes("text/html")) {
2184
+ throw new Error("Not HTML");
2185
+ }
2186
+ return response.text();
2187
+ })
2188
+ .then((html) => {
2189
+ const modified = injectLoader(html, scriptUrl);
2190
+ document.open();
2191
+ document.write(modified);
2192
+ document.close();
2193
+ if (pushState) {
2194
+ try {
2195
+ history.pushState({}, "", pushState);
2196
+ }
2197
+ catch {
2198
+ // ignore — URL might already match
2199
+ }
2200
+ }
2201
+ })
2202
+ .catch(() => {
2203
+ // Soft replace failed — fall back to hard navigation
2204
+ if (pushState) {
2205
+ window.location.href = pushState;
2206
+ }
2207
+ else {
2208
+ window.location.reload();
2209
+ }
2210
+ });
2211
+ }
2212
+ /** Check if a URL is same-origin as the current page. */
2213
+ function isSameOrigin(url) {
2214
+ try {
2215
+ const target = new URL(url, location.href);
2216
+ return target.origin === location.origin;
2217
+ }
2218
+ catch {
2219
+ return false;
2220
+ }
2221
+ }
2222
+ // ── Global navigation interception ────────────────────────────────────
2223
+ let _interceptInstalled = false;
2224
+ /**
2225
+ * Install global listeners that intercept same-origin link clicks and
2226
+ * form submissions, routing them through soft navigation so the debugger
2227
+ * stays connected.
2228
+ *
2229
+ * Called once from HyphaDebugger.start().
2131
2230
  */
2231
+ function installNavigationInterceptor() {
2232
+ if (_interceptInstalled)
2233
+ return () => { };
2234
+ _interceptInstalled = true;
2235
+ /**
2236
+ * Click handler: intercept <a> clicks that would navigate to a
2237
+ * same-origin HTML page.
2238
+ */
2239
+ const onClick = (e) => {
2240
+ // Skip if modifier keys (new tab, etc.) or not left-click
2241
+ if (e.defaultPrevented || e.button !== 0)
2242
+ return;
2243
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
2244
+ return;
2245
+ // Walk up from target to find the nearest <a>
2246
+ let anchor = null;
2247
+ let el = e.target;
2248
+ while (el) {
2249
+ if (el.tagName === "A") {
2250
+ anchor = el;
2251
+ break;
2252
+ }
2253
+ el = el.parentElement;
2254
+ }
2255
+ if (!anchor)
2256
+ return;
2257
+ const href = anchor.href;
2258
+ if (!href)
2259
+ return;
2260
+ // Skip non-http(s), download links, target=_blank, javascript:, #hash-only
2261
+ if (anchor.target && anchor.target !== "_self")
2262
+ return;
2263
+ if (anchor.hasAttribute("download"))
2264
+ return;
2265
+ if (href.startsWith("javascript:") || href.startsWith("mailto:") || href.startsWith("tel:"))
2266
+ return;
2267
+ // Skip hash-only links (same page anchor)
2268
+ try {
2269
+ const target = new URL(href, location.href);
2270
+ if (target.origin === location.origin &&
2271
+ target.pathname === location.pathname &&
2272
+ target.search === location.search &&
2273
+ target.hash !== location.hash) {
2274
+ return; // Just a hash change, let browser handle it
2275
+ }
2276
+ }
2277
+ catch {
2278
+ return;
2279
+ }
2280
+ // Skip cross-origin
2281
+ if (!isSameOrigin(href))
2282
+ return;
2283
+ // Intercept: prevent default navigation and do soft replace
2284
+ e.preventDefault();
2285
+ const targetUrl = new URL(href, location.href).href;
2286
+ softReplace(targetUrl, targetUrl);
2287
+ };
2288
+ /**
2289
+ * Submit handler: intercept form submissions to same-origin action URLs.
2290
+ * Only handles GET forms (POST forms need the request body which is harder
2291
+ * to replicate via fetch).
2292
+ */
2293
+ const onSubmit = (e) => {
2294
+ if (e.defaultPrevented)
2295
+ return;
2296
+ const form = e.target;
2297
+ const method = (form.method || "GET").toUpperCase();
2298
+ // Only intercept GET forms — POST forms are too complex to replicate
2299
+ if (method !== "GET")
2300
+ return;
2301
+ const action = form.action || location.href;
2302
+ if (!isSameOrigin(action))
2303
+ return;
2304
+ // Build the URL with form data as query params
2305
+ const formData = new FormData(form);
2306
+ const url = new URL(action, location.href);
2307
+ for (const [key, value] of formData.entries()) {
2308
+ if (typeof value === "string") {
2309
+ url.searchParams.set(key, value);
2310
+ }
2311
+ }
2312
+ // Skip if target is _blank or similar
2313
+ if (form.target && form.target !== "_self")
2314
+ return;
2315
+ e.preventDefault();
2316
+ softReplace(url.href, url.href);
2317
+ };
2318
+ /**
2319
+ * Popstate handler: intercept browser back/forward (bfcache miss).
2320
+ * When the browser navigates via back/forward and there's no bfcache,
2321
+ * we can catch it via popstate and do a soft load of the target URL.
2322
+ */
2323
+ const onPopState = () => {
2324
+ // The URL has already changed when popstate fires.
2325
+ // Do a soft load of the current URL (which is the target of back/forward).
2326
+ softReplace(location.href);
2327
+ };
2328
+ document.addEventListener("click", onClick, true); // capture phase
2329
+ document.addEventListener("submit", onSubmit, true);
2330
+ window.addEventListener("popstate", onPopState);
2331
+ // Return cleanup function
2332
+ return () => {
2333
+ document.removeEventListener("click", onClick, true);
2334
+ document.removeEventListener("submit", onSubmit, true);
2335
+ window.removeEventListener("popstate", onPopState);
2336
+ _interceptInstalled = false;
2337
+ };
2338
+ }
2339
+ // ── navigate ──────────────────────────────────────────────────────────
2132
2340
  function navigate(url) {
2133
2341
  try {
2134
- window.location.href = url;
2135
- return { success: true, message: `Navigating to ${url}` };
2342
+ const targetUrl = new URL(url, location.href);
2343
+ const sameOrigin = targetUrl.origin === location.origin;
2344
+ if (sameOrigin) {
2345
+ // Soft navigate: fetch + inject + document.write, then pushState
2346
+ // Schedule after RPC response is sent
2347
+ setTimeout(() => softReplace(targetUrl.href, targetUrl.href), 150);
2348
+ return {
2349
+ success: true,
2350
+ message: `Navigating to ${url} (debugger will auto-reconnect)`,
2351
+ };
2352
+ }
2353
+ else {
2354
+ // Cross-origin: can't soft navigate, fall back to hard
2355
+ window.location.href = url;
2356
+ return {
2357
+ success: true,
2358
+ message: `Navigating to ${url} (cross-origin, debugger will disconnect)`,
2359
+ };
2360
+ }
2136
2361
  }
2137
2362
  catch (err) {
2138
- return { success: false, message: `Navigation failed: ${err.message ?? err}` };
2363
+ return {
2364
+ success: false,
2365
+ message: `Navigation failed: ${err.message ?? err}`,
2366
+ };
2139
2367
  }
2140
2368
  }
2141
2369
  navigate.__schema__ = {
2142
2370
  name: "navigate",
2143
- description: "Navigate the browser to a new URL.",
2371
+ description: "Navigate the browser to a new URL. For same-origin URLs, the debugger auto-reconnects. Cross-origin navigation will disconnect the debugger.",
2144
2372
  parameters: {
2145
2373
  type: "object",
2146
2374
  properties: {
@@ -2152,18 +2380,25 @@ navigate.__schema__ = {
2152
2380
  required: ["url"],
2153
2381
  },
2154
2382
  };
2383
+ // ── goBack / goForward ───────────────────────────────────────────────
2155
2384
  function goBack() {
2156
2385
  try {
2157
2386
  window.history.back();
2158
- return { success: true, message: "Navigated back" };
2387
+ return {
2388
+ success: true,
2389
+ message: "Navigated back (debugger will auto-reconnect via popstate)",
2390
+ };
2159
2391
  }
2160
2392
  catch (err) {
2161
- return { success: false, message: `Back navigation failed: ${err.message ?? err}` };
2393
+ return {
2394
+ success: false,
2395
+ message: `Back navigation failed: ${err.message ?? err}`,
2396
+ };
2162
2397
  }
2163
2398
  }
2164
2399
  goBack.__schema__ = {
2165
2400
  name: "goBack",
2166
- description: "Navigate back in browser history.",
2401
+ description: "Navigate back in browser history. The debugger auto-reconnects for same-origin pages.",
2167
2402
  parameters: {
2168
2403
  type: "object",
2169
2404
  properties: {},
@@ -2172,7 +2407,10 @@ goBack.__schema__ = {
2172
2407
  function goForward() {
2173
2408
  try {
2174
2409
  window.history.forward();
2175
- return { success: true, message: "Navigated forward" };
2410
+ return {
2411
+ success: true,
2412
+ message: "Navigated forward (debugger will auto-reconnect via popstate)",
2413
+ };
2176
2414
  }
2177
2415
  catch (err) {
2178
2416
  return {
@@ -2183,16 +2421,21 @@ function goForward() {
2183
2421
  }
2184
2422
  goForward.__schema__ = {
2185
2423
  name: "goForward",
2186
- description: "Navigate forward in browser history.",
2424
+ description: "Navigate forward in browser history. The debugger auto-reconnects for same-origin pages.",
2187
2425
  parameters: {
2188
2426
  type: "object",
2189
2427
  properties: {},
2190
2428
  },
2191
2429
  };
2430
+ // ── reload ───────────────────────────────────────────────────────────
2192
2431
  function reload() {
2193
2432
  try {
2194
- window.location.reload();
2195
- return { success: true, message: "Reloading page" };
2433
+ // Schedule soft reload after RPC response is sent
2434
+ setTimeout(() => softReplace(location.href), 150);
2435
+ return {
2436
+ success: true,
2437
+ message: "Reloading page (debugger will auto-reconnect)",
2438
+ };
2196
2439
  }
2197
2440
  catch (err) {
2198
2441
  return { success: false, message: `Reload failed: ${err.message ?? err}` };
@@ -2200,7 +2443,7 @@ function reload() {
2200
2443
  }
2201
2444
  reload.__schema__ = {
2202
2445
  name: "reload",
2203
- description: "Reload the current page.",
2446
+ description: "Reload the current page. The debugger auto-reconnects after reload using soft page replacement.",
2204
2447
  parameters: {
2205
2448
  type: "object",
2206
2449
  properties: {},
@@ -5311,12 +5554,16 @@ function randomHex(bytes = 8) {
5311
5554
  crypto.getRandomValues(arr);
5312
5555
  return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
5313
5556
  }
5557
+ /** sessionStorage key for persisting debugger config across reloads. */
5558
+ const STORAGE_KEY = "__hypha_debugger_config__";
5314
5559
  class HyphaDebugger {
5315
5560
  constructor(config) {
5316
5561
  this.overlay = null;
5317
5562
  this.cursor = null;
5318
5563
  this.server = null;
5319
5564
  this.serviceInfo = null;
5565
+ this.boundBeforeUnload = null;
5566
+ this.cleanupInterceptor = null;
5320
5567
  const requireToken = config.require_token ?? false;
5321
5568
  // Always append random suffix unless user provided a custom id.
5322
5569
  let serviceId = config.service_id ?? "web-debugger";
@@ -5376,6 +5623,13 @@ class HyphaDebugger {
5376
5623
  // Store globally
5377
5624
  w.__HYPHA_DEBUGGER__ = w.__HYPHA_DEBUGGER__ ?? {};
5378
5625
  w.__HYPHA_DEBUGGER__.instance = this;
5626
+ // Persist config to sessionStorage for auto-reconnect after reload
5627
+ this.saveConfigToStorage();
5628
+ this.boundBeforeUnload = () => this.saveConfigToStorage();
5629
+ window.addEventListener("beforeunload", this.boundBeforeUnload);
5630
+ // Intercept same-origin link clicks, form submits, and popstate
5631
+ // so the debugger survives user-initiated navigation
5632
+ this.cleanupInterceptor = installNavigationInterceptor();
5379
5633
  return session;
5380
5634
  }
5381
5635
  catch (err) {
@@ -5391,6 +5645,15 @@ class HyphaDebugger {
5391
5645
  }
5392
5646
  }
5393
5647
  async destroy() {
5648
+ // Remove event listeners
5649
+ if (this.boundBeforeUnload) {
5650
+ window.removeEventListener("beforeunload", this.boundBeforeUnload);
5651
+ this.boundBeforeUnload = null;
5652
+ }
5653
+ if (this.cleanupInterceptor) {
5654
+ this.cleanupInterceptor();
5655
+ this.cleanupInterceptor = null;
5656
+ }
5394
5657
  try {
5395
5658
  if (this.serviceInfo && this.server) {
5396
5659
  await this.server.unregisterService(this.serviceInfo.id);
@@ -5399,6 +5662,13 @@ class HyphaDebugger {
5399
5662
  catch {
5400
5663
  // Ignore unregister errors on cleanup
5401
5664
  }
5665
+ // Clear sessionStorage config (explicit destroy = user wants to stop)
5666
+ try {
5667
+ sessionStorage.removeItem(STORAGE_KEY);
5668
+ }
5669
+ catch {
5670
+ // ignore
5671
+ }
5402
5672
  disposeController();
5403
5673
  this.cursor?.destroy();
5404
5674
  this.cursor = null;
@@ -5410,6 +5680,48 @@ class HyphaDebugger {
5410
5680
  delete w.__HYPHA_DEBUGGER__.session;
5411
5681
  }
5412
5682
  }
5683
+ /**
5684
+ * Persist debugger config to sessionStorage so the debugger can
5685
+ * auto-reconnect after a page reload (soft reload injects the script,
5686
+ * autoStart() reads this config).
5687
+ */
5688
+ saveConfigToStorage() {
5689
+ try {
5690
+ const data = {
5691
+ server_url: this.config.server_url,
5692
+ workspace: this.server?.config?.workspace ?? this.config.workspace,
5693
+ token: this.config.token,
5694
+ service_id: this.config.service_id,
5695
+ service_name: this.config.service_name,
5696
+ show_ui: this.config.show_ui,
5697
+ visibility: this.config.visibility,
5698
+ require_token: this.config.require_token,
5699
+ script_url: this.detectScriptUrl(),
5700
+ };
5701
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data));
5702
+ }
5703
+ catch {
5704
+ // sessionStorage might be unavailable (private browsing, full quota)
5705
+ }
5706
+ }
5707
+ /**
5708
+ * Detect the URL of the currently loaded hypha-debugger script.
5709
+ * Used by navigate.ts to inject the correct script after soft reload.
5710
+ */
5711
+ detectScriptUrl() {
5712
+ try {
5713
+ const scripts = document.querySelectorAll("script[src]");
5714
+ for (const s of Array.from(scripts)) {
5715
+ if (s.src && s.src.includes("hypha-debugger")) {
5716
+ return s.src;
5717
+ }
5718
+ }
5719
+ }
5720
+ catch {
5721
+ // ignore
5722
+ }
5723
+ return "https://cdn.jsdelivr.net/npm/hypha-debugger/dist/hypha-debugger.min.js";
5724
+ }
5413
5725
  /**
5414
5726
  * Generate token, build service URL, update overlay instructions, and
5415
5727
  * return a DebugSession.
@@ -5665,6 +5977,26 @@ async function startDebugger(config) {
5665
5977
  function autoStart() {
5666
5978
  if (typeof window === "undefined" || typeof document === "undefined")
5667
5979
  return;
5980
+ // Skip if already started
5981
+ if (window.__HYPHA_DEBUGGER__?.instance)
5982
+ return;
5983
+ // Check sessionStorage for saved config (auto-reconnect after soft reload)
5984
+ try {
5985
+ const saved = sessionStorage.getItem("__hypha_debugger_config__");
5986
+ if (saved) {
5987
+ const savedConfig = JSON.parse(saved);
5988
+ if (savedConfig.server_url) {
5989
+ console.log("[hypha-debugger] Reconnecting from saved session...");
5990
+ startDebugger(savedConfig).catch((err) => {
5991
+ console.error("[hypha-debugger] Auto-reconnect failed:", err);
5992
+ });
5993
+ return;
5994
+ }
5995
+ }
5996
+ }
5997
+ catch {
5998
+ // sessionStorage not available or parse error — continue to script tag detection
5999
+ }
5668
6000
  // Find our own script tag
5669
6001
  const scripts = document.querySelectorAll("script[src]");
5670
6002
  let scriptEl = null;
@@ -5677,9 +6009,6 @@ function autoStart() {
5677
6009
  // Skip if data-manual is set
5678
6010
  if (scriptEl?.hasAttribute("data-manual"))
5679
6011
  return;
5680
- // Skip if already started
5681
- if (window.__HYPHA_DEBUGGER__?.instance)
5682
- return;
5683
6012
  const serverUrl = scriptEl?.getAttribute("data-server-url") ?? "https://hypha.aicell.io";
5684
6013
  const config = {
5685
6014
  server_url: serverUrl,
@@ -5713,5 +6042,5 @@ if (typeof window !== "undefined") {
5713
6042
  }
5714
6043
  }
5715
6044
 
5716
- export { AICursor, HyphaDebugger, PageController, clickElement$1 as clickElement, clickElementByIndex, disposeController, executeScript, fillInput, generateSkillMd, getBrowserState, getComputedStyles, getConsoleLogs, getElementBounds, getHtml, getPageInfo, getReactTree, goBack, goForward, inputText, installConsoleCapture, navigate, queryDom, reload, removeHighlights, scroll, scrollTo, selectOption, startDebugger, takeScreenshot, wrapFn };
6045
+ export { AICursor, HyphaDebugger, PageController, clickElement$1 as clickElement, clickElementByIndex, disposeController, executeScript, fillInput, generateSkillMd, getBrowserState, getComputedStyles, getConsoleLogs, getElementBounds, getHtml, getPageInfo, getReactTree, goBack, goForward, inputText, installConsoleCapture, installNavigationInterceptor, navigate, queryDom, reload, removeHighlights, scroll, scrollTo, selectOption, softReplace, startDebugger, takeScreenshot, wrapFn };
5717
6046
  //# sourceMappingURL=hypha-debugger.mjs.map