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.
@@ -45,9 +45,22 @@ export declare class HyphaDebugger {
45
45
  private cursor;
46
46
  private server;
47
47
  private serviceInfo;
48
+ private boundBeforeUnload;
49
+ private cleanupInterceptor;
48
50
  constructor(config: DebuggerConfig);
49
51
  start(): Promise<DebugSession>;
50
52
  destroy(): Promise<void>;
53
+ /**
54
+ * Persist debugger config to sessionStorage so the debugger can
55
+ * auto-reconnect after a page reload (soft reload injects the script,
56
+ * autoStart() reads this config).
57
+ */
58
+ private saveConfigToStorage;
59
+ /**
60
+ * Detect the URL of the currently loaded hypha-debugger script.
61
+ * Used by navigate.ts to inject the correct script after soft reload.
62
+ */
63
+ private detectScriptUrl;
51
64
  /**
52
65
  * Generate token, build service URL, update overlay instructions, and
53
66
  * return a DebugSession.
@@ -10584,20 +10584,248 @@
10584
10584
  };
10585
10585
 
10586
10586
  /**
10587
- * Navigation service.
10587
+ * Navigation service with auto-reconnect support.
10588
+ *
10589
+ * For agent-triggered reload() and same-origin navigate(), we use a "soft"
10590
+ * approach: fetch the target HTML, inject the debugger <script> tag, then
10591
+ * replace the document via document.write(). The injected script auto-starts
10592
+ * from sessionStorage config, so the debugger reconnects with the same
10593
+ * workspace and service ID. The agent's URL stays stable.
10594
+ *
10595
+ * For cross-origin navigate(), goBack(), and goForward(), we fall back to
10596
+ * normal navigation. The debugger config is saved to sessionStorage so
10597
+ * re-clicking the bookmarklet reconnects seamlessly.
10598
+ */
10599
+ const STORAGE_KEY$1 = "__hypha_debugger_config__";
10600
+ /** Read the saved script URL from sessionStorage, or fall back to CDN. */
10601
+ function getScriptUrl() {
10602
+ try {
10603
+ const raw = sessionStorage.getItem(STORAGE_KEY$1);
10604
+ if (raw) {
10605
+ const config = JSON.parse(raw);
10606
+ if (config.script_url)
10607
+ return config.script_url;
10608
+ }
10609
+ }
10610
+ catch {
10611
+ // ignore
10612
+ }
10613
+ return "https://cdn.jsdelivr.net/npm/hypha-debugger/dist/hypha-debugger.min.js";
10614
+ }
10615
+ /**
10616
+ * Inject the debugger loader script into HTML before </body> (or append).
10617
+ */
10618
+ function injectLoader(html, scriptUrl) {
10619
+ const loader = `<script src="${scriptUrl}"><\/script>`;
10620
+ if (html.includes("</body>")) {
10621
+ return html.replace("</body>", loader + "\n</body>");
10622
+ }
10623
+ if (html.includes("</html>")) {
10624
+ return html.replace("</html>", loader + "\n</html>");
10625
+ }
10626
+ return html + "\n" + loader;
10627
+ }
10628
+ /**
10629
+ * Perform a soft page replacement: fetch HTML, inject debugger script,
10630
+ * replace the document via document.write(). If the fetch or write fails,
10631
+ * falls back to hard navigation.
10632
+ */
10633
+ function softReplace(url, pushState) {
10634
+ const scriptUrl = getScriptUrl();
10635
+ fetch(url, { credentials: "same-origin", cache: "reload" })
10636
+ .then((response) => {
10637
+ if (!response.ok)
10638
+ throw new Error(`HTTP ${response.status}`);
10639
+ const contentType = response.headers.get("content-type") ?? "";
10640
+ if (!contentType.includes("text/html")) {
10641
+ throw new Error("Not HTML");
10642
+ }
10643
+ return response.text();
10644
+ })
10645
+ .then((html) => {
10646
+ const modified = injectLoader(html, scriptUrl);
10647
+ document.open();
10648
+ document.write(modified);
10649
+ document.close();
10650
+ if (pushState) {
10651
+ try {
10652
+ history.pushState({}, "", pushState);
10653
+ }
10654
+ catch {
10655
+ // ignore — URL might already match
10656
+ }
10657
+ }
10658
+ })
10659
+ .catch(() => {
10660
+ // Soft replace failed — fall back to hard navigation
10661
+ if (pushState) {
10662
+ window.location.href = pushState;
10663
+ }
10664
+ else {
10665
+ window.location.reload();
10666
+ }
10667
+ });
10668
+ }
10669
+ /** Check if a URL is same-origin as the current page. */
10670
+ function isSameOrigin(url) {
10671
+ try {
10672
+ const target = new URL(url, location.href);
10673
+ return target.origin === location.origin;
10674
+ }
10675
+ catch {
10676
+ return false;
10677
+ }
10678
+ }
10679
+ // ── Global navigation interception ────────────────────────────────────
10680
+ let _interceptInstalled = false;
10681
+ /**
10682
+ * Install global listeners that intercept same-origin link clicks and
10683
+ * form submissions, routing them through soft navigation so the debugger
10684
+ * stays connected.
10685
+ *
10686
+ * Called once from HyphaDebugger.start().
10588
10687
  */
10688
+ function installNavigationInterceptor() {
10689
+ if (_interceptInstalled)
10690
+ return () => { };
10691
+ _interceptInstalled = true;
10692
+ /**
10693
+ * Click handler: intercept <a> clicks that would navigate to a
10694
+ * same-origin HTML page.
10695
+ */
10696
+ const onClick = (e) => {
10697
+ // Skip if modifier keys (new tab, etc.) or not left-click
10698
+ if (e.defaultPrevented || e.button !== 0)
10699
+ return;
10700
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
10701
+ return;
10702
+ // Walk up from target to find the nearest <a>
10703
+ let anchor = null;
10704
+ let el = e.target;
10705
+ while (el) {
10706
+ if (el.tagName === "A") {
10707
+ anchor = el;
10708
+ break;
10709
+ }
10710
+ el = el.parentElement;
10711
+ }
10712
+ if (!anchor)
10713
+ return;
10714
+ const href = anchor.href;
10715
+ if (!href)
10716
+ return;
10717
+ // Skip non-http(s), download links, target=_blank, javascript:, #hash-only
10718
+ if (anchor.target && anchor.target !== "_self")
10719
+ return;
10720
+ if (anchor.hasAttribute("download"))
10721
+ return;
10722
+ if (href.startsWith("javascript:") || href.startsWith("mailto:") || href.startsWith("tel:"))
10723
+ return;
10724
+ // Skip hash-only links (same page anchor)
10725
+ try {
10726
+ const target = new URL(href, location.href);
10727
+ if (target.origin === location.origin &&
10728
+ target.pathname === location.pathname &&
10729
+ target.search === location.search &&
10730
+ target.hash !== location.hash) {
10731
+ return; // Just a hash change, let browser handle it
10732
+ }
10733
+ }
10734
+ catch {
10735
+ return;
10736
+ }
10737
+ // Skip cross-origin
10738
+ if (!isSameOrigin(href))
10739
+ return;
10740
+ // Intercept: prevent default navigation and do soft replace
10741
+ e.preventDefault();
10742
+ const targetUrl = new URL(href, location.href).href;
10743
+ softReplace(targetUrl, targetUrl);
10744
+ };
10745
+ /**
10746
+ * Submit handler: intercept form submissions to same-origin action URLs.
10747
+ * Only handles GET forms (POST forms need the request body which is harder
10748
+ * to replicate via fetch).
10749
+ */
10750
+ const onSubmit = (e) => {
10751
+ if (e.defaultPrevented)
10752
+ return;
10753
+ const form = e.target;
10754
+ const method = (form.method || "GET").toUpperCase();
10755
+ // Only intercept GET forms — POST forms are too complex to replicate
10756
+ if (method !== "GET")
10757
+ return;
10758
+ const action = form.action || location.href;
10759
+ if (!isSameOrigin(action))
10760
+ return;
10761
+ // Build the URL with form data as query params
10762
+ const formData = new FormData(form);
10763
+ const url = new URL(action, location.href);
10764
+ for (const [key, value] of formData.entries()) {
10765
+ if (typeof value === "string") {
10766
+ url.searchParams.set(key, value);
10767
+ }
10768
+ }
10769
+ // Skip if target is _blank or similar
10770
+ if (form.target && form.target !== "_self")
10771
+ return;
10772
+ e.preventDefault();
10773
+ softReplace(url.href, url.href);
10774
+ };
10775
+ /**
10776
+ * Popstate handler: intercept browser back/forward (bfcache miss).
10777
+ * When the browser navigates via back/forward and there's no bfcache,
10778
+ * we can catch it via popstate and do a soft load of the target URL.
10779
+ */
10780
+ const onPopState = () => {
10781
+ // The URL has already changed when popstate fires.
10782
+ // Do a soft load of the current URL (which is the target of back/forward).
10783
+ softReplace(location.href);
10784
+ };
10785
+ document.addEventListener("click", onClick, true); // capture phase
10786
+ document.addEventListener("submit", onSubmit, true);
10787
+ window.addEventListener("popstate", onPopState);
10788
+ // Return cleanup function
10789
+ return () => {
10790
+ document.removeEventListener("click", onClick, true);
10791
+ document.removeEventListener("submit", onSubmit, true);
10792
+ window.removeEventListener("popstate", onPopState);
10793
+ _interceptInstalled = false;
10794
+ };
10795
+ }
10796
+ // ── navigate ──────────────────────────────────────────────────────────
10589
10797
  function navigate(url) {
10590
10798
  try {
10591
- window.location.href = url;
10592
- return { success: true, message: `Navigating to ${url}` };
10799
+ const targetUrl = new URL(url, location.href);
10800
+ const sameOrigin = targetUrl.origin === location.origin;
10801
+ if (sameOrigin) {
10802
+ // Soft navigate: fetch + inject + document.write, then pushState
10803
+ // Schedule after RPC response is sent
10804
+ setTimeout(() => softReplace(targetUrl.href, targetUrl.href), 150);
10805
+ return {
10806
+ success: true,
10807
+ message: `Navigating to ${url} (debugger will auto-reconnect)`,
10808
+ };
10809
+ }
10810
+ else {
10811
+ // Cross-origin: can't soft navigate, fall back to hard
10812
+ window.location.href = url;
10813
+ return {
10814
+ success: true,
10815
+ message: `Navigating to ${url} (cross-origin, debugger will disconnect)`,
10816
+ };
10817
+ }
10593
10818
  }
10594
10819
  catch (err) {
10595
- return { success: false, message: `Navigation failed: ${err.message ?? err}` };
10820
+ return {
10821
+ success: false,
10822
+ message: `Navigation failed: ${err.message ?? err}`,
10823
+ };
10596
10824
  }
10597
10825
  }
10598
10826
  navigate.__schema__ = {
10599
10827
  name: "navigate",
10600
- description: "Navigate the browser to a new URL.",
10828
+ description: "Navigate the browser to a new URL. For same-origin URLs, the debugger auto-reconnects. Cross-origin navigation will disconnect the debugger.",
10601
10829
  parameters: {
10602
10830
  type: "object",
10603
10831
  properties: {
@@ -10609,18 +10837,25 @@
10609
10837
  required: ["url"],
10610
10838
  },
10611
10839
  };
10840
+ // ── goBack / goForward ───────────────────────────────────────────────
10612
10841
  function goBack() {
10613
10842
  try {
10614
10843
  window.history.back();
10615
- return { success: true, message: "Navigated back" };
10844
+ return {
10845
+ success: true,
10846
+ message: "Navigated back (debugger will auto-reconnect via popstate)",
10847
+ };
10616
10848
  }
10617
10849
  catch (err) {
10618
- return { success: false, message: `Back navigation failed: ${err.message ?? err}` };
10850
+ return {
10851
+ success: false,
10852
+ message: `Back navigation failed: ${err.message ?? err}`,
10853
+ };
10619
10854
  }
10620
10855
  }
10621
10856
  goBack.__schema__ = {
10622
10857
  name: "goBack",
10623
- description: "Navigate back in browser history.",
10858
+ description: "Navigate back in browser history. The debugger auto-reconnects for same-origin pages.",
10624
10859
  parameters: {
10625
10860
  type: "object",
10626
10861
  properties: {},
@@ -10629,7 +10864,10 @@
10629
10864
  function goForward() {
10630
10865
  try {
10631
10866
  window.history.forward();
10632
- return { success: true, message: "Navigated forward" };
10867
+ return {
10868
+ success: true,
10869
+ message: "Navigated forward (debugger will auto-reconnect via popstate)",
10870
+ };
10633
10871
  }
10634
10872
  catch (err) {
10635
10873
  return {
@@ -10640,16 +10878,21 @@
10640
10878
  }
10641
10879
  goForward.__schema__ = {
10642
10880
  name: "goForward",
10643
- description: "Navigate forward in browser history.",
10881
+ description: "Navigate forward in browser history. The debugger auto-reconnects for same-origin pages.",
10644
10882
  parameters: {
10645
10883
  type: "object",
10646
10884
  properties: {},
10647
10885
  },
10648
10886
  };
10887
+ // ── reload ───────────────────────────────────────────────────────────
10649
10888
  function reload() {
10650
10889
  try {
10651
- window.location.reload();
10652
- return { success: true, message: "Reloading page" };
10890
+ // Schedule soft reload after RPC response is sent
10891
+ setTimeout(() => softReplace(location.href), 150);
10892
+ return {
10893
+ success: true,
10894
+ message: "Reloading page (debugger will auto-reconnect)",
10895
+ };
10653
10896
  }
10654
10897
  catch (err) {
10655
10898
  return { success: false, message: `Reload failed: ${err.message ?? err}` };
@@ -10657,7 +10900,7 @@
10657
10900
  }
10658
10901
  reload.__schema__ = {
10659
10902
  name: "reload",
10660
- description: "Reload the current page.",
10903
+ description: "Reload the current page. The debugger auto-reconnects after reload using soft page replacement.",
10661
10904
  parameters: {
10662
10905
  type: "object",
10663
10906
  properties: {},
@@ -13768,12 +14011,16 @@
13768
14011
  crypto.getRandomValues(arr);
13769
14012
  return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
13770
14013
  }
14014
+ /** sessionStorage key for persisting debugger config across reloads. */
14015
+ const STORAGE_KEY = "__hypha_debugger_config__";
13771
14016
  class HyphaDebugger {
13772
14017
  constructor(config) {
13773
14018
  this.overlay = null;
13774
14019
  this.cursor = null;
13775
14020
  this.server = null;
13776
14021
  this.serviceInfo = null;
14022
+ this.boundBeforeUnload = null;
14023
+ this.cleanupInterceptor = null;
13777
14024
  const requireToken = config.require_token ?? false;
13778
14025
  // Always append random suffix unless user provided a custom id.
13779
14026
  let serviceId = config.service_id ?? "web-debugger";
@@ -13833,6 +14080,13 @@
13833
14080
  // Store globally
13834
14081
  w.__HYPHA_DEBUGGER__ = w.__HYPHA_DEBUGGER__ ?? {};
13835
14082
  w.__HYPHA_DEBUGGER__.instance = this;
14083
+ // Persist config to sessionStorage for auto-reconnect after reload
14084
+ this.saveConfigToStorage();
14085
+ this.boundBeforeUnload = () => this.saveConfigToStorage();
14086
+ window.addEventListener("beforeunload", this.boundBeforeUnload);
14087
+ // Intercept same-origin link clicks, form submits, and popstate
14088
+ // so the debugger survives user-initiated navigation
14089
+ this.cleanupInterceptor = installNavigationInterceptor();
13836
14090
  return session;
13837
14091
  }
13838
14092
  catch (err) {
@@ -13848,6 +14102,15 @@
13848
14102
  }
13849
14103
  }
13850
14104
  async destroy() {
14105
+ // Remove event listeners
14106
+ if (this.boundBeforeUnload) {
14107
+ window.removeEventListener("beforeunload", this.boundBeforeUnload);
14108
+ this.boundBeforeUnload = null;
14109
+ }
14110
+ if (this.cleanupInterceptor) {
14111
+ this.cleanupInterceptor();
14112
+ this.cleanupInterceptor = null;
14113
+ }
13851
14114
  try {
13852
14115
  if (this.serviceInfo && this.server) {
13853
14116
  await this.server.unregisterService(this.serviceInfo.id);
@@ -13856,6 +14119,13 @@
13856
14119
  catch {
13857
14120
  // Ignore unregister errors on cleanup
13858
14121
  }
14122
+ // Clear sessionStorage config (explicit destroy = user wants to stop)
14123
+ try {
14124
+ sessionStorage.removeItem(STORAGE_KEY);
14125
+ }
14126
+ catch {
14127
+ // ignore
14128
+ }
13859
14129
  disposeController();
13860
14130
  this.cursor?.destroy();
13861
14131
  this.cursor = null;
@@ -13867,6 +14137,48 @@
13867
14137
  delete w.__HYPHA_DEBUGGER__.session;
13868
14138
  }
13869
14139
  }
14140
+ /**
14141
+ * Persist debugger config to sessionStorage so the debugger can
14142
+ * auto-reconnect after a page reload (soft reload injects the script,
14143
+ * autoStart() reads this config).
14144
+ */
14145
+ saveConfigToStorage() {
14146
+ try {
14147
+ const data = {
14148
+ server_url: this.config.server_url,
14149
+ workspace: this.server?.config?.workspace ?? this.config.workspace,
14150
+ token: this.config.token,
14151
+ service_id: this.config.service_id,
14152
+ service_name: this.config.service_name,
14153
+ show_ui: this.config.show_ui,
14154
+ visibility: this.config.visibility,
14155
+ require_token: this.config.require_token,
14156
+ script_url: this.detectScriptUrl(),
14157
+ };
14158
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data));
14159
+ }
14160
+ catch {
14161
+ // sessionStorage might be unavailable (private browsing, full quota)
14162
+ }
14163
+ }
14164
+ /**
14165
+ * Detect the URL of the currently loaded hypha-debugger script.
14166
+ * Used by navigate.ts to inject the correct script after soft reload.
14167
+ */
14168
+ detectScriptUrl() {
14169
+ try {
14170
+ const scripts = document.querySelectorAll("script[src]");
14171
+ for (const s of Array.from(scripts)) {
14172
+ if (s.src && s.src.includes("hypha-debugger")) {
14173
+ return s.src;
14174
+ }
14175
+ }
14176
+ }
14177
+ catch {
14178
+ // ignore
14179
+ }
14180
+ return "https://cdn.jsdelivr.net/npm/hypha-debugger/dist/hypha-debugger.min.js";
14181
+ }
13870
14182
  /**
13871
14183
  * Generate token, build service URL, update overlay instructions, and
13872
14184
  * return a DebugSession.
@@ -14122,6 +14434,26 @@
14122
14434
  function autoStart() {
14123
14435
  if (typeof window === "undefined" || typeof document === "undefined")
14124
14436
  return;
14437
+ // Skip if already started
14438
+ if (window.__HYPHA_DEBUGGER__?.instance)
14439
+ return;
14440
+ // Check sessionStorage for saved config (auto-reconnect after soft reload)
14441
+ try {
14442
+ const saved = sessionStorage.getItem("__hypha_debugger_config__");
14443
+ if (saved) {
14444
+ const savedConfig = JSON.parse(saved);
14445
+ if (savedConfig.server_url) {
14446
+ console.log("[hypha-debugger] Reconnecting from saved session...");
14447
+ startDebugger(savedConfig).catch((err) => {
14448
+ console.error("[hypha-debugger] Auto-reconnect failed:", err);
14449
+ });
14450
+ return;
14451
+ }
14452
+ }
14453
+ }
14454
+ catch {
14455
+ // sessionStorage not available or parse error — continue to script tag detection
14456
+ }
14125
14457
  // Find our own script tag
14126
14458
  const scripts = document.querySelectorAll("script[src]");
14127
14459
  let scriptEl = null;
@@ -14134,9 +14466,6 @@
14134
14466
  // Skip if data-manual is set
14135
14467
  if (scriptEl?.hasAttribute("data-manual"))
14136
14468
  return;
14137
- // Skip if already started
14138
- if (window.__HYPHA_DEBUGGER__?.instance)
14139
- return;
14140
14469
  const serverUrl = scriptEl?.getAttribute("data-server-url") ?? "https://hypha.aicell.io";
14141
14470
  const config = {
14142
14471
  server_url: serverUrl,
@@ -14190,6 +14519,7 @@
14190
14519
  exports.goForward = goForward;
14191
14520
  exports.inputText = inputText;
14192
14521
  exports.installConsoleCapture = installConsoleCapture;
14522
+ exports.installNavigationInterceptor = installNavigationInterceptor;
14193
14523
  exports.navigate = navigate;
14194
14524
  exports.queryDom = queryDom;
14195
14525
  exports.reload = reload;
@@ -14197,6 +14527,7 @@
14197
14527
  exports.scroll = scroll;
14198
14528
  exports.scrollTo = scrollTo;
14199
14529
  exports.selectOption = selectOption;
14530
+ exports.softReplace = softReplace;
14200
14531
  exports.startDebugger = startDebugger;
14201
14532
  exports.takeScreenshot = takeScreenshot;
14202
14533
  exports.wrapFn = wrapFn;