agentic-browser 1.4.1 → 1.5.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/AGENTS.md CHANGED
@@ -117,15 +117,15 @@ AgenticBrowserCore → ControlApi → SessionManager → BrowserController (CDP)
117
117
 
118
118
  Subcommand: `agentic-browser mcp` (stdio transport). Setup: `agentic-browser setup`. Tools:
119
119
 
120
- | Tool | Purpose |
121
- | ----------------------- | ------------------------------ |
122
- | `browser_start_session` | Start Chrome, return sessionId |
123
- | `browser_navigate` | Navigate to URL |
124
- | `browser_interact` | click / type / press / waitFor |
125
- | `browser_get_content` | Get page title / text / html |
126
- | `browser_get_elements` | Discover interactive elements |
127
- | `browser_search_memory` | Search task memory |
128
- | `browser_stop_session` | Stop Chrome session |
120
+ | Tool | Purpose |
121
+ | ----------------------- | --------------------------------------------------------------------------------------------------------- |
122
+ | `browser_start_session` | Start Chrome, return sessionId |
123
+ | `browser_navigate` | Navigate to URL |
124
+ | `browser_interact` | click / type / press / waitFor / scroll / hover / select / toggle / goBack / goForward / refresh / dialog |
125
+ | `browser_get_content` | Get page title / text / html |
126
+ | `browser_get_elements` | Discover interactive elements |
127
+ | `browser_search_memory` | Search task memory |
128
+ | `browser_stop_session` | Stop Chrome session |
129
129
 
130
130
  ## For Browser Automation Tasks
131
131
 
package/README.md CHANGED
@@ -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 usable in `agent run interact`:
135
+ Returns a JSON array of elements with CSS selectors and fallback selectors usable in `agent run interact`:
136
136
 
137
137
  ```json
138
138
  {
@@ -141,11 +141,9 @@ Returns a JSON array of elements with CSS selectors usable in `agent run interac
141
141
  "elements": [
142
142
  {
143
143
  "selector": "#login-btn",
144
+ "fallbackSelectors": ["button[aria-label=\"Login\"]"],
144
145
  "role": "button",
145
- "tagName": "button",
146
146
  "text": "Login",
147
- "actions": ["click"],
148
- "visible": true,
149
147
  "enabled": true
150
148
  }
151
149
  ],
@@ -154,13 +152,17 @@ Returns a JSON array of elements with CSS selectors usable in `agent run interac
154
152
  }
155
153
  ```
156
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.
156
+
157
+ ````
158
+
157
159
  ## MCP Server
158
160
 
159
161
  ### Quick Setup
160
162
 
161
163
  ```bash
162
164
  npx agentic-browser setup
163
- ```
165
+ ````
164
166
 
165
167
  Detects your AI tools (Claude Code, Cursor) and writes the MCP config automatically.
166
168
 
@@ -211,6 +213,12 @@ More `interact` actions:
211
213
  - `{"action":"type","selector":"input[name=q]","text":"innoq"}`
212
214
  - `{"action":"press","key":"Enter"}`
213
215
  - `{"action":"waitFor","selector":"main","timeoutMs":4000}`
216
+ - `{"action":"goBack"}` — browser back
217
+ - `{"action":"goForward"}` — browser forward
218
+ - `{"action":"refresh"}` — reload page
219
+ - `{"action":"dialog"}` — accept a JS dialog (alert/confirm/prompt)
220
+ - `{"action":"dialog","text":"dismiss"}` — dismiss a dialog
221
+ - `{"action":"dialog","value":"answer"}` — respond to a prompt dialog
214
222
 
215
223
  ### 4. Read Page Content
216
224
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as createCliRuntime } from "../runtime-C32ai0TQ.mjs";
2
+ import { r as createCliRuntime } from "../runtime-OsYo7Rh2.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-C32ai0TQ.mjs";
2
+ import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-OsYo7Rh2.mjs";
3
3
 
4
4
  export { AgenticBrowserCore, createAgenticBrowserCore, createMockAgenticBrowserCore };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as createAgenticBrowserCore } from "../runtime-C32ai0TQ.mjs";
