agentic-browser 2.6.1 → 2.7.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 +25 -19
- package/README.md +16 -1
- package/dist/cli/index.mjs +300 -54
- package/dist/index.d.mts +158 -99
- package/dist/index.mjs +1 -2
- package/dist/mcp/index.d.mts +1 -4
- package/dist/mcp/index.mjs +47 -18
- package/dist/{runtime-BzdbtFF4.mjs → runtime-CfQ0xGAm.mjs} +1055 -207
- package/dist/{setup-DUAjdbQ3.mjs → setup-DwFaZOPf.mjs} +13 -6
- package/package.json +21 -19
|
@@ -8,7 +8,33 @@ import WebSocket from "ws";
|
|
|
8
8
|
import crypto from "node:crypto";
|
|
9
9
|
import { URL as URL$1 } from "node:url";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Create a directory (recursively) and tighten it to owner-only if it already existed
|
|
13
|
+
* with wider permissions (skips home, filesystem root, and dirs owned by another user).
|
|
14
|
+
*/
|
|
15
|
+
function ensurePrivateDir(dir) {
|
|
16
|
+
fs.mkdirSync(dir, {
|
|
17
|
+
recursive: true,
|
|
18
|
+
mode: 448
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
if (process.platform === "win32" || typeof process.getuid !== "function") return;
|
|
22
|
+
const resolved = path.resolve(dir);
|
|
23
|
+
if (resolved === path.parse(resolved).root || resolved === path.resolve(os.homedir())) return;
|
|
24
|
+
const st = fs.statSync(resolved);
|
|
25
|
+
if (!st.isDirectory() || st.uid !== process.getuid()) return;
|
|
26
|
+
if ((st.mode & 63) !== 0) fs.chmodSync(resolved, 448);
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
/** Write a file with owner-only permissions, tightening any pre-existing file. */
|
|
30
|
+
function writePrivateFileSync(file, data) {
|
|
31
|
+
fs.writeFileSync(file, data, {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
mode: 384
|
|
34
|
+
});
|
|
35
|
+
fs.chmodSync(file, 384);
|
|
36
|
+
}
|
|
37
|
+
// src/session/chrome-launcher.ts
|
|
12
38
|
const CANDIDATES = [
|
|
13
39
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
14
40
|
"/usr/bin/google-chrome",
|
|
@@ -21,8 +47,36 @@ function discoverChrome(explicitPath) {
|
|
|
21
47
|
if (!found) throw new Error("No supported Chrome installation found.");
|
|
22
48
|
return found;
|
|
23
49
|
}
|
|
24
|
-
|
|
25
|
-
|
|
50
|
+
// src/session/browser-controller.ts
|
|
51
|
+
/**
|
|
52
|
+
* Detect a running Chrome that owns `profileDir`. On macOS/Linux `SingletonLock`
|
|
53
|
+
* is a dangling symlink to `<hostname>-<pid>`, so existsSync (which follows
|
|
54
|
+
* symlinks) cannot see it; inspect the link itself and probe the pid.
|
|
55
|
+
*/
|
|
56
|
+
function isProfileLocked(profileDir) {
|
|
57
|
+
const lockFile = path.join(profileDir, "SingletonLock");
|
|
58
|
+
let target;
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.lstatSync(lockFile).isSymbolicLink()) return { locked: true };
|
|
61
|
+
target = fs.readlinkSync(lockFile);
|
|
62
|
+
} catch {
|
|
63
|
+
return { locked: false };
|
|
64
|
+
}
|
|
65
|
+
const pid = Number(target.slice(target.lastIndexOf("-") + 1));
|
|
66
|
+
if (!Number.isInteger(pid) || pid <= 0) return { locked: true };
|
|
67
|
+
try {
|
|
68
|
+
process.kill(pid, 0);
|
|
69
|
+
return {
|
|
70
|
+
locked: true,
|
|
71
|
+
pid
|
|
72
|
+
};
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return {
|
|
75
|
+
locked: e.code === "EPERM",
|
|
76
|
+
pid
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
26
80
|
var CdpConnection = class CdpConnection {
|
|
27
81
|
ws;
|
|
28
82
|
nextId = 0;
|
|
@@ -339,6 +393,28 @@ async function initTarget(targetWsUrl, userAgent) {
|
|
|
339
393
|
conn.close();
|
|
340
394
|
}
|
|
341
395
|
}
|
|
396
|
+
/** Called on the iframe element: origin of its content box in top-level viewport coordinates. */
|
|
397
|
+
const FRAME_CONTENT_OFFSET_FUNCTION = `function () {
|
|
398
|
+
const rect = this.getBoundingClientRect();
|
|
399
|
+
const style = getComputedStyle(this);
|
|
400
|
+
let x = rect.left + this.clientLeft + (parseFloat(style.paddingLeft) || 0);
|
|
401
|
+
let y = rect.top + this.clientTop + (parseFloat(style.paddingTop) || 0);
|
|
402
|
+
let doc = this.ownerDocument;
|
|
403
|
+
while (doc) {
|
|
404
|
+
let frameElement = null;
|
|
405
|
+
try {
|
|
406
|
+
frameElement = doc.defaultView && doc.defaultView.frameElement;
|
|
407
|
+
} catch {
|
|
408
|
+
frameElement = null;
|
|
409
|
+
}
|
|
410
|
+
if (!frameElement) break;
|
|
411
|
+
const frameRect = frameElement.getBoundingClientRect();
|
|
412
|
+
x += frameRect.left + frameElement.clientLeft;
|
|
413
|
+
y += frameRect.top + frameElement.clientTop;
|
|
414
|
+
doc = frameElement.ownerDocument;
|
|
415
|
+
}
|
|
416
|
+
return { x, y };
|
|
417
|
+
}`;
|
|
342
418
|
/**
|
|
343
419
|
* Run Runtime.evaluate and surface in-page exceptions as errors.
|
|
344
420
|
* CDP reports page-side throws via `exceptionDetails` on an otherwise successful
|
|
@@ -439,6 +515,82 @@ const LOCATOR_RUNTIME_HELPERS = String.raw`
|
|
|
439
515
|
return false;
|
|
440
516
|
}
|
|
441
517
|
|
|
518
|
+
const TEXT_INTERACTIVE_SELECTOR = [
|
|
519
|
+
'a[href]', 'button', 'input[type="button"]', 'input[type="submit"]', 'input[type="reset"]',
|
|
520
|
+
'[role="button"]', '[role="link"]', '[role="menuitem"]', '[role="tab"]', '[role="option"]',
|
|
521
|
+
'[role="checkbox"]', '[role="radio"]', '[role="switch"]', 'summary', 'label', 'option',
|
|
522
|
+
'[onclick]', '[tabindex]'
|
|
523
|
+
].join(',');
|
|
524
|
+
|
|
525
|
+
function normalizeText(value) {
|
|
526
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function textLabels(el, useInnerText) {
|
|
530
|
+
const labels = [];
|
|
531
|
+
const body = useInnerText && typeof el.innerText === 'string' ? el.innerText : el.textContent;
|
|
532
|
+
if (body) labels.push(normalizeText(body));
|
|
533
|
+
for (const attr of ['aria-label', 'title', 'alt', 'placeholder']) {
|
|
534
|
+
const value = el.getAttribute && el.getAttribute(attr);
|
|
535
|
+
if (value) labels.push(normalizeText(value));
|
|
536
|
+
}
|
|
537
|
+
if (el.tagName && el.tagName.toLowerCase() === 'input' && typeof el.value === 'string' && el.value) {
|
|
538
|
+
labels.push(normalizeText(el.value));
|
|
539
|
+
}
|
|
540
|
+
return labels;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function hasBox(el) {
|
|
544
|
+
const rect = el.getBoundingClientRect();
|
|
545
|
+
return rect.width > 0 && rect.height > 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// text=Foo matches by visible text (exact first, then substring);
|
|
549
|
+
// text="Foo" matches exactly. Interactive and visible elements win.
|
|
550
|
+
function queryByText(root, query) {
|
|
551
|
+
let raw = String(query).trim();
|
|
552
|
+
let exactOnly = false;
|
|
553
|
+
if (raw.length >= 2 && (raw[0] === '"' || raw[0] === "'") && raw[raw.length - 1] === raw[0]) {
|
|
554
|
+
raw = raw.slice(1, -1);
|
|
555
|
+
exactOnly = true;
|
|
556
|
+
}
|
|
557
|
+
const needle = normalizeText(raw);
|
|
558
|
+
if (!needle) return null;
|
|
559
|
+
|
|
560
|
+
function pick(candidates, useInnerText, preferSmallest) {
|
|
561
|
+
let best = null;
|
|
562
|
+
let bestScore = -1;
|
|
563
|
+
let bestSize = Infinity;
|
|
564
|
+
for (const el of candidates) {
|
|
565
|
+
const labels = textLabels(el, useInnerText);
|
|
566
|
+
let score = -1;
|
|
567
|
+
if (labels.some((label) => label === needle)) score = 2;
|
|
568
|
+
else if (!exactOnly && labels.some((label) => label.includes(needle))) score = 1;
|
|
569
|
+
if (score < 0) continue;
|
|
570
|
+
if (hasBox(el)) score += 4;
|
|
571
|
+
const size = preferSmallest ? (el.textContent || '').length : 0;
|
|
572
|
+
if (score > bestScore || (score === bestScore && size < bestSize)) {
|
|
573
|
+
best = el;
|
|
574
|
+
bestScore = score;
|
|
575
|
+
bestSize = size;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return best;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// On equal score the element with the least text is the most specific match.
|
|
582
|
+
const interactive = pick(root.querySelectorAll(TEXT_INTERACTIVE_SELECTOR), true, true);
|
|
583
|
+
if (interactive) return interactive;
|
|
584
|
+
return pick(root.querySelectorAll(root.nodeType === 9 ? 'body *' : '*'), false, true);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function querySegment(root, part) {
|
|
588
|
+
if (/^text=/i.test(part)) {
|
|
589
|
+
return queryByText(root, part.slice(5));
|
|
590
|
+
}
|
|
591
|
+
return root.querySelector(part);
|
|
592
|
+
}
|
|
593
|
+
|
|
442
594
|
function resolveLocator(locator) {
|
|
443
595
|
const parts = splitLocator(locator);
|
|
444
596
|
if (parts.length === 0) return null;
|
|
@@ -446,7 +598,7 @@ const LOCATOR_RUNTIME_HELPERS = String.raw`
|
|
|
446
598
|
let root = document;
|
|
447
599
|
let element = null;
|
|
448
600
|
for (let index = 0; index < parts.length; index += 1) {
|
|
449
|
-
element = root
|
|
601
|
+
element = querySegment(root, parts[index]);
|
|
450
602
|
if (!element) return null;
|
|
451
603
|
|
|
452
604
|
if (index === parts.length - 1) {
|
|
@@ -502,7 +654,7 @@ const LOCATOR_RUNTIME_HELPERS = String.raw`
|
|
|
502
654
|
if (lastError) {
|
|
503
655
|
throw lastError;
|
|
504
656
|
}
|
|
505
|
-
throw new Error('
|
|
657
|
+
throw new Error('Element not found: ' + locators.join(' | '));
|
|
506
658
|
}
|
|
507
659
|
|
|
508
660
|
function ensureInView(element) {
|
|
@@ -749,7 +901,7 @@ function buildKeyDispatchPayload(inputKey) {
|
|
|
749
901
|
};
|
|
750
902
|
}
|
|
751
903
|
/** Connection pool configuration */
|
|
752
|
-
const CONNECTION_IDLE_TIMEOUT_MS =
|
|
904
|
+
const CONNECTION_IDLE_TIMEOUT_MS = 3e5;
|
|
753
905
|
const CONNECTION_CLEANUP_INTERVAL_MS = 6e4;
|
|
754
906
|
/**
|
|
755
907
|
* True when the error means the socket was closed/not-open — i.e. the request
|
|
@@ -765,6 +917,7 @@ var ChromeCdpBrowserController = class {
|
|
|
765
917
|
baseDir;
|
|
766
918
|
connectionFactory;
|
|
767
919
|
connections = /* @__PURE__ */ new Map();
|
|
920
|
+
pendingConnections = /* @__PURE__ */ new Map();
|
|
768
921
|
cleanupInterval;
|
|
769
922
|
/**
|
|
770
923
|
* User-agent override resolved at target init, keyed by target ws URL.
|
|
@@ -793,17 +946,27 @@ var ChromeCdpBrowserController = class {
|
|
|
793
946
|
cached.lastUsedAt = Date.now();
|
|
794
947
|
return cached.conn;
|
|
795
948
|
}
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
conn
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
949
|
+
const pending = this.pendingConnections.get(targetWsUrl);
|
|
950
|
+
if (pending) return pending;
|
|
951
|
+
const p = (async () => {
|
|
952
|
+
const conn = await this.connectionFactory(targetWsUrl);
|
|
953
|
+
await this.applySessionOverrides(conn, targetWsUrl);
|
|
954
|
+
this.connections.set(targetWsUrl, {
|
|
955
|
+
conn,
|
|
956
|
+
enabled: {
|
|
957
|
+
page: false,
|
|
958
|
+
runtime: false
|
|
959
|
+
},
|
|
960
|
+
lastUsedAt: Date.now()
|
|
961
|
+
});
|
|
962
|
+
return conn;
|
|
963
|
+
})();
|
|
964
|
+
this.pendingConnections.set(targetWsUrl, p);
|
|
965
|
+
try {
|
|
966
|
+
return await p;
|
|
967
|
+
} finally {
|
|
968
|
+
this.pendingConnections.delete(targetWsUrl);
|
|
969
|
+
}
|
|
807
970
|
}
|
|
808
971
|
/**
|
|
809
972
|
* Re-apply session-scoped stealth patches + UA override on a fresh connection.
|
|
@@ -883,9 +1046,11 @@ var ChromeCdpBrowserController = class {
|
|
|
883
1046
|
if (userProfileDir === "default") profileDir = resolveDefaultProfileDir();
|
|
884
1047
|
else if (userProfileDir) profileDir = userProfileDir;
|
|
885
1048
|
else profileDir = path.join(this.baseDir, "profiles", sessionId);
|
|
886
|
-
fs.mkdirSync(profileDir, { recursive: true });
|
|
1049
|
+
if (userProfileDir) fs.mkdirSync(profileDir, { recursive: true });
|
|
1050
|
+
else ensurePrivateDir(profileDir);
|
|
887
1051
|
const lockFile = path.join(profileDir, "SingletonLock");
|
|
888
|
-
|
|
1052
|
+
const lock = isProfileLocked(profileDir);
|
|
1053
|
+
if (lock.locked) throw new Error(`Chrome profile is already in use (lock file exists: ${lockFile}${lock.pid ? `, pid ${lock.pid}` : ""}). Quit the running Chrome instance first, or use --cdp-url to connect to it instead.`);
|
|
889
1054
|
const launchAttempts = headless ? [{ headless: true }] : [{ headless: false }, { headless: true }];
|
|
890
1055
|
let lastError;
|
|
891
1056
|
for (const attempt of launchAttempts) {
|
|
@@ -948,6 +1113,182 @@ var ChromeCdpBrowserController = class {
|
|
|
948
1113
|
return await fn(conn);
|
|
949
1114
|
}
|
|
950
1115
|
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Split a composed locator at the first iframe whose document the page
|
|
1118
|
+
* cannot reach (cross-origin). Returns null when no such boundary exists.
|
|
1119
|
+
*/
|
|
1120
|
+
async splitAtCrossOriginFrame(conn, locator) {
|
|
1121
|
+
if (!locator.includes(">>>")) return null;
|
|
1122
|
+
return (await evaluateOnPage(conn, {
|
|
1123
|
+
expression: buildLocatorRuntimeExpression("locator", locator, `
|
|
1124
|
+
const parts = splitLocator(locator);
|
|
1125
|
+
let root = document;
|
|
1126
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
1127
|
+
const el = querySegment(root, parts[index]);
|
|
1128
|
+
if (!el) return null;
|
|
1129
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
1130
|
+
if (tag === 'iframe' || tag === 'frame') {
|
|
1131
|
+
let doc = null;
|
|
1132
|
+
try {
|
|
1133
|
+
doc = el.contentDocument;
|
|
1134
|
+
} catch {
|
|
1135
|
+
doc = null;
|
|
1136
|
+
}
|
|
1137
|
+
if (!doc) {
|
|
1138
|
+
return {
|
|
1139
|
+
frame: parts.slice(0, index + 1).join(LOCATOR_SEPARATOR),
|
|
1140
|
+
rest: parts.slice(index + 1).join(LOCATOR_SEPARATOR),
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
root = doc;
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (el.shadowRoot) {
|
|
1147
|
+
root = el.shadowRoot;
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
return null;
|
|
1153
|
+
`),
|
|
1154
|
+
returnByValue: true,
|
|
1155
|
+
awaitPromise: true
|
|
1156
|
+
})).result.value ?? null;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Work out which frame a payload targets. An explicit `frame` wins;
|
|
1160
|
+
* otherwise a composed selector crossing a cross-origin iframe is split.
|
|
1161
|
+
*/
|
|
1162
|
+
async resolveFrameTarget(conn, payload) {
|
|
1163
|
+
if (payload.frame) return {
|
|
1164
|
+
frame: payload.frame,
|
|
1165
|
+
payload
|
|
1166
|
+
};
|
|
1167
|
+
if (!payload.selector) return { payload };
|
|
1168
|
+
const split = await this.splitAtCrossOriginFrame(conn, payload.selector);
|
|
1169
|
+
if (!split) return { payload };
|
|
1170
|
+
const prefix = split.frame + " >>> ";
|
|
1171
|
+
const fallbackSelectors = payload.fallbackSelectors?.map((fallback) => fallback.startsWith(prefix) ? fallback.slice(prefix.length) : fallback);
|
|
1172
|
+
return {
|
|
1173
|
+
frame: split.frame,
|
|
1174
|
+
payload: {
|
|
1175
|
+
...payload,
|
|
1176
|
+
selector: split.rest,
|
|
1177
|
+
fallbackSelectors
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Open an evaluation scope inside an iframe. Same-process frames get an
|
|
1183
|
+
* isolated world on the page connection; out-of-process (cross-site) frames
|
|
1184
|
+
* get their own CDP connection to the frame target. Input events still go
|
|
1185
|
+
* to the page connection, translated by `getOffset()`.
|
|
1186
|
+
*/
|
|
1187
|
+
async openFrameScope(conn, targetWsUrl, frameLocator) {
|
|
1188
|
+
const objectId = (await evaluateOnPage(conn, {
|
|
1189
|
+
expression: buildLocatorRuntimeExpression("frameLocator", frameLocator, `
|
|
1190
|
+
const el = resolveLocator(frameLocator);
|
|
1191
|
+
if (!el) throw new Error('Frame not found: ' + frameLocator);
|
|
1192
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
1193
|
+
if (tag !== 'iframe' && tag !== 'frame') {
|
|
1194
|
+
throw new Error('Frame locator does not match an iframe: ' + frameLocator);
|
|
1195
|
+
}
|
|
1196
|
+
ensureInView(el);
|
|
1197
|
+
return el;
|
|
1198
|
+
`),
|
|
1199
|
+
awaitPromise: true
|
|
1200
|
+
})).result.objectId;
|
|
1201
|
+
if (!objectId) throw new Error(`Frame not found: ${frameLocator}`);
|
|
1202
|
+
const releaseObject = () => {
|
|
1203
|
+
conn.send("Runtime.releaseObject", { objectId }).catch(() => {});
|
|
1204
|
+
};
|
|
1205
|
+
try {
|
|
1206
|
+
const frameId = (await conn.send("DOM.describeNode", { objectId })).node?.frameId;
|
|
1207
|
+
if (!frameId) throw new Error(`Frame has no document yet: ${frameLocator}`);
|
|
1208
|
+
const getOffset = async () => {
|
|
1209
|
+
return (await conn.send("Runtime.callFunctionOn", {
|
|
1210
|
+
objectId,
|
|
1211
|
+
functionDeclaration: FRAME_CONTENT_OFFSET_FUNCTION,
|
|
1212
|
+
returnByValue: true
|
|
1213
|
+
})).result.value ?? {
|
|
1214
|
+
x: 0,
|
|
1215
|
+
y: 0
|
|
1216
|
+
};
|
|
1217
|
+
};
|
|
1218
|
+
let world = null;
|
|
1219
|
+
try {
|
|
1220
|
+
world = await conn.send("Page.createIsolatedWorld", {
|
|
1221
|
+
frameId,
|
|
1222
|
+
worldName: "agentic-browser"
|
|
1223
|
+
});
|
|
1224
|
+
} catch {
|
|
1225
|
+
world = null;
|
|
1226
|
+
}
|
|
1227
|
+
if (world?.executionContextId !== void 0) {
|
|
1228
|
+
const contextId = world.executionContextId;
|
|
1229
|
+
return {
|
|
1230
|
+
client: {
|
|
1231
|
+
send: (method, params, timeoutMs) => conn.send(method, method === "Runtime.evaluate" ? {
|
|
1232
|
+
...params,
|
|
1233
|
+
contextId
|
|
1234
|
+
} : params, timeoutMs),
|
|
1235
|
+
waitForEvent: (method, timeoutMs) => conn.waitForEvent(method, timeoutMs),
|
|
1236
|
+
close: () => {}
|
|
1237
|
+
},
|
|
1238
|
+
getOffset,
|
|
1239
|
+
release: releaseObject
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
const frameWsUrl = targetWsUrl.replace(/\/devtools\/page\/[^/?#]+/, `/devtools/page/${frameId}`);
|
|
1243
|
+
if (frameWsUrl === targetWsUrl) throw new Error(`Cannot derive a frame target URL from ${targetWsUrl}`);
|
|
1244
|
+
let frameConn;
|
|
1245
|
+
let lastError;
|
|
1246
|
+
const listUrl = targetWsUrl.replace(/^ws(s?):\/\/([^/]+)\/.*$/, "http$1://$2/json/list");
|
|
1247
|
+
for (let attempt = 0; attempt < 4 && !frameConn; attempt++) {
|
|
1248
|
+
if (attempt > 0) {
|
|
1249
|
+
await getJson(listUrl).catch(() => {});
|
|
1250
|
+
if (attempt > 1) await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
frameConn = await this.connectionFactory(frameWsUrl);
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
lastError = error;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (!frameConn) throw new Error(`Unable to attach to cross-origin frame ${frameLocator}: ${lastError?.message}`, { cause: lastError });
|
|
1259
|
+
const attached = frameConn;
|
|
1260
|
+
return {
|
|
1261
|
+
client: attached,
|
|
1262
|
+
getOffset,
|
|
1263
|
+
release: () => {
|
|
1264
|
+
releaseObject();
|
|
1265
|
+
try {
|
|
1266
|
+
attached.close();
|
|
1267
|
+
} catch {}
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
releaseObject();
|
|
1272
|
+
throw error;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
/** Run `fn` against the frame a payload targets (the page itself when none). */
|
|
1276
|
+
async withFrameScope(conn, targetWsUrl, payload, fn) {
|
|
1277
|
+
const target = await this.resolveFrameTarget(conn, payload);
|
|
1278
|
+
if (!target.frame) return await fn(conn, payload, async () => ({
|
|
1279
|
+
x: 0,
|
|
1280
|
+
y: 0
|
|
1281
|
+
}));
|
|
1282
|
+
const scope = await this.openFrameScope(conn, targetWsUrl, target.frame);
|
|
1283
|
+
try {
|
|
1284
|
+
return await fn(scope.client, {
|
|
1285
|
+
...target.payload,
|
|
1286
|
+
frame: void 0
|
|
1287
|
+
}, scope.getOffset);
|
|
1288
|
+
} finally {
|
|
1289
|
+
scope.release();
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
951
1292
|
async navigate(targetWsUrl, url) {
|
|
952
1293
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
953
1294
|
await this.ensureEnabled(targetWsUrl);
|
|
@@ -1078,6 +1419,40 @@ var ChromeCdpBrowserController = class {
|
|
|
1078
1419
|
if (value !== "ready" && value !== "cleared") throw new Error("Unable to resolve type target");
|
|
1079
1420
|
return value;
|
|
1080
1421
|
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Arm a wait for a main-frame navigation BEFORE the click is dispatched, so
|
|
1424
|
+
* the start event cannot be missed. Resolves "navigated" once the main frame
|
|
1425
|
+
* started loading and then committed/loaded (each stage timeout-tolerant),
|
|
1426
|
+
* or "none" when no main-frame load started within 500 ms.
|
|
1427
|
+
*/
|
|
1428
|
+
async armMainFrameNavigationWait(conn) {
|
|
1429
|
+
const matching = conn.waitForEventMatching?.bind(conn);
|
|
1430
|
+
if (!matching) return { result: (async () => {
|
|
1431
|
+
try {
|
|
1432
|
+
await conn.waitForEvent("Page.frameNavigated", 50);
|
|
1433
|
+
} catch {
|
|
1434
|
+
return "none";
|
|
1435
|
+
}
|
|
1436
|
+
try {
|
|
1437
|
+
await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
|
|
1438
|
+
} catch {}
|
|
1439
|
+
return "navigated";
|
|
1440
|
+
})() };
|
|
1441
|
+
let mainFrameId;
|
|
1442
|
+
try {
|
|
1443
|
+
mainFrameId = (await conn.send("Page.getFrameTree")).frameTree?.frame?.id;
|
|
1444
|
+
} catch {}
|
|
1445
|
+
const isMain = (params) => mainFrameId === void 0 || params.frameId === mainFrameId;
|
|
1446
|
+
const started = matching("Page.frameStartedLoading", isMain, 500).then(() => true, () => false);
|
|
1447
|
+
return { result: (async () => {
|
|
1448
|
+
if (!await started) return "none";
|
|
1449
|
+
const navigated = matching("Page.frameNavigated", (params) => !params.frame?.parentId, 5e3).catch(() => void 0);
|
|
1450
|
+
const loaded = Promise.race([matching("Page.frameStoppedLoading", isMain, 5e3), matching("Page.loadEventFired", () => true, 5e3)]).catch(() => void 0);
|
|
1451
|
+
await navigated;
|
|
1452
|
+
await loaded;
|
|
1453
|
+
return "navigated";
|
|
1454
|
+
})() };
|
|
1455
|
+
}
|
|
1081
1456
|
async dispatchMouseClick(conn, x, y) {
|
|
1082
1457
|
await conn.send("Input.dispatchMouseEvent", {
|
|
1083
1458
|
type: "mouseMoved",
|
|
@@ -1175,36 +1550,36 @@ var ChromeCdpBrowserController = class {
|
|
|
1175
1550
|
if (payload.action === "dialog") return await this.handleDialogAction(targetWsUrl, payload);
|
|
1176
1551
|
if (payload.action === "click" || payload.action === "hover" || payload.action === "type" || payload.action === "press") return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1177
1552
|
await this.ensureEnabled(targetWsUrl);
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
await
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1553
|
+
return await this.withFrameScope(conn, targetWsUrl, payload, async (client, scoped, getOffset) => {
|
|
1554
|
+
if (scoped.action === "click") {
|
|
1555
|
+
const local = await this.preparePointerTarget(client, scoped);
|
|
1556
|
+
const offset = await getOffset();
|
|
1557
|
+
const nav = await this.armMainFrameNavigationWait(conn);
|
|
1558
|
+
await this.dispatchMouseClick(conn, local.x + offset.x, local.y + offset.y);
|
|
1559
|
+
await nav.result;
|
|
1560
|
+
return "clicked";
|
|
1561
|
+
}
|
|
1562
|
+
if (scoped.action === "hover") {
|
|
1563
|
+
const local = await this.preparePointerTarget(client, scoped);
|
|
1564
|
+
const offset = await getOffset();
|
|
1565
|
+
await conn.send("Input.dispatchMouseEvent", {
|
|
1566
|
+
type: "mouseMoved",
|
|
1567
|
+
x: local.x + offset.x,
|
|
1568
|
+
y: local.y + offset.y,
|
|
1569
|
+
button: "none"
|
|
1570
|
+
});
|
|
1571
|
+
return "hovered";
|
|
1572
|
+
}
|
|
1573
|
+
if (scoped.action === "press") {
|
|
1574
|
+
await this.focusTarget(client, scoped);
|
|
1575
|
+
await this.dispatchKeyPress(conn, scoped.key);
|
|
1576
|
+
return "pressed";
|
|
1577
|
+
}
|
|
1578
|
+
if (await this.prepareTypeTarget(client, scoped) === "ready") await conn.send("Input.insertText", { text: scoped.text ?? "" });
|
|
1579
|
+
return "typed";
|
|
1580
|
+
});
|
|
1206
1581
|
}, "connection");
|
|
1207
|
-
const
|
|
1582
|
+
const buildExpression = (scoped) => buildLocatorRuntimeExpression("payload", scoped, `
|
|
1208
1583
|
if (payload.action === 'waitFor') {
|
|
1209
1584
|
const timeout = payload.timeoutMs ?? 2000;
|
|
1210
1585
|
const started = Date.now();
|
|
@@ -1251,11 +1626,17 @@ var ChromeCdpBrowserController = class {
|
|
|
1251
1626
|
`);
|
|
1252
1627
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1253
1628
|
await this.ensureEnabled(targetWsUrl);
|
|
1254
|
-
return
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1629
|
+
return await this.withFrameScope(conn, targetWsUrl, payload, async (client, scoped) => {
|
|
1630
|
+
return (await evaluateOnPage(client, {
|
|
1631
|
+
expression: scoped.action === "evaluate" ? `(async () => {
|
|
1632
|
+
const __agenticResult = await (${scoped.text ?? "undefined"}
|
|
1633
|
+
);
|
|
1634
|
+
return typeof __agenticResult === 'string' ? __agenticResult : JSON.stringify(__agenticResult);
|
|
1635
|
+
})()` : buildExpression(scoped),
|
|
1636
|
+
returnByValue: true,
|
|
1637
|
+
awaitPromise: true
|
|
1638
|
+
})).result.value ?? "";
|
|
1639
|
+
});
|
|
1259
1640
|
}, "connection");
|
|
1260
1641
|
}
|
|
1261
1642
|
async screenshot(targetWsUrl, options) {
|
|
@@ -1291,6 +1672,66 @@ var ChromeCdpBrowserController = class {
|
|
|
1291
1672
|
})).result.value ?? "";
|
|
1292
1673
|
});
|
|
1293
1674
|
}
|
|
1675
|
+
async getPageState(targetWsUrl) {
|
|
1676
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1677
|
+
return (await evaluateOnPage(conn, {
|
|
1678
|
+
expression: "({ url: location.href, title: document.title, readyState: document.readyState })",
|
|
1679
|
+
returnByValue: true
|
|
1680
|
+
})).result.value ?? {
|
|
1681
|
+
url: "",
|
|
1682
|
+
title: "",
|
|
1683
|
+
readyState: "loading"
|
|
1684
|
+
};
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
async listTabs(cdpUrl) {
|
|
1688
|
+
return (await getJson(`${cdpUrl}/json/list`)).filter((target) => target.type === "page" && target.webSocketDebuggerUrl).map((target) => ({
|
|
1689
|
+
id: target.id,
|
|
1690
|
+
url: target.url ?? "",
|
|
1691
|
+
title: target.title ?? "",
|
|
1692
|
+
targetWsUrl: target.webSocketDebuggerUrl
|
|
1693
|
+
}));
|
|
1694
|
+
}
|
|
1695
|
+
async openTab(cdpUrl) {
|
|
1696
|
+
const endpoint = `${cdpUrl}/json/new?${encodeURIComponent("about:blank")}`;
|
|
1697
|
+
let created;
|
|
1698
|
+
for (const method of ["PUT", "GET"]) try {
|
|
1699
|
+
const response = await fetch(endpoint, {
|
|
1700
|
+
method,
|
|
1701
|
+
signal: AbortSignal.timeout(5e3)
|
|
1702
|
+
});
|
|
1703
|
+
if (!response.ok) continue;
|
|
1704
|
+
created = await response.json();
|
|
1705
|
+
if (created.webSocketDebuggerUrl) break;
|
|
1706
|
+
} catch {}
|
|
1707
|
+
if (!created?.webSocketDebuggerUrl) throw new Error(`Unable to open a new tab via ${cdpUrl}/json/new`);
|
|
1708
|
+
const targetWsUrl = created.webSocketDebuggerUrl;
|
|
1709
|
+
const host = new URL(cdpUrl).host;
|
|
1710
|
+
let userAgent;
|
|
1711
|
+
for (const [wsUrl, ua] of this.userAgentOverrides) if (new URL(wsUrl).host === host) {
|
|
1712
|
+
userAgent = ua;
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
const appliedUa = await initTarget(targetWsUrl, userAgent);
|
|
1716
|
+
if (appliedUa) this.userAgentOverrides.set(targetWsUrl, appliedUa);
|
|
1717
|
+
return {
|
|
1718
|
+
id: created.id,
|
|
1719
|
+
url: created.url ?? "about:blank",
|
|
1720
|
+
title: created.title ?? "",
|
|
1721
|
+
targetWsUrl
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
async activateTab(cdpUrl, tabId) {
|
|
1725
|
+
if (!(await fetch(`${cdpUrl}/json/activate/${encodeURIComponent(tabId)}`, { signal: AbortSignal.timeout(5e3) })).ok) throw new Error(`Tab not found: ${tabId}`);
|
|
1726
|
+
}
|
|
1727
|
+
async closeTab(cdpUrl, tabId) {
|
|
1728
|
+
const tab = (await this.listTabs(cdpUrl)).find((candidate) => candidate.id === tabId);
|
|
1729
|
+
if (!tab) throw new Error(`Tab not found: ${tabId}`);
|
|
1730
|
+
this.dropConnection(tab.targetWsUrl);
|
|
1731
|
+
this.userAgentOverrides.delete(tab.targetWsUrl);
|
|
1732
|
+
const response = await fetch(`${cdpUrl}/json/close/${encodeURIComponent(tabId)}`, { signal: AbortSignal.timeout(5e3) });
|
|
1733
|
+
if (!response.ok) throw new Error(`Unable to close tab ${tabId}: HTTP ${response.status}`);
|
|
1734
|
+
}
|
|
1294
1735
|
async getContent(targetWsUrl, options) {
|
|
1295
1736
|
if (options.mode === "summary") return await this.getSummaryContent(targetWsUrl, options);
|
|
1296
1737
|
if (options.mode === "a11y") {
|
|
@@ -1676,6 +2117,10 @@ var ChromeCdpBrowserController = class {
|
|
|
1676
2117
|
const limit = options.limit ?? 50;
|
|
1677
2118
|
const scopeSelector = options.selector;
|
|
1678
2119
|
const roleFilter = options.roles ? new Set(options.roles) : null;
|
|
2120
|
+
const hrefFilter = options.href ? String(options.href).toLowerCase() : '';
|
|
2121
|
+
const textFilter = options.text
|
|
2122
|
+
? String(options.text).replace(/\\s+/g, ' ').trim().toLowerCase()
|
|
2123
|
+
: '';
|
|
1679
2124
|
const candidateSelector = [
|
|
1680
2125
|
'a[href]',
|
|
1681
2126
|
'button',
|
|
@@ -1947,6 +2392,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1947
2392
|
}
|
|
1948
2393
|
|
|
1949
2394
|
const results = [];
|
|
2395
|
+
const crossOriginFrames = [];
|
|
1950
2396
|
let totalFound = 0;
|
|
1951
2397
|
|
|
1952
2398
|
function maybeRecordElement(root, path, el) {
|
|
@@ -1964,6 +2410,25 @@ var ChromeCdpBrowserController = class {
|
|
|
1964
2410
|
const visible = isVisible(el);
|
|
1965
2411
|
if (visibleOnly && !visible) return;
|
|
1966
2412
|
|
|
2413
|
+
if (hrefFilter) {
|
|
2414
|
+
const href = typeof el.href === 'string' ? el.href : el.getAttribute('href');
|
|
2415
|
+
if (!href || !String(href).toLowerCase().includes(hrefFilter)) return;
|
|
2416
|
+
}
|
|
2417
|
+
if (textFilter) {
|
|
2418
|
+
const haystack = [
|
|
2419
|
+
el.innerText || el.textContent || '',
|
|
2420
|
+
el.getAttribute('aria-label'),
|
|
2421
|
+
el.getAttribute('placeholder'),
|
|
2422
|
+
el.getAttribute('title'),
|
|
2423
|
+
typeof el.value === 'string' ? el.value : '',
|
|
2424
|
+
]
|
|
2425
|
+
.filter(Boolean)
|
|
2426
|
+
.join(' ')
|
|
2427
|
+
.replace(/\\s+/g, ' ')
|
|
2428
|
+
.toLowerCase();
|
|
2429
|
+
if (!haystack.includes(textFilter)) return;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
1967
2432
|
totalFound += 1;
|
|
1968
2433
|
if (results.length >= limit) return;
|
|
1969
2434
|
|
|
@@ -2007,12 +2472,21 @@ var ChromeCdpBrowserController = class {
|
|
|
2007
2472
|
}
|
|
2008
2473
|
|
|
2009
2474
|
if (current.tagName && current.tagName.toLowerCase() === 'iframe') {
|
|
2475
|
+
let frameDocument = null;
|
|
2010
2476
|
try {
|
|
2011
|
-
|
|
2012
|
-
walkRoot(current.contentDocument, path.concat(buildSelector(root, current)));
|
|
2013
|
-
}
|
|
2477
|
+
frameDocument = current.contentDocument;
|
|
2014
2478
|
} catch {
|
|
2015
|
-
|
|
2479
|
+
frameDocument = null;
|
|
2480
|
+
}
|
|
2481
|
+
if (frameDocument) {
|
|
2482
|
+
walkRoot(frameDocument, path.concat(buildSelector(root, current)));
|
|
2483
|
+
} else if (crossOriginFrames.length < 20 && isVisible(current)) {
|
|
2484
|
+
// Cross-origin: not walkable from here, report it so callers can pass it as frame.
|
|
2485
|
+
const info = { selector: composeLocator(path, buildSelector(root, current)) };
|
|
2486
|
+
const title = current.getAttribute('title');
|
|
2487
|
+
if (title) info.title = title.slice(0, 80);
|
|
2488
|
+
if (current.src) info.src = String(current.src).slice(0, 200);
|
|
2489
|
+
crossOriginFrames.push(info);
|
|
2016
2490
|
}
|
|
2017
2491
|
}
|
|
2018
2492
|
|
|
@@ -2023,7 +2497,9 @@ var ChromeCdpBrowserController = class {
|
|
|
2023
2497
|
}
|
|
2024
2498
|
|
|
2025
2499
|
walkRoot(document, []);
|
|
2026
|
-
|
|
2500
|
+
const output = { elements: results, totalFound, truncated: totalFound > results.length };
|
|
2501
|
+
if (crossOriginFrames.length) output.crossOriginFrames = crossOriginFrames;
|
|
2502
|
+
return output;
|
|
2027
2503
|
`);
|
|
2028
2504
|
const emptyResult = {
|
|
2029
2505
|
elements: [],
|
|
@@ -2037,12 +2513,31 @@ var ChromeCdpBrowserController = class {
|
|
|
2037
2513
|
};
|
|
2038
2514
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
2039
2515
|
await this.ensureEnabled(targetWsUrl);
|
|
2040
|
-
const
|
|
2516
|
+
const evaluate = async (client) => extract(await evaluateOnPage(client, {
|
|
2041
2517
|
expression,
|
|
2042
2518
|
returnByValue: true,
|
|
2043
2519
|
awaitPromise: true
|
|
2044
|
-
});
|
|
2045
|
-
return
|
|
2520
|
+
}));
|
|
2521
|
+
if (!options.frame) return await evaluate(conn);
|
|
2522
|
+
const scope = await this.openFrameScope(conn, targetWsUrl, options.frame);
|
|
2523
|
+
try {
|
|
2524
|
+
const inner = await evaluate(scope.client);
|
|
2525
|
+
const withPrefix = (selector) => `${options.frame} >>> ${selector}`;
|
|
2526
|
+
return {
|
|
2527
|
+
...inner,
|
|
2528
|
+
elements: inner.elements.map((element) => ({
|
|
2529
|
+
...element,
|
|
2530
|
+
selector: withPrefix(element.selector),
|
|
2531
|
+
fallbackSelectors: element.fallbackSelectors?.map(withPrefix)
|
|
2532
|
+
})),
|
|
2533
|
+
crossOriginFrames: inner.crossOriginFrames?.map((frame) => ({
|
|
2534
|
+
...frame,
|
|
2535
|
+
selector: withPrefix(frame.selector)
|
|
2536
|
+
}))
|
|
2537
|
+
};
|
|
2538
|
+
} finally {
|
|
2539
|
+
scope.release();
|
|
2540
|
+
}
|
|
2046
2541
|
});
|
|
2047
2542
|
}
|
|
2048
2543
|
/**
|
|
@@ -2056,22 +2551,33 @@ var ChromeCdpBrowserController = class {
|
|
|
2056
2551
|
* Returns silently on failure — never blocks navigation.
|
|
2057
2552
|
*/
|
|
2058
2553
|
async dismissCookieBanner(targetWsUrl) {
|
|
2554
|
+
const notes = /* @__PURE__ */ new Set();
|
|
2059
2555
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2060
2556
|
try {
|
|
2061
2557
|
const domResult = await this.dismissViaDom(targetWsUrl);
|
|
2062
2558
|
if (domResult.dismissed) return domResult;
|
|
2063
|
-
} catch {
|
|
2559
|
+
} catch (error) {
|
|
2560
|
+
notes.add(`dom: ${error.message}`);
|
|
2561
|
+
}
|
|
2064
2562
|
try {
|
|
2065
2563
|
const iframeResult = await this.dismissViaIframe(targetWsUrl);
|
|
2066
2564
|
if (iframeResult.dismissed) return iframeResult;
|
|
2067
|
-
|
|
2565
|
+
if (iframeResult.detail) notes.add(iframeResult.detail);
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
notes.add(`iframe: ${error.message}`);
|
|
2568
|
+
}
|
|
2068
2569
|
try {
|
|
2069
2570
|
const a11yResult = await this.dismissViaA11y(targetWsUrl);
|
|
2070
2571
|
if (a11yResult.dismissed) return a11yResult;
|
|
2071
|
-
} catch {
|
|
2572
|
+
} catch (error) {
|
|
2573
|
+
notes.add(`a11y: ${error.message}`);
|
|
2574
|
+
}
|
|
2072
2575
|
if (attempt === 0) await new Promise((resolve) => setTimeout(resolve, 800));
|
|
2073
2576
|
}
|
|
2074
|
-
return {
|
|
2577
|
+
return {
|
|
2578
|
+
dismissed: false,
|
|
2579
|
+
detail: notes.size ? [...notes].join("; ") : "no consent banner or accept button found (page, consent iframes, a11y tree)"
|
|
2580
|
+
};
|
|
2075
2581
|
}
|
|
2076
2582
|
async dismissViaDom(targetWsUrl) {
|
|
2077
2583
|
const expression = `(() => {
|
|
@@ -2326,7 +2832,42 @@ var ChromeCdpBrowserController = class {
|
|
|
2326
2832
|
* These banners render inside an iframe that needs to be accessed separately.
|
|
2327
2833
|
*/
|
|
2328
2834
|
async dismissViaIframe(targetWsUrl) {
|
|
2835
|
+
const innerSearch = `function (doc) {
|
|
2836
|
+
const innerSelectors = [
|
|
2837
|
+
// Sourcepoint specific
|
|
2838
|
+
'button.sp_choice_type_11', // Accept all
|
|
2839
|
+
'button.sp_choice_type_ACCEPT_ALL', // Accept all variant
|
|
2840
|
+
'button.sp_choice_type_SAVE_AND_EXIT', // Save & exit
|
|
2841
|
+
'.priv-save-btn', // Sourcepoint frame
|
|
2842
|
+
'.message-button', // Generic Sourcepoint
|
|
2843
|
+
// Title-based
|
|
2844
|
+
'button[title*="Accept"]', 'button[title*="accept"]',
|
|
2845
|
+
'button[title*="Agree"]', 'button[title*="agree"]',
|
|
2846
|
+
'button[title*="Allow"]',
|
|
2847
|
+
'button[title*="Akzeptieren"]', 'button[title*="akzeptieren"]',
|
|
2848
|
+
'button[title*="Zustimmen"]',
|
|
2849
|
+
];
|
|
2850
|
+
for (const innerSel of innerSelectors) {
|
|
2851
|
+
const btn = doc.querySelector(innerSel);
|
|
2852
|
+
if (btn && btn.offsetWidth > 0) {
|
|
2853
|
+
btn.click();
|
|
2854
|
+
return { method: 'iframe', detail: innerSel };
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
// Text-based fallback inside the iframe
|
|
2858
|
+
const buttons = doc.querySelectorAll('button, [role="button"]');
|
|
2859
|
+
for (const btn of buttons) {
|
|
2860
|
+
const text = (btn.textContent || '').trim();
|
|
2861
|
+
if (text.length > 60) continue;
|
|
2862
|
+
if (/accept|agree|allow|akzeptieren|zustimmen|einwilligen|accepter|aceptar/i.test(text)) {
|
|
2863
|
+
btn.click();
|
|
2864
|
+
return { method: 'iframe-text', detail: text };
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
return null;
|
|
2868
|
+
}`;
|
|
2329
2869
|
const expression = `(() => {
|
|
2870
|
+
const clickAccept = ${innerSearch};
|
|
2330
2871
|
// Known consent iframe selectors
|
|
2331
2872
|
const iframeSelectors = [
|
|
2332
2873
|
'iframe[id^="sp_message_iframe"]', // Sourcepoint (dynamic ID suffix)
|
|
@@ -2347,62 +2888,69 @@ var ChromeCdpBrowserController = class {
|
|
|
2347
2888
|
'iframe[src*="consentmanager"]',
|
|
2348
2889
|
];
|
|
2349
2890
|
|
|
2891
|
+
// Cross-origin consent frames are handed back so the caller can attach to them.
|
|
2892
|
+
const crossOrigin = [];
|
|
2350
2893
|
for (const sel of iframeSelectors) {
|
|
2894
|
+
const iframe = document.querySelector(sel);
|
|
2895
|
+
if (!iframe) continue;
|
|
2896
|
+
let doc = null;
|
|
2351
2897
|
try {
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
'.priv-save-btn', // Sourcepoint frame
|
|
2364
|
-
'.message-button', // Generic Sourcepoint
|
|
2365
|
-
// Title-based
|
|
2366
|
-
'button[title*="Accept"]', 'button[title*="accept"]',
|
|
2367
|
-
'button[title*="Agree"]', 'button[title*="agree"]',
|
|
2368
|
-
'button[title*="Allow"]',
|
|
2369
|
-
'button[title*="Akzeptieren"]', 'button[title*="akzeptieren"]',
|
|
2370
|
-
'button[title*="Zustimmen"]',
|
|
2371
|
-
];
|
|
2372
|
-
for (const innerSel of innerSelectors) {
|
|
2373
|
-
const btn = doc.querySelector(innerSel);
|
|
2374
|
-
if (btn && btn.offsetWidth > 0) {
|
|
2375
|
-
btn.click();
|
|
2376
|
-
return JSON.stringify({ dismissed: true, method: 'iframe', detail: sel + ' > ' + innerSel });
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
// Text-based fallback inside iframe
|
|
2381
|
-
const buttons = doc.querySelectorAll('button, [role="button"]');
|
|
2382
|
-
for (const btn of buttons) {
|
|
2383
|
-
const text = (btn.textContent || '').trim();
|
|
2384
|
-
if (text.length > 60) continue;
|
|
2385
|
-
if (/accept|agree|allow|akzeptieren|zustimmen|einwilligen|accepter|aceptar/i.test(text)) {
|
|
2386
|
-
btn.click();
|
|
2387
|
-
return JSON.stringify({ dismissed: true, method: 'iframe-text', detail: text });
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
} catch { /* cross-origin iframe — skip */ }
|
|
2898
|
+
doc = iframe.contentDocument || iframe.contentWindow?.document || null;
|
|
2899
|
+
} catch {
|
|
2900
|
+
doc = null;
|
|
2901
|
+
}
|
|
2902
|
+
if (!doc) {
|
|
2903
|
+
const locator = iframe.id ? '#' + CSS.escape(iframe.id) : sel;
|
|
2904
|
+
if (!crossOrigin.includes(locator)) crossOrigin.push(locator);
|
|
2905
|
+
continue;
|
|
2906
|
+
}
|
|
2907
|
+
const hit = clickAccept(doc);
|
|
2908
|
+
if (hit) return { dismissed: true, method: hit.method, detail: sel + ' > ' + hit.detail };
|
|
2391
2909
|
}
|
|
2392
|
-
|
|
2393
|
-
return JSON.stringify({ dismissed: false });
|
|
2910
|
+
return { dismissed: false, crossOrigin };
|
|
2394
2911
|
})()`;
|
|
2395
2912
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
2396
2913
|
await this.ensureEnabled(targetWsUrl);
|
|
2397
|
-
const
|
|
2914
|
+
const value = (await evaluateOnPage(conn, {
|
|
2398
2915
|
expression,
|
|
2399
2916
|
returnByValue: true
|
|
2400
|
-
})).result.value
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2917
|
+
})).result.value;
|
|
2918
|
+
if (!value) return { dismissed: false };
|
|
2919
|
+
if (value.dismissed) return {
|
|
2920
|
+
dismissed: true,
|
|
2921
|
+
method: value.method,
|
|
2922
|
+
detail: value.detail
|
|
2923
|
+
};
|
|
2924
|
+
const notes = [];
|
|
2925
|
+
for (const frame of value.crossOrigin ?? []) {
|
|
2926
|
+
let scope;
|
|
2927
|
+
try {
|
|
2928
|
+
scope = await this.openFrameScope(conn, targetWsUrl, frame);
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
notes.push(`consent iframe ${frame}: ${error.message}`);
|
|
2931
|
+
continue;
|
|
2932
|
+
}
|
|
2933
|
+
try {
|
|
2934
|
+
const hit = (await evaluateOnPage(scope.client, {
|
|
2935
|
+
expression: `(${innerSearch})(document)`,
|
|
2936
|
+
returnByValue: true
|
|
2937
|
+
})).result.value;
|
|
2938
|
+
if (hit) return {
|
|
2939
|
+
dismissed: true,
|
|
2940
|
+
method: hit.method,
|
|
2941
|
+
detail: `${frame} >>> ${hit.detail}`
|
|
2942
|
+
};
|
|
2943
|
+
notes.push(`consent iframe ${frame}: no accept button found`);
|
|
2944
|
+
} catch (error) {
|
|
2945
|
+
notes.push(`consent iframe ${frame}: ${error.message}`);
|
|
2946
|
+
} finally {
|
|
2947
|
+
scope.release();
|
|
2948
|
+
}
|
|
2405
2949
|
}
|
|
2950
|
+
return notes.length ? {
|
|
2951
|
+
dismissed: false,
|
|
2952
|
+
detail: notes.join("; ")
|
|
2953
|
+
} : { dismissed: false };
|
|
2406
2954
|
});
|
|
2407
2955
|
}
|
|
2408
2956
|
/**
|
|
@@ -2599,6 +3147,64 @@ var MockBrowserController = class {
|
|
|
2599
3147
|
async getCurrentUrl(cdpUrl) {
|
|
2600
3148
|
return this.pages.get(cdpUrl)?.url ?? "about:blank";
|
|
2601
3149
|
}
|
|
3150
|
+
tabs = /* @__PURE__ */ new Map();
|
|
3151
|
+
tabCounter = 0;
|
|
3152
|
+
/** Tab ids are the page keys (targetWsUrl); the initial page is the cdpUrl itself. */
|
|
3153
|
+
tabIds(cdpUrl) {
|
|
3154
|
+
let ids = this.tabs.get(cdpUrl);
|
|
3155
|
+
if (!ids) {
|
|
3156
|
+
ids = this.pages.has(cdpUrl) ? [cdpUrl] : [];
|
|
3157
|
+
this.tabs.set(cdpUrl, ids);
|
|
3158
|
+
}
|
|
3159
|
+
return ids;
|
|
3160
|
+
}
|
|
3161
|
+
async getPageState(targetWsUrl) {
|
|
3162
|
+
const page = this.pages.get(targetWsUrl);
|
|
3163
|
+
if (!page) throw new Error("mock page missing");
|
|
3164
|
+
return {
|
|
3165
|
+
url: page.url,
|
|
3166
|
+
title: page.title,
|
|
3167
|
+
readyState: "complete"
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
async listTabs(cdpUrl) {
|
|
3171
|
+
return this.tabIds(cdpUrl).map((id) => {
|
|
3172
|
+
const page = this.pages.get(id);
|
|
3173
|
+
return {
|
|
3174
|
+
id,
|
|
3175
|
+
url: page?.url ?? "about:blank",
|
|
3176
|
+
title: page?.title ?? "",
|
|
3177
|
+
targetWsUrl: id
|
|
3178
|
+
};
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
async openTab(cdpUrl) {
|
|
3182
|
+
this.tabCounter += 1;
|
|
3183
|
+
const id = `${cdpUrl}/tab-${this.tabCounter}`;
|
|
3184
|
+
this.pages.set(id, {
|
|
3185
|
+
url: "about:blank",
|
|
3186
|
+
title: "about:blank",
|
|
3187
|
+
text: "",
|
|
3188
|
+
html: "<html><body></body></html>"
|
|
3189
|
+
});
|
|
3190
|
+
this.tabIds(cdpUrl).push(id);
|
|
3191
|
+
return {
|
|
3192
|
+
id,
|
|
3193
|
+
url: "about:blank",
|
|
3194
|
+
title: "about:blank",
|
|
3195
|
+
targetWsUrl: id
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
async activateTab(cdpUrl, tabId) {
|
|
3199
|
+
if (!this.tabIds(cdpUrl).includes(tabId)) throw new Error(`Tab not found: ${tabId}`);
|
|
3200
|
+
}
|
|
3201
|
+
async closeTab(cdpUrl, tabId) {
|
|
3202
|
+
const ids = this.tabIds(cdpUrl);
|
|
3203
|
+
const index = ids.indexOf(tabId);
|
|
3204
|
+
if (index === -1) throw new Error(`Tab not found: ${tabId}`);
|
|
3205
|
+
ids.splice(index, 1);
|
|
3206
|
+
this.pages.delete(tabId);
|
|
3207
|
+
}
|
|
2602
3208
|
async getContent(cdpUrl, options) {
|
|
2603
3209
|
const page = this.pages.get(cdpUrl);
|
|
2604
3210
|
if (!page) throw new Error("mock page missing");
|
|
@@ -2640,8 +3246,7 @@ var MockBrowserController = class {
|
|
|
2640
3246
|
terminate(_pid) {}
|
|
2641
3247
|
closeConnection(_targetWsUrl) {}
|
|
2642
3248
|
};
|
|
2643
|
-
|
|
2644
|
-
//#region src/lib/domain-schemas.ts
|
|
3249
|
+
// src/lib/domain-schemas.ts
|
|
2645
3250
|
const InteractActionSchema = z.enum([
|
|
2646
3251
|
"click",
|
|
2647
3252
|
"type",
|
|
@@ -2666,7 +3271,8 @@ const InteractPayloadSchema = z.object({
|
|
|
2666
3271
|
value: z.string().optional(),
|
|
2667
3272
|
scrollX: z.number().optional(),
|
|
2668
3273
|
scrollY: z.number().optional(),
|
|
2669
|
-
timeoutMs: z.number().optional()
|
|
3274
|
+
timeoutMs: z.number().optional(),
|
|
3275
|
+
frame: z.string().optional()
|
|
2670
3276
|
});
|
|
2671
3277
|
const SessionStatusSchema = z.enum([
|
|
2672
3278
|
"starting",
|
|
@@ -2747,12 +3353,11 @@ z.object({
|
|
|
2747
3353
|
createdAt: z.string().datetime(),
|
|
2748
3354
|
severity: EventSeveritySchema
|
|
2749
3355
|
});
|
|
2750
|
-
|
|
2751
|
-
//#region src/session/session-store.ts
|
|
3356
|
+
// src/session/session-store.ts
|
|
2752
3357
|
var SessionStore = class {
|
|
2753
3358
|
filePath;
|
|
2754
3359
|
constructor(baseDir) {
|
|
2755
|
-
|
|
3360
|
+
ensurePrivateDir(baseDir);
|
|
2756
3361
|
this.filePath = path.join(baseDir, "sessions.json");
|
|
2757
3362
|
if (!fs.existsSync(this.filePath)) this.write({ sessions: {} });
|
|
2758
3363
|
}
|
|
@@ -2838,12 +3443,68 @@ var SessionStore = class {
|
|
|
2838
3443
|
}
|
|
2839
3444
|
write(state) {
|
|
2840
3445
|
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
2841
|
-
|
|
3446
|
+
writePrivateFileSync(tmp, JSON.stringify(state, null, 2));
|
|
2842
3447
|
fs.renameSync(tmp, this.filePath);
|
|
2843
3448
|
}
|
|
2844
3449
|
};
|
|
2845
|
-
|
|
2846
|
-
|
|
3450
|
+
// src/session/url-policy.ts
|
|
3451
|
+
function assertNavigableUrl(raw, opts) {
|
|
3452
|
+
let parsed;
|
|
3453
|
+
try {
|
|
3454
|
+
parsed = new URL(raw);
|
|
3455
|
+
} catch {
|
|
3456
|
+
throw new Error(`Invalid URL: ${raw}`);
|
|
3457
|
+
}
|
|
3458
|
+
const { protocol } = parsed;
|
|
3459
|
+
if (protocol === "http:" || protocol === "https:") return;
|
|
3460
|
+
if (raw === "about:blank") return;
|
|
3461
|
+
if (protocol === "file:" && opts.allowFileUrls) return;
|
|
3462
|
+
throw new Error(`Navigation to ${protocol} URLs is not allowed${protocol === "file:" ? " (set AGENTIC_BROWSER_ALLOW_FILE_URLS=true to allow file: URLs)" : ""}`);
|
|
3463
|
+
}
|
|
3464
|
+
// src/memory/redact-step-payload.ts
|
|
3465
|
+
const ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
3466
|
+
"action",
|
|
3467
|
+
"selector",
|
|
3468
|
+
"fallbackSelectors",
|
|
3469
|
+
"ref",
|
|
3470
|
+
"key",
|
|
3471
|
+
"direction",
|
|
3472
|
+
"amount",
|
|
3473
|
+
"deltaY",
|
|
3474
|
+
"scrollX",
|
|
3475
|
+
"scrollY",
|
|
3476
|
+
"timeoutMs",
|
|
3477
|
+
"redacted"
|
|
3478
|
+
]);
|
|
3479
|
+
const URL_IN_TEXT = /\bhttps?:\/\/[^\s"'<>]+/gi;
|
|
3480
|
+
/** Reduce a URL to origin + pathname, dropping query string and fragment. */
|
|
3481
|
+
function stripUrl(rawUrl) {
|
|
3482
|
+
try {
|
|
3483
|
+
const parsed = new URL(rawUrl);
|
|
3484
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
3485
|
+
} catch {
|
|
3486
|
+
return;
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
/** Strip query strings and fragments from every http(s) URL embedded in free text. */
|
|
3490
|
+
function redactUrlsInText(text) {
|
|
3491
|
+
return text.replace(URL_IN_TEXT, (match) => stripUrl(match) ?? "[url]");
|
|
3492
|
+
}
|
|
3493
|
+
/** Keep only structural, non-secret keys of a step payload; typed text and values are dropped. */
|
|
3494
|
+
function redactStepPayload(payload) {
|
|
3495
|
+
const result = {};
|
|
3496
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
3497
|
+
if (key === "url") {
|
|
3498
|
+
const stripped = typeof value === "string" ? stripUrl(value) : void 0;
|
|
3499
|
+
if (stripped !== void 0) result.url = stripped;
|
|
3500
|
+
continue;
|
|
3501
|
+
}
|
|
3502
|
+
if (ALLOWED_KEYS.has(key)) result[key] = value;
|
|
3503
|
+
}
|
|
3504
|
+
if ("text" in payload || "value" in payload) result.redacted = true;
|
|
3505
|
+
return result;
|
|
3506
|
+
}
|
|
3507
|
+
// src/session/session-manager.ts
|
|
2847
3508
|
var SessionManager = class {
|
|
2848
3509
|
ctx;
|
|
2849
3510
|
browser;
|
|
@@ -2926,6 +3587,7 @@ var SessionManager = class {
|
|
|
2926
3587
|
if (input.type === "navigate") {
|
|
2927
3588
|
const url = String(input.payload.url ?? "");
|
|
2928
3589
|
if (!url) throw new Error("navigate command requires payload.url");
|
|
3590
|
+
assertNavigableUrl(url, { allowFileUrls: this.ctx.config.allowFileUrls });
|
|
2929
3591
|
const finalUrl = await this.browser.navigate(record.targetWsUrl, url);
|
|
2930
3592
|
this.store.setLastUrl(sessionId, finalUrl);
|
|
2931
3593
|
resultMessage = `Navigated to ${finalUrl}`;
|
|
@@ -3001,6 +3663,68 @@ var SessionManager = class {
|
|
|
3001
3663
|
const record = await this.ensureSession(sessionId);
|
|
3002
3664
|
return await this.browser.getCurrentUrl(record.targetWsUrl);
|
|
3003
3665
|
}
|
|
3666
|
+
async getPageState(sessionId) {
|
|
3667
|
+
const record = await this.ensureSession(sessionId);
|
|
3668
|
+
return await this.browser.getPageState(record.targetWsUrl);
|
|
3669
|
+
}
|
|
3670
|
+
async listTabs(sessionId) {
|
|
3671
|
+
const record = await this.ensureSession(sessionId);
|
|
3672
|
+
return (await this.browser.listTabs(record.cdpUrl)).map((tab) => ({
|
|
3673
|
+
...tab,
|
|
3674
|
+
active: tab.targetWsUrl === record.targetWsUrl
|
|
3675
|
+
}));
|
|
3676
|
+
}
|
|
3677
|
+
/** Points the session at another tab; later commands run there. */
|
|
3678
|
+
async switchTab(sessionId, tabId) {
|
|
3679
|
+
const record = await this.ensureSession(sessionId);
|
|
3680
|
+
const tab = (await this.browser.listTabs(record.cdpUrl)).find((t) => t.id === tabId);
|
|
3681
|
+
if (!tab) throw new Error(`Tab not found: ${tabId}`);
|
|
3682
|
+
await this.browser.activateTab(record.cdpUrl, tab.id);
|
|
3683
|
+
this.store.save({
|
|
3684
|
+
...record,
|
|
3685
|
+
targetWsUrl: tab.targetWsUrl,
|
|
3686
|
+
lastUrl: tab.url || record.lastUrl
|
|
3687
|
+
});
|
|
3688
|
+
this.recordEvent(sessionId, "lifecycle", "info", `Switched to tab ${tab.id}`);
|
|
3689
|
+
return tab;
|
|
3690
|
+
}
|
|
3691
|
+
async newTab(sessionId) {
|
|
3692
|
+
const record = await this.ensureSession(sessionId);
|
|
3693
|
+
const tab = await this.browser.openTab(record.cdpUrl);
|
|
3694
|
+
await this.browser.activateTab(record.cdpUrl, tab.id);
|
|
3695
|
+
this.store.save({
|
|
3696
|
+
...record,
|
|
3697
|
+
targetWsUrl: tab.targetWsUrl,
|
|
3698
|
+
lastUrl: tab.url
|
|
3699
|
+
});
|
|
3700
|
+
this.recordEvent(sessionId, "lifecycle", "info", `Opened tab ${tab.id}`);
|
|
3701
|
+
return tab;
|
|
3702
|
+
}
|
|
3703
|
+
/** Closes a tab; closing the active one moves the session to a remaining tab. */
|
|
3704
|
+
async closeTab(sessionId, tabId) {
|
|
3705
|
+
const record = await this.ensureSession(sessionId);
|
|
3706
|
+
const tabs = await this.browser.listTabs(record.cdpUrl);
|
|
3707
|
+
const target = tabs.find((t) => t.id === tabId);
|
|
3708
|
+
if (!target) throw new Error(`Tab not found: ${tabId}`);
|
|
3709
|
+
const remaining = tabs.filter((t) => t.id !== tabId);
|
|
3710
|
+
if (remaining.length === 0) throw new Error("Refusing to close the last tab; use agent stop to end the session");
|
|
3711
|
+
await this.browser.closeTab(record.cdpUrl, tabId);
|
|
3712
|
+
let active = tabs.find((t) => t.targetWsUrl === record.targetWsUrl) ?? remaining[0];
|
|
3713
|
+
if (target.targetWsUrl === record.targetWsUrl) {
|
|
3714
|
+
active = remaining[remaining.length - 1];
|
|
3715
|
+
await this.browser.activateTab(record.cdpUrl, active.id);
|
|
3716
|
+
this.store.save({
|
|
3717
|
+
...record,
|
|
3718
|
+
targetWsUrl: active.targetWsUrl,
|
|
3719
|
+
lastUrl: active.url
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
this.recordEvent(sessionId, "lifecycle", "info", `Closed tab ${tabId}`);
|
|
3723
|
+
return {
|
|
3724
|
+
closed: tabId,
|
|
3725
|
+
active
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3004
3728
|
async dismissCookieBanner(sessionId) {
|
|
3005
3729
|
const record = await this.ensureSession(sessionId);
|
|
3006
3730
|
const result = await this.browser.dismissCookieBanner(record.targetWsUrl);
|
|
@@ -3219,7 +3943,7 @@ var SessionManager = class {
|
|
|
3219
3943
|
type: input.type === "navigate" ? "navigate" : input.type === "interact" ? "interact" : "assert",
|
|
3220
3944
|
summary: input.type === "interact" ? `interact:${action ?? "unknown"}` : input.type,
|
|
3221
3945
|
selector,
|
|
3222
|
-
payload: input.payload
|
|
3946
|
+
payload: redactStepPayload(input.payload)
|
|
3223
3947
|
},
|
|
3224
3948
|
selector,
|
|
3225
3949
|
url: inputUrl
|
|
@@ -3246,8 +3970,7 @@ var SessionManager = class {
|
|
|
3246
3970
|
}
|
|
3247
3971
|
}
|
|
3248
3972
|
};
|
|
3249
|
-
|
|
3250
|
-
//#region src/transport/control-api.ts
|
|
3973
|
+
// src/transport/control-api.ts
|
|
3251
3974
|
var ControlApi = class {
|
|
3252
3975
|
sessions;
|
|
3253
3976
|
constructor(sessions) {
|
|
@@ -3286,6 +4009,21 @@ var ControlApi = class {
|
|
|
3286
4009
|
async getCurrentUrl(sessionId) {
|
|
3287
4010
|
return await this.sessions.getCurrentUrl(sessionId);
|
|
3288
4011
|
}
|
|
4012
|
+
async getPageState(sessionId) {
|
|
4013
|
+
return await this.sessions.getPageState(sessionId);
|
|
4014
|
+
}
|
|
4015
|
+
async listTabs(sessionId) {
|
|
4016
|
+
return await this.sessions.listTabs(sessionId);
|
|
4017
|
+
}
|
|
4018
|
+
async switchTab(sessionId, tabId) {
|
|
4019
|
+
return await this.sessions.switchTab(sessionId, tabId);
|
|
4020
|
+
}
|
|
4021
|
+
async newTab(sessionId) {
|
|
4022
|
+
return await this.sessions.newTab(sessionId);
|
|
4023
|
+
}
|
|
4024
|
+
async closeTab(sessionId, tabId) {
|
|
4025
|
+
return await this.sessions.closeTab(sessionId, tabId);
|
|
4026
|
+
}
|
|
3289
4027
|
listEvents(sessionId, limit = 100) {
|
|
3290
4028
|
return { events: this.sessions.listEvents(sessionId, limit) };
|
|
3291
4029
|
}
|
|
@@ -3305,8 +4043,7 @@ var ControlApi = class {
|
|
|
3305
4043
|
return this.sessions.cleanupSessions(input);
|
|
3306
4044
|
}
|
|
3307
4045
|
};
|
|
3308
|
-
|
|
3309
|
-
//#region src/lib/config.ts
|
|
4046
|
+
// src/lib/config.ts
|
|
3310
4047
|
function loadConfig(env = process.env) {
|
|
3311
4048
|
const userProfile = env.AGENTIC_BROWSER_USER_PROFILE;
|
|
3312
4049
|
let userProfileDir;
|
|
@@ -3318,11 +4055,11 @@ function loadConfig(env = process.env) {
|
|
|
3318
4055
|
cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
|
|
3319
4056
|
userProfileDir,
|
|
3320
4057
|
headless: env.AGENTIC_BROWSER_HEADLESS === "true" || env.AGENTIC_BROWSER_HEADLESS === "1",
|
|
3321
|
-
userAgent: env.AGENTIC_BROWSER_USER_AGENT || void 0
|
|
4058
|
+
userAgent: env.AGENTIC_BROWSER_USER_AGENT || void 0,
|
|
4059
|
+
allowFileUrls: env.AGENTIC_BROWSER_ALLOW_FILE_URLS === "true" || env.AGENTIC_BROWSER_ALLOW_FILE_URLS === "1"
|
|
3322
4060
|
};
|
|
3323
4061
|
}
|
|
3324
|
-
|
|
3325
|
-
//#region src/observability/logger.ts
|
|
4062
|
+
// src/observability/logger.ts
|
|
3326
4063
|
var Logger = class {
|
|
3327
4064
|
namespace;
|
|
3328
4065
|
constructor(namespace) {
|
|
@@ -3353,8 +4090,7 @@ var Logger = class {
|
|
|
3353
4090
|
this.emit("error", message, context);
|
|
3354
4091
|
}
|
|
3355
4092
|
};
|
|
3356
|
-
|
|
3357
|
-
//#region src/observability/event-store.ts
|
|
4093
|
+
// src/observability/event-store.ts
|
|
3358
4094
|
const MAX_EVENTS_PER_SESSION = 500;
|
|
3359
4095
|
var EventStore = class {
|
|
3360
4096
|
events = /* @__PURE__ */ new Map();
|
|
@@ -3364,8 +4100,11 @@ var EventStore = class {
|
|
|
3364
4100
|
pendingFlushDelayMs = Number.POSITIVE_INFINITY;
|
|
3365
4101
|
flushInFlight;
|
|
3366
4102
|
constructor(baseDir) {
|
|
3367
|
-
|
|
4103
|
+
ensurePrivateDir(baseDir);
|
|
3368
4104
|
this.filePath = path.join(baseDir, "session-events.log");
|
|
4105
|
+
try {
|
|
4106
|
+
if (fs.existsSync(this.filePath)) fs.chmodSync(this.filePath, 384);
|
|
4107
|
+
} catch {}
|
|
3369
4108
|
process.on("beforeExit", this.flushSync);
|
|
3370
4109
|
process.on("exit", this.flushSync);
|
|
3371
4110
|
}
|
|
@@ -3416,7 +4155,10 @@ var EventStore = class {
|
|
|
3416
4155
|
async flushAsync() {
|
|
3417
4156
|
if (this.flushInFlight || this.bufferedLines.length === 0) return;
|
|
3418
4157
|
const chunk = this.bufferedLines.splice(0, this.bufferedLines.length).join("");
|
|
3419
|
-
this.flushInFlight = fs.promises.appendFile(this.filePath, chunk,
|
|
4158
|
+
this.flushInFlight = fs.promises.appendFile(this.filePath, chunk, {
|
|
4159
|
+
encoding: "utf8",
|
|
4160
|
+
mode: 384
|
|
4161
|
+
}).catch(() => {}).finally(() => {
|
|
3420
4162
|
this.flushInFlight = void 0;
|
|
3421
4163
|
if (this.bufferedLines.length > 0) this.flushSoon(0);
|
|
3422
4164
|
});
|
|
@@ -3430,12 +4172,14 @@ var EventStore = class {
|
|
|
3430
4172
|
if (this.bufferedLines.length === 0) return;
|
|
3431
4173
|
const chunk = this.bufferedLines.splice(0, this.bufferedLines.length).join("");
|
|
3432
4174
|
try {
|
|
3433
|
-
fs.appendFileSync(this.filePath, chunk,
|
|
4175
|
+
fs.appendFileSync(this.filePath, chunk, {
|
|
4176
|
+
encoding: "utf8",
|
|
4177
|
+
mode: 384
|
|
4178
|
+
});
|
|
3434
4179
|
} catch {}
|
|
3435
4180
|
};
|
|
3436
4181
|
};
|
|
3437
|
-
|
|
3438
|
-
//#region src/auth/session-token.ts
|
|
4182
|
+
// src/auth/session-token.ts
|
|
3439
4183
|
var SessionTokenService = class {
|
|
3440
4184
|
tokens = /* @__PURE__ */ new Map();
|
|
3441
4185
|
issue(sessionId) {
|
|
@@ -3461,8 +4205,7 @@ var SessionTokenService = class {
|
|
|
3461
4205
|
this.tokens.set(sessionId, token);
|
|
3462
4206
|
}
|
|
3463
4207
|
};
|
|
3464
|
-
|
|
3465
|
-
//#region src/memory/memory-index.ts
|
|
4208
|
+
// src/memory/memory-index.ts
|
|
3466
4209
|
function normalize(value) {
|
|
3467
4210
|
return value.trim().toLowerCase();
|
|
3468
4211
|
}
|
|
@@ -3571,8 +4314,7 @@ var MemoryIndex = class {
|
|
|
3571
4314
|
return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup)).filter(relevant).toSorted((a, b) => b.result.score - a.result.score).slice(0, limit).map((scored) => scored.result);
|
|
3572
4315
|
}
|
|
3573
4316
|
};
|
|
3574
|
-
|
|
3575
|
-
//#region src/memory/staleness-detector.ts
|
|
4317
|
+
// src/memory/staleness-detector.ts
|
|
3576
4318
|
const MAX_SUSPECT_STRIKES = 2;
|
|
3577
4319
|
function detectStalenessSignal(errorMessage, selector) {
|
|
3578
4320
|
const lowered = errorMessage.toLowerCase();
|
|
@@ -3600,7 +4342,7 @@ function applyFailure(insight, signal) {
|
|
|
3600
4342
|
};
|
|
3601
4343
|
}
|
|
3602
4344
|
function applyAgeStaleness(insight) {
|
|
3603
|
-
const ageDays = (Date.now() - Date.parse(insight.lastVerifiedAt)) /
|
|
4345
|
+
const ageDays = (Date.now() - Date.parse(insight.lastVerifiedAt)) / 864e5;
|
|
3604
4346
|
if (insight.freshness === "fresh" && ageDays > 30) return {
|
|
3605
4347
|
...insight,
|
|
3606
4348
|
freshness: "suspect",
|
|
@@ -3627,8 +4369,57 @@ function applySuccess(insight) {
|
|
|
3627
4369
|
updatedAt: now
|
|
3628
4370
|
};
|
|
3629
4371
|
}
|
|
3630
|
-
|
|
3631
|
-
|
|
4372
|
+
// src/lib/flush-registry.ts
|
|
4373
|
+
const REGISTRY = Symbol.for("agentic-browser.flush-registry");
|
|
4374
|
+
function flushAll(items) {
|
|
4375
|
+
for (const item of items) try {
|
|
4376
|
+
item.flushSync();
|
|
4377
|
+
} catch {}
|
|
4378
|
+
}
|
|
4379
|
+
function createRegistry() {
|
|
4380
|
+
const items = /* @__PURE__ */ new Set();
|
|
4381
|
+
const onSignal = (signal) => {
|
|
4382
|
+
flushAll(items);
|
|
4383
|
+
if (process.listenerCount(signal) === 1) process.exit(0);
|
|
4384
|
+
};
|
|
4385
|
+
return {
|
|
4386
|
+
items,
|
|
4387
|
+
onExit: () => flushAll(items),
|
|
4388
|
+
onSigint: () => onSignal("SIGINT"),
|
|
4389
|
+
onSigterm: () => onSignal("SIGTERM")
|
|
4390
|
+
};
|
|
4391
|
+
}
|
|
4392
|
+
/**
|
|
4393
|
+
* Register an item to be flushed synchronously on process exit / SIGINT / SIGTERM.
|
|
4394
|
+
* Returns an idempotent unregister function; the process listeners are removed
|
|
4395
|
+
* once the last item is unregistered.
|
|
4396
|
+
*/
|
|
4397
|
+
function registerFlushable(item) {
|
|
4398
|
+
const proc = process;
|
|
4399
|
+
let registry = proc[REGISTRY];
|
|
4400
|
+
if (!registry) {
|
|
4401
|
+
registry = createRegistry();
|
|
4402
|
+
proc[REGISTRY] = registry;
|
|
4403
|
+
process.on("exit", registry.onExit);
|
|
4404
|
+
process.on("SIGINT", registry.onSigint);
|
|
4405
|
+
process.on("SIGTERM", registry.onSigterm);
|
|
4406
|
+
}
|
|
4407
|
+
registry.items.add(item);
|
|
4408
|
+
const owner = registry;
|
|
4409
|
+
let done = false;
|
|
4410
|
+
return () => {
|
|
4411
|
+
if (done) return;
|
|
4412
|
+
done = true;
|
|
4413
|
+
owner.items.delete(item);
|
|
4414
|
+
if (owner.items.size === 0 && proc[REGISTRY] === owner) {
|
|
4415
|
+
process.off("exit", owner.onExit);
|
|
4416
|
+
process.off("SIGINT", owner.onSigint);
|
|
4417
|
+
process.off("SIGTERM", owner.onSigterm);
|
|
4418
|
+
delete proc[REGISTRY];
|
|
4419
|
+
}
|
|
4420
|
+
};
|
|
4421
|
+
}
|
|
4422
|
+
// src/memory/memory-schemas.ts
|
|
3632
4423
|
const InsightFreshnessSchema = z.enum([
|
|
3633
4424
|
"fresh",
|
|
3634
4425
|
"suspect",
|
|
@@ -3705,27 +4496,25 @@ const SiteProfileSchema = z.object({
|
|
|
3705
4496
|
createdAt: z.string().datetime()
|
|
3706
4497
|
});
|
|
3707
4498
|
const SiteProfileStateSchema = z.object({ profiles: z.array(SiteProfileSchema) });
|
|
3708
|
-
|
|
3709
|
-
//#region src/memory/site-profile-store.ts
|
|
4499
|
+
// src/memory/site-profile-store.ts
|
|
3710
4500
|
const EMPTY_STATE$1 = { profiles: [] };
|
|
3711
4501
|
const FLUSH_DELAY_MS$1 = 500;
|
|
3712
|
-
const REGISTERED_EXIT_HANDLERS$1 = Symbol.for("agentic-browser.site-profile-store.exit-handlers");
|
|
3713
|
-
const REGISTERED_STORES$1 = Symbol.for("agentic-browser.site-profile-store.instances");
|
|
3714
4502
|
var SiteProfileStore = class {
|
|
3715
4503
|
filePath;
|
|
3716
4504
|
cached = null;
|
|
3717
4505
|
dirty = false;
|
|
3718
4506
|
flushTimer = null;
|
|
4507
|
+
unregister;
|
|
3719
4508
|
constructor(baseDir) {
|
|
3720
4509
|
const memoryDir = path.join(baseDir, "memory");
|
|
3721
|
-
|
|
4510
|
+
ensurePrivateDir(memoryDir);
|
|
3722
4511
|
this.filePath = path.join(memoryDir, "site-profiles.json");
|
|
3723
4512
|
const tmpPath = `${this.filePath}.tmp`;
|
|
3724
4513
|
try {
|
|
3725
4514
|
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
3726
4515
|
} catch {}
|
|
3727
4516
|
if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE$1);
|
|
3728
|
-
this.
|
|
4517
|
+
this.unregister = registerFlushable(this);
|
|
3729
4518
|
}
|
|
3730
4519
|
list() {
|
|
3731
4520
|
return this.getCache();
|
|
@@ -3746,6 +4535,11 @@ var SiteProfileStore = class {
|
|
|
3746
4535
|
else profiles.push(normalized);
|
|
3747
4536
|
this.markDirty();
|
|
3748
4537
|
}
|
|
4538
|
+
/** Flush pending writes and stop flushing on process exit. Idempotent. */
|
|
4539
|
+
dispose() {
|
|
4540
|
+
this.flushSync();
|
|
4541
|
+
this.unregister();
|
|
4542
|
+
}
|
|
3749
4543
|
flushSync() {
|
|
3750
4544
|
if (this.flushTimer) {
|
|
3751
4545
|
clearTimeout(this.flushTimer);
|
|
@@ -3802,51 +4596,30 @@ var SiteProfileStore = class {
|
|
|
3802
4596
|
}
|
|
3803
4597
|
writeDisk(state) {
|
|
3804
4598
|
const tempPath = `${this.filePath}.tmp`;
|
|
3805
|
-
|
|
4599
|
+
writePrivateFileSync(tempPath, JSON.stringify(state, null, 2));
|
|
3806
4600
|
fs.renameSync(tempPath, this.filePath);
|
|
3807
4601
|
}
|
|
3808
|
-
registerProcessHandlers() {
|
|
3809
|
-
const proc = process;
|
|
3810
|
-
const stores = proc[REGISTERED_STORES$1] ??= /* @__PURE__ */ new Set();
|
|
3811
|
-
stores.add(this);
|
|
3812
|
-
if (proc[REGISTERED_EXIT_HANDLERS$1]) return;
|
|
3813
|
-
proc[REGISTERED_EXIT_HANDLERS$1] = true;
|
|
3814
|
-
const flushAll = () => {
|
|
3815
|
-
for (const store of stores) store.flushSync();
|
|
3816
|
-
};
|
|
3817
|
-
process.on("exit", flushAll);
|
|
3818
|
-
process.on("SIGINT", () => {
|
|
3819
|
-
flushAll();
|
|
3820
|
-
process.exit(0);
|
|
3821
|
-
});
|
|
3822
|
-
process.on("SIGTERM", () => {
|
|
3823
|
-
flushAll();
|
|
3824
|
-
process.exit(0);
|
|
3825
|
-
});
|
|
3826
|
-
}
|
|
3827
4602
|
};
|
|
3828
|
-
|
|
3829
|
-
//#region src/memory/task-insight-store.ts
|
|
4603
|
+
// src/memory/task-insight-store.ts
|
|
3830
4604
|
const EMPTY_STATE = { insights: [] };
|
|
3831
4605
|
const FLUSH_DELAY_MS = 500;
|
|
3832
|
-
const REGISTERED_EXIT_HANDLERS = Symbol.for("agentic-browser.task-insight-store.exit-handlers");
|
|
3833
|
-
const REGISTERED_STORES = Symbol.for("agentic-browser.task-insight-store.instances");
|
|
3834
4606
|
var TaskInsightStore = class {
|
|
3835
4607
|
filePath;
|
|
3836
4608
|
/** In-memory cache – authoritative after first load. */
|
|
3837
4609
|
cached = null;
|
|
3838
4610
|
dirty = false;
|
|
3839
4611
|
flushTimer = null;
|
|
4612
|
+
unregister;
|
|
3840
4613
|
constructor(baseDir) {
|
|
3841
4614
|
const memoryDir = path.join(baseDir, "memory");
|
|
3842
|
-
|
|
4615
|
+
ensurePrivateDir(memoryDir);
|
|
3843
4616
|
this.filePath = path.join(memoryDir, "insights.json");
|
|
3844
4617
|
const tmpPath = `${this.filePath}.tmp`;
|
|
3845
4618
|
try {
|
|
3846
4619
|
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
3847
4620
|
} catch {}
|
|
3848
4621
|
if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE);
|
|
3849
|
-
this.
|
|
4622
|
+
this.unregister = registerFlushable(this);
|
|
3850
4623
|
}
|
|
3851
4624
|
list() {
|
|
3852
4625
|
return this.getCache();
|
|
@@ -3876,6 +4649,11 @@ var TaskInsightStore = class {
|
|
|
3876
4649
|
this.cached = insights;
|
|
3877
4650
|
this.markDirty();
|
|
3878
4651
|
}
|
|
4652
|
+
/** Flush pending writes and stop flushing on process exit. Idempotent. */
|
|
4653
|
+
dispose() {
|
|
4654
|
+
this.flushSync();
|
|
4655
|
+
this.unregister();
|
|
4656
|
+
}
|
|
3879
4657
|
/** Force an immediate synchronous flush (used at shutdown). */
|
|
3880
4658
|
flushSync() {
|
|
3881
4659
|
if (this.flushTimer) {
|
|
@@ -3892,8 +4670,52 @@ var TaskInsightStore = class {
|
|
|
3892
4670
|
if (this.cached) return this.cached;
|
|
3893
4671
|
const state = this.readDisk();
|
|
3894
4672
|
this.cached = state.insights;
|
|
4673
|
+
if (this.sanitizeCached(this.cached)) this.markDirty();
|
|
3895
4674
|
return this.cached;
|
|
3896
4675
|
}
|
|
4676
|
+
/**
|
|
4677
|
+
* Redact already-persisted step payloads and URL query strings/fragments in place.
|
|
4678
|
+
* Returns true if anything changed.
|
|
4679
|
+
*/
|
|
4680
|
+
sanitizeCached(insights) {
|
|
4681
|
+
let changed = false;
|
|
4682
|
+
for (const insight of insights) {
|
|
4683
|
+
const expectedOutcome = redactUrlsInText(insight.expectedOutcome);
|
|
4684
|
+
if (expectedOutcome !== insight.expectedOutcome) {
|
|
4685
|
+
insight.expectedOutcome = expectedOutcome;
|
|
4686
|
+
changed = true;
|
|
4687
|
+
}
|
|
4688
|
+
const taskIntent = redactUrlsInText(insight.taskIntent);
|
|
4689
|
+
if (taskIntent !== insight.taskIntent) {
|
|
4690
|
+
insight.taskIntent = taskIntent;
|
|
4691
|
+
changed = true;
|
|
4692
|
+
}
|
|
4693
|
+
for (const evidence of insight.evidence) {
|
|
4694
|
+
if (evidence.url !== void 0) {
|
|
4695
|
+
const url = stripUrl(evidence.url);
|
|
4696
|
+
if (url !== evidence.url) {
|
|
4697
|
+
evidence.url = url;
|
|
4698
|
+
changed = true;
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
if (evidence.reason !== void 0) {
|
|
4702
|
+
const reason = redactUrlsInText(evidence.reason);
|
|
4703
|
+
if (reason !== evidence.reason) {
|
|
4704
|
+
evidence.reason = reason;
|
|
4705
|
+
changed = true;
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
for (const step of insight.actionRecipe) {
|
|
4710
|
+
const redacted = redactStepPayload(step.payload);
|
|
4711
|
+
if (JSON.stringify(redacted) !== JSON.stringify(step.payload)) {
|
|
4712
|
+
step.payload = redacted;
|
|
4713
|
+
changed = true;
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
return changed;
|
|
4718
|
+
}
|
|
3897
4719
|
markDirty() {
|
|
3898
4720
|
this.dirty = true;
|
|
3899
4721
|
if (!this.flushTimer) this.flushTimer = setTimeout(() => {
|
|
@@ -3935,31 +4757,11 @@ var TaskInsightStore = class {
|
|
|
3935
4757
|
}
|
|
3936
4758
|
writeDisk(state) {
|
|
3937
4759
|
const tempPath = `${this.filePath}.tmp`;
|
|
3938
|
-
|
|
4760
|
+
writePrivateFileSync(tempPath, JSON.stringify(state, null, 2));
|
|
3939
4761
|
fs.renameSync(tempPath, this.filePath);
|
|
3940
4762
|
}
|
|
3941
|
-
registerProcessHandlers() {
|
|
3942
|
-
const proc = process;
|
|
3943
|
-
const stores = proc[REGISTERED_STORES] ??= /* @__PURE__ */ new Set();
|
|
3944
|
-
stores.add(this);
|
|
3945
|
-
if (proc[REGISTERED_EXIT_HANDLERS]) return;
|
|
3946
|
-
proc[REGISTERED_EXIT_HANDLERS] = true;
|
|
3947
|
-
const flushAll = () => {
|
|
3948
|
-
for (const store of stores) store.flushSync();
|
|
3949
|
-
};
|
|
3950
|
-
process.on("exit", flushAll);
|
|
3951
|
-
process.on("SIGINT", () => {
|
|
3952
|
-
flushAll();
|
|
3953
|
-
process.exit(0);
|
|
3954
|
-
});
|
|
3955
|
-
process.on("SIGTERM", () => {
|
|
3956
|
-
flushAll();
|
|
3957
|
-
process.exit(0);
|
|
3958
|
-
});
|
|
3959
|
-
}
|
|
3960
4763
|
};
|
|
3961
|
-
|
|
3962
|
-
//#region src/memory/memory-service.ts
|
|
4764
|
+
// src/memory/memory-service.ts
|
|
3963
4765
|
/** Memory service configuration */
|
|
3964
4766
|
const SEARCH_CACHE_TTL_MS = 2e3;
|
|
3965
4767
|
const MAX_SELECTOR_ALIASES = 10;
|
|
@@ -3987,6 +4789,11 @@ var MemoryService = class {
|
|
|
3987
4789
|
this.store.flushSync();
|
|
3988
4790
|
this.profileStore.flushSync();
|
|
3989
4791
|
}
|
|
4792
|
+
/** Flush pending writes and release the stores' process exit hooks. */
|
|
4793
|
+
dispose() {
|
|
4794
|
+
this.store.dispose();
|
|
4795
|
+
this.profileStore.dispose();
|
|
4796
|
+
}
|
|
3990
4797
|
search(input) {
|
|
3991
4798
|
const cacheKey = `${input.taskIntent}\0${input.siteDomain ?? ""}\0${input.limit ?? 10}`;
|
|
3992
4799
|
const now = Date.now();
|
|
@@ -4048,7 +4855,8 @@ var MemoryService = class {
|
|
|
4048
4855
|
this.invalidateSearchCache();
|
|
4049
4856
|
return verified;
|
|
4050
4857
|
}
|
|
4051
|
-
recordSuccess(
|
|
4858
|
+
recordSuccess(rawInput) {
|
|
4859
|
+
const input = this.sanitizeInput(rawInput);
|
|
4052
4860
|
this.updateSiteProfile(input.siteDomain, input.selector, input.url, input.sitePathPattern);
|
|
4053
4861
|
const insights = this.store.list();
|
|
4054
4862
|
const matched = this.findBestExactMatch(insights, input);
|
|
@@ -4107,13 +4915,14 @@ var MemoryService = class {
|
|
|
4107
4915
|
this.invalidateSearchCache();
|
|
4108
4916
|
return withAliases;
|
|
4109
4917
|
}
|
|
4110
|
-
recordFailure(
|
|
4918
|
+
recordFailure(rawInput, errorMessage) {
|
|
4919
|
+
const input = this.sanitizeInput(rawInput);
|
|
4111
4920
|
this.updateSiteProfile(input.siteDomain, input.selector, input.url, input.sitePathPattern);
|
|
4112
4921
|
const insights = this.store.list();
|
|
4113
4922
|
const matched = this.findBestExactMatch(insights, input);
|
|
4114
4923
|
if (!matched) return;
|
|
4115
4924
|
const signal = detectStalenessSignal(errorMessage, input.selector);
|
|
4116
|
-
const evidence = this.createEvidence(input, "failure", errorMessage);
|
|
4925
|
+
const evidence = this.createEvidence(input, "failure", redactUrlsInText(errorMessage));
|
|
4117
4926
|
const failed = applyFailure({
|
|
4118
4927
|
...matched,
|
|
4119
4928
|
useCount: matched.useCount + 1,
|
|
@@ -4235,6 +5044,15 @@ var MemoryService = class {
|
|
|
4235
5044
|
const rightSegment = normalizePathSegment(right);
|
|
4236
5045
|
return Boolean(leftSegment) && leftSegment === rightSegment;
|
|
4237
5046
|
}
|
|
5047
|
+
/** Strip query strings and fragments from every URL-bearing field before it is persisted. */
|
|
5048
|
+
sanitizeInput(input) {
|
|
5049
|
+
return {
|
|
5050
|
+
...input,
|
|
5051
|
+
url: input.url ? stripUrl(input.url) ?? void 0 : void 0,
|
|
5052
|
+
expectedOutcome: redactUrlsInText(input.expectedOutcome),
|
|
5053
|
+
taskIntent: redactUrlsInText(input.taskIntent)
|
|
5054
|
+
};
|
|
5055
|
+
}
|
|
4238
5056
|
createEvidence(input, result, reason) {
|
|
4239
5057
|
return {
|
|
4240
5058
|
evidenceId: crypto.randomUUID(),
|
|
@@ -4287,8 +5105,7 @@ var MemoryService = class {
|
|
|
4287
5105
|
return [...recipe, step].slice(-8);
|
|
4288
5106
|
}
|
|
4289
5107
|
};
|
|
4290
|
-
|
|
4291
|
-
//#region src/cli/app.ts
|
|
5108
|
+
// src/cli/app.ts
|
|
4292
5109
|
function createAppContext(env = process.env) {
|
|
4293
5110
|
const config = loadConfig(env);
|
|
4294
5111
|
return {
|
|
@@ -4299,17 +5116,31 @@ function createAppContext(env = process.env) {
|
|
|
4299
5116
|
memoryService: new MemoryService(config.dataDir)
|
|
4300
5117
|
};
|
|
4301
5118
|
}
|
|
4302
|
-
|
|
4303
|
-
//#region src/cli/runtime.ts
|
|
5119
|
+
// src/cli/runtime.ts
|
|
4304
5120
|
var AgenticBrowserCore = class {
|
|
4305
5121
|
context;
|
|
4306
5122
|
sessions;
|
|
4307
5123
|
api;
|
|
5124
|
+
browserController;
|
|
5125
|
+
disposed = false;
|
|
4308
5126
|
constructor(context, browserController) {
|
|
4309
5127
|
this.context = context;
|
|
5128
|
+
this.browserController = browserController;
|
|
4310
5129
|
this.sessions = new SessionManager(context, browserController);
|
|
4311
5130
|
this.api = new ControlApi(this.sessions);
|
|
4312
5131
|
}
|
|
5132
|
+
/**
|
|
5133
|
+
* Release process-level resources (event store and memory store exit handlers,
|
|
5134
|
+
* controller timers). Flushes pending memory writes.
|
|
5135
|
+
* Does not terminate browser sessions. Idempotent.
|
|
5136
|
+
*/
|
|
5137
|
+
async dispose() {
|
|
5138
|
+
if (this.disposed) return;
|
|
5139
|
+
this.disposed = true;
|
|
5140
|
+
this.browserController.dispose?.();
|
|
5141
|
+
this.context.eventStore.dispose();
|
|
5142
|
+
this.context.memoryService.dispose();
|
|
5143
|
+
}
|
|
4313
5144
|
async startSession(input = { browser: "chrome" }) {
|
|
4314
5145
|
return await this.api.createSession(input);
|
|
4315
5146
|
}
|
|
@@ -4336,7 +5167,10 @@ var AgenticBrowserCore = class {
|
|
|
4336
5167
|
roles: input.roles,
|
|
4337
5168
|
visibleOnly: input.visibleOnly,
|
|
4338
5169
|
limit: input.limit,
|
|
4339
|
-
selector: input.selector
|
|
5170
|
+
selector: input.selector,
|
|
5171
|
+
href: input.href,
|
|
5172
|
+
text: input.text,
|
|
5173
|
+
frame: input.frame
|
|
4340
5174
|
});
|
|
4341
5175
|
}
|
|
4342
5176
|
async dismissCookieBanner(sessionId) {
|
|
@@ -4348,6 +5182,21 @@ var AgenticBrowserCore = class {
|
|
|
4348
5182
|
async getCurrentUrl(sessionId) {
|
|
4349
5183
|
return await this.api.getCurrentUrl(sessionId);
|
|
4350
5184
|
}
|
|
5185
|
+
async getPageState(sessionId) {
|
|
5186
|
+
return await this.api.getPageState(sessionId);
|
|
5187
|
+
}
|
|
5188
|
+
async listTabs(sessionId) {
|
|
5189
|
+
return await this.api.listTabs(sessionId);
|
|
5190
|
+
}
|
|
5191
|
+
async switchTab(sessionId, tabId) {
|
|
5192
|
+
return await this.api.switchTab(sessionId, tabId);
|
|
5193
|
+
}
|
|
5194
|
+
async newTab(sessionId) {
|
|
5195
|
+
return await this.api.newTab(sessionId);
|
|
5196
|
+
}
|
|
5197
|
+
async closeTab(sessionId, tabId) {
|
|
5198
|
+
return await this.api.closeTab(sessionId, tabId);
|
|
5199
|
+
}
|
|
4351
5200
|
async restartSession(sessionId) {
|
|
4352
5201
|
return await this.api.restartSession(sessionId);
|
|
4353
5202
|
}
|
|
@@ -4380,5 +5229,4 @@ function createMockAgenticBrowserCore(env) {
|
|
|
4380
5229
|
function createCliRuntime() {
|
|
4381
5230
|
return createAgenticBrowserCore();
|
|
4382
5231
|
}
|
|
4383
|
-
|
|
4384
|
-
export { createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, createCliRuntime as r, AgenticBrowserCore as t };
|
|
5232
|
+
export { isConnectionError as a, createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, ensurePrivateDir as o, createCliRuntime as r, writePrivateFileSync as s, AgenticBrowserCore as t };
|