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 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 | Description |
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 | Launch with a real profile |
99
- | `AGENTIC_BROWSER_HEADLESS` | `true` | Run Chrome in headless mode |
100
- | `AGENTIC_BROWSER_USER_AGENT` | `MyBot/1.0` | Override the browser user-agent |
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
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as createCliRuntime } from "../runtime-Dvmv5Xi_.mjs";
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-Dvmv5Xi_.mjs";
2
+ import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-C32ai0TQ.mjs";
3
3
 
4
4
  export { AgenticBrowserCore, createAgenticBrowserCore, createMockAgenticBrowserCore };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as createAgenticBrowserCore } from "../runtime-Dvmv5Xi_.mjs";
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 for web automation. Call this first before using any other browser tool. Returns a sessionId you'll need for all subsequent calls.", {}, async () => {
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. The browser must have an active session.", {
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 (uses active session if omitted)")
41
+ sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
32
42
  }, async ({ url, sessionId }) => {
33
- const sid = sessionId ?? activeSessionId;
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 (uses active session if omitted)")
58
- }, async ({ action, selector, text, key, timeoutMs, sessionId }) => {
59
- const sid = sessionId ?? activeSessionId;
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: \"title\" (page title only), \"text\" (readable text content), \"html\" (raw HTML). Use selector to scope to a specific element.", {
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 (uses active session if omitted)")
103
+ sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
85
104
  }, async ({ mode, selector, sessionId }) => {
86
- const sid = sessionId ?? activeSessionId;
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. Call this to understand what's on the page before interacting.", {
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 (uses active session if omitted)")
131
+ sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
114
132
  }, async ({ roles, visibleOnly, limit, selector, sessionId }) => {
115
- const sid = sessionId ?? activeSessionId;
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. Call this when you're done with browser automation.", { sessionId: z.string().optional().describe("Session ID (uses active session if omitted)") }, async ({ sessionId }) => {
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) throw new Error("No active session.");
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
- async function applyUserAgent(targetWsUrl, userAgent) {
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
- await conn.send("Network.enable");
157
- await conn.send("Network.setUserAgentOverride", { userAgent });
158
- } finally {
159
- conn.close();
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
- })).result.value ?? "";
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 evaluateExpression(targetWsUrl, "window.location.href");
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
- if (!cached.enabled.page) {
251
- await cached.conn.send("Page.enable");
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
- if (userAgent) await applyUserAgent(targetWsUrl, userAgent);
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") throw new Error("A managed session is already active");
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.mustGetRecord(sessionId);
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.mustGetRecord(sessionId);
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.mustGetRecord(sessionId);
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
- const relaunched = await this.browser.launch(sessionId, {
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.appendFileSync(this.filePath, `${JSON.stringify(event)}\n`, "utf8");
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
- return insights.filter((insight) => insight.taskIntent.toLowerCase() === taskIntent.toLowerCase() && insight.siteDomain.toLowerCase() === siteDomain.toLowerCase()).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-browser",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",