agentic-browser 2.6.0 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +25 -19
- package/README.md +1 -1
- package/dist/cli/index.mjs +38 -39
- package/dist/index.d.mts +20 -33
- package/dist/index.mjs +1 -2
- package/dist/mcp/index.d.mts +1 -4
- package/dist/mcp/index.mjs +47 -18
- package/dist/{runtime-BzdbtFF4.mjs → runtime-Bo7FmRYk.mjs} +359 -116
- package/dist/{setup-DUAjdbQ3.mjs → setup-DwFaZOPf.mjs} +13 -6
- package/package.json +20 -18
|
@@ -8,7 +8,33 @@ import WebSocket from "ws";
|
|
|
8
8
|
import crypto from "node:crypto";
|
|
9
9
|
import { URL as URL$1 } from "node:url";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Create a directory (recursively) and tighten it to owner-only if it already existed
|
|
13
|
+
* with wider permissions (skips home, filesystem root, and dirs owned by another user).
|
|
14
|
+
*/
|
|
15
|
+
function ensurePrivateDir(dir) {
|
|
16
|
+
fs.mkdirSync(dir, {
|
|
17
|
+
recursive: true,
|
|
18
|
+
mode: 448
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
if (process.platform === "win32" || typeof process.getuid !== "function") return;
|
|
22
|
+
const resolved = path.resolve(dir);
|
|
23
|
+
if (resolved === path.parse(resolved).root || resolved === path.resolve(os.homedir())) return;
|
|
24
|
+
const st = fs.statSync(resolved);
|
|
25
|
+
if (!st.isDirectory() || st.uid !== process.getuid()) return;
|
|
26
|
+
if ((st.mode & 63) !== 0) fs.chmodSync(resolved, 448);
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
/** Write a file with owner-only permissions, tightening any pre-existing file. */
|
|
30
|
+
function writePrivateFileSync(file, data) {
|
|
31
|
+
fs.writeFileSync(file, data, {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
mode: 384
|
|
34
|
+
});
|
|
35
|
+
fs.chmodSync(file, 384);
|
|
36
|
+
}
|
|
37
|
+
// src/session/chrome-launcher.ts
|
|
12
38
|
const CANDIDATES = [
|
|
13
39
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
14
40
|
"/usr/bin/google-chrome",
|
|
@@ -21,8 +47,36 @@ function discoverChrome(explicitPath) {
|
|
|
21
47
|
if (!found) throw new Error("No supported Chrome installation found.");
|
|
22
48
|
return found;
|
|
23
49
|
}
|
|
24
|
-
|
|
25
|
-
|
|
50
|
+
// src/session/browser-controller.ts
|
|
51
|
+
/**
|
|
52
|
+
* Detect a running Chrome that owns `profileDir`. On macOS/Linux `SingletonLock`
|
|
53
|
+
* is a dangling symlink to `<hostname>-<pid>`, so existsSync (which follows
|
|
54
|
+
* symlinks) cannot see it; inspect the link itself and probe the pid.
|
|
55
|
+
*/
|
|
56
|
+
function isProfileLocked(profileDir) {
|
|
57
|
+
const lockFile = path.join(profileDir, "SingletonLock");
|
|
58
|
+
let target;
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.lstatSync(lockFile).isSymbolicLink()) return { locked: true };
|
|
61
|
+
target = fs.readlinkSync(lockFile);
|
|
62
|
+
} catch {
|
|
63
|
+
return { locked: false };
|
|
64
|
+
}
|
|
65
|
+
const pid = Number(target.slice(target.lastIndexOf("-") + 1));
|
|
66
|
+
if (!Number.isInteger(pid) || pid <= 0) return { locked: true };
|
|
67
|
+
try {
|
|
68
|
+
process.kill(pid, 0);
|
|
69
|
+
return {
|
|
70
|
+
locked: true,
|
|
71
|
+
pid
|
|
72
|
+
};
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return {
|
|
75
|
+
locked: e.code === "EPERM",
|
|
76
|
+
pid
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
26
80
|
var CdpConnection = class CdpConnection {
|
|
27
81
|
ws;
|
|
28
82
|
nextId = 0;
|
|
@@ -749,7 +803,7 @@ function buildKeyDispatchPayload(inputKey) {
|
|
|
749
803
|
};
|
|
750
804
|
}
|
|
751
805
|
/** Connection pool configuration */
|
|
752
|
-
const CONNECTION_IDLE_TIMEOUT_MS =
|
|
806
|
+
const CONNECTION_IDLE_TIMEOUT_MS = 3e5;
|
|
753
807
|
const CONNECTION_CLEANUP_INTERVAL_MS = 6e4;
|
|
754
808
|
/**
|
|
755
809
|
* True when the error means the socket was closed/not-open — i.e. the request
|
|
@@ -765,6 +819,7 @@ var ChromeCdpBrowserController = class {
|
|
|
765
819
|
baseDir;
|
|
766
820
|
connectionFactory;
|
|
767
821
|
connections = /* @__PURE__ */ new Map();
|
|
822
|
+
pendingConnections = /* @__PURE__ */ new Map();
|
|
768
823
|
cleanupInterval;
|
|
769
824
|
/**
|
|
770
825
|
* User-agent override resolved at target init, keyed by target ws URL.
|
|
@@ -793,17 +848,27 @@ var ChromeCdpBrowserController = class {
|
|
|
793
848
|
cached.lastUsedAt = Date.now();
|
|
794
849
|
return cached.conn;
|
|
795
850
|
}
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
conn
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
851
|
+
const pending = this.pendingConnections.get(targetWsUrl);
|
|
852
|
+
if (pending) return pending;
|
|
853
|
+
const p = (async () => {
|
|
854
|
+
const conn = await this.connectionFactory(targetWsUrl);
|
|
855
|
+
await this.applySessionOverrides(conn, targetWsUrl);
|
|
856
|
+
this.connections.set(targetWsUrl, {
|
|
857
|
+
conn,
|
|
858
|
+
enabled: {
|
|
859
|
+
page: false,
|
|
860
|
+
runtime: false
|
|
861
|
+
},
|
|
862
|
+
lastUsedAt: Date.now()
|
|
863
|
+
});
|
|
864
|
+
return conn;
|
|
865
|
+
})();
|
|
866
|
+
this.pendingConnections.set(targetWsUrl, p);
|
|
867
|
+
try {
|
|
868
|
+
return await p;
|
|
869
|
+
} finally {
|
|
870
|
+
this.pendingConnections.delete(targetWsUrl);
|
|
871
|
+
}
|
|
807
872
|
}
|
|
808
873
|
/**
|
|
809
874
|
* Re-apply session-scoped stealth patches + UA override on a fresh connection.
|
|
@@ -883,9 +948,11 @@ var ChromeCdpBrowserController = class {
|
|
|
883
948
|
if (userProfileDir === "default") profileDir = resolveDefaultProfileDir();
|
|
884
949
|
else if (userProfileDir) profileDir = userProfileDir;
|
|
885
950
|
else profileDir = path.join(this.baseDir, "profiles", sessionId);
|
|
886
|
-
fs.mkdirSync(profileDir, { recursive: true });
|
|
951
|
+
if (userProfileDir) fs.mkdirSync(profileDir, { recursive: true });
|
|
952
|
+
else ensurePrivateDir(profileDir);
|
|
887
953
|
const lockFile = path.join(profileDir, "SingletonLock");
|
|
888
|
-
|
|
954
|
+
const lock = isProfileLocked(profileDir);
|
|
955
|
+
if (lock.locked) throw new Error(`Chrome profile is already in use (lock file exists: ${lockFile}${lock.pid ? `, pid ${lock.pid}` : ""}). Quit the running Chrome instance first, or use --cdp-url to connect to it instead.`);
|
|
889
956
|
const launchAttempts = headless ? [{ headless: true }] : [{ headless: false }, { headless: true }];
|
|
890
957
|
let lastError;
|
|
891
958
|
for (const attempt of launchAttempts) {
|
|
@@ -1078,6 +1145,40 @@ var ChromeCdpBrowserController = class {
|
|
|
1078
1145
|
if (value !== "ready" && value !== "cleared") throw new Error("Unable to resolve type target");
|
|
1079
1146
|
return value;
|
|
1080
1147
|
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Arm a wait for a main-frame navigation BEFORE the click is dispatched, so
|
|
1150
|
+
* the start event cannot be missed. Resolves "navigated" once the main frame
|
|
1151
|
+
* started loading and then committed/loaded (each stage timeout-tolerant),
|
|
1152
|
+
* or "none" when no main-frame load started within 500 ms.
|
|
1153
|
+
*/
|
|
1154
|
+
async armMainFrameNavigationWait(conn) {
|
|
1155
|
+
const matching = conn.waitForEventMatching?.bind(conn);
|
|
1156
|
+
if (!matching) return { result: (async () => {
|
|
1157
|
+
try {
|
|
1158
|
+
await conn.waitForEvent("Page.frameNavigated", 50);
|
|
1159
|
+
} catch {
|
|
1160
|
+
return "none";
|
|
1161
|
+
}
|
|
1162
|
+
try {
|
|
1163
|
+
await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
|
|
1164
|
+
} catch {}
|
|
1165
|
+
return "navigated";
|
|
1166
|
+
})() };
|
|
1167
|
+
let mainFrameId;
|
|
1168
|
+
try {
|
|
1169
|
+
mainFrameId = (await conn.send("Page.getFrameTree")).frameTree?.frame?.id;
|
|
1170
|
+
} catch {}
|
|
1171
|
+
const isMain = (params) => mainFrameId === void 0 || params.frameId === mainFrameId;
|
|
1172
|
+
const started = matching("Page.frameStartedLoading", isMain, 500).then(() => true, () => false);
|
|
1173
|
+
return { result: (async () => {
|
|
1174
|
+
if (!await started) return "none";
|
|
1175
|
+
const navigated = matching("Page.frameNavigated", (params) => !params.frame?.parentId, 5e3).catch(() => void 0);
|
|
1176
|
+
const loaded = Promise.race([matching("Page.frameStoppedLoading", isMain, 5e3), matching("Page.loadEventFired", () => true, 5e3)]).catch(() => void 0);
|
|
1177
|
+
await navigated;
|
|
1178
|
+
await loaded;
|
|
1179
|
+
return "navigated";
|
|
1180
|
+
})() };
|
|
1181
|
+
}
|
|
1081
1182
|
async dispatchMouseClick(conn, x, y) {
|
|
1082
1183
|
await conn.send("Input.dispatchMouseEvent", {
|
|
1083
1184
|
type: "mouseMoved",
|
|
@@ -1177,13 +1278,9 @@ var ChromeCdpBrowserController = class {
|
|
|
1177
1278
|
await this.ensureEnabled(targetWsUrl);
|
|
1178
1279
|
if (payload.action === "click") {
|
|
1179
1280
|
const point = await this.preparePointerTarget(conn, payload);
|
|
1281
|
+
const nav = await this.armMainFrameNavigationWait(conn);
|
|
1180
1282
|
await this.dispatchMouseClick(conn, point.x, point.y);
|
|
1181
|
-
|
|
1182
|
-
await conn.waitForEvent("Page.frameNavigated", 50);
|
|
1183
|
-
try {
|
|
1184
|
-
await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
|
|
1185
|
-
} catch {}
|
|
1186
|
-
} catch {}
|
|
1283
|
+
await nav.result;
|
|
1187
1284
|
return "clicked";
|
|
1188
1285
|
}
|
|
1189
1286
|
if (payload.action === "hover") {
|
|
@@ -2640,8 +2737,7 @@ var MockBrowserController = class {
|
|
|
2640
2737
|
terminate(_pid) {}
|
|
2641
2738
|
closeConnection(_targetWsUrl) {}
|
|
2642
2739
|
};
|
|
2643
|
-
|
|
2644
|
-
//#region src/lib/domain-schemas.ts
|
|
2740
|
+
// src/lib/domain-schemas.ts
|
|
2645
2741
|
const InteractActionSchema = z.enum([
|
|
2646
2742
|
"click",
|
|
2647
2743
|
"type",
|
|
@@ -2747,12 +2843,11 @@ z.object({
|
|
|
2747
2843
|
createdAt: z.string().datetime(),
|
|
2748
2844
|
severity: EventSeveritySchema
|
|
2749
2845
|
});
|
|
2750
|
-
|
|
2751
|
-
//#region src/session/session-store.ts
|
|
2846
|
+
// src/session/session-store.ts
|
|
2752
2847
|
var SessionStore = class {
|
|
2753
2848
|
filePath;
|
|
2754
2849
|
constructor(baseDir) {
|
|
2755
|
-
|
|
2850
|
+
ensurePrivateDir(baseDir);
|
|
2756
2851
|
this.filePath = path.join(baseDir, "sessions.json");
|
|
2757
2852
|
if (!fs.existsSync(this.filePath)) this.write({ sessions: {} });
|
|
2758
2853
|
}
|
|
@@ -2838,12 +2933,68 @@ var SessionStore = class {
|
|
|
2838
2933
|
}
|
|
2839
2934
|
write(state) {
|
|
2840
2935
|
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
2841
|
-
|
|
2936
|
+
writePrivateFileSync(tmp, JSON.stringify(state, null, 2));
|
|
2842
2937
|
fs.renameSync(tmp, this.filePath);
|
|
2843
2938
|
}
|
|
2844
2939
|
};
|
|
2845
|
-
|
|
2846
|
-
|
|
2940
|
+
// src/session/url-policy.ts
|
|
2941
|
+
function assertNavigableUrl(raw, opts) {
|
|
2942
|
+
let parsed;
|
|
2943
|
+
try {
|
|
2944
|
+
parsed = new URL(raw);
|
|
2945
|
+
} catch {
|
|
2946
|
+
throw new Error(`Invalid URL: ${raw}`);
|
|
2947
|
+
}
|
|
2948
|
+
const { protocol } = parsed;
|
|
2949
|
+
if (protocol === "http:" || protocol === "https:") return;
|
|
2950
|
+
if (raw === "about:blank") return;
|
|
2951
|
+
if (protocol === "file:" && opts.allowFileUrls) return;
|
|
2952
|
+
throw new Error(`Navigation to ${protocol} URLs is not allowed${protocol === "file:" ? " (set AGENTIC_BROWSER_ALLOW_FILE_URLS=true to allow file: URLs)" : ""}`);
|
|
2953
|
+
}
|
|
2954
|
+
// src/memory/redact-step-payload.ts
|
|
2955
|
+
const ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
2956
|
+
"action",
|
|
2957
|
+
"selector",
|
|
2958
|
+
"fallbackSelectors",
|
|
2959
|
+
"ref",
|
|
2960
|
+
"key",
|
|
2961
|
+
"direction",
|
|
2962
|
+
"amount",
|
|
2963
|
+
"deltaY",
|
|
2964
|
+
"scrollX",
|
|
2965
|
+
"scrollY",
|
|
2966
|
+
"timeoutMs",
|
|
2967
|
+
"redacted"
|
|
2968
|
+
]);
|
|
2969
|
+
const URL_IN_TEXT = /\bhttps?:\/\/[^\s"'<>]+/gi;
|
|
2970
|
+
/** Reduce a URL to origin + pathname, dropping query string and fragment. */
|
|
2971
|
+
function stripUrl(rawUrl) {
|
|
2972
|
+
try {
|
|
2973
|
+
const parsed = new URL(rawUrl);
|
|
2974
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
2975
|
+
} catch {
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
/** Strip query strings and fragments from every http(s) URL embedded in free text. */
|
|
2980
|
+
function redactUrlsInText(text) {
|
|
2981
|
+
return text.replace(URL_IN_TEXT, (match) => stripUrl(match) ?? "[url]");
|
|
2982
|
+
}
|
|
2983
|
+
/** Keep only structural, non-secret keys of a step payload; typed text and values are dropped. */
|
|
2984
|
+
function redactStepPayload(payload) {
|
|
2985
|
+
const result = {};
|
|
2986
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
2987
|
+
if (key === "url") {
|
|
2988
|
+
const stripped = typeof value === "string" ? stripUrl(value) : void 0;
|
|
2989
|
+
if (stripped !== void 0) result.url = stripped;
|
|
2990
|
+
continue;
|
|
2991
|
+
}
|
|
2992
|
+
if (ALLOWED_KEYS.has(key)) result[key] = value;
|
|
2993
|
+
}
|
|
2994
|
+
if ("text" in payload || "value" in payload) result.redacted = true;
|
|
2995
|
+
return result;
|
|
2996
|
+
}
|
|
2997
|
+
// src/session/session-manager.ts
|
|
2847
2998
|
var SessionManager = class {
|
|
2848
2999
|
ctx;
|
|
2849
3000
|
browser;
|
|
@@ -2926,6 +3077,7 @@ var SessionManager = class {
|
|
|
2926
3077
|
if (input.type === "navigate") {
|
|
2927
3078
|
const url = String(input.payload.url ?? "");
|
|
2928
3079
|
if (!url) throw new Error("navigate command requires payload.url");
|
|
3080
|
+
assertNavigableUrl(url, { allowFileUrls: this.ctx.config.allowFileUrls });
|
|
2929
3081
|
const finalUrl = await this.browser.navigate(record.targetWsUrl, url);
|
|
2930
3082
|
this.store.setLastUrl(sessionId, finalUrl);
|
|
2931
3083
|
resultMessage = `Navigated to ${finalUrl}`;
|
|
@@ -3219,7 +3371,7 @@ var SessionManager = class {
|
|
|
3219
3371
|
type: input.type === "navigate" ? "navigate" : input.type === "interact" ? "interact" : "assert",
|
|
3220
3372
|
summary: input.type === "interact" ? `interact:${action ?? "unknown"}` : input.type,
|
|
3221
3373
|
selector,
|
|
3222
|
-
payload: input.payload
|
|
3374
|
+
payload: redactStepPayload(input.payload)
|
|
3223
3375
|
},
|
|
3224
3376
|
selector,
|
|
3225
3377
|
url: inputUrl
|
|
@@ -3246,8 +3398,7 @@ var SessionManager = class {
|
|
|
3246
3398
|
}
|
|
3247
3399
|
}
|
|
3248
3400
|
};
|
|
3249
|
-
|
|
3250
|
-
//#region src/transport/control-api.ts
|
|
3401
|
+
// src/transport/control-api.ts
|
|
3251
3402
|
var ControlApi = class {
|
|
3252
3403
|
sessions;
|
|
3253
3404
|
constructor(sessions) {
|
|
@@ -3305,8 +3456,7 @@ var ControlApi = class {
|
|
|
3305
3456
|
return this.sessions.cleanupSessions(input);
|
|
3306
3457
|
}
|
|
3307
3458
|
};
|
|
3308
|
-
|
|
3309
|
-
//#region src/lib/config.ts
|
|
3459
|
+
// src/lib/config.ts
|
|
3310
3460
|
function loadConfig(env = process.env) {
|
|
3311
3461
|
const userProfile = env.AGENTIC_BROWSER_USER_PROFILE;
|
|
3312
3462
|
let userProfileDir;
|
|
@@ -3318,11 +3468,11 @@ function loadConfig(env = process.env) {
|
|
|
3318
3468
|
cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
|
|
3319
3469
|
userProfileDir,
|
|
3320
3470
|
headless: env.AGENTIC_BROWSER_HEADLESS === "true" || env.AGENTIC_BROWSER_HEADLESS === "1",
|
|
3321
|
-
userAgent: env.AGENTIC_BROWSER_USER_AGENT || void 0
|
|
3471
|
+
userAgent: env.AGENTIC_BROWSER_USER_AGENT || void 0,
|
|
3472
|
+
allowFileUrls: env.AGENTIC_BROWSER_ALLOW_FILE_URLS === "true" || env.AGENTIC_BROWSER_ALLOW_FILE_URLS === "1"
|
|
3322
3473
|
};
|
|
3323
3474
|
}
|
|
3324
|
-
|
|
3325
|
-
//#region src/observability/logger.ts
|
|
3475
|
+
// src/observability/logger.ts
|
|
3326
3476
|
var Logger = class {
|
|
3327
3477
|
namespace;
|
|
3328
3478
|
constructor(namespace) {
|
|
@@ -3353,8 +3503,7 @@ var Logger = class {
|
|
|
3353
3503
|
this.emit("error", message, context);
|
|
3354
3504
|
}
|
|
3355
3505
|
};
|
|
3356
|
-
|
|
3357
|
-
//#region src/observability/event-store.ts
|
|
3506
|
+
// src/observability/event-store.ts
|
|
3358
3507
|
const MAX_EVENTS_PER_SESSION = 500;
|
|
3359
3508
|
var EventStore = class {
|
|
3360
3509
|
events = /* @__PURE__ */ new Map();
|
|
@@ -3364,8 +3513,11 @@ var EventStore = class {
|
|
|
3364
3513
|
pendingFlushDelayMs = Number.POSITIVE_INFINITY;
|
|
3365
3514
|
flushInFlight;
|
|
3366
3515
|
constructor(baseDir) {
|
|
3367
|
-
|
|
3516
|
+
ensurePrivateDir(baseDir);
|
|
3368
3517
|
this.filePath = path.join(baseDir, "session-events.log");
|
|
3518
|
+
try {
|
|
3519
|
+
if (fs.existsSync(this.filePath)) fs.chmodSync(this.filePath, 384);
|
|
3520
|
+
} catch {}
|
|
3369
3521
|
process.on("beforeExit", this.flushSync);
|
|
3370
3522
|
process.on("exit", this.flushSync);
|
|
3371
3523
|
}
|
|
@@ -3416,7 +3568,10 @@ var EventStore = class {
|
|
|
3416
3568
|
async flushAsync() {
|
|
3417
3569
|
if (this.flushInFlight || this.bufferedLines.length === 0) return;
|
|
3418
3570
|
const chunk = this.bufferedLines.splice(0, this.bufferedLines.length).join("");
|
|
3419
|
-
this.flushInFlight = fs.promises.appendFile(this.filePath, chunk,
|
|
3571
|
+
this.flushInFlight = fs.promises.appendFile(this.filePath, chunk, {
|
|
3572
|
+
encoding: "utf8",
|
|
3573
|
+
mode: 384
|
|
3574
|
+
}).catch(() => {}).finally(() => {
|
|
3420
3575
|
this.flushInFlight = void 0;
|
|
3421
3576
|
if (this.bufferedLines.length > 0) this.flushSoon(0);
|
|
3422
3577
|
});
|
|
@@ -3430,12 +3585,14 @@ var EventStore = class {
|
|
|
3430
3585
|
if (this.bufferedLines.length === 0) return;
|
|
3431
3586
|
const chunk = this.bufferedLines.splice(0, this.bufferedLines.length).join("");
|
|
3432
3587
|
try {
|
|
3433
|
-
fs.appendFileSync(this.filePath, chunk,
|
|
3588
|
+
fs.appendFileSync(this.filePath, chunk, {
|
|
3589
|
+
encoding: "utf8",
|
|
3590
|
+
mode: 384
|
|
3591
|
+
});
|
|
3434
3592
|
} catch {}
|
|
3435
3593
|
};
|
|
3436
3594
|
};
|
|
3437
|
-
|
|
3438
|
-
//#region src/auth/session-token.ts
|
|
3595
|
+
// src/auth/session-token.ts
|
|
3439
3596
|
var SessionTokenService = class {
|
|
3440
3597
|
tokens = /* @__PURE__ */ new Map();
|
|
3441
3598
|
issue(sessionId) {
|
|
@@ -3461,8 +3618,7 @@ var SessionTokenService = class {
|
|
|
3461
3618
|
this.tokens.set(sessionId, token);
|
|
3462
3619
|
}
|
|
3463
3620
|
};
|
|
3464
|
-
|
|
3465
|
-
//#region src/memory/memory-index.ts
|
|
3621
|
+
// src/memory/memory-index.ts
|
|
3466
3622
|
function normalize(value) {
|
|
3467
3623
|
return value.trim().toLowerCase();
|
|
3468
3624
|
}
|
|
@@ -3571,8 +3727,7 @@ var MemoryIndex = class {
|
|
|
3571
3727
|
return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup)).filter(relevant).toSorted((a, b) => b.result.score - a.result.score).slice(0, limit).map((scored) => scored.result);
|
|
3572
3728
|
}
|
|
3573
3729
|
};
|
|
3574
|
-
|
|
3575
|
-
//#region src/memory/staleness-detector.ts
|
|
3730
|
+
// src/memory/staleness-detector.ts
|
|
3576
3731
|
const MAX_SUSPECT_STRIKES = 2;
|
|
3577
3732
|
function detectStalenessSignal(errorMessage, selector) {
|
|
3578
3733
|
const lowered = errorMessage.toLowerCase();
|
|
@@ -3600,7 +3755,7 @@ function applyFailure(insight, signal) {
|
|
|
3600
3755
|
};
|
|
3601
3756
|
}
|
|
3602
3757
|
function applyAgeStaleness(insight) {
|
|
3603
|
-
const ageDays = (Date.now() - Date.parse(insight.lastVerifiedAt)) /
|
|
3758
|
+
const ageDays = (Date.now() - Date.parse(insight.lastVerifiedAt)) / 864e5;
|
|
3604
3759
|
if (insight.freshness === "fresh" && ageDays > 30) return {
|
|
3605
3760
|
...insight,
|
|
3606
3761
|
freshness: "suspect",
|
|
@@ -3627,8 +3782,57 @@ function applySuccess(insight) {
|
|
|
3627
3782
|
updatedAt: now
|
|
3628
3783
|
};
|
|
3629
3784
|
}
|
|
3630
|
-
|
|
3631
|
-
|
|
3785
|
+
// src/lib/flush-registry.ts
|
|
3786
|
+
const REGISTRY = Symbol.for("agentic-browser.flush-registry");
|
|
3787
|
+
function flushAll(items) {
|
|
3788
|
+
for (const item of items) try {
|
|
3789
|
+
item.flushSync();
|
|
3790
|
+
} catch {}
|
|
3791
|
+
}
|
|
3792
|
+
function createRegistry() {
|
|
3793
|
+
const items = /* @__PURE__ */ new Set();
|
|
3794
|
+
const onSignal = (signal) => {
|
|
3795
|
+
flushAll(items);
|
|
3796
|
+
if (process.listenerCount(signal) === 1) process.exit(0);
|
|
3797
|
+
};
|
|
3798
|
+
return {
|
|
3799
|
+
items,
|
|
3800
|
+
onExit: () => flushAll(items),
|
|
3801
|
+
onSigint: () => onSignal("SIGINT"),
|
|
3802
|
+
onSigterm: () => onSignal("SIGTERM")
|
|
3803
|
+
};
|
|
3804
|
+
}
|
|
3805
|
+
/**
|
|
3806
|
+
* Register an item to be flushed synchronously on process exit / SIGINT / SIGTERM.
|
|
3807
|
+
* Returns an idempotent unregister function; the process listeners are removed
|
|
3808
|
+
* once the last item is unregistered.
|
|
3809
|
+
*/
|
|
3810
|
+
function registerFlushable(item) {
|
|
3811
|
+
const proc = process;
|
|
3812
|
+
let registry = proc[REGISTRY];
|
|
3813
|
+
if (!registry) {
|
|
3814
|
+
registry = createRegistry();
|
|
3815
|
+
proc[REGISTRY] = registry;
|
|
3816
|
+
process.on("exit", registry.onExit);
|
|
3817
|
+
process.on("SIGINT", registry.onSigint);
|
|
3818
|
+
process.on("SIGTERM", registry.onSigterm);
|
|
3819
|
+
}
|
|
3820
|
+
registry.items.add(item);
|
|
3821
|
+
const owner = registry;
|
|
3822
|
+
let done = false;
|
|
3823
|
+
return () => {
|
|
3824
|
+
if (done) return;
|
|
3825
|
+
done = true;
|
|
3826
|
+
owner.items.delete(item);
|
|
3827
|
+
if (owner.items.size === 0 && proc[REGISTRY] === owner) {
|
|
3828
|
+
process.off("exit", owner.onExit);
|
|
3829
|
+
process.off("SIGINT", owner.onSigint);
|
|
3830
|
+
process.off("SIGTERM", owner.onSigterm);
|
|
3831
|
+
delete proc[REGISTRY];
|
|
3832
|
+
}
|
|
3833
|
+
};
|
|
3834
|
+
}
|
|
3835
|
+
// src/memory/memory-schemas.ts
|
|
3632
3836
|
const InsightFreshnessSchema = z.enum([
|
|
3633
3837
|
"fresh",
|
|
3634
3838
|
"suspect",
|
|
@@ -3705,27 +3909,25 @@ const SiteProfileSchema = z.object({
|
|
|
3705
3909
|
createdAt: z.string().datetime()
|
|
3706
3910
|
});
|
|
3707
3911
|
const SiteProfileStateSchema = z.object({ profiles: z.array(SiteProfileSchema) });
|
|
3708
|
-
|
|
3709
|
-
//#region src/memory/site-profile-store.ts
|
|
3912
|
+
// src/memory/site-profile-store.ts
|
|
3710
3913
|
const EMPTY_STATE$1 = { profiles: [] };
|
|
3711
3914
|
const FLUSH_DELAY_MS$1 = 500;
|
|
3712
|
-
const REGISTERED_EXIT_HANDLERS$1 = Symbol.for("agentic-browser.site-profile-store.exit-handlers");
|
|
3713
|
-
const REGISTERED_STORES$1 = Symbol.for("agentic-browser.site-profile-store.instances");
|
|
3714
3915
|
var SiteProfileStore = class {
|
|
3715
3916
|
filePath;
|
|
3716
3917
|
cached = null;
|
|
3717
3918
|
dirty = false;
|
|
3718
3919
|
flushTimer = null;
|
|
3920
|
+
unregister;
|
|
3719
3921
|
constructor(baseDir) {
|
|
3720
3922
|
const memoryDir = path.join(baseDir, "memory");
|
|
3721
|
-
|
|
3923
|
+
ensurePrivateDir(memoryDir);
|
|
3722
3924
|
this.filePath = path.join(memoryDir, "site-profiles.json");
|
|
3723
3925
|
const tmpPath = `${this.filePath}.tmp`;
|
|
3724
3926
|
try {
|
|
3725
3927
|
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
3726
3928
|
} catch {}
|
|
3727
3929
|
if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE$1);
|
|
3728
|
-
this.
|
|
3930
|
+
this.unregister = registerFlushable(this);
|
|
3729
3931
|
}
|
|
3730
3932
|
list() {
|
|
3731
3933
|
return this.getCache();
|
|
@@ -3746,6 +3948,11 @@ var SiteProfileStore = class {
|
|
|
3746
3948
|
else profiles.push(normalized);
|
|
3747
3949
|
this.markDirty();
|
|
3748
3950
|
}
|
|
3951
|
+
/** Flush pending writes and stop flushing on process exit. Idempotent. */
|
|
3952
|
+
dispose() {
|
|
3953
|
+
this.flushSync();
|
|
3954
|
+
this.unregister();
|
|
3955
|
+
}
|
|
3749
3956
|
flushSync() {
|
|
3750
3957
|
if (this.flushTimer) {
|
|
3751
3958
|
clearTimeout(this.flushTimer);
|
|
@@ -3802,51 +4009,30 @@ var SiteProfileStore = class {
|
|
|
3802
4009
|
}
|
|
3803
4010
|
writeDisk(state) {
|
|
3804
4011
|
const tempPath = `${this.filePath}.tmp`;
|
|
3805
|
-
|
|
4012
|
+
writePrivateFileSync(tempPath, JSON.stringify(state, null, 2));
|
|
3806
4013
|
fs.renameSync(tempPath, this.filePath);
|
|
3807
4014
|
}
|
|
3808
|
-
registerProcessHandlers() {
|
|
3809
|
-
const proc = process;
|
|
3810
|
-
const stores = proc[REGISTERED_STORES$1] ??= /* @__PURE__ */ new Set();
|
|
3811
|
-
stores.add(this);
|
|
3812
|
-
if (proc[REGISTERED_EXIT_HANDLERS$1]) return;
|
|
3813
|
-
proc[REGISTERED_EXIT_HANDLERS$1] = true;
|
|
3814
|
-
const flushAll = () => {
|
|
3815
|
-
for (const store of stores) store.flushSync();
|
|
3816
|
-
};
|
|
3817
|
-
process.on("exit", flushAll);
|
|
3818
|
-
process.on("SIGINT", () => {
|
|
3819
|
-
flushAll();
|
|
3820
|
-
process.exit(0);
|
|
3821
|
-
});
|
|
3822
|
-
process.on("SIGTERM", () => {
|
|
3823
|
-
flushAll();
|
|
3824
|
-
process.exit(0);
|
|
3825
|
-
});
|
|
3826
|
-
}
|
|
3827
4015
|
};
|
|
3828
|
-
|
|
3829
|
-
//#region src/memory/task-insight-store.ts
|
|
4016
|
+
// src/memory/task-insight-store.ts
|
|
3830
4017
|
const EMPTY_STATE = { insights: [] };
|
|
3831
4018
|
const FLUSH_DELAY_MS = 500;
|
|
3832
|
-
const REGISTERED_EXIT_HANDLERS = Symbol.for("agentic-browser.task-insight-store.exit-handlers");
|
|
3833
|
-
const REGISTERED_STORES = Symbol.for("agentic-browser.task-insight-store.instances");
|
|
3834
4019
|
var TaskInsightStore = class {
|
|
3835
4020
|
filePath;
|
|
3836
4021
|
/** In-memory cache – authoritative after first load. */
|
|
3837
4022
|
cached = null;
|
|
3838
4023
|
dirty = false;
|
|
3839
4024
|
flushTimer = null;
|
|
4025
|
+
unregister;
|
|
3840
4026
|
constructor(baseDir) {
|
|
3841
4027
|
const memoryDir = path.join(baseDir, "memory");
|
|
3842
|
-
|
|
4028
|
+
ensurePrivateDir(memoryDir);
|
|
3843
4029
|
this.filePath = path.join(memoryDir, "insights.json");
|
|
3844
4030
|
const tmpPath = `${this.filePath}.tmp`;
|
|
3845
4031
|
try {
|
|
3846
4032
|
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
3847
4033
|
} catch {}
|
|
3848
4034
|
if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE);
|
|
3849
|
-
this.
|
|
4035
|
+
this.unregister = registerFlushable(this);
|
|
3850
4036
|
}
|
|
3851
4037
|
list() {
|
|
3852
4038
|
return this.getCache();
|
|
@@ -3876,6 +4062,11 @@ var TaskInsightStore = class {
|
|
|
3876
4062
|
this.cached = insights;
|
|
3877
4063
|
this.markDirty();
|
|
3878
4064
|
}
|
|
4065
|
+
/** Flush pending writes and stop flushing on process exit. Idempotent. */
|
|
4066
|
+
dispose() {
|
|
4067
|
+
this.flushSync();
|
|
4068
|
+
this.unregister();
|
|
4069
|
+
}
|
|
3879
4070
|
/** Force an immediate synchronous flush (used at shutdown). */
|
|
3880
4071
|
flushSync() {
|
|
3881
4072
|
if (this.flushTimer) {
|
|
@@ -3892,8 +4083,52 @@ var TaskInsightStore = class {
|
|
|
3892
4083
|
if (this.cached) return this.cached;
|
|
3893
4084
|
const state = this.readDisk();
|
|
3894
4085
|
this.cached = state.insights;
|
|
4086
|
+
if (this.sanitizeCached(this.cached)) this.markDirty();
|
|
3895
4087
|
return this.cached;
|
|
3896
4088
|
}
|
|
4089
|
+
/**
|
|
4090
|
+
* Redact already-persisted step payloads and URL query strings/fragments in place.
|
|
4091
|
+
* Returns true if anything changed.
|
|
4092
|
+
*/
|
|
4093
|
+
sanitizeCached(insights) {
|
|
4094
|
+
let changed = false;
|
|
4095
|
+
for (const insight of insights) {
|
|
4096
|
+
const expectedOutcome = redactUrlsInText(insight.expectedOutcome);
|
|
4097
|
+
if (expectedOutcome !== insight.expectedOutcome) {
|
|
4098
|
+
insight.expectedOutcome = expectedOutcome;
|
|
4099
|
+
changed = true;
|
|
4100
|
+
}
|
|
4101
|
+
const taskIntent = redactUrlsInText(insight.taskIntent);
|
|
4102
|
+
if (taskIntent !== insight.taskIntent) {
|
|
4103
|
+
insight.taskIntent = taskIntent;
|
|
4104
|
+
changed = true;
|
|
4105
|
+
}
|
|
4106
|
+
for (const evidence of insight.evidence) {
|
|
4107
|
+
if (evidence.url !== void 0) {
|
|
4108
|
+
const url = stripUrl(evidence.url);
|
|
4109
|
+
if (url !== evidence.url) {
|
|
4110
|
+
evidence.url = url;
|
|
4111
|
+
changed = true;
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
if (evidence.reason !== void 0) {
|
|
4115
|
+
const reason = redactUrlsInText(evidence.reason);
|
|
4116
|
+
if (reason !== evidence.reason) {
|
|
4117
|
+
evidence.reason = reason;
|
|
4118
|
+
changed = true;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
for (const step of insight.actionRecipe) {
|
|
4123
|
+
const redacted = redactStepPayload(step.payload);
|
|
4124
|
+
if (JSON.stringify(redacted) !== JSON.stringify(step.payload)) {
|
|
4125
|
+
step.payload = redacted;
|
|
4126
|
+
changed = true;
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
return changed;
|
|
4131
|
+
}
|
|
3897
4132
|
markDirty() {
|
|
3898
4133
|
this.dirty = true;
|
|
3899
4134
|
if (!this.flushTimer) this.flushTimer = setTimeout(() => {
|
|
@@ -3935,31 +4170,11 @@ var TaskInsightStore = class {
|
|
|
3935
4170
|
}
|
|
3936
4171
|
writeDisk(state) {
|
|
3937
4172
|
const tempPath = `${this.filePath}.tmp`;
|
|
3938
|
-
|
|
4173
|
+
writePrivateFileSync(tempPath, JSON.stringify(state, null, 2));
|
|
3939
4174
|
fs.renameSync(tempPath, this.filePath);
|
|
3940
4175
|
}
|
|
3941
|
-
registerProcessHandlers() {
|
|
3942
|
-
const proc = process;
|
|
3943
|
-
const stores = proc[REGISTERED_STORES] ??= /* @__PURE__ */ new Set();
|
|
3944
|
-
stores.add(this);
|
|
3945
|
-
if (proc[REGISTERED_EXIT_HANDLERS]) return;
|
|
3946
|
-
proc[REGISTERED_EXIT_HANDLERS] = true;
|
|
3947
|
-
const flushAll = () => {
|
|
3948
|
-
for (const store of stores) store.flushSync();
|
|
3949
|
-
};
|
|
3950
|
-
process.on("exit", flushAll);
|
|
3951
|
-
process.on("SIGINT", () => {
|
|
3952
|
-
flushAll();
|
|
3953
|
-
process.exit(0);
|
|
3954
|
-
});
|
|
3955
|
-
process.on("SIGTERM", () => {
|
|
3956
|
-
flushAll();
|
|
3957
|
-
process.exit(0);
|
|
3958
|
-
});
|
|
3959
|
-
}
|
|
3960
4176
|
};
|
|
3961
|
-
|
|
3962
|
-
//#region src/memory/memory-service.ts
|
|
4177
|
+
// src/memory/memory-service.ts
|
|
3963
4178
|
/** Memory service configuration */
|
|
3964
4179
|
const SEARCH_CACHE_TTL_MS = 2e3;
|
|
3965
4180
|
const MAX_SELECTOR_ALIASES = 10;
|
|
@@ -3987,6 +4202,11 @@ var MemoryService = class {
|
|
|
3987
4202
|
this.store.flushSync();
|
|
3988
4203
|
this.profileStore.flushSync();
|
|
3989
4204
|
}
|
|
4205
|
+
/** Flush pending writes and release the stores' process exit hooks. */
|
|
4206
|
+
dispose() {
|
|
4207
|
+
this.store.dispose();
|
|
4208
|
+
this.profileStore.dispose();
|
|
4209
|
+
}
|
|
3990
4210
|
search(input) {
|
|
3991
4211
|
const cacheKey = `${input.taskIntent}\0${input.siteDomain ?? ""}\0${input.limit ?? 10}`;
|
|
3992
4212
|
const now = Date.now();
|
|
@@ -4048,7 +4268,8 @@ var MemoryService = class {
|
|
|
4048
4268
|
this.invalidateSearchCache();
|
|
4049
4269
|
return verified;
|
|
4050
4270
|
}
|
|
4051
|
-
recordSuccess(
|
|
4271
|
+
recordSuccess(rawInput) {
|
|
4272
|
+
const input = this.sanitizeInput(rawInput);
|
|
4052
4273
|
this.updateSiteProfile(input.siteDomain, input.selector, input.url, input.sitePathPattern);
|
|
4053
4274
|
const insights = this.store.list();
|
|
4054
4275
|
const matched = this.findBestExactMatch(insights, input);
|
|
@@ -4107,13 +4328,14 @@ var MemoryService = class {
|
|
|
4107
4328
|
this.invalidateSearchCache();
|
|
4108
4329
|
return withAliases;
|
|
4109
4330
|
}
|
|
4110
|
-
recordFailure(
|
|
4331
|
+
recordFailure(rawInput, errorMessage) {
|
|
4332
|
+
const input = this.sanitizeInput(rawInput);
|
|
4111
4333
|
this.updateSiteProfile(input.siteDomain, input.selector, input.url, input.sitePathPattern);
|
|
4112
4334
|
const insights = this.store.list();
|
|
4113
4335
|
const matched = this.findBestExactMatch(insights, input);
|
|
4114
4336
|
if (!matched) return;
|
|
4115
4337
|
const signal = detectStalenessSignal(errorMessage, input.selector);
|
|
4116
|
-
const evidence = this.createEvidence(input, "failure", errorMessage);
|
|
4338
|
+
const evidence = this.createEvidence(input, "failure", redactUrlsInText(errorMessage));
|
|
4117
4339
|
const failed = applyFailure({
|
|
4118
4340
|
...matched,
|
|
4119
4341
|
useCount: matched.useCount + 1,
|
|
@@ -4235,6 +4457,15 @@ var MemoryService = class {
|
|
|
4235
4457
|
const rightSegment = normalizePathSegment(right);
|
|
4236
4458
|
return Boolean(leftSegment) && leftSegment === rightSegment;
|
|
4237
4459
|
}
|
|
4460
|
+
/** Strip query strings and fragments from every URL-bearing field before it is persisted. */
|
|
4461
|
+
sanitizeInput(input) {
|
|
4462
|
+
return {
|
|
4463
|
+
...input,
|
|
4464
|
+
url: input.url ? stripUrl(input.url) ?? void 0 : void 0,
|
|
4465
|
+
expectedOutcome: redactUrlsInText(input.expectedOutcome),
|
|
4466
|
+
taskIntent: redactUrlsInText(input.taskIntent)
|
|
4467
|
+
};
|
|
4468
|
+
}
|
|
4238
4469
|
createEvidence(input, result, reason) {
|
|
4239
4470
|
return {
|
|
4240
4471
|
evidenceId: crypto.randomUUID(),
|
|
@@ -4287,8 +4518,7 @@ var MemoryService = class {
|
|
|
4287
4518
|
return [...recipe, step].slice(-8);
|
|
4288
4519
|
}
|
|
4289
4520
|
};
|
|
4290
|
-
|
|
4291
|
-
//#region src/cli/app.ts
|
|
4521
|
+
// src/cli/app.ts
|
|
4292
4522
|
function createAppContext(env = process.env) {
|
|
4293
4523
|
const config = loadConfig(env);
|
|
4294
4524
|
return {
|
|
@@ -4299,17 +4529,31 @@ function createAppContext(env = process.env) {
|
|
|
4299
4529
|
memoryService: new MemoryService(config.dataDir)
|
|
4300
4530
|
};
|
|
4301
4531
|
}
|
|
4302
|
-
|
|
4303
|
-
//#region src/cli/runtime.ts
|
|
4532
|
+
// src/cli/runtime.ts
|
|
4304
4533
|
var AgenticBrowserCore = class {
|
|
4305
4534
|
context;
|
|
4306
4535
|
sessions;
|
|
4307
4536
|
api;
|
|
4537
|
+
browserController;
|
|
4538
|
+
disposed = false;
|
|
4308
4539
|
constructor(context, browserController) {
|
|
4309
4540
|
this.context = context;
|
|
4541
|
+
this.browserController = browserController;
|
|
4310
4542
|
this.sessions = new SessionManager(context, browserController);
|
|
4311
4543
|
this.api = new ControlApi(this.sessions);
|
|
4312
4544
|
}
|
|
4545
|
+
/**
|
|
4546
|
+
* Release process-level resources (event store and memory store exit handlers,
|
|
4547
|
+
* controller timers). Flushes pending memory writes.
|
|
4548
|
+
* Does not terminate browser sessions. Idempotent.
|
|
4549
|
+
*/
|
|
4550
|
+
async dispose() {
|
|
4551
|
+
if (this.disposed) return;
|
|
4552
|
+
this.disposed = true;
|
|
4553
|
+
this.browserController.dispose?.();
|
|
4554
|
+
this.context.eventStore.dispose();
|
|
4555
|
+
this.context.memoryService.dispose();
|
|
4556
|
+
}
|
|
4313
4557
|
async startSession(input = { browser: "chrome" }) {
|
|
4314
4558
|
return await this.api.createSession(input);
|
|
4315
4559
|
}
|
|
@@ -4380,5 +4624,4 @@ function createMockAgenticBrowserCore(env) {
|
|
|
4380
4624
|
function createCliRuntime() {
|
|
4381
4625
|
return createAgenticBrowserCore();
|
|
4382
4626
|
}
|
|
4383
|
-
|
|
4384
|
-
export { createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, createCliRuntime as r, AgenticBrowserCore as t };
|
|
4627
|
+
export { isConnectionError as a, createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, ensurePrivateDir as o, createCliRuntime as r, writePrivateFileSync as s, AgenticBrowserCore as t };
|