agentic-browser 2.2.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.
@@ -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");
@@ -2114,7 +2294,7 @@ var SessionManager = class {
2114
2294
  constructor(ctx, browser) {
2115
2295
  this.ctx = ctx;
2116
2296
  this.browser = browser;
2117
- this.store = new SessionStore(this.ctx.config.logDir);
2297
+ this.store = new SessionStore(this.ctx.config.dataDir);
2118
2298
  }
2119
2299
  async createSession(input) {
2120
2300
  if (input.browser !== "chrome") throw new Error("Only chrome is supported");
@@ -2235,15 +2415,38 @@ var SessionManager = class {
2235
2415
  }
2236
2416
  async getContent(sessionId, options) {
2237
2417
  const record = await this.ensureSession(sessionId);
2238
- return await this.browser.getContent(record.targetWsUrl, options);
2418
+ const result = await this.browser.getContent(record.targetWsUrl, options);
2419
+ if (options.mode === "summary" && result.structuredContent) {
2420
+ const sc = result.structuredContent;
2421
+ const url = typeof sc.url === "string" ? sc.url : record.lastUrl;
2422
+ const parsed = this.parseUrl(url);
2423
+ const headingTexts = (sc.headings ?? []).map((h) => typeof h === "string" ? h : h.text ?? "");
2424
+ const landmarkTexts = (sc.landmarks ?? []).map((l) => typeof l === "string" ? l : l.role ?? "");
2425
+ this.ctx.memoryService.updateProfileFromContent(parsed.domain, parsed.pathPattern, {
2426
+ headings: headingTexts,
2427
+ landmarks: landmarkTexts
2428
+ });
2429
+ }
2430
+ return result;
2239
2431
  }
2240
2432
  async getInteractiveElements(sessionId, options) {
2241
2433
  const record = await this.ensureSession(sessionId);
2242
2434
  return await this.browser.getInteractiveElements(record.targetWsUrl, options);
2243
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
+ }
2244
2444
  async dismissCookieBanner(sessionId) {
2245
2445
  const record = await this.ensureSession(sessionId);
2246
- return await this.browser.dismissCookieBanner(record.targetWsUrl);
2446
+ const result = await this.browser.dismissCookieBanner(record.targetWsUrl);
2447
+ const parsed = this.parseUrl(record.lastUrl);
2448
+ this.ctx.memoryService.updateProfileCookieBanner(parsed.domain, result);
2449
+ return result;
2247
2450
  }
2248
2451
  setStatus(status, reason) {
2249
2452
  const active = this.store.getActive();
@@ -2361,7 +2564,7 @@ var SessionManager = class {
2361
2564
  removedSessionIds.push(record.session.sessionId);
2362
2565
  }
2363
2566
  const keepIds = new Set(keep.map((record) => record.session.sessionId));
2364
- const profilesDir = path.join(this.ctx.config.logDir, "profiles");
2567
+ const profilesDir = path.join(this.ctx.config.dataDir, "profiles");
2365
2568
  const removedProfileDirs = [];
2366
2569
  if (fs.existsSync(profilesDir)) for (const entry of fs.readdirSync(profilesDir)) {
2367
2570
  const fullPath = path.join(profilesDir, entry);
@@ -2513,6 +2716,12 @@ var ControlApi = class {
2513
2716
  async dismissCookieBanner(sessionId) {
2514
2717
  return await this.sessions.dismissCookieBanner(sessionId);
2515
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
+ }
2516
2725
  listEvents(sessionId, limit = 100) {
2517
2726
  return { events: this.eventStore.list(sessionId, limit) };
2518
2727
  }
@@ -2549,7 +2758,7 @@ function loadConfig(env = process.env) {
2549
2758
  host: env.AGENTIC_BROWSER_HOST ?? "127.0.0.1",
2550
2759
  wsPort,
2551
2760
  commandTimeoutMs,
2552
- logDir: env.AGENTIC_BROWSER_LOG_DIR ?? path.resolve(process.cwd(), ".agentic-browser"),
2761
+ dataDir: env.AGENTIC_BROWSER_DIR ?? path.join(os.homedir(), ".agentic-browser"),
2553
2762
  browserExecutablePath: env.AGENTIC_BROWSER_CHROME_EXECUTABLE_PATH ?? env.AGENTIC_BROWSER_CHROME_PATH,
2554
2763
  cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
2555
2764
  userProfileDir,
@@ -2751,7 +2960,7 @@ function selectorSignal(insight) {
2751
2960
  const evidenceStrength = Math.min(selectorEvidence / 5, 1);
2752
2961
  return .7 * recipeCoverage + .3 * evidenceStrength;
2753
2962
  }
2754
- function scoreInsight(insight, normalizedIntent, normalizedDomain) {
2963
+ function scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup) {
2755
2964
  const insightIntent = normalize(insight.taskIntent);
2756
2965
  const intentMatch = insightIntent === normalizedIntent ? 1 : 0;
2757
2966
  const intentPartial = intentMatch === 1 || insightIntent.includes(normalizedIntent) || normalizedIntent.includes(insightIntent) ? .65 : 0;
@@ -2759,7 +2968,7 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain) {
2759
2968
  const reliability = .6 * confidenceFromCounts(insight.successCount, insight.failureCount) + .4 * freshnessWeight(insight.freshness);
2760
2969
  const selectorQuality = selectorSignal(insight);
2761
2970
  const score = .5 * Math.max(intentMatch, intentPartial) + .2 * domainMatch + .15 * reliability + .15 * selectorQuality;
2762
- return {
2971
+ const result = {
2763
2972
  insightId: insight.insightId,
2764
2973
  taskIntent: insight.taskIntent,
2765
2974
  siteDomain: insight.siteDomain,
@@ -2770,6 +2979,14 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain) {
2770
2979
  selectorAliases: buildSelectorAliases(insight),
2771
2980
  score
2772
2981
  };
2982
+ const profile = profileLookup?.(insight.siteDomain);
2983
+ if (profile) result.siteProfile = {
2984
+ selectorPatterns: profile.selectorPatterns.slice(0, 5),
2985
+ layoutFingerprints: profile.layoutFingerprints.slice(0, 3),
2986
+ cookieBanner: profile.cookieBanner,
2987
+ visitCount: profile.visitCount
2988
+ };
2989
+ return result;
2773
2990
  }
2774
2991
  var MemoryIndex = class {
2775
2992
  /** Domain → insights index, rebuilt lazily when insight list changes. */
@@ -2790,7 +3007,7 @@ var MemoryIndex = class {
2790
3007
  }
2791
3008
  this.indexedRefs = insights.slice();
2792
3009
  }
2793
- search(insights, input) {
3010
+ search(insights, input, profileLookup) {
2794
3011
  const normalizedIntent = normalize(input.taskIntent);
2795
3012
  const normalizedDomain = input.siteDomain ? normalize(input.siteDomain) : void 0;
2796
3013
  const limit = input.limit ?? 10;
@@ -2799,7 +3016,7 @@ var MemoryIndex = class {
2799
3016
  if (limit === 1 && normalizedDomain) {
2800
3017
  let best;
2801
3018
  for (const insight of candidates) {
2802
- const result = scoreInsight(insight, normalizedIntent, normalizedDomain);
3019
+ const result = scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup);
2803
3020
  if (result.score > 0 && (!best || result.score > best.score)) {
2804
3021
  best = result;
2805
3022
  if (best.score >= .95) break;
@@ -2807,7 +3024,7 @@ var MemoryIndex = class {
2807
3024
  }
2808
3025
  return best ? [best] : [];
2809
3026
  }
2810
- return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain)).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
3027
+ return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup)).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
2811
3028
  }
2812
3029
  };
2813
3030
  //#endregion
@@ -2838,6 +3055,20 @@ function applyFailure(insight, signal) {
2838
3055
  updatedAt: now
2839
3056
  };
2840
3057
  }
3058
+ function applyAgeStaleness(insight) {
3059
+ const ageDays = (Date.now() - Date.parse(insight.lastVerifiedAt)) / (1440 * 60 * 1e3);
3060
+ if (insight.freshness === "fresh" && ageDays > 30) return {
3061
+ ...insight,
3062
+ freshness: "suspect",
3063
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3064
+ };
3065
+ if (insight.freshness === "suspect" && ageDays > 44) return {
3066
+ ...insight,
3067
+ freshness: "stale",
3068
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3069
+ };
3070
+ return insight;
3071
+ }
2841
3072
  function applySuccess(insight) {
2842
3073
  const now = (/* @__PURE__ */ new Date()).toISOString();
2843
3074
  const successCount = insight.successCount + 1;
@@ -2904,6 +3135,147 @@ const TaskInsightSchema = z.object({
2904
3135
  selectorAliases: z.array(SelectorAliasSchema).default([])
2905
3136
  });
2906
3137
  const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
3138
+ const SelectorPatternSchema = z.object({
3139
+ pattern: z.string().min(1),
3140
+ frequency: z.number().int().nonnegative(),
3141
+ lastSeenAt: z.string().datetime()
3142
+ });
3143
+ const PageLayoutFingerprintSchema = z.object({
3144
+ pathPattern: z.string().min(1),
3145
+ headings: z.array(z.string()).default([]),
3146
+ landmarks: z.array(z.string()).default([]),
3147
+ lastSeenAt: z.string().datetime()
3148
+ });
3149
+ const SiteProfileSchema = z.object({
3150
+ domain: z.string().min(1),
3151
+ selectorPatterns: z.array(SelectorPatternSchema).default([]),
3152
+ layoutFingerprints: z.array(PageLayoutFingerprintSchema).default([]),
3153
+ navigationPaths: z.array(z.string()).default([]),
3154
+ cookieBanner: z.object({
3155
+ detected: z.boolean(),
3156
+ autoDismissed: z.boolean().optional(),
3157
+ method: z.string().optional()
3158
+ }).optional(),
3159
+ visitCount: z.number().int().nonnegative().default(0),
3160
+ lastVisitAt: z.string().datetime(),
3161
+ createdAt: z.string().datetime()
3162
+ });
3163
+ const SiteProfileStateSchema = z.object({ profiles: z.array(SiteProfileSchema) });
3164
+ //#endregion
3165
+ //#region src/memory/site-profile-store.ts
3166
+ const EMPTY_STATE$1 = { profiles: [] };
3167
+ const FLUSH_DELAY_MS$1 = 500;
3168
+ const REGISTERED_EXIT_HANDLERS$1 = Symbol.for("agentic-browser.site-profile-store.exit-handlers");
3169
+ const REGISTERED_STORES$1 = Symbol.for("agentic-browser.site-profile-store.instances");
3170
+ var SiteProfileStore = class {
3171
+ filePath;
3172
+ cached = null;
3173
+ dirty = false;
3174
+ flushTimer = null;
3175
+ constructor(baseDir) {
3176
+ const memoryDir = path.join(baseDir, "memory");
3177
+ fs.mkdirSync(memoryDir, { recursive: true });
3178
+ this.filePath = path.join(memoryDir, "site-profiles.json");
3179
+ const tmpPath = `${this.filePath}.tmp`;
3180
+ try {
3181
+ if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
3182
+ } catch {}
3183
+ if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE$1);
3184
+ this.registerProcessHandlers();
3185
+ }
3186
+ list() {
3187
+ return this.getCache();
3188
+ }
3189
+ get(domain) {
3190
+ const normalized = domain.toLowerCase();
3191
+ return this.getCache().find((p) => p.domain === normalized);
3192
+ }
3193
+ upsert(profile) {
3194
+ SiteProfileSchema.parse(profile);
3195
+ const profiles = this.getCache();
3196
+ const index = profiles.findIndex((p) => p.domain === profile.domain);
3197
+ if (index >= 0) profiles[index] = profile;
3198
+ else profiles.push(profile);
3199
+ this.markDirty();
3200
+ }
3201
+ flushSync() {
3202
+ if (this.flushTimer) {
3203
+ clearTimeout(this.flushTimer);
3204
+ this.flushTimer = null;
3205
+ }
3206
+ if (this.dirty && this.cached) {
3207
+ this.writeDisk({ profiles: this.cached });
3208
+ this.dirty = false;
3209
+ }
3210
+ }
3211
+ getCache() {
3212
+ if (this.cached) return this.cached;
3213
+ this.cached = this.readDisk().profiles;
3214
+ return this.cached;
3215
+ }
3216
+ markDirty() {
3217
+ this.dirty = true;
3218
+ if (!this.flushTimer) this.flushTimer = setTimeout(() => {
3219
+ this.flushTimer = null;
3220
+ this.flushSync();
3221
+ }, FLUSH_DELAY_MS$1);
3222
+ }
3223
+ readDisk() {
3224
+ let raw;
3225
+ try {
3226
+ raw = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
3227
+ } catch {
3228
+ this.backupAndReset();
3229
+ return EMPTY_STATE$1;
3230
+ }
3231
+ try {
3232
+ return SiteProfileStateSchema.parse(raw);
3233
+ } catch {
3234
+ const obj = raw;
3235
+ if (Array.isArray(obj?.profiles)) {
3236
+ const salvaged = obj.profiles.filter((item) => SiteProfileSchema.safeParse(item).success);
3237
+ if (salvaged.length > 0) {
3238
+ const state = { profiles: salvaged };
3239
+ this.backupAndReset();
3240
+ this.writeDisk(state);
3241
+ return state;
3242
+ }
3243
+ }
3244
+ this.backupAndReset();
3245
+ return EMPTY_STATE$1;
3246
+ }
3247
+ }
3248
+ backupAndReset() {
3249
+ const corruptPath = `${this.filePath}.corrupt-${Date.now()}`;
3250
+ try {
3251
+ fs.copyFileSync(this.filePath, corruptPath);
3252
+ } catch {}
3253
+ }
3254
+ writeDisk(state) {
3255
+ const tempPath = `${this.filePath}.tmp`;
3256
+ fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf8");
3257
+ fs.renameSync(tempPath, this.filePath);
3258
+ }
3259
+ registerProcessHandlers() {
3260
+ const proc = process;
3261
+ const stores = proc[REGISTERED_STORES$1] ??= /* @__PURE__ */ new Set();
3262
+ stores.add(this);
3263
+ if (proc[REGISTERED_EXIT_HANDLERS$1]) return;
3264
+ proc[REGISTERED_EXIT_HANDLERS$1] = true;
3265
+ const flushAll = () => {
3266
+ for (const store of stores) store.flushSync();
3267
+ };
3268
+ process.on("exit", flushAll);
3269
+ process.on("SIGINT", () => {
3270
+ flushAll();
3271
+ process.exit(0);
3272
+ });
3273
+ process.on("SIGTERM", () => {
3274
+ flushAll();
3275
+ process.exit(0);
3276
+ });
3277
+ }
3278
+ };
2907
3279
  //#endregion
2908
3280
  //#region src/memory/task-insight-store.ts
2909
3281
  const EMPTY_STATE = { insights: [] };
@@ -3030,25 +3402,42 @@ var TaskInsightStore = class {
3030
3402
  //#endregion
3031
3403
  //#region src/memory/memory-service.ts
3032
3404
  const SEARCH_CACHE_TTL_MS = 2e3;
3405
+ const MAX_NAVIGATION_PATHS = 20;
3406
+ const MAX_LAYOUT_FINGERPRINTS = 10;
3407
+ const MAX_SELECTOR_PATTERNS = 15;
3033
3408
  var MemoryService = class {
3034
3409
  store;
3410
+ profileStore;
3035
3411
  index;
3036
3412
  /** Simple TTL cache for search results keyed by intent+domain+limit. */
3037
3413
  searchCache = /* @__PURE__ */ new Map();
3038
3414
  constructor(baseDir) {
3039
3415
  this.store = new TaskInsightStore(baseDir);
3416
+ this.profileStore = new SiteProfileStore(baseDir);
3040
3417
  this.index = new MemoryIndex();
3041
3418
  }
3042
3419
  /** Force an immediate synchronous flush of pending writes. */
3043
3420
  flushSync() {
3044
3421
  this.store.flushSync();
3422
+ this.profileStore.flushSync();
3045
3423
  }
3046
3424
  search(input) {
3047
3425
  const cacheKey = `${input.taskIntent}\0${input.siteDomain ?? ""}\0${input.limit ?? 10}`;
3048
3426
  const now = Date.now();
3049
3427
  const cached = this.searchCache.get(cacheKey);
3050
3428
  if (cached && now - cached.ts < SEARCH_CACHE_TTL_MS) return cached.results;
3051
- const results = this.index.search(this.store.list(), input);
3429
+ const insights = this.store.list();
3430
+ let mutated = false;
3431
+ for (let i = 0; i < insights.length; i++) {
3432
+ const decayed = applyAgeStaleness(insights[i]);
3433
+ if (decayed.freshness !== insights[i].freshness) {
3434
+ insights[i] = decayed;
3435
+ this.store.upsert(decayed);
3436
+ mutated = true;
3437
+ }
3438
+ }
3439
+ if (mutated) this.searchCache.clear();
3440
+ const results = this.index.search(insights, input, (domain) => this.profileStore.get(domain));
3052
3441
  this.searchCache.set(cacheKey, {
3053
3442
  results,
3054
3443
  ts: now
@@ -3092,6 +3481,7 @@ var MemoryService = class {
3092
3481
  return verified;
3093
3482
  }
3094
3483
  recordSuccess(input) {
3484
+ this.updateSiteProfile(input.siteDomain, input.selector, input.url, input.sitePathPattern);
3095
3485
  const insights = this.store.list();
3096
3486
  const matched = this.findBestExactMatch(insights, input);
3097
3487
  const evidence = this.createEvidence(input, "success");
@@ -3149,6 +3539,7 @@ var MemoryService = class {
3149
3539
  return withAliases;
3150
3540
  }
3151
3541
  recordFailure(input, errorMessage) {
3542
+ this.updateSiteProfile(input.siteDomain, input.selector, input.url, input.sitePathPattern);
3152
3543
  const insights = this.store.list();
3153
3544
  const matched = this.findBestExactMatch(insights, input);
3154
3545
  if (!matched) return;
@@ -3163,6 +3554,91 @@ var MemoryService = class {
3163
3554
  this.invalidateSearchCache();
3164
3555
  return failed;
3165
3556
  }
3557
+ /** Update site profile from page content (call after getContent with mode="summary"). */
3558
+ updateProfileFromContent(domain, pathPattern, structuredContent) {
3559
+ if (!domain || domain === "unknown") return;
3560
+ const normalized = domain.toLowerCase();
3561
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3562
+ const profile = this.profileStore.get(normalized) ?? this.createEmptyProfile(normalized, now);
3563
+ const fingerprint = {
3564
+ pathPattern,
3565
+ headings: (structuredContent.headings ?? []).slice(0, 8),
3566
+ landmarks: (structuredContent.landmarks ?? []).slice(0, 8),
3567
+ lastSeenAt: now
3568
+ };
3569
+ const fpIndex = profile.layoutFingerprints.findIndex((fp) => fp.pathPattern === pathPattern);
3570
+ if (fpIndex >= 0) profile.layoutFingerprints[fpIndex] = fingerprint;
3571
+ else {
3572
+ profile.layoutFingerprints.push(fingerprint);
3573
+ if (profile.layoutFingerprints.length > MAX_LAYOUT_FINGERPRINTS) profile.layoutFingerprints = profile.layoutFingerprints.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt)).slice(0, MAX_LAYOUT_FINGERPRINTS);
3574
+ }
3575
+ profile.lastVisitAt = now;
3576
+ this.profileStore.upsert(profile);
3577
+ }
3578
+ /** Update site profile with cookie banner behavior. */
3579
+ updateProfileCookieBanner(domain, result) {
3580
+ if (!domain || domain === "unknown") return;
3581
+ const normalized = domain.toLowerCase();
3582
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3583
+ const profile = this.profileStore.get(normalized) ?? this.createEmptyProfile(normalized, now);
3584
+ profile.cookieBanner = {
3585
+ detected: true,
3586
+ autoDismissed: result.dismissed,
3587
+ method: result.method
3588
+ };
3589
+ profile.lastVisitAt = now;
3590
+ this.profileStore.upsert(profile);
3591
+ }
3592
+ /** Look up a site profile by domain. */
3593
+ getSiteProfile(domain) {
3594
+ return this.profileStore.get(domain);
3595
+ }
3596
+ updateSiteProfile(domain, selector, url, pathPattern) {
3597
+ if (!domain || domain === "unknown") return;
3598
+ const normalized = domain.toLowerCase();
3599
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3600
+ const profile = this.profileStore.get(normalized) ?? this.createEmptyProfile(normalized, now);
3601
+ profile.visitCount += 1;
3602
+ profile.lastVisitAt = now;
3603
+ if (selector) {
3604
+ const pattern = this.classifySelectorPattern(selector);
3605
+ if (pattern) {
3606
+ const existing = profile.selectorPatterns.find((p) => p.pattern === pattern);
3607
+ if (existing) {
3608
+ existing.frequency += 1;
3609
+ existing.lastSeenAt = now;
3610
+ } else if (profile.selectorPatterns.length < MAX_SELECTOR_PATTERNS) profile.selectorPatterns.push({
3611
+ pattern,
3612
+ frequency: 1,
3613
+ lastSeenAt: now
3614
+ });
3615
+ }
3616
+ }
3617
+ if (pathPattern && pathPattern !== "/") {
3618
+ if (!profile.navigationPaths.includes(pathPattern) && profile.navigationPaths.length < MAX_NAVIGATION_PATHS) profile.navigationPaths.push(pathPattern);
3619
+ }
3620
+ this.profileStore.upsert(profile);
3621
+ }
3622
+ createEmptyProfile(domain, now) {
3623
+ return {
3624
+ domain,
3625
+ selectorPatterns: [],
3626
+ layoutFingerprints: [],
3627
+ navigationPaths: [],
3628
+ visitCount: 0,
3629
+ lastVisitAt: now,
3630
+ createdAt: now
3631
+ };
3632
+ }
3633
+ classifySelectorPattern(selector) {
3634
+ if (selector.includes("[data-testid=")) return "data-testid";
3635
+ if (selector.includes("[data-cy=")) return "data-cy";
3636
+ if (selector.includes("[data-test=")) return "data-test";
3637
+ if (selector.includes("[aria-label=")) return "aria-label";
3638
+ if (selector.includes("[name=")) return "name-attribute";
3639
+ if (/^#[\w-]+$/.test(selector)) return "id-selector";
3640
+ if (selector.includes("[role=")) return "role-attribute";
3641
+ }
3166
3642
  findBestExactMatch(insights, input) {
3167
3643
  const intentLower = input.taskIntent.toLowerCase();
3168
3644
  const domainLower = input.siteDomain.toLowerCase();
@@ -3251,7 +3727,7 @@ var MemoryService = class {
3251
3727
  function createAppContext(env = process.env) {
3252
3728
  const config = loadConfig(env);
3253
3729
  const logger = new Logger("app");
3254
- const eventStore = new EventStore(config.logDir);
3730
+ const eventStore = new EventStore(config.dataDir);
3255
3731
  const tokenService = new SessionTokenService();
3256
3732
  return {
3257
3733
  config,
@@ -3263,7 +3739,7 @@ function createAppContext(env = process.env) {
3263
3739
  port: config.wsPort,
3264
3740
  tokenService
3265
3741
  }),
3266
- memoryService: new MemoryService(config.logDir)
3742
+ memoryService: new MemoryService(config.dataDir)
3267
3743
  };
3268
3744
  }
3269
3745
  //#endregion
@@ -3284,8 +3760,9 @@ var AgenticBrowserCore = class {
3284
3760
  return this.api.getSession(sessionId);
3285
3761
  }
3286
3762
  async runCommand(input) {
3763
+ const commandId = input.commandId ?? `${input.type}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3287
3764
  return await this.api.executeCommand(input.sessionId, {
3288
- commandId: input.commandId,
3765
+ commandId,
3289
3766
  type: input.type,
3290
3767
  payload: input.payload
3291
3768
  });
@@ -3308,6 +3785,12 @@ var AgenticBrowserCore = class {
3308
3785
  async dismissCookieBanner(sessionId) {
3309
3786
  return await this.api.dismissCookieBanner(sessionId);
3310
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
+ }
3311
3794
  async restartSession(sessionId) {
3312
3795
  return await this.api.restartSession(sessionId);
3313
3796
  }
@@ -3332,7 +3815,7 @@ var AgenticBrowserCore = class {
3332
3815
  };
3333
3816
  function createAgenticBrowserCore(options = {}) {
3334
3817
  const context = createAppContext(options.env);
3335
- return new AgenticBrowserCore(context, options.browserController ?? new ChromeCdpBrowserController(context.config.logDir));
3818
+ return new AgenticBrowserCore(context, options.browserController ?? new ChromeCdpBrowserController(context.config.dataDir));
3336
3819
  }
3337
3820
  function createMockAgenticBrowserCore(env) {
3338
3821
  return new AgenticBrowserCore(createAppContext(env), new MockBrowserController());