agentic-browser 2.1.0 → 2.2.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
@@ -113,7 +113,7 @@ agentic-browser agent start --user-agent "MyBot/1.0"
113
113
  agentic-browser agent status
114
114
  agentic-browser agent navigate https://example.com
115
115
  agentic-browser agent click "#login"
116
- agentic-browser agent content --mode text
116
+ agentic-browser agent content --mode summary
117
117
  agentic-browser agent content --mode html --selector main
118
118
  agentic-browser agent elements
119
119
  agentic-browser agent elements --roles button,link --limit 20
@@ -156,6 +156,8 @@ MCP responses are compact — `visible`, `actions`, and `tagName` are omitted to
156
156
 
157
157
  When an element lives inside an open shadow root or a same-origin iframe, discovery returns a composed locator using `>>>` to cross boundaries, for example `iframe[name="checkout"] >>> button[aria-label="Pay"]`. The same locator string works with `agent click`, `agent type`, MCP `browser_interact`, and fallback selectors.
158
158
 
159
+ `agent content` and MCP `browser_get_content` now default to `summary`, which returns a low-token overview with `title`, `headings`, `landmarks`, `primaryActions`, `inputs`, `alerts`, and iframe metadata. Use `a11y` when you need the deeper accessibility structure, and `text` or `html` only when you need raw content.
160
+
159
161
  ````
160
162
 
161
163
  ## MCP Server
@@ -225,8 +227,8 @@ More `interact` actions:
225
227
  ### 4. Read Page Content
226
228
 
227
229
  ```bash
230
+ agentic-browser page:content <sessionId> --mode summary
228
231
  agentic-browser page:content <sessionId> --mode title
229
- agentic-browser page:content <sessionId> --mode text
230
232
  agentic-browser page:content <sessionId> --mode html --selector main
231
233
  ```
232
234
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as createCliRuntime } from "../runtime-CEvs2yzE.mjs";
2
+ import { r as createCliRuntime } from "../runtime-CRkUsvhV.mjs";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import crypto from "node:crypto";
@@ -151,7 +151,8 @@ async function agentContent(runtime, input) {
151
151
  action: "content",
152
152
  ...await runtime.api.getContent(sessionId, {
153
153
  mode: input.mode,
154
- selector: input.selector
154
+ selector: input.selector,
155
+ maxChars: input.maxChars
155
156
  })
156
157
  };
157
158
  }
@@ -249,7 +250,8 @@ async function runCommand(runtime, input) {
249
250
  async function runPageContent(runtime, input) {
250
251
  return await runtime.api.getContent(input.sessionId, {
251
252
  mode: input.mode,
252
- selector: input.selector
253
+ selector: input.selector,
254
+ maxChars: input.maxChars
253
255
  });
254
256
  }
255
257
  //#endregion
@@ -322,11 +324,12 @@ async function main() {
322
324
  });
323
325
  console.log(JSON.stringify(result));
324
326
  });