2
+ import { n as createAgenticBrowserCore } from "../runtime-OsYo7Rh2.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";
@@ -31,9 +31,10 @@ const server = new McpServer({
31
31
  server.tool("browser_start_session", "Start a new Chrome browser session (or return the existing one if healthy). Sessions auto-start when you call any other browser tool, so you rarely need to call this explicitly. Use this to force a fresh session after stopping the previous one.", {}, async () => {
32
32
  const session = await getCore().startSession();
33
33
  activeSessionId = session.sessionId;
34
+ const { authTokenRef: _, ...compactSession } = session;
34
35
  return { content: [{
35
36
  type: "text",
36
- text: JSON.stringify(session)
37
+ text: JSON.stringify(compactSession)
37
38
  }] };
38
39
  });
39
40
  server.tool("browser_navigate", "Navigate the browser to a URL. A session is auto-started if needed.", {
@@ -47,12 +48,16 @@ server.tool("browser_navigate", "Navigate the browser to a URL. A session is aut
47
48
  type: "navigate",
48
49
  payload: { url }
49
50
  });
51
+ const compact = {
52
+ resultStatus: result.resultStatus,
53
+ resultMessage: result.resultMessage
54
+ };
50
55
  return { content: [{
51
56
  type: "text",
52
- text: JSON.stringify(result)
57
+ text: JSON.stringify(compact)
53
58
  }] };
54
59
  });
55
- server.tool("browser_interact", "Interact with a page element. Actions: \"click\" (click element), \"type\" (type text into input), \"press\" (press a keyboard key like Enter), \"waitFor\" (wait for element to appear), \"scroll\" (scroll page or element), \"hover\" (hover over element), \"select\" (pick option in <select>), \"toggle\" (toggle checkbox/radio/switch). A session is auto-started if needed.", {
60
+ server.tool("browser_interact", "Interact with a page element or perform browser actions. Element actions: \"click\", \"type\", \"press\", \"waitFor\", \"scroll\", \"hover\", \"select\", \"toggle\". Navigation actions: \"goBack\" (browser back), \"goForward\" (browser forward), \"refresh\" (reload page). Dialog action: \"dialog\" (handle JS alert/confirm/prompt — use text=\"dismiss\" to cancel, value=\"...\" for prompt input). Fallback selectors are tried automatically if the primary selector fails. A session is auto-started if needed.", {
56
61
  action: z.enum([
57
62
  "click",
58
63
  "type",
@@ -61,20 +66,26 @@ server.tool("browser_interact", "Interact with a page element. Actions: \"click\
61
66
  "scroll",
62
67
  "hover",
63
68
  "select",
64
- "toggle"
69
+ "toggle",
70
+ "goBack",
71
+ "goForward",
72
+ "refresh",
73
+ "dialog"
65
74
  ]).describe("The interaction type"),
66
75
  selector: z.string().optional().describe("CSS selector for the target element"),
67
- text: z.string().optional().describe("Text to type (required for \"type\" action)"),
76
+ fallbackSelectors: z.array(z.string()).optional().describe("Backup CSS selectors tried if the primary selector fails (from browser_get_elements)"),
77
+ text: z.string().optional().describe("Text to type (for \"type\"), or \"dismiss\" to dismiss a dialog (for \"dialog\")"),
68
78
  key: z.string().optional().describe("Key to press (required for \"press\" action, e.g. \"Enter\", \"Tab\")"),
69
- value: z.string().optional().describe("Option value to select (required for \"select\" action)"),
79
+ value: z.string().optional().describe("Option value to select (for \"select\"), or prompt text (for \"dialog\")"),
70
80
  scrollX: z.number().optional().describe("Horizontal scroll delta in pixels (for \"scroll\" action)"),
71
81
  scrollY: z.number().optional().describe("Vertical scroll delta in pixels (for \"scroll\" action, positive = down)"),
72
82
  timeoutMs: z.number().optional().describe("Timeout in milliseconds (for \"waitFor\" action, default 4000)"),
73
83
  sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
74
- }, async ({ action, selector, text, key, value, scrollX, scrollY, timeoutMs, sessionId }) => {
84
+ }, async ({ action, selector, fallbackSelectors, text, key, value, scrollX, scrollY, timeoutMs, sessionId }) => {
75
85
  const sid = await resolveSession(sessionId);
76
86
  const payload = { action };
77
87
  if (selector) payload.selector = selector;
88
+ if (fallbackSelectors) payload.fallbackSelectors = fallbackSelectors;
78
89
  if (text) payload.text = text;
79
90
  if (key) payload.key = key;
80
91
  if (value) payload.value = value;
@@ -87,9 +98,13 @@ server.tool("browser_interact", "Interact with a page element. Actions: \"click\
87
98
  type: "interact",
88
99
  payload
89
100
  });
101
+ const compact = {
102
+ resultStatus: result.resultStatus,
103
+ resultMessage: result.resultMessage
104
+ };
90
105
  return { content: [{
91
106
  type: "text",
92
- text: JSON.stringify(result)
107
+ text: JSON.stringify(compact)
93
108
  }] };
94
109
  });
95
110
  server.tool("browser_get_content", "Get the current page content. Modes: \"text\" (readable text), \"a11y\" (accessibility tree — best for understanding page structure), \"title\" (page title only), \"html\" (raw HTML). Use \"a11y\" to see the full page hierarchy with roles, names, and states. A session is auto-started if needed.", {
@@ -100,20 +115,42 @@ server.tool("browser_get_content", "Get the current page content. Modes: \"text\
100
115
  "a11y"
101
116
  ]).default("text").describe("Content extraction mode"),
102
117
  selector: z.string().optional().describe("CSS selector to scope content (e.g. \"main\", \"#content\")"),
118
+ maxChars: z.number().optional().describe("Maximum characters to return (default: 12000 for text/a11y, 6000 for html, no cap for title). Use a CSS selector to scope content instead of raising this limit."),
103
119
  sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
104
- }, async ({ mode, selector, sessionId }) => {
120
+ }, async ({ mode, selector, maxChars, sessionId }) => {
105
121
  const sid = await resolveSession(sessionId);
106
122
  const result = await getCore().getPageContent({
107
123
  sessionId: sid,
108
124
  mode,
109
125
  selector
110
126
  });
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
+ }
111
145
  return { content: [{
112
146
  type: "text",
113
- text: JSON.stringify(result)
147
+ text: JSON.stringify({
148
+ ...result,
149
+ truncated: false
150
+ })
114
151
  }] };
115
152
  });
116
- server.tool("browser_get_elements", "Discover all interactive elements on the current page (buttons, links, inputs, etc.). Returns CSS selectors you can use with browser_interact. A session is auto-started if needed.", {
153
+ 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.", {
117
154
  roles: z.array(z.enum([
118
155
  "link",
119
156
  "button",
@@ -138,24 +175,46 @@ server.tool("browser_get_elements", "Discover all interactive elements on the cu
138
175
  limit,
139
176
  selector
140
177
  });
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
+ });
141
186
  return { content: [{
142
187
  type: "text",
143
- text: JSON.stringify(result)
188
+ text: JSON.stringify({
189
+ elements: compactElements,
190
+ totalFound: result.totalFound,
191
+ truncated: result.truncated
192
+ })
144
193
  }] };
145
194
  });
146
- server.tool("browser_search_memory", "Search task memory for previously learned selectors and interaction patterns. Use this before interacting with a known site to reuse proven selectors instead of rediscovering them.", {
195
+ 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.", {
147
196
  taskIntent: z.string().describe("What you want to do, e.g. \"login:github.com\" or \"search:amazon.de\""),
148
197
  siteDomain: z.string().optional().describe("Domain to scope the search"),
149
198
  limit: z.number().default(5).describe("Maximum number of results")
150
199
  }, async ({ taskIntent, siteDomain, limit }) => {
151
- const result = getCore().searchMemory({
200
+ const compactResults = getCore().searchMemory({
152
201
  taskIntent,
153
202
  siteDomain,
154
203
  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;
155
214
  });
156
215
  return { content: [{
157
216
  type: "text",
158
- text: JSON.stringify(result)
217
+ text: JSON.stringify({ results: compactResults })
159
218
  }] };
160
219
  });
161
220
  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 }) => {
@@ -92,6 +92,16 @@ var CdpConnection = class CdpConnection {
92
92
  this.ws.on("message", onMessage);
93
93
  });
94
94
  }
