agentic-browser 2.4.0 → 2.5.1

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as createCliRuntime } from "../runtime-PnWvjdzU.mjs";
2
+ import { r as createCliRuntime } from "../runtime-BeD8ZcjH.mjs";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import crypto from "node:crypto";
@@ -242,7 +242,7 @@ async function runCommand(runtime, input) {
242
242
  payload: input.payload
243
243
  });
244
244
  } catch (error) {
245
- throw new Error(`Command rejected: ${error.message}`);
245
+ throw new Error(`Command rejected: ${error.message}`, { cause: error });
246
246
  }
247
247
  }
248
248
  //#endregion
@@ -279,11 +279,11 @@ async function runMemoryStats(runtime) {
279
279
  }
280
280
  //#endregion
281
281
  //#region src/cli/index.ts
282
+ const collect = (value, previous = []) => [...previous, value];
282
283
  async function main() {
283
284
  const runtime = createCliRuntime();
284
285
  const program = new Command();
285
286
  program.name("agentic-browser").description("Agentic browser CLI");
286
- const collect = (value, previous = []) => [...previous, value];
287
287
  program.command("session:start").option("--cdp-url <url>", "connect to existing Chrome via CDP endpoint URL").option("--user-profile <path>", "use 'default' for system Chrome profile or an absolute path").option("--headless", "run Chrome in headless mode (no visible window)").option("--user-agent <string>", "override the browser user-agent string").action(async (options) => {
288
288
  if (options.cdpUrl) runtime.context.config.cdpUrl = options.cdpUrl;
289
289
  if (options.userProfile) runtime.context.config.userProfileDir = options.userProfile === "true" || options.userProfile === "default" ? "default" : options.userProfile;
package/dist/index.d.mts CHANGED
@@ -99,6 +99,7 @@ interface BrowserController {
99
99
  getCurrentUrl(targetWsUrl: string): Promise<string>;
100
100
  terminate(pid: number): void;
101
101
  closeConnection?(targetWsUrl: string): void;
102
+ dispose?(): void;
102
103
  }
103
104
  //#endregion
104
105
  //#region src/memory/memory-schemas.d.ts
@@ -600,8 +601,7 @@ declare class SessionManager {
600
601
  //#region src/transport/control-api.d.ts
601
602
  declare class ControlApi {
602
603
  private readonly sessions;
603
- private readonly eventStore;
604
- constructor(sessions: SessionManager, eventStore: EventStore);
604
+ constructor(sessions: SessionManager);
605
605
  createSession(input: {
606
606
  browser: "chrome";
607
607
  }): Promise<{
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-PnWvjdzU.mjs";
2
+ import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-BeD8ZcjH.mjs";
3
3
  export { AgenticBrowserCore, createAgenticBrowserCore, createMockAgenticBrowserCore };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as createAgenticBrowserCore } from "../runtime-PnWvjdzU.mjs";
2
+ import { n as createAgenticBrowserCore } from "../runtime-BeD8ZcjH.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -155,7 +155,7 @@ function summarizeElements(elements) {
155
155
  });
156
156
  }
157
157
  return {
158
- countsByRole: Object.fromEntries([...byRole.entries()].sort((a, b) => b[1] - a[1])),
158
+ countsByRole: Object.fromEntries([...byRole.entries()].toSorted((a, b) => b[1] - a[1])),
159
159
  primaryActions
160
160
  };
161
161
  }
@@ -175,6 +175,10 @@ async function getJson(url, timeoutMs = 5e3) {
175
175
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);
176
176
  return await response.json();
177
177
  }
178
+ /** Debugger connection timing */
179
+ const DEBUGGER_READY_TIMEOUT_MS = 15e3;
180
+ const DEBUGGER_INITIAL_RETRY_DELAY_MS = 50;
181
+ const DEBUGGER_MAX_RETRY_DELAY_MS = 250;
178
182
  /** Check if the debug port is accepting TCP connections (faster than an HTTP fetch). */
179
183
  function probePort(port) {
180
184
  return new Promise((resolve) => {
@@ -193,16 +197,15 @@ function probePort(port) {
193
197
  });
194
198
  }
195
199
  async function waitForDebugger(port) {
196
- const maxMs = 15e3;
197
200
  const start = Date.now();
198
- let delay = 50;
199
- while (Date.now() - start < maxMs) {
201
+ let delay = DEBUGGER_INITIAL_RETRY_DELAY_MS;
202
+ while (Date.now() - start < DEBUGGER_READY_TIMEOUT_MS) {
200
203
  if (await probePort(port)) try {
201
204
  await getJson(`http://127.0.0.1:${port}/json/version`);
202
205
  return;
203
206
  } catch {}
204
207
  await new Promise((resolve) => setTimeout(resolve, delay));
205
- delay = Math.min(delay * 2, 250);
208
+ delay = Math.min(delay * 2, DEBUGGER_MAX_RETRY_DELAY_MS);
206
209
  }
207
210
  throw new Error("Chrome debug endpoint did not become ready in time");
208
211
  }
@@ -694,6 +697,9 @@ function buildKeyDispatchPayload(inputKey) {
694
697
  nativeVirtualKeyCode: 0
695
698
  };
696
699
  }
700
+ /** Connection pool configuration */
701
+ const CONNECTION_IDLE_TIMEOUT_MS = 300 * 1e3;
702
+ const CONNECTION_CLEANUP_INTERVAL_MS = 6e4;
697
703
  var ChromeCdpBrowserController = class {
698
704
  connections = /* @__PURE__ */ new Map();
699
705
  cleanupInterval;
@@ -701,11 +707,16 @@ var ChromeCdpBrowserController = class {
701
707
  this.baseDir = baseDir;
702
708
  this.connectionFactory = connectionFactory;
703
709
  this.cleanupInterval = setInterval(() => {
704
- const cutoff = Date.now() - 300 * 1e3;
710
+ const cutoff = Date.now() - CONNECTION_IDLE_TIMEOUT_MS;
705
711
  for (const [url, entry] of this.connections) if (entry.lastUsedAt < cutoff) this.dropConnection(url);
706
- }, 6e4);
712
+ }, CONNECTION_CLEANUP_INTERVAL_MS);
707
713
  this.cleanupInterval.unref();
708
714
  }
