agentic-browser 2.3.0 → 2.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/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-BCGndjhR.mjs";
2
+ import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-PnWvjdzU.mjs";
3
3
  export { AgenticBrowserCore, createAgenticBrowserCore, createMockAgenticBrowserCore };
@@ -0,0 +1,5 @@
1
+
2
+ //#region src/mcp/index.d.ts
3
+ declare function main(): Promise<void>;
4
+ //#endregion
5
+ export { main };
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { n as createAgenticBrowserCore } from "../runtime-BCGndjhR.mjs";
2
+ import { n as createAgenticBrowserCore } from "../runtime-PnWvjdzU.mjs";
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  import { z } from "zod";
4
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -199,6 +202,29 @@ function compactMemoryResults(results) {
199
202
  }
200
203
  //#endregion
201
204
  //#region src/mcp/index.ts
205
+ function readPackageVersion() {
206
+ try {
207
+ const dir = dirname(fileURLToPath(import.meta.url));
208
+ for (const rel of ["../../package.json", "../package.json"]) try {
209
+ const pkg = JSON.parse(readFileSync(join(dir, rel), "utf8"));
210
+ if (pkg.version) return pkg.version;
211
+ } catch {}
212
+ } catch {}
213
+ return "0.0.0";
214
+ }
215
+ function classifyError(message) {
216
+ const lower = message.toLowerCase();
217
+ if (lower.includes("not found") || lower.includes("no element")) return "SELECTOR_NOT_FOUND";
218
+ if (lower.includes("covered") || lower.includes("overlay")) return "ELEMENT_COVERED";
219
+ if (lower.includes("not editable") || lower.includes("readonly")) return "ELEMENT_NOT_EDITABLE";
220
+ if (lower.includes("navigation failed")) return "NAVIGATION_FAILED";
221
+ if (lower.includes("navigation") && lower.includes("timeout")) return "NAVIGATION_TIMEOUT";
222
+ if (lower.includes("cdp connection") || lower.includes("websocket")) return "CDP_DISCONNECTED";
223
+ if (lower.includes("session") && (lower.includes("dead") || lower.includes("terminated"))) return "SESSION_DEAD";
224
+ if (lower.includes("dialog")) return "DIALOG_BLOCKING";
225
+ if (lower.includes("timeout")) return "TIMEOUT";
226
+ return "UNKNOWN";
227
+ }
202
228
  let core;
203
229
  let activeSessionId;
