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.
- package/AGENTS.md +1 -1
- package/README.md +5 -3
- package/dist/cli/index.mjs +15 -8
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.mjs +63 -11
- package/dist/{runtime-CEvs2yzE.mjs → runtime-BCGndjhR.mjs} +779 -79
- package/package.json +5 -5
|
@@ -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
|
|
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
|
|
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 === "
|
|
913
|
-
|
|
914
|
-
|
|
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
|
-
|
|
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
|
|
1121
|
+
const limited = normalizeRuntimeContentPayload((await conn.send("Runtime.evaluate", {
|
|
934
1122
|
expression,
|
|
935
|
-
returnByValue: true
|
|
936
|
-
|
|
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 === "
|
|
1620
|
-
mode: "
|
|
1621
|
-
content:
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
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:
|
|
1629
|
-
content:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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());
|