agentic-browser 1.5.1 → 1.6.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.
package/README.md CHANGED
@@ -152,7 +152,7 @@ Returns a JSON array of elements with CSS selectors and fallback selectors usabl
152
152
  }
153
153
  ```
154
154
 
155
- MCP responses are compact — `visible`, `actions`, and `tagName` are omitted to reduce token usage. The full element shape is available via the programmatic API.
155
+ MCP responses are compact — `visible`, `actions`, and `tagName` are omitted to reduce token usage, and `enabled` is omitted when it is `true`. Responses also include a `summary` block with `countsByRole` and `primaryActions` so an LLM can identify the main controls faster. The full element shape is available via the programmatic API.
156
156
 
157
157
  ````
158
158
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as createCliRuntime } from "../runtime-OsYo7Rh2.mjs";
2
+ import { r as createCliRuntime } from "../runtime-DUSoQl5x.mjs";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import crypto from "node:crypto";
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-OsYo7Rh2.mjs";
2
+ import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-DUSoQl5x.mjs";
3
3
 
4
4
  export { AgenticBrowserCore, createAgenticBrowserCore, createMockAgenticBrowserCore };
@@ -1,9 +1,163 @@
1
1
  #!/usr/bin/env node
2
- import { n as createAgenticBrowserCore } from "../runtime-OsYo7Rh2.mjs";
2
+ import { n as createAgenticBrowserCore } from "../runtime-DUSoQl5x.mjs";
3
3
  import { z } from "zod";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
 
7
+ //#region src/mcp/response-helpers.ts
8
+ function uniqueLines(content, maxLineLength = 160) {
9
+ const seen = /* @__PURE__ */ new Set();
10
+ const lines = [];
11
+ for (const raw of content.split("\n")) {
12
+ const line = raw.replace(/\s+/g, " ").trim();
13
+ if (!line || seen.has(line)) continue;
14
+ seen.add(line);
15
+ lines.push(line.slice(0, maxLineLength));
16
+ }
17
+ return lines;
18
+ }
19
+ function truncateContent(content, limit) {
20
+ if (!limit || content.length <= limit) return {
21
+ content,
22
+ truncated: false
23
+ };
24
+ const originalLength = content.length;
25
+ return {
26
+ content: content.slice(0, limit) + `\n\n[Truncated — showing first ${limit} of ${originalLength} characters. Use a CSS selector to scope the content.]`,
27
+ truncated: true
28
+ };
29
+ }
30
+ function summarizeTextContent(content) {
31
+ const lines = uniqueLines(content);
32
+ return {
33
+ summaryLines: lines.slice(0, 8),
34
+ previewLines: lines.slice(0, 20)
35
+ };
36
+ }
37
+ function summarizeA11yContent(content) {
38
+ const lines = uniqueLines(content);
39
+ const headings = [];
40
+ const landmarks = [];
41
+ const actions = [];
42
+ const inputs = [];
43
+ for (const line of lines) {
44
+ const lower = line.toLowerCase();
45
+ if ((lower.startsWith("heading ") || lower.includes(" heading ")) && headings.length < 8) {
46
+ headings.push(line);
47
+ continue;
48
+ }
49
+ if ((lower.startsWith("main") || lower.startsWith("navigation") || lower.startsWith("banner") || lower.startsWith("complementary") || lower.startsWith("contentinfo") || lower.startsWith("search")) && landmarks.length < 8) {
50
+ landmarks.push(line);
51
+ continue;
52
+ }
53
+ if ((lower.includes("button") || lower.includes("link")) && actions.length < 12) {
54
+ actions.push(line);
55
+ continue;
56
+ }
57
+ if ((lower.includes("textbox") || lower.includes("combobox") || lower.includes("searchbox") || lower.includes("checkbox") || lower.includes("radio")) && inputs.length < 12) inputs.push(line);
58
+ }
59
+ return {
60
+ headings,
61
+ landmarks,
62
+ actions,
63
+ inputs,
64
+ previewLines: lines.slice(0, 30)
65
+ };
66
+ }
67
+ function compactPageContent(result, maxChars) {
68
+ const limit = maxChars ?? {
69
+ text: 8e3,
70
+ a11y: 1e4,
71
+ html: 4e3,
72
+ title: void 0
73
+ }[result.mode];
74
+ const truncated = truncateContent(result.content, limit);
75
+ if (result.mode === "title") return {
76
+ ...result,
77
+ truncated: false
78
+ };
79
+ if (result.mode === "text") {
80
+ const summary = summarizeTextContent(truncated.content);
81
+ return {
82
+ mode: result.mode,
83
+ content: truncated.content,
84
+ truncated: truncated.truncated,
85
+ summaryLines: summary.summaryLines,
86
+ previewLines: summary.previewLines
87
+ };
88
+ }
89
+ if (result.mode === "a11y") {
90
+ const summary = summarizeA11yContent(truncated.content);
91
+ return {
92
+ mode: result.mode,
93
+ content: truncated.content,
94
+ truncated: truncated.truncated,
95
+ headings: summary.headings,
96
+ landmarks: summary.landmarks,
97
+ actions: summary.actions,
98
+ inputs: summary.inputs,
99
+ previewLines: summary.previewLines
100
+ };
101
+ }
102
+ return {
103
+ mode: result.mode,
104
+ content: truncated.content,
105
+ truncated: truncated.truncated
106
+ };
107
+ }
108
+ function summarizeElements(elements) {
109
+ const byRole = /* @__PURE__ */ new Map();
110
+ const primaryActions = [];
111
+ for (const element of elements) {
112
+ const role = String(element.role ?? "unknown");
113
+ byRole.set(role, (byRole.get(role) ?? 0) + 1);
114
+ if (primaryActions.length < 12 && typeof element.selector === "string" && typeof element.text === "string" && element.text) primaryActions.push({
115
+ role,
116
+ text: element.text,
117
+ selector: element.selector,
118
+ fallbackSelectors: element.fallbackSelectors
119
+ });
120
+ }
121
+ return {
122
+ countsByRole: Object.fromEntries([...byRole.entries()].sort((a, b) => b[1] - a[1])),
123
+ primaryActions
124
+ };
125
+ }
126
+ function compactElement(element, visibleOnly) {
127
+ const compact = { ...element };
128
+ if (visibleOnly) delete compact.visible;
129
+ delete compact.actions;
130
+ delete compact.tagName;
131
+ if (compact.enabled === true) delete compact.enabled;
132
+ if (!compact.text) delete compact.text;
133
+ if (compact.ariaLabel && compact.ariaLabel === compact.text) delete compact.ariaLabel;
134
+ if (compact.placeholder && compact.placeholder === compact.text) delete compact.placeholder;
135
+ return compact;
136
+ }
137
+ function compactInteractiveElementsResult(result, visibleOnly) {
138
+ const elements = result.elements.map((el) => compactElement(el, visibleOnly));
139
+ return {
140
+ elements,
141
+ totalFound: result.totalFound,
142
+ truncated: result.truncated,
143
+ summary: summarizeElements(elements)
144
+ };
145
+ }
146
+ function compactMemoryResults(results) {
147
+ return { results: results.map((r) => {
148
+ const compact = { ...r };
149
+ delete compact.score;
150
+ delete compact.lastVerifiedAt;
151
+ if (Array.isArray(compact.selectorAliases)) compact.selectorAliases = compact.selectorAliases.map((alias) => {
152
+ const compactAlias = { ...alias };
153
+ if (Array.isArray(compactAlias.fallbackSelectors) && compactAlias.fallbackSelectors.length === 0) delete compactAlias.fallbackSelectors;
154
+ return compactAlias;
155
+ });
156
+ return compact;
157
+ }) };
158
+ }
159
+
160
+ //#endregion
7
161
  //#region src/mcp/index.ts
8
162
  let core;
9
163
  let activeSessionId;
@@ -48,9 +202,15 @@ server.tool("browser_navigate", "Navigate the browser to a URL. A session is aut
48
202
  type: "navigate",
49
203
  payload: { url }
50
204
  });
205
+ let cookieBanner;
206
+ if (result.resultStatus === "success") try {
207
+ const dismissed = await getCore().dismissCookieBanner(sid);
208
+ if (dismissed.dismissed) cookieBanner = dismissed;
209
+ } catch {}
51
210
  const compact = {
52
211
  resultStatus: result.resultStatus,
53
- resultMessage: result.resultMessage
212
+ resultMessage: result.resultMessage,
213
+ cookieBanner
54
214
  };
55
215
  return { content: [{
56
216
  type: "text",
@@ -124,30 +284,9 @@ server.tool("browser_get_content", "Get the current page content. Modes: \"text\
124
284
  mode,
125
285
  selector
126
286
  });
127
- const limit = maxChars ?? {
128
- text: 12e3,
129
- a11y: 12e3,
130
- html: 6e3,
131
- title: void 0
132
- }[mode];
133
- if (limit && typeof result.content === "string" && result.content.length > limit) {
134
- const originalLength = result.content.length;
135
- const truncatedContent = result.content.slice(0, limit) + `\n\n[Truncated — showing first ${limit} of ${originalLength} characters. Use a CSS selector to scope the content.]`;
136
- return { content: [{
137
- type: "text",
138
- text: JSON.stringify({
139
- ...result,
140
- content: truncatedContent,
141
- truncated: true
142
- })
143
- }] };
144
- }
145
287
  return { content: [{
146
288
  type: "text",
147
- text: JSON.stringify({
148
- ...result,
149
- truncated: false
150
- })
289
+ text: JSON.stringify(compactPageContent(result, maxChars))
151
290
  }] };
152
291
  });
153
292
  server.tool("browser_get_elements", "Discover all interactive elements on the current page (buttons, links, inputs, etc.). Returns CSS selectors and fallbackSelectors you can use with browser_interact. Pass fallbackSelectors to browser_interact for automatic retry when the primary selector breaks. Actions are derived from role: link/button/custom→click, input/textarea/contenteditable→click+type+press, select→click+select, checkbox/radio→toggle. A session is auto-started if needed.", {
@@ -175,21 +314,9 @@ server.tool("browser_get_elements", "Discover all interactive elements on the cu
175
314
  limit,
176
315
  selector
177
316
  });
178
- const compactElements = result.elements.map((el) => {
179
- const compact = { ...el };
180
- if (visibleOnly) delete compact.visible;
181
- delete compact.actions;
182
- delete compact.tagName;
183
- if (compact.ariaLabel && compact.ariaLabel === compact.text) delete compact.ariaLabel;
184
- return compact;
185
- });
186
317
  return { content: [{
187
318
  type: "text",
188
- text: JSON.stringify({
189
- elements: compactElements,
190
- totalFound: result.totalFound,
191
- truncated: result.truncated
192
- })
319
+ text: JSON.stringify(compactInteractiveElementsResult(result, visibleOnly))
193
320
  }] };
194
321
  });
195
322
  server.tool("browser_search_memory", "Search task memory for previously learned selectors, selector aliases, and interaction patterns. Results include selectorHints (proven selectors) and selectorAliases (human-readable names mapped to selectors with fallbacks). Use this before interacting with a known site to reuse proven selectors instead of rediscovering them.", {
@@ -197,24 +324,22 @@ server.tool("browser_search_memory", "Search task memory for previously learned
197
324
  siteDomain: z.string().optional().describe("Domain to scope the search"),
198
325
  limit: z.number().default(5).describe("Maximum number of results")
199
326
  }, async ({ taskIntent, siteDomain, limit }) => {
200
- const compactResults = getCore().searchMemory({
327
+ const result = getCore().searchMemory({
201
328
  taskIntent,
202
329
  siteDomain,
203
330
  limit
204
- }).results.map((r) => {
205
- const compact = { ...r };
206
- delete compact.score;
207
- delete compact.lastVerifiedAt;
208
- if (Array.isArray(compact.selectorAliases)) compact.selectorAliases = compact.selectorAliases.map((alias) => {
209
- const compactAlias = { ...alias };
210
- if (Array.isArray(compactAlias.fallbackSelectors) && compactAlias.fallbackSelectors.length === 0) delete compactAlias.fallbackSelectors;
211
- return compactAlias;
212
- });
213
- return compact;
214
331
  });
215
332
  return { content: [{
216
333
  type: "text",
217
- text: JSON.stringify({ results: compactResults })
334
+ text: JSON.stringify(compactMemoryResults(result.results))
335
+ }] };
336
+ });
337
+ server.tool("browser_dismiss_cookies", "Dismiss cookie consent banners on the current page. Uses the accessibility tree first (most robust), then falls back to known CSS selectors and text-based button matching. Supports banners in English, German, French, Spanish, Italian, Dutch, and Portuguese. Call this after navigating to a new page. A session is auto-started if needed.", { sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)") }, async ({ sessionId }) => {
338
+ const sid = await resolveSession(sessionId);
339
+ const result = await getCore().dismissCookieBanner(sid);
340
+ return { content: [{
341
+ type: "text",
342
+ text: JSON.stringify(result)
218
343
  }] };
219
344
  });
220
345
  server.tool("browser_stop_session", "Stop the browser session and terminate Chrome. The next browser tool call will auto-start a fresh session.", { sessionId: z.string().optional().describe("Session ID (uses active session if omitted)") }, async ({ sessionId }) => {
@@ -245,6 +370,9 @@ async function main() {
245
370
  } catch {}
246
371
  activeSessionId = void 0;
247
372
  }
373
+ try {
374
+ getCore().context.memoryService.flushSync();
375
+ } catch {}
248
376
  try {
249
377
  getCore().sessions.cleanupSessions({ maxAgeDays: 0 });
250
378
  } catch {}
@@ -50,6 +50,7 @@ var CdpConnection = class CdpConnection {
50
50
  return new CdpConnection(ws);
51
51
  }
52
52
  async send(method, params, timeoutMs = 15e3) {
53
+ if (this.ws.readyState !== WebSocket.OPEN) throw new Error(`WebSocket not open (state=${this.ws.readyState}), cannot send '${method}'`);
53
54
  const id = ++this.nextId;
54
55
  const payload = {
55
56
  id,
@@ -106,8 +107,8 @@ var CdpConnection = class CdpConnection {
106
107
  this.ws.close();
107
108
  }
108
109
  };
109
- async function getJson(url) {
110
- const response = await fetch(url);
110
+ async function getJson(url, timeoutMs = 5e3) {
111
+ const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
111
112
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);
112
113
  return await response.json();
113
114
  }
@@ -202,9 +203,15 @@ function resolveDefaultProfileDir() {
202
203
  }
203
204
  var ChromeCdpBrowserController = class {
204
205
  connections = /* @__PURE__ */ new Map();
206
+ cleanupInterval;
205
207
  constructor(baseDir, connectionFactory = CdpConnection.connect) {
206
208
  this.baseDir = baseDir;
207
209
  this.connectionFactory = connectionFactory;
210
+ this.cleanupInterval = setInterval(() => {
211
+ const cutoff = Date.now() - 300 * 1e3;
212
+ for (const [url, entry] of this.connections) if (entry.lastUsedAt < cutoff) this.dropConnection(url);
213
+ }, 6e4);
214
+ this.cleanupInterval.unref();
208
215
  }
209
216
  async getConnection(targetWsUrl) {
210
217
  const cached = this.connections.get(targetWsUrl);
@@ -354,7 +361,8 @@ var ChromeCdpBrowserController = class {
354
361
  await this.ensureEnabled(targetWsUrl);
355
362
  const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 6e3).catch(() => void 0);
356
363
  const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
357
- await conn.send("Page.navigate", { url });
364
+ const navResult = await conn.send("Page.navigate", { url });
365
+ if (navResult.errorText) throw new Error(`Navigation failed: ${navResult.errorText}`);
358
366
  const navigatedEvent = await navigatedPromise;
359
367
  try {
360
368
  await loadPromise;
@@ -913,6 +921,164 @@ var ChromeCdpBrowserController = class {
913
921
  }));
914
922
  });
915
923
  }
924
+ /**
925
+ * Best-effort cookie/consent banner dismissal.
926
+ *
927
+ * Strategy (in order):
928
+ * 1. A11y tree scan — find buttons with consent-like accessible names
929
+ * 2. CSS selector scan — check common consent framework IDs/classes
930
+ * 3. Text-based scan — find visible buttons matching accept/agree patterns
931
+ *
932
+ * Returns silently on failure — never blocks navigation.
933
+ */
934
+ async dismissCookieBanner(targetWsUrl) {
935
+ try {
936
+ const domResult = await this.dismissViaDom(targetWsUrl);
937
+ if (domResult.dismissed) return domResult;
938
+ } catch {}
939
+ try {
940
+ const a11yResult = await this.dismissViaA11y(targetWsUrl);
941
+ if (a11yResult.dismissed) return a11yResult;
942
+ } catch {}
943
+ return { dismissed: false };
944
+ }
945
+ async dismissViaDom(targetWsUrl) {
946
+ const expression = `(() => {
947
+ // Common cookie-consent button selectors (ordered by specificity)
948
+ const selectors = [
949
+ '#onetrust-accept-btn-handler',
950
+ '#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll',
951
+ '#CybotCookiebotDialogBodyButtonAccept',
952
+ '#accept-cookies', '#acceptCookies',
953
+ '#cookie-accept', '#cookieAccept',
954
+ '#gdpr-accept', '#consent-accept',
955
+ '#uc-btn-accept-banner',
956
+ '.cc-accept', '.cc-btn.cc-dismiss',
957
+ '.cookie-consent-accept', '.cookie-accept-btn',
958
+ '.js-cookie-accept', '.js-accept-cookies',
959
+ '.consent-accept', '.consent-btn-accept',
960
+ '.gdpr-accept',
961
+ '[data-testid="cookie-accept"]',
962
+ '[data-action="accept-cookies"]',
963
+ '[data-cookiefirst-action="accept"]',
964
+ ];
965
+
966
+ for (const sel of selectors) {
967
+ const el = document.querySelector(sel);
968
+ if (el && el.offsetParent !== null) {
969
+ el.click();
970
+ return JSON.stringify({ dismissed: true, method: 'selector', detail: sel });
971
+ }
972
+ }
973
+
974
+ // Text-based fallback: find visible buttons with consent text
975
+ const patterns = [
976
+ /^accept\\s*(all)?\\s*(cookies)?$/i,
977
+ /^(alle\\s+)?akzeptieren$/i,
978
+ /^(i\\s+)?agree$/i,
979
+ /^allow\\s*(all)?\\s*(cookies)?$/i,
980
+ /^(alle\\s+)?zustimmen$/i,
981
+ /^got\\s*it$/i,
982
+ /^ok$/i,
983
+ /^consent$/i,
984
+ /^(j')?accepte(r)?$/i,
985
+ /^(tout\\s+)?accepter$/i,
986
+ ];
987
+ const candidates = [
988
+ ...document.querySelectorAll('button'),
989
+ ...document.querySelectorAll('a[role="button"]'),
990
+ ...document.querySelectorAll('[role="button"]'),
991
+ ];
992
+ for (const el of candidates) {
993
+ if (!el.offsetParent) continue;
994
+ const text = (el.textContent || '').trim();
995
+ if (text.length > 40) continue;
996
+ for (const pat of patterns) {
997
+ if (pat.test(text)) {
998
+ el.click();
999
+ return JSON.stringify({ dismissed: true, method: 'text', detail: text });
1000
+ }
1001
+ }
1002
+ }
1003
+
1004
+ return JSON.stringify({ dismissed: false });
1005
+ })()`;
1006
+ return await this.withRetry(targetWsUrl, async (conn) => {
1007
+ await this.ensureEnabled(targetWsUrl);
1008
+ const raw = (await conn.send("Runtime.evaluate", {
1009
+ expression,
1010
+ returnByValue: true
1011
+ })).result.value ?? "{\"dismissed\":false}";
1012
+ try {
1013
+ return JSON.parse(raw);
1014
+ } catch {
1015
+ return { dismissed: false };
1016
+ }
1017
+ });
1018
+ }
1019
+ /**
1020
+ * Use the CDP Accessibility tree to find and click consent buttons.
1021
+ * This works even when consent banners use shadow DOM, iframes, or obfuscated class names.
1022
+ */
1023
+ async dismissViaA11y(targetWsUrl) {
1024
+ return await this.withRetry(targetWsUrl, async (conn) => {
1025
+ await conn.send("Accessibility.enable");
1026
+ const { nodes } = await conn.send("Accessibility.getFullAXTree");
1027
+ const consentPatterns = [
1028
+ /^accept\s*(all)?\s*(cookies)?$/i,
1029
+ /^(i\s+)?agree(\s+to\s+all)?$/i,
1030
+ /^allow\s*(all)?\s*(cookies)?$/i,
1031
+ /^got\s*it$/i,
1032
+ /^ok$/i,
1033
+ /^consent$/i,
1034
+ /^(alle\s+)?akzeptieren$/i,
1035
+ /^(alle\s+)?zustimmen$/i,
1036
+ /^einverstanden$/i,
1037
+ /^(j')?accepte(r)?(\s+tout)?$/i,
1038
+ /^(tout\s+)?accepter$/i,
1039
+ /^aceptar(\s+todo)?$/i,
1040
+ /^accetta(\s+tutto)?$/i,
1041
+ /^accepteren$/i,
1042
+ /^alle\s+accepteren$/i,
1043
+ /^aceitar(\s+tudo)?$/i
1044
+ ];
1045
+ const candidates = [];
1046
+ for (const node of nodes) {
1047
+ if (node.ignored) continue;
1048
+ const role = node.role?.value;
1049
+ if (role !== "button" && role !== "link") continue;
1050
+ const name = node.name?.value?.trim();
1051
+ if (!name || name.length > 50) continue;
1052
+ if (node.properties?.some((p) => p.name === "disabled" && p.value.value === true)) continue;
1053
+ for (const pat of consentPatterns) if (pat.test(name) && node.backendDOMNodeId) {
1054
+ candidates.push({
1055
+ nodeId: node.nodeId,
1056
+ backendDOMNodeId: node.backendDOMNodeId,
1057
+ name
1058
+ });
1059
+ break;
1060
+ }
1061
+ }
1062
+ if (candidates.length === 0) return { dismissed: false };
1063
+ const target = candidates[0];
1064
+ try {
1065
+ const { object } = await conn.send("DOM.resolveNode", { backendNodeId: target.backendDOMNodeId });
1066
+ if (object?.objectId) {
1067
+ await conn.send("Runtime.callFunctionOn", {
1068
+ objectId: object.objectId,
1069
+ functionDeclaration: "function() { this.click(); }",
1070
+ returnByValue: true
1071
+ });
1072
+ return {
1073
+ dismissed: true,
1074
+ method: "a11y",
1075
+ detail: target.name
1076
+ };
1077
+ }
1078
+ } catch {}
1079
+ return { dismissed: false };
1080
+ });
1081
+ }
916
1082
  terminate(pid) {
917
1083
  if (pid === 0) return;
918
1084
  try {
@@ -1006,6 +1172,9 @@ var MockBrowserController = class {
1006
1172
  truncated: false
1007
1173
  };
1008
1174
  }
1175
+ async dismissCookieBanner(_targetWsUrl) {
1176
+ return { dismissed: false };
1177
+ }
1009
1178
  terminate(_pid) {}
1010
1179
  closeConnection(_targetWsUrl) {}
1011
1180
  };
@@ -1014,6 +1183,7 @@ var MockBrowserController = class {
1014
1183
  //#region src/session/session-store.ts
1015
1184
  var SessionStore = class {
1016
1185
  filePath;
1186
+ cache = null;
1017
1187
  constructor(baseDir) {
1018
1188
  fs.mkdirSync(baseDir, { recursive: true });
1019
1189
  this.filePath = path.join(baseDir, "sessions.json");
@@ -1081,9 +1251,19 @@ var SessionStore = class {
1081
1251
  this.write(state);
1082
1252
  }
1083
1253
  read() {
1084
- return JSON.parse(fs.readFileSync(this.filePath, "utf8"));
1254
+ if (this.cache) return this.cache;
1255
+ try {
1256
+ const parsed = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
1257
+ this.cache = parsed;
1258
+ return parsed;
1259
+ } catch {
1260
+ const empty = { sessions: {} };
1261
+ this.write(empty);
1262
+ return empty;
1263
+ }
1085
1264
  }
1086
1265
  write(state) {
1266
+ this.cache = state;
1087
1267
  fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), "utf8");
1088
1268
  }
1089
1269
  };
@@ -1222,6 +1402,10 @@ var SessionManager = class {
1222
1402
  const record = await this.ensureSession(sessionId);
1223
1403
  return await this.browser.getInteractiveElements(record.targetWsUrl, options);
1224
1404
  }
1405
+ async dismissCookieBanner(sessionId) {
1406
+ const record = await this.ensureSession(sessionId);
1407
+ return await this.browser.dismissCookieBanner(record.targetWsUrl);
1408
+ }
1225
1409
  setStatus(status, reason) {
1226
1410
  const active = this.store.getActive();
1227
1411
  if (!active) throw new Error("No active session");
@@ -1488,6 +1672,9 @@ var ControlApi = class {
1488
1672
  async getInteractiveElements(sessionId, options) {
1489
1673
  return await this.sessions.getInteractiveElements(sessionId, options);
1490
1674
  }
1675
+ async dismissCookieBanner(sessionId) {
1676
+ return await this.sessions.dismissCookieBanner(sessionId);
1677
+ }
1491
1678
  listEvents(sessionId, limit = 100) {
1492
1679
  return { events: this.eventStore.list(sessionId, limit) };
1493
1680
  }
@@ -1526,7 +1713,7 @@ function loadConfig(env = process.env) {
1526
1713
  wsPort,
1527
1714
  commandTimeoutMs,
1528
1715
  logDir: env.AGENTIC_BROWSER_LOG_DIR ?? path.resolve(process.cwd(), ".agentic-browser"),
1529
- browserExecutablePath: env.AGENTIC_BROWSER_CHROME_PATH,
1716
+ browserExecutablePath: env.AGENTIC_BROWSER_CHROME_EXECUTABLE_PATH ?? env.AGENTIC_BROWSER_CHROME_PATH,
1530
1717
  cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
1531
1718
  userProfileDir,
1532
1719
  headless: env.AGENTIC_BROWSER_HEADLESS === "true" || env.AGENTIC_BROWSER_HEADLESS === "1",
@@ -1568,54 +1755,55 @@ var Logger = class {
1568
1755
 
1569
1756
  //#endregion
1570
1757
  //#region src/observability/event-store.ts
1758
+ const MAX_EVENTS_PER_SESSION = 500;
1571
1759
  var EventStore = class {
1572
1760
  events = /* @__PURE__ */ new Map();
1573
1761
  filePath;
1574
1762
  constructor(baseDir) {
1575
- this.baseDir = baseDir;
1576
1763
  fs.mkdirSync(baseDir, { recursive: true });
1577
1764
  this.filePath = path.join(baseDir, "session-events.log");
1578
1765
  }
1579
1766
  append(event) {
1580
- const existing = this.events.get(event.sessionId) ?? [];
1767
+ let existing = this.events.get(event.sessionId) ?? [];
1581
1768
  existing.push(event);
1769
+ if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-MAX_EVENTS_PER_SESSION);
1582
1770
  this.events.set(event.sessionId, existing);
1583
- fs.appendFile(this.filePath, `${JSON.stringify(event)}\n`, "utf8", () => {});
1771
+ fs.appendFileSync(this.filePath, `${JSON.stringify(event)}\n`, "utf8");
1584
1772
  }
1585
1773
  list(sessionId, limit = 100) {
1586
1774
  const entries = this.events.get(sessionId) ?? [];
1587
1775
  return entries.slice(Math.max(0, entries.length - limit));
1588
1776
  }
1777
+ clear(sessionId) {
1778
+ this.events.delete(sessionId);
1779
+ }
1589
1780
  };
1590
1781
 
1591
1782
  //#endregion
1592
1783
  //#region src/auth/session-token.ts
1593
1784
  var SessionTokenService = class {
1594
- bySession = /* @__PURE__ */ new Map();
1785
+ tokens = /* @__PURE__ */ new Map();
1595
1786
  issue(sessionId) {
1596
1787
  const token = crypto.randomBytes(24).toString("hex");
1597
- this.bySession.set(sessionId, { token });
1788
+ this.tokens.set(sessionId, token);
1598
1789
  return token;
1599
1790
  }
1600
1791
  validate(sessionId, token) {
1601
- const record = this.bySession.get(sessionId);
1602
- if (!record || record.revokedAt) return false;
1603
- const expected = Buffer.from(record.token);
1604
- const provided = Buffer.from(token);
1605
- if (expected.length !== provided.length) return false;
1606
- return crypto.timingSafeEqual(expected, provided);
1792
+ const expected = this.tokens.get(sessionId);
1793
+ if (!expected) return false;
1794
+ const expectedBuf = Buffer.from(expected);
1795
+ const providedBuf = Buffer.from(token);
1796
+ if (expectedBuf.length !== providedBuf.length) return false;
1797
+ return crypto.timingSafeEqual(expectedBuf, providedBuf);
1607
1798
  }
1608
1799
  revoke(sessionId) {
1609
- const record = this.bySession.get(sessionId);
1610
- if (!record) return;
1611
- record.revokedAt = (/* @__PURE__ */ new Date()).toISOString();
1612
- this.bySession.set(sessionId, record);
1800
+ this.tokens.delete(sessionId);
1613
1801
  }
1614
1802
  get(sessionId) {
1615
- return this.bySession.get(sessionId)?.token;
1803
+ return this.tokens.get(sessionId);
1616
1804
  }
1617
1805
  seed(sessionId, token) {
1618
- this.bySession.set(sessionId, { token });
1806
+ this.tokens.set(sessionId, token);
1619
1807
  }
1620
1808
  };
1621
1809
 
@@ -1853,6 +2041,8 @@ const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
1853
2041
  //#region src/memory/task-insight-store.ts
1854
2042
  const EMPTY_STATE = { insights: [] };
1855
2043
  const FLUSH_DELAY_MS = 500;
2044
+ const REGISTERED_EXIT_HANDLERS = Symbol.for("agentic-browser.task-insight-store.exit-handlers");
2045
+ const REGISTERED_STORES = Symbol.for("agentic-browser.task-insight-store.instances");
1856
2046
  var TaskInsightStore = class {
1857
2047
  filePath;
1858
2048
  /** In-memory cache – authoritative after first load. */
@@ -1868,16 +2058,7 @@ var TaskInsightStore = class {
1868
2058
  if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
1869
2059
  } catch {}
1870
2060
  if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE);
1871
- const onExit = () => this.flushSync();
1872
- process.on("exit", onExit);
1873
- process.on("SIGINT", () => {
1874
- this.flushSync();
1875
- process.exit(0);
1876
- });
1877
- process.on("SIGTERM", () => {
1878
- this.flushSync();
1879
- process.exit(0);
1880
- });
2061
+ this.registerProcessHandlers();
1881
2062
  }
1882
2063
  list() {
1883
2064
  return this.getCache();
@@ -1917,13 +2098,10 @@ var TaskInsightStore = class {
1917
2098
  }
1918
2099
  markDirty() {
1919
2100
  this.dirty = true;
1920
- if (!this.flushTimer) {
1921
- this.flushTimer = setTimeout(() => {
1922
- this.flushTimer = null;
1923
- this.flushSync();
1924
- }, FLUSH_DELAY_MS);
1925
- if (this.flushTimer && typeof this.flushTimer === "object" && "unref" in this.flushTimer) this.flushTimer.unref();
1926
- }
2101
+ if (!this.flushTimer) this.flushTimer = setTimeout(() => {
2102
+ this.flushTimer = null;
2103
+ this.flushSync();
2104
+ }, FLUSH_DELAY_MS);
1927
2105
  }
1928
2106
  /** Read and validate from disk (only on first load or corruption recovery). */
1929
2107
  readDisk() {
@@ -1962,6 +2140,25 @@ var TaskInsightStore = class {
1962
2140
  fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf8");
1963
2141
  fs.renameSync(tempPath, this.filePath);
1964
2142
  }
2143
+ registerProcessHandlers() {
2144
+ const proc = process;
2145
+ const stores = proc[REGISTERED_STORES] ??= /* @__PURE__ */ new Set();
2146
+ stores.add(this);
2147
+ if (proc[REGISTERED_EXIT_HANDLERS]) return;
2148
+ proc[REGISTERED_EXIT_HANDLERS] = true;
2149
+ const flushAll = () => {
2150
+ for (const store of stores) store.flushSync();
2151
+ };
2152
+ process.on("exit", flushAll);
2153
+ process.on("SIGINT", () => {
2154
+ flushAll();
2155
+ process.exit(0);
2156
+ });
2157
+ process.on("SIGTERM", () => {
2158
+ flushAll();
2159
+ process.exit(0);
2160
+ });
2161
+ }
1965
2162
  };
1966
2163
 
1967
2164
  //#endregion
@@ -1976,6 +2173,10 @@ var MemoryService = class {
1976
2173
  this.store = new TaskInsightStore(baseDir);
1977
2174
  this.index = new MemoryIndex();
1978
2175
  }
2176
+ /** Force an immediate synchronous flush of pending writes. */
2177
+ flushSync() {
2178
+ this.store.flushSync();
2179
+ }
1979
2180
  search(input) {
1980
2181
  const cacheKey = `${input.taskIntent}\0${input.siteDomain ?? ""}\0${input.limit ?? 10}`;
1981
2182
  const now = Date.now();
@@ -2026,7 +2227,7 @@ var MemoryService = class {
2026
2227
  }
2027
2228
  recordSuccess(input) {
2028
2229
  const insights = this.store.list();
2029
- const matched = this.findBestExactMatch(insights, input.taskIntent, input.siteDomain);
2230
+ const matched = this.findBestExactMatch(insights, input);
2030
2231
  const evidence = this.createEvidence(input, "success");
2031
2232
  if (!matched) {
2032
2233
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -2083,7 +2284,7 @@ var MemoryService = class {
2083
2284
  }
2084
2285
  recordFailure(input, errorMessage) {
2085
2286
  const insights = this.store.list();
2086
- const matched = this.findBestExactMatch(insights, input.taskIntent, input.siteDomain);
2287
+ const matched = this.findBestExactMatch(insights, input);
2087
2288
  if (!matched) return;
2088
2289
  const signal = detectStalenessSignal(errorMessage, input.selector);
2089
2290
  const evidence = this.createEvidence(input, "failure", errorMessage);
@@ -2096,15 +2297,37 @@ var MemoryService = class {
2096
2297
  this.invalidateSearchCache();
2097
2298
  return failed;
2098
2299
  }
2099
- findBestExactMatch(insights, taskIntent, siteDomain) {
2100
- const intentLower = taskIntent.toLowerCase();
2101
- const domainLower = siteDomain.toLowerCase();
2300
+ findBestExactMatch(insights, input) {
2301
+ const intentLower = input.taskIntent.toLowerCase();
2302
+ const domainLower = input.siteDomain.toLowerCase();
2102
2303
  let best;
2103
2304
  for (const insight of insights) if (insight.taskIntent.toLowerCase() === intentLower && insight.siteDomain.toLowerCase() === domainLower) {
2104
- if (!best || insight.updatedAt > best.updatedAt) best = insight;
2305
+ if (!best || this.scoreExactMatch(insight, input) > this.scoreExactMatch(best, input) || this.scoreExactMatch(insight, input) === this.scoreExactMatch(best, input) && insight.updatedAt > best.updatedAt) best = insight;
2105
2306
  }
2106
2307
  return best;
2107
2308
  }
2309
+ scoreExactMatch(insight, input) {
2310
+ let score = 0;
2311
+ if (insight.sitePathPattern === input.sitePathPattern) score += 4;
2312
+ else if (insight.sitePathPattern === "/" || input.sitePathPattern === "/" || this.sameFirstPathSegment(insight.sitePathPattern, input.sitePathPattern)) score += 2;
2313
+ if (input.selector) {
2314
+ if (insight.actionRecipe.some((step) => step.selector === input.selector)) score += 3;
2315
+ if (insight.evidence.some((ev) => ev.selector === input.selector && ev.result === "success")) score += 2;
2316
+ if ((insight.selectorAliases ?? []).some((alias) => alias.selector === input.selector)) score += 1;
2317
+ }
2318
+ score += Math.min(insight.successCount, 5) * .1;
2319
+ score -= Math.min(insight.failureCount, 5) * .1;
2320
+ return score;
2321
+ }
2322
+ sameFirstPathSegment(left, right) {
2323
+ const normalize = (value) => {
2324
+ const [firstSegment = ""] = value.replace(/\/\*$/, "").split("/").filter(Boolean);
2325
+ return firstSegment.toLowerCase();
2326
+ };
2327
+ const leftSegment = normalize(left);
2328
+ const rightSegment = normalize(right);
2329
+ return Boolean(leftSegment) && leftSegment === rightSegment;
2330
+ }
2108
2331
  createEvidence(input, result, reason) {
2109
2332
  return {
2110
2333
  evidenceId: crypto.randomUUID(),
@@ -2217,6 +2440,9 @@ var AgenticBrowserCore = class {
2217
2440
  selector: input.selector
2218
2441
  });
2219
2442
  }
2443
+ async dismissCookieBanner(sessionId) {
2444
+ return await this.api.dismissCookieBanner(sessionId);
2445
+ }
2220
2446
  async restartSession(sessionId) {
2221
2447
  return await this.api.restartSession(sessionId);
2222
2448
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-browser",
3
- "version": "1.5.1",
3
+ "version": "1.6.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",