agentic-browser 2.1.0 → 2.3.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.
@@ -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) {
@@ -1737,7 +2114,7 @@ var SessionManager = class {
1737
2114
  constructor(ctx, browser) {
1738
2115
  this.ctx = ctx;
1739
2116
  this.browser = browser;
1740
- this.store = new SessionStore(this.ctx.config.logDir);
2117
+ this.store = new SessionStore(this.ctx.config.dataDir);
1741
2118
  }
1742
2119
  async createSession(input) {
1743
2120
  if (input.browser !== "chrome") throw new Error("Only chrome is supported");
@@ -1858,7 +2235,19 @@ var SessionManager = class {
1858
2235
  }
1859
2236
  async getContent(sessionId, options) {
1860
2237
  const record = await this.ensureSession(sessionId);
1861
- return await this.browser.getContent(record.targetWsUrl, options);
2238
+ const result = await this.browser.getContent(record.targetWsUrl, options);
2239
+ if (options.mode === "summary" && result.structuredContent) {
2240
+ const sc = result.structuredContent;
2241
+ const url = typeof sc.url === "string" ? sc.url : record.lastUrl;
2242
+ const parsed = this.parseUrl(url);
2243
+ const headingTexts = (sc.headings ?? []).map((h) => typeof h === "string" ? h : h.text ?? "");
2244
+ const landmarkTexts = (sc.landmarks ?? []).map((l) => typeof l === "string" ? l : l.role ?? "");
2245
+ this.ctx.memoryService.updateProfileFromContent(parsed.domain, parsed.pathPattern, {
2246
+ headings: headingTexts,
2247
+ landmarks: landmarkTexts
2248
+ });
2249
+ }
2250
+ return result;
1862
2251
  }
1863
2252
  async getInteractiveElements(sessionId, options) {
1864
2253
  const record = await this.ensureSession(sessionId);
@@ -1866,7 +2255,10 @@ var SessionManager = class {
1866
2255
  }
1867
2256
  async dismissCookieBanner(sessionId) {
1868
2257
  const record = await this.ensureSession(sessionId);
1869
- return await this.browser.dismissCookieBanner(record.targetWsUrl);
2258
+ const result = await this.browser.dismissCookieBanner(record.targetWsUrl);
2259
+ const parsed = this.parseUrl(record.lastUrl);
2260
+ this.ctx.memoryService.updateProfileCookieBanner(parsed.domain, result);
2261
+ return result;
1870
2262
  }
1871
2263
  setStatus(status, reason) {
1872
2264
  const active = this.store.getActive();
@@ -1984,7 +2376,7 @@ var SessionManager = class {
1984
2376
  removedSessionIds.push(record.session.sessionId);
1985
2377
  }
1986
2378
  const keepIds = new Set(keep.map((record) => record.session.sessionId));
1987
- const profilesDir = path.join(this.ctx.config.logDir, "profiles");
2379
+ const profilesDir = path.join(this.ctx.config.dataDir, "profiles");
1988
2380
  const removedProfileDirs = [];
1989
2381
  if (fs.existsSync(profilesDir)) for (const entry of fs.readdirSync(profilesDir)) {
1990
2382
  const fullPath = path.join(profilesDir, entry);
@@ -2172,7 +2564,7 @@ function loadConfig(env = process.env) {
2172
2564
  host: env.AGENTIC_BROWSER_HOST ?? "127.0.0.1",
2173
2565
  wsPort,
2174
2566
  commandTimeoutMs,
2175
- logDir: env.AGENTIC_BROWSER_LOG_DIR ?? path.resolve(process.cwd(), ".agentic-browser"),
2567
+ dataDir: env.AGENTIC_BROWSER_DIR ?? path.join(os.homedir(), ".agentic-browser"),
2176
2568
  browserExecutablePath: env.AGENTIC_BROWSER_CHROME_EXECUTABLE_PATH ?? env.AGENTIC_BROWSER_CHROME_PATH,
2177
2569
  cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
2178
2570
  userProfileDir,
@@ -2217,16 +2609,26 @@ const MAX_EVENTS_PER_SESSION = 500;
2217
2609
  var EventStore = class {
2218
2610
  events = /* @__PURE__ */ new Map();
2219
2611
  filePath;
2612
+ bufferedLines = [];
2613
+ flushTimer;
2614
+ flushInFlight;
2220
2615
  constructor(baseDir) {
2221
2616
  fs.mkdirSync(baseDir, { recursive: true });
2222
2617
  this.filePath = path.join(baseDir, "session-events.log");
2618
+ process.once("beforeExit", this.flushSync);
2619
+ process.once("exit", this.flushSync);
2223
2620
  }
2224
2621
  append(event) {
2225
2622
  let existing = this.events.get(event.sessionId) ?? [];
2226
2623
  existing.push(event);
2227
2624
  if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-MAX_EVENTS_PER_SESSION);
2228
2625
  this.events.set(event.sessionId, existing);
2229
- fs.appendFileSync(this.filePath, `${JSON.stringify(event)}\n`, "utf8");
2626
+ this.bufferedLines.push(`${JSON.stringify(event)}\n`);
2627
+ if (this.bufferedLines.length >= 20) {
2628
+ this.flushSoon(0);
2629
+ return;
2630
+ }
2631
+ this.flushSoon(25);
2230
2632
  }
2231
2633
  list(sessionId, limit = 100) {
2232
2634
  const entries = this.events.get(sessionId) ?? [];
@@ -2235,6 +2637,36 @@ var EventStore = class {
2235
2637
  clear(sessionId) {
2236
2638
  this.events.delete(sessionId);
2237
2639
  }
2640
+ flushSoon(delayMs) {
2641
+ if (this.flushInFlight) return this.flushInFlight;
2642
+ if (this.flushTimer) return Promise.resolve();
2643
+ this.flushTimer = setTimeout(() => {
2644
+ this.flushTimer = void 0;
2645
+ this.flushAsync();
2646
+ }, delayMs);
2647
+ this.flushTimer.unref();
2648
+ return Promise.resolve();
2649
+ }
2650
+ async flushAsync() {
2651
+ if (this.flushInFlight || this.bufferedLines.length === 0) return;
2652
+ const chunk = this.bufferedLines.splice(0, this.bufferedLines.length).join("");
2653
+ this.flushInFlight = fs.promises.appendFile(this.filePath, chunk, "utf8").catch(() => {}).finally(() => {
2654
+ this.flushInFlight = void 0;
2655
+ if (this.bufferedLines.length > 0) this.flushSoon(0);
2656
+ });
2657
+ await this.flushInFlight;
2658
+ }
2659
+ flushSync = () => {
2660
+ if (this.flushTimer) {
2661
+ clearTimeout(this.flushTimer);
2662
+ this.flushTimer = void 0;
2663
+ }
2664
+ if (this.bufferedLines.length === 0) return;
2665
+ const chunk = this.bufferedLines.splice(0, this.bufferedLines.length).join("");
2666
+ try {
2667
+ fs.appendFileSync(this.filePath, chunk, "utf8");
2668
+ } catch {}
2669
+ };
2238
2670
  };
2239
2671
  //#endregion
2240
2672
  //#region src/auth/session-token.ts
@@ -2334,7 +2766,7 @@ function selectorSignal(insight) {
2334
2766
  const evidenceStrength = Math.min(selectorEvidence / 5, 1);
2335
2767
  return .7 * recipeCoverage + .3 * evidenceStrength;
2336
2768
  }
2337
- function scoreInsight(insight, normalizedIntent, normalizedDomain) {
2769
+ function scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup) {
2338
2770
  const insightIntent = normalize(insight.taskIntent);
2339
2771
  const intentMatch = insightIntent === normalizedIntent ? 1 : 0;
2340
2772
  const intentPartial = intentMatch === 1 || insightIntent.includes(normalizedIntent) || normalizedIntent.includes(insightIntent) ? .65 : 0;
@@ -2342,7 +2774,7 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain) {
2342
2774
  const reliability = .6 * confidenceFromCounts(insight.successCount, insight.failureCount) + .4 * freshnessWeight(insight.freshness);
2343
2775
  const selectorQuality = selectorSignal(insight);
2344
2776
  const score = .5 * Math.max(intentMatch, intentPartial) + .2 * domainMatch + .15 * reliability + .15 * selectorQuality;
2345
- return {
2777
+ const result = {
2346
2778
  insightId: insight.insightId,
2347
2779
  taskIntent: insight.taskIntent,
2348
2780
  siteDomain: insight.siteDomain,
@@ -2353,6 +2785,14 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain) {
2353
2785
  selectorAliases: buildSelectorAliases(insight),
2354
2786
  score
2355
2787
  };
2788
+ const profile = profileLookup?.(insight.siteDomain);
2789
+ if (profile) result.siteProfile = {
2790
+ selectorPatterns: profile.selectorPatterns.slice(0, 5),
2791
+ layoutFingerprints: profile.layoutFingerprints.slice(0, 3),
2792
+ cookieBanner: profile.cookieBanner,
2793
+ visitCount: profile.visitCount
2794
+ };
2795
+ return result;
2356
2796
  }
2357
2797
  var MemoryIndex = class {
2358
2798
  /** Domain → insights index, rebuilt lazily when insight list changes. */
@@ -2373,7 +2813,7 @@ var MemoryIndex = class {
2373
2813
  }
2374
2814
  this.indexedRefs = insights.slice();
2375
2815
  }
2376
- search(insights, input) {
2816
+ search(insights, input, profileLookup) {
2377
2817
  const normalizedIntent = normalize(input.taskIntent);
2378
2818
  const normalizedDomain = input.siteDomain ? normalize(input.siteDomain) : void 0;
2379
2819
  const limit = input.limit ?? 10;
@@ -2382,7 +2822,7 @@ var MemoryIndex = class {
2382
2822
  if (limit === 1 && normalizedDomain) {
2383
2823
  let best;
2384
2824
  for (const insight of candidates) {
2385
- const result = scoreInsight(insight, normalizedIntent, normalizedDomain);
2825
+ const result = scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup);
2386
2826
  if (result.score > 0 && (!best || result.score > best.score)) {
2387
2827
  best = result;
2388
2828
  if (best.score >= .95) break;
@@ -2390,7 +2830,7 @@ var MemoryIndex = class {
2390
2830
  }
2391
2831
  return best ? [best] : [];
2392
2832
  }
2393
- return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain)).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
2833
+ return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup)).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
2394
2834
  }
2395
2835
  };
2396
2836
  //#endregion
@@ -2421,6 +2861,20 @@ function applyFailure(insight, signal) {
2421
2861
  updatedAt: now
2422
2862
  };
2423
2863
  }
2864
+ function applyAgeStaleness(insight) {
2865
+ const ageDays = (Date.now() - Date.parse(insight.lastVerifiedAt)) / (1440 * 60 * 1e3);
2866
+ if (insight.freshness === "fresh" && ageDays > 30) return {
2867
+ ...insight,
2868
+ freshness: "suspect",
2869
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2870
+ };
2871
+ if (insight.freshness === "suspect" && ageDays > 44) return {
2872
+ ...insight,
2873
+ freshness: "stale",
2874
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2875
+ };
2876
+ return insight;
2877
+ }
2424
2878
  function applySuccess(insight) {
2425
2879
  const now = (/* @__PURE__ */ new Date()).toISOString();
2426
2880
  const successCount = insight.successCount + 1;
@@ -2487,6 +2941,147 @@ const TaskInsightSchema = z.object({
2487
2941
  selectorAliases: z.array(SelectorAliasSchema).default([])
2488
2942
  });
2489
2943
  const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
2944
+ const SelectorPatternSchema = z.object({
2945
+ pattern: z.string().min(1),
2946
+ frequency: z.number().int().nonnegative(),
2947
+ lastSeenAt: z.string().datetime()
2948
+ });
2949
+ const PageLayoutFingerprintSchema = z.object({
2950
+ pathPattern: z.string().min(1),
2951
+ headings: z.array(z.string()).default([]),
2952
+ landmarks: z.array(z.string()).default([]),
2953
+ lastSeenAt: z.string().datetime()
2954
+ });
2955
+ const SiteProfileSchema = z.object({
2956
+ domain: z.string().min(1),
2957
+ selectorPatterns: z.array(SelectorPatternSchema).default([]),
2958
+ layoutFingerprints: z.array(PageLayoutFingerprintSchema).default([]),
2959
+ navigationPaths: z.array(z.string()).default([]),
2960
+ cookieBanner: z.object({
2961
+ detected: z.boolean(),
2962
+ autoDismissed: z.boolean().optional(),
2963
+ method: z.string().optional()
2964
+ }).optional(),
2965
+ visitCount: z.number().int().nonnegative().default(0),
2966
+ lastVisitAt: z.string().datetime(),
2967
+ createdAt: z.string().datetime()
2968
+ });
2969
+ const SiteProfileStateSchema = z.object({ profiles: z.array(SiteProfileSchema) });
2970
+ //#endregion
2971
+ //#region src/memory/site-profile-store.ts
2972
+ const EMPTY_STATE$1 = { profiles: [] };
2973
+ const FLUSH_DELAY_MS$1 = 500;
2974
+ const REGISTERED_EXIT_HANDLERS$1 = Symbol.for("agentic-browser.site-profile-store.exit-handlers");
2975
+ const REGISTERED_STORES$1 = Symbol.for("agentic-browser.site-profile-store.instances");
2976
+ var SiteProfileStore = class {
2977
+ filePath;
2978
+ cached = null;
2979
+ dirty = false;
2980
+ flushTimer = null;
2981
+ constructor(baseDir) {
2982
+ const memoryDir = path.join(baseDir, "memory");
2983
+ fs.mkdirSync(memoryDir, { recursive: true });
2984
+ this.filePath = path.join(memoryDir, "site-profiles.json");
2985
+ const tmpPath = `${this.filePath}.tmp`;
2986
+ try {
2987
+ if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
2988
+ } catch {}
2989
+ if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE$1);
2990
+ this.registerProcessHandlers();
2991
+ }
2992
+ list() {
2993
+ return this.getCache();
2994
+ }
2995
+ get(domain) {
2996
+ const normalized = domain.toLowerCase();
2997
+ return this.getCache().find((p) => p.domain === normalized);
2998
+ }
2999
+ upsert(profile) {
3000
+ SiteProfileSchema.parse(profile);
3001
+ const profiles = this.getCache();
3002
+ const index = profiles.findIndex((p) => p.domain === profile.domain);
3003
+ if (index >= 0) profiles[index] = profile;
3004
+ else profiles.push(profile);
3005
+ this.markDirty();
3006
+ }
3007
+ flushSync() {
3008
+ if (this.flushTimer) {
3009
+ clearTimeout(this.flushTimer);
3010
+ this.flushTimer = null;
3011
+ }
3012
+ if (this.dirty && this.cached) {
3013
+ this.writeDisk({ profiles: this.cached });
3014
+ this.dirty = false;
3015
+ }
3016
+ }
3017
+ getCache() {
3018
+ if (this.cached) return this.cached;
3019
+ this.cached = this.readDisk().profiles;
3020
+ return this.cached;
3021
+ }
3022
+ markDirty() {
3023
+ this.dirty = true;
3024
+ if (!this.flushTimer) this.flushTimer = setTimeout(() => {
3025
+ this.flushTimer = null;
3026
+ this.flushSync();
3027
+ }, FLUSH_DELAY_MS$1);
3028
+ }
3029
+ readDisk() {
3030
+ let raw;
3031
+ try {
3032
+ raw = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
3033
+ } catch {
3034
+ this.backupAndReset();
3035
+ return EMPTY_STATE$1;
3036
+ }
3037
+ try {
3038
+ return SiteProfileStateSchema.parse(raw);
3039
+ } catch {
3040
+ const obj = raw;
3041
+ if (Array.isArray(obj?.profiles)) {
3042
+ const salvaged = obj.profiles.filter((item) => SiteProfileSchema.safeParse(item).success);
3043
+ if (salvaged.length > 0) {
3044
+ const state = { profiles: salvaged };
3045
+ this.backupAndReset();
3046
+ this.writeDisk(state);
3047
+ return state;
3048
+ }
3049
+ }
3050
+ this.backupAndReset();
3051
+ return EMPTY_STATE$1;
3052
+ }
3053
+ }
3054
+ backupAndReset() {
3055
+ const corruptPath = `${this.filePath}.corrupt-${Date.now()}`;
3056
+ try {
3057
+ fs.copyFileSync(this.filePath, corruptPath);
3058
+ } catch {}
3059
+ }
3060
+ writeDisk(state) {
3061
+ const tempPath = `${this.filePath}.tmp`;
3062
+ fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf8");
3063
+ fs.renameSync(tempPath, this.filePath);
3064
+ }
3065
+ registerProcessHandlers() {
3066
+ const proc = process;
3067
+ const stores = proc[REGISTERED_STORES$1] ??= /* @__PURE__ */ new Set();
3068
+ stores.add(this);
3069
+ if (proc[REGISTERED_EXIT_HANDLERS$1]) return;
3070
+ proc[REGISTERED_EXIT_HANDLERS$1] = true;
3071
+ const flushAll = () => {
3072
+ for (const store of stores) store.flushSync();
3073
+ };
3074
+ process.on("exit", flushAll);
3075
+ process.on("SIGINT", () => {
3076
+ flushAll();
3077
+ process.exit(0);
3078
+ });
3079
+ process.on("SIGTERM", () => {
3080
+ flushAll();
3081
+ process.exit(0);
3082
+ });
3083
+ }
3084
+ };
2490
3085
  //#endregion
2491
3086
  //#region src/memory/task-insight-store.ts
2492
3087
  const EMPTY_STATE = { insights: [] };
@@ -2613,25 +3208,42 @@ var TaskInsightStore = class {
2613
3208
  //#endregion
2614
3209
  //#region src/memory/memory-service.ts
2615
3210
  const SEARCH_CACHE_TTL_MS = 2e3;
3211
+ const MAX_NAVIGATION_PATHS = 20;
3212
+ const MAX_LAYOUT_FINGERPRINTS = 10;
3213
+ const MAX_SELECTOR_PATTERNS = 15;
2616
3214
  var MemoryService = class {
2617
3215
  store;
3216
+ profileStore;
2618
3217
  index;
2619
3218
  /** Simple TTL cache for search results keyed by intent+domain+limit. */
2620
3219
  searchCache = /* @__PURE__ */ new Map();
2621
3220
  constructor(baseDir) {
2622
3221
  this.store = new TaskInsightStore(baseDir);
3222
+ this.profileStore = new SiteProfileStore(baseDir);
2623
3223
  this.index = new MemoryIndex();
2624
3224
  }
2625
3225
  /** Force an immediate synchronous flush of pending writes. */
2626
3226
  flushSync() {
2627
3227
  this.store.flushSync();
3228
+ this.profileStore.flushSync();
2628
3229
  }
2629
3230
  search(input) {
2630
3231
  const cacheKey = `${input.taskIntent}\0${input.siteDomain ?? ""}\0${input.limit ?? 10}`;
2631
3232
  const now = Date.now();
2632
3233
  const cached = this.searchCache.get(cacheKey);
2633
3234
  if (cached && now - cached.ts < SEARCH_CACHE_TTL_MS) return cached.results;
2634
- const results = this.index.search(this.store.list(), input);
3235
+ const insights = this.store.list();
3236
+ let mutated = false;
3237
+ for (let i = 0; i < insights.length; i++) {
3238
+ const decayed = applyAgeStaleness(insights[i]);
3239
+ if (decayed.freshness !== insights[i].freshness) {
3240
+ insights[i] = decayed;
3241
+ this.store.upsert(decayed);
3242
+ mutated = true;
3243
+ }
3244
+ }
3245
+ if (mutated) this.searchCache.clear();
3246
+ const results = this.index.search(insights, input, (domain) => this.profileStore.get(domain));
2635
3247
  this.searchCache.set(cacheKey, {
2636
3248
  results,
2637
3249
  ts: now
@@ -2675,6 +3287,7 @@ var MemoryService = class {
2675
3287
  return verified;
2676
3288
  }
2677
3289
  recordSuccess(input) {
3290
+ this.updateSiteProfile(input.siteDomain, input.selector, input.url, input.sitePathPattern);
2678
3291
  const insights = this.store.list();
2679
3292
  const matched = this.findBestExactMatch(insights, input);
2680
3293
  const evidence = this.createEvidence(input, "success");
@@ -2732,6 +3345,7 @@ var MemoryService = class {
2732
3345
  return withAliases;
2733
3346
  }
2734
3347
  recordFailure(input, errorMessage) {
3348
+ this.updateSiteProfile(input.siteDomain, input.selector, input.url, input.sitePathPattern);
2735
3349
  const insights = this.store.list();
2736
3350
  const matched = this.findBestExactMatch(insights, input);
2737
3351
  if (!matched) return;
@@ -2746,6 +3360,91 @@ var MemoryService = class {
2746
3360
  this.invalidateSearchCache();
2747
3361
  return failed;
2748
3362
  }
3363
+ /** Update site profile from page content (call after getContent with mode="summary"). */
3364
+ updateProfileFromContent(domain, pathPattern, structuredContent) {
3365
+ if (!domain || domain === "unknown") return;
3366
+ const normalized = domain.toLowerCase();
3367
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3368
+ const profile = this.profileStore.get(normalized) ?? this.createEmptyProfile(normalized, now);
3369
+ const fingerprint = {
3370
+ pathPattern,
3371
+ headings: (structuredContent.headings ?? []).slice(0, 8),
3372
+ landmarks: (structuredContent.landmarks ?? []).slice(0, 8),
3373
+ lastSeenAt: now
3374
+ };
3375
+ const fpIndex = profile.layoutFingerprints.findIndex((fp) => fp.pathPattern === pathPattern);
3376
+ if (fpIndex >= 0) profile.layoutFingerprints[fpIndex] = fingerprint;
3377
+ else {
3378
+ profile.layoutFingerprints.push(fingerprint);
3379
+ if (profile.layoutFingerprints.length > MAX_LAYOUT_FINGERPRINTS) profile.layoutFingerprints = profile.layoutFingerprints.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt)).slice(0, MAX_LAYOUT_FINGERPRINTS);
3380
+ }
3381
+ profile.lastVisitAt = now;
3382
+ this.profileStore.upsert(profile);
3383
+ }
3384
+ /** Update site profile with cookie banner behavior. */
3385
+ updateProfileCookieBanner(domain, result) {
3386
+ if (!domain || domain === "unknown") return;
3387
+ const normalized = domain.toLowerCase();
3388
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3389
+ const profile = this.profileStore.get(normalized) ?? this.createEmptyProfile(normalized, now);
3390
+ profile.cookieBanner = {
3391
+ detected: true,
3392
+ autoDismissed: result.dismissed,
3393
+ method: result.method
3394
+ };
3395
+ profile.lastVisitAt = now;
3396
+ this.profileStore.upsert(profile);
3397
+ }
3398
+ /** Look up a site profile by domain. */
3399
+ getSiteProfile(domain) {
3400
+ return this.profileStore.get(domain);
3401
+ }
3402
+ updateSiteProfile(domain, selector, url, pathPattern) {
3403
+ if (!domain || domain === "unknown") return;
3404
+ const normalized = domain.toLowerCase();
3405
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3406
+ const profile = this.profileStore.get(normalized) ?? this.createEmptyProfile(normalized, now);
3407
+ profile.visitCount += 1;
3408
+ profile.lastVisitAt = now;
3409
+ if (selector) {
3410
+ const pattern = this.classifySelectorPattern(selector);
3411
+ if (pattern) {
3412
+ const existing = profile.selectorPatterns.find((p) => p.pattern === pattern);
3413
+ if (existing) {
3414
+ existing.frequency += 1;
3415
+ existing.lastSeenAt = now;
3416
+ } else if (profile.selectorPatterns.length < MAX_SELECTOR_PATTERNS) profile.selectorPatterns.push({
3417
+ pattern,
3418
+ frequency: 1,
3419
+ lastSeenAt: now
3420
+ });
3421
+ }
3422
+ }
3423
+ if (pathPattern && pathPattern !== "/") {
3424
+ if (!profile.navigationPaths.includes(pathPattern) && profile.navigationPaths.length < MAX_NAVIGATION_PATHS) profile.navigationPaths.push(pathPattern);
3425
+ }
3426
+ this.profileStore.upsert(profile);
3427
+ }
3428
+ createEmptyProfile(domain, now) {
3429
+ return {
3430
+ domain,
3431
+ selectorPatterns: [],
3432
+ layoutFingerprints: [],
3433
+ navigationPaths: [],
3434
+ visitCount: 0,
3435
+ lastVisitAt: now,
3436
+ createdAt: now
3437
+ };
3438
+ }
3439
+ classifySelectorPattern(selector) {
3440
+ if (selector.includes("[data-testid=")) return "data-testid";
3441
+ if (selector.includes("[data-cy=")) return "data-cy";
3442
+ if (selector.includes("[data-test=")) return "data-test";
3443
+ if (selector.includes("[aria-label=")) return "aria-label";
3444
+ if (selector.includes("[name=")) return "name-attribute";
3445
+ if (/^#[\w-]+$/.test(selector)) return "id-selector";
3446
+ if (selector.includes("[role=")) return "role-attribute";
3447
+ }
2749
3448
  findBestExactMatch(insights, input) {
2750
3449
  const intentLower = input.taskIntent.toLowerCase();
2751
3450
  const domainLower = input.siteDomain.toLowerCase();
@@ -2834,7 +3533,7 @@ var MemoryService = class {
2834
3533
  function createAppContext(env = process.env) {
2835
3534
  const config = loadConfig(env);
2836
3535
  const logger = new Logger("app");
2837
- const eventStore = new EventStore(config.logDir);
3536
+ const eventStore = new EventStore(config.dataDir);
2838
3537
  const tokenService = new SessionTokenService();
2839
3538
  return {
2840
3539
  config,
@@ -2846,7 +3545,7 @@ function createAppContext(env = process.env) {
2846
3545
  port: config.wsPort,
2847
3546
  tokenService
2848
3547
  }),
2849
- memoryService: new MemoryService(config.logDir)
3548
+ memoryService: new MemoryService(config.dataDir)
2850
3549
  };
2851
3550
  }
2852
3551
  //#endregion
@@ -2876,7 +3575,8 @@ var AgenticBrowserCore = class {
2876
3575
  async getPageContent(input) {
2877
3576
  return await this.api.getContent(input.sessionId, {
2878
3577
  mode: input.mode,
2879
- selector: input.selector
3578
+ selector: input.selector,
3579
+ maxChars: input.maxChars
2880
3580
  });
2881
3581
  }
2882
3582
  async getInteractiveElements(input) {
@@ -2914,7 +3614,7 @@ var AgenticBrowserCore = class {
2914
3614
  };
2915
3615
  function createAgenticBrowserCore(options = {}) {
2916
3616
  const context = createAppContext(options.env);
2917
- return new AgenticBrowserCore(context, options.browserController ?? new ChromeCdpBrowserController(context.config.logDir));
3617
+ return new AgenticBrowserCore(context, options.browserController ?? new ChromeCdpBrowserController(context.config.dataDir));
2918
3618
  }
2919
3619
  function createMockAgenticBrowserCore(env) {
2920
3620
  return new AgenticBrowserCore(createAppContext(env), new MockBrowserController());