95
+ onEvent(method, handler) {
96
+ const onMessage = (raw) => {
97
+ const message = JSON.parse(raw.toString("utf8"));
98
+ if (message.method === method) handler(message.params ?? {});
99
+ };
100
+ this.ws.on("message", onMessage);
101
+ return () => {
102
+ this.ws.off("message", onMessage);
103
+ };
104
+ }
95
105
  close() {
96
106
  this.ws.close();
97
107
  }
@@ -248,6 +258,20 @@ var ChromeCdpBrowserController = class {
248
258
  cached.enabled.runtime = true;
249
259
  }));
250
260
  if (promises.length) await Promise.all(promises);
261
+ if (!cached.dialogListenerAttached && cached.conn.onEvent) {
262
+ cached.conn.onEvent("Page.javascriptDialogOpening", (params) => {
263
+ cached.pendingDialog = {
264
+ type: params.type,
265
+ message: params.message,
266
+ defaultPrompt: params.defaultPrompt
267
+ };
268
+ if (params.type === "alert") {
269
+ cached.conn.send("Page.handleJavaScriptDialog", { accept: true }).catch(() => {});
270
+ cached.pendingDialog = void 0;
271
+ }
272
+ });
273
+ cached.dialogListenerAttached = true;
274
+ }
251
275
  }