325
- program.command("page:content").argument("<sessionId>").option("--mode <mode>", "title|text|html|a11y", "text").option("--selector <selector>", "optional CSS selector").action(async (sessionId, options) => {
327
+ program.command("page:content").argument("<sessionId>").option("--mode <mode>", "title|text|html|a11y|summary", "summary").option("--selector <selector>", "optional CSS selector").option("--max-chars <n>", "max characters for text/html/a11y modes").action(async (sessionId, options) => {
326
328
  const result = await runPageContent(runtime, {
327
329
  sessionId,
328
330
  mode: options.mode,
329
- selector: options.selector
331
+ selector: options.selector,
332
+ maxChars: options.maxChars ? Number.parseInt(options.maxChars, 10) : void 0
330
333
  });
331
334
  console.log(JSON.stringify(result));
332
335
  });
@@ -470,8 +473,12 @@ async function main() {
470
473
  const result = await agentTerminate(runtime);
471
474
  console.log(JSON.stringify(result));
472
475
  });
473
- agent.command("content").option("--mode <mode>", "title|text|html|a11y", "text").option("--selector <selector>", "optional CSS selector").action(async (options) => {
474
- const result = await agentContent(runtime, options);
476
+ agent.command("content").option("--mode <mode>", "title|text|html|a11y|summary", "summary").option("--selector <selector>", "optional CSS selector").option("--max-chars <n>", "max characters for text/html/a11y modes").action(async (options) => {
477
+ const result = await agentContent(runtime, {
478
+ mode: options.mode,
479
+ selector: options.selector,
480
+ maxChars: options.maxChars ? Number.parseInt(options.maxChars, 10) : void 0
481
+ });
475
482
  console.log(JSON.stringify(result));
476
483
  });
477
484
  agent.command("elements").description("List interactive elements on the current page").option("--roles <roles>", "comma-separated roles filter", (v) => v.split(",")).option("--visible-only", "only visible elements", true).option("--no-visible-only", "include hidden elements").option("--limit <n>", "max elements", "50").option("--selector <selector>", "scope to CSS selector subtree").action(async (options) => {
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-CEvs2yzE.mjs";
2
+ import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-CRkUsvhV.mjs";
3
3
  export { AgenticBrowserCore, createAgenticBrowserCore, createMockAgenticBrowserCore };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as createAgenticBrowserCore } from "../runtime-CEvs2yzE.mjs";
2
+ import { n as createAgenticBrowserCore } from "../runtime-CRkUsvhV.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";
@@ -22,10 +22,27 @@ function truncateContent(content, limit) {
22
22
  };
23
23
  const originalLength = content.length;
24
24
  return {
25
- content: content.slice(0, limit) + `\n\n[Truncated — showing first ${limit} of ${originalLength} characters. Use a CSS selector to scope the content.]`,
25
+ content: content.slice(0, limit) + `\n\n[Truncated - showing first ${limit} of ${originalLength} characters. Use a CSS selector to scope the content, or use mode="summary" for a lower-token overview.]`,
26
26
  truncated: true
27
27
  };
28
28
  }
29
+ function resolveCompactedContent(result, limit) {
30
+ if (result.truncated) return {
31
+ content: result.content,
32
+ truncated: true
33
+ };
34
+ return truncateContent(result.content, limit);
35
+ }
36
+ function parseStructuredSummary(result) {
37
+ if (result.structuredContent) return result.structuredContent;
38
+ if (!result.content) return;
39
+ try {
40
+ const parsed = JSON.parse(result.content);
41
+ return parsed && typeof parsed === "object" ? parsed : void 0;
42
+ } catch {
43
+ return;
44
+ }
45
+ }
29
46
  function summarizeTextContent(content) {
30
47
  const lines = uniqueLines(content);
31
48
  return {
@@ -68,9 +85,23 @@ function compactPageContent(result, maxChars) {
68
85
  text: 8e3,
69
86
  a11y: 1e4,
70
87
  html: 4e3,
71
- title: void 0
88
+ title: void 0,
89
+ summary: void 0
72
90
  }[result.mode];
73
- const truncated = truncateContent(result.content, limit);
91
+ if (result.mode === "summary") {
92
+ const structured = parseStructuredSummary(result);
93
+ if (structured) return {
94
+ mode: "summary",
95
+ ...structured,
96
+ truncated: false
97
+ };
98
+ return {
99
+ mode: "summary",
100
+ content: result.content,
101
+ truncated: false
102
+ };
103
+ }
104
+ const truncated = resolveCompactedContent(result, limit);
74
105
  if (result.mode === "title") return {
75
106
  ...result,
76
107
  truncated: false
@@ -81,6 +112,7 @@ function compactPageContent(result, maxChars) {
81
112
  mode: result.mode,
82
113
  content: truncated.content,
83
114
  truncated: truncated.truncated,
115
+ originalLength: result.originalLength,
84
116
  summaryLines: summary.summaryLines,
85
117
  previewLines: summary.previewLines
86
118
  };
@@ -91,6 +123,7 @@ function compactPageContent(result, maxChars) {
91
123
  mode: result.mode,
92
124
  content: truncated.content,
93
125
  truncated: truncated.truncated,
126
+ originalLength: result.originalLength,
94
127
  headings: summary.headings,
95
128
  landmarks: summary.landmarks,
96
129
  actions: summary.actions,
@@ -101,7 +134,8 @@ function compactPageContent(result, maxChars) {
101
134
  return {
102
135
  mode: result.mode,
103
136
  content: truncated.content,
104
- truncated: truncated.truncated
137
+ truncated: truncated.truncated,
138
+ originalLength: result.originalLength
105
139
  };
106
140
  }
107
141
  function summarizeElements(elements) {
@@ -159,6 +193,13 @@ function compactMemoryResults(results) {
159
193
  //#region src/mcp/index.ts
160
194
  let core;
161
195
  let activeSessionId;
196
+ const DEFAULT_MCP_MAX_CHARS = {
197
+ summary: void 0,
198
+ title: void 0,
199
+ text: 8e3,
200
+ a11y: 1e4,
201
+ html: 4e3
202
+ };
162
203
  function getCore() {
163
204
  if (!core) core = createAgenticBrowserCore();
164
205
  return core;
@@ -265,26 +306,29 @@ server.tool("browser_interact", "Interact with a page element or perform browser
265
306
  text: JSON.stringify(compact)
266
307
  }] };
267
308
  });
268
- 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.", {
309
+ server.tool("browser_get_content", "Get the current page content. Modes: \"summary\" (recommended low-token overview with headings, actions, inputs, alerts, and iframe awareness), \"text\" (readable text), \"a11y\" (accessibility tree for deeper structure), \"title\" (page title only), \"html\" (raw HTML). A session is auto-started if needed.", {
269
310
  mode: z.enum([
270
311
  "title",
271
312
  "text",
272
313
  "html",
273
- "a11y"
274
- ]).default("text").describe("Content extraction mode"),
314
+ "a11y",
315
+ "summary"
316
+ ]).default("summary").describe("Content extraction mode"),
275
317
  selector: z.string().optional().describe("CSS selector to scope content (e.g. \"main\", \"#content\")"),
276
- maxChars: z.number().optional().describe("Maximum characters to return (default: 12000 for text/a11y, 6000 for html, no cap for title). Use a CSS selector to scope content instead of raising this limit."),
318
+ maxChars: z.number().optional().describe("Maximum characters to return (default: 8000 for text, 10000 for a11y, 4000 for html, ignored for summary/title). Use a CSS selector to scope content instead of raising this limit."),
277
319
  sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
278
320
  }, async ({ mode, selector, maxChars, sessionId }) => {
279
321
  const sid = await resolveSession(sessionId);
322
+ const effectiveMaxChars = maxChars ?? DEFAULT_MCP_MAX_CHARS[mode];
280
323
  const result = await getCore().getPageContent({
281
324
  sessionId: sid,
282
325
  mode,
283
- selector
326
+ selector,
327
+ maxChars: effectiveMaxChars
284
328
  });
285
329
  return { content: [{
286
330
  type: "text",
287
- text: JSON.stringify(compactPageContent(result, maxChars))
331
+ text: JSON.stringify(compactPageContent(result))
288
332
  }] };
289
333
  });
290
334
  server.tool("browser_get_elements", "Discover all interactive elements on the current page (buttons, links, inputs, etc.). Returns CSS selectors and fallbackSelectors you can use with browser_interact. Pass fallbackSelectors to browser_interact for automatic retry when the primary selector breaks. Actions are derived from role: link/button/custom→click, input/textarea/contenteditable→click+type+press, select→click+select, checkbox/radio→toggle. A session is auto-started if needed.", {
@@ -5,8 +5,8 @@ import net from "node:net";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import WebSocket, { WebSocketServer } from "ws";
8
- import { URL as URL$1, fileURLToPath } from "node:url";
9
8
  import crypto from "node:crypto";
9
+ import { URL as URL$1 } from "node:url";
10
10
  import { z } from "zod";
11
11
  //#region src/session/chrome-launcher.ts
12
12
  const CANDIDATES = [
@@ -22,16 +22,6 @@ function discoverChrome(explicitPath) {
22
22
  return found;
23
23
  }
24
24
  //#endregion
25
- //#region src/session/extension-loader.ts
26
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
- function loadControlExtension() {
28
- const packageRoot = path.resolve(__dirname, "..");
29
- return {
30
- extensionPath: path.resolve(packageRoot, "extension"),
31
- loadedAt: (/* @__PURE__ */ new Date()).toISOString()
32
- };
33
- }
34
- //#endregion
35
25
  //#region src/session/browser-controller.ts
36
26
  var CdpConnection = class CdpConnection {
37
27
  nextId = 0;
@@ -274,6 +264,7 @@ function resolveDefaultProfileDir() {
274
264
  if (platform === "win32") return path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome", "User Data");
275
265
  return path.join(home, ".config", "google-chrome");
276
266
  }
267
+ const CONTENT_TRUNCATION_HINT = "Use a CSS selector to scope the content, or use mode=\"summary\" for a lower-token overview.";
277
268
  const LOCATOR_RUNTIME_HELPERS = String.raw`
278
269
  const LOCATOR_SEPARATOR = ' >>> ';
279
270
 
@@ -482,6 +473,168 @@ ${LOCATOR_RUNTIME_HELPERS}
482
473
  ${body}
483
474
  })()`;
484
475
  }
476
+ const SPECIAL_KEY_PAYLOADS = new Map([
477
+ ["Enter", {
478
+ key: "Enter",
479
+ code: "Enter",
480
+ windowsVirtualKeyCode: 13,
481
+ nativeVirtualKeyCode: 13,
482
+ text: "\r",
483
+ unmodifiedText: "\r"
484
+ }],
485
+ ["Tab", {
486
+ key: "Tab",
487
+ code: "Tab",
488
+ windowsVirtualKeyCode: 9,
489
+ nativeVirtualKeyCode: 9
490
+ }],
491
+ ["Escape", {
492
+ key: "Escape",
493
+ code: "Escape",
494
+ windowsVirtualKeyCode: 27,
495
+ nativeVirtualKeyCode: 27
496
+ }],
497
+ ["Backspace", {
498
+ key: "Backspace",
499
+ code: "Backspace",
500
+ windowsVirtualKeyCode: 8,
501
+ nativeVirtualKeyCode: 8
502
+ }],
503
+ ["Delete", {
504
+ key: "Delete",
505
+ code: "Delete",
506
+ windowsVirtualKeyCode: 46,
507
+ nativeVirtualKeyCode: 46
508
+ }],
509
+ ["Space", {
510
+ key: " ",
511
+ code: "Space",
512
+ windowsVirtualKeyCode: 32,
513
+ nativeVirtualKeyCode: 32,
514
+ text: " ",
515
+ unmodifiedText: " "
516
+ }],
517
+ [" ", {
518
+ key: " ",
519
+ code: "Space",
520
+ windowsVirtualKeyCode: 32,
521
+ nativeVirtualKeyCode: 32,
522
+ text: " ",
523
+ unmodifiedText: " "
524
+ }],
525
+ ["ArrowUp", {
526
+ key: "ArrowUp",
527
+ code: "ArrowUp",
528
+ windowsVirtualKeyCode: 38,
529
+ nativeVirtualKeyCode: 38
530
+ }],
531
+ ["ArrowDown", {
532
+ key: "ArrowDown",
533
+ code: "ArrowDown",
534
+ windowsVirtualKeyCode: 40,
535
+ nativeVirtualKeyCode: 40
536
+ }],
537
+ ["ArrowLeft", {
538
+ key: "ArrowLeft",
539
+ code: "ArrowLeft",
540
+ windowsVirtualKeyCode: 37,
541
+ nativeVirtualKeyCode: 37
542
+ }],
543
+ ["ArrowRight", {
544
+ key: "ArrowRight",
545
+ code: "ArrowRight",
546
+ windowsVirtualKeyCode: 39,
547
+ nativeVirtualKeyCode: 39
548
+ }],
549
+ ["Home", {
550
+ key: "Home",
551
+ code: "Home",
552
+ windowsVirtualKeyCode: 36,
553
+ nativeVirtualKeyCode: 36
554
+ }],
555
+ ["End", {
556
+ key: "End",
557
+ code: "End",
558
+ windowsVirtualKeyCode: 35,
559
+ nativeVirtualKeyCode: 35
560
+ }],
561
+ ["PageUp", {
562
+ key: "PageUp",
563
+ code: "PageUp",
564
+ windowsVirtualKeyCode: 33,
565
+ nativeVirtualKeyCode: 33
566
+ }],
567
+ ["PageDown", {
568
+ key: "PageDown",
569
+ code: "PageDown",
570
+ windowsVirtualKeyCode: 34,
571
+ nativeVirtualKeyCode: 34
572
+ }]
573
+ ]);
574
+ function appendTruncationNotice(content, limit, originalLength) {
575
+ return content + `\n\n[Truncated - showing first ${limit} of ${originalLength} characters. ${CONTENT_TRUNCATION_HINT}]`;
576
+ }
577
+ function applyContentLimit(content, limit, originalLength = content.length) {
578
+ if (!limit || originalLength <= limit) return {
579
+ content,
580
+ truncated: false
581
+ };
582
+ return {
583
+ content: appendTruncationNotice(content.slice(0, limit), limit, originalLength),
584
+ truncated: true,
585
+ originalLength
586
+ };
587
+ }
588
+ function normalizeRuntimeContentPayload(value, limit) {
589
+ if (typeof value === "string") {
590
+ const limited = applyContentLimit(value, limit);
591
+ return {
592
+ content: limited.content,
593
+ truncated: limited.truncated || void 0,
594
+ originalLength: limited.originalLength
595
+ };
596
+ }
597
+ const content = value?.content ?? "";
598
+ if (value?.truncated) {
599
+ const originalLength = value.originalLength ?? content.length;
600
+ const effectiveLimit = limit && limit > 0 ? limit : content.length;
601
+ return {
602
+ content: content.includes("[Truncated - showing first") || effectiveLimit === 0 ? content : appendTruncationNotice(content, effectiveLimit, originalLength),
603
+ truncated: true,
604
+ originalLength
605
+ };
606
+ }
607
+ const limited = applyContentLimit(content, limit, value?.originalLength ?? content.length);
608
+ return {
609
+ content: limited.content,
610
+ truncated: limited.truncated || void 0,
611
+ originalLength: limited.originalLength
612
+ };
613
+ }
614
+ function buildKeyDispatchPayload(inputKey) {
615
+ const key = inputKey || "Enter";
616
+ const special = SPECIAL_KEY_PAYLOADS.get(key);
617
+ if (special) return { ...special };
618
+ if (key.length === 1) {
619
+ const upper = key.toUpperCase();
620
+ const code = key >= "a" && key <= "z" ? `Key${upper}` : key >= "A" && key <= "Z" ? `Key${key}` : key >= "0" && key <= "9" ? `Digit${key}` : "Unidentified";
621
+ const keyCode = upper.charCodeAt(0);
622
+ return {
623
+ key,
624
+ code,
625
+ windowsVirtualKeyCode: keyCode,
626
+ nativeVirtualKeyCode: keyCode,
627
+ text: key,
628
+ unmodifiedText: key
629
+ };
630
+ }
631
+ return {
632
+ key,
633
+ code: key,
634
+ windowsVirtualKeyCode: 0,
635
+ nativeVirtualKeyCode: 0
636
+ };
637
+ }
485
638
  var ChromeCdpBrowserController = class {
486
639
  connections = /* @__PURE__ */ new Map();
487
640
  cleanupInterval;
@@ -564,7 +717,6 @@ var ChromeCdpBrowserController = class {
564
717
  async launch(sessionId, options) {
565
718
  const { executablePath: explicitPath, userProfileDir, headless, userAgent } = options ?? {};
566
719
  const executablePath = discoverChrome(explicitPath);
567
- const extension = loadControlExtension();
568
720
  let profileDir;
569
721
  if (userProfileDir === "default") profileDir = resolveDefaultProfileDir();
570
722
  else if (userProfileDir) profileDir = userProfileDir;
@@ -572,23 +724,7 @@ var ChromeCdpBrowserController = class {
572
724
  fs.mkdirSync(profileDir, { recursive: true });
573
725
  const lockFile = path.join(profileDir, "SingletonLock");
574
726
  if (fs.existsSync(lockFile)) throw new Error(`Chrome profile is already in use (lock file exists: ${lockFile}). Quit the running Chrome instance first, or use --cdp-url to connect to it instead.`);
575
- const launchAttempts = headless ? [{
576
- withExtension: false,
577
- headless: true
578
- }] : [
579
- {
580
- withExtension: true,
581
- headless: false
582
- },
583
- {
584
- withExtension: false,
585
- headless: false
586
- },
587
- {
588
- withExtension: false,
589
- headless: true
590
- }
591
- ];
727
+ const launchAttempts = headless ? [{ headless: true }] : [{ headless: false }, { headless: true }];
592
728
  let lastError;
593
729
  for (const attempt of launchAttempts) {
594
730
  const port = await getFreePort();
@@ -598,7 +734,6 @@ var ChromeCdpBrowserController = class {
598
734
  "--no-first-run",
599
735
  "--no-default-browser-check"
600
736
  ];
601
- if (attempt.withExtension) args.push(`--disable-extensions-except=${extension.extensionPath}`, `--load-extension=${extension.extensionPath}`);
602
737
  if (attempt.headless) args.push("--headless=new");
603
738
  args.push("about:blank");
604
739
  const child = spawn(executablePath, args, {
@@ -782,6 +917,42 @@ var ChromeCdpBrowserController = class {
782
917
  clickCount: 1
783
918
  });
784
919
  }
920
+ async focusTarget(conn, payload) {
921
+ if (!payload.selector) return;
922
+ const expression = buildLocatorRuntimeExpression("payload", payload, `
923
+ const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
924
+ const el = resolved.element;
925
+ ensureInView(el);
926
+ if (typeof el.focus === 'function') {
927
+ el.focus();
928
+ }
929
+ return true;
930
+ `);
931
+ await conn.send("Runtime.evaluate", {
932
+ expression,
933
+ returnByValue: true,
934
+ awaitPromise: true
935
+ });
936
+ }
937
+ async dispatchKeyPress(conn, keyInput) {
938
+ const payload = buildKeyDispatchPayload(keyInput ?? "Enter");
939
+ await conn.send("Input.dispatchKeyEvent", {
940
+ type: payload.text ? "keyDown" : "rawKeyDown",
941
+ key: payload.key,
942
+ code: payload.code,
943
+ windowsVirtualKeyCode: payload.windowsVirtualKeyCode,
944
+ nativeVirtualKeyCode: payload.nativeVirtualKeyCode,
945
+ text: payload.text,
946
+ unmodifiedText: payload.unmodifiedText
947
+ });
948
+ await conn.send("Input.dispatchKeyEvent", {
949
+ type: "keyUp",
950
+ key: payload.key,
951
+ code: payload.code,
952
+ windowsVirtualKeyCode: payload.windowsVirtualKeyCode,
953
+ nativeVirtualKeyCode: payload.nativeVirtualKeyCode
954
+ });
955
+ }
785
956
  async interact(targetWsUrl, payload) {
786
957
  if (payload.action === "goBack") return await this.withRetry(targetWsUrl, async (conn) => {
787
958
  await this.ensureEnabled(targetWsUrl);
@@ -820,7 +991,7 @@ var ChromeCdpBrowserController = class {
820
991
  return "page refreshed";
821
992
  });
822
993
  if (payload.action === "dialog") return await this.handleDialogAction(targetWsUrl, payload);
823
- if (payload.action === "click" || payload.action === "hover" || payload.action === "type") return await this.withRetry(targetWsUrl, async (conn) => {
994
+ if (payload.action === "click" || payload.action === "hover" || payload.action === "type" || payload.action === "press") return await this.withRetry(targetWsUrl, async (conn) => {
824
995
  await this.ensureEnabled(targetWsUrl);
825
996
  if (payload.action === "click") {
826
997
  const point = await this.preparePointerTarget(conn, payload);
@@ -843,18 +1014,15 @@ var ChromeCdpBrowserController = class {
843
1014
  });
844
1015
  return "hovered";
845
1016
  }
1017
+ if (payload.action === "press") {
1018
+ await this.focusTarget(conn, payload);
1019
+ await this.dispatchKeyPress(conn, payload.key);
1020
+ return "pressed";
1021
+ }
846
1022
  if (await this.prepareTypeTarget(conn, payload) === "ready") await conn.send("Input.insertText", { text: payload.text ?? "" });
847
1023
  return "typed";
848
1024
  });
849
1025
  const expression = buildLocatorRuntimeExpression("payload", payload, `
850
- if (payload.action === 'press') {
851
- const target = deepActiveElement();
852
- if (!target) throw new Error('No active element to press key on');
853
- const key = payload.key ?? 'Enter';
854
- target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
855
- target.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));
856
- return 'pressed';
857
- }
858
1026
  if (payload.action === 'waitFor') {
859
1027
  const timeout = payload.timeoutMs ?? 2000;
860
1028
  const started = Date.now();
@@ -877,10 +1045,10 @@ var ChromeCdpBrowserController = class {
877
1045
  if (payload.action === 'scroll') {
878
1046
  if (payload.selector) {
879
1047
  const el = resolveWithFallbacks(payload.selector, payload.fallbackSelectors).element;
880
- el.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0, behavior: 'smooth' });
1048
+ el.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0 });
881
1049
  return 'scrolled element';
882
1050
  }
883
- window.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0, behavior: 'smooth' });
1051
+ window.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0 });
884
1052
  return 'scrolled page';
885
1053
  }
886
1054
  if (payload.action === 'select') {
@@ -909,37 +1077,234 @@ var ChromeCdpBrowserController = class {
909
1077
  });
910
1078
  }
911
1079
  async getContent(targetWsUrl, options) {
912
- if (options.mode === "a11y") return {
913
- mode: "a11y",
914
- content: await this.getAccessibilityTree(targetWsUrl, options.selector)
915
- };
1080
+ if (options.mode === "summary") return await this.getSummaryContent(targetWsUrl, options);
1081
+ if (options.mode === "a11y") {
1082
+ const limited = normalizeRuntimeContentPayload(await this.getAccessibilityTree(targetWsUrl, options.selector), options.maxChars);
1083
+ return {
1084
+ mode: "a11y",
1085
+ content: limited.content,
1086
+ truncated: limited.truncated,
1087
+ originalLength: limited.originalLength
1088
+ };
1089
+ }
916
1090
  const expression = buildLocatorRuntimeExpression("options", options, `
917
- if (options.mode === 'title') return document.title ?? '';
1091
+ const limit =
1092
+ typeof options.maxChars === 'number' && Number.isFinite(options.maxChars) && options.maxChars > 0
1093
+ ? Math.floor(options.maxChars)
1094
+ : null;
1095
+
1096
+ function serializeContent(content) {
1097
+ const normalized = typeof content === 'string' ? content : String(content ?? '');
1098
+ const originalLength = normalized.length;
1099
+ if (limit !== null && originalLength > limit) {
1100
+ return { content: normalized.slice(0, limit), truncated: true, originalLength };
1101
+ }
1102
+ return { content: normalized, truncated: false, originalLength };
1103
+ }
1104
+
1105
+ if (options.mode === 'title') return serializeContent(document.title ?? '');
918
1106
  if (options.mode === 'html') {
919
1107
  if (options.selector) {
920
1108
  const el = resolveLocator(options.selector);
921
- return el ? el.outerHTML : '';
1109
+ return serializeContent(el ? el.outerHTML : '');
922
1110
  }
923
- return document.documentElement?.outerHTML ?? '';
1111
+ return serializeContent(document.documentElement?.outerHTML ?? '');
924
1112
  }
925
1113
  if (options.selector) {
926
1114
  const el = resolveLocator(options.selector);
927
- return el ? el.innerText ?? '' : '';
1115
+ return serializeContent(el ? el.innerText ?? '' : '');
928
1116
  }
929
- return document.body?.innerText ?? '';
1117
+ return serializeContent(document.body?.innerText ?? '');
930
1118
  `);
931
1119
  return await this.withRetry(targetWsUrl, async (conn) => {
932
1120
  await this.ensureEnabled(targetWsUrl);
933
- const content = (await conn.send("Runtime.evaluate", {
1121
+ const limited = normalizeRuntimeContentPayload((await conn.send("Runtime.evaluate", {
934
1122
  expression,
935
- returnByValue: true
936
- })).result.value ?? "";
1123
+ returnByValue: true,
1124
+ awaitPromise: true
1125
+ })).result.value, options.maxChars);
937
1126
  return {
938
1127
  mode: options.mode,
939
- content
1128
+ content: limited.content,
1129
+ truncated: limited.truncated,
1130
+ originalLength: limited.originalLength
940
1131
  };
941
1132
  });
942
1133
  }
1134
+ summarizeInteractiveElements(result) {
1135
+ const primaryActions = [];
1136
+ const inputs = [];
1137
+ for (const element of result.elements) {
1138
+ if (primaryActions.length < 8 && (element.role === "button" || element.role === "link" || element.role === "custom")) primaryActions.push({
1139
+ role: element.role,
1140
+ text: element.text,
1141
+ selector: element.selector,
1142
+ fallbackSelectors: element.fallbackSelectors,
1143
+ href: element.href
1144
+ });
1145
+ if (inputs.length < 8 && (element.role === "input" || element.role === "textarea" || element.role === "select" || element.role === "checkbox" || element.role === "radio" || element.role === "contenteditable")) inputs.push({
1146
+ role: element.role,
1147
+ text: element.text,
1148
+ selector: element.selector,
1149
+ fallbackSelectors: element.fallbackSelectors,
1150
+ inputType: element.inputType,
1151
+ placeholder: element.placeholder,
1152
+ ariaLabel: element.ariaLabel
1153
+ });
1154
+ if (primaryActions.length >= 8 && inputs.length >= 8) break;
1155
+ }
1156
+ return {
1157
+ primaryActions,
1158
+ inputs
1159
+ };
1160
+ }
1161
+ async getSummaryContent(targetWsUrl, options) {
1162
+ const expression = buildLocatorRuntimeExpression("options", options, `
1163
+ const scopeSelector = options.selector;
1164
+ let scopeRoot = document.body ?? document.documentElement;
1165
+ if (scopeSelector) {
1166
+ try {
1167
+ scopeRoot = resolveLocator(scopeSelector) ?? scopeRoot;
1168
+ } catch {
1169
+ scopeRoot = scopeRoot ?? document.documentElement;
1170
+ }
1171
+ }
1172
+
1173
+ function escapeAttr(value) {
1174
+ return String(value ?? '').replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\\\"');
1175
+ }
1176
+
1177
+ function uniqueValues(values, limit) {
1178
+ const seen = new Set();
1179
+ const results = [];
1180
+ for (const value of values) {
1181
+ const normalized = String(value ?? '').replace(/\\s+/g, ' ').trim();
1182
+ if (!normalized || seen.has(normalized)) continue;
1183
+ seen.add(normalized);
1184
+ results.push(normalized.slice(0, 160));
1185
+ if (results.length >= limit) break;
1186
+ }
1187
+ return results;
1188
+ }
1189
+
1190
+ function textOf(el) {
1191
+ if (!el) return '';
1192
+ const ariaLabel = el.getAttribute && el.getAttribute('aria-label');
1193
+ if (ariaLabel) return ariaLabel;
1194
+ const text = el.innerText || el.textContent || '';
1195
+ return text.replace(/\\s+/g, ' ').trim();
1196
+ }
1197
+
1198
+ function frameSelector(frame) {
1199
+ if (frame.id) return '#' + CSS.escape(frame.id);
1200
+ const name = frame.getAttribute('name');
1201
+ if (name) return 'iframe[name="' + escapeAttr(name) + '"]';
1202
+ const title = frame.getAttribute('title');
1203
+ if (title) return 'iframe[title="' + escapeAttr(title) + '"]';
1204
+ const src = frame.getAttribute('src');
1205
+ if (src) return 'iframe[src="' + escapeAttr(src) + '"]';
1206
+ const parent = frame.parentElement;
1207
+ if (!parent) return 'iframe';
1208
+ const siblings = Array.from(parent.children).filter((candidate) => candidate.tagName === frame.tagName);
1209
+ if (siblings.length === 1) return 'iframe';
1210
+ return 'iframe:nth-of-type(' + (siblings.indexOf(frame) + 1) + ')';
1211
+ }
1212
+
1213
+ const headingSelectors = 'h1,h2,h3,h4,h5,h6,[role="heading"]';
1214
+ const landmarkSelectors = [
1215
+ 'main',
1216
+ 'nav',
1217
+ 'aside',
1218
+ 'header',
1219
+ 'footer',
1220
+ '[role="main"]',
1221
+ '[role="navigation"]',
1222
+ '[role="banner"]',
1223
+ '[role="complementary"]',
1224
+ '[role="contentinfo"]',
1225
+ '[role="search"]'
1226
+ ].join(',');
1227
+ const alertSelectors = '[role="alert"], [aria-live="assertive"], dialog[open], [aria-modal="true"]';
1228
+
1229
+ const headings = uniqueValues(
1230
+ Array.from(scopeRoot.querySelectorAll(headingSelectors)).map((el) => textOf(el)),
1231
+ 8
1232
+ );
1233
+ const landmarks = uniqueValues(
1234
+ Array.from(scopeRoot.querySelectorAll(landmarkSelectors)).map((el) => {
1235
+ const role = el.getAttribute('role') || el.tagName.toLowerCase();
1236
+ const label = textOf(el);
1237
+ return label ? role + ' "' + label.slice(0, 80) + '"' : role;
1238
+ }),
1239
+ 8
1240
+ );
1241
+ const alerts = uniqueValues(
1242
+ Array.from(scopeRoot.querySelectorAll(alertSelectors)).map((el) => textOf(el)),
1243
+ 6
1244
+ );
1245
+
1246
+ const allFrames = Array.from(scopeRoot.querySelectorAll('iframe'));
1247
+ const frames = allFrames.slice(0, 8).map((frame) => {
1248
+ let sameOrigin = false;
1249
+ try {
1250
+ sameOrigin = !!frame.contentDocument;
1251
+ } catch {
1252
+ sameOrigin = false;
1253
+ }
1254
+
1255
+ return {
1256
+ selector: frameSelector(frame),
1257
+ name: frame.getAttribute('name') || undefined,
1258
+ title: frame.getAttribute('title') || undefined,
1259
+ src: frame.getAttribute('src') || frame.src || undefined,
1260
+ sameOrigin,
1261
+ };
1262
+ });
1263
+
1264
+ return {
1265
+ url: window.location.href,
1266
+ title: document.title ?? '',
1267
+ headings,
1268
+ landmarks,
1269
+ alerts,
1270
+ frames,
1271
+ crossOriginFrameCount: allFrames.filter((frame) => {
1272
+ try {
1273
+ return !frame.contentDocument;
1274
+ } catch {
1275
+ return true;
1276
+ }
1277
+ }).length,
1278
+ hasMoreFrames: allFrames.length > frames.length,
1279
+ scopedToSelector: scopeSelector || undefined,
1280
+ };
1281
+ `);
1282
+ const pageSummary = await this.withRetry(targetWsUrl, async (conn) => {
1283
+ await this.ensureEnabled(targetWsUrl);
1284
+ return (await conn.send("Runtime.evaluate", {
1285
+ expression,
1286
+ returnByValue: true,
1287
+ awaitPromise: true
1288
+ })).result.value ?? {};
1289
+ });
1290
+ const elements = await this.getInteractiveElements(targetWsUrl, {
1291
+ selector: options.selector,
1292
+ visibleOnly: true,
1293
+ limit: 24
1294
+ });
1295
+ const elementSummary = this.summarizeInteractiveElements(elements);
1296
+ return {
1297
+ mode: "summary",
1298
+ content: "",
1299
+ structuredContent: {
1300
+ mode: "summary",
1301
+ ...pageSummary,
1302
+ ...elementSummary,
1303
+ totalInteractiveElements: elements.totalFound,
1304
+ truncatedInteractiveElements: elements.truncated
1305
+ }
1306
+ };
1307
+ }
943
1308
  formatAccessibilityTree(nodes) {
944
1309
  const childrenMap = /* @__PURE__ */ new Map();
945
1310
  const nodeMap = /* @__PURE__ */ new Map();
@@ -1616,17 +1981,29 @@ var MockBrowserController = class {
1616
1981
  async getContent(cdpUrl, options) {
1617
1982
  const page = this.pages.get(cdpUrl);
1618
1983
  if (!page) throw new Error("mock page missing");
1619
- if (options.mode === "title") return {
1620
- mode: "title",
1621
- content: page.title
1622
- };
1623
- if (options.mode === "html") return {
1624
- mode: "html",
1625
- content: page.html
1984
+ if (options.mode === "summary") return {
1985
+ mode: "summary",
1986
+ content: "",
1987
+ structuredContent: {
1988
+ mode: "summary",
1989
+ url: page.url,
1990
+ title: page.title,
1991
+ headings: [page.title],
1992
+ landmarks: [],
1993
+ alerts: [],
1994
+ frames: [],
1995
+ primaryActions: [],
1996
+ inputs: [],
1997
+ totalInteractiveElements: 0,
1998
+ truncatedInteractiveElements: false
1999
+ }
1626
2000
  };
2001
+ const limited = applyContentLimit(options.mode === "title" ? page.title : options.mode === "html" ? page.html : page.text, options.maxChars);
1627
2002
  return {
1628
- mode: "text",
1629
- content: page.text
2003
+ mode: options.mode,
2004
+ content: limited.content,
2005
+ truncated: limited.truncated || void 0,
2006
+ originalLength: limited.originalLength
1630
2007
  };
1631
2008
  }
1632
2009
  async getInteractiveElements(_targetWsUrl, _options) {
@@ -2217,16 +2594,26 @@ const MAX_EVENTS_PER_SESSION = 500;
2217
2594
  var EventStore = class {
2218
2595
  events = /* @__PURE__ */ new Map();
2219
2596
  filePath;
2597
+ bufferedLines = [];
2598
+ flushTimer;
2599
+ flushInFlight;
2220
2600
  constructor(baseDir) {
2221
2601
  fs.mkdirSync(baseDir, { recursive: true });
2222
2602
  this.filePath = path.join(baseDir, "session-events.log");
2603
+ process.once("beforeExit", this.flushSync);
2604
+ process.once("exit", this.flushSync);
2223
2605
  }
2224
2606
  append(event) {
2225
2607
  let existing = this.events.get(event.sessionId) ?? [];
2226
2608
  existing.push(event);
2227
2609
  if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-MAX_EVENTS_PER_SESSION);
2228
2610
  this.events.set(event.sessionId, existing);
2229
- fs.appendFileSync(this.filePath, `${JSON.stringify(event)}\n`, "utf8");
2611
+ this.bufferedLines.push(`${JSON.stringify(event)}\n`);
2612
+ if (this.bufferedLines.length >= 20) {
2613
+ this.flushSoon(0);
2614
+ return;
2615
+ }
2616
+ this.flushSoon(25);
2230
2617
  }
2231
2618
  list(sessionId, limit = 100) {
2232
2619
  const entries = this.events.get(sessionId) ?? [];
@@ -2235,6 +2622,36 @@ var EventStore = class {
2235
2622
  clear(sessionId) {
2236
2623
  this.events.delete(sessionId);
2237
2624
  }
2625
+ flushSoon(delayMs) {
2626
+ if (this.flushInFlight) return this.flushInFlight;
2627
+ if (this.flushTimer) return Promise.resolve();
2628
+ this.flushTimer = setTimeout(() => {
2629
+ this.flushTimer = void 0;
2630
+ this.flushAsync();
2631
+ }, delayMs);
2632
+ this.flushTimer.unref();
2633
+ return Promise.resolve();
2634
+ }
2635
+ async flushAsync() {
2636
+ if (this.flushInFlight || this.bufferedLines.length === 0) return;
2637
+ const chunk = this.bufferedLines.splice(0, this.bufferedLines.length).join("");
2638
+ this.flushInFlight = fs.promises.appendFile(this.filePath, chunk, "utf8").catch(() => {}).finally(() => {
2639
+ this.flushInFlight = void 0;
2640
+ if (this.bufferedLines.length > 0) this.flushSoon(0);
2641
+ });
2642
+ await this.flushInFlight;
2643
+ }
2644
+ flushSync = () => {
2645
+ if (this.flushTimer) {
2646
+ clearTimeout(this.flushTimer);
2647
+ this.flushTimer = void 0;
2648
+ }
2649
+ if (this.bufferedLines.length === 0) return;
2650
+ const chunk = this.bufferedLines.splice(0, this.bufferedLines.length).join("");
2651
+ try {
2652
+ fs.appendFileSync(this.filePath, chunk, "utf8");
2653
+ } catch {}
2654
+ };
2238
2655
  };
2239
2656
  //#endregion
2240
2657
  //#region src/auth/session-token.ts
@@ -2876,7 +3293,8 @@ var AgenticBrowserCore = class {
2876
3293
  async getPageContent(input) {
2877
3294
  return await this.api.getContent(input.sessionId, {
2878
3295
  mode: input.mode,
2879
- selector: input.selector
3296
+ selector: input.selector,
3297
+ maxChars: input.maxChars
2880
3298
  });
2881
3299
  }
2882
3300
  async getInteractiveElements(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-browser",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",