204
230
  const DEFAULT_MCP_MAX_CHARS = {
@@ -227,7 +253,7 @@ async function resolveSession(sessionId) {
227
253
  }
228
254
  const server = new McpServer({
229
255
  name: "agentic-browser",
230
- version: "0.1.0"
256
+ version: readPackageVersion()
231
257
  });
232
258
  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 () => {
233
259
  const session = await getCore().startSession();
@@ -254,11 +280,17 @@ server.tool("browser_navigate", "Navigate the browser to a URL. A session is aut
254
280
  const dismissed = await getCore().dismissCookieBanner(sid);
255
281
  if (dismissed.dismissed) cookieBanner = dismissed;
256
282
  } catch {}
283
+ let currentUrl;
284
+ try {
285
+ currentUrl = await getCore().getCurrentUrl(sid);
286
+ } catch {}
257
287
  const compact = {
258
288
  resultStatus: result.resultStatus,
259
289
  resultMessage: result.resultMessage,
290
+ currentUrl,
260
291
  cookieBanner
261
292
  };
293
+ if (result.resultStatus === "failed") compact.errorCode = classifyError(result.resultMessage ?? "");
262
294
  return { content: [{
263
295
  type: "text",
264
296
  text: JSON.stringify(compact)
@@ -305,10 +337,16 @@ server.tool("browser_interact", "Interact with a page element or perform browser
305
337
  type: "interact",
306
338
  payload
307
339
  });
340
+ let currentUrl;
341
+ try {
342
+ currentUrl = await getCore().getCurrentUrl(sid);
343
+ } catch {}
308
344
  const compact = {
309
345
  resultStatus: result.resultStatus,
310
- resultMessage: result.resultMessage
346
+ resultMessage: result.resultMessage,
347
+ currentUrl
311
348
  };
349
+ if (result.resultStatus === "failed") compact.errorCode = classifyError(result.resultMessage ?? "");
312
350
  return { content: [{
313
351
  type: "text",
314
352
  text: JSON.stringify(compact)
@@ -392,6 +430,24 @@ server.tool("browser_dismiss_cookies", "Dismiss cookie consent banners on the cu
392
430
  text: JSON.stringify(result)
393
431
  }] };
394
432
  });
433
+ server.tool("browser_screenshot", "Capture a screenshot of the current page. Returns a base64-encoded image. Useful for visual verification, debugging element discovery failures, or when you need to see what the page looks like. A session is auto-started if needed.", {
434
+ format: z.enum(["jpeg", "png"]).default("jpeg").describe("Image format (jpeg is smaller, png is lossless)"),
435
+ quality: z.number().optional().describe("JPEG quality 0-100 (default 60, ignored for png)"),
436
+ fullPage: z.boolean().default(false).describe("Capture the full scrollable page instead of just the viewport"),
437
+ sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
438
+ }, async ({ format, quality, fullPage, sessionId }) => {
439
+ const sid = await resolveSession(sessionId);
440
+ const result = await getCore().screenshot(sid, {
441
+ format,
442
+ quality,
443
+ fullPage
444
+ });
445
+ return { content: [{
446
+ type: "image",
447
+ data: result.data,
448
+ mimeType: result.mimeType
449
+ }] };
450
+ });
395
451
  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 }) => {
396
452
  const sid = sessionId ?? activeSessionId;
397
453
  if (!sid) return { content: [{
@@ -224,19 +224,78 @@ async function createTarget(cdpUrl, url = "about:blank") {
224
224
  } catch {}
225
225
  return await ensurePageWebSocketUrl(cdpUrl);
226
226
  }
227
- /** Verify the page is ready and optionally set a custom user-agent, using a single connection. */
227
+ /**
228
+ * Apply stealth patches that remove common automation fingerprints.
229
+ * These run before any page loads so sites cannot detect the bot.
230
+ */
231
+ const STEALTH_SCRIPT = `
232
+ // Remove navigator.webdriver flag
233
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
234
+
235
+ // Provide realistic plugins array (empty = headless fingerprint)
236
+ Object.defineProperty(navigator, 'plugins', {
237
+ get: () => {
238
+ const plugins = [
239
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
240
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
241
+ { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
242
+ ];
243
+ plugins.length = 3;
244
+ Object.setPrototypeOf(plugins, PluginArray.prototype);
245
+ return plugins;
246
+ }
247
+ });
248
+
249
+ // Provide realistic languages
250
+ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
251
+
252
+ // Patch chrome.runtime to look like a real Chrome
253
+ if (!window.chrome) window.chrome = {};
254
+ if (!window.chrome.runtime) window.chrome.runtime = { id: undefined };
255
+
256
+ // Remove the CDP-injected sourceURL comment from Runtime.evaluate calls
257
+ const origToString = Function.prototype.toString;
258
+ Function.prototype.toString = function() {
259
+ if (this === Function.prototype.toString) return 'function toString() { [native code] }';
260
+ return origToString.call(this);
261
+ };
262
+
263
+ // Patch permissions query to report "prompt" instead of "denied" for notifications
264
+ const origQuery = window.Permissions?.prototype?.query;
265
+ if (origQuery) {
266
+ window.Permissions.prototype.query = function(params) {
267
+ if (params?.name === 'notifications') {
268
+ return Promise.resolve({ state: 'prompt', onchange: null });
269
+ }
270
+ return origQuery.call(this, params);
271
+ };
272
+ }
273
+ `;
274
+ /** Verify the page is ready, apply stealth, and optionally set a custom user-agent. */
228
275
  async function initTarget(targetWsUrl, userAgent) {
229
276
  const conn = await CdpConnection.connect(targetWsUrl);
230
277
  try {
231
278
  const enables = [conn.send("Page.enable"), conn.send("Runtime.enable")];
232
- if (userAgent) enables.push(conn.send("Network.enable"));
279
+ enables.push(conn.send("Network.enable"));
233
280
  await Promise.all(enables);
281
+ await conn.send("Page.addScriptToEvaluateOnNewDocument", { source: STEALTH_SCRIPT });
282
+ await conn.send("Runtime.evaluate", {
283
+ expression: STEALTH_SCRIPT,
284
+ returnByValue: true
285
+ });
234
286
  await conn.send("Runtime.evaluate", {
235
287
  expression: "window.location.href",
236
288
  returnByValue: true,
237
289
  awaitPromise: true
238
290
  });
239
291
  if (userAgent) await conn.send("Network.setUserAgentOverride", { userAgent });
292
+ else try {
293
+ const { userAgent: currentUA } = await conn.send("Browser.getVersion");
294
+ if (currentUA?.includes("HeadlessChrome")) {
295
+ const cleanUA = currentUA.replace(/HeadlessChrome/g, "Chrome");
296
+ await conn.send("Network.setUserAgentOverride", { userAgent: cleanUA });
297
+ }
298
+ } catch {}
240
299
  } finally {
241
300
  conn.close();
242
301
  }
@@ -732,7 +791,10 @@ var ChromeCdpBrowserController = class {
732
791
  `--remote-debugging-port=${port}`,
733
792
  `--user-data-dir=${profileDir}`,
734
793
  "--no-first-run",
735
- "--no-default-browser-check"
794
+ "--no-default-browser-check",
795
+ "--disable-blink-features=AutomationControlled",
796
+ "--disable-infobars",
797
+ "--excludeSwitches=enable-automation"
736
798
  ];
737
799
  if (attempt.headless) args.push("--headless=new");
738
800
  args.push("about:blank");
@@ -1076,6 +1138,38 @@ var ChromeCdpBrowserController = class {
1076
1138
  })).result.value ?? "";
1077
1139
  });
1078
1140
  }
