agentic-browser 2.5.0 → 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 +6 -5
- package/dist/index.d.mts +75 -88
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.mjs +112 -24
- package/dist/{runtime-C3Y-UgC1.mjs → runtime-BzdbtFF4.mjs} +461 -171
- package/package.json +19 -19
- /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) => {
|
|
@@ -175,6 +196,10 @@ async function getJson(url, timeoutMs = 5e3) {
|
|
|
175
196
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);
|
|
176
197
|
return await response.json();
|
|
177
198
|
}
|
|
199
|
+
/** Debugger connection timing */
|
|
200
|
+
const DEBUGGER_READY_TIMEOUT_MS = 15e3;
|
|
201
|
+
const DEBUGGER_INITIAL_RETRY_DELAY_MS = 50;
|
|
202
|
+
const DEBUGGER_MAX_RETRY_DELAY_MS = 250;
|
|
178
203
|
/** Check if the debug port is accepting TCP connections (faster than an HTTP fetch). */
|
|
179
204
|
function probePort(port) {
|
|
180
205
|
return new Promise((resolve) => {
|
|
@@ -193,16 +218,15 @@ function probePort(port) {
|
|
|
193
218
|
});
|
|
194
219
|
}
|
|
195
220
|
async function waitForDebugger(port) {
|
|
196
|
-
const maxMs = 15e3;
|
|
197
221
|
const start = Date.now();
|
|
198
|
-
let delay =
|
|
199
|
-
while (Date.now() - start <
|
|
222
|
+
let delay = DEBUGGER_INITIAL_RETRY_DELAY_MS;
|
|
223
|
+
while (Date.now() - start < DEBUGGER_READY_TIMEOUT_MS) {
|
|
200
224
|
if (await probePort(port)) try {
|
|
201
225
|
await getJson(`http://127.0.0.1:${port}/json/version`);
|
|
202
226
|
return;
|
|
203
227
|
} catch {}
|
|
204
228
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
205
|
-
delay = Math.min(delay * 2,
|
|
229
|
+
delay = Math.min(delay * 2, DEBUGGER_MAX_RETRY_DELAY_MS);
|
|
206
230
|
}
|
|
207
231
|
throw new Error("Chrome debug endpoint did not become ready in time");
|
|
208
232
|
}
|
|
@@ -228,7 +252,12 @@ async function createTarget(cdpUrl, url = "about:blank") {
|
|
|
228
252
|
* Apply stealth patches that remove common automation fingerprints.
|
|
229
253
|
* These run before any page loads so sites cannot detect the bot.
|
|
230
254
|
*/
|
|
231
|
-
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
|
+
|
|
232
261
|
// Remove navigator.webdriver flag
|
|
233
262
|
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
234
263
|
|
|
@@ -270,8 +299,13 @@ const STEALTH_SCRIPT = `
|
|
|
270
299
|
return origQuery.call(this, params);
|
|
271
300
|
};
|
|
272
301
|
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
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
|
+
*/
|
|
275
309
|
async function initTarget(targetWsUrl, userAgent) {
|
|
276
310
|
const conn = await CdpConnection.connect(targetWsUrl);
|
|
277
311
|
try {
|
|
@@ -288,18 +322,38 @@ async function initTarget(targetWsUrl, userAgent) {
|
|
|
288
322
|
returnByValue: true,
|
|
289
323
|
awaitPromise: true
|
|
290
324
|
});
|
|
291
|
-
if (userAgent)
|
|
292
|
-
|
|
325
|
+
if (userAgent) {
|
|
326
|
+
await conn.send("Network.setUserAgentOverride", { userAgent });
|
|
327
|
+
return userAgent;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
293
330
|
const { userAgent: currentUA } = await conn.send("Browser.getVersion");
|
|
294
331
|
if (currentUA?.includes("HeadlessChrome")) {
|
|
295
332
|
const cleanUA = currentUA.replace(/HeadlessChrome/g, "Chrome");
|
|
296
333
|
await conn.send("Network.setUserAgentOverride", { userAgent: cleanUA });
|
|
334
|
+
return cleanUA;
|
|
297
335
|
}
|
|
298
336
|
} catch {}
|
|
337
|
+
return;
|
|
299
338
|
} finally {
|
|
300
339
|
conn.close();
|
|
301
340
|
}
|
|
302
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
|
+
}
|
|
303
357
|
async function getFreePort() {
|
|
304
358
|
return await new Promise((resolve, reject) => {
|
|
305
359
|
const server = net.createServer();
|
|
@@ -532,7 +586,7 @@ ${LOCATOR_RUNTIME_HELPERS}
|
|
|
532
586
|
${body}
|
|
533
587
|
})()`;
|
|
534
588
|
}
|
|
535
|
-
const SPECIAL_KEY_PAYLOADS = new Map([
|
|
589
|
+
const SPECIAL_KEY_PAYLOADS = /* @__PURE__ */ new Map([
|
|
536
590
|
["Enter", {
|
|
537
591
|
key: "Enter",
|
|
538
592
|
code: "Enter",
|
|
@@ -694,18 +748,45 @@ function buildKeyDispatchPayload(inputKey) {
|
|
|
694
748
|
nativeVirtualKeyCode: 0
|
|
695
749
|
};
|
|
696
750
|
}
|
|
751
|
+
/** Connection pool configuration */
|
|
752
|
+
const CONNECTION_IDLE_TIMEOUT_MS = 300 * 1e3;
|
|
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
|
+
}
|
|
697
764
|
var ChromeCdpBrowserController = class {
|
|
765
|
+
baseDir;
|
|
766
|
+
connectionFactory;
|
|
698
767
|
connections = /* @__PURE__ */ new Map();
|
|
699
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();
|
|
700
776
|
constructor(baseDir, connectionFactory = CdpConnection.connect) {
|
|
701
777
|
this.baseDir = baseDir;
|
|
702
778
|
this.connectionFactory = connectionFactory;
|
|
703
779
|
this.cleanupInterval = setInterval(() => {
|
|
704
|
-
const cutoff = Date.now() -
|
|
780
|
+
const cutoff = Date.now() - CONNECTION_IDLE_TIMEOUT_MS;
|
|
705
781
|
for (const [url, entry] of this.connections) if (entry.lastUsedAt < cutoff) this.dropConnection(url);
|
|
706
|
-
},
|
|
782
|
+
}, CONNECTION_CLEANUP_INTERVAL_MS);
|
|
707
783
|
this.cleanupInterval.unref();
|
|
708
784
|
}
|
|
785
|
+
/** Stop the cleanup interval and close all connections. Call when disposing the controller. */
|
|
786
|
+
dispose() {
|
|
787
|
+
clearInterval(this.cleanupInterval);
|
|
788
|
+
for (const url of this.connections.keys()) this.dropConnection(url);
|
|
789
|
+
}
|
|
709
790
|
async getConnection(targetWsUrl) {
|
|
710
791
|
const cached = this.connections.get(targetWsUrl);
|
|
711
792
|
if (cached) {
|
|
@@ -713,6 +794,7 @@ var ChromeCdpBrowserController = class {
|
|
|
713
794
|
return cached.conn;
|
|
714
795
|
}
|
|
715
796
|
const conn = await this.connectionFactory(targetWsUrl);
|
|
797
|
+
await this.applySessionOverrides(conn, targetWsUrl);
|
|
716
798
|
this.connections.set(targetWsUrl, {
|
|
717
799
|
conn,
|
|
718
800
|
enabled: {
|
|
@@ -723,6 +805,26 @@ var ChromeCdpBrowserController = class {
|
|
|
723
805
|
});
|
|
724
806
|
return conn;
|
|
725
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
|
+
}
|
|
726
828
|
dropConnection(targetWsUrl) {
|
|
727
829
|
const cached = this.connections.get(targetWsUrl);
|
|
728
830
|
if (!cached) return;
|
|
@@ -740,7 +842,8 @@ var ChromeCdpBrowserController = class {
|
|
|
740
842
|
if (!port) throw new Error(`Invalid CDP URL: could not extract port from ${cdpUrl}`);
|
|
741
843
|
await waitForDebugger(port);
|
|
742
844
|
const targetWsUrl = await createTarget(cdpUrl);
|
|
743
|
-
await initTarget(targetWsUrl, options?.userAgent);
|
|
845
|
+
const appliedUa = await initTarget(targetWsUrl, options?.userAgent);
|
|
846
|
+
if (appliedUa) this.userAgentOverrides.set(targetWsUrl, appliedUa);
|
|
744
847
|
return {
|
|
745
848
|
pid: 0,
|
|
746
849
|
cdpUrl,
|
|
@@ -808,7 +911,8 @@ var ChromeCdpBrowserController = class {
|
|
|
808
911
|
const cdpUrl = `http://127.0.0.1:${port}`;
|
|
809
912
|
const targetWsUrl = await createTarget(cdpUrl, "about:blank");
|
|
810
913
|
if (!child.pid) throw new Error("Failed to launch Chrome process");
|
|
811
|
-
await initTarget(targetWsUrl, userAgent);
|
|
914
|
+
const appliedUa = await initTarget(targetWsUrl, userAgent);
|
|
915
|
+
if (appliedUa) this.userAgentOverrides.set(targetWsUrl, appliedUa);
|
|
812
916
|
return {
|
|
813
917
|
pid: child.pid,
|
|
814
918
|
cdpUrl,
|
|
@@ -823,12 +927,22 @@ var ChromeCdpBrowserController = class {
|
|
|
823
927
|
}
|
|
824
928
|
throw new Error(lastError?.message ?? "Unable to launch Chrome");
|
|
825
929
|
}
|
|
826
|
-
/**
|
|
827
|
-
|
|
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") {
|
|
828
941
|
let conn = await this.getConnection(targetWsUrl);
|
|
829
942
|
try {
|
|
830
943
|
return await fn(conn);
|
|
831
|
-
} catch {
|
|
944
|
+
} catch (error) {
|
|
945
|
+
if (retryOn === "connection" && !isConnectionError(error)) throw error;
|
|
832
946
|
this.dropConnection(targetWsUrl);
|
|
833
947
|
conn = await this.getConnection(targetWsUrl);
|
|
834
948
|
return await fn(conn);
|
|
@@ -837,11 +951,12 @@ var ChromeCdpBrowserController = class {
|
|
|
837
951
|
async navigate(targetWsUrl, url) {
|
|
838
952
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
839
953
|
await this.ensureEnabled(targetWsUrl);
|
|
840
|
-
const
|
|
841
|
-
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);
|
|
842
955
|
const navResult = await conn.send("Page.navigate", { url });
|
|
843
956
|
if (navResult.errorText) throw new Error(`Navigation failed: ${navResult.errorText}`);
|
|
844
|
-
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;
|
|
845
960
|
try {
|
|
846
961
|
await loadPromise;
|
|
847
962
|
} catch {}
|
|
@@ -849,12 +964,18 @@ var ChromeCdpBrowserController = class {
|
|
|
849
964
|
});
|
|
850
965
|
}
|
|
851
966
|
async handleDialogAction(targetWsUrl, payload) {
|
|
967
|
+
const DIALOG_WAIT_TIMEOUT_MS = 500;
|
|
968
|
+
const DIALOG_POLL_INTERVAL_MS = 10;
|
|
969
|
+
const MAX_POLL_ATTEMPTS = 10;
|
|
852
970
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
853
971
|
await this.ensureEnabled(targetWsUrl);
|
|
854
972
|
const cached = this.connections.get(targetWsUrl);
|
|
855
973
|
if (!cached?.pendingDialog) try {
|
|
856
|
-
await conn.waitForEvent("Page.javascriptDialogOpening",
|
|
857
|
-
|
|
974
|
+
await conn.waitForEvent("Page.javascriptDialogOpening", DIALOG_WAIT_TIMEOUT_MS);
|
|
975
|
+
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
|
|
976
|
+
if (cached?.pendingDialog) break;
|
|
977
|
+
await new Promise((r) => setTimeout(r, DIALOG_POLL_INTERVAL_MS));
|
|
978
|
+
}
|
|
858
979
|
} catch {
|
|
859
980
|
return "no dialog present";
|
|
860
981
|
}
|
|
@@ -871,7 +992,8 @@ var ChromeCdpBrowserController = class {
|
|
|
871
992
|
});
|
|
872
993
|
}
|
|
873
994
|
async preparePointerTarget(conn, payload) {
|
|
874
|
-
const
|
|
995
|
+
const point = (await evaluateOnPage(conn, {
|
|
996
|
+
expression: buildLocatorRuntimeExpression("payload", payload, `
|
|
875
997
|
const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
|
|
876
998
|
const el = resolved.element;
|
|
877
999
|
ensureInView(el);
|
|
@@ -888,9 +1010,7 @@ var ChromeCdpBrowserController = class {
|
|
|
888
1010
|
}
|
|
889
1011
|
|
|
890
1012
|
return { x: point.x, y: point.y };
|
|
891
|
-
`)
|
|
892
|
-
const point = (await conn.send("Runtime.evaluate", {
|
|
893
|
-
expression,
|
|
1013
|
+
`),
|
|
894
1014
|
returnByValue: true,
|
|
895
1015
|
awaitPromise: true
|
|
896
1016
|
})).result.value;
|
|
@@ -901,7 +1021,8 @@ var ChromeCdpBrowserController = class {
|
|
|
901
1021
|
};
|
|
902
1022
|
}
|
|
903
1023
|
async prepareTypeTarget(conn, payload) {
|
|
904
|
-
const
|
|
1024
|
+
const value = (await evaluateOnPage(conn, {
|
|
1025
|
+
expression: buildLocatorRuntimeExpression("payload", payload, `
|
|
905
1026
|
const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
|
|
906
1027
|
const el = resolved.element;
|
|
907
1028
|
ensureInView(el);
|
|
@@ -950,12 +1071,12 @@ var ChromeCdpBrowserController = class {
|
|
|
950
1071
|
}
|
|
951
1072
|
|
|
952
1073
|
return 'ready';
|
|
953
|
-
`)
|
|
954
|
-
return (await conn.send("Runtime.evaluate", {
|
|
955
|
-
expression,
|
|
1074
|
+
`),
|
|
956
1075
|
returnByValue: true,
|
|
957
1076
|
awaitPromise: true
|
|
958
|
-
})).result.value
|
|
1077
|
+
})).result.value;
|
|
1078
|
+
if (value !== "ready" && value !== "cleared") throw new Error("Unable to resolve type target");
|
|
1079
|
+
return value;
|
|
959
1080
|
}
|
|
960
1081
|
async dispatchMouseClick(conn, x, y) {
|
|
961
1082
|
await conn.send("Input.dispatchMouseEvent", {
|
|
@@ -981,7 +1102,8 @@ var ChromeCdpBrowserController = class {
|
|
|
981
1102
|
}
|
|
982
1103
|
async focusTarget(conn, payload) {
|
|
983
1104
|
if (!payload.selector) return;
|
|
984
|
-
|
|
1105
|
+
await evaluateOnPage(conn, {
|
|
1106
|
+
expression: buildLocatorRuntimeExpression("payload", payload, `
|
|
985
1107
|
const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
|
|
986
1108
|
const el = resolved.element;
|
|
987
1109
|
ensureInView(el);
|
|
@@ -989,9 +1111,7 @@ var ChromeCdpBrowserController = class {
|
|
|
989
1111
|
el.focus();
|
|
990
1112
|
}
|
|
991
1113
|
return true;
|
|
992
|
-
`)
|
|
993
|
-
await conn.send("Runtime.evaluate", {
|
|
994
|
-
expression,
|
|
1114
|
+
`),
|
|
995
1115
|
returnByValue: true,
|
|
996
1116
|
awaitPromise: true
|
|
997
1117
|
});
|
|
@@ -1018,8 +1138,8 @@ var ChromeCdpBrowserController = class {
|
|
|
1018
1138
|
async interact(targetWsUrl, payload) {
|
|
1019
1139
|
if (payload.action === "goBack") return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1020
1140
|
await this.ensureEnabled(targetWsUrl);
|
|
1021
|
-
const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 3e3).catch(() => void 0);
|
|
1022
|
-
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, {
|
|
1023
1143
|
expression: "history.back()",
|
|
1024
1144
|
returnByValue: true
|
|
1025
1145
|
});
|
|
@@ -1032,8 +1152,8 @@ var ChromeCdpBrowserController = class {
|
|
|
1032
1152
|
});
|
|
1033
1153
|
if (payload.action === "goForward") return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1034
1154
|
await this.ensureEnabled(targetWsUrl);
|
|
1035
|
-
const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 3e3).catch(() => void 0);
|
|
1036
|
-
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, {
|
|
1037
1157
|
expression: "history.forward()",
|
|
1038
1158
|
returnByValue: true
|
|
1039
1159
|
});
|
|
@@ -1083,7 +1203,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1083
1203
|
}
|
|
1084
1204
|
if (await this.prepareTypeTarget(conn, payload) === "ready") await conn.send("Input.insertText", { text: payload.text ?? "" });
|
|
1085
1205
|
return "typed";
|
|
1086
|
-
});
|
|
1206
|
+
}, "connection");
|
|
1087
1207
|
const expression = buildLocatorRuntimeExpression("payload", payload, `
|
|
1088
1208
|
if (payload.action === 'waitFor') {
|
|
1089
1209
|
const timeout = payload.timeoutMs ?? 2000;
|
|
@@ -1131,12 +1251,12 @@ var ChromeCdpBrowserController = class {
|
|
|
1131
1251
|
`);
|
|
1132
1252
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1133
1253
|
await this.ensureEnabled(targetWsUrl);
|
|
1134
|
-
return (await conn
|
|
1254
|
+
return (await evaluateOnPage(conn, {
|
|
1135
1255
|
expression,
|
|
1136
1256
|
returnByValue: true,
|
|
1137
1257
|
awaitPromise: true
|
|
1138
1258
|
})).result.value ?? "";
|
|
1139
|
-
});
|
|
1259
|
+
}, "connection");
|
|
1140
1260
|
}
|
|
1141
1261
|
async screenshot(targetWsUrl, options) {
|
|
1142
1262
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
@@ -1155,6 +1275,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1155
1275
|
height: metrics.contentSize.height,
|
|
1156
1276
|
scale: 1
|
|
1157
1277
|
};
|
|
1278
|
+
params.captureBeyondViewport = true;
|
|
1158
1279
|
}
|
|
1159
1280
|
return {
|
|
1160
1281
|
data: (await conn.send("Page.captureScreenshot", params)).data,
|
|
@@ -1164,7 +1285,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1164
1285
|
}
|
|
1165
1286
|
async getCurrentUrl(targetWsUrl) {
|
|
1166
1287
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1167
|
-
return (await conn
|
|
1288
|
+
return (await evaluateOnPage(conn, {
|
|
1168
1289
|
expression: "window.location.href",
|
|
1169
1290
|
returnByValue: true
|
|
1170
1291
|
})).result.value ?? "";
|
|
@@ -1212,7 +1333,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1212
1333
|
`);
|
|
1213
1334
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1214
1335
|
await this.ensureEnabled(targetWsUrl);
|
|
1215
|
-
const limited = normalizeRuntimeContentPayload((await conn
|
|
1336
|
+
const limited = normalizeRuntimeContentPayload((await evaluateOnPage(conn, {
|
|
1216
1337
|
expression,
|
|
1217
1338
|
returnByValue: true,
|
|
1218
1339
|
awaitPromise: true
|
|
@@ -1449,7 +1570,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1449
1570
|
`);
|
|
1450
1571
|
const pageSummary = await this.withRetry(targetWsUrl, async (conn) => {
|
|
1451
1572
|
await this.ensureEnabled(targetWsUrl);
|
|
1452
|
-
return (await conn
|
|
1573
|
+
return (await evaluateOnPage(conn, {
|
|
1453
1574
|
expression,
|
|
1454
1575
|
returnByValue: true,
|
|
1455
1576
|
awaitPromise: true
|
|
@@ -1521,10 +1642,11 @@ var ChromeCdpBrowserController = class {
|
|
|
1521
1642
|
let nodes;
|
|
1522
1643
|
if (selector) {
|
|
1523
1644
|
const objectGroup = "agentic-browser-a11y";
|
|
1524
|
-
const selected = await conn
|
|
1645
|
+
const selected = await evaluateOnPage(conn, {
|
|
1525
1646
|
expression: buildLocatorRuntimeExpression("selector", selector, `
|
|
1526
1647
|
return resolveLocator(selector);
|
|
1527
1648
|
`),
|
|
1649
|
+
awaitPromise: true,
|
|
1528
1650
|
objectGroup
|
|
1529
1651
|
});
|
|
1530
1652
|
const objectId = selected.result.objectId;
|
|
@@ -1915,10 +2037,12 @@ var ChromeCdpBrowserController = class {
|
|
|
1915
2037
|
};
|
|
1916
2038
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1917
2039
|
await this.ensureEnabled(targetWsUrl);
|
|
1918
|
-
|
|
2040
|
+
const result = await evaluateOnPage(conn, {
|
|
1919
2041
|
expression,
|
|
1920
|
-
returnByValue: true
|
|
1921
|
-
|
|
2042
|
+
returnByValue: true,
|
|
2043
|
+
awaitPromise: true
|
|
2044
|
+
});
|
|
2045
|
+
return extract(result);
|
|
1922
2046
|
});
|
|
1923
2047
|
}
|
|
1924
2048
|
/**
|
|
@@ -2143,6 +2267,28 @@ var ChromeCdpBrowserController = class {
|
|
|
2143
2267
|
}
|
|
2144
2268
|
return results;
|
|
2145
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
|
+
}
|
|
2146
2292
|
const candidates = collectButtons(document);
|
|
2147
2293
|
for (const el of candidates) {
|
|
2148
2294
|
if (!isVisible(el)) continue;
|
|
@@ -2150,6 +2296,10 @@ var ChromeCdpBrowserController = class {
|
|
|
2150
2296
|
if (text.length > 60) continue;
|
|
2151
2297
|
for (const pat of patterns) {
|
|
2152
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;
|
|
2153
2303
|
el.click();
|
|
2154
2304
|
return JSON.stringify({ dismissed: true, method: 'text', detail: text });
|
|
2155
2305
|
}
|
|
@@ -2160,7 +2310,7 @@ var ChromeCdpBrowserController = class {
|
|
|
2160
2310
|
})()`;
|
|
2161
2311
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
2162
2312
|
await this.ensureEnabled(targetWsUrl);
|
|
2163
|
-
const raw = (await conn
|
|
2313
|
+
const raw = (await evaluateOnPage(conn, {
|
|
2164
2314
|
expression,
|
|
2165
2315
|
returnByValue: true
|
|
2166
2316
|
})).result.value ?? "{\"dismissed\":false}";
|
|
@@ -2244,7 +2394,7 @@ var ChromeCdpBrowserController = class {
|
|
|
2244
2394
|
})()`;
|
|
2245
2395
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
2246
2396
|
await this.ensureEnabled(targetWsUrl);
|
|
2247
|
-
const raw = (await conn
|
|
2397
|
+
const raw = (await evaluateOnPage(conn, {
|
|
2248
2398
|
expression,
|
|
2249
2399
|
returnByValue: true
|
|
2250
2400
|
})).result.value ?? "{\"dismissed\":false}";
|
|
@@ -2265,14 +2415,21 @@ var ChromeCdpBrowserController = class {
|
|
|
2265
2415
|
await conn.send("Accessibility.enable");
|
|
2266
2416
|
await conn.send("Page.enable");
|
|
2267
2417
|
const { nodes } = await conn.send("Accessibility.getFullAXTree");
|
|
2268
|
-
const
|
|
2418
|
+
const nodeSets = [{
|
|
2419
|
+
nodes,
|
|
2420
|
+
trusted: false
|
|
2421
|
+
}];
|
|
2269
2422
|
try {
|
|
2270
2423
|
const { frameTree } = await conn.send("Page.getFrameTree");
|
|
2271
2424
|
for (const child of frameTree.childFrames ?? []) {
|
|
2272
2425
|
const url = child.frame.url ?? "";
|
|
2273
|
-
|
|
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 {
|
|
2274
2428
|
const frameResult = await conn.send("Accessibility.getFullAXTree", { frameId: child.frame.id });
|
|
2275
|
-
|
|
2429
|
+
nodeSets.push({
|
|
2430
|
+
nodes: frameResult.nodes,
|
|
2431
|
+
trusted: consentFrame
|
|
2432
|
+
});
|
|
2276
2433
|
} catch {}
|
|
2277
2434
|
}
|
|
2278
2435
|
} catch {}
|
|
@@ -2309,21 +2466,38 @@ var ChromeCdpBrowserController = class {
|
|
|
2309
2466
|
/accepter(\s+alle)?/i,
|
|
2310
2467
|
/aksepter(\s+alle)?/i
|
|
2311
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
|
+
};
|
|
2312
2483
|
const candidates = [];
|
|
2313
|
-
for (const
|
|
2314
|
-
|
|
2315
|
-
const
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
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
|
+
}
|
|
2327
2501
|
}
|
|
2328
2502
|
}
|
|
2329
2503
|
if (candidates.length === 0) return { dismissed: false };
|
|
@@ -2467,10 +2641,116 @@ var MockBrowserController = class {
|
|
|
2467
2641
|
closeConnection(_targetWsUrl) {}
|
|
2468
2642
|
};
|
|
2469
2643
|
//#endregion
|
|
2644
|
+
//#region src/lib/domain-schemas.ts
|
|
2645
|
+
const InteractActionSchema = z.enum([
|
|
2646
|
+
"click",
|
|
2647
|
+
"type",
|
|
2648
|
+
"press",
|
|
2649
|
+
"waitFor",
|
|
2650
|
+
"evaluate",
|
|
2651
|
+
"scroll",
|
|
2652
|
+
"hover",
|
|
2653
|
+
"select",
|
|
2654
|
+
"toggle",
|
|
2655
|
+
"goBack",
|
|
2656
|
+
"goForward",
|
|
2657
|
+
"refresh",
|
|
2658
|
+
"dialog"
|
|
2659
|
+
]);
|
|
2660
|
+
const InteractPayloadSchema = z.object({
|
|
2661
|
+
action: InteractActionSchema,
|
|
2662
|
+
selector: z.string().optional(),
|
|
2663
|
+
fallbackSelectors: z.array(z.string()).optional(),
|
|
2664
|
+
text: z.string().optional(),
|
|
2665
|
+
key: z.string().optional(),
|
|
2666
|
+
value: z.string().optional(),
|
|
2667
|
+
scrollX: z.number().optional(),
|
|
2668
|
+
scrollY: z.number().optional(),
|
|
2669
|
+
timeoutMs: z.number().optional()
|
|
2670
|
+
});
|
|
2671
|
+
const SessionStatusSchema = z.enum([
|
|
2672
|
+
"starting",
|
|
2673
|
+
"ready",
|
|
2674
|
+
"disconnected",
|
|
2675
|
+
"failed",
|
|
2676
|
+
"terminated"
|
|
2677
|
+
]);
|
|
2678
|
+
const CommandTypeSchema = z.enum([
|
|
2679
|
+
"navigate",
|
|
2680
|
+
"interact",
|
|
2681
|
+
"restart",
|
|
2682
|
+
"terminate"
|
|
2683
|
+
]);
|
|
2684
|
+
const ResultStatusSchema = z.enum([
|
|
2685
|
+
"success",
|
|
2686
|
+
"failed",
|
|
2687
|
+
"timed_out"
|
|
2688
|
+
]);
|
|
2689
|
+
const ConnectionStateSchema = z.enum([
|
|
2690
|
+
"connecting",
|
|
2691
|
+
"ready",
|
|
2692
|
+
"disconnected",
|
|
2693
|
+
"failed"
|
|
2694
|
+
]);
|
|
2695
|
+
const EventCategorySchema = z.enum([
|
|
2696
|
+
"lifecycle",
|
|
2697
|
+
"security",
|
|
2698
|
+
"command"
|
|
2699
|
+
]);
|
|
2700
|
+
const EventSeveritySchema = z.enum([
|
|
2701
|
+
"info",
|
|
2702
|
+
"warning",
|
|
2703
|
+
"error"
|
|
2704
|
+
]);
|
|
2705
|
+
z.object({
|
|
2706
|
+
sessionId: z.string().min(1),
|
|
2707
|
+
status: SessionStatusSchema,
|
|
2708
|
+
browserType: z.literal("chrome"),
|
|
2709
|
+
startedAt: z.string().datetime(),
|
|
2710
|
+
endedAt: z.string().datetime().optional(),
|
|
2711
|
+
authTokenRef: z.string().min(1)
|
|
2712
|
+
}).refine((session) => session.status === "terminated" ? Boolean(session.endedAt) : true, {
|
|
2713
|
+
message: "endedAt is required for terminated sessions",
|
|
2714
|
+
path: ["endedAt"]
|
|
2715
|
+
});
|
|
2716
|
+
z.object({
|
|
2717
|
+
commandId: z.string().min(1),
|
|
2718
|
+
sessionId: z.string().min(1),
|
|
2719
|
+
type: CommandTypeSchema,
|
|
2720
|
+
payload: z.record(z.string(), z.unknown()),
|
|
2721
|
+
submittedAt: z.string().datetime(),
|
|
2722
|
+
completedAt: z.string().datetime().optional(),
|
|
2723
|
+
resultStatus: ResultStatusSchema.optional(),
|
|
2724
|
+
resultMessage: z.string().optional()
|
|
2725
|
+
}).refine((command) => command.completedAt ? Boolean(command.resultStatus) : true, {
|
|
2726
|
+
message: "resultStatus is required when completedAt is set",
|
|
2727
|
+
path: ["resultStatus"]
|
|
2728
|
+
});
|
|
2729
|
+
z.object({
|
|
2730
|
+
sessionId: z.string().min(1),
|
|
2731
|
+
state: ConnectionStateSchema,
|
|
2732
|
+
lastHeartbeatAt: z.string().datetime().optional(),
|
|
2733
|
+
disconnectReason: z.string().optional()
|
|
2734
|
+
});
|
|
2735
|
+
z.object({
|
|
2736
|
+
sessionId: z.string().min(1),
|
|
2737
|
+
tokenId: z.string().min(1),
|
|
2738
|
+
issuedAt: z.string().datetime(),
|
|
2739
|
+
expiresAt: z.string().datetime().optional(),
|
|
2740
|
+
revokedAt: z.string().datetime().optional()
|
|
2741
|
+
});
|
|
2742
|
+
z.object({
|
|
2743
|
+
eventId: z.string().min(1),
|
|
2744
|
+
sessionId: z.string().min(1),
|
|
2745
|
+
category: EventCategorySchema,
|
|
2746
|
+
message: z.string().min(1),
|
|
2747
|
+
createdAt: z.string().datetime(),
|
|
2748
|
+
severity: EventSeveritySchema
|
|
2749
|
+
});
|
|
2750
|
+
//#endregion
|
|
2470
2751
|
//#region src/session/session-store.ts
|
|
2471
2752
|
var SessionStore = class {
|
|
2472
2753
|
filePath;
|
|
2473
|
-
cache = null;
|
|
2474
2754
|
constructor(baseDir) {
|
|
2475
2755
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
2476
2756
|
this.filePath = path.join(baseDir, "sessions.json");
|
|
@@ -2538,25 +2818,35 @@ var SessionStore = class {
|
|
|
2538
2818
|
this.write(state);
|
|
2539
2819
|
}
|
|
2540
2820
|
read() {
|
|
2541
|
-
|
|
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
|
+
}
|
|
2542
2828
|
try {
|
|
2543
|
-
|
|
2544
|
-
this.cache = parsed;
|
|
2545
|
-
return parsed;
|
|
2829
|
+
return JSON.parse(raw);
|
|
2546
2830
|
} catch {
|
|
2831
|
+
try {
|
|
2832
|
+
fs.copyFileSync(this.filePath, `${this.filePath}.corrupt`);
|
|
2833
|
+
} catch {}
|
|
2547
2834
|
const empty = { sessions: {} };
|
|
2548
2835
|
this.write(empty);
|
|
2549
2836
|
return empty;
|
|
2550
2837
|
}
|
|
2551
2838
|
}
|
|
2552
2839
|
write(state) {
|
|
2553
|
-
|
|
2554
|
-
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);
|
|
2555
2843
|
}
|
|
2556
2844
|
};
|
|
2557
2845
|
//#endregion
|
|
2558
2846
|
//#region src/session/session-manager.ts
|
|
2559
2847
|
var SessionManager = class {
|
|
2848
|
+
ctx;
|
|
2849
|
+
browser;
|
|
2560
2850
|
store;
|
|
2561
2851
|
constructor(ctx, browser) {
|
|
2562
2852
|
this.ctx = ctx;
|
|
@@ -2610,7 +2900,7 @@ var SessionManager = class {
|
|
|
2610
2900
|
return this.mustGetRecord(recovered.sessionId);
|
|
2611
2901
|
} catch (restartError) {
|
|
2612
2902
|
this.recordEvent(sessionId, "lifecycle", "error", `Recovery failed: ${restartError.message}`);
|
|
2613
|
-
throw new Error(`Session is not ready and recovery failed: ${restartError.message}
|
|
2903
|
+
throw new Error(`Session is not ready and recovery failed: ${restartError.message}`, { cause: restartError });
|
|
2614
2904
|
}
|
|
2615
2905
|
throw new Error("Session is terminated. Start a new session.");
|
|
2616
2906
|
}
|
|
@@ -2639,14 +2929,17 @@ var SessionManager = class {
|
|
|
2639
2929
|
const finalUrl = await this.browser.navigate(record.targetWsUrl, url);
|
|
2640
2930
|
this.store.setLastUrl(sessionId, finalUrl);
|
|
2641
2931
|
resultMessage = `Navigated to ${finalUrl}`;
|
|
2642
|
-
} else if (input.type === "interact")
|
|
2643
|
-
|
|
2932
|
+
} else if (input.type === "interact") {
|
|
2933
|
+
const parsedPayload = InteractPayloadSchema.safeParse(input.payload);
|
|
2934
|
+
if (!parsedPayload.success) throw new Error(`Invalid interact payload: ${parsedPayload.error.message}`);
|
|
2935
|
+
resultMessage = `Interaction result: ${await this.browser.interact(record.targetWsUrl, parsedPayload.data)}`;
|
|
2936
|
+
} else if (input.type === "restart") {
|
|
2644
2937
|
await this.restartSession(sessionId);
|
|
2645
2938
|
resultMessage = "Session restarted";
|
|
2646
2939
|
} else if (input.type === "terminate") {
|
|
2647
2940
|
await this.terminateSession(sessionId);
|
|
2648
2941
|
resultMessage = "Session terminated";
|
|
2649
|
-
}
|
|
2942
|
+
} else throw new Error(`Unsupported command type: ${String(input.type)}`);
|
|
2650
2943
|
} catch (error) {
|
|
2651
2944
|
resultStatus = "failed";
|
|
2652
2945
|
resultMessage = error.message;
|
|
@@ -2749,6 +3042,7 @@ var SessionManager = class {
|
|
|
2749
3042
|
}
|
|
2750
3043
|
async restartSession(sessionId) {
|
|
2751
3044
|
const record = this.mustGetRecord(sessionId);
|
|
3045
|
+
if (record.session.status === "terminated") throw new Error("Session is terminated. Start a new session.");
|
|
2752
3046
|
if (this.browser.closeConnection) try {
|
|
2753
3047
|
this.browser.closeConnection(record.targetWsUrl);
|
|
2754
3048
|
} catch {}
|
|
@@ -2780,6 +3074,7 @@ var SessionManager = class {
|
|
|
2780
3074
|
}
|
|
2781
3075
|
rotateAuthToken(sessionId) {
|
|
2782
3076
|
const record = this.mustGetRecord(sessionId);
|
|
3077
|
+
if (record.session.status === "terminated") throw new Error("Session is terminated. Start a new session.");
|
|
2783
3078
|
this.ctx.tokenService.revoke(sessionId);
|
|
2784
3079
|
const nextToken = this.ctx.tokenService.issue(sessionId);
|
|
2785
3080
|
const updated = {
|
|
@@ -2891,8 +3186,10 @@ var SessionManager = class {
|
|
|
2891
3186
|
const record = this.store.get(sessionId);
|
|
2892
3187
|
if (!record) throw new Error("Session not found");
|
|
2893
3188
|
if (!record.targetWsUrl) throw new Error("Session target is missing. Restart the session.");
|
|
2894
|
-
|
|
2895
|
-
|
|
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
|
+
}
|
|
2896
3193
|
return record;
|
|
2897
3194
|
}
|
|
2898
3195
|
recordEvent(sessionId, category, severity, message) {
|
|
@@ -2952,9 +3249,9 @@ var SessionManager = class {
|
|
|
2952
3249
|
//#endregion
|
|
2953
3250
|
//#region src/transport/control-api.ts
|
|
2954
3251
|
var ControlApi = class {
|
|
2955
|
-
|
|
3252
|
+
sessions;
|
|
3253
|
+
constructor(sessions) {
|
|
2956
3254
|
this.sessions = sessions;
|
|
2957
|
-
this.eventStore = eventStore;
|
|
2958
3255
|
}
|
|
2959
3256
|
async createSession(input) {
|
|
2960
3257
|
return await this.sessions.createSession(input);
|
|
@@ -2990,7 +3287,7 @@ var ControlApi = class {
|
|
|
2990
3287
|
return await this.sessions.getCurrentUrl(sessionId);
|
|
2991
3288
|
}
|
|
2992
3289
|
listEvents(sessionId, limit = 100) {
|
|
2993
|
-
return { events: this.
|
|
3290
|
+
return { events: this.sessions.listEvents(sessionId, limit) };
|
|
2994
3291
|
}
|
|
2995
3292
|
searchMemory(input) {
|
|
2996
3293
|
return { results: this.sessions.searchMemory(input) };
|
|
@@ -3010,21 +3307,12 @@ var ControlApi = class {
|
|
|
3010
3307
|
};
|
|
3011
3308
|
//#endregion
|
|
3012
3309
|
//#region src/lib/config.ts
|
|
3013
|
-
const DEFAULT_PORT = 43111;
|
|
3014
|
-
const DEFAULT_TIMEOUT_MS = 2e3;
|
|
3015
3310
|
function loadConfig(env = process.env) {
|
|
3016
|
-
const wsPort = Number.parseInt(env.AGENTIC_BROWSER_WS_PORT ?? `${DEFAULT_PORT}`, 10);
|
|
3017
|
-
const commandTimeoutMs = Number.parseInt(env.AGENTIC_BROWSER_COMMAND_TIMEOUT_MS ?? `${DEFAULT_TIMEOUT_MS}`, 10);
|
|
3018
|
-
if (Number.isNaN(wsPort) || wsPort <= 0) throw new Error("AGENTIC_BROWSER_WS_PORT must be a positive integer");
|
|
3019
|
-
if (Number.isNaN(commandTimeoutMs) || commandTimeoutMs <= 0) throw new Error("AGENTIC_BROWSER_COMMAND_TIMEOUT_MS must be a positive integer");
|
|
3020
3311
|
const userProfile = env.AGENTIC_BROWSER_USER_PROFILE;
|
|
3021
3312
|
let userProfileDir;
|
|
3022
3313
|
if (userProfile === "true" || userProfile === "default") userProfileDir = "default";
|
|
3023
3314
|
else if (userProfile && path.isAbsolute(userProfile)) userProfileDir = userProfile;
|
|
3024
3315
|
return {
|
|
3025
|
-
host: env.AGENTIC_BROWSER_HOST ?? "127.0.0.1",
|
|
3026
|
-
wsPort,
|
|
3027
|
-
commandTimeoutMs,
|
|
3028
3316
|
dataDir: env.AGENTIC_BROWSER_DIR ?? path.join(os.homedir(), ".agentic-browser"),
|
|
3029
3317
|
browserExecutablePath: env.AGENTIC_BROWSER_CHROME_EXECUTABLE_PATH ?? env.AGENTIC_BROWSER_CHROME_PATH,
|
|
3030
3318
|
cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
|
|
@@ -3036,6 +3324,7 @@ function loadConfig(env = process.env) {
|
|
|
3036
3324
|
//#endregion
|
|
3037
3325
|
//#region src/observability/logger.ts
|
|
3038
3326
|
var Logger = class {
|
|
3327
|
+
namespace;
|
|
3039
3328
|
constructor(namespace) {
|
|
3040
3329
|
this.namespace = namespace;
|
|
3041
3330
|
}
|
|
@@ -3072,17 +3361,28 @@ var EventStore = class {
|
|
|
3072
3361
|
filePath;
|
|
3073
3362
|
bufferedLines = [];
|
|
3074
3363
|
flushTimer;
|
|
3364
|
+
pendingFlushDelayMs = Number.POSITIVE_INFINITY;
|
|
3075
3365
|
flushInFlight;
|
|
3076
3366
|
constructor(baseDir) {
|
|
3077
3367
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
3078
3368
|
this.filePath = path.join(baseDir, "session-events.log");
|
|
3079
|
-
process.
|
|
3080
|
-
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();
|
|
3081
3381
|
}
|
|
3082
3382
|
append(event) {
|
|
3083
3383
|
let existing = this.events.get(event.sessionId) ?? [];
|
|
3084
3384
|
existing.push(event);
|
|
3085
|
-
if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-
|
|
3385
|
+
if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-500);
|
|
3086
3386
|
this.events.set(event.sessionId, existing);
|
|
3087
3387
|
this.bufferedLines.push(`${JSON.stringify(event)}\n`);
|
|
3088
3388
|
if (this.bufferedLines.length >= 20) {
|
|
@@ -3100,7 +3400,12 @@ var EventStore = class {
|
|
|
3100
3400
|
}
|
|
3101
3401
|
flushSoon(delayMs) {
|
|
3102
3402
|
if (this.flushInFlight) return this.flushInFlight;
|
|
3103
|
-
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;
|
|
3104
3409
|
this.flushTimer = setTimeout(() => {
|
|
3105
3410
|
this.flushTimer = void 0;
|
|
3106
3411
|
this.flushAsync();
|
|
@@ -3157,40 +3462,6 @@ var SessionTokenService = class {
|
|
|
3157
3462
|
}
|
|
3158
3463
|
};
|
|
3159
3464
|
//#endregion
|
|
3160
|
-
//#region src/transport/ws-server.ts
|
|
3161
|
-
var AuthenticatedWsServer = class {
|
|
3162
|
-
logger = new Logger("ws-server");
|
|
3163
|
-
server;
|
|
3164
|
-
constructor(options) {
|
|
3165
|
-
this.options = options;
|
|
3166
|
-
}
|
|
3167
|
-
start(onMessage) {
|
|
3168
|
-
this.server = new WebSocketServer({
|
|
3169
|
-
host: this.options.host,
|
|
3170
|
-
port: this.options.port
|
|
3171
|
-
});
|
|
3172
|
-
this.server.on("connection", (socket, request) => {
|
|
3173
|
-
const url = new URL(request.url ?? "/", `http://${this.options.host}:${this.options.port}`);
|
|
3174
|
-
const sessionId = url.searchParams.get("sessionId");
|
|
3175
|
-
const token = url.searchParams.get("token");
|
|
3176
|
-
if (!sessionId || !token || !this.options.tokenService.validate(sessionId, token)) {
|
|
3177
|
-
this.logger.warning("Rejected websocket client", { sessionId });
|
|
3178
|
-
socket.close(4401, "Unauthorized");
|
|
3179
|
-
return;
|
|
3180
|
-
}
|
|
3181
|
-
socket.on("message", (payload) => onMessage(socket, payload.toString("utf8")));
|
|
3182
|
-
});
|
|
3183
|
-
this.logger.info("WebSocket server started", {
|
|
3184
|
-
host: this.options.host,
|
|
3185
|
-
port: this.options.port
|
|
3186
|
-
});
|
|
3187
|
-
}
|
|
3188
|
-
stop() {
|
|
3189
|
-
this.server?.close();
|
|
3190
|
-
this.server = void 0;
|
|
3191
|
-
}
|
|
3192
|
-
};
|
|
3193
|
-
//#endregion
|
|
3194
3465
|
//#region src/memory/memory-index.ts
|
|
3195
3466
|
function normalize(value) {
|
|
3196
3467
|
return value.trim().toLowerCase();
|
|
@@ -3215,7 +3486,7 @@ function buildSelectorHints(insight) {
|
|
|
3215
3486
|
if (!evidence.selector || evidence.result !== "success") continue;
|
|
3216
3487
|
weightedSelectors.set(evidence.selector, (weightedSelectors.get(evidence.selector) ?? 0) + 3);
|
|
3217
3488
|
}
|
|
3218
|
-
return [...weightedSelectors.entries()].
|
|
3489
|
+
return [...weightedSelectors.entries()].toSorted((a, b) => b[1] - a[1]).slice(0, 5).map(([selector]) => selector);
|
|
3219
3490
|
}
|
|
3220
3491
|
function buildSelectorAliases(insight) {
|
|
3221
3492
|
return insight.selectorAliases ?? [];
|
|
@@ -3231,10 +3502,11 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup
|
|
|
3231
3502
|
const insightIntent = normalize(insight.taskIntent);
|
|
3232
3503
|
const intentMatch = insightIntent === normalizedIntent ? 1 : 0;
|
|
3233
3504
|
const intentPartial = intentMatch === 1 || insightIntent.includes(normalizedIntent) || normalizedIntent.includes(insightIntent) ? .65 : 0;
|
|
3505
|
+
const intentSignal = Math.max(intentMatch, intentPartial);
|
|
3234
3506
|
const domainMatch = normalizedDomain && normalize(insight.siteDomain) === normalizedDomain ? 1 : normalizedDomain ? 0 : .6;
|
|
3235
3507
|
const reliability = .6 * confidenceFromCounts(insight.successCount, insight.failureCount) + .4 * freshnessWeight(insight.freshness);
|
|
3236
3508
|
const selectorQuality = selectorSignal(insight);
|
|
3237
|
-
const score = .5 *
|
|
3509
|
+
const score = .5 * intentSignal + .2 * domainMatch + .15 * reliability + .15 * selectorQuality;
|
|
3238
3510
|
const result = {
|
|
3239
3511
|
insightId: insight.insightId,
|
|
3240
3512
|
taskIntent: insight.taskIntent,
|
|
@@ -3253,7 +3525,10 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup
|
|
|
3253
3525
|
cookieBanner: profile.cookieBanner,
|
|
3254
3526
|
visitCount: profile.visitCount
|
|
3255
3527
|
};
|
|
3256
|
-
return
|
|
3528
|
+
return {
|
|
3529
|
+
result,
|
|
3530
|
+
intentSignal
|
|
3531
|
+
};
|
|
3257
3532
|
}
|
|
3258
3533
|
var MemoryIndex = class {
|
|
3259
3534
|
/** Domain → insights index, rebuilt lazily when insight list changes. */
|
|
@@ -3280,18 +3555,20 @@ var MemoryIndex = class {
|
|
|
3280
3555
|
const limit = input.limit ?? 10;
|
|
3281
3556
|
this.ensureIndex(insights);
|
|
3282
3557
|
const candidates = normalizedDomain ? this.domainIndex.get(normalizedDomain) ?? [] : insights;
|
|
3558
|
+
const relevant = (scored) => scored.intentSignal > 0;
|
|
3283
3559
|
if (limit === 1 && normalizedDomain) {
|
|
3284
3560
|
let best;
|
|
3285
3561
|
for (const insight of candidates) {
|
|
3286
|
-
const
|
|
3287
|
-
if (
|
|
3288
|
-
|
|
3289
|
-
|
|
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;
|
|
3290
3567
|
}
|
|
3291
3568
|
}
|
|
3292
|
-
return best ? [best] : [];
|
|
3569
|
+
return best ? [best.result] : [];
|
|
3293
3570
|
}
|
|
3294
|
-
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);
|
|
3295
3572
|
}
|
|
3296
3573
|
};
|
|
3297
3574
|
//#endregion
|
|
@@ -3458,11 +3735,15 @@ var SiteProfileStore = class {
|
|
|
3458
3735
|
return this.getCache().find((p) => p.domain === normalized);
|
|
3459
3736
|
}
|
|
3460
3737
|
upsert(profile) {
|
|
3461
|
-
|
|
3738
|
+
const normalized = {
|
|
3739
|
+
...profile,
|
|
3740
|
+
domain: profile.domain.toLowerCase()
|
|
3741
|
+
};
|
|
3742
|
+
SiteProfileSchema.parse(normalized);
|
|
3462
3743
|
const profiles = this.getCache();
|
|
3463
|
-
const index = profiles.findIndex((p) => p.domain ===
|
|
3464
|
-
if (index >= 0) profiles[index] =
|
|
3465
|
-
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);
|
|
3466
3747
|
this.markDirty();
|
|
3467
3748
|
}
|
|
3468
3749
|
flushSync() {
|
|
@@ -3477,7 +3758,8 @@ var SiteProfileStore = class {
|
|
|
3477
3758
|
}
|
|
3478
3759
|
getCache() {
|
|
3479
3760
|
if (this.cached) return this.cached;
|
|
3480
|
-
|
|
3761
|
+
const state = this.readDisk();
|
|
3762
|
+
this.cached = state.profiles;
|
|
3481
3763
|
return this.cached;
|
|
3482
3764
|
}
|
|
3483
3765
|
markDirty() {
|
|
@@ -3500,7 +3782,7 @@ var SiteProfileStore = class {
|
|
|
3500
3782
|
} catch {
|
|
3501
3783
|
const obj = raw;
|
|
3502
3784
|
if (Array.isArray(obj?.profiles)) {
|
|
3503
|
-
const salvaged = obj.profiles.
|
|
3785
|
+
const salvaged = obj.profiles.map((item) => SiteProfileSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
|
|
3504
3786
|
if (salvaged.length > 0) {
|
|
3505
3787
|
const state = { profiles: salvaged };
|
|
3506
3788
|
this.backupAndReset();
|
|
@@ -3580,6 +3862,15 @@ var TaskInsightStore = class {
|
|
|
3580
3862
|
else insights.push(insight);
|
|
3581
3863
|
this.markDirty();
|
|
3582
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
|
+
}
|
|
3583
3874
|
replaceMany(insights) {
|
|
3584
3875
|
for (const insight of insights) TaskInsightSchema.parse(insight);
|
|
3585
3876
|
this.cached = insights;
|
|
@@ -3599,7 +3890,8 @@ var TaskInsightStore = class {
|
|
|
3599
3890
|
/** Return the in-memory cache, loading from disk on first access. */
|
|
3600
3891
|
getCache() {
|
|
3601
3892
|
if (this.cached) return this.cached;
|
|
3602
|
-
|
|
3893
|
+
const state = this.readDisk();
|
|
3894
|
+
this.cached = state.insights;
|
|
3603
3895
|
return this.cached;
|
|
3604
3896
|
}
|
|
3605
3897
|
markDirty() {
|
|
@@ -3623,7 +3915,7 @@ var TaskInsightStore = class {
|
|
|
3623
3915
|
} catch {
|
|
3624
3916
|
const obj = raw;
|
|
3625
3917
|
if (Array.isArray(obj?.insights)) {
|
|
3626
|
-
const salvaged = obj.insights.
|
|
3918
|
+
const salvaged = obj.insights.map((item) => TaskInsightSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
|
|
3627
3919
|
if (salvaged.length > 0) {
|
|
3628
3920
|
const state = { insights: salvaged };
|
|
3629
3921
|
this.backupAndReset();
|
|
@@ -3668,10 +3960,17 @@ var TaskInsightStore = class {
|
|
|
3668
3960
|
};
|
|
3669
3961
|
//#endregion
|
|
3670
3962
|
//#region src/memory/memory-service.ts
|
|
3963
|
+
/** Memory service configuration */
|
|
3671
3964
|
const SEARCH_CACHE_TTL_MS = 2e3;
|
|
3965
|
+
const MAX_SELECTOR_ALIASES = 10;
|
|
3966
|
+
/** Site profile limits */
|
|
3672
3967
|
const MAX_NAVIGATION_PATHS = 20;
|
|
3673
3968
|
const MAX_LAYOUT_FINGERPRINTS = 10;
|
|
3674
3969
|
const MAX_SELECTOR_PATTERNS = 15;
|
|
3970
|
+
function normalizePathSegment(value) {
|
|
3971
|
+
const [firstSegment = ""] = value.replace(/\/\*$/, "").split("/").filter(Boolean);
|
|
3972
|
+
return firstSegment.toLowerCase();
|
|
3973
|
+
}
|
|
3675
3974
|
var MemoryService = class {
|
|
3676
3975
|
store;
|
|
3677
3976
|
profileStore;
|
|
@@ -3729,7 +4028,7 @@ var MemoryService = class {
|
|
|
3729
4028
|
fresh: insights.filter((x) => x.freshness === "fresh").length,
|
|
3730
4029
|
suspect: insights.filter((x) => x.freshness === "suspect").length,
|
|
3731
4030
|
stale: insights.filter((x) => x.freshness === "stale").length,
|
|
3732
|
-
topDomains: [...byDomain.entries()].
|
|
4031
|
+
topDomains: [...byDomain.entries()].toSorted((a, b) => b[1] - a[1]).slice(0, 10).map(([domain, count]) => ({
|
|
3733
4032
|
domain,
|
|
3734
4033
|
count
|
|
3735
4034
|
}))
|
|
@@ -3740,6 +4039,8 @@ var MemoryService = class {
|
|
|
3740
4039
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3741
4040
|
const verified = {
|
|
3742
4041
|
...insight,
|
|
4042
|
+
freshness: "fresh",
|
|
4043
|
+
staleStrikeCount: 0,
|
|
3743
4044
|
lastVerifiedAt: now,
|
|
3744
4045
|
updatedAt: now
|
|
3745
4046
|
};
|
|
@@ -3797,6 +4098,7 @@ var MemoryService = class {
|
|
|
3797
4098
|
};
|
|
3798
4099
|
versioned = this.maybeGenerateAliases(versioned);
|
|
3799
4100
|
this.store.upsert(versioned);
|
|
4101
|
+
this.store.remove(matched.insightId);
|
|
3800
4102
|
this.invalidateSearchCache();
|
|
3801
4103
|
return versioned;
|
|
3802
4104
|
}
|
|
@@ -3837,7 +4139,7 @@ var MemoryService = class {
|
|
|
3837
4139
|
if (fpIndex >= 0) profile.layoutFingerprints[fpIndex] = fingerprint;
|
|
3838
4140
|
else {
|
|
3839
4141
|
profile.layoutFingerprints.push(fingerprint);
|
|
3840
|
-
if (profile.layoutFingerprints.length > MAX_LAYOUT_FINGERPRINTS) profile.layoutFingerprints = profile.layoutFingerprints.
|
|
4142
|
+
if (profile.layoutFingerprints.length > MAX_LAYOUT_FINGERPRINTS) profile.layoutFingerprints = profile.layoutFingerprints.toSorted((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt)).slice(0, MAX_LAYOUT_FINGERPRINTS);
|
|
3841
4143
|
}
|
|
3842
4144
|
profile.lastVisitAt = now;
|
|
3843
4145
|
this.profileStore.upsert(profile);
|
|
@@ -3929,12 +4231,8 @@ var MemoryService = class {
|
|
|
3929
4231
|
return score;
|
|
3930
4232
|
}
|
|
3931
4233
|
sameFirstPathSegment(left, right) {
|
|
3932
|
-
const
|
|
3933
|
-
|
|
3934
|
-
return firstSegment.toLowerCase();
|
|
3935
|
-
};
|
|
3936
|
-
const leftSegment = normalize(left);
|
|
3937
|
-
const rightSegment = normalize(right);
|
|
4234
|
+
const leftSegment = normalizePathSegment(left);
|
|
4235
|
+
const rightSegment = normalizePathSegment(right);
|
|
3938
4236
|
return Boolean(leftSegment) && leftSegment === rightSegment;
|
|
3939
4237
|
}
|
|
3940
4238
|
createEvidence(input, result, reason) {
|
|
@@ -3963,11 +4261,11 @@ var MemoryService = class {
|
|
|
3963
4261
|
selector,
|
|
3964
4262
|
fallbackSelectors: []
|
|
3965
4263
|
});
|
|
3966
|
-
if (aliasMap.size >=
|
|
4264
|
+
if (aliasMap.size >= MAX_SELECTOR_ALIASES) break;
|
|
3967
4265
|
}
|
|
3968
4266
|
return {
|
|
3969
4267
|
...insight,
|
|
3970
|
-
selectorAliases: [...aliasMap.values()].slice(0,
|
|
4268
|
+
selectorAliases: [...aliasMap.values()].slice(0, MAX_SELECTOR_ALIASES)
|
|
3971
4269
|
};
|
|
3972
4270
|
}
|
|
3973
4271
|
deriveAliasName(selector) {
|
|
@@ -3993,19 +4291,11 @@ var MemoryService = class {
|
|
|
3993
4291
|
//#region src/cli/app.ts
|
|
3994
4292
|
function createAppContext(env = process.env) {
|
|
3995
4293
|
const config = loadConfig(env);
|
|
3996
|
-
const logger = new Logger("app");
|
|
3997
|
-
const eventStore = new EventStore(config.dataDir);
|
|
3998
|
-
const tokenService = new SessionTokenService();
|
|
3999
4294
|
return {
|
|
4000
4295
|
config,
|
|
4001
|
-
logger,
|
|
4002
|
-
eventStore,
|
|
4003
|
-
tokenService,
|
|
4004
|
-
wsServer: new AuthenticatedWsServer({
|
|
4005
|
-
host: config.host,
|
|
4006
|
-
port: config.wsPort,
|
|
4007
|
-
tokenService
|
|
4008
|
-
}),
|
|
4296
|
+
logger: new Logger("app"),
|
|
4297
|
+
eventStore: new EventStore(config.dataDir),
|
|
4298
|
+
tokenService: new SessionTokenService(),
|
|
4009
4299
|
memoryService: new MemoryService(config.dataDir)
|
|
4010
4300
|
};
|
|
4011
4301
|
}
|
|
@@ -4018,7 +4308,7 @@ var AgenticBrowserCore = class {
|
|
|
4018
4308
|
constructor(context, browserController) {
|
|
4019
4309
|
this.context = context;
|
|
4020
4310
|
this.sessions = new SessionManager(context, browserController);
|
|
4021
|
-
this.api = new ControlApi(this.sessions
|
|
4311
|
+
this.api = new ControlApi(this.sessions);
|
|
4022
4312
|
}
|
|
4023
4313
|
async startSession(input = { browser: "chrome" }) {
|
|
4024
4314
|
return await this.api.createSession(input);
|