agentic-browser 1.6.0 → 2.0.0

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
@@ -111,8 +111,8 @@ agentic-browser agent start --user-profile default
111
111
  agentic-browser agent start --headless
112
112
  agentic-browser agent start --user-agent "MyBot/1.0"
113
113
  agentic-browser agent status
114
- agentic-browser agent run navigate '{"url":"https://example.com"}'
115
- agentic-browser agent run interact '{"action":"click","selector":"#login"}'
114
+ agentic-browser agent navigate https://example.com
115
+ agentic-browser agent click "#login"
116
116
  agentic-browser agent content --mode text
117
117
  agentic-browser agent content --mode html --selector main
118
118
  agentic-browser agent elements
@@ -132,7 +132,7 @@ agentic-browser agent elements --roles button,link,input --visible-only --limit
132
132
  agentic-browser agent elements --selector "#main-content"
133
133
  ```
134
134
 
135
- Returns a JSON array of elements with CSS selectors and fallback selectors usable in `agent run interact`:
135
+ Returns a JSON array of elements with CSS selectors and fallback selectors usable in the typed agent commands like `agent click`, `agent type`, and `agent select`:
136
136
 
137
137
  ```json
138
138
  {
@@ -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
 
@@ -255,7 +255,7 @@ agentic-browser memory:stats
255
255
 
256
256
  1. `agent start` — launch Chrome and persist session.
257
257
  2. `agent elements` — discover what's on the page.
258
- 3. `agent run navigate/interact` — execute actions using discovered selectors.
258
+ 3. `agent navigate`, `agent click`, `agent type`, `agent select`, and related typed commands — execute actions using discovered selectors.
259
259
  4. `agent content` — read page content after actions.
260
260
  5. `agent memory-search` — reuse known selectors for repeated tasks.
261
261
  6. `agent stop` — terminate when done.
@@ -265,7 +265,7 @@ agentic-browser memory:stats
265
265
  - Exactly **one** managed session is supported at a time.
266
266
  - Session state is persisted in `.agentic-browser/`.
267
267
  - All commands print exactly one JSON line to `stdout`.
268
- - `payloadJson` must be valid JSON.
268
+ - The typed `agent` commands are the preferred interface for LLMs. Use low-level `command:run` only when you need raw JSON payload control.
269
269
  - Parse only `stdout` as result object and use exit code for success/failure.
270
270
 
271
271
  ## Programmatic API
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as createCliRuntime } from "../runtime-C7B024bN.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";
@@ -64,7 +64,7 @@ async function agentStop(runtime) {
64
64
  sessionId
65
65
  };
66
66
  }
