agentic-browser 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/cli/index.mjs +3 -3
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.mjs +49 -23
- package/dist/{runtime-Dvmv5Xi_.mjs → runtime-C32ai0TQ.mjs} +182 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,7 @@ agentic-browser agent start --user-profile /path/to/chrome/profile
|
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
Default profile locations per platform:
|
|
87
|
+
|
|
87
88
|
- **macOS:** `~/Library/Application Support/Google/Chrome`
|
|
88
89
|
- **Linux:** `~/.config/google-chrome`
|
|
89
90
|
- **Windows:** `%LOCALAPPDATA%\Google\Chrome\User Data`
|
|
@@ -92,12 +93,12 @@ Default profile locations per platform:
|
|
|
92
93
|
|
|
93
94
|
These options can also be set via environment variables (CLI flags take precedence):
|
|
94
95
|
|
|
95
|
-
| Variable | Example
|
|
96
|
-
| ------------------------------ |
|
|
97
|
-
| `AGENTIC_BROWSER_CDP_URL` | `http://127.0.0.1:9222` | Connect to a running Chrome
|
|
98
|
-
| `AGENTIC_BROWSER_USER_PROFILE` | `default` or an absolute path
|
|
99
|
-
| `AGENTIC_BROWSER_HEADLESS` | `true`
|
|
100
|
-
| `AGENTIC_BROWSER_USER_AGENT` | `MyBot/1.0`
|
|
96
|
+
| Variable | Example | Description |
|
|
97
|
+
| ------------------------------ | ----------------------------- | ------------------------------- |
|
|
98
|
+
| `AGENTIC_BROWSER_CDP_URL` | `http://127.0.0.1:9222` | Connect to a running Chrome |
|
|
99
|
+
| `AGENTIC_BROWSER_USER_PROFILE` | `default` or an absolute path | Launch with a real profile |
|
|
100
|
+
| `AGENTIC_BROWSER_HEADLESS` | `true` | Run Chrome in headless mode |
|
|
101
|
+
| `AGENTIC_BROWSER_USER_AGENT` | `MyBot/1.0` | Override the browser user-agent |
|
|
101
102
|
|
|
102
103
|
## Agent Commands (Recommended for LLMs)
|
|
103
104
|
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { r as createCliRuntime } from "../runtime-
|
|
2
|
+
import { r as createCliRuntime } from "../runtime-C32ai0TQ.mjs";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import crypto from "node:crypto";
|
|
@@ -283,7 +283,7 @@ async function main() {
|
|
|
283
283
|
});
|
|
284
284
|
console.log(JSON.stringify(result));
|
|
285
285
|
});
|
|
286
|
-
program.command("page:content").argument("<sessionId>").option("--mode <mode>", "title|text|html", "text").option("--selector <selector>", "optional CSS selector").action(async (sessionId, options) => {
|
|
286
|
+
program.command("page:content").argument("<sessionId>").option("--mode <mode>", "title|text|html|a11y", "text").option("--selector <selector>", "optional CSS selector").action(async (sessionId, options) => {
|
|
287
287
|
const result = await runPageContent(runtime, {
|
|
288
288
|
sessionId,
|
|
289
289
|
mode: options.mode,
|
|
@@ -335,7 +335,7 @@ async function main() {
|
|
|
335
335
|
});
|
|
336
336
|
console.log(JSON.stringify(result));
|
|
337
337
|
});
|
|
338
|
-
agent.command("content").option("--mode <mode>", "title|text|html", "text").option("--selector <selector>", "optional CSS selector").action(async (options) => {
|
|
338
|
+
agent.command("content").option("--mode <mode>", "title|text|html|a11y", "text").option("--selector <selector>", "optional CSS selector").action(async (options) => {
|
|
339
339
|
const result = await agentContent(runtime, options);
|
|
340
340
|
console.log(JSON.stringify(result));
|
|
341
341
|
});
|
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-C32ai0TQ.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-C32ai0TQ.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";
|
|
@@ -14,11 +14,21 @@ function getCore() {
|
|
|
14
14
|
function genId(prefix) {
|
|
15
15
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a session ID — auto-starts a session if none exists.
|
|
19
|
+
* This means the LLM never has to call browser_start_session explicitly.
|
|
20
|
+
*/
|
|
21
|
+
async function resolveSession(sessionId) {
|
|
22
|
+
if (sessionId) return sessionId;
|
|
23
|
+
if (activeSessionId) return activeSessionId;
|
|
24
|
+
activeSessionId = (await getCore().startSession()).sessionId;
|
|
25
|
+
return activeSessionId;
|
|
26
|
+
}
|
|
17
27
|
const server = new McpServer({
|
|
18
28
|
name: "agentic-browser",
|
|
19
29
|
version: "0.1.0"
|
|
20
30
|
});
|
|
21
|
-
server.tool("browser_start_session", "Start a Chrome browser session
|
|
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 () => {
|
|
22
32
|
const session = await getCore().startSession();
|
|
23
33
|
activeSessionId = session.sessionId;
|
|
24
34
|
return { content: [{
|
|
@@ -26,12 +36,11 @@ server.tool("browser_start_session", "Start a Chrome browser session for web aut
|
|
|
26
36
|
text: JSON.stringify(session)
|
|
27
37
|
}] };
|
|
28
38
|
});
|
|
29
|
-
server.tool("browser_navigate", "Navigate the browser to a URL.
|
|
39
|
+
server.tool("browser_navigate", "Navigate the browser to a URL. A session is auto-started if needed.", {
|
|
30
40
|
url: z.string().describe("The URL to navigate to"),
|
|
31
|
-
sessionId: z.string().optional().describe("Session ID (
|
|
41
|
+
sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
|
|
32
42
|
}, async ({ url, sessionId }) => {
|
|
33
|
-
const sid = sessionId
|
|
34
|
-
if (!sid) throw new Error("No active session. Call browser_start_session first.");
|
|
43
|
+
const sid = await resolveSession(sessionId);
|
|
35
44
|
const result = await getCore().runCommand({
|
|
36
45
|
sessionId: sid,
|
|
37
46
|
commandId: genId("nav"),
|
|
@@ -43,25 +52,34 @@ server.tool("browser_navigate", "Navigate the browser to a URL. The browser must
|
|
|
43
52
|
text: JSON.stringify(result)
|
|
44
53
|
}] };
|
|
45
54
|
});
|
|
46
|
-
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).", {
|
|
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.", {
|
|
47
56
|
action: z.enum([
|
|
48
57
|
"click",
|
|
49
58
|
"type",
|
|
50
59
|
"press",
|
|
51
|
-
"waitFor"
|
|
60
|
+
"waitFor",
|
|
61
|
+
"scroll",
|
|
62
|
+
"hover",
|
|
63
|
+
"select",
|
|
64
|
+
"toggle"
|
|
52
65
|
]).describe("The interaction type"),
|
|
53
66
|
selector: z.string().optional().describe("CSS selector for the target element"),
|
|
54
67
|
text: z.string().optional().describe("Text to type (required for \"type\" action)"),
|
|
55
68
|
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)"),
|
|
70
|
+
scrollX: z.number().optional().describe("Horizontal scroll delta in pixels (for \"scroll\" action)"),
|
|
71
|
+
scrollY: z.number().optional().describe("Vertical scroll delta in pixels (for \"scroll\" action, positive = down)"),
|
|
56
72
|
timeoutMs: z.number().optional().describe("Timeout in milliseconds (for \"waitFor\" action, default 4000)"),
|
|
57
|
-
sessionId: z.string().optional().describe("Session ID (
|
|
58
|
-
}, async ({ action, selector, text, key, timeoutMs, sessionId }) => {
|
|
59
|
-
const sid = sessionId
|
|
60
|
-
if (!sid) throw new Error("No active session. Call browser_start_session first.");
|
|
73
|
+
sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
|
|
74
|
+
}, async ({ action, selector, text, key, value, scrollX, scrollY, timeoutMs, sessionId }) => {
|
|
75
|
+
const sid = await resolveSession(sessionId);
|
|
61
76
|
const payload = { action };
|
|
62
77
|
if (selector) payload.selector = selector;
|
|
63
78
|
if (text) payload.text = text;
|
|
64
79
|
if (key) payload.key = key;
|
|
80
|
+
if (value) payload.value = value;
|
|
81
|
+
if (scrollX !== void 0) payload.scrollX = scrollX;
|
|
82
|
+
if (scrollY !== void 0) payload.scrollY = scrollY;
|
|
65
83
|
if (timeoutMs) payload.timeoutMs = timeoutMs;
|
|
66
84
|
const result = await getCore().runCommand({
|
|
67
85
|
sessionId: sid,
|
|
@@ -74,17 +92,17 @@ server.tool("browser_interact", "Interact with a page element. Actions: \"click\
|
|
|
74
92
|
text: JSON.stringify(result)
|
|
75
93
|
}] };
|
|
76
94
|
});
|
|
77
|
-
server.tool("browser_get_content", "Get the current page content. Modes: \"
|
|
95
|
+
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.", {
|
|
78
96
|
mode: z.enum([
|
|
79
97
|
"title",
|
|
80
98
|
"text",
|
|
81
|
-
"html"
|
|
99
|
+
"html",
|
|
100
|
+
"a11y"
|
|
82
101
|
]).default("text").describe("Content extraction mode"),
|
|
83
102
|
selector: z.string().optional().describe("CSS selector to scope content (e.g. \"main\", \"#content\")"),
|
|
84
|
-
sessionId: z.string().optional().describe("Session ID (
|
|
103
|
+
sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
|
|
85
104
|
}, async ({ mode, selector, sessionId }) => {
|
|
86
|
-
const sid = sessionId
|
|
87
|
-
if (!sid) throw new Error("No active session. Call browser_start_session first.");
|
|
105
|
+
const sid = await resolveSession(sessionId);
|
|
88
106
|
const result = await getCore().getPageContent({
|
|
89
107
|
sessionId: sid,
|
|
90
108
|
mode,
|
|
@@ -95,7 +113,7 @@ server.tool("browser_get_content", "Get the current page content. Modes: \"title
|
|
|
95
113
|
text: JSON.stringify(result)
|
|
96
114
|
}] };
|
|
97
115
|
});
|
|
98
|
-
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.
|
|
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.", {
|
|
99
117
|
roles: z.array(z.enum([
|
|
100
118
|
"link",
|
|
101
119
|
"button",
|
|
@@ -110,10 +128,9 @@ server.tool("browser_get_elements", "Discover all interactive elements on the cu
|
|
|
110
128
|
visibleOnly: z.boolean().default(true).describe("Only return visible elements"),
|
|
111
129
|
limit: z.number().default(50).describe("Maximum number of elements to return"),
|
|
112
130
|
selector: z.string().optional().describe("CSS selector to scope element discovery to a subtree"),
|
|
113
|
-
sessionId: z.string().optional().describe("Session ID (
|
|
131
|
+
sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
|
|
114
132
|
}, async ({ roles, visibleOnly, limit, selector, sessionId }) => {
|
|
115
|
-
const sid = sessionId
|
|
116
|
-
if (!sid) throw new Error("No active session. Call browser_start_session first.");
|
|
133
|
+
const sid = await resolveSession(sessionId);
|
|
117
134
|
const result = await getCore().getInteractiveElements({
|
|
118
135
|
sessionId: sid,
|
|
119
136
|
roles,
|
|
@@ -141,9 +158,15 @@ server.tool("browser_search_memory", "Search task memory for previously learned
|
|
|
141
158
|
text: JSON.stringify(result)
|
|
142
159
|
}] };
|
|
143
160
|
});
|
|
144
|
-
server.tool("browser_stop_session", "Stop the browser session and terminate Chrome.
|
|
161
|
+
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 }) => {
|
|
145
162
|
const sid = sessionId ?? activeSessionId;
|
|
146
|
-
if (!sid)
|
|
163
|
+
if (!sid) return { content: [{
|
|
164
|
+
type: "text",
|
|
165
|
+
text: JSON.stringify({
|
|
166
|
+
ok: true,
|
|
167
|
+
message: "No active session to stop."
|
|
168
|
+
})
|
|
169
|
+
}] };
|
|
147
170
|
await getCore().stopSession(sid);
|
|
148
171
|
if (activeSessionId === sid) activeSessionId = void 0;
|
|
149
172
|
return { content: [{
|
|
@@ -163,6 +186,9 @@ async function main() {
|
|
|
163
186
|
} catch {}
|
|
164
187
|
activeSessionId = void 0;
|
|
165
188
|
}
|
|
189
|
+
try {
|
|
190
|
+
getCore().sessions.cleanupSessions({ maxAgeDays: 0 });
|
|
191
|
+
} catch {}
|
|
166
192
|
};
|
|
167
193
|
await server.connect(transport);
|
|
168
194
|
}
|
|
@@ -150,25 +150,19 @@ async function createTarget(cdpUrl, url = "about:blank") {
|
|
|
150
150
|
} catch {}
|
|
151
151
|
return await ensurePageWebSocketUrl(cdpUrl);
|
|
152
152
|
}
|
|
153
|
-
|
|
153
|
+
/** Verify the page is ready and optionally set a custom user-agent, using a single connection. */
|
|
154
|
+
async function initTarget(targetWsUrl, userAgent) {
|
|
154
155
|
const conn = await CdpConnection.connect(targetWsUrl);
|
|
155
156
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
conn.
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
async function evaluateExpression(targetWsUrl, expression) {
|
|
163
|
-
const conn = await CdpConnection.connect(targetWsUrl);
|
|
164
|
-
try {
|
|
165
|
-
await conn.send("Page.enable");
|
|
166
|
-
await conn.send("Runtime.enable");
|
|
167
|
-
return (await conn.send("Runtime.evaluate", {
|
|
168
|
-
expression,
|
|
157
|
+
const enables = [conn.send("Page.enable"), conn.send("Runtime.enable")];
|
|
158
|
+
if (userAgent) enables.push(conn.send("Network.enable"));
|
|
159
|
+
await Promise.all(enables);
|
|
160
|
+
await conn.send("Runtime.evaluate", {
|
|
161
|
+
expression: "window.location.href",
|
|
169
162
|
returnByValue: true,
|
|
170
163
|
awaitPromise: true
|
|
171
|
-
})
|
|
164
|
+
});
|
|
165
|
+
if (userAgent) await conn.send("Network.setUserAgentOverride", { userAgent });
|
|
172
166
|
} finally {
|
|
173
167
|
conn.close();
|
|
174
168
|
}
|
|
@@ -236,8 +230,7 @@ var ChromeCdpBrowserController = class {
|
|
|
236
230
|
if (!port) throw new Error(`Invalid CDP URL: could not extract port from ${cdpUrl}`);
|
|
237
231
|
await waitForDebugger(port);
|
|
238
232
|
const targetWsUrl = await createTarget(cdpUrl);
|
|
239
|
-
await
|
|
240
|
-
if (options?.userAgent) await applyUserAgent(targetWsUrl, options.userAgent);
|
|
233
|
+
await initTarget(targetWsUrl, options?.userAgent);
|
|
241
234
|
return {
|
|
242
235
|
pid: 0,
|
|
243
236
|
cdpUrl,
|
|
@@ -247,14 +240,14 @@ var ChromeCdpBrowserController = class {
|
|
|
247
240
|
async ensureEnabled(targetWsUrl) {
|
|
248
241
|
const cached = this.connections.get(targetWsUrl);
|
|
249
242
|
if (!cached) return;
|
|
250
|
-
|
|
251
|
-
|
|
243
|
+
const promises = [];
|
|
244
|
+
if (!cached.enabled.page) promises.push(cached.conn.send("Page.enable").then(() => {
|
|
252
245
|
cached.enabled.page = true;
|
|
253
|
-
}
|
|
254
|
-
if (!cached.enabled.runtime) {
|
|
255
|
-
await cached.conn.send("Runtime.enable");
|
|
246
|
+
}));
|
|
247
|
+
if (!cached.enabled.runtime) promises.push(cached.conn.send("Runtime.enable").then(() => {
|
|
256
248
|
cached.enabled.runtime = true;
|
|
257
|
-
}
|
|
249
|
+
}));
|
|
250
|
+
if (promises.length) await Promise.all(promises);
|
|
258
251
|
}
|
|
259
252
|
async launch(sessionId, options) {
|
|
260
253
|
const { executablePath: explicitPath, userProfileDir, headless, userAgent } = options ?? {};
|
|
@@ -305,9 +298,8 @@ var ChromeCdpBrowserController = class {
|
|
|
305
298
|
await waitForDebugger(port);
|
|
306
299
|
const cdpUrl = `http://127.0.0.1:${port}`;
|
|
307
300
|
const targetWsUrl = await createTarget(cdpUrl, "about:blank");
|
|
308
|
-
await evaluateExpression(targetWsUrl, "window.location.href");
|
|
309
301
|
if (!child.pid) throw new Error("Failed to launch Chrome process");
|
|
310
|
-
|
|
302
|
+
await initTarget(targetWsUrl, userAgent);
|
|
311
303
|
return {
|
|
312
304
|
pid: child.pid,
|
|
313
305
|
cdpUrl,
|
|
@@ -403,6 +395,43 @@ var ChromeCdpBrowserController = class {
|
|
|
403
395
|
}
|
|
404
396
|
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
405
397
|
}
|
|
398
|
+
if (payload.action === 'scroll') {
|
|
399
|
+
if (payload.selector) {
|
|
400
|
+
const el = document.querySelector(payload.selector);
|
|
401
|
+
if (!el) throw new Error('Selector not found');
|
|
402
|
+
el.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0, behavior: 'smooth' });
|
|
403
|
+
return 'scrolled element';
|
|
404
|
+
}
|
|
405
|
+
window.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0, behavior: 'smooth' });
|
|
406
|
+
return 'scrolled page';
|
|
407
|
+
}
|
|
408
|
+
if (payload.action === 'hover') {
|
|
409
|
+
const el = document.querySelector(payload.selector);
|
|
410
|
+
if (!el) throw new Error('Selector not found');
|
|
411
|
+
const rect = el.getBoundingClientRect();
|
|
412
|
+
const cx = rect.left + rect.width / 2;
|
|
413
|
+
const cy = rect.top + rect.height / 2;
|
|
414
|
+
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, clientX: cx, clientY: cy }));
|
|
415
|
+
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: cx, clientY: cy }));
|
|
416
|
+
el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: cx, clientY: cy }));
|
|
417
|
+
return 'hovered';
|
|
418
|
+
}
|
|
419
|
+
if (payload.action === 'select') {
|
|
420
|
+
const el = document.querySelector(payload.selector);
|
|
421
|
+
if (!el) throw new Error('Selector not found');
|
|
422
|
+
if (el.tagName.toLowerCase() !== 'select') throw new Error('Element is not a <select>');
|
|
423
|
+
el.value = payload.value ?? '';
|
|
424
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
425
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
426
|
+
return 'selected ' + el.value;
|
|
427
|
+
}
|
|
428
|
+
if (payload.action === 'toggle') {
|
|
429
|
+
const el = document.querySelector(payload.selector);
|
|
430
|
+
if (!el) throw new Error('Selector not found');
|
|
431
|
+
el.click();
|
|
432
|
+
const checked = el.checked !== undefined ? el.checked : el.getAttribute('aria-checked') === 'true';
|
|
433
|
+
return 'toggled to ' + (checked ? 'checked' : 'unchecked');
|
|
434
|
+
}
|
|
406
435
|
throw new Error('Unsupported interact action');
|
|
407
436
|
})()`;
|
|
408
437
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
@@ -422,6 +451,10 @@ var ChromeCdpBrowserController = class {
|
|
|
422
451
|
});
|
|
423
452
|
}
|
|
424
453
|
async getContent(targetWsUrl, options) {
|
|
454
|
+
if (options.mode === "a11y") return {
|
|
455
|
+
mode: "a11y",
|
|
456
|
+
content: await this.getAccessibilityTree(targetWsUrl)
|
|
457
|
+
};
|
|
425
458
|
const expression = `(() => {
|
|
426
459
|
const options = ${JSON.stringify(options)};
|
|
427
460
|
if (options.mode === 'title') return document.title ?? '';
|
|
@@ -450,6 +483,51 @@ var ChromeCdpBrowserController = class {
|
|
|
450
483
|
};
|
|
451
484
|
});
|
|
452
485
|
}
|
|
486
|
+
async getAccessibilityTree(targetWsUrl) {
|
|
487
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
488
|
+
await conn.send("Accessibility.enable");
|
|
489
|
+
const { nodes } = await conn.send("Accessibility.getFullAXTree");
|
|
490
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
491
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
492
|
+
for (const node of nodes) {
|
|
493
|
+
nodeMap.set(node.nodeId, node);
|
|
494
|
+
if (node.parentId) {
|
|
495
|
+
const siblings = childrenMap.get(node.parentId);
|
|
496
|
+
if (siblings) siblings.push(node.nodeId);
|
|
497
|
+
else childrenMap.set(node.parentId, [node.nodeId]);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const lines = [];
|
|
501
|
+
const formatNode = (nodeId, depth) => {
|
|
502
|
+
const node = nodeMap.get(nodeId);
|
|
503
|
+
if (!node) return;
|
|
504
|
+
const role = node.role?.value ?? "unknown";
|
|
505
|
+
const name = node.name?.value ?? "";
|
|
506
|
+
const value = node.value?.value ?? "";
|
|
507
|
+
if (node.ignored) {
|
|
508
|
+
const children = childrenMap.get(nodeId) ?? node.childIds ?? [];
|
|
509
|
+
for (const childId of children) formatNode(childId, depth);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const skip = !name && !value && (role === "generic" || role === "none" || role === "GenericContainer");
|
|
513
|
+
if (!skip) {
|
|
514
|
+
let line = `${" ".repeat(depth)}${role}`;
|
|
515
|
+
if (name) line += ` "${name}"`;
|
|
516
|
+
if (value) line += ` value="${value}"`;
|
|
517
|
+
if (node.properties) {
|
|
518
|
+
for (const prop of node.properties) if (prop.value.value === true) line += ` [${prop.name}]`;
|
|
519
|
+
else if (prop.name === "checked" && prop.value.value === "mixed") line += ` [indeterminate]`;
|
|
520
|
+
}
|
|
521
|
+
lines.push(line);
|
|
522
|
+
}
|
|
523
|
+
const children = childrenMap.get(nodeId) ?? node.childIds ?? [];
|
|
524
|
+
for (const childId of children) formatNode(childId, skip ? depth : depth + 1);
|
|
525
|
+
};
|
|
526
|
+
const roots = nodes.filter((n) => !n.parentId);
|
|
527
|
+
for (const root of roots) formatNode(root.nodeId, 0);
|
|
528
|
+
return lines.join("\n");
|
|
529
|
+
});
|
|
530
|
+
}
|
|
453
531
|
async getInteractiveElements(targetWsUrl, options) {
|
|
454
532
|
const expression = `(() => {
|
|
455
533
|
const options = ${JSON.stringify(options)};
|
|
@@ -799,6 +877,15 @@ var SessionStore = class {
|
|
|
799
877
|
};
|
|
800
878
|
this.write(state);
|
|
801
879
|
}
|
|
880
|
+
/** Remove all terminated sessions from the store. Returns the count removed. */
|
|
881
|
+
purgeTerminated() {
|
|
882
|
+
const state = this.read();
|
|
883
|
+
const before = Object.keys(state.sessions).length;
|
|
884
|
+
for (const [id, record] of Object.entries(state.sessions)) if (record.session.status === "terminated" && id !== state.activeSessionId) delete state.sessions[id];
|
|
885
|
+
const removed = before - Object.keys(state.sessions).length;
|
|
886
|
+
if (removed > 0) this.write(state);
|
|
887
|
+
return removed;
|
|
888
|
+
}
|
|
802
889
|
replaceSessions(sessions, activeSessionId) {
|
|
803
890
|
const state = {
|
|
804
891
|
sessions: Object.fromEntries(sessions.map((record) => [record.session.sessionId, record])),
|
|
@@ -826,7 +913,10 @@ var SessionManager = class {
|
|
|
826
913
|
async createSession(input) {
|
|
827
914
|
if (input.browser !== "chrome") throw new Error("Only chrome is supported");
|
|
828
915
|
const active = this.store.getActive();
|
|
829
|
-
if (active && active.session.status !== "terminated")
|
|
916
|
+
if (active && active.session.status !== "terminated") {
|
|
917
|
+
if (await this.isSessionAlive(active)) return active.session;
|
|
918
|
+
await this.forceTerminate(active);
|
|
919
|
+
}
|
|
830
920
|
const sessionId = crypto.randomUUID();
|
|
831
921
|
const token = this.ctx.tokenService.issue(sessionId);
|
|
832
922
|
const launched = this.ctx.config.cdpUrl ? await this.browser.connect(this.ctx.config.cdpUrl, { userAgent: this.ctx.config.userAgent }) : await this.browser.launch(sessionId, {
|
|
@@ -854,9 +944,25 @@ var SessionManager = class {
|
|
|
854
944
|
getSession(sessionId) {
|
|
855
945
|
return this.mustGetRecord(sessionId).session;
|
|
856
946
|
}
|
|
947
|
+
/** Return a healthy session, recovering automatically if needed. */
|
|
948
|
+
async ensureSession(sessionId) {
|
|
949
|
+
const record = this.store.get(sessionId);
|
|
950
|
+
if (!record) throw new Error("Session not found");
|
|
951
|
+
if (record.session.status === "ready") {
|
|
952
|
+
if (await this.isSessionAlive(record)) return record;
|
|
953
|
+
this.recordEvent(sessionId, "lifecycle", "warning", "Session connection lost, recovering");
|
|
954
|
+
}
|
|
955
|
+
if (record.session.status !== "terminated") try {
|
|
956
|
+
const recovered = await this.restartSession(sessionId);
|
|
957
|
+
return this.mustGetRecord(recovered.sessionId);
|
|
958
|
+
} catch (restartError) {
|
|
959
|
+
this.recordEvent(sessionId, "lifecycle", "error", `Recovery failed: ${restartError.message}`);
|
|
960
|
+
throw new Error(`Session is not ready and recovery failed: ${restartError.message}`);
|
|
961
|
+
}
|
|
962
|
+
throw new Error("Session is terminated. Start a new session.");
|
|
963
|
+
}
|
|
857
964
|
async executeCommand(sessionId, input) {
|
|
858
|
-
const record = this.
|
|
859
|
-
if (record.session.status !== "ready") throw new Error("Session is not ready. Restart session and retry.");
|
|
965
|
+
const record = await this.ensureSession(sessionId);
|
|
860
966
|
const command = {
|
|
861
967
|
commandId: input.commandId,
|
|
862
968
|
sessionId,
|
|
@@ -922,13 +1028,11 @@ var SessionManager = class {
|
|
|
922
1028
|
return completed;
|
|
923
1029
|
}
|
|
924
1030
|
async getContent(sessionId, options) {
|
|
925
|
-
const record = this.
|
|
926
|
-
if (record.session.status !== "ready") throw new Error("Session is not ready");
|
|
1031
|
+
const record = await this.ensureSession(sessionId);
|
|
927
1032
|
return await this.browser.getContent(record.targetWsUrl, options);
|
|
928
1033
|
}
|
|
929
1034
|
async getInteractiveElements(sessionId, options) {
|
|
930
|
-
const record = this.
|
|
931
|
-
if (record.session.status !== "ready") throw new Error("Session is not ready");
|
|
1035
|
+
const record = await this.ensureSession(sessionId);
|
|
932
1036
|
return await this.browser.getInteractiveElements(record.targetWsUrl, options);
|
|
933
1037
|
}
|
|
934
1038
|
setStatus(status, reason) {
|
|
@@ -965,11 +1069,13 @@ var SessionManager = class {
|
|
|
965
1069
|
}
|
|
966
1070
|
async restartSession(sessionId) {
|
|
967
1071
|
const record = this.mustGetRecord(sessionId);
|
|
968
|
-
if (record.session.status === "ready") return record.session;
|
|
969
1072
|
if (this.browser.closeConnection) try {
|
|
970
1073
|
this.browser.closeConnection(record.targetWsUrl);
|
|
971
1074
|
} catch {}
|
|
972
|
-
|
|
1075
|
+
if (record.pid > 0) try {
|
|
1076
|
+
this.browser.terminate(record.pid);
|
|
1077
|
+
} catch {}
|
|
1078
|
+
const relaunched = this.ctx.config.cdpUrl ? await this.browser.connect(this.ctx.config.cdpUrl, { userAgent: this.ctx.config.userAgent }) : await this.browser.launch(sessionId, {
|
|
973
1079
|
executablePath: this.ctx.config.browserExecutablePath,
|
|
974
1080
|
userProfileDir: this.ctx.config.userProfileDir,
|
|
975
1081
|
headless: this.ctx.config.headless,
|
|
@@ -1067,6 +1173,40 @@ var SessionManager = class {
|
|
|
1067
1173
|
dryRun
|
|
1068
1174
|
};
|
|
1069
1175
|
}
|
|
1176
|
+
/** Quick health check — probe the CDP connection with a lightweight evaluate. */
|
|
1177
|
+
async isSessionAlive(record) {
|
|
1178
|
+
if (!record.targetWsUrl) return false;
|
|
1179
|
+
if (record.pid > 0) try {
|
|
1180
|
+
process.kill(record.pid, 0);
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
if (err.code === "EPERM") {} else return false;
|
|
1183
|
+
}
|
|
1184
|
+
try {
|
|
1185
|
+
await this.browser.getContent(record.targetWsUrl, { mode: "title" });
|
|
1186
|
+
return true;
|
|
1187
|
+
} catch {
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
/** Force-terminate a session record, cleaning up process, connection, and store. */
|
|
1192
|
+
async forceTerminate(record) {
|
|
1193
|
+
const { sessionId } = record.session;
|
|
1194
|
+
if (this.browser.closeConnection) try {
|
|
1195
|
+
this.browser.closeConnection(record.targetWsUrl);
|
|
1196
|
+
} catch {}
|
|
1197
|
+
if (record.pid > 0) try {
|
|
1198
|
+
this.browser.terminate(record.pid);
|
|
1199
|
+
} catch {}
|
|
1200
|
+
this.ctx.tokenService.revoke(sessionId);
|
|
1201
|
+
const terminated = {
|
|
1202
|
+
...record.session,
|
|
1203
|
+
status: "terminated",
|
|
1204
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1205
|
+
};
|
|
1206
|
+
this.store.setSession(terminated);
|
|
1207
|
+
this.store.clearActive(sessionId);
|
|
1208
|
+
this.recordEvent(sessionId, "lifecycle", "warning", "Session force-terminated (stale)");
|
|
1209
|
+
}
|
|
1070
1210
|
mustGetRecord(sessionId) {
|
|
1071
1211
|
const record = this.store.get(sessionId);
|
|
1072
1212
|
if (!record) throw new Error("Session not found");
|
|
@@ -1253,7 +1393,7 @@ var EventStore = class {
|
|
|
1253
1393
|
const existing = this.events.get(event.sessionId) ?? [];
|
|
1254
1394
|
existing.push(event);
|
|
1255
1395
|
this.events.set(event.sessionId, existing);
|
|
1256
|
-
fs.
|
|
1396
|
+
fs.appendFile(this.filePath, `${JSON.stringify(event)}\n`, "utf8", () => {});
|
|
1257
1397
|
}
|
|
1258
1398
|
list(sessionId, limit = 100) {
|
|
1259
1399
|
const entries = this.events.get(sessionId) ?? [];
|
|
@@ -1756,7 +1896,13 @@ var MemoryService = class {
|
|
|
1756
1896
|
return failed;
|
|
1757
1897
|
}
|
|
1758
1898
|
findBestExactMatch(insights, taskIntent, siteDomain) {
|
|
1759
|
-
|
|
1899
|
+
const intentLower = taskIntent.toLowerCase();
|
|
1900
|
+
const domainLower = siteDomain.toLowerCase();
|
|
1901
|
+
let best;
|
|
1902
|
+
for (const insight of insights) if (insight.taskIntent.toLowerCase() === intentLower && insight.siteDomain.toLowerCase() === domainLower) {
|
|
1903
|
+
if (!best || insight.updatedAt > best.updatedAt) best = insight;
|
|
1904
|
+
}
|
|
1905
|
+
return best;
|
|
1760
1906
|
}
|
|
1761
1907
|
createEvidence(input, result, reason) {
|
|
1762
1908
|
return {
|