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 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-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-Dvmv5Xi_.mjs";
2
+ import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-CODdeRWR.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-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 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
  }
@@ -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") throw new Error("A managed session is already active");
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.mustGetRecord(sessionId);
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.mustGetRecord(sessionId);
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.mustGetRecord(sessionId);
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
- const relaunched = await this.browser.launch(sessionId, {
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-browser",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",