67
- async function agentRun(runtime, input) {
67
+ async function executeAgentCommand(runtime, input) {
68
68
  const sessionId = requireSessionId(runtime);
69
69
  await ensureReady(runtime, sessionId);
70
70
  const attempt = () => runtime.api.executeCommand(sessionId, {
@@ -75,22 +75,75 @@ async function agentRun(runtime, input) {
75
75
  try {
76
76
  const first = await attempt();
77
77
  if (first.resultStatus === "failed") throw new Error(first.resultMessage ?? "Command failed");
78
- return {
79
- ok: true,
80
- action: "run",
81
- ...first
82
- };
78
+ return first;
83
79
  } catch {
84
80
  await runtime.api.restartSession(sessionId);
85
81
  const second = await attempt();
86
82
  if (second.resultStatus === "failed") throw new Error(second.resultMessage ?? "Command failed after retry");
87
- return {
88
- ok: true,
89
- action: "run",
90
- ...second
91
- };
83
+ return second;
92
84
  }
93
85
  }
86
+ async function agentNavigate(runtime, input) {
87
+ const result = await executeAgentCommand(runtime, {
88
+ type: "navigate",
89
+ payload: { url: input.url }
90
+ });
91
+ let cookieBanner;
92
+ if (result.resultStatus === "success") try {
93
+ const sessionId = requireSessionId(runtime);
94
+ const dismissed = await runtime.api.dismissCookieBanner(sessionId);
95
+ if (dismissed.dismissed) cookieBanner = dismissed;
96
+ } catch {}
97
+ return {
98
+ ok: true,
99
+ action: "navigate",
100
+ ...result,
101
+ cookieBanner
102
+ };
103
+ }
104
+ async function agentInteract(runtime, input) {
105
+ const result = await executeAgentCommand(runtime, {
106
+ type: "interact",
107
+ payload: input
108
+ });
109
+ return {
110
+ ok: true,
111
+ action: input.action,
112
+ ...result
113
+ };
114
+ }
115
+ async function agentRestart(runtime) {
116
+ return {
117
+ ok: true,
118
+ action: "restart",
119
+ ...await executeAgentCommand(runtime, {
120
+ type: "restart",
121
+ payload: {}
122
+ })
123
+ };
124
+ }
125
+ async function agentTerminate(runtime) {
126
+ const result = await executeAgentCommand(runtime, {
127
+ type: "terminate",
128
+ payload: {}
129
+ });
130
+ saveState(runtime, { sessionId: null });
131
+ return {
132
+ ok: true,
133
+ action: "terminate",
134
+ ...result
135
+ };
136
+ }
137
+ async function agentDismissCookies(runtime) {
138
+ const sessionId = requireSessionId(runtime);
139
+ await ensureReady(runtime, sessionId);
140
+ return {
141
+ ok: true,
142
+ action: "cookies",
143
+ sessionId,
144
+ ...await runtime.api.dismissCookieBanner(sessionId)
145
+ };
146
+ }
94
147
  async function agentContent(runtime, input) {
95
148
  const sessionId = requireSessionId(runtime);
96
149
  await ensureReady(runtime, sessionId);
@@ -243,6 +296,7 @@ async function main() {
243
296
  const runtime = createCliRuntime();
244
297
  const program = new Command();
245
298
  program.name("agentic-browser").description("Agentic browser CLI");
299
+ const collect = (value, previous = []) => [...previous, value];
246
300
  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) => {
247
301
  if (options.cdpUrl) runtime.context.config.cdpUrl = options.cdpUrl;
248
302
  if (options.userProfile) runtime.context.config.userProfileDir = options.userProfile === "true" || options.userProfile === "default" ? "default" : options.userProfile;
@@ -328,13 +382,109 @@ async function main() {
328
382
  const result = await agentStop(runtime);
329
383
  console.log(JSON.stringify(result));
330
384
  });
331
- agent.command("run").argument("<type>", "navigate|interact|restart|terminate").argument("<payloadJson>", "JSON payload").action(async (type, payloadJson) => {
332
- const result = await agentRun(runtime, {
333
- type,
334
- payload: JSON.parse(payloadJson)
385
+ agent.command("navigate").argument("<url>").action(async (url) => {
386
+ const result = await agentNavigate(runtime, { url });
387
+ console.log(JSON.stringify(result));
388
+ });
389
+ agent.command("click").argument("<selector>").option("--fallback <selector>", "backup CSS selector", collect, []).action(async (selector, options) => {
390
+ const result = await agentInteract(runtime, {
391
+ action: "click",
392
+ selector,
393
+ fallbackSelectors: options.fallback
394
+ });
395
+ console.log(JSON.stringify(result));
396
+ });
397
+ agent.command("type").argument("<selector>").argument("<text>").option("--fallback <selector>", "backup CSS selector", collect, []).action(async (selector, text, options) => {
398
+ const result = await agentInteract(runtime, {
399
+ action: "type",
400
+ selector,
401
+ text,
402
+ fallbackSelectors: options.fallback
335
403
  });
336
404
  console.log(JSON.stringify(result));
337
405
  });
406
+ agent.command("press").argument("<key>").action(async (key) => {
407
+ const result = await agentInteract(runtime, {
408
+ action: "press",
409
+ key
410
+ });
411
+ console.log(JSON.stringify(result));
412
+ });
413
+ agent.command("wait").argument("<selector>").option("--timeout <ms>", "timeout in milliseconds", "2000").action(async (selector, options) => {
414
+ const result = await agentInteract(runtime, {
415
+ action: "waitFor",
416
+ selector,
417
+ timeoutMs: Number.parseInt(options.timeout, 10)
418
+ });
419
+ console.log(JSON.stringify(result));
420
+ });
421
+ agent.command("hover").argument("<selector>").option("--fallback <selector>", "backup CSS selector", collect, []).action(async (selector, options) => {
422
+ const result = await agentInteract(runtime, {
423
+ action: "hover",
424
+ selector,
425
+ fallbackSelectors: options.fallback
426
+ });
427
+ console.log(JSON.stringify(result));
428
+ });
429
+ agent.command("select").argument("<selector>").argument("<value>").option("--fallback <selector>", "backup CSS selector", collect, []).action(async (selector, value, options) => {
430
+ const result = await agentInteract(runtime, {
431
+ action: "select",
432
+ selector,
433
+ value,
434
+ fallbackSelectors: options.fallback
435
+ });
436
+ console.log(JSON.stringify(result));
437
+ });
438
+ agent.command("toggle").argument("<selector>").option("--fallback <selector>", "backup CSS selector", collect, []).action(async (selector, options) => {
439
+ const result = await agentInteract(runtime, {
440
+ action: "toggle",
441
+ selector,
442
+ fallbackSelectors: options.fallback
443
+ });
444
+ console.log(JSON.stringify(result));
445
+ });
446
+ agent.command("scroll").option("--x <pixels>", "horizontal scroll delta", "0").option("--y <pixels>", "vertical scroll delta", "0").option("--selector <selector>", "scroll a specific element instead of the page").option("--fallback <selector>", "backup CSS selector", collect, []).action(async (options) => {
447
+ const result = await agentInteract(runtime, {
448
+ action: "scroll",
449
+ selector: options.selector,
450
+ fallbackSelectors: options.fallback,
451
+ scrollX: Number.parseInt(options.x, 10),
452
+ scrollY: Number.parseInt(options.y, 10)
453
+ });
454
+ console.log(JSON.stringify(result));
455
+ });
456
+ agent.command("back").action(async () => {
457
+ const result = await agentInteract(runtime, { action: "goBack" });
458
+ console.log(JSON.stringify(result));
459
+ });
460
+ agent.command("forward").action(async () => {
461
+ const result = await agentInteract(runtime, { action: "goForward" });
462
+ console.log(JSON.stringify(result));
463
+ });
464
+ agent.command("refresh").action(async () => {
465
+ const result = await agentInteract(runtime, { action: "refresh" });
466
+ console.log(JSON.stringify(result));
467
+ });
468
+ agent.command("dialog").option("--dismiss", "dismiss the current dialog").option("--value <text>", "prompt response text").action(async (options) => {
469
+ const result = await agentInteract(runtime, {
470
+ action: "dialog",
471
+ text: options.dismiss ? "dismiss" : void 0,
472
+ value: options.value
473
+ });
474
+ console.log(JSON.stringify(result));
475
+ });
476
+ agent.command("cookies").action(async () => {
477
+ const result = await agentDismissCookies(runtime);
478
+ console.log(JSON.stringify(result));
479
+ });
480
+ agent.command("restart").action(async () => {
481
+ const result = await agentRestart(runtime);
482
+ console.log(JSON.stringify(result));
483
+ });
484
+ agent.command("terminate").action(async () => {
485
+ const result = await agentTerminate(runtime);
486
+ console.log(JSON.stringify(result));
487
+ });
338
488
  agent.command("content").option("--mode <mode>", "title|text|html|a11y", "text").option("--selector <selector>", "optional CSS selector").action(async (options) => {
339
489
  const result = await agentContent(runtime, options);
340
490
  console.log(JSON.stringify(result));
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-C7B024bN.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-C7B024bN.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,14 @@ 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))
218
335
  }] };
219
336
  });
220
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 }) => {
@@ -253,6 +370,9 @@ async function main() {
253
370
  } catch {}
254
371
  activeSessionId = void 0;
255
372
  }
373
+ try {
374
+ getCore().context.memoryService.flushSync();
375
+ } catch {}
256
376
  try {
257
377
  getCore().sessions.cleanupSessions({ maxAgeDays: 0 });
258
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;
@@ -924,10 +932,17 @@ var ChromeCdpBrowserController = class {
924
932
  * Returns silently on failure — never blocks navigation.
925
933
  */
926
934
  async dismissCookieBanner(targetWsUrl) {
935
+ try {
936
+ const domResult = await this.dismissViaDom(targetWsUrl);
937
+ if (domResult.dismissed) return domResult;
938
+ } catch {}
927
939
  try {
928
940
  const a11yResult = await this.dismissViaA11y(targetWsUrl);
929
941
  if (a11yResult.dismissed) return a11yResult;
930
942
  } catch {}
943
+ return { dismissed: false };
944
+ }
945
+ async dismissViaDom(targetWsUrl) {
931
946
  const expression = `(() => {
932
947
  // Common cookie-consent button selectors (ordered by specificity)
933
948
  const selectors = [
@@ -988,22 +1003,18 @@ var ChromeCdpBrowserController = class {
988
1003
 
989
1004
  return JSON.stringify({ dismissed: false });
990
1005
  })()`;
991
- try {
992
- return await this.withRetry(targetWsUrl, async (conn) => {
993
- await this.ensureEnabled(targetWsUrl);
994
- const raw = (await conn.send("Runtime.evaluate", {
995
- expression,
996
- returnByValue: true
997
- })).result.value ?? "{\"dismissed\":false}";
998
- try {
999
- return JSON.parse(raw);
1000
- } catch {
1001
- return { dismissed: false };
1002
- }
1003
- });
1004
- } catch {
1005
- return { dismissed: false };
1006
- }
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
+ });
1007
1018
  }
1008
1019
  /**
1009
1020
  * Use the CDP Accessibility tree to find and click consent buttons.
@@ -1172,6 +1183,7 @@ var MockBrowserController = class {
1172
1183
  //#region src/session/session-store.ts
1173
1184
  var SessionStore = class {
1174
1185
  filePath;
1186
+ cache = null;
1175
1187
  constructor(baseDir) {
1176
1188
  fs.mkdirSync(baseDir, { recursive: true });
1177
1189
  this.filePath = path.join(baseDir, "sessions.json");
@@ -1239,9 +1251,19 @@ var SessionStore = class {
1239
1251
  this.write(state);
1240
1252
  }
1241
1253
  read() {
1242
- 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
+ }
1243
1264
  }
1244
1265
  write(state) {
1266
+ this.cache = state;
1245
1267
  fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), "utf8");
1246
1268
  }
1247
1269
  };
@@ -1691,7 +1713,7 @@ function loadConfig(env = process.env) {
1691
1713
  wsPort,
1692
1714
  commandTimeoutMs,
1693
1715
  logDir: env.AGENTIC_BROWSER_LOG_DIR ?? path.resolve(process.cwd(), ".agentic-browser"),
1694
- browserExecutablePath: env.AGENTIC_BROWSER_CHROME_PATH,
1716
+ browserExecutablePath: env.AGENTIC_BROWSER_CHROME_EXECUTABLE_PATH ?? env.AGENTIC_BROWSER_CHROME_PATH,
1695
1717
  cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
1696
1718
  userProfileDir,
1697
1719
  headless: env.AGENTIC_BROWSER_HEADLESS === "true" || env.AGENTIC_BROWSER_HEADLESS === "1",
@@ -1733,54 +1755,55 @@ var Logger = class {
1733
1755
 
1734
1756
  //#endregion
1735
1757
  //#region src/observability/event-store.ts
1758
+ const MAX_EVENTS_PER_SESSION = 500;
1736
1759
  var EventStore = class {
1737
1760
  events = /* @__PURE__ */ new Map();
1738
1761
  filePath;
1739
1762
  constructor(baseDir) {
1740
- this.baseDir = baseDir;
1741
1763
  fs.mkdirSync(baseDir, { recursive: true });
1742
1764
  this.filePath = path.join(baseDir, "session-events.log");
1743
1765
  }
1744
1766
  append(event) {
1745
- const existing = this.events.get(event.sessionId) ?? [];
1767
+ let existing = this.events.get(event.sessionId) ?? [];
1746
1768
  existing.push(event);
1769
+ if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-MAX_EVENTS_PER_SESSION);
1747
1770
  this.events.set(event.sessionId, existing);
1748
- fs.appendFile(this.filePath, `${JSON.stringify(event)}\n`, "utf8", () => {});
1771
+ fs.appendFileSync(this.filePath, `${JSON.stringify(event)}\n`, "utf8");
1749
1772
  }
1750
1773
  list(sessionId, limit = 100) {
1751
1774
  const entries = this.events.get(sessionId) ?? [];
1752
1775
  return entries.slice(Math.max(0, entries.length - limit));
1753
1776
  }
1777
+ clear(sessionId) {
1778
+ this.events.delete(sessionId);
1779
+ }
1754
1780
  };