715
+ /** Stop the cleanup interval and close all connections. Call when disposing the controller. */
716
+ dispose() {
717
+ clearInterval(this.cleanupInterval);
718
+ for (const url of this.connections.keys()) this.dropConnection(url);
719
+ }
709
720
  async getConnection(targetWsUrl) {
710
721
  const cached = this.connections.get(targetWsUrl);
711
722
  if (cached) {
@@ -849,12 +860,18 @@ var ChromeCdpBrowserController = class {
849
860
  });
850
861
  }
851
862
  async handleDialogAction(targetWsUrl, payload) {
863
+ const DIALOG_WAIT_TIMEOUT_MS = 500;
864
+ const DIALOG_POLL_INTERVAL_MS = 10;
865
+ const MAX_POLL_ATTEMPTS = 10;
852
866
  return await this.withRetry(targetWsUrl, async (conn) => {
853
867
  await this.ensureEnabled(targetWsUrl);
854
868
  const cached = this.connections.get(targetWsUrl);
855
869
  if (!cached?.pendingDialog) try {
856
- await conn.waitForEvent("Page.javascriptDialogOpening", 500);
857
- await new Promise((r) => setTimeout(r, 50));
870
+ await conn.waitForEvent("Page.javascriptDialogOpening", DIALOG_WAIT_TIMEOUT_MS);
871
+ for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
872
+ if (cached?.pendingDialog) break;
873
+ await new Promise((r) => setTimeout(r, DIALOG_POLL_INTERVAL_MS));
874
+ }
858
875
  } catch {
859
876
  return "no dialog present";
860
877
  }
@@ -1932,27 +1949,122 @@ var ChromeCdpBrowserController = class {
1932
1949
  * Returns silently on failure — never blocks navigation.
1933
1950
  */
1934
1951
  async dismissCookieBanner(targetWsUrl) {
1935
- try {
1936
- const domResult = await this.dismissViaDom(targetWsUrl);
1937
- if (domResult.dismissed) return domResult;
1938
- } catch {}
1939
- try {
1940
- const a11yResult = await this.dismissViaA11y(targetWsUrl);
1941
- if (a11yResult.dismissed) return a11yResult;
1942
- } catch {}
1952
+ for (let attempt = 0; attempt < 2; attempt++) {
1953
+ try {
1954
+ const domResult = await this.dismissViaDom(targetWsUrl);
1955
+ if (domResult.dismissed) return domResult;
1956
+ } catch {}
1957
+ try {
1958
+ const iframeResult = await this.dismissViaIframe(targetWsUrl);
1959
+ if (iframeResult.dismissed) return iframeResult;
1960
+ } catch {}
1961
+ try {
1962
+ const a11yResult = await this.dismissViaA11y(targetWsUrl);
1963
+ if (a11yResult.dismissed) return a11yResult;
1964
+ } catch {}
1965
+ if (attempt === 0) await new Promise((resolve) => setTimeout(resolve, 800));
1966
+ }
1943
1967
  return { dismissed: false };
1944
1968
  }
1945
1969
  async dismissViaDom(targetWsUrl) {
1946
1970
  const expression = `(() => {
1947
- // Common cookie-consent button selectors (ordered by specificity)
1971
+ function isVisible(el) {
1972
+ if (!el) return false;
1973
+ const style = getComputedStyle(el);
1974
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && el.offsetWidth > 0;
1975
+ }
1976
+
1977
+ // Comprehensive CMP selectors (ordered by specificity/popularity)
1948
1978
  const selectors = [
1979
+ // OneTrust
1949
1980
  '#onetrust-accept-btn-handler',
1981
+ // Cookiebot
1950
1982
  '#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll',
1951
1983
  '#CybotCookiebotDialogBodyButtonAccept',
1984
+ // Usercentrics
1985
+ '#uc-btn-accept-banner',
1986
+ '[data-testid="uc-accept-all-button"]',
1987
+ '.uc-btn-accept',
1988
+ // Didomi
1989
+ '#didomi-notice-agree-button',
1990
+ '.didomi-button[data-action="accept"]',
1991
+ '.didomi-components-button--color.didomi-button-highlight',
1992
+ // Quantcast / GDPR
1993
+ '.qc-cmp2-summary-buttons button[mode="primary"]',
1994
+ '.qc-cmp-button[data-action="accept"]',
1995
+ '#qc-cmp2-ui button.css-1litn2c',
1996
+ // TrustArc / TrueVault
1997
+ '#truste-consent-button',
1998
+ '.trustarc-agree-btn',
1999
+ '#consent_agree',
2000
+ // Consentmanager (zeit.de, etc.)
2001
+ '#cmpbox .cmpboxbtn.cmpboxbtnyes',
2002
+ '#cmpwelcomebtnyes a',
2003
+ '#cmpbntyestxt',
2004
+ '.cmpboxbtnyes',
2005
+ 'a.cmpboxbtn.cmpboxbtnyes',
2006
+ // iubenda
2007
+ '.iubenda-cs-accept-btn',
2008
+ '#iubenda-cs-banner .iubenda-cs-accept-btn',
2009
+ // Klaro
2010
+ '.klaro .cm-btn-accept',
2011
+ '.klaro .cm-btn-success',
2012
+ // Osano
2013
+ '.osano-cm-accept-all',
2014
+ '.osano-cm-button--type_accept',
2015
+ // CookieYes / GDPR Cookie Consent
2016
+ '#cookie_action_close_header',
2017
+ '.cky-btn-accept',
2018
+ '[data-cky-tag="accept-button"]',
2019
+ // Termly
2020
+ '.t-acceptAllButton',
2021
+ '#termly-code-snippet-support .t-acceptAllButton',
2022
+ // Axeptio
2023
+ '#axeptio_btn_acceptAll',
2024
+ '[data-axeptio="accept"]',
2025
+ // Complianz / Really Simple Cookie Consent
2026
+ '.cmplz-accept',
2027
+ '#cmplz-cookiebanner-container .cmplz-accept',
2028
+ // CookieFirst
2029
+ '[data-cookiefirst-action="accept"]',
2030
+ // Cookie Notice / Cookie Law Info
2031
+ '#cookie_action_close_header_reject',
2032
+ '#cn-accept-cookie',
2033
+ // Borlabs Cookie (WordPress)
2034
+ '#CookieBoxSaveButton',
2035
+ 'a[data-cookie-accept]',
2036
+ // Piwik PRO
2037
+ '#PiwikPROConsentForm-agree-to-all',
2038
+ '.PiwikPROConsentForm-agree-to-all-button',
2039
+ // Civic Cookie Control
2040
+ '#ccc-recommended-settings',
2041
+ '#ccc-dismiss-button',
2042
+ '.ccc-accept-settings',
2043
+ // Moove GDPR (WordPress)
2044
+ '.moove-gdpr-infobar-allow-all',
2045
+ '#moove_gdpr_cookie_modal .moove-gdpr-modal-allow-all',
2046
+ // CookieHub
2047
+ '.cookiehub-accept',
2048
+ '#cookiehub-accept-btn',
2049
+ '.ch2-btn-primary',
2050
+ // Cookie Script
2051
+ '#cookiescript_accept',
2052
+ '#cookiescript_injected .cookiescript_accept',
2053
+ // HubSpot Cookie Banner
2054
+ '#hs-eu-confirmation-button',
2055
+ '#hs-eu-cookie-confirmation .hs-eu-cta',
2056
+ // MediaVine
2057
+ '[data-name="mediavine-gdpr-cmp"] .pur-accept',
2058
+ // Sirdata
2059
+ '#sd-cmp .sd-cmp-JEaB',
2060
+ // Google Consent Mode / Funding Choices
2061
+ '.fc-cta-consent',
2062
+ '.fc-button-background[aria-label="Consent"]',
2063
+ // Generic selectors
1952
2064
  '#accept-cookies', '#acceptCookies',
1953
2065
  '#cookie-accept', '#cookieAccept',
1954
2066
  '#gdpr-accept', '#consent-accept',
1955
- '#uc-btn-accept-banner',
2067
+ '#cookies-accept-all', '#accept-all-cookies',
1956
2068
  '.cc-accept', '.cc-btn.cc-dismiss',
1957
2069
  '.cookie-consent-accept', '.cookie-accept-btn',
1958
2070
  '.js-cookie-accept', '.js-accept-cookies',
@@ -1960,39 +2072,99 @@ var ChromeCdpBrowserController = class {
1960
2072
  '.gdpr-accept',
1961
2073
  '[data-testid="cookie-accept"]',
1962
2074
  '[data-action="accept-cookies"]',
1963
- '[data-cookiefirst-action="accept"]',
2075
+ '[data-gdpr="accept"]',
2076
+ '[data-consent="accept"]',
1964
2077
  ];
1965
2078
 
1966
- for (const sel of selectors) {
2079
+ // Query selector in main DOM and open shadow roots
2080
+ function deepQuery(sel) {
1967
2081
  const el = document.querySelector(sel);
1968
- if (el && el.offsetParent !== null) {
1969
- el.click();
1970
- return JSON.stringify({ dismissed: true, method: 'selector', detail: sel });
2082
+ if (el) return el;
2083
+ for (const host of document.querySelectorAll('*')) {
2084
+ if (host.shadowRoot) {
2085
+ const inner = host.shadowRoot.querySelector(sel);
2086
+ if (inner) return inner;
2087
+ }
1971
2088
  }
2089
+ return null;
2090
+ }
2091
+
2092
+ for (const sel of selectors) {
2093
+ try {
2094
+ const el = deepQuery(sel);
2095
+ if (el && isVisible(el)) {
2096
+ el.click();
2097
+ return JSON.stringify({ dismissed: true, method: 'selector', detail: sel });
2098
+ }
2099
+ } catch { /* invalid selector — skip */ }
1972
2100
  }
1973
2101
 
1974
2102
  // Text-based fallback: find visible buttons with consent text
2103
+ // Patterns are intentionally loose — they use .* instead of strict ^/$ anchors
1975
2104
  const patterns = [
1976
- /^accept\\s*(all)?\\s*(cookies)?$/i,
1977
- /^(alle\\s+)?akzeptieren$/i,
1978
- /^(i\\s+)?agree$/i,
1979
- /^allow\\s*(all)?\\s*(cookies)?$/i,
1980
- /^(alle\\s+)?zustimmen$/i,
1981
- /^got\\s*it$/i,
2105
+ // English
2106
+ /accept\\s*(all)?\\s*(cookies)?/i,
2107
+ /allow\\s*(all)?\\s*(cookies)?/i,
2108
+ /(i\\s+)?agree(\\s+to\\s+all)?/i,
2109
+ /got\\s*it/i,
1982
2110
  /^ok$/i,
1983
2111
  /^consent$/i,
1984
- /^(j')?accepte(r)?$/i,
1985
- /^(tout\\s+)?accepter$/i,
1986
- ];
1987
- const candidates = [
1988
- ...document.querySelectorAll('button'),
1989
- ...document.querySelectorAll('a[role="button"]'),
1990
- ...document.querySelectorAll('[role="button"]'),
2112
+ /continue\\s+without\\s+accepting/i,
2113
+ // German
2114
+ /(alle\\s+)?akzeptieren/i,
2115
+ /alle\\s+cookies\\s+akzeptieren/i,
2116
+ /(alle\\s+)?zustimmen/i,
2117
+ /einwilligen/i,
2118
+ /einverstanden/i,
2119
+ /alles\\s+erlauben/i,
2120
+ /alles\\s+akzeptieren/i,
2121
+ /geht\\s+klar/i,
2122
+ /verstanden/i,
2123
+ // French
2124
+ /(j')?accepte(r)?(\\s+tout)?/i,
2125
+ /(tout\\s+)?accepter/i,
2126
+ /autoriser/i,
2127
+ // Spanish
2128
+ /aceptar(\\s+todo)?/i,
2129
+ /aceptar\\s+cookies/i,
2130
+ // Italian
2131
+ /accett(a|o)(\\s+tutt[oi])?/i,
2132
+ /accetta\\s+cookies/i,
2133
+ // Dutch
2134
+ /(alle\\s+)?accepteren/i,
2135
+ /alles\\s+accepteren/i,
2136
+ // Portuguese
2137
+ /aceitar(\\s+tudo)?/i,
2138
+ /aceitar\\s+cookies/i,
2139
+ // Polish
2140
+ /akceptuj(\\s+wszystk[oi]e)?/i,
2141
+ /zgadzam\\s+si/i,
2142
+ // Swedish
2143
+ /acceptera(\\s+alla)?/i,
2144
+ // Danish
2145
+ /accepter(\\s+alle)?/i,
2146
+ // Norwegian
2147
+ /aksepter(\\s+alle)?/i,
1991
2148
  ];
2149
+ // Collect candidates from main DOM and open shadow roots
2150
+ function collectButtons(root) {
2151
+ const results = [];
2152
+ for (const tag of ['button', 'a[role="button"]', '[role="button"]', 'div[role="button"]', 'span[role="button"]']) {
2153
+ results.push(...root.querySelectorAll(tag));
2154
+ }
2155
+ // Traverse open shadow roots for CMPs that use web components
2156
+ for (const el of root.querySelectorAll('*')) {
2157
+ if (el.shadowRoot) {
2158
+ results.push(...collectButtons(el.shadowRoot));
2159
+ }
2160
+ }
2161
+ return results;
2162
+ }
2163
+ const candidates = collectButtons(document);
1992
2164
  for (const el of candidates) {
1993
- if (!el.offsetParent) continue;
2165
+ if (!isVisible(el)) continue;
1994
2166
  const text = (el.textContent || '').trim();
1995
- if (text.length > 40) continue;
2167
+ if (text.length > 60) continue;
1996
2168
  for (const pat of patterns) {
1997
2169
  if (pat.test(text)) {
1998
2170
  el.click();
@@ -2001,6 +2173,90 @@ var ChromeCdpBrowserController = class {
2001
2173
  }
2002
2174
  }
2003
2175
 
2176
+ return JSON.stringify({ dismissed: false });
2177
+ })()`;
2178
+ return await this.withRetry(targetWsUrl, async (conn) => {
2179
+ await this.ensureEnabled(targetWsUrl);
2180
+ const raw = (await conn.send("Runtime.evaluate", {
2181
+ expression,
2182
+ returnByValue: true
2183
+ })).result.value ?? "{\"dismissed\":false}";
2184
+ try {
2185
+ return JSON.parse(raw);
2186
+ } catch {
2187
+ return { dismissed: false };
2188
+ }
2189
+ });
2190
+ }
2191
+ /**
2192
+ * Handle iframe-based consent banners (Sourcepoint, some OneTrust configurations, etc.)
2193
+ * These banners render inside an iframe that needs to be accessed separately.
2194
+ */
2195
+ async dismissViaIframe(targetWsUrl) {
2196
+ const expression = `(() => {
2197
+ // Known consent iframe selectors
2198
+ const iframeSelectors = [
2199
+ 'iframe[id^="sp_message_iframe"]', // Sourcepoint (dynamic ID suffix)
2200
+ '#sp_message_iframe', // Sourcepoint (static ID)
2201
+ 'iframe[id*="sp_message"]', // Sourcepoint variants
2202
+ 'div[id^="sp_message_container"] iframe', // Sourcepoint container with iframe
2203
+ 'iframe[title*="cookie"]', // Generic cookie iframes
2204
+ 'iframe[title*="consent"]', // Generic consent iframes
2205
+ 'iframe[title*="Cookie"]',
2206
+ 'iframe[title*="Consent"]',
2207
+ 'iframe[title*="Privacy"]',
2208
+ 'iframe[title*="privacy"]',
2209
+ 'iframe[title*="GDPR"]',
2210
+ 'iframe[src*="consent"]',
2211
+ 'iframe[src*="cookie"]',
2212
+ 'iframe[src*="privacy"]',
2213
+ 'iframe[src*="sourcepoint"]',
2214
+ 'iframe[src*="consentmanager"]',
2215
+ ];
2216
+
2217
+ for (const sel of iframeSelectors) {
2218
+ try {
2219
+ const iframe = document.querySelector(sel);
2220
+ if (!iframe) continue;
2221
+ const doc = iframe.contentDocument || iframe.contentWindow?.document;
2222
+ if (!doc) continue;
2223
+
2224
+ // Try known selectors inside the iframe
2225
+ const innerSelectors = [
2226
+ // Sourcepoint specific
2227
+ 'button.sp_choice_type_11', // Accept all
2228
+ 'button.sp_choice_type_ACCEPT_ALL', // Accept all variant
2229
+ 'button.sp_choice_type_SAVE_AND_EXIT', // Save & exit
2230
+ '.priv-save-btn', // Sourcepoint frame
2231
+ '.message-button', // Generic Sourcepoint
2232
+ // Title-based
2233
+ 'button[title*="Accept"]', 'button[title*="accept"]',
2234
+ 'button[title*="Agree"]', 'button[title*="agree"]',
2235
+ 'button[title*="Allow"]',
2236
+ 'button[title*="Akzeptieren"]', 'button[title*="akzeptieren"]',
2237
+ 'button[title*="Zustimmen"]',
2238
+ ];
2239
+ for (const innerSel of innerSelectors) {
2240
+ const btn = doc.querySelector(innerSel);
2241
+ if (btn && btn.offsetWidth > 0) {
2242
+ btn.click();
2243
+ return JSON.stringify({ dismissed: true, method: 'iframe', detail: sel + ' > ' + innerSel });
2244
+ }
2245
+ }
2246
+
2247
+ // Text-based fallback inside iframe
2248
+ const buttons = doc.querySelectorAll('button, [role="button"]');
2249
+ for (const btn of buttons) {
2250
+ const text = (btn.textContent || '').trim();
2251
+ if (text.length > 60) continue;
2252
+ if (/accept|agree|allow|akzeptieren|zustimmen|einwilligen|accepter|aceptar/i.test(text)) {
2253
+ btn.click();
2254
+ return JSON.stringify({ dismissed: true, method: 'iframe-text', detail: text });
2255
+ }
2256
+ }
2257
+ } catch { /* cross-origin iframe — skip */ }
2258
+ }
2259
+
2004
2260
  return JSON.stringify({ dismissed: false });
2005
2261
  })()`;
2006
2262
  return await this.withRetry(targetWsUrl, async (conn) => {
@@ -2018,32 +2274,60 @@ var ChromeCdpBrowserController = class {
2018
2274
  }
2019
2275
  /**
2020
2276
  * Use the CDP Accessibility tree to find and click consent buttons.
2021
- * This works even when consent banners use shadow DOM, iframes, or obfuscated class names.
2277
+ * This works even when consent banners use shadow DOM or obfuscated class names.
2278
+ * Also queries cross-origin iframe frames via Page.getFrameTree for Sourcepoint etc.
2022
2279
  */
2023
2280
  async dismissViaA11y(targetWsUrl) {
2024
2281
  return await this.withRetry(targetWsUrl, async (conn) => {
2025
2282
  await conn.send("Accessibility.enable");
2283
+ await conn.send("Page.enable");
2026
2284
  const { nodes } = await conn.send("Accessibility.getFullAXTree");
2285
+ const allNodes = [...nodes];
2286
+ try {
2287
+ const { frameTree } = await conn.send("Page.getFrameTree");
2288
+ for (const child of frameTree.childFrames ?? []) {
2289
+ const url = child.frame.url ?? "";
2290
+ if (/consent|cookie|privacy|sourcepoint|didomi|cmp|gdpr|trustarc|consentmanager|sp_message|onetrust|usercentrics|consent-cdn/i.test(url) || url === "about:blank") try {
2291
+ const frameResult = await conn.send("Accessibility.getFullAXTree", { frameId: child.frame.id });
2292
+ allNodes.push(...frameResult.nodes);
2293
+ } catch {}
2294
+ }
2295
+ } catch {}
2027
2296
  const consentPatterns = [
2028
- /^accept\s*(all)?\s*(cookies)?$/i,
2029
- /^(i\s+)?agree(\s+to\s+all)?$/i,
2030
- /^allow\s*(all)?\s*(cookies)?$/i,
2031
- /^got\s*it$/i,
2297
+ /accept\s*(all)?\s*(cookies)?/i,
2298
+ /allow\s*(all)?\s*(cookies)?/i,
2299
+ /(i\s+)?agree(\s+to\s+all)?/i,
2300
+ /got\s*it/i,
2032
2301
  /^ok$/i,
2033
2302
  /^consent$/i,
2034
- /^(alle\s+)?akzeptieren$/i,
2035
- /^(alle\s+)?zustimmen$/i,
2036
- /^einverstanden$/i,
2037
- /^(j')?accepte(r)?(\s+tout)?$/i,
2038
- /^(tout\s+)?accepter$/i,
2039
- /^aceptar(\s+todo)?$/i,
2040
- /^accetta(\s+tutto)?$/i,
2041
- /^accepteren$/i,
2042
- /^alle\s+accepteren$/i,
2043
- /^aceitar(\s+tudo)?$/i
2303
+ /(alle\s+)?akzeptieren/i,
2304
+ /alle\s+cookies\s+akzeptieren/i,
2305
+ /(alle\s+)?zustimmen/i,
2306
+ /einwilligen/i,
2307
+ /einverstanden/i,
2308
+ /alles\s+erlauben/i,
2309
+ /alles\s+akzeptieren/i,
2310
+ /geht\s+klar/i,
2311
+ /verstanden/i,
2312
+ /(j')?accepte(r)?(\s+tout)?/i,
2313
+ /(tout\s+)?accepter/i,
2314
+ /autoriser/i,
2315
+ /aceptar(\s+todo)?/i,
2316
+ /aceptar\s+cookies/i,
2317
+ /accett(a|o)(\s+tutt[oi])?/i,
2318
+ /accetta\s+cookies/i,
2319
+ /(alle\s+)?accepteren/i,
2320
+ /alles\s+accepteren/i,
2321
+ /aceitar(\s+tudo)?/i,
2322
+ /aceitar\s+cookies/i,
2323
+ /akceptuj(\s+wszystk[oi]e)?/i,
2324
+ /zgadzam\s+si/i,
2325
+ /acceptera(\s+alla)?/i,
2326
+ /accepter(\s+alle)?/i,
2327
+ /aksepter(\s+alle)?/i
2044
2328
  ];
2045
2329
  const candidates = [];
2046
- for (const node of nodes) {
2330
+ for (const node of allNodes) {
2047
2331
  if (node.ignored) continue;
2048
2332
  const role = node.role?.value;
2049
2333
  if (role !== "button" && role !== "link") continue;
@@ -2200,6 +2484,113 @@ var MockBrowserController = class {
2200
2484
  closeConnection(_targetWsUrl) {}
2201
2485
  };
2202
2486
  //#endregion
2487
+ //#region src/lib/domain-schemas.ts
2488
+ const InteractActionSchema = z.enum([
2489
+ "click",
2490
+ "type",
2491
+ "press",
2492
+ "waitFor",
2493
+ "evaluate",
2494
+ "scroll",
2495
+ "hover",
2496
+ "select",
2497
+ "toggle",
2498
+ "goBack",
2499
+ "goForward",
2500
+ "refresh",
2501
+ "dialog"
2502
+ ]);
2503
+ const InteractPayloadSchema = z.object({
2504
+ action: InteractActionSchema,
2505
+ selector: z.string().optional(),
2506
+ fallbackSelectors: z.array(z.string()).optional(),
2507
+ text: z.string().optional(),
2508
+ key: z.string().optional(),
2509
+ value: z.string().optional(),
2510
+ scrollX: z.number().optional(),
2511
+ scrollY: z.number().optional(),
2512
+ timeoutMs: z.number().optional()
2513
+ });
2514
+ const SessionStatusSchema = z.enum([
2515
+ "starting",
2516
+ "ready",
2517
+ "disconnected",
2518
+ "failed",
2519
+ "terminated"
2520
+ ]);
2521
+ const CommandTypeSchema = z.enum([
2522
+ "navigate",
2523
+ "interact",
2524
+ "restart",
2525
+ "terminate"
2526
+ ]);
2527
+ const ResultStatusSchema = z.enum([
2528
+ "success",
2529
+ "failed",
2530
+ "timed_out"
2531
+ ]);
2532
+ const ConnectionStateSchema = z.enum([
2533
+ "connecting",
2534
+ "ready",
2535
+ "disconnected",
2536
+ "failed"
2537
+ ]);
2538
+ const EventCategorySchema = z.enum([
2539
+ "lifecycle",
2540
+ "security",
2541
+ "command"
2542
+ ]);
2543
+ const EventSeveritySchema = z.enum([
2544
+ "info",
2545
+ "warning",
2546
+ "error"
2547
+ ]);
2548
+ z.object({
2549
+ sessionId: z.string().min(1),
2550
+ status: SessionStatusSchema,
2551
+ browserType: z.literal("chrome"),
2552
+ startedAt: z.string().datetime(),
2553
+ endedAt: z.string().datetime().optional(),
2554
+ authTokenRef: z.string().min(1)
2555
+ }).refine((session) => session.status === "terminated" ? Boolean(session.endedAt) : true, {
2556
+ message: "endedAt is required for terminated sessions",
2557
+ path: ["endedAt"]
2558
+ });
2559
+ z.object({
2560
+ commandId: z.string().min(1),
2561
+ sessionId: z.string().min(1),
2562
+ type: CommandTypeSchema,
2563
+ payload: z.record(z.string(), z.unknown()),
2564
+ submittedAt: z.string().datetime(),
2565
+ completedAt: z.string().datetime().optional(),
2566
+ resultStatus: ResultStatusSchema.optional(),
2567
+ resultMessage: z.string().optional()
2568
+ }).refine((command) => command.completedAt ? Boolean(command.resultStatus) : true, {
2569
+ message: "resultStatus is required when completedAt is set",
2570
+ path: ["resultStatus"]
2571
+ });
2572
+ z.object({
2573
+ sessionId: z.string().min(1),
2574
+ state: ConnectionStateSchema,
2575
+ lastHeartbeatAt: z.string().datetime().optional(),
2576
+ disconnectReason: z.string().optional()
2577
+ });
2578
+ z.object({
2579
+ sessionId: z.string().min(1),
2580
+ tokenId: z.string().min(1),
2581
+ issuedAt: z.string().datetime(),
2582
+ expiresAt: z.string().datetime().optional(),
2583
+ revokedAt: z.string().datetime().optional()
2584
+ });
2585
+ z.object({
2586
+ eventId: z.string().min(1),
2587
+ sessionId: z.string().min(1),
2588
+ category: EventCategorySchema,
2589
+ message: z.string().min(1),
2590
+ createdAt: z.string().datetime(),
2591
+ severity: EventSeveritySchema
2592
+ });
2593
+ //#endregion
2203
2594
  //#region src/session/session-store.ts
2204
2595
  var SessionStore = class {
2205
2596
  filePath;
@@ -2343,7 +2734,7 @@ var SessionManager = class {
2343
2734
  return this.mustGetRecord(recovered.sessionId);
2344
2735
  } catch (restartError) {
2345
2736
  this.recordEvent(sessionId, "lifecycle", "error", `Recovery failed: ${restartError.message}`);
2346
- throw new Error(`Session is not ready and recovery failed: ${restartError.message}`);
2737
+ throw new Error(`Session is not ready and recovery failed: ${restartError.message}`, { cause: restartError });
2347
2738
  }
2348
2739
  throw new Error("Session is terminated. Start a new session.");
2349
2740
  }
@@ -2372,8 +2763,11 @@ var SessionManager = class {
2372
2763
  const finalUrl = await this.browser.navigate(record.targetWsUrl, url);
2373
2764
  this.store.setLastUrl(sessionId, finalUrl);
2374
2765
  resultMessage = `Navigated to ${finalUrl}`;
2375
- } else if (input.type === "interact") resultMessage = `Interaction result: ${await this.browser.interact(record.targetWsUrl, input.payload)}`;
2376
- else if (input.type === "restart") {
2766
+ } else if (input.type === "interact") {
2767
+ const parsedPayload = InteractPayloadSchema.safeParse(input.payload);
2768
+ if (!parsedPayload.success) throw new Error(`Invalid interact payload: ${parsedPayload.error.message}`);
2769
+ resultMessage = `Interaction result: ${await this.browser.interact(record.targetWsUrl, parsedPayload.data)}`;
2770
+ } else if (input.type === "restart") {
2377
2771
  await this.restartSession(sessionId);
2378
2772
  resultMessage = "Session restarted";
2379
2773
  } else if (input.type === "terminate") {
@@ -2685,9 +3079,8 @@ var SessionManager = class {
2685
3079
  //#endregion
2686
3080
  //#region src/transport/control-api.ts
2687
3081
  var ControlApi = class {
2688
- constructor(sessions, eventStore) {
3082
+ constructor(sessions) {
2689
3083
  this.sessions = sessions;
2690
- this.eventStore = eventStore;
2691
3084
  }
2692
3085
  async createSession(input) {
2693
3086
  return await this.sessions.createSession(input);
@@ -2723,7 +3116,7 @@ var ControlApi = class {
2723
3116
  return await this.sessions.getCurrentUrl(sessionId);
2724
3117
  }
2725
3118
  listEvents(sessionId, limit = 100) {
2726
- return { events: this.eventStore.list(sessionId, limit) };
3119
+ return { events: this.sessions.listEvents(sessionId, limit) };
2727
3120
  }
2728
3121
  searchMemory(input) {
2729
3122
  return { results: this.sessions.searchMemory(input) };
@@ -2948,7 +3341,7 @@ function buildSelectorHints(insight) {
2948
3341
  if (!evidence.selector || evidence.result !== "success") continue;
2949
3342
  weightedSelectors.set(evidence.selector, (weightedSelectors.get(evidence.selector) ?? 0) + 3);
2950
3343
  }
2951
- return [...weightedSelectors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([selector]) => selector);
3344
+ return [...weightedSelectors.entries()].toSorted((a, b) => b[1] - a[1]).slice(0, 5).map(([selector]) => selector);
2952
3345
  }
2953
3346
  function buildSelectorAliases(insight) {
2954
3347
  return insight.selectorAliases ?? [];
@@ -3024,7 +3417,7 @@ var MemoryIndex = class {
3024
3417
  }
3025
3418
  return best ? [best] : [];
3026
3419
  }
3027
- return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup)).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
3420
+ return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup)).filter((result) => result.score > 0).toSorted((a, b) => b.score - a.score).slice(0, limit);
3028
3421
  }
3029
3422
  };
3030
3423
  //#endregion
@@ -3401,10 +3794,19 @@ var TaskInsightStore = class {
3401
3794
  };
3402
3795
  //#endregion
3403
3796
  //#region src/memory/memory-service.ts
3797
+ /** Memory service configuration */
3404
3798
  const SEARCH_CACHE_TTL_MS = 2e3;
3799
+ const MAX_EVIDENCE_RECORDS = 50;
3800
+ const MAX_RECIPE_STEPS = 8;
3801
+ const MAX_SELECTOR_ALIASES = 10;
3802
+ /** Site profile limits */
3405
3803
  const MAX_NAVIGATION_PATHS = 20;
3406
3804
  const MAX_LAYOUT_FINGERPRINTS = 10;
3407
3805
  const MAX_SELECTOR_PATTERNS = 15;
3806
+ function normalizePathSegment(value) {
3807
+ const [firstSegment = ""] = value.replace(/\/\*$/, "").split("/").filter(Boolean);
3808
+ return firstSegment.toLowerCase();
3809
+ }
3408
3810
  var MemoryService = class {
3409
3811
  store;
3410
3812
  profileStore;
@@ -3462,7 +3864,7 @@ var MemoryService = class {
3462
3864
  fresh: insights.filter((x) => x.freshness === "fresh").length,
3463
3865
  suspect: insights.filter((x) => x.freshness === "suspect").length,
3464
3866
  stale: insights.filter((x) => x.freshness === "stale").length,
3465
- topDomains: [...byDomain.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([domain, count]) => ({
3867
+ topDomains: [...byDomain.entries()].toSorted((a, b) => b[1] - a[1]).slice(0, 10).map(([domain, count]) => ({
3466
3868
  domain,
3467
3869
  count
3468
3870
  }))
@@ -3514,7 +3916,7 @@ var MemoryService = class {
3514
3916
  const refreshed = applySuccess({
3515
3917
  ...matched,
3516
3918
  useCount: matched.useCount + 1,
3517
- evidence: [...matched.evidence.slice(-49), evidence],
3919
+ evidence: [...matched.evidence.slice(-(MAX_EVIDENCE_RECORDS - 1)), evidence],
3518
3920
  actionRecipe: this.mergeRecipe(matched.actionRecipe, input.step),
3519
3921
  expectedOutcome: input.expectedOutcome
3520
3922
  });
@@ -3548,7 +3950,7 @@ var MemoryService = class {
3548
3950
  const failed = applyFailure({
3549
3951
  ...matched,
3550
3952
  useCount: matched.useCount + 1,
3551
- evidence: [...matched.evidence.slice(-49), evidence]
3953
+ evidence: [...matched.evidence.slice(-(MAX_EVIDENCE_RECORDS - 1)), evidence]
3552
3954
  }, signal);
3553
3955
  this.store.upsert(failed);
3554
3956
  this.invalidateSearchCache();
@@ -3570,7 +3972,7 @@ var MemoryService = class {
3570
3972
  if (fpIndex >= 0) profile.layoutFingerprints[fpIndex] = fingerprint;
3571
3973
  else {
3572
3974
  profile.layoutFingerprints.push(fingerprint);
3573
- if (profile.layoutFingerprints.length > MAX_LAYOUT_FINGERPRINTS) profile.layoutFingerprints = profile.layoutFingerprints.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt)).slice(0, MAX_LAYOUT_FINGERPRINTS);
3975
+ if (profile.layoutFingerprints.length > MAX_LAYOUT_FINGERPRINTS) profile.layoutFingerprints = profile.layoutFingerprints.toSorted((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt)).slice(0, MAX_LAYOUT_FINGERPRINTS);
3574
3976
  }
3575
3977
  profile.lastVisitAt = now;
3576
3978
  this.profileStore.upsert(profile);
@@ -3662,12 +4064,8 @@ var MemoryService = class {
3662
4064
  return score;
3663
4065
  }
3664
4066
  sameFirstPathSegment(left, right) {
3665
- const normalize = (value) => {
3666
- const [firstSegment = ""] = value.replace(/\/\*$/, "").split("/").filter(Boolean);
3667
- return firstSegment.toLowerCase();
3668
- };
3669
- const leftSegment = normalize(left);
3670
- const rightSegment = normalize(right);
4067
+ const leftSegment = normalizePathSegment(left);
4068
+ const rightSegment = normalizePathSegment(right);
3671
4069
  return Boolean(leftSegment) && leftSegment === rightSegment;
3672
4070
  }
3673
4071
  createEvidence(input, result, reason) {
@@ -3696,11 +4094,11 @@ var MemoryService = class {
3696
4094
  selector,
3697
4095
  fallbackSelectors: []
3698
4096
  });
3699
- if (aliasMap.size >= 10) break;
4097
+ if (aliasMap.size >= MAX_SELECTOR_ALIASES) break;
3700
4098
  }
3701
4099
  return {
3702
4100
  ...insight,
3703
- selectorAliases: [...aliasMap.values()].slice(0, 10)
4101
+ selectorAliases: [...aliasMap.values()].slice(0, MAX_SELECTOR_ALIASES)
3704
4102
  };
3705
4103
  }
3706
4104
  deriveAliasName(selector) {
@@ -3719,7 +4117,7 @@ var MemoryService = class {
3719
4117
  }
3720
4118
  mergeRecipe(recipe, step) {
3721
4119
  if (recipe.find((existing) => existing.summary === step.summary && existing.selector === step.selector)) return recipe;
3722
- return [...recipe, step].slice(-8);
4120
+ return [...recipe, step].slice(-MAX_RECIPE_STEPS);
3723
4121
  }
3724
4122
  };
3725
4123
  //#endregion
@@ -3751,7 +4149,7 @@ var AgenticBrowserCore = class {
3751
4149
  constructor(context, browserController) {
3752
4150
  this.context = context;
3753
4151
  this.sessions = new SessionManager(context, browserController);
3754
- this.api = new ControlApi(this.sessions, context.eventStore);
4152
+ this.api = new ControlApi(this.sessions);
3755
4153
  }
3756
4154
  async startSession(input = { browser: "chrome" }) {
3757
4155
  return await this.api.createSession(input);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-browser",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "description": "CLI and MCP server for AI agents to control Chrome via CDP",
5
5
  "keywords": [
6
6
  "agent",
@@ -66,9 +66,9 @@
66
66
  "typecheck": "tsc"
67
67
  },
68
68
  "dependencies": {
69
- "@modelcontextprotocol/sdk": "^1.27.1",
69
+ "@modelcontextprotocol/sdk": "^1.29.0",
70
70
  "commander": "^14.0.3",
71
- "ws": "^8.19.0",
71
+ "ws": "^8.20.0",
72
72
  "zod": "^4.3.6"
73
73
  },
74
74
  "devDependencies": {
@@ -76,20 +76,20 @@
76
76
  "@semantic-release/github": "^12.0.6",
77
77
  "@semantic-release/npm": "^13.1.5",
78
78
  "@semantic-release/release-notes-generator": "^14.1.0",
79
- "@types/node": "^25.4.0",
79
+ "@types/node": "^25.6.0",
80
80
  "@types/ws": "^8.18.1",
81
- "oxfmt": "^0.37.0",
82
- "oxlint": "^1.52.0",
83
- "react": "^19.2.4",
84
- "react-dom": "^19.2.4",
81
+ "oxfmt": "^0.44.0",
82
+ "oxlint": "^1.59.0",
83
+ "react": "^19.2.5",
84
+ "react-dom": "^19.2.5",
85
85
  "semantic-release": "^25.0.3",
86
- "tsdown": "^0.21.1",
87
- "typescript": "^5.9.3",
88
- "vitest": "^4.0.18",
86
+ "tsdown": "^0.21.7",
87
+ "typescript": "^6.0.2",
88
+ "vitest": "^4.1.4",
89
89
  "vocs": "1.4.1"
90
90
  },
91
91
  "engines": {
92
- "node": ">=20"
92
+ "node": ">=22"
93
93
  },
94
- "packageManager": "pnpm@10.32.0"
94
+ "packageManager": "pnpm@10.33.0"
95
95
  }