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 +9 -9
- package/README.md +13 -5
- package/dist/cli/index.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.mjs +75 -16
- package/dist/{runtime-C32ai0TQ.mjs → runtime-OsYo7Rh2.mjs} +263 -26
- package/package.json +1 -1
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
|
|
package/dist/cli/index.mjs
CHANGED
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-
|
|
2
|
+
import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-OsYo7Rh2.mjs";
|
|
3
3
|
|
|
4
4
|
export { AgenticBrowserCore, createAgenticBrowserCore, createMockAgenticBrowserCore };
|
package/dist/mcp/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as createAgenticBrowserCore } from "../runtime-
|
|
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(
|
|
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(
|
|
57
|
+
text: JSON.stringify(compact)
|
|
53
58
|
}] };
|
|
54
59
|
});
|
|
55
|
-
server.tool("browser_interact", "Interact with a page element.
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2079
|
+
const withAliases = this.maybeGenerateAliases(refreshed);
|
|
2080
|
+
this.store.upsert(withAliases);
|
|
1880
2081
|
this.invalidateSearchCache();
|
|
1881
|
-
return
|
|
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);
|