1755
1781
 
1756
1782
  //#endregion
1757
1783
  //#region src/auth/session-token.ts
1758
1784
  var SessionTokenService = class {
1759
- bySession = /* @__PURE__ */ new Map();
1785
+ tokens = /* @__PURE__ */ new Map();
1760
1786
  issue(sessionId) {
1761
1787
  const token = crypto.randomBytes(24).toString("hex");
1762
- this.bySession.set(sessionId, { token });
1788
+ this.tokens.set(sessionId, token);
1763
1789
  return token;
1764
1790
  }
1765
1791
  validate(sessionId, token) {
1766
- const record = this.bySession.get(sessionId);
1767
- if (!record || record.revokedAt) return false;
1768
- const expected = Buffer.from(record.token);
1769
- const provided = Buffer.from(token);
1770
- if (expected.length !== provided.length) return false;
1771
- 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);
1772
1798
  }
1773
1799
  revoke(sessionId) {
1774
- const record = this.bySession.get(sessionId);
1775
- if (!record) return;
1776
- record.revokedAt = (/* @__PURE__ */ new Date()).toISOString();
1777
- this.bySession.set(sessionId, record);
1800
+ this.tokens.delete(sessionId);
1778
1801
  }
1779
1802
  get(sessionId) {
1780
- return this.bySession.get(sessionId)?.token;
1803
+ return this.tokens.get(sessionId);
1781
1804
  }