252
276
  async launch(sessionId, options) {
253
277
  const { executablePath: explicitPath, userProfileDir, headless, userAgent } = options ?? {};
@@ -338,12 +362,83 @@ var ChromeCdpBrowserController = class {
338
362
  return navigatedEvent?.frame?.url ?? url;
339
363
  });
340
364
  }
365
+ async handleDialogAction(targetWsUrl, payload) {
366
+ return await this.withRetry(targetWsUrl, async (conn) => {
367
+ await this.ensureEnabled(targetWsUrl);
368
+ const cached = this.connections.get(targetWsUrl);
369
+ if (!cached?.pendingDialog) try {
370
+ await conn.waitForEvent("Page.javascriptDialogOpening", 500);
371
+ await new Promise((r) => setTimeout(r, 50));
372
+ } catch {
373
+ return "no dialog present";
374
+ }
375
+ const dialog = cached?.pendingDialog;
376
+ if (!dialog) return "no dialog present";
377
+ const dismiss = payload.text === "dismiss";
378
+ await conn.send("Page.handleJavaScriptDialog", {
379
+ accept: !dismiss,
380
+ promptText: payload.value
381
+ });
382
+ const result = dismiss ? `dismissed ${dialog.type}` : `accepted ${dialog.type}`;
383
+ if (cached) cached.pendingDialog = void 0;
384
+ return `${result}: ${dialog.message}`;
385
+ });
386
+ }
341
387
  async interact(targetWsUrl, payload) {
388
+ if (payload.action === "goBack") return await this.withRetry(targetWsUrl, async (conn) => {
389
+ await this.ensureEnabled(targetWsUrl);
390
+ const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 3e3).catch(() => void 0);
391
+ await conn.send("Runtime.evaluate", {
392
+ expression: "history.back()",
393
+ returnByValue: true
394
+ });
395
+ const event = await navigatedPromise;
396
+ if (!event) return "no history to go back";
397
+ try {
398
+ await Promise.race([conn.waitForEvent("Page.loadEventFired", 5e3), conn.waitForEvent("Page.frameStoppedLoading", 5e3)]);
399
+ } catch {}
400
+ return `navigated back to ${event.frame?.url ?? "previous page"}`;
401
+ });
402
+ if (payload.action === "goForward") return await this.withRetry(targetWsUrl, async (conn) => {
403
+ await this.ensureEnabled(targetWsUrl);
404
+ const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 3e3).catch(() => void 0);
405
+ await conn.send("Runtime.evaluate", {
406
+ expression: "history.forward()",
407
+ returnByValue: true
408
+ });
409
+ const event = await navigatedPromise;
410
+ if (!event) return "no history to go forward";
411
+ try {
412
+ await Promise.race([conn.waitForEvent("Page.loadEventFired", 5e3), conn.waitForEvent("Page.frameStoppedLoading", 5e3)]);
413
+ } catch {}
414
+ return `navigated forward to ${event.frame?.url ?? "next page"}`;
415
+ });
416
+ if (payload.action === "refresh") return await this.withRetry(targetWsUrl, async (conn) => {
417
+ await this.ensureEnabled(targetWsUrl);
418
+ await conn.send("Page.reload");
419
+ try {
420
+ await Promise.race([conn.waitForEvent("Page.loadEventFired", 1e4), conn.waitForEvent("Page.frameStoppedLoading", 1e4)]);
421
+ } catch {}
422
+ return "page refreshed";
423
+ });
424
+ if (payload.action === "dialog") return await this.handleDialogAction(targetWsUrl, payload);
342
425
  const expression = `(async () => {
343
426
  const payload = ${JSON.stringify(payload)};
427
+
428
+ function resolveElement(selector, fallbacks) {
429
+ let el = selector ? document.querySelector(selector) : null;
430
+ if (el) return el;
431
+ if (fallbacks && fallbacks.length) {
432
+ for (const fb of fallbacks) {
433
+ el = document.querySelector(fb);
434
+ if (el) return el;
435
+ }
436
+ }
437
+ throw new Error('Selector not found');
438
+ }
439
+
344
440
  if (payload.action === 'click') {
345
- const el = document.querySelector(payload.selector);
346
- if (!el) throw new Error('Selector not found');
441
+ const el = resolveElement(payload.selector, payload.fallbackSelectors);
347
442
  const rect = el.getBoundingClientRect();
348
443
  if (rect.width === 0 && rect.height === 0) {
349
444
  throw new Error('Element has zero size – it may be hidden or not rendered');
@@ -361,8 +456,7 @@ var ChromeCdpBrowserController = class {
361
456
  return 'clicked';
362
457
  }
363
458
  if (payload.action === 'type') {
364
- const el = document.querySelector(payload.selector);
365
- if (!el) throw new Error('Selector not found');
459
+ const el = resolveElement(payload.selector, payload.fallbackSelectors);
366
460
  el.focus();
367
461
  el.value = payload.text ?? '';
368
462
  el.dispatchEvent(new Event('input', { bubbles: true }));
@@ -397,8 +491,7 @@ var ChromeCdpBrowserController = class {
397
491
  }
398
492
  if (payload.action === 'scroll') {
399
493
  if (payload.selector) {
400
- const el = document.querySelector(payload.selector);
401
- if (!el) throw new Error('Selector not found');
494
+ const el = resolveElement(payload.selector, payload.fallbackSelectors);
402
495
  el.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0, behavior: 'smooth' });
403
496
  return 'scrolled element';
404
497
  }
@@ -406,8 +499,7 @@ var ChromeCdpBrowserController = class {
406
499
  return 'scrolled page';
407
500
  }
408
501
  if (payload.action === 'hover') {
409
- const el = document.querySelector(payload.selector);
410
- if (!el) throw new Error('Selector not found');
502
+ const el = resolveElement(payload.selector, payload.fallbackSelectors);
411
503
  const rect = el.getBoundingClientRect();
412
504
  const cx = rect.left + rect.width / 2;
413
505
  const cy = rect.top + rect.height / 2;
@@ -417,8 +509,7 @@ var ChromeCdpBrowserController = class {
417
509
  return 'hovered';
418
510
  }
419
511
  if (payload.action === 'select') {
420
- const el = document.querySelector(payload.selector);
421
- if (!el) throw new Error('Selector not found');
512
+ const el = resolveElement(payload.selector, payload.fallbackSelectors);
422
513
  if (el.tagName.toLowerCase() !== 'select') throw new Error('Element is not a <select>');
423
514
  el.value = payload.value ?? '';
424
515
  el.dispatchEvent(new Event('change', { bubbles: true }));
@@ -426,8 +517,7 @@ var ChromeCdpBrowserController = class {
426
517
  return 'selected ' + el.value;
427
518
  }
428
519
  if (payload.action === 'toggle') {
429
- const el = document.querySelector(payload.selector);
430
- if (!el) throw new Error('Selector not found');
520
+ const el = resolveElement(payload.selector, payload.fallbackSelectors);
431
521
  el.click();
432
522
  const checked = el.checked !== undefined ? el.checked : el.getAttribute('aria-checked') === 'true';
433
523
  return 'toggled to ' + (checked ? 'checked' : 'unchecked');
@@ -600,34 +690,62 @@ var ChromeCdpBrowserController = class {
600
690
  return value.replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\\\"');
601
691
  }
602
692
 
693
+ function tryUniqueSelector(sel) {
694
+ try { return document.querySelectorAll(sel).length === 1 ? sel : null; }
695
+ catch { return null; }
696
+ }
697
+
603
698
  function buildSelector(el) {
604
699
  if (el.id) return '#' + CSS.escape(el.id);
605
700
 
606
701
  const name = el.getAttribute('name');
607
702
  if (name) {
608
703
  const tag = el.tagName.toLowerCase();
609
- const sel = tag + '[name="' + escapeAttr(name) + '"]';
610
- if (document.querySelectorAll(sel).length === 1) return sel;
704
+ const sel = tryUniqueSelector(tag + '[name="' + escapeAttr(name) + '"]');
705
+ if (sel) return sel;
611
706
  }
612
707
 
613
708
  const testId = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
614
709
  if (testId) {
615
710
  const attr = el.hasAttribute('data-testid') ? 'data-testid' : 'data-test-id';
616
- const sel = '[' + attr + '="' + escapeAttr(testId) + '"]';
617
- if (document.querySelectorAll(sel).length === 1) return sel;
711
+ const sel = tryUniqueSelector('[' + attr + '="' + escapeAttr(testId) + '"]');
712
+ if (sel) return sel;
618
713
  }
619
714
 
620
715
  const ariaLabel = el.getAttribute('aria-label');
621
716
  if (ariaLabel) {
622
717
  const tag = el.tagName.toLowerCase();
623
- const sel = tag + '[aria-label="' + escapeAttr(ariaLabel) + '"]';
624
- if (document.querySelectorAll(sel).length === 1) return sel;
718
+ const sel = tryUniqueSelector(tag + '[aria-label="' + escapeAttr(ariaLabel) + '"]');
719
+ if (sel) return sel;
720
+ }
721
+
722
+ const dataCy = el.getAttribute('data-cy');
723
+ if (dataCy) {
724
+ const sel = tryUniqueSelector('[data-cy="' + escapeAttr(dataCy) + '"]');
725
+ if (sel) return sel;
726
+ }
727
+
728
+ const dataTest = el.getAttribute('data-test');
729
+ if (dataTest) {
730
+ const sel = tryUniqueSelector('[data-test="' + escapeAttr(dataTest) + '"]');
731
+ if (sel) return sel;
625
732
  }
626
733
 
734
+ const role = el.getAttribute('role');
735
+ if (role && ariaLabel) {
736
+ const sel = tryUniqueSelector('[role="' + escapeAttr(role) + '"][aria-label="' + escapeAttr(ariaLabel) + '"]');
737
+ if (sel) return sel;
738
+ }
739
+
740
+ // Path fallback — anchor at nearest ancestor with an id for shorter selectors
627
741
  const parts = [];
628
742
  let current = el;
629
743
  while (current && current !== document.documentElement) {
630
744
  const tag = current.tagName.toLowerCase();
745
+ if (current !== el && current.id) {
746
+ parts.unshift('#' + CSS.escape(current.id));
747
+ break;
748
+ }
631
749
  const parent = current.parentElement;
632
750
  if (!parent) { parts.unshift(tag); break; }
633
751
  const siblings = Array.from(parent.children).filter(
@@ -644,6 +762,50 @@ var ChromeCdpBrowserController = class {
644
762
  return parts.join(' > ');
645
763
  }
646
764
 
765
+ function buildFallbackSelectors(el, primarySelector) {
766
+ const fallbacks = [];
767
+ const candidates = [];
768
+
769
+ if (el.id) candidates.push('#' + CSS.escape(el.id));
770
+
771
+ const name = el.getAttribute('name');
772
+ if (name) {
773
+ const tag = el.tagName.toLowerCase();
774
+ const sel = tag + '[name="' + escapeAttr(name) + '"]';
775
+ candidates.push(sel);
776
+ }
777
+
778
+ const testId = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
779
+ if (testId) {
780
+ const attr = el.hasAttribute('data-testid') ? 'data-testid' : 'data-test-id';
781
+ candidates.push('[' + attr + '="' + escapeAttr(testId) + '"]');
782
+ }
783
+
784
+ const ariaLabel = el.getAttribute('aria-label');
785
+ if (ariaLabel) {
786
+ const tag = el.tagName.toLowerCase();
787
+ candidates.push(tag + '[aria-label="' + escapeAttr(ariaLabel) + '"]');
788
+ }
789
+
790
+ const dataCy = el.getAttribute('data-cy');
791
+ if (dataCy) candidates.push('[data-cy="' + escapeAttr(dataCy) + '"]');
792
+
793
+ const dataTest = el.getAttribute('data-test');
794
+ if (dataTest) candidates.push('[data-test="' + escapeAttr(dataTest) + '"]');
795
+
796
+ const role = el.getAttribute('role');
797
+ if (role && ariaLabel) {
798
+ candidates.push('[role="' + escapeAttr(role) + '"][aria-label="' + escapeAttr(ariaLabel) + '"]');
799
+ }
800
+
801
+ for (const sel of candidates) {
802
+ if (sel === primarySelector) continue;
803
+ if (tryUniqueSelector(sel)) fallbacks.push(sel);
804
+ if (fallbacks.length >= 3) break;
805
+ }
806
+ return fallbacks;
807
+ }
808
+
647
809
  function isVisible(el) {
648
810
  const style = window.getComputedStyle(el);
649
811
  if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
@@ -707,8 +869,9 @@ var ChromeCdpBrowserController = class {
707
869
  totalFound++;
708
870
  if (results.length >= limit) continue;
709
871
 
872
+ const primarySelector = buildSelector(el);
710
873
  const entry = {
711
- selector: buildSelector(el),
874
+ selector: primarySelector,
712
875
  role,
713
876
  tagName: el.tagName.toLowerCase(),
714
877
  text: getText(el),
@@ -717,6 +880,9 @@ var ChromeCdpBrowserController = class {
717
880
  enabled: isEnabled(el),
718
881
  };
719
882
 
883
+ const fb = buildFallbackSelectors(el, primarySelector);
884
+ if (fb.length) entry.fallbackSelectors = fb;
885
+
720
886
  if (role === 'link' && el.href) entry.href = el.href;
721
887
  if (role === 'input') entry.inputType = (el.type || 'text').toLowerCase();
722
888
  const al = el.getAttribute('aria-label');
@@ -791,9 +957,30 @@ var MockBrowserController = class {
791
957
  page.title = url;
792
958
  page.text = `Content of ${url}`;
793
959
  page.html = `<html><body>${page.text}</body></html>`;
960
+ this.history.splice(this.historyIndex + 1);
961
+ this.history.push(url);
962
+ this.historyIndex = this.history.length - 1;
794
963
  return url;
795
964
  }
796
- async interact(_cdpUrl, payload) {
965
+ history = [];
966
+ historyIndex = -1;
967
+ async interact(cdpUrl, payload) {
968
+ if (payload.action === "goBack") {
969
+ if (this.historyIndex <= 0) return "no history to go back";
970
+ this.historyIndex--;
971
+ const page = this.pages.get(cdpUrl);
972
+ if (page) page.url = this.history[this.historyIndex] ?? page.url;
973
+ return `navigated back to ${page?.url ?? "previous page"}`;
974
+ }
975
+ if (payload.action === "goForward") {
976
+ if (this.historyIndex >= this.history.length - 1) return "no history to go forward";
977
+ this.historyIndex++;
978
+ const page = this.pages.get(cdpUrl);
979
+ if (page) page.url = this.history[this.historyIndex] ?? page.url;
980
+ return `navigated forward to ${page?.url ?? "next page"}`;
981
+ }
982
+ if (payload.action === "refresh") return "page refreshed";
983
+ if (payload.action === "dialog") return payload.text === "dismiss" ? "dismissed confirm: mock dialog" : "accepted confirm: mock dialog";
797
984
  return `interacted:${payload.action}`;
798
985
  }
799
986
  async getContent(cdpUrl, options) {
@@ -1494,6 +1681,9 @@ function buildSelectorHints(insight) {
1494
1681
  }
1495
1682
  return [...weightedSelectors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([selector]) => selector);
1496
1683
  }
1684
+ function buildSelectorAliases(insight) {
1685
+ return insight.selectorAliases ?? [];
1686
+ }
1497
1687
  function selectorSignal(insight) {
1498
1688
  const recipeSelectors = insight.actionRecipe.filter((step) => Boolean(step.selector)).length;
1499
1689
  const recipeCoverage = insight.actionRecipe.length > 0 ? recipeSelectors / insight.actionRecipe.length : 0;
@@ -1517,6 +1707,7 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain) {
1517
1707
  freshness: insight.freshness,
1518
1708
  lastVerifiedAt: insight.lastVerifiedAt,
1519
1709
  selectorHints: buildSelectorHints(insight),
1710
+ selectorAliases: buildSelectorAliases(insight),
1520
1711
  score
1521
1712
  };
1522
1713
  }
@@ -1631,6 +1822,11 @@ const EvidenceRecordSchema = z.object({
1631
1822
  url: z.string().optional(),
1632
1823
  recordedAt: z.string().datetime()
1633
1824
  });
1825
+ const SelectorAliasSchema = z.object({
1826
+ alias: z.string().min(1),
1827
+ selector: z.string().min(1),
1828
+ fallbackSelectors: z.array(z.string()).default([])
1829
+ });
1634
1830
  const TaskInsightSchema = z.object({
1635
1831
  insightId: z.string().min(1),
1636
1832
  taskIntent: z.string().min(1),
@@ -1648,7 +1844,8 @@ const TaskInsightSchema = z.object({
1648
1844
  createdAt: z.string().datetime(),
1649
1845
  updatedAt: z.string().datetime(),
1650
1846
  supersedes: z.string().optional(),
1651
- evidence: z.array(EvidenceRecordSchema)
1847
+ evidence: z.array(EvidenceRecordSchema),
1848
+ selectorAliases: z.array(SelectorAliasSchema).default([])
1652
1849
  });
1653
1850
  const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
1654
1851
 
@@ -1833,7 +2030,7 @@ var MemoryService = class {
1833
2030
  const evidence = this.createEvidence(input, "success");
1834
2031
  if (!matched) {
1835
2032
  const now = (/* @__PURE__ */ new Date()).toISOString();
1836
- const created = {
2033
+ let created = {
1837
2034
  insightId: crypto.randomUUID(),
1838
2035
  taskIntent: input.taskIntent,
1839
2036
  siteDomain: input.siteDomain,
@@ -1849,8 +2046,10 @@ var MemoryService = class {
1849
2046
  lastVerifiedAt: now,
1850
2047
  createdAt: now,
1851
2048
  updatedAt: now,
1852
- evidence: [evidence]
2049
+ evidence: [evidence],
2050
+ selectorAliases: []
1853
2051
  };
2052
+ created = this.maybeGenerateAliases(created);
1854
2053
  this.store.upsert(created);
1855
2054
  this.invalidateSearchCache();
1856
2055
  return created;
@@ -1864,7 +2063,7 @@ var MemoryService = class {
1864
2063
  });
1865
2064
  if (matched.freshness === "suspect" || matched.freshness === "stale") {
1866
2065
  const now = (/* @__PURE__ */ new Date()).toISOString();
1867
- const versioned = {
2066
+ let versioned = {
1868
2067
  ...refreshed,
1869
2068
  insightId: crypto.randomUUID(),
1870
2069
  supersedes: matched.insightId,
@@ -1872,13 +2071,15 @@ var MemoryService = class {
1872
2071
  createdAt: now,
1873
2072
  updatedAt: now
1874
2073
  };
2074
+ versioned = this.maybeGenerateAliases(versioned);
1875
2075
  this.store.upsert(versioned);
1876
2076
  this.invalidateSearchCache();
1877
2077
  return versioned;
1878
2078
  }
1879
- this.store.upsert(refreshed);
2079
+ const withAliases = this.maybeGenerateAliases(refreshed);
2080
+ this.store.upsert(withAliases);
1880
2081
  this.invalidateSearchCache();
1881
- return refreshed;
2082
+ return withAliases;
1882
2083
  }
1883
2084
  recordFailure(input, errorMessage) {
1884
2085
  const insights = this.store.list();
@@ -1915,6 +2116,42 @@ var MemoryService = class {
1915
2116
  recordedAt: (/* @__PURE__ */ new Date()).toISOString()
1916
2117
  };
1917
2118
  }
2119
+ maybeGenerateAliases(insight) {
2120
+ if (insight.confidence < .8 || insight.successCount < 3) return insight;
2121
+ const aliasMap = /* @__PURE__ */ new Map();
2122
+ for (const existing of insight.selectorAliases ?? []) aliasMap.set(existing.selector, existing);
2123
+ const selectors = /* @__PURE__ */ new Set();
2124
+ for (const step of insight.actionRecipe) if (step.selector) selectors.add(step.selector);
2125
+ for (const ev of insight.evidence) if (ev.selector && ev.result === "success") selectors.add(ev.selector);
2126
+ for (const selector of selectors) {
2127
+ if (aliasMap.has(selector)) continue;
2128
+ const alias = this.deriveAliasName(selector);
2129
+ if (alias) aliasMap.set(selector, {
2130
+ alias,
2131
+ selector,
2132
+ fallbackSelectors: []
2133
+ });
2134
+ if (aliasMap.size >= 10) break;
2135
+ }
2136
+ return {
2137
+ ...insight,
2138
+ selectorAliases: [...aliasMap.values()].slice(0, 10)
2139
+ };
2140
+ }
2141
+ deriveAliasName(selector) {
2142
+ const idMatch = selector.match(/^#([\w-]+)$/);
2143
+ if (idMatch) return idMatch[1].replace(/[-_]/g, " ");
2144
+ const nameMatch = selector.match(/\[name="([^"]+)"\]/);
2145
+ if (nameMatch) return nameMatch[1];
2146
+ const ariaMatch = selector.match(/\[aria-label="([^"]+)"\]/);
2147
+ if (ariaMatch) return ariaMatch[1];
2148
+ const testIdMatch = selector.match(/\[data-testid="([^"]+)"\]/);
2149
+ if (testIdMatch) return testIdMatch[1];
2150
+ const cyMatch = selector.match(/\[data-cy="([^"]+)"\]/);
2151
+ if (cyMatch) return cyMatch[1];
2152
+ const testMatch = selector.match(/\[data-test="([^"]+)"\]/);
2153
+ if (testMatch) return testMatch[1];
2154
+ }
1918
2155
  mergeRecipe(recipe, step) {
1919
2156
  if (recipe.find((existing) => existing.summary === step.summary && existing.selector === step.selector)) return recipe;
1920
2157
  return [...recipe, step].slice(-8);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-browser",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",