agentic-browser 2.5.1 → 2.6.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/dist/cli/index.d.mts +1 -1
- package/dist/cli/index.mjs +4 -3
- package/dist/index.d.mts +73 -86
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.mjs +111 -23
- package/dist/{runtime-BeD8ZcjH.mjs → runtime-BzdbtFF4.mjs} +309 -150
- package/package.json +17 -17
- /package/dist/{setup-BP0YyerC.mjs → setup-DUAjdbQ3.mjs} +0 -0
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import WebSocket
|
|
7
|
+
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";
|
|
@@ -24,6 +24,7 @@ function discoverChrome(explicitPath) {
|
|
|
24
24
|
//#endregion
|
|
25
25
|
//#region src/session/browser-controller.ts
|
|
26
26
|
var CdpConnection = class CdpConnection {
|
|
27
|
+
ws;
|
|
27
28
|
nextId = 0;
|
|
28
29
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
29
30
|
pendingEvents = /* @__PURE__ */ new Map();
|
|
@@ -151,6 +152,26 @@ var CdpConnection = class CdpConnection {
|
|
|
151
152
|
this.pendingEvents.set(method, waiters);
|
|
152
153
|
});
|
|
153
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolve on the first event of `method` whose params satisfy `predicate`.
|
|
157
|
+
* Uses the persistent onEvent channel so non-matching events (e.g. subframe
|
|
158
|
+
* navigations) don't consume the wait.
|
|
159
|
+
*/
|
|
160
|
+
waitForEventMatching(method, predicate, timeoutMs = 5e3) {
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
let unsubscribe = () => {};
|
|
163
|
+
const timeout = setTimeout(() => {
|
|
164
|
+
unsubscribe();
|
|
165
|
+
reject(/* @__PURE__ */ new Error(`Timed out waiting for ${method}`));
|
|
166
|
+
}, timeoutMs);
|
|
167
|
+
unsubscribe = this.onEvent(method, (params) => {
|
|
168
|
+
if (!predicate(params)) return;
|
|
169
|
+
clearTimeout(timeout);
|
|
170
|
+
unsubscribe();
|
|
171
|
+
resolve(params);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
154
175
|
onEvent(method, handler) {
|
|
155
176
|
const handlers = this.eventHandlers.get(method) ?? /* @__PURE__ */ new Set();
|
|
156
177
|
const wrapped = (params) => {
|
|
@@ -231,7 +252,12 @@ async function createTarget(cdpUrl, url = "about:blank") {
|
|
|
231
252
|
* Apply stealth patches that remove common automation fingerprints.
|
|
232
253
|
* These run before any page loads so sites cannot detect the bot.
|
|
233
254
|
*/
|
|
234
|
-
const STEALTH_SCRIPT = `
|
|
255
|
+
const STEALTH_SCRIPT = `(() => {
|
|
256
|
+
// Guard: registered once per CDP session, so re-connects would re-run the patches
|
|
257
|
+
// and the non-configurable defineProperty calls below would throw.
|
|
258
|
+
if (window.__agenticStealthApplied) return;
|
|
259
|
+
window.__agenticStealthApplied = true;
|
|
260
|
+
|
|
235
261
|
// Remove navigator.webdriver flag
|
|
236
262
|
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
237
263
|
|
|
@@ -273,8 +299,13 @@ const STEALTH_SCRIPT = `
|
|
|
273
299
|
return origQuery.call(this, params);
|
|
274
300
|
};
|
|
275
301
|
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
302
|
+
})();`;
|
|
303
|
+
/**
|
|
304
|
+
* Verify the page is ready, apply stealth, and optionally set a custom user-agent.
|
|
305
|
+
* Returns the user-agent override that was applied (if any) so callers can re-apply
|
|
306
|
+
* it on later CDP sessions — script/UA overrides are scoped to the session that set
|
|
307
|
+
* them and die when this bootstrap connection closes.
|
|
308
|
+
*/
|
|
278
309
|
async function initTarget(targetWsUrl, userAgent) {
|
|
279
310
|
const conn = await CdpConnection.connect(targetWsUrl);
|
|
280
311
|
try {
|
|
@@ -291,18 +322,38 @@ async function initTarget(targetWsUrl, userAgent) {
|
|
|
291
322
|
returnByValue: true,
|
|
292
323
|
awaitPromise: true
|
|
293
324
|
});
|
|
294
|
-
if (userAgent)
|
|
295
|
-
|
|
325
|
+
if (userAgent) {
|
|
326
|
+
await conn.send("Network.setUserAgentOverride", { userAgent });
|
|
327
|
+
return userAgent;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
296
330
|
const { userAgent: currentUA } = await conn.send("Browser.getVersion");
|
|
297
331
|
if (currentUA?.includes("HeadlessChrome")) {
|
|
298
332
|
const cleanUA = currentUA.replace(/HeadlessChrome/g, "Chrome");
|
|
299
333
|
await conn.send("Network.setUserAgentOverride", { userAgent: cleanUA });
|
|
334
|
+
return cleanUA;
|
|
300
335
|
}
|
|
301
336
|
} catch {}
|
|
337
|
+
return;
|
|
302
338
|
} finally {
|
|
303
339
|
conn.close();
|
|
304
340
|
}
|
|
305
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Run Runtime.evaluate and surface in-page exceptions as errors.
|
|
344
|
+
* CDP reports page-side throws via `exceptionDetails` on an otherwise successful
|
|
345
|
+
* response — callers that read `result.value` directly would treat a throw as an
|
|
346
|
+
* empty (successful) result.
|
|
347
|
+
*/
|
|
348
|
+
async function evaluateOnPage(conn, params) {
|
|
349
|
+
const result = await conn.send("Runtime.evaluate", params);
|
|
350
|
+
const details = result.exceptionDetails;
|
|
351
|
+
if (details) {
|
|
352
|
+
const raw = details.exception?.description ?? (typeof details.exception?.value === "string" ? details.exception.value : void 0) ?? details.text ?? "Page evaluation failed";
|
|
353
|
+
throw new Error(raw.split("\n")[0].replace(/^Error:\s*/, ""));
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
306
357
|
async function getFreePort() {
|
|
307
358
|
return await new Promise((resolve, reject) => {
|
|
308
359
|
const server = net.createServer();
|
|
@@ -535,7 +586,7 @@ ${LOCATOR_RUNTIME_HELPERS}
|
|
|
535
586
|
${body}
|
|
536
587
|
})()`;
|
|
537
588
|
}
|
|
538
|
-
const SPECIAL_KEY_PAYLOADS = new Map([
|
|
589
|
+
const SPECIAL_KEY_PAYLOADS = /* @__PURE__ */ new Map([
|
|
539
590
|
["Enter", {
|
|
540
591
|
key: "Enter",
|
|
541
592
|
code: "Enter",
|
|
@@ -700,9 +751,28 @@ function buildKeyDispatchPayload(inputKey) {
|
|
|
700
751
|
/** Connection pool configuration */
|
|
701
752
|
const CONNECTION_IDLE_TIMEOUT_MS = 300 * 1e3;
|
|
702
753
|
const CONNECTION_CLEANUP_INTERVAL_MS = 6e4;
|
|
754
|
+
/**
|
|
755
|
+
* True when the error means the socket was closed/not-open — i.e. the request
|
|
756
|
+
* was never delivered, so re-running it cannot double-apply a side effect.
|
|
757
|
+
* Deliberately excludes CDP timeouts: a timed-out request may have reached
|
|
758
|
+
* Chrome, so replaying it for a non-idempotent op could fire the action twice.
|
|
759
|
+
*/
|
|
760
|
+
function isConnectionError(error) {
|
|
761
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
762
|
+
return message.includes("CDP connection closed") || message.includes("WebSocket not open");
|
|
763
|
+
}
|
|
703
764
|
var ChromeCdpBrowserController = class {
|
|
765
|
+
baseDir;
|
|
766
|
+
connectionFactory;
|
|
704
767
|
connections = /* @__PURE__ */ new Map();
|
|
705
768
|
cleanupInterval;
|
|
769
|
+
/**
|
|
770
|
+
* User-agent override resolved at target init, keyed by target ws URL.
|
|
771
|
+
* Stealth script + UA override are scoped to the CDP session that set them,
|
|
772
|
+
* so every pooled connection must re-apply them (initTarget's bootstrap
|
|
773
|
+
* connection closes right after launch).
|
|
774
|
+
*/
|
|
775
|
+
userAgentOverrides = /* @__PURE__ */ new Map();
|
|
706
776
|
constructor(baseDir, connectionFactory = CdpConnection.connect) {
|
|
707
777
|
this.baseDir = baseDir;
|
|
708
778
|
this.connectionFactory = connectionFactory;
|
|
@@ -724,6 +794,7 @@ var ChromeCdpBrowserController = class {
|
|
|
724
794
|
return cached.conn;
|
|
725
795
|
}
|
|
726
796
|
const conn = await this.connectionFactory(targetWsUrl);
|
|
797
|
+
await this.applySessionOverrides(conn, targetWsUrl);
|
|
727
798
|
this.connections.set(targetWsUrl, {
|
|
728
799
|
conn,
|
|
729
800
|
enabled: {
|
|
@@ -734,6 +805,26 @@ var ChromeCdpBrowserController = class {
|
|
|
734
805
|
});
|
|
735
806
|
return conn;
|
|
736
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* Re-apply session-scoped stealth patches + UA override on a fresh connection.
|
|
810
|
+
* Overrides are per-CDP-session, so every pooled connection must re-apply them
|
|
811
|
+
* — including in a fresh CLI process that reconnects to an existing target
|
|
812
|
+
* (the in-memory override map is empty there, so fall back to deriving one).
|
|
813
|
+
*/
|
|
814
|
+
async applySessionOverrides(conn, targetWsUrl) {
|
|
815
|
+
try {
|
|
816
|
+
await conn.send("Page.addScriptToEvaluateOnNewDocument", { source: STEALTH_SCRIPT });
|
|
817
|
+
let userAgent = this.userAgentOverrides.get(targetWsUrl);
|
|
818
|
+
if (!userAgent) try {
|
|
819
|
+
const { userAgent: currentUA } = await conn.send("Browser.getVersion");
|
|
820
|
+
if (currentUA?.includes("HeadlessChrome")) {
|
|
821
|
+
userAgent = currentUA.replace(/HeadlessChrome/g, "Chrome");
|
|
822
|
+
this.userAgentOverrides.set(targetWsUrl, userAgent);
|
|
823
|
+
}
|
|
824
|
+
} catch {}
|
|
825
|
+
if (userAgent) await conn.send("Emulation.setUserAgentOverride", { userAgent });
|
|
826
|
+
} catch {}
|
|
827
|
+
}
|
|
737
828
|
dropConnection(targetWsUrl) {
|
|
738
829
|
const cached = this.connections.get(targetWsUrl);
|
|
739
830
|
if (!cached) return;
|
|
@@ -751,7 +842,8 @@ var ChromeCdpBrowserController = class {
|
|
|
751
842
|
if (!port) throw new Error(`Invalid CDP URL: could not extract port from ${cdpUrl}`);
|
|
752
843
|
await waitForDebugger(port);
|
|
753
844
|
const targetWsUrl = await createTarget(cdpUrl);
|
|
754
|
-
await initTarget(targetWsUrl, options?.userAgent);
|
|
845
|
+
const appliedUa = await initTarget(targetWsUrl, options?.userAgent);
|
|
846
|
+
if (appliedUa) this.userAgentOverrides.set(targetWsUrl, appliedUa);
|
|
755
847
|
return {
|
|
756
848
|
pid: 0,
|
|
757
849
|
cdpUrl,
|
|
@@ -819,7 +911,8 @@ var ChromeCdpBrowserController = class {
|
|
|
819
911
|
const cdpUrl = `http://127.0.0.1:${port}`;
|
|
820
912
|
const targetWsUrl = await createTarget(cdpUrl, "about:blank");
|
|
821
913
|
if (!child.pid) throw new Error("Failed to launch Chrome process");
|
|
822
|
-
await initTarget(targetWsUrl, userAgent);
|
|
914
|
+
const appliedUa = await initTarget(targetWsUrl, userAgent);
|
|
915
|
+
if (appliedUa) this.userAgentOverrides.set(targetWsUrl, appliedUa);
|
|
823
916
|
return {
|
|
824
917
|
pid: child.pid,
|
|
825
918
|
cdpUrl,
|
|
@@ -834,12 +927,22 @@ var ChromeCdpBrowserController = class {
|
|
|
834
927
|
}
|
|
835
928
|
throw new Error(lastError?.message ?? "Unable to launch Chrome");
|
|
836
929
|
}
|
|
837
|
-
/**
|
|
838
|
-
|
|
930
|
+
/**
|
|
931
|
+
* Execute fn with a pooled connection; on failure drop the connection and retry once.
|
|
932
|
+
*
|
|
933
|
+
* `retryOn`:
|
|
934
|
+
* - "any" (default): retry any error — safe for reads/idempotent work.
|
|
935
|
+
* - "connection": retry only when the connection itself died. Use for
|
|
936
|
+
* non-idempotent input (click/type/press/toggle/select): a mid-operation
|
|
937
|
+
* connection drop after an event was already dispatched must not replay it,
|
|
938
|
+
* and in-page errors (selector missing, covered element) are not transient.
|
|
939
|
+
*/
|
|
940
|
+
async withRetry(targetWsUrl, fn, retryOn = "any") {
|
|
839
941
|
let conn = await this.getConnection(targetWsUrl);
|
|
840
942
|
try {
|
|
841
943
|
return await fn(conn);
|
|
842
|
-
} catch {
|
|
944
|
+
} catch (error) {
|
|
945
|
+
if (retryOn === "connection" && !isConnectionError(error)) throw error;
|
|
843
946
|
this.dropConnection(targetWsUrl);
|
|
844
947
|
conn = await this.getConnection(targetWsUrl);
|
|
845
948
|
return await fn(conn);
|
|
@@ -848,11 +951,12 @@ var ChromeCdpBrowserController = class {
|
|
|
848
951
|
async navigate(targetWsUrl, url) {
|
|
849
952
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
850
953
|
await this.ensureEnabled(targetWsUrl);
|
|
851
|
-
const
|
|
852
|
-
const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
|
|
954
|
+
const mainFrameNavigated = conn.waitForEventMatching ? conn.waitForEventMatching("Page.frameNavigated", (params) => !params.frame?.parentId, 6e3).catch(() => void 0) : conn.waitForEvent("Page.frameNavigated", 6e3).catch(() => void 0);
|
|
853
955
|
const navResult = await conn.send("Page.navigate", { url });
|
|
854
956
|
if (navResult.errorText) throw new Error(`Navigation failed: ${navResult.errorText}`);
|
|
855
|
-
const
|
|
957
|
+
const mainFrameId = navResult.frameId;
|
|
958
|
+
const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEventMatching ? conn.waitForEventMatching("Page.frameStoppedLoading", (params) => !mainFrameId || params.frameId === mainFrameId, 6e3) : conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
|
|
959
|
+
const navigatedEvent = await mainFrameNavigated;
|
|
856
960
|
try {
|
|
857
961
|
await loadPromise;
|
|
858
962
|
} catch {}
|
|
@@ -888,7 +992,8 @@ var ChromeCdpBrowserController = class {
|
|
|
888
992
|
});
|
|
889
993
|
}
|
|
890
994
|
async preparePointerTarget(conn, payload) {
|
|
891
|
-
const
|
|
995
|
+
const point = (await evaluateOnPage(conn, {
|
|
996
|
+
expression: buildLocatorRuntimeExpression("payload", payload, `
|
|
892
997
|
const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
|
|
893
998
|
const el = resolved.element;
|
|
894
999
|
ensureInView(el);
|
|
@@ -905,9 +1010,7 @@ var ChromeCdpBrowserController = class {
|
|
|
905
1010
|
}
|
|
906
1011
|
|
|
907
1012
|
return { x: point.x, y: point.y };
|
|
908
|
-
`)
|
|
909
|
-
const point = (await conn.send("Runtime.evaluate", {
|
|
910
|
-
expression,
|
|
1013
|
+
`),
|
|
911
1014
|
returnByValue: true,
|
|
912
1015
|
awaitPromise: true
|
|
913
1016
|
})).result.value;
|
|
@@ -918,7 +1021,8 @@ var ChromeCdpBrowserController = class {
|
|
|
918
1021
|
};
|
|
919
1022
|
}
|
|
920
1023
|
async prepareTypeTarget(conn, payload) {
|
|
921
|
-
const
|
|
1024
|
+
const value = (await evaluateOnPage(conn, {
|
|
1025
|
+
expression: buildLocatorRuntimeExpression("payload", payload, `
|
|
922
1026
|
const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
|
|
923
1027
|
const el = resolved.element;
|
|
924
1028
|
ensureInView(el);
|
|
@@ -967,12 +1071,12 @@ var ChromeCdpBrowserController = class {
|
|
|
967
1071
|
}
|
|
968
1072
|
|
|
969
1073
|
return 'ready';
|
|
970
|
-
`)
|
|
971
|
-
return (await conn.send("Runtime.evaluate", {
|
|
972
|
-
expression,
|
|
1074
|
+
`),
|
|
973
1075
|
returnByValue: true,
|
|
974
1076
|
awaitPromise: true
|
|
975
|
-
})).result.value
|
|
1077
|
+
})).result.value;
|
|
1078
|
+
if (value !== "ready" && value !== "cleared") throw new Error("Unable to resolve type target");
|
|
1079
|
+
return value;
|
|
976
1080
|
}
|
|
977
1081
|
async dispatchMouseClick(conn, x, y) {
|
|
978
1082
|
await conn.send("Input.dispatchMouseEvent", {
|
|
@@ -998,7 +1102,8 @@ var ChromeCdpBrowserController = class {
|
|
|
998
1102
|
}
|
|
999
1103
|
async focusTarget(conn, payload) {
|
|
1000
1104
|
if (!payload.selector) return;
|
|
1001
|
-
|
|
1105
|
+
await evaluateOnPage(conn, {
|
|
1106
|
+
expression: buildLocatorRuntimeExpression("payload", payload, `
|
|
1002
1107
|
const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
|
|
1003
1108
|
const el = resolved.element;
|
|
1004
1109
|
ensureInView(el);
|
|
@@ -1006,9 +1111,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1006
1111
|
el.focus();
|
|
1007
1112
|
}
|
|
1008
1113
|
return true;
|
|
1009
|
-
`)
|
|
1010
|
-
await conn.send("Runtime.evaluate", {
|
|
1011
|
-
expression,
|
|
1114
|
+
`),
|
|
1012
1115
|
returnByValue: true,
|
|
1013
1116
|
awaitPromise: true
|
|
1014
1117
|
});
|
|
@@ -1035,8 +1138,8 @@ var ChromeCdpBrowserController = class {
|
|
|
1035
1138
|
async interact(targetWsUrl, payload) {
|
|
1036
1139
|
if (payload.action === "goBack") return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1037
1140
|
await this.ensureEnabled(targetWsUrl);
|
|
1038
|
-
const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 3e3).catch(() => void 0);
|
|
1039
|
-
await conn
|
|
1141
|
+
const navigatedPromise = (conn.waitForEventMatching ? conn.waitForEventMatching("Page.frameNavigated", (params) => !params.frame?.parentId, 3e3) : conn.waitForEvent("Page.frameNavigated", 3e3)).catch(() => void 0);
|
|
1142
|
+
await evaluateOnPage(conn, {
|
|
1040
1143
|
expression: "history.back()",
|
|
1041
1144
|
returnByValue: true
|
|
1042
1145
|
});
|
|
@@ -1049,8 +1152,8 @@ var ChromeCdpBrowserController = class {
|
|
|
1049
1152
|
});
|
|
1050
1153
|
if (payload.action === "goForward") return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1051
1154
|
await this.ensureEnabled(targetWsUrl);
|
|
1052
|
-
const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 3e3).catch(() => void 0);
|
|
1053
|
-
await conn
|
|
1155
|
+
const navigatedPromise = (conn.waitForEventMatching ? conn.waitForEventMatching("Page.frameNavigated", (params) => !params.frame?.parentId, 3e3) : conn.waitForEvent("Page.frameNavigated", 3e3)).catch(() => void 0);
|
|
1156
|
+
await evaluateOnPage(conn, {
|
|
1054
1157
|
expression: "history.forward()",
|
|
1055
1158
|
returnByValue: true
|
|
1056
1159
|
});
|
|
@@ -1100,7 +1203,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1100
1203
|
}
|
|
1101
1204
|
if (await this.prepareTypeTarget(conn, payload) === "ready") await conn.send("Input.insertText", { text: payload.text ?? "" });
|
|
1102
1205
|
return "typed";
|
|
1103
|
-
});
|
|
1206
|
+
}, "connection");
|
|
1104
1207
|
const expression = buildLocatorRuntimeExpression("payload", payload, `
|
|
1105
1208
|
if (payload.action === 'waitFor') {
|
|
1106
1209
|
const timeout = payload.timeoutMs ?? 2000;
|
|
@@ -1148,12 +1251,12 @@ var ChromeCdpBrowserController = class {
|
|
|
1148
1251
|
`);
|
|
1149
1252
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1150
1253
|
await this.ensureEnabled(targetWsUrl);
|
|
1151
|
-
return (await conn
|
|
1254
|
+
return (await evaluateOnPage(conn, {
|
|
1152
1255
|
expression,
|
|
1153
1256
|
returnByValue: true,
|
|
1154
1257
|
awaitPromise: true
|
|
1155
1258
|
})).result.value ?? "";
|
|
1156
|
-
});
|
|
1259
|
+
}, "connection");
|
|
1157
1260
|
}
|
|
1158
1261
|
async screenshot(targetWsUrl, options) {
|
|
1159
1262
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
@@ -1172,6 +1275,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1172
1275
|
height: metrics.contentSize.height,
|
|
1173
1276
|
scale: 1
|
|
1174
1277
|
};
|
|
1278
|
+
params.captureBeyondViewport = true;
|
|
1175
1279
|
}
|
|
1176
1280
|
return {
|
|
1177
1281
|
data: (await conn.send("Page.captureScreenshot", params)).data,
|
|
@@ -1181,7 +1285,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1181
1285
|
}
|
|
1182
1286
|
async getCurrentUrl(targetWsUrl) {
|
|
1183
1287
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1184
|
-
return (await conn
|
|
1288
|
+
return (await evaluateOnPage(conn, {
|
|
1185
1289
|
expression: "window.location.href",
|
|
1186
1290
|
returnByValue: true
|
|
1187
1291
|
})).result.value ?? "";
|
|
@@ -1229,7 +1333,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1229
1333
|
`);
|
|
1230
1334
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1231
1335
|
await this.ensureEnabled(targetWsUrl);
|
|
1232
|
-
const limited = normalizeRuntimeContentPayload((await conn
|
|
1336
|
+
const limited = normalizeRuntimeContentPayload((await evaluateOnPage(conn, {
|
|
1233
1337
|
expression,
|
|
1234
1338
|
returnByValue: true,
|
|
1235
1339
|
awaitPromise: true
|
|
@@ -1466,7 +1570,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1466
1570
|
`);
|
|
1467
1571
|
const pageSummary = await this.withRetry(targetWsUrl, async (conn) => {
|
|
1468
1572
|
await this.ensureEnabled(targetWsUrl);
|
|
1469
|
-
return (await conn
|
|
1573
|
+
return (await evaluateOnPage(conn, {
|
|
1470
1574
|
expression,
|
|
1471
1575
|
returnByValue: true,
|
|
1472
1576
|
awaitPromise: true
|
|
@@ -1538,10 +1642,11 @@ var ChromeCdpBrowserController = class {
|
|
|
1538
1642
|
let nodes;
|
|
1539
1643
|
if (selector) {
|
|
1540
1644
|
const objectGroup = "agentic-browser-a11y";
|
|
1541
|
-
const selected = await conn
|
|
1645
|
+
const selected = await evaluateOnPage(conn, {
|
|
1542
1646
|
expression: buildLocatorRuntimeExpression("selector", selector, `
|
|
1543
1647
|
return resolveLocator(selector);
|
|
1544
1648
|
`),
|
|
1649
|
+
awaitPromise: true,
|
|
1545
1650
|
objectGroup
|
|
1546
1651
|
});
|
|
1547
1652
|
const objectId = selected.result.objectId;
|
|
@@ -1932,10 +2037,12 @@ var ChromeCdpBrowserController = class {
|
|
|
1932
2037
|
};
|
|
1933
2038
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1934
2039
|
await this.ensureEnabled(targetWsUrl);
|
|
1935
|
-
|
|
2040
|
+
const result = await evaluateOnPage(conn, {
|
|
1936
2041
|
expression,
|
|
1937
|
-
returnByValue: true
|
|
1938
|
-
|
|
2042
|
+
returnByValue: true,
|
|
2043
|
+
awaitPromise: true
|
|
2044
|
+
});
|
|
2045
|
+
return extract(result);
|
|
1939
2046
|
});
|
|
1940
2047
|
}
|
|
1941
2048
|
/**
|
|
@@ -2160,6 +2267,28 @@ var ChromeCdpBrowserController = class {
|
|
|
2160
2267
|
}
|
|
2161
2268
|
return results;
|
|
2162
2269
|
}
|
|
2270
|
+
// Only click buttons that live inside something that looks like a consent
|
|
2271
|
+
// banner: a dialog/modal, an element with consent-ish id/class, or a
|
|
2272
|
+
// fixed/sticky overlay — AND whose container text actually talks about
|
|
2273
|
+
// cookies/consent. Without this the loose text patterns above would click
|
|
2274
|
+
// arbitrary page buttons ("Accept invitation", "I agree" form submits).
|
|
2275
|
+
const containerTextPattern = /cookie|consent|privacy|gdpr|datenschutz|einwillig|zustimm|tracking|personal data|persönliche daten|données personnelles|datos personales/i;
|
|
2276
|
+
function findBannerContainer(el) {
|
|
2277
|
+
let node = el;
|
|
2278
|
+
let depth = 0;
|
|
2279
|
+
while (node && node.nodeType === 1 && depth < 20) {
|
|
2280
|
+
const role = (node.getAttribute('role') || '').toLowerCase();
|
|
2281
|
+
if (role === 'dialog' || role === 'alertdialog') return node;
|
|
2282
|
+
if (node.getAttribute('aria-modal') === 'true') return node;
|
|
2283
|
+
const idClass = (node.id + ' ' + (typeof node.className === 'string' ? node.className : '')).toLowerCase();
|
|
2284
|
+
if (/cookie|consent|gdpr|cmp|privacy|datenschutz/.test(idClass)) return node;
|
|
2285
|
+
const style = getComputedStyle(node);
|
|
2286
|
+
if (style.position === 'fixed' || style.position === 'sticky') return node;
|
|
2287
|
+
node = node.parentElement || (node.getRootNode && node.getRootNode().host) || null;
|
|
2288
|
+
depth++;
|
|
2289
|
+
}
|
|
2290
|
+
return null;
|
|
2291
|
+
}
|
|
2163
2292
|
const candidates = collectButtons(document);
|
|
2164
2293
|
for (const el of candidates) {
|
|
2165
2294
|
if (!isVisible(el)) continue;
|
|
@@ -2167,6 +2296,10 @@ var ChromeCdpBrowserController = class {
|
|
|
2167
2296
|
if (text.length > 60) continue;
|
|
2168
2297
|
for (const pat of patterns) {
|
|
2169
2298
|
if (pat.test(text)) {
|
|
2299
|
+
const container = findBannerContainer(el);
|
|
2300
|
+
if (!container) break;
|
|
2301
|
+
const containerText = (container.textContent || '').slice(0, 4000);
|
|
2302
|
+
if (!containerTextPattern.test(containerText)) break;
|
|
2170
2303
|
el.click();
|
|
2171
2304
|
return JSON.stringify({ dismissed: true, method: 'text', detail: text });
|
|
2172
2305
|
}
|
|
@@ -2177,7 +2310,7 @@ var ChromeCdpBrowserController = class {
|
|
|
2177
2310
|
})()`;
|
|
2178
2311
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
2179
2312
|
await this.ensureEnabled(targetWsUrl);
|
|
2180
|
-
const raw = (await conn
|
|
2313
|
+
const raw = (await evaluateOnPage(conn, {
|
|
2181
2314
|
expression,
|
|
2182
2315
|
returnByValue: true
|
|
2183
2316
|
})).result.value ?? "{\"dismissed\":false}";
|
|
@@ -2261,7 +2394,7 @@ var ChromeCdpBrowserController = class {
|
|
|
2261
2394
|
})()`;
|
|
2262
2395
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
2263
2396
|
await this.ensureEnabled(targetWsUrl);
|
|
2264
|
-
const raw = (await conn
|
|
2397
|
+
const raw = (await evaluateOnPage(conn, {
|
|
2265
2398
|
expression,
|
|
2266
2399
|
returnByValue: true
|
|
2267
2400
|
})).result.value ?? "{\"dismissed\":false}";
|
|
@@ -2282,14 +2415,21 @@ var ChromeCdpBrowserController = class {
|
|
|
2282
2415
|
await conn.send("Accessibility.enable");
|
|
2283
2416
|
await conn.send("Page.enable");
|
|
2284
2417
|
const { nodes } = await conn.send("Accessibility.getFullAXTree");
|
|
2285
|
-
const
|
|
2418
|
+
const nodeSets = [{
|
|
2419
|
+
nodes,
|
|
2420
|
+
trusted: false
|
|
2421
|
+
}];
|
|
2286
2422
|
try {
|
|
2287
2423
|
const { frameTree } = await conn.send("Page.getFrameTree");
|
|
2288
2424
|
for (const child of frameTree.childFrames ?? []) {
|
|
2289
2425
|
const url = child.frame.url ?? "";
|
|
2290
|
-
|
|
2426
|
+
const consentFrame = /consent|cookie|privacy|sourcepoint|didomi|cmp|gdpr|trustarc|consentmanager|sp_message|onetrust|usercentrics|consent-cdn/i.test(url);
|
|
2427
|
+
if (consentFrame || url === "about:blank") try {
|
|
2291
2428
|
const frameResult = await conn.send("Accessibility.getFullAXTree", { frameId: child.frame.id });
|
|
2292
|
-
|
|
2429
|
+
nodeSets.push({
|
|
2430
|
+
nodes: frameResult.nodes,
|
|
2431
|
+
trusted: consentFrame
|
|
2432
|
+
});
|
|
2293
2433
|
} catch {}
|
|
2294
2434
|
}
|
|
2295
2435
|
} catch {}
|
|
@@ -2326,21 +2466,38 @@ var ChromeCdpBrowserController = class {
|
|
|
2326
2466
|
/accepter(\s+alle)?/i,
|
|
2327
2467
|
/aksepter(\s+alle)?/i
|
|
2328
2468
|
];
|
|
2469
|
+
const consentAncestorName = /cookie|consent|privacy|gdpr|datenschutz|einwillig|zustimm/i;
|
|
2470
|
+
const hasConsentAncestor = (byId, node) => {
|
|
2471
|
+
let current = node.parentId ? byId.get(node.parentId) : void 0;
|
|
2472
|
+
let depth = 0;
|
|
2473
|
+
while (current && depth < 50) {
|
|
2474
|
+
const role = current.role?.value;
|
|
2475
|
+
if (role === "dialog" || role === "alertdialog") return true;
|
|
2476
|
+
const name = current.name?.value;
|
|
2477
|
+
if (name && consentAncestorName.test(name)) return true;
|
|
2478
|
+
current = current.parentId ? byId.get(current.parentId) : void 0;
|
|
2479
|
+
depth++;
|
|
2480
|
+
}
|
|
2481
|
+
return false;
|
|
2482
|
+
};
|
|
2329
2483
|
const candidates = [];
|
|
2330
|
-
for (const
|
|
2331
|
-
|
|
2332
|
-
const
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2484
|
+
for (const { nodes: setNodes, trusted } of nodeSets) {
|
|
2485
|
+
const byId = new Map(setNodes.map((n) => [n.nodeId, n]));
|
|
2486
|
+
for (const node of setNodes) {
|
|
2487
|
+
if (node.ignored) continue;
|
|
2488
|
+
const role = node.role?.value;
|
|
2489
|
+
if (role !== "button" && role !== "link") continue;
|
|
2490
|
+
const name = node.name?.value?.trim();
|
|
2491
|
+
if (!name || name.length > 50) continue;
|
|
2492
|
+
if (node.properties?.some((p) => p.name === "disabled" && p.value.value === true)) continue;
|
|
2493
|
+
for (const pat of consentPatterns) if (pat.test(name) && node.backendDOMNodeId) {
|
|
2494
|
+
if (trusted || hasConsentAncestor(byId, node)) candidates.push({
|
|
2495
|
+
nodeId: node.nodeId,
|
|
2496
|
+
backendDOMNodeId: node.backendDOMNodeId,
|
|
2497
|
+
name
|
|
2498
|
+
});
|
|
2499
|
+
break;
|
|
2500
|
+
}
|
|
2344
2501
|
}
|
|
2345
2502
|
}
|
|
2346
2503
|
if (candidates.length === 0) return { dismissed: false };
|
|
@@ -2594,7 +2751,6 @@ z.object({
|
|
|
2594
2751
|
//#region src/session/session-store.ts
|
|
2595
2752
|
var SessionStore = class {
|
|
2596
2753
|
filePath;
|
|
2597
|
-
cache = null;
|
|
2598
2754
|
constructor(baseDir) {
|
|
2599
2755
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
2600
2756
|
this.filePath = path.join(baseDir, "sessions.json");
|
|
@@ -2662,25 +2818,35 @@ var SessionStore = class {
|
|
|
2662
2818
|
this.write(state);
|
|
2663
2819
|
}
|
|
2664
2820
|
read() {
|
|
2665
|
-
|
|
2821
|
+
let raw;
|
|
2822
|
+
try {
|
|
2823
|
+
raw = fs.readFileSync(this.filePath, "utf8");
|
|
2824
|
+
} catch (error) {
|
|
2825
|
+
if (error.code === "ENOENT") return { sessions: {} };
|
|
2826
|
+
throw error;
|
|
2827
|
+
}
|
|
2666
2828
|
try {
|
|
2667
|
-
|
|
2668
|
-
this.cache = parsed;
|
|
2669
|
-
return parsed;
|
|
2829
|
+
return JSON.parse(raw);
|
|
2670
2830
|
} catch {
|
|
2831
|
+
try {
|
|
2832
|
+
fs.copyFileSync(this.filePath, `${this.filePath}.corrupt`);
|
|
2833
|
+
} catch {}
|
|
2671
2834
|
const empty = { sessions: {} };
|
|
2672
2835
|
this.write(empty);
|
|
2673
2836
|
return empty;
|
|
2674
2837
|
}
|
|
2675
2838
|
}
|
|
2676
2839
|
write(state) {
|
|
2677
|
-
|
|
2678
|
-
fs.writeFileSync(
|
|
2840
|
+
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
2841
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
|
|
2842
|
+
fs.renameSync(tmp, this.filePath);
|
|
2679
2843
|
}
|
|
2680
2844
|
};
|
|
2681
2845
|
//#endregion
|
|
2682
2846
|
//#region src/session/session-manager.ts
|
|
2683
2847
|
var SessionManager = class {
|
|
2848
|
+
ctx;
|
|
2849
|
+
browser;
|
|
2684
2850
|
store;
|
|
2685
2851
|
constructor(ctx, browser) {
|
|
2686
2852
|
this.ctx = ctx;
|
|
@@ -2773,7 +2939,7 @@ var SessionManager = class {
|
|
|
2773
2939
|
} else if (input.type === "terminate") {
|
|
2774
2940
|
await this.terminateSession(sessionId);
|
|
2775
2941
|
resultMessage = "Session terminated";
|
|
2776
|
-
}
|
|
2942
|
+
} else throw new Error(`Unsupported command type: ${String(input.type)}`);
|
|
2777
2943
|
} catch (error) {
|
|
2778
2944
|
resultStatus = "failed";
|
|
2779
2945
|
resultMessage = error.message;
|
|
@@ -2876,6 +3042,7 @@ var SessionManager = class {
|
|
|
2876
3042
|
}
|
|
2877
3043
|
async restartSession(sessionId) {
|
|
2878
3044
|
const record = this.mustGetRecord(sessionId);
|
|
3045
|
+
if (record.session.status === "terminated") throw new Error("Session is terminated. Start a new session.");
|
|
2879
3046
|
if (this.browser.closeConnection) try {
|
|
2880
3047
|
this.browser.closeConnection(record.targetWsUrl);
|
|
2881
3048
|
} catch {}
|
|
@@ -2907,6 +3074,7 @@ var SessionManager = class {
|
|
|
2907
3074
|
}
|
|
2908
3075
|
rotateAuthToken(sessionId) {
|
|
2909
3076
|
const record = this.mustGetRecord(sessionId);
|
|
3077
|
+
if (record.session.status === "terminated") throw new Error("Session is terminated. Start a new session.");
|
|
2910
3078
|
this.ctx.tokenService.revoke(sessionId);
|
|
2911
3079
|
const nextToken = this.ctx.tokenService.issue(sessionId);
|
|
2912
3080
|
const updated = {
|
|
@@ -3018,8 +3186,10 @@ var SessionManager = class {
|
|
|
3018
3186
|
const record = this.store.get(sessionId);
|
|
3019
3187
|
if (!record) throw new Error("Session not found");
|
|
3020
3188
|
if (!record.targetWsUrl) throw new Error("Session target is missing. Restart the session.");
|
|
3021
|
-
|
|
3022
|
-
|
|
3189
|
+
if (record.session.status !== "terminated") {
|
|
3190
|
+
const seeded = this.ctx.tokenService.get(sessionId);
|
|
3191
|
+
if (!seeded || seeded !== record.session.authTokenRef) this.ctx.tokenService.seed(sessionId, record.session.authTokenRef);
|
|
3192
|
+
}
|
|
3023
3193
|
return record;
|
|
3024
3194
|
}
|
|
3025
3195
|
recordEvent(sessionId, category, severity, message) {
|
|
@@ -3079,6 +3249,7 @@ var SessionManager = class {
|
|
|
3079
3249
|
//#endregion
|
|
3080
3250
|
//#region src/transport/control-api.ts
|
|
3081
3251
|
var ControlApi = class {
|
|
3252
|
+
sessions;
|
|
3082
3253
|
constructor(sessions) {
|
|
3083
3254
|
this.sessions = sessions;
|
|
3084
3255
|
}
|
|
@@ -3136,21 +3307,12 @@ var ControlApi = class {
|
|
|
3136
3307
|
};
|
|
3137
3308
|
//#endregion
|
|
3138
3309
|
//#region src/lib/config.ts
|
|
3139
|
-
const DEFAULT_PORT = 43111;
|
|
3140
|
-
const DEFAULT_TIMEOUT_MS = 2e3;
|
|
3141
3310
|
function loadConfig(env = process.env) {
|
|
3142
|
-
const wsPort = Number.parseInt(env.AGENTIC_BROWSER_WS_PORT ?? `${DEFAULT_PORT}`, 10);
|
|
3143
|
-
const commandTimeoutMs = Number.parseInt(env.AGENTIC_BROWSER_COMMAND_TIMEOUT_MS ?? `${DEFAULT_TIMEOUT_MS}`, 10);
|
|
3144
|
-
if (Number.isNaN(wsPort) || wsPort <= 0) throw new Error("AGENTIC_BROWSER_WS_PORT must be a positive integer");
|
|
3145
|
-
if (Number.isNaN(commandTimeoutMs) || commandTimeoutMs <= 0) throw new Error("AGENTIC_BROWSER_COMMAND_TIMEOUT_MS must be a positive integer");
|
|
3146
3311
|
const userProfile = env.AGENTIC_BROWSER_USER_PROFILE;
|
|
3147
3312
|
let userProfileDir;
|
|
3148
3313
|
if (userProfile === "true" || userProfile === "default") userProfileDir = "default";
|
|
3149
3314
|
else if (userProfile && path.isAbsolute(userProfile)) userProfileDir = userProfile;
|
|
3150
3315
|
return {
|
|
3151
|
-
host: env.AGENTIC_BROWSER_HOST ?? "127.0.0.1",
|
|
3152
|
-
wsPort,
|
|
3153
|
-
commandTimeoutMs,
|
|
3154
3316
|
dataDir: env.AGENTIC_BROWSER_DIR ?? path.join(os.homedir(), ".agentic-browser"),
|
|
3155
3317
|
browserExecutablePath: env.AGENTIC_BROWSER_CHROME_EXECUTABLE_PATH ?? env.AGENTIC_BROWSER_CHROME_PATH,
|
|
3156
3318
|
cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
|
|
@@ -3162,6 +3324,7 @@ function loadConfig(env = process.env) {
|
|
|
3162
3324
|
//#endregion
|
|
3163
3325
|
//#region src/observability/logger.ts
|
|
3164
3326
|
var Logger = class {
|
|
3327
|
+
namespace;
|
|
3165
3328
|
constructor(namespace) {
|
|
3166
3329
|
this.namespace = namespace;
|
|
3167
3330
|
}
|
|
@@ -3198,17 +3361,28 @@ var EventStore = class {
|
|
|
3198
3361
|
filePath;
|
|
3199
3362
|
bufferedLines = [];
|
|
3200
3363
|
flushTimer;
|
|
3364
|
+
pendingFlushDelayMs = Number.POSITIVE_INFINITY;
|
|
3201
3365
|
flushInFlight;
|
|
3202
3366
|
constructor(baseDir) {
|
|
3203
3367
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
3204
3368
|
this.filePath = path.join(baseDir, "session-events.log");
|
|
3205
|
-
process.
|
|
3206
|
-
process.
|
|
3369
|
+
process.on("beforeExit", this.flushSync);
|
|
3370
|
+
process.on("exit", this.flushSync);
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Remove the process exit handlers and flush any buffered lines.
|
|
3374
|
+
* Call when discarding a store (repeated context creation in tests/embedders
|
|
3375
|
+
* would otherwise leak a listener pair per instance).
|
|
3376
|
+
*/
|
|
3377
|
+
dispose() {
|
|
3378
|
+
process.off("beforeExit", this.flushSync);
|
|
3379
|
+
process.off("exit", this.flushSync);
|
|
3380
|
+
this.flushSync();
|
|
3207
3381
|
}
|
|
3208
3382
|
append(event) {
|
|
3209
3383
|
let existing = this.events.get(event.sessionId) ?? [];
|
|
3210
3384
|
existing.push(event);
|
|
3211
|
-
if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-
|
|
3385
|
+
if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-500);
|
|
3212
3386
|
this.events.set(event.sessionId, existing);
|
|
3213
3387
|
this.bufferedLines.push(`${JSON.stringify(event)}\n`);
|
|
3214
3388
|
if (this.bufferedLines.length >= 20) {
|
|
@@ -3226,7 +3400,12 @@ var EventStore = class {
|
|
|
3226
3400
|
}
|
|
3227
3401
|
flushSoon(delayMs) {
|
|
3228
3402
|
if (this.flushInFlight) return this.flushInFlight;
|
|
3229
|
-
if (this.flushTimer)
|
|
3403
|
+
if (this.flushTimer) {
|
|
3404
|
+
if (delayMs >= this.pendingFlushDelayMs) return Promise.resolve();
|
|
3405
|
+
clearTimeout(this.flushTimer);
|
|
3406
|
+
this.flushTimer = void 0;
|
|
3407
|
+
}
|
|
3408
|
+
this.pendingFlushDelayMs = delayMs;
|
|
3230
3409
|
this.flushTimer = setTimeout(() => {
|
|
3231
3410
|
this.flushTimer = void 0;
|
|
3232
3411
|
this.flushAsync();
|
|
@@ -3283,40 +3462,6 @@ var SessionTokenService = class {
|
|
|
3283
3462
|
}
|
|
3284
3463
|
};
|
|
3285
3464
|
//#endregion
|
|
3286
|
-
//#region src/transport/ws-server.ts
|
|
3287
|
-
var AuthenticatedWsServer = class {
|
|
3288
|
-
logger = new Logger("ws-server");
|
|
3289
|
-
server;
|
|
3290
|
-
constructor(options) {
|
|
3291
|
-
this.options = options;
|
|
3292
|
-
}
|
|
3293
|
-
start(onMessage) {
|
|
3294
|
-
this.server = new WebSocketServer({
|
|
3295
|
-
host: this.options.host,
|
|
3296
|
-
port: this.options.port
|
|
3297
|
-
});
|
|
3298
|
-
this.server.on("connection", (socket, request) => {
|
|
3299
|
-
const url = new URL(request.url ?? "/", `http://${this.options.host}:${this.options.port}`);
|
|
3300
|
-
const sessionId = url.searchParams.get("sessionId");
|
|
3301
|
-
const token = url.searchParams.get("token");
|
|
3302
|
-
if (!sessionId || !token || !this.options.tokenService.validate(sessionId, token)) {
|
|
3303
|
-
this.logger.warning("Rejected websocket client", { sessionId });
|
|
3304
|
-
socket.close(4401, "Unauthorized");
|
|
3305
|
-
return;
|
|
3306
|
-
}
|
|
3307
|
-
socket.on("message", (payload) => onMessage(socket, payload.toString("utf8")));
|
|
3308
|
-
});
|
|
3309
|
-
this.logger.info("WebSocket server started", {
|
|
3310
|
-
host: this.options.host,
|
|
3311
|
-
port: this.options.port
|
|
3312
|
-
});
|
|
3313
|
-
}
|
|
3314
|
-
stop() {
|
|
3315
|
-
this.server?.close();
|
|
3316
|
-
this.server = void 0;
|
|
3317
|
-
}
|
|
3318
|
-
};
|
|
3319
|
-
//#endregion
|
|
3320
3465
|
//#region src/memory/memory-index.ts
|
|
3321
3466
|
function normalize(value) {
|
|
3322
3467
|
return value.trim().toLowerCase();
|
|
@@ -3357,10 +3502,11 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup
|
|
|
3357
3502
|
const insightIntent = normalize(insight.taskIntent);
|
|
3358
3503
|
const intentMatch = insightIntent === normalizedIntent ? 1 : 0;
|
|
3359
3504
|
const intentPartial = intentMatch === 1 || insightIntent.includes(normalizedIntent) || normalizedIntent.includes(insightIntent) ? .65 : 0;
|
|
3505
|
+
const intentSignal = Math.max(intentMatch, intentPartial);
|
|
3360
3506
|
const domainMatch = normalizedDomain && normalize(insight.siteDomain) === normalizedDomain ? 1 : normalizedDomain ? 0 : .6;
|
|
3361
3507
|
const reliability = .6 * confidenceFromCounts(insight.successCount, insight.failureCount) + .4 * freshnessWeight(insight.freshness);
|
|
3362
3508
|
const selectorQuality = selectorSignal(insight);
|
|
3363
|
-
const score = .5 *
|
|
3509
|
+
const score = .5 * intentSignal + .2 * domainMatch + .15 * reliability + .15 * selectorQuality;
|
|
3364
3510
|
const result = {
|
|
3365
3511
|
insightId: insight.insightId,
|
|
3366
3512
|
taskIntent: insight.taskIntent,
|
|
@@ -3379,7 +3525,10 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup
|
|
|
3379
3525
|
cookieBanner: profile.cookieBanner,
|
|
3380
3526
|
visitCount: profile.visitCount
|
|
3381
3527
|
};
|
|
3382
|
-
return
|
|
3528
|
+
return {
|
|
3529
|
+
result,
|
|
3530
|
+
intentSignal
|
|
3531
|
+
};
|
|
3383
3532
|
}
|
|
3384
3533
|
var MemoryIndex = class {
|
|
3385
3534
|
/** Domain → insights index, rebuilt lazily when insight list changes. */
|
|
@@ -3406,18 +3555,20 @@ var MemoryIndex = class {
|
|
|
3406
3555
|
const limit = input.limit ?? 10;
|
|
3407
3556
|
this.ensureIndex(insights);
|
|
3408
3557
|
const candidates = normalizedDomain ? this.domainIndex.get(normalizedDomain) ?? [] : insights;
|
|
3558
|
+
const relevant = (scored) => scored.intentSignal > 0;
|
|
3409
3559
|
if (limit === 1 && normalizedDomain) {
|
|
3410
3560
|
let best;
|
|
3411
3561
|
for (const insight of candidates) {
|
|
3412
|
-
const
|
|
3413
|
-
if (
|
|
3414
|
-
|
|
3415
|
-
|
|
3562
|
+
const scored = scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup);
|
|
3563
|
+
if (!relevant(scored)) continue;
|
|
3564
|
+
if (!best || scored.result.score > best.result.score) {
|
|
3565
|
+
best = scored;
|
|
3566
|
+
if (best.result.score >= 1) break;
|
|
3416
3567
|
}
|
|
3417
3568
|
}
|
|
3418
|
-
return best ? [best] : [];
|
|
3569
|
+
return best ? [best.result] : [];
|
|
3419
3570
|
}
|
|
3420
|
-
return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup)).filter(
|
|
3571
|
+
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);
|
|
3421
3572
|
}
|
|
3422
3573
|
};
|
|
3423
3574
|
//#endregion
|
|
@@ -3584,11 +3735,15 @@ var SiteProfileStore = class {
|
|
|
3584
3735
|
return this.getCache().find((p) => p.domain === normalized);
|
|
3585
3736
|
}
|
|
3586
3737
|
upsert(profile) {
|
|
3587
|
-
|
|
3738
|
+
const normalized = {
|
|
3739
|
+
...profile,
|
|
3740
|
+
domain: profile.domain.toLowerCase()
|
|
3741
|
+
};
|
|
3742
|
+
SiteProfileSchema.parse(normalized);
|
|
3588
3743
|
const profiles = this.getCache();
|
|
3589
|
-
const index = profiles.findIndex((p) => p.domain ===
|
|
3590
|
-
if (index >= 0) profiles[index] =
|
|
3591
|
-
else profiles.push(
|
|
3744
|
+
const index = profiles.findIndex((p) => p.domain === normalized.domain);
|
|
3745
|
+
if (index >= 0) profiles[index] = normalized;
|
|
3746
|
+
else profiles.push(normalized);
|
|
3592
3747
|
this.markDirty();
|
|
3593
3748
|
}
|
|
3594
3749
|
flushSync() {
|
|
@@ -3603,7 +3758,8 @@ var SiteProfileStore = class {
|
|
|
3603
3758
|
}
|
|
3604
3759
|
getCache() {
|
|
3605
3760
|
if (this.cached) return this.cached;
|
|
3606
|
-
|
|
3761
|
+
const state = this.readDisk();
|
|
3762
|
+
this.cached = state.profiles;
|
|
3607
3763
|
return this.cached;
|
|
3608
3764
|
}
|
|
3609
3765
|
markDirty() {
|
|
@@ -3626,7 +3782,7 @@ var SiteProfileStore = class {
|
|
|
3626
3782
|
} catch {
|
|
3627
3783
|
const obj = raw;
|
|
3628
3784
|
if (Array.isArray(obj?.profiles)) {
|
|
3629
|
-
const salvaged = obj.profiles.
|
|
3785
|
+
const salvaged = obj.profiles.map((item) => SiteProfileSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
|
|
3630
3786
|
if (salvaged.length > 0) {
|
|
3631
3787
|
const state = { profiles: salvaged };
|
|
3632
3788
|
this.backupAndReset();
|
|
@@ -3706,6 +3862,15 @@ var TaskInsightStore = class {
|
|
|
3706
3862
|
else insights.push(insight);
|
|
3707
3863
|
this.markDirty();
|
|
3708
3864
|
}
|
|
3865
|
+
/** Remove an insight by id. Returns true if a record was removed. */
|
|
3866
|
+
remove(insightId) {
|
|
3867
|
+
const insights = this.getCache();
|
|
3868
|
+
const index = insights.findIndex((entry) => entry.insightId === insightId);
|
|
3869
|
+
if (index < 0) return false;
|
|
3870
|
+
insights.splice(index, 1);
|
|
3871
|
+
this.markDirty();
|
|
3872
|
+
return true;
|
|
3873
|
+
}
|
|
3709
3874
|
replaceMany(insights) {
|
|
3710
3875
|
for (const insight of insights) TaskInsightSchema.parse(insight);
|
|
3711
3876
|
this.cached = insights;
|
|
@@ -3725,7 +3890,8 @@ var TaskInsightStore = class {
|
|
|
3725
3890
|
/** Return the in-memory cache, loading from disk on first access. */
|
|
3726
3891
|
getCache() {
|
|
3727
3892
|
if (this.cached) return this.cached;
|
|
3728
|
-
|
|
3893
|
+
const state = this.readDisk();
|
|
3894
|
+
this.cached = state.insights;
|
|
3729
3895
|
return this.cached;
|
|
3730
3896
|
}
|
|
3731
3897
|
markDirty() {
|
|
@@ -3749,7 +3915,7 @@ var TaskInsightStore = class {
|
|
|
3749
3915
|
} catch {
|
|
3750
3916
|
const obj = raw;
|
|
3751
3917
|
if (Array.isArray(obj?.insights)) {
|
|
3752
|
-
const salvaged = obj.insights.
|
|
3918
|
+
const salvaged = obj.insights.map((item) => TaskInsightSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
|
|
3753
3919
|
if (salvaged.length > 0) {
|
|
3754
3920
|
const state = { insights: salvaged };
|
|
3755
3921
|
this.backupAndReset();
|
|
@@ -3796,8 +3962,6 @@ var TaskInsightStore = class {
|
|
|
3796
3962
|
//#region src/memory/memory-service.ts
|
|
3797
3963
|
/** Memory service configuration */
|
|
3798
3964
|
const SEARCH_CACHE_TTL_MS = 2e3;
|
|
3799
|
-
const MAX_EVIDENCE_RECORDS = 50;
|
|
3800
|
-
const MAX_RECIPE_STEPS = 8;
|
|
3801
3965
|
const MAX_SELECTOR_ALIASES = 10;
|
|
3802
3966
|
/** Site profile limits */
|
|
3803
3967
|
const MAX_NAVIGATION_PATHS = 20;
|
|
@@ -3875,6 +4039,8 @@ var MemoryService = class {
|
|
|
3875
4039
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3876
4040
|
const verified = {
|
|
3877
4041
|
...insight,
|
|
4042
|
+
freshness: "fresh",
|
|
4043
|
+
staleStrikeCount: 0,
|
|
3878
4044
|
lastVerifiedAt: now,
|
|
3879
4045
|
updatedAt: now
|
|
3880
4046
|
};
|
|
@@ -3916,7 +4082,7 @@ var MemoryService = class {
|
|
|
3916
4082
|
const refreshed = applySuccess({
|
|
3917
4083
|
...matched,
|
|
3918
4084
|
useCount: matched.useCount + 1,
|
|
3919
|
-
evidence: [...matched.evidence.slice(-
|
|
4085
|
+
evidence: [...matched.evidence.slice(-49), evidence],
|
|
3920
4086
|
actionRecipe: this.mergeRecipe(matched.actionRecipe, input.step),
|
|
3921
4087
|
expectedOutcome: input.expectedOutcome
|
|
3922
4088
|
});
|
|
@@ -3932,6 +4098,7 @@ var MemoryService = class {
|
|
|
3932
4098
|
};
|
|
3933
4099
|
versioned = this.maybeGenerateAliases(versioned);
|
|
3934
4100
|
this.store.upsert(versioned);
|
|
4101
|
+
this.store.remove(matched.insightId);
|
|
3935
4102
|
this.invalidateSearchCache();
|
|
3936
4103
|
return versioned;
|
|
3937
4104
|
}
|
|
@@ -3950,7 +4117,7 @@ var MemoryService = class {
|
|
|
3950
4117
|
const failed = applyFailure({
|
|
3951
4118
|
...matched,
|
|
3952
4119
|
useCount: matched.useCount + 1,
|
|
3953
|
-
evidence: [...matched.evidence.slice(-
|
|
4120
|
+
evidence: [...matched.evidence.slice(-49), evidence]
|
|
3954
4121
|
}, signal);
|
|
3955
4122
|
this.store.upsert(failed);
|
|
3956
4123
|
this.invalidateSearchCache();
|
|
@@ -4117,26 +4284,18 @@ var MemoryService = class {
|
|
|
4117
4284
|
}
|
|
4118
4285
|
mergeRecipe(recipe, step) {
|
|
4119
4286
|
if (recipe.find((existing) => existing.summary === step.summary && existing.selector === step.selector)) return recipe;
|
|
4120
|
-
return [...recipe, step].slice(-
|
|
4287
|
+
return [...recipe, step].slice(-8);
|
|
4121
4288
|
}
|
|
4122
4289
|
};
|
|
4123
4290
|
//#endregion
|
|
4124
4291
|
//#region src/cli/app.ts
|
|
4125
4292
|
function createAppContext(env = process.env) {
|
|
4126
4293
|
const config = loadConfig(env);
|
|
4127
|
-
const logger = new Logger("app");
|
|
4128
|
-
const eventStore = new EventStore(config.dataDir);
|
|
4129
|
-
const tokenService = new SessionTokenService();
|
|
4130
4294
|
return {
|
|
4131
4295
|
config,
|
|
4132
|
-
logger,
|
|
4133
|
-
eventStore,
|
|
4134
|
-
tokenService,
|
|
4135
|
-
wsServer: new AuthenticatedWsServer({
|
|
4136
|
-
host: config.host,
|
|
4137
|
-
port: config.wsPort,
|
|
4138
|
-
tokenService
|
|
4139
|
-
}),
|
|
4296
|
+
logger: new Logger("app"),
|
|
4297
|
+
eventStore: new EventStore(config.dataDir),
|
|
4298
|
+
tokenService: new SessionTokenService(),
|
|
4140
4299
|
memoryService: new MemoryService(config.dataDir)
|
|
4141
4300
|
};
|
|
4142
4301
|
}
|