1782
1805
  seed(sessionId, token) {
1783
- this.bySession.set(sessionId, { token });
1806
+ this.tokens.set(sessionId, token);
1784
1807
  }
1785
1808
  };
1786
1809
 
@@ -2018,6 +2041,8 @@ const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
2018
2041
  //#region src/memory/task-insight-store.ts
2019
2042
  const EMPTY_STATE = { insights: [] };
2020
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");
2021
2046
  var TaskInsightStore = class {
2022
2047
  filePath;
2023
2048
  /** In-memory cache – authoritative after first load. */
@@ -2033,16 +2058,7 @@ var TaskInsightStore = class {
2033
2058
  if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
2034
2059
  } catch {}
2035
2060
  if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE);
2036
- const onExit = () => this.flushSync();
2037
- process.on("exit", onExit);
2038
- process.on("SIGINT", () => {
2039
- this.flushSync();
2040
- process.exit(0);
2041
- });
2042
- process.on("SIGTERM", () => {
2043
- this.flushSync();
2044
- process.exit(0);
2045
- });
2061
+ this.registerProcessHandlers();
2046
2062
  }
2047
2063
  list() {
2048
2064
  return this.getCache();
@@ -2082,13 +2098,10 @@ var TaskInsightStore = class {
2082
2098
  }
2083
2099
  markDirty() {
2084
2100
  this.dirty = true;
2085
- if (!this.flushTimer) {
2086
- this.flushTimer = setTimeout(() => {
2087
- this.flushTimer = null;
2088
- this.flushSync();
2089
- }, FLUSH_DELAY_MS);
2090
- if (this.flushTimer && typeof this.flushTimer === "object" && "unref" in this.flushTimer) this.flushTimer.unref();
2091
- }
2101
+ if (!this.flushTimer) this.flushTimer = setTimeout(() => {
2102
+ this.flushTimer = null;
2103
+ this.flushSync();
2104
+ }, FLUSH_DELAY_MS);
2092
2105
  }
2093
2106
  /** Read and validate from disk (only on first load or corruption recovery). */
2094
2107
  readDisk() {
@@ -2127,6 +2140,25 @@ var TaskInsightStore = class {
2127
2140
  fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf8");
2128
2141
  fs.renameSync(tempPath, this.filePath);
2129
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
+ }
2130
2162
  };
2131
2163
 
2132
2164
  //#endregion
@@ -2141,6 +2173,10 @@ var MemoryService = class {
2141
2173
  this.store = new TaskInsightStore(baseDir);
2142
2174
  this.index = new MemoryIndex();
2143
2175
  }
2176
+ /** Force an immediate synchronous flush of pending writes. */
2177
+ flushSync() {
2178
+ this.store.flushSync();
2179
+ }
2144
2180
  search(input) {
2145
2181
  const cacheKey = `${input.taskIntent}\0${input.siteDomain ?? ""}\0${input.limit ?? 10}`;
2146
2182
  const now = Date.now();
@@ -2191,7 +2227,7 @@ var MemoryService = class {
2191
2227
  }
2192
2228
  recordSuccess(input) {
2193
2229
  const insights = this.store.list();
2194
- const matched = this.findBestExactMatch(insights, input.taskIntent, input.siteDomain);
2230
+ const matched = this.findBestExactMatch(insights, input);
2195
2231
  const evidence = this.createEvidence(input, "success");
2196
2232
  if (!matched) {
2197
2233
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -2248,7 +2284,7 @@ var MemoryService = class {
2248
2284
  }
2249
2285
  recordFailure(input, errorMessage) {
2250
2286
  const insights = this.store.list();
2251
- const matched = this.findBestExactMatch(insights, input.taskIntent, input.siteDomain);
2287
+ const matched = this.findBestExactMatch(insights, input);
2252
2288
  if (!matched) return;
2253
2289
  const signal = detectStalenessSignal(errorMessage, input.selector);
2254
2290
  const evidence = this.createEvidence(input, "failure", errorMessage);
@@ -2261,15 +2297,37 @@ var MemoryService = class {
2261
2297
  this.invalidateSearchCache();
2262
2298
  return failed;
2263
2299
  }
2264
- findBestExactMatch(insights, taskIntent, siteDomain) {
2265
- const intentLower = taskIntent.toLowerCase();
2266
- const domainLower = siteDomain.toLowerCase();
2300
+ findBestExactMatch(insights, input) {
2301
+ const intentLower = input.taskIntent.toLowerCase();
2302
+ const domainLower = input.siteDomain.toLowerCase();
2267
2303
  let best;
2268
2304
  for (const insight of insights) if (insight.taskIntent.toLowerCase() === intentLower && insight.siteDomain.toLowerCase() === domainLower) {
2269
- 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;
2270
2306
  }
2271
2307
  return best;
2272
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
+ }
2273
2331
  createEvidence(input, result, reason) {
2274
2332
  return {
2275
2333
  evidenceId: crypto.randomUUID(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-browser",
3
- "version": "1.6.0",
3
+ "version": "2.0.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",