1141
+ async screenshot(targetWsUrl, options) {
1142
+ return await this.withRetry(targetWsUrl, async (conn) => {
1143
+ await this.ensureEnabled(targetWsUrl);
1144
+ const format = options?.format ?? "jpeg";
1145
+ const params = {
1146
+ format,
1147
+ quality: format === "jpeg" ? options?.quality ?? 60 : void 0
1148
+ };
1149
+ if (options?.fullPage) {
1150
+ const metrics = await conn.send("Page.getLayoutMetrics");
1151
+ params.clip = {
1152
+ x: 0,
1153
+ y: 0,
1154
+ width: metrics.contentSize.width,
1155
+ height: metrics.contentSize.height,
1156
+ scale: 1
1157
+ };
1158
+ }
1159
+ return {
1160
+ data: (await conn.send("Page.captureScreenshot", params)).data,
1161
+ mimeType: format === "png" ? "image/png" : "image/jpeg"
1162
+ };
1163
+ });
1164
+ }
1165
+ async getCurrentUrl(targetWsUrl) {
1166
+ return await this.withRetry(targetWsUrl, async (conn) => {
1167
+ return (await conn.send("Runtime.evaluate", {
1168
+ expression: "window.location.href",
1169
+ returnByValue: true
1170
+ })).result.value ?? "";
1171
+ });
1172
+ }
1079
1173
  async getContent(targetWsUrl, options) {
1080
1174
  if (options.mode === "summary") return await this.getSummaryContent(targetWsUrl, options);
1081
1175
  if (options.mode === "a11y") {
@@ -1149,7 +1243,8 @@ var ChromeCdpBrowserController = class {
1149
1243
  fallbackSelectors: element.fallbackSelectors,
1150
1244
  inputType: element.inputType,
1151
1245
  placeholder: element.placeholder,
1152
- ariaLabel: element.ariaLabel
1246
+ ariaLabel: element.ariaLabel,
1247
+ currentValue: element.currentValue
1153
1248
  });
1154
1249
  if (primaryActions.length >= 8 && inputs.length >= 8) break;
1155
1250
  }
@@ -1261,11 +1356,84 @@ var ChromeCdpBrowserController = class {
1261
1356
  };
1262
1357
  });
1263
1358
 
