agentic-browser 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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-CODdeRWR.mjs} +157 -9
- 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-CODdeRWR.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-CODdeRWR.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-CODdeRWR.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
|
}
|
|
@@ -403,6 +403,43 @@ var ChromeCdpBrowserController = class {
|
|
|
403
403
|
}
|
|
404
404
|
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
405
405
|
}
|
|
406
|
+
if (payload.action === 'scroll') {
|
|
407
|
+
if (payload.selector) {
|
|
408
|
+
const el = document.querySelector(payload.selector);
|
|
409
|
+
if (!el) throw new Error('Selector not found');
|
|
410
|
+
el.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0, behavior: 'smooth' });
|
|
411
|
+
return 'scrolled element';
|
|
412
|
+
}
|
|
413
|
+
window.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0, behavior: 'smooth' });
|
|
414
|
+
return 'scrolled page';
|
|
415
|
+
}
|
|
416
|
+
if (payload.action === 'hover') {
|
|
417
|
+
const el = document.querySelector(payload.selector);
|
|
418
|
+
if (!el) throw new Error('Selector not found');
|
|
419
|
+
const rect = el.getBoundingClientRect();
|
|
420
|
+
const cx = rect.left + rect.width / 2;
|
|
421
|
+
const cy = rect.top + rect.height / 2;
|
|
422
|
+
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, clientX: cx, clientY: cy }));
|
|
423
|
+
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: cx, clientY: cy }));
|
|
424
|
+
el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: cx, clientY: cy }));
|
|
425
|
+
return 'hovered';
|
|
426
|
+
}
|
|
427
|
+
if (payload.action === 'select') {
|
|
428
|
+
const el = document.querySelector(payload.selector);
|
|
429
|
+
if (!el) throw new Error('Selector not found');
|
|
430
|
+
if (el.tagName.toLowerCase() !== 'select') throw new Error('Element is not a <select>');
|
|
431
|
+
el.value = payload.value ?? '';
|
|
432
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
433
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
434
|
+
return 'selected ' + el.value;
|
|
435
|
+
}
|
|
436
|
+
if (payload.action === 'toggle') {
|
|
437
|
+
const el = document.querySelector(payload.selector);
|
|
438
|
+
if (!el) throw new Error('Selector not found');
|
|
439
|
+
el.click();
|
|
440
|
+
const checked = el.checked !== undefined ? el.checked : el.getAttribute('aria-checked') === 'true';
|
|
441
|
+
return 'toggled to ' + (checked ? 'checked' : 'unchecked');
|
|
442
|
+
}
|
|
406
443
|
throw new Error('Unsupported interact action');
|
|
407
444
|
})()`;
|
|
408
445
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
@@ -422,6 +459,10 @@ var ChromeCdpBrowserController = class {
|
|
|
422
459
|
});
|
|
423
460
|
}
|
|
424
461
|
async getContent(targetWsUrl, options) {
|
|
462
|
+
if (options.mode === "a11y") return {
|
|
463
|
+
mode: "a11y",
|
|
464
|
+
content: await this.getAccessibilityTree(targetWsUrl)
|
|
465
|
+
};
|
|
425
466
|
const expression = `(() => {
|
|
426
467
|
const options = ${JSON.stringify(options)};
|
|
427
468
|
if (options.mode === 'title') return document.title ?? '';
|
|
@@ -450,6 +491,51 @@ var ChromeCdpBrowserController = class {
|
|
|
450
491
|
};
|
|
451
492
|
});
|
|
452
493
|
}
|
|
494
|
+
async getAccessibilityTree(targetWsUrl) {
|
|
495
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
496
|
+
await conn.send("Accessibility.enable");
|
|
497
|
+
const { nodes } = await conn.send("Accessibility.getFullAXTree");
|
|
498
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
499
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
500
|
+
for (const node of nodes) {
|
|
501
|
+
nodeMap.set(node.nodeId, node);
|
|
502
|
+
if (node.parentId) {
|
|
503
|
+
const siblings = childrenMap.get(node.parentId);
|
|
504
|
+
if (siblings) siblings.push(node.nodeId);
|
|
505
|
+
else childrenMap.set(node.parentId, [node.nodeId]);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const lines = [];
|
|
509
|
+
const formatNode = (nodeId, depth) => {
|
|
510
|
+
const node = nodeMap.get(nodeId);
|
|
511
|
+
if (!node) return;
|
|
512
|
+
const role = node.role?.value ?? "unknown";
|
|
513
|
+
const name = node.name?.value ?? "";
|
|
514
|
+
const value = node.value?.value ?? "";
|
|
515
|
+
if (node.ignored) {
|
|
516
|
+
const children = childrenMap.get(nodeId) ?? node.childIds ?? [];
|
|
517
|
+
for (const childId of children) formatNode(childId, depth);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const skip = !name && !value && (role === "generic" || role === "none" || role === "GenericContainer");
|
|
521
|
+
if (!skip) {
|
|
522
|
+
let line = `${" ".repeat(depth)}${role}`;
|
|
523
|
+
if (name) line += ` "${name}"`;
|
|
524
|
+
if (value) line += ` value="${value}"`;
|
|
525
|
+
if (node.properties) {
|
|
526
|
+
for (const prop of node.properties) if (prop.value.value === true) line += ` [${prop.name}]`;
|
|
527
|
+
else if (prop.name === "checked" && prop.value.value === "mixed") line += ` [indeterminate]`;
|
|
528
|
+
}
|
|
529
|
+
lines.push(line);
|
|
530
|
+
}
|
|
531
|
+
const children = childrenMap.get(nodeId) ?? node.childIds ?? [];
|
|
532
|
+
for (const childId of children) formatNode(childId, skip ? depth : depth + 1);
|
|
533
|
+
};
|
|
534
|
+
const roots = nodes.filter((n) => !n.parentId);
|
|
535
|
+
for (const root of roots) formatNode(root.nodeId, 0);
|
|
536
|
+
return lines.join("\n");
|
|
537
|
+
});
|
|
538
|
+
}
|
|
453
539
|
async getInteractiveElements(targetWsUrl, options) {
|
|
454
540
|
const expression = `(() => {
|
|
455
541
|
const options = ${JSON.stringify(options)};
|
|
@@ -799,6 +885,15 @@ var SessionStore = class {
|
|
|
799
885
|
};
|
|
800
886
|
this.write(state);
|
|
801
887
|
}
|
|
888
|
+
/** Remove all terminated sessions from the store. Returns the count removed. */
|
|
889
|
+
purgeTerminated() {
|
|
890
|
+
const state = this.read();
|
|
891
|
+
const before = Object.keys(state.sessions).length;
|
|
892
|
+
for (const [id, record] of Object.entries(state.sessions)) if (record.session.status === "terminated" && id !== state.activeSessionId) delete state.sessions[id];
|
|
893
|
+
const removed = before - Object.keys(state.sessions).length;
|
|
894
|
+
if (removed > 0) this.write(state);
|
|
895
|
+
return removed;
|
|
896
|
+
}
|
|
802
897
|
replaceSessions(sessions, activeSessionId) {
|
|
803
898
|
const state = {
|
|
804
899
|
sessions: Object.fromEntries(sessions.map((record) => [record.session.sessionId, record])),
|
|
@@ -826,7 +921,10 @@ var SessionManager = class {
|
|
|
826
921
|
async createSession(input) {
|
|
827
922
|
if (input.browser !== "chrome") throw new Error("Only chrome is supported");
|
|
828
923
|
const active = this.store.getActive();
|
|
829
|
-
if (active && active.session.status !== "terminated")
|
|
924
|
+
if (active && active.session.status !== "terminated") {
|
|
925
|
+
if (await this.isSessionAlive(active)) return active.session;
|
|
926
|
+
await this.forceTerminate(active);
|
|
927
|
+
}
|
|
830
928
|
const sessionId = crypto.randomUUID();
|
|
831
929
|
const token = this.ctx.tokenService.issue(sessionId);
|
|
832
930
|
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 +952,25 @@ var SessionManager = class {
|
|
|
854
952
|
getSession(sessionId) {
|
|
855
953
|
return this.mustGetRecord(sessionId).session;
|
|
856
954
|
}
|
|
955
|
+
/** Return a healthy session, recovering automatically if needed. */
|
|
956
|
+
async ensureSession(sessionId) {
|
|
957
|
+
const record = this.store.get(sessionId);
|
|
958
|
+
if (!record) throw new Error("Session not found");
|
|
959
|
+
if (record.session.status === "ready") {
|
|
960
|
+
if (await this.isSessionAlive(record)) return record;
|
|
961
|
+
this.recordEvent(sessionId, "lifecycle", "warning", "Session connection lost, recovering");
|
|
962
|
+
}
|
|
963
|
+
if (record.session.status !== "terminated") try {
|
|
964
|
+
const recovered = await this.restartSession(sessionId);
|
|
965
|
+
return this.mustGetRecord(recovered.sessionId);
|
|
966
|
+
} catch (restartError) {
|
|
967
|
+
this.recordEvent(sessionId, "lifecycle", "error", `Recovery failed: ${restartError.message}`);
|
|
968
|
+
throw new Error(`Session is not ready and recovery failed: ${restartError.message}`);
|
|
969
|
+
}
|
|
970
|
+
throw new Error("Session is terminated. Start a new session.");
|
|
971
|
+
}
|
|
857
972
|
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.");
|
|
973
|
+
const record = await this.ensureSession(sessionId);
|
|
860
974
|
const command = {
|
|
861
975
|
commandId: input.commandId,
|
|
862
976
|
sessionId,
|
|
@@ -922,13 +1036,11 @@ var SessionManager = class {
|
|
|
922
1036
|
return completed;
|
|
923
1037
|
}
|
|
924
1038
|
async getContent(sessionId, options) {
|
|
925
|
-
const record = this.
|
|
926
|
-
if (record.session.status !== "ready") throw new Error("Session is not ready");
|
|
1039
|
+
const record = await this.ensureSession(sessionId);
|
|
927
1040
|
return await this.browser.getContent(record.targetWsUrl, options);
|
|
928
1041
|
}
|
|
929
1042
|
async getInteractiveElements(sessionId, options) {
|
|
930
|
-
const record = this.
|
|
931
|
-
if (record.session.status !== "ready") throw new Error("Session is not ready");
|
|
1043
|
+
const record = await this.ensureSession(sessionId);
|
|
932
1044
|
return await this.browser.getInteractiveElements(record.targetWsUrl, options);
|
|
933
1045
|
}
|
|
934
1046
|
setStatus(status, reason) {
|
|
@@ -965,11 +1077,13 @@ var SessionManager = class {
|
|
|
965
1077
|
}
|
|
966
1078
|
async restartSession(sessionId) {
|
|
967
1079
|
const record = this.mustGetRecord(sessionId);
|
|
968
|
-
if (record.session.status === "ready") return record.session;
|
|
969
1080
|
if (this.browser.closeConnection) try {
|
|
970
1081
|
this.browser.closeConnection(record.targetWsUrl);
|
|
971
1082
|
} catch {}
|
|
972
|
-
|
|
1083
|
+
if (record.pid > 0) try {
|
|
1084
|
+
this.browser.terminate(record.pid);
|
|
1085
|
+
} catch {}
|
|
1086
|
+
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
1087
|
executablePath: this.ctx.config.browserExecutablePath,
|
|
974
1088
|
userProfileDir: this.ctx.config.userProfileDir,
|
|
975
1089
|
headless: this.ctx.config.headless,
|
|
@@ -1067,6 +1181,40 @@ var SessionManager = class {
|
|
|
1067
1181
|
dryRun
|
|
1068
1182
|
};
|
|
1069
1183
|
}
|
|
1184
|
+
/** Quick health check — probe the CDP connection with a lightweight evaluate. */
|
|
1185
|
+
async isSessionAlive(record) {
|
|
1186
|
+
if (!record.targetWsUrl) return false;
|
|
1187
|
+
if (record.pid > 0) try {
|
|
1188
|
+
process.kill(record.pid, 0);
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
if (err.code === "EPERM") {} else return false;
|
|
1191
|
+
}
|
|
1192
|
+
try {
|
|
1193
|
+
await this.browser.getContent(record.targetWsUrl, { mode: "title" });
|
|
1194
|
+
return true;
|
|
1195
|
+
} catch {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
/** Force-terminate a session record, cleaning up process, connection, and store. */
|
|
1200
|
+
async forceTerminate(record) {
|
|
1201
|
+
const { sessionId } = record.session;
|
|
1202
|
+
if (this.browser.closeConnection) try {
|
|
1203
|
+
this.browser.closeConnection(record.targetWsUrl);
|
|
1204
|
+
} catch {}
|
|
1205
|
+
if (record.pid > 0) try {
|
|
1206
|
+
this.browser.terminate(record.pid);
|
|
1207
|
+
} catch {}
|
|
1208
|
+
this.ctx.tokenService.revoke(sessionId);
|
|
1209
|
+
const terminated = {
|
|
1210
|
+
...record.session,
|
|
1211
|
+
status: "terminated",
|
|
1212
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1213
|
+
};
|
|
1214
|
+
this.store.setSession(terminated);
|
|
1215
|
+
this.store.clearActive(sessionId);
|
|
1216
|
+
this.recordEvent(sessionId, "lifecycle", "warning", "Session force-terminated (stale)");
|
|
1217
|
+
}
|
|
1070
1218
|
mustGetRecord(sessionId) {
|
|
1071
1219
|
const record = this.store.get(sessionId);
|
|
1072
1220
|
if (!record) throw new Error("Session not found");
|