1359
+ // Extract a readable content excerpt so the LLM understands what the page is about
1360
+ function extractContentExcerpt(root, maxChars) {
1361
+ const skipTags = new Set(['SCRIPT','STYLE','NOSCRIPT','SVG','PATH','META','LINK','BR','HR','IMG']);
1362
+ const blocks = [];
1363
+ let totalChars = 0;
1364
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
1365
+ acceptNode: function(node) {
1366
+ const parent = node.parentElement;
1367
+ if (!parent) return NodeFilter.FILTER_REJECT;
1368
+ if (skipTags.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
1369
+ const view = (parent.ownerDocument && parent.ownerDocument.defaultView) ? parent.ownerDocument.defaultView : window;
1370
+ const style = view.getComputedStyle(parent);
1371
+ if (style.display === 'none' || style.visibility === 'hidden') return NodeFilter.FILTER_REJECT;
1372
+ return NodeFilter.FILTER_ACCEPT;
1373
+ }
1374
+ });
1375
+ let node;
1376
+ while ((node = walker.nextNode())) {
1377
+ const text = node.textContent.replace(/\\s+/g, ' ').trim();
1378
+ if (!text || text.length < 2) continue;
1379
+ if (totalChars + text.length > maxChars) {
1380
+ const remaining = maxChars - totalChars;
1381
+ if (remaining > 20) blocks.push(text.slice(0, remaining));
1382
+ break;
1383
+ }
1384
+ blocks.push(text);
1385
+ totalChars += text.length;
1386
+ }
1387
+ return blocks.join(' ').replace(/\\s+/g, ' ').trim();
1388
+ }
1389
+
1390
+ // Group inputs by their parent form for contextual understanding
1391
+ function discoverForms(root) {
1392
+ const forms = [];
1393
+ for (const form of Array.from(root.querySelectorAll('form')).slice(0, 6)) {
1394
+ const formInputs = Array.from(form.querySelectorAll('input:not([type="hidden"]), select, textarea'));
1395
+ if (!formInputs.length) continue;
1396
+ const submitBtn = form.querySelector('button[type="submit"], input[type="submit"], button:not([type])');
1397
+ forms.push({
1398
+ action: form.getAttribute('action') || undefined,
1399
+ method: (form.getAttribute('method') || 'get').toUpperCase(),
1400
+ fields: formInputs.slice(0, 10).map(function(el) {
1401
+ const tag = el.tagName.toLowerCase();
1402
+ return {
1403
+ name: el.getAttribute('name') || undefined,
1404
+ type: tag === 'select' ? 'select' : tag === 'textarea' ? 'textarea' : (el.type || 'text'),
1405
+ label: (function() {
1406
+ const id = el.id;
1407
+ if (id) {
1408
+ const label = root.querySelector('label[for="' + CSS.escape(id) + '"]');
1409
+ if (label) return textOf(label).slice(0, 60);
1410
+ }
1411
+ const closest = el.closest('label');
1412
+ if (closest) return textOf(closest).slice(0, 60);
1413
+ const ariaLabel = el.getAttribute('aria-label');
1414
+ if (ariaLabel) return ariaLabel.slice(0, 60);
1415
+ return el.getAttribute('placeholder') || undefined;
1416
+ })(),
1417
+ required: el.required || el.getAttribute('aria-required') === 'true' || false,
1418
+ currentValue: ('value' in el && typeof el.value === 'string' && el.value) ? el.value.slice(0, 100) : undefined,
1419
+ };
1420
+ }),
1421
+ submitText: submitBtn ? textOf(submitBtn).slice(0, 40) : undefined,
1422
+ });
1423
+ }
1424
+ return forms;
1425
+ }
1426
+
1427
+ const contentExcerpt = extractContentExcerpt(scopeRoot, 1500);
1428
+ const forms = discoverForms(scopeRoot);
1429
+
1264
1430
  return {
1265
1431
  url: window.location.href,
1266
1432
  title: document.title ?? '',
1433
+ contentExcerpt,
1267
1434
  headings,
1268
1435
  landmarks,
1436
+ forms: forms.length ? forms : undefined,
1269
1437
  alerts,
1270
1438
  frames,
1271
1439
  crossOriginFrameCount: allFrames.filter((frame) => {
@@ -1697,6 +1865,9 @@ var ChromeCdpBrowserController = class {
1697
1865
  if (ariaLabel) entry.ariaLabel = ariaLabel;
1698
1866
  const placeholder = el.getAttribute('placeholder');
1699
1867
  if (placeholder) entry.placeholder = placeholder;
1868
+ if ('value' in el && typeof el.value === 'string' && el.value) {
1869
+ entry.currentValue = el.value.slice(0, 200);
1870
+ }
1700
1871
 
1701
1872
  results.push(entry);
1702
1873
  }
@@ -1978,6 +2149,15 @@ var MockBrowserController = class {
1978
2149
  if (payload.action === "dialog") return payload.text === "dismiss" ? "dismissed confirm: mock dialog" : "accepted confirm: mock dialog";
1979
2150
  return `interacted:${payload.action}`;
1980
2151
  }
2152
+ async screenshot(_targetWsUrl, options) {
2153
+ return {
2154
+ data: "bW9jaw==",
2155
+ mimeType: (options?.format ?? "jpeg") === "png" ? "image/png" : "image/jpeg"
2156
+ };
2157
+ }
2158
+ async getCurrentUrl(cdpUrl) {
2159
+ return this.pages.get(cdpUrl)?.url ?? "about:blank";
2160
+ }
1981
2161
  async getContent(cdpUrl, options) {
1982
2162
  const page = this.pages.get(cdpUrl);
1983
2163
  if (!page) throw new Error("mock page missing");
@@ -2253,6 +2433,14 @@ var SessionManager = class {
2253
2433
  const record = await this.ensureSession(sessionId);
2254
2434
  return await this.browser.getInteractiveElements(record.targetWsUrl, options);
2255
2435
  }
2436
+ async screenshot(sessionId, options) {
2437
+ const record = await this.ensureSession(sessionId);
2438
+ return await this.browser.screenshot(record.targetWsUrl, options);
2439
+ }
2440
+ async getCurrentUrl(sessionId) {
2441
+ const record = await this.ensureSession(sessionId);
2442
+ return await this.browser.getCurrentUrl(record.targetWsUrl);
2443
+ }
2256
2444
  async dismissCookieBanner(sessionId) {
2257
2445
  const record = await this.ensureSession(sessionId);
2258
2446
  const result = await this.browser.dismissCookieBanner(record.targetWsUrl);
@@ -2528,6 +2716,12 @@ var ControlApi = class {
2528
2716
  async dismissCookieBanner(sessionId) {
2529
2717
  return await this.sessions.dismissCookieBanner(sessionId);
2530
2718
  }
2719
+ async screenshot(sessionId, options) {
2720
+ return await this.sessions.screenshot(sessionId, options);
2721
+ }
2722
+ async getCurrentUrl(sessionId) {
2723
+ return await this.sessions.getCurrentUrl(sessionId);
2724
+ }
2531
2725
  listEvents(sessionId, limit = 100) {
2532
2726
  return { events: this.eventStore.list(sessionId, limit) };
2533
2727
  }
@@ -3566,8 +3760,9 @@ var AgenticBrowserCore = class {
3566
3760
  return this.api.getSession(sessionId);
3567
3761
  }
3568
3762
  async runCommand(input) {
3763
+ const commandId = input.commandId ?? `${input.type}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3569
3764
  return await this.api.executeCommand(input.sessionId, {
3570
- commandId: input.commandId,
3765
+ commandId,
3571
3766
  type: input.type,
3572
3767
  payload: input.payload
3573
3768
  });
@@ -3590,6 +3785,12 @@ var AgenticBrowserCore = class {
3590
3785
  async dismissCookieBanner(sessionId) {
3591
3786
  return await this.api.dismissCookieBanner(sessionId);
3592
3787
  }
3788
+ async screenshot(sessionId, options) {
3789
+ return await this.api.screenshot(sessionId, options);
3790
+ }
3791
+ async getCurrentUrl(sessionId) {
3792
+ return await this.api.getCurrentUrl(sessionId);
3793
+ }
3593
3794
  async restartSession(sessionId) {
3594
3795
  return await this.api.restartSession(sessionId);
3595
3796
  }
package/package.json CHANGED
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "name": "agentic-browser",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
+ "description": "CLI and MCP server for AI agents to control Chrome via CDP",
5
+ "keywords": [
6
+ "agent",
7
+ "ai",
8
+ "automation",
9
+ "browser",
10
+ "cdp",
11
+ "chrome",
12
+ "devtools-protocol",
13
+ "llm",
14
+ "mcp"
15
+ ],
16
+ "homepage": "https://ph1p.github.io/agentic-browser",
4
17
  "license": "MIT",
5
18
  "repository": {
6
19
  "type": "git",
@@ -17,10 +30,20 @@
17
30
  ],
18
31
  "type": "module",
19
32
  "main": "dist/index.mjs",
33
+ "types": "dist/index.d.mts",
20
34
  "exports": {
21
- ".": "./dist/index.mjs",
22
- "./cli": "./dist/cli/index.mjs",
23
- "./mcp": "./dist/mcp/index.mjs"
35
+ ".": {
36
+ "types": "./dist/index.d.mts",
37
+ "default": "./dist/index.mjs"
38
+ },
39
+ "./cli": {
40
+ "types": "./dist/cli/index.d.mts",
41
+ "default": "./dist/cli/index.mjs"
42
+ },
43
+ "./mcp": {
44
+ "types": "./dist/mcp/index.d.mts",
45
+ "default": "./dist/mcp/index.mjs"
46
+ }
24
47
  },
25
48
  "publishConfig": {
26
49
  "access": "public",
@@ -65,5 +88,8 @@
65
88
  "vitest": "^4.0.18",
66
89
  "vocs": "1.4.1"
67
90
  },
91
+ "engines": {
92
+ "node": ">=20"
93
+ },
68
94
  "packageManager": "pnpm@10.32.0"
69
95
  }