@agenticmail/enterprise 0.5.428 → 0.5.430
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/dist/agent-tools-3PNQJJTK.js +14587 -0
- package/dist/agent-tools-ZH3PS3IC.js +14587 -0
- package/dist/browser-tool-EZSPX3TC.js +1422 -0
- package/dist/browser-tool-KZKTZWKC.js +1421 -0
- package/dist/chunk-BNBIAOFW.js +2245 -0
- package/dist/chunk-BPMFEANX.js +5174 -0
- package/dist/chunk-ENZC7XHZ.js +1728 -0
- package/dist/chunk-G3WYWPBQ.js +4993 -0
- package/dist/chunk-GLSSBRED.js +615 -0
- package/dist/chunk-GMZVPQRB.js +5174 -0
- package/dist/chunk-LPBX6ZJH.js +4993 -0
- package/dist/chunk-M2BWAQLV.js +1728 -0
- package/dist/chunk-MCZNCJQ2.js +1950 -0
- package/dist/chunk-PD6VAQUR.js +2130 -0
- package/dist/chunk-XIKM3XJ2.js +359 -0
- package/dist/cli-agent-G7QJYDRZ.js +2715 -0
- package/dist/cli-agent-GSSLO7UA.js +2715 -0
- package/dist/cli-serve-CYNJ5YXG.js +286 -0
- package/dist/cli-serve-DTHX2577.js +286 -0
- package/dist/cli.js +3 -3
- package/dist/index.js +3 -3
- package/dist/pw-ai-67OVXPAQ.js +2325 -0
- package/dist/routes-M3VDE73Z.js +10 -0
- package/dist/routes-OQL7SSF4.js +10 -0
- package/dist/runtime-A3GQ7RQU.js +46 -0
- package/dist/runtime-NS4VTG75.js +46 -0
- package/dist/server-PTGCJ7CS.js +28 -0
- package/dist/server-TRTPSYSO.js +28 -0
- package/dist/server-context-ITH6WO3K.js +12 -0
- package/dist/setup-2RQUWYQR.js +20 -0
- package/dist/setup-ZBLSE6JX.js +20 -0
- package/logs/cloudflared-error.log +0 -367
- package/logs/cloudflared-error__2026-03-07_00-00-00.log +375 -0
- package/logs/john-error.log +0 -25
- package/logs/john-error__2026-03-07_00-00-00.log +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1422 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBrowserRouteContext
|
|
3
|
+
} from "./chunk-GLSSBRED.js";
|
|
4
|
+
import {
|
|
5
|
+
registerBrowserRoutes
|
|
6
|
+
} from "./chunk-BNBIAOFW.js";
|
|
7
|
+
import {
|
|
8
|
+
resolveBrowserConfig,
|
|
9
|
+
resolveProfile
|
|
10
|
+
} from "./chunk-XIKM3XJ2.js";
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
|
|
13
|
+
ensureChromeExtensionRelayServer
|
|
14
|
+
} from "./chunk-MCZNCJQ2.js";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_UPLOAD_DIR,
|
|
17
|
+
createSubsystemLogger,
|
|
18
|
+
ensureGatewayStartupAuth,
|
|
19
|
+
escapeRegExp,
|
|
20
|
+
formatCliCommand,
|
|
21
|
+
loadConfig,
|
|
22
|
+
resolveGatewayAuth,
|
|
23
|
+
resolvePathsWithinRoot,
|
|
24
|
+
wrapExternalContent
|
|
25
|
+
} from "./chunk-A3PUJDNH.js";
|
|
26
|
+
import {
|
|
27
|
+
imageResultFromFile,
|
|
28
|
+
jsonResult,
|
|
29
|
+
readStringParam
|
|
30
|
+
} from "./chunk-ZB3VC2MR.js";
|
|
31
|
+
import "./chunk-KFQGP6VL.js";
|
|
32
|
+
|
|
33
|
+
// src/browser/client-actions-url.ts
|
|
34
|
+
function buildProfileQuery(profile) {
|
|
35
|
+
return profile ? `?profile=${encodeURIComponent(profile)}` : "";
|
|
36
|
+
}
|
|
37
|
+
function withBaseUrl(baseUrl, path) {
|
|
38
|
+
const trimmed = baseUrl?.trim();
|
|
39
|
+
if (!trimmed) {
|
|
40
|
+
return path;
|
|
41
|
+
}
|
|
42
|
+
return `${trimmed.replace(/\/$/, "")}${path}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/browser/bridge-auth-registry.ts
|
|
46
|
+
var authByPort = /* @__PURE__ */ new Map();
|
|
47
|
+
function getBridgeAuthForPort(port) {
|
|
48
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
return authByPort.get(port);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/browser/control-auth.ts
|
|
55
|
+
function resolveBrowserControlAuth(cfg, env = process.env) {
|
|
56
|
+
const auth = resolveGatewayAuth({
|
|
57
|
+
authConfig: cfg?.gateway?.auth,
|
|
58
|
+
env,
|
|
59
|
+
tailscaleMode: cfg?.gateway?.tailscale?.mode
|
|
60
|
+
});
|
|
61
|
+
const token = typeof auth?.token === "string" ? auth.token.trim() : "";
|
|
62
|
+
const password = typeof auth?.password === "string" ? auth.password.trim() : "";
|
|
63
|
+
return {
|
|
64
|
+
token: token || void 0,
|
|
65
|
+
password: password || void 0
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function shouldAutoGenerateBrowserAuth(env) {
|
|
69
|
+
const nodeEnv = (env.NODE_ENV ?? "").trim().toLowerCase();
|
|
70
|
+
if (nodeEnv === "test") {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const vitest = (env.VITEST ?? "").trim().toLowerCase();
|
|
74
|
+
if (vitest && vitest !== "0" && vitest !== "false" && vitest !== "off") {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
async function ensureBrowserControlAuth(params) {
|
|
80
|
+
const env = params.env ?? process.env;
|
|
81
|
+
const auth = resolveBrowserControlAuth(params.cfg, env);
|
|
82
|
+
if (auth.token || auth.password) {
|
|
83
|
+
return { auth };
|
|
84
|
+
}
|
|
85
|
+
if (!shouldAutoGenerateBrowserAuth(env)) {
|
|
86
|
+
return { auth };
|
|
87
|
+
}
|
|
88
|
+
if (params.cfg.gateway?.auth?.mode === "password") {
|
|
89
|
+
return { auth };
|
|
90
|
+
}
|
|
91
|
+
if (params.cfg.gateway?.auth?.mode === "none") {
|
|
92
|
+
return { auth };
|
|
93
|
+
}
|
|
94
|
+
if (params.cfg.gateway?.auth?.mode === "trusted-proxy") {
|
|
95
|
+
return { auth };
|
|
96
|
+
}
|
|
97
|
+
const latestCfg = loadConfig();
|
|
98
|
+
const latestAuth = resolveBrowserControlAuth(latestCfg, env);
|
|
99
|
+
if (latestAuth.token || latestAuth.password) {
|
|
100
|
+
return { auth: latestAuth };
|
|
101
|
+
}
|
|
102
|
+
if (latestCfg.gateway?.auth?.mode === "password") {
|
|
103
|
+
return { auth: latestAuth };
|
|
104
|
+
}
|
|
105
|
+
if (latestCfg.gateway?.auth?.mode === "none") {
|
|
106
|
+
return { auth: latestAuth };
|
|
107
|
+
}
|
|
108
|
+
if (latestCfg.gateway?.auth?.mode === "trusted-proxy") {
|
|
109
|
+
return { auth: latestAuth };
|
|
110
|
+
}
|
|
111
|
+
const ensured = await ensureGatewayStartupAuth({
|
|
112
|
+
cfg: latestCfg,
|
|
113
|
+
env,
|
|
114
|
+
persist: true
|
|
115
|
+
});
|
|
116
|
+
const ensuredAuth = resolveBrowserControlAuth(ensured.cfg, env);
|
|
117
|
+
return {
|
|
118
|
+
auth: ensuredAuth,
|
|
119
|
+
generatedToken: ensured.generatedToken
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/browser/server-lifecycle.ts
|
|
124
|
+
async function ensureExtensionRelayForProfiles(params) {
|
|
125
|
+
for (const name of Object.keys(params.resolved.profiles)) {
|
|
126
|
+
const profile = resolveProfile(params.resolved, name);
|
|
127
|
+
if (!profile || profile.driver !== "extension") {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch((err) => {
|
|
131
|
+
params.onWarn(`Chrome extension relay init failed for profile "${name}": ${String(err)}`);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/browser/control-service.ts
|
|
137
|
+
var state = null;
|
|
138
|
+
var log = createSubsystemLogger("browser");
|
|
139
|
+
var logService = log.child("service");
|
|
140
|
+
function createBrowserControlContext() {
|
|
141
|
+
return createBrowserRouteContext({
|
|
142
|
+
getState: () => state,
|
|
143
|
+
refreshConfigFromDisk: true
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async function startBrowserControlServiceFromConfig() {
|
|
147
|
+
if (state) {
|
|
148
|
+
return state;
|
|
149
|
+
}
|
|
150
|
+
const cfg = loadConfig();
|
|
151
|
+
const resolved = resolveBrowserConfig(cfg.browser, cfg);
|
|
152
|
+
if (!resolved.enabled) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const ensured = await ensureBrowserControlAuth({ cfg });
|
|
157
|
+
if (ensured.generatedToken) {
|
|
158
|
+
logService.info("No browser auth configured; generated gateway.auth.token automatically.");
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
logService.warn(`failed to auto-configure browser auth: ${String(err)}`);
|
|
162
|
+
}
|
|
163
|
+
state = {
|
|
164
|
+
server: null,
|
|
165
|
+
port: resolved.controlPort,
|
|
166
|
+
resolved,
|
|
167
|
+
profiles: /* @__PURE__ */ new Map()
|
|
168
|
+
};
|
|
169
|
+
await ensureExtensionRelayForProfiles({
|
|
170
|
+
resolved,
|
|
171
|
+
onWarn: (message) => logService.warn(message)
|
|
172
|
+
});
|
|
173
|
+
logService.info(
|
|
174
|
+
`Browser control service ready (profiles=${Object.keys(resolved.profiles).length})`
|
|
175
|
+
);
|
|
176
|
+
return state;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/browser/routes/dispatcher.ts
|
|
180
|
+
function compileRoute(path) {
|
|
181
|
+
const paramNames = [];
|
|
182
|
+
const parts = path.split("/").map((part) => {
|
|
183
|
+
if (part.startsWith(":")) {
|
|
184
|
+
const name = part.slice(1);
|
|
185
|
+
paramNames.push(name);
|
|
186
|
+
return "([^/]+)";
|
|
187
|
+
}
|
|
188
|
+
return escapeRegExp(part);
|
|
189
|
+
});
|
|
190
|
+
return { regex: new RegExp(`^${parts.join("/")}$`), paramNames };
|
|
191
|
+
}
|
|
192
|
+
function createRegistry() {
|
|
193
|
+
const routes = [];
|
|
194
|
+
const register = (method) => (path, handler) => {
|
|
195
|
+
const { regex, paramNames } = compileRoute(path);
|
|
196
|
+
routes.push({ method, path, regex, paramNames, handler });
|
|
197
|
+
};
|
|
198
|
+
const router = {
|
|
199
|
+
get: register("GET"),
|
|
200
|
+
post: register("POST"),
|
|
201
|
+
delete: register("DELETE")
|
|
202
|
+
};
|
|
203
|
+
return { routes, router };
|
|
204
|
+
}
|
|
205
|
+
function normalizePath(path) {
|
|
206
|
+
if (!path) {
|
|
207
|
+
return "/";
|
|
208
|
+
}
|
|
209
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
210
|
+
}
|
|
211
|
+
function createBrowserRouteDispatcher(ctx) {
|
|
212
|
+
const registry = createRegistry();
|
|
213
|
+
registerBrowserRoutes(registry.router, ctx);
|
|
214
|
+
return {
|
|
215
|
+
dispatch: async (req) => {
|
|
216
|
+
const method = req.method;
|
|
217
|
+
const path = normalizePath(req.path);
|
|
218
|
+
const query = req.query ?? {};
|
|
219
|
+
const body = req.body;
|
|
220
|
+
const signal = req.signal;
|
|
221
|
+
const match = registry.routes.find((route) => {
|
|
222
|
+
if (route.method !== method) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return route.regex.test(path);
|
|
226
|
+
});
|
|
227
|
+
if (!match) {
|
|
228
|
+
return { status: 404, body: { error: "Not Found" } };
|
|
229
|
+
}
|
|
230
|
+
const exec = match.regex.exec(path);
|
|
231
|
+
const params = {};
|
|
232
|
+
if (exec) {
|
|
233
|
+
for (const [idx, name] of match.paramNames.entries()) {
|
|
234
|
+
const value = exec[idx + 1];
|
|
235
|
+
if (typeof value === "string") {
|
|
236
|
+
params[name] = decodeURIComponent(value);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
let status = 200;
|
|
241
|
+
let payload = void 0;
|
|
242
|
+
const res = {
|
|
243
|
+
status(code) {
|
|
244
|
+
status = code;
|
|
245
|
+
return res;
|
|
246
|
+
},
|
|
247
|
+
json(bodyValue) {
|
|
248
|
+
payload = bodyValue;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
try {
|
|
252
|
+
await match.handler(
|
|
253
|
+
{
|
|
254
|
+
params,
|
|
255
|
+
query,
|
|
256
|
+
body,
|
|
257
|
+
signal
|
|
258
|
+
},
|
|
259
|
+
res
|
|
260
|
+
);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
return { status: 500, body: { error: String(err) } };
|
|
263
|
+
}
|
|
264
|
+
return { status, body: payload };
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/browser/client-fetch.ts
|
|
270
|
+
function isAbsoluteHttp(url) {
|
|
271
|
+
return /^https?:\/\//i.test(url.trim());
|
|
272
|
+
}
|
|
273
|
+
function isLoopbackHttpUrl(url) {
|
|
274
|
+
try {
|
|
275
|
+
const host = new URL(url).hostname.trim().toLowerCase();
|
|
276
|
+
const normalizedHost = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
277
|
+
return normalizedHost === "127.0.0.1" || normalizedHost === "localhost" || normalizedHost === "::1";
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function withLoopbackBrowserAuthImpl(url, init, deps) {
|
|
283
|
+
const headers = new Headers(init?.headers ?? {});
|
|
284
|
+
if (headers.has("authorization") || headers.has("x-agenticmail-password")) {
|
|
285
|
+
return { ...init, headers };
|
|
286
|
+
}
|
|
287
|
+
if (!isLoopbackHttpUrl(url)) {
|
|
288
|
+
return { ...init, headers };
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const cfg = deps.loadConfig();
|
|
292
|
+
const auth = deps.resolveBrowserControlAuth(cfg);
|
|
293
|
+
if (auth.token) {
|
|
294
|
+
headers.set("Authorization", `Bearer ${auth.token}`);
|
|
295
|
+
return { ...init, headers };
|
|
296
|
+
}
|
|
297
|
+
if (auth.password) {
|
|
298
|
+
headers.set("x-agenticmail-password", auth.password);
|
|
299
|
+
return { ...init, headers };
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const parsed = new URL(url);
|
|
305
|
+
const port = parsed.port && Number.parseInt(parsed.port, 10) > 0 ? Number.parseInt(parsed.port, 10) : parsed.protocol === "https:" ? 443 : 80;
|
|
306
|
+
const bridgeAuth = deps.getBridgeAuthForPort(port);
|
|
307
|
+
if (bridgeAuth?.token) {
|
|
308
|
+
headers.set("Authorization", `Bearer ${bridgeAuth.token}`);
|
|
309
|
+
} else if (bridgeAuth?.password) {
|
|
310
|
+
headers.set("x-agenticmail-password", bridgeAuth.password);
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
return { ...init, headers };
|
|
315
|
+
}
|
|
316
|
+
function withLoopbackBrowserAuth(url, init) {
|
|
317
|
+
return withLoopbackBrowserAuthImpl(url, init, {
|
|
318
|
+
loadConfig,
|
|
319
|
+
resolveBrowserControlAuth,
|
|
320
|
+
getBridgeAuthForPort
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
var consecutiveBrowserErrors = 0;
|
|
324
|
+
var MAX_TRANSIENT_ERRORS = 2;
|
|
325
|
+
function resetBrowserErrorCount() {
|
|
326
|
+
consecutiveBrowserErrors = 0;
|
|
327
|
+
}
|
|
328
|
+
function enhanceBrowserFetchError(url, err, timeoutMs) {
|
|
329
|
+
consecutiveBrowserErrors += 1;
|
|
330
|
+
const isLocal = !isAbsoluteHttp(url);
|
|
331
|
+
const msg = String(err);
|
|
332
|
+
const msgLower = msg.toLowerCase();
|
|
333
|
+
const looksLikeTimeout = msgLower.includes("timed out") || msgLower.includes("timeout") || msgLower.includes("aborted") || msgLower.includes("abort") || msgLower.includes("aborterror");
|
|
334
|
+
if (consecutiveBrowserErrors <= MAX_TRANSIENT_ERRORS) {
|
|
335
|
+
const retryHint = "This may be a transient browser connection issue. You can retry once \u2014 if it fails again, stop and inform the user.";
|
|
336
|
+
if (looksLikeTimeout) {
|
|
337
|
+
return new Error(
|
|
338
|
+
`Browser timed out after ${timeoutMs}ms. ${retryHint}`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
return new Error(
|
|
342
|
+
`Browser connection error: ${msg}. ${retryHint}`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const operatorHint = isLocal ? `Restart the AgenticMail gateway (AgenticMail.app menubar, or \`${formatCliCommand("agenticmail gateway")}\`).` : "If this is a sandboxed session, ensure the sandbox browser is running.";
|
|
346
|
+
const modelHint = "Do NOT retry the browser tool \u2014 it will keep failing. Use an alternative approach or inform the user that the browser is currently unavailable.";
|
|
347
|
+
if (looksLikeTimeout) {
|
|
348
|
+
return new Error(
|
|
349
|
+
`Can't reach the AgenticMail browser control service (timed out after ${timeoutMs}ms). ${operatorHint} ${modelHint}`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return new Error(
|
|
353
|
+
`Can't reach the AgenticMail browser control service. ${operatorHint} ${modelHint} (${msg})`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
async function fetchHttpJson(url, init) {
|
|
357
|
+
const timeoutMs = init.timeoutMs ?? 5e3;
|
|
358
|
+
const ctrl = new AbortController();
|
|
359
|
+
const upstreamSignal = init.signal;
|
|
360
|
+
let upstreamAbortListener;
|
|
361
|
+
if (upstreamSignal) {
|
|
362
|
+
if (upstreamSignal.aborted) {
|
|
363
|
+
ctrl.abort(upstreamSignal.reason);
|
|
364
|
+
} else {
|
|
365
|
+
upstreamAbortListener = () => ctrl.abort(upstreamSignal.reason);
|
|
366
|
+
upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const t = setTimeout(() => ctrl.abort(new Error("timed out")), timeoutMs);
|
|
370
|
+
try {
|
|
371
|
+
const res = await fetch(url, { ...init, signal: ctrl.signal });
|
|
372
|
+
if (!res.ok) {
|
|
373
|
+
const text = await res.text().catch(() => "");
|
|
374
|
+
throw new Error(text || `HTTP ${res.status}`);
|
|
375
|
+
}
|
|
376
|
+
return await res.json();
|
|
377
|
+
} finally {
|
|
378
|
+
clearTimeout(t);
|
|
379
|
+
if (upstreamSignal && upstreamAbortListener) {
|
|
380
|
+
upstreamSignal.removeEventListener("abort", upstreamAbortListener);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function isOwnBrowserServer(url) {
|
|
385
|
+
if (!isAbsoluteHttp(url)) return false;
|
|
386
|
+
const ownPort = globalThis.__agenticmail_browser_port;
|
|
387
|
+
if (!ownPort) return false;
|
|
388
|
+
try {
|
|
389
|
+
const parsed = new URL(url);
|
|
390
|
+
const host = parsed.hostname.toLowerCase();
|
|
391
|
+
const port = parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
|
|
392
|
+
return (host === "127.0.0.1" || host === "localhost" || host === "::1") && port === ownPort;
|
|
393
|
+
} catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async function fetchBrowserJson(url, init) {
|
|
398
|
+
const timeoutMs = init?.timeoutMs ?? 5e3;
|
|
399
|
+
try {
|
|
400
|
+
if (isAbsoluteHttp(url) && !isOwnBrowserServer(url)) {
|
|
401
|
+
const httpInit = withLoopbackBrowserAuth(url, init);
|
|
402
|
+
const result2 = await fetchHttpJson(url, { ...httpInit, timeoutMs });
|
|
403
|
+
resetBrowserErrorCount();
|
|
404
|
+
return result2;
|
|
405
|
+
}
|
|
406
|
+
const existingCtx = globalThis.__agenticmail_browser_ctx;
|
|
407
|
+
let dispatcher = globalThis.__agenticmail_browser_dispatcher;
|
|
408
|
+
if (!dispatcher) {
|
|
409
|
+
if (existingCtx) {
|
|
410
|
+
dispatcher = createBrowserRouteDispatcher(existingCtx);
|
|
411
|
+
} else {
|
|
412
|
+
const started = await startBrowserControlServiceFromConfig();
|
|
413
|
+
if (!started) {
|
|
414
|
+
throw new Error("browser control disabled");
|
|
415
|
+
}
|
|
416
|
+
dispatcher = createBrowserRouteDispatcher(createBrowserControlContext());
|
|
417
|
+
}
|
|
418
|
+
globalThis.__agenticmail_browser_dispatcher = dispatcher;
|
|
419
|
+
}
|
|
420
|
+
const parsed = new URL(url, "http://localhost");
|
|
421
|
+
const query = {};
|
|
422
|
+
for (const [key, value] of parsed.searchParams.entries()) {
|
|
423
|
+
query[key] = value;
|
|
424
|
+
}
|
|
425
|
+
let body = init?.body;
|
|
426
|
+
if (typeof body === "string") {
|
|
427
|
+
try {
|
|
428
|
+
body = JSON.parse(body);
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const abortCtrl = new AbortController();
|
|
433
|
+
const upstreamSignal = init?.signal;
|
|
434
|
+
let upstreamAbortListener;
|
|
435
|
+
if (upstreamSignal) {
|
|
436
|
+
if (upstreamSignal.aborted) {
|
|
437
|
+
abortCtrl.abort(upstreamSignal.reason);
|
|
438
|
+
} else {
|
|
439
|
+
upstreamAbortListener = () => abortCtrl.abort(upstreamSignal.reason);
|
|
440
|
+
upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
let abortListener;
|
|
444
|
+
const abortPromise = abortCtrl.signal.aborted ? Promise.reject(abortCtrl.signal.reason ?? new Error("aborted")) : new Promise((_, reject) => {
|
|
445
|
+
abortListener = () => reject(abortCtrl.signal.reason ?? new Error("aborted"));
|
|
446
|
+
abortCtrl.signal.addEventListener("abort", abortListener, { once: true });
|
|
447
|
+
});
|
|
448
|
+
let timer;
|
|
449
|
+
if (timeoutMs) {
|
|
450
|
+
timer = setTimeout(() => abortCtrl.abort(new Error("timed out")), timeoutMs);
|
|
451
|
+
}
|
|
452
|
+
const dispatchPromise = dispatcher.dispatch({
|
|
453
|
+
method: init?.method?.toUpperCase() === "DELETE" ? "DELETE" : init?.method?.toUpperCase() === "POST" ? "POST" : "GET",
|
|
454
|
+
path: parsed.pathname,
|
|
455
|
+
query,
|
|
456
|
+
body,
|
|
457
|
+
signal: abortCtrl.signal
|
|
458
|
+
});
|
|
459
|
+
const result = await Promise.race([dispatchPromise, abortPromise]).finally(() => {
|
|
460
|
+
if (timer) {
|
|
461
|
+
clearTimeout(timer);
|
|
462
|
+
}
|
|
463
|
+
if (abortListener) {
|
|
464
|
+
abortCtrl.signal.removeEventListener("abort", abortListener);
|
|
465
|
+
}
|
|
466
|
+
if (upstreamSignal && upstreamAbortListener) {
|
|
467
|
+
upstreamSignal.removeEventListener("abort", upstreamAbortListener);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
if (result.status >= 400) {
|
|
471
|
+
const message = result.body && typeof result.body === "object" && "error" in result.body ? String(result.body.error) : `HTTP ${result.status}`;
|
|
472
|
+
const err = new Error(message);
|
|
473
|
+
if (result.status < 500) {
|
|
474
|
+
err._browserValidation = true;
|
|
475
|
+
resetBrowserErrorCount();
|
|
476
|
+
}
|
|
477
|
+
throw err;
|
|
478
|
+
}
|
|
479
|
+
resetBrowserErrorCount();
|
|
480
|
+
return result.body;
|
|
481
|
+
} catch (err) {
|
|
482
|
+
if (err && typeof err === "object" && "_browserValidation" in err) {
|
|
483
|
+
throw err;
|
|
484
|
+
}
|
|
485
|
+
throw enhanceBrowserFetchError(url, err, timeoutMs);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/browser/client-actions-core.ts
|
|
490
|
+
async function browserNavigate(baseUrl, opts) {
|
|
491
|
+
const q = buildProfileQuery(opts.profile);
|
|
492
|
+
return await fetchBrowserJson(withBaseUrl(baseUrl, `/navigate${q}`), {
|
|
493
|
+
method: "POST",
|
|
494
|
+
headers: { "Content-Type": "application/json" },
|
|
495
|
+
body: JSON.stringify({ url: opts.url, targetId: opts.targetId }),
|
|
496
|
+
timeoutMs: 5e4
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async function browserArmDialog(baseUrl, opts) {
|
|
500
|
+
const q = buildProfileQuery(opts.profile);
|
|
501
|
+
return await fetchBrowserJson(withBaseUrl(baseUrl, `/hooks/dialog${q}`), {
|
|
502
|
+
method: "POST",
|
|
503
|
+
headers: { "Content-Type": "application/json" },
|
|
504
|
+
body: JSON.stringify({
|
|
505
|
+
accept: opts.accept,
|
|
506
|
+
promptText: opts.promptText,
|
|
507
|
+
targetId: opts.targetId,
|
|
508
|
+
timeoutMs: opts.timeoutMs
|
|
509
|
+
}),
|
|
510
|
+
timeoutMs: 5e4
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
async function browserArmFileChooser(baseUrl, opts) {
|
|
514
|
+
const q = buildProfileQuery(opts.profile);
|
|
515
|
+
return await fetchBrowserJson(withBaseUrl(baseUrl, `/hooks/file-chooser${q}`), {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: { "Content-Type": "application/json" },
|
|
518
|
+
body: JSON.stringify({
|
|
519
|
+
paths: opts.paths,
|
|
520
|
+
ref: opts.ref,
|
|
521
|
+
inputRef: opts.inputRef,
|
|
522
|
+
element: opts.element,
|
|
523
|
+
targetId: opts.targetId,
|
|
524
|
+
timeoutMs: opts.timeoutMs
|
|
525
|
+
}),
|
|
526
|
+
timeoutMs: 5e4
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
async function browserAct(baseUrl, req, opts) {
|
|
530
|
+
const q = buildProfileQuery(opts?.profile);
|
|
531
|
+
return await fetchBrowserJson(withBaseUrl(baseUrl, `/act${q}`), {
|
|
532
|
+
method: "POST",
|
|
533
|
+
headers: { "Content-Type": "application/json" },
|
|
534
|
+
body: JSON.stringify(req),
|
|
535
|
+
timeoutMs: 5e4
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
async function browserScreenshotAction(baseUrl, opts) {
|
|
539
|
+
const q = buildProfileQuery(opts.profile);
|
|
540
|
+
return await fetchBrowserJson(withBaseUrl(baseUrl, `/screenshot${q}`), {
|
|
541
|
+
method: "POST",
|
|
542
|
+
headers: { "Content-Type": "application/json" },
|
|
543
|
+
body: JSON.stringify({
|
|
544
|
+
targetId: opts.targetId,
|
|
545
|
+
fullPage: opts.fullPage,
|
|
546
|
+
ref: opts.ref,
|
|
547
|
+
element: opts.element,
|
|
548
|
+
type: opts.type
|
|
549
|
+
}),
|
|
550
|
+
timeoutMs: 5e4
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/browser/client-actions-observe.ts
|
|
555
|
+
function buildQuerySuffix(params) {
|
|
556
|
+
const query = new URLSearchParams();
|
|
557
|
+
for (const [key, value] of params) {
|
|
558
|
+
if (typeof value === "boolean") {
|
|
559
|
+
query.set(key, String(value));
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (typeof value === "string" && value.length > 0) {
|
|
563
|
+
query.set(key, value);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const encoded = query.toString();
|
|
567
|
+
return encoded.length > 0 ? `?${encoded}` : "";
|
|
568
|
+
}
|
|
569
|
+
async function browserConsoleMessages(baseUrl, opts = {}) {
|
|
570
|
+
const suffix = buildQuerySuffix([
|
|
571
|
+
["level", opts.level],
|
|
572
|
+
["targetId", opts.targetId],
|
|
573
|
+
["profile", opts.profile]
|
|
574
|
+
]);
|
|
575
|
+
return await fetchBrowserJson(withBaseUrl(baseUrl, `/console${suffix}`), { timeoutMs: 2e4 });
|
|
576
|
+
}
|
|
577
|
+
async function browserPdfSave(baseUrl, opts = {}) {
|
|
578
|
+
const q = buildProfileQuery(opts.profile);
|
|
579
|
+
return await fetchBrowserJson(withBaseUrl(baseUrl, `/pdf${q}`), {
|
|
580
|
+
method: "POST",
|
|
581
|
+
headers: { "Content-Type": "application/json" },
|
|
582
|
+
body: JSON.stringify({ targetId: opts.targetId }),
|
|
583
|
+
timeoutMs: 2e4
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/browser/client.ts
|
|
588
|
+
function buildProfileQuery2(profile) {
|
|
589
|
+
return profile ? `?profile=${encodeURIComponent(profile)}` : "";
|
|
590
|
+
}
|
|
591
|
+
function withBaseUrl2(baseUrl, path) {
|
|
592
|
+
const trimmed = baseUrl?.trim();
|
|
593
|
+
if (!trimmed) {
|
|
594
|
+
return path;
|
|
595
|
+
}
|
|
596
|
+
return `${trimmed.replace(/\/$/, "")}${path}`;
|
|
597
|
+
}
|
|
598
|
+
async function browserStatus(baseUrl, opts) {
|
|
599
|
+
const q = buildProfileQuery2(opts?.profile);
|
|
600
|
+
return await fetchBrowserJson(withBaseUrl2(baseUrl, `/${q}`), {
|
|
601
|
+
timeoutMs: 1500
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
async function browserProfiles(baseUrl) {
|
|
605
|
+
const res = await fetchBrowserJson(
|
|
606
|
+
withBaseUrl2(baseUrl, `/profiles`),
|
|
607
|
+
{
|
|
608
|
+
timeoutMs: 3e3
|
|
609
|
+
}
|
|
610
|
+
);
|
|
611
|
+
return res.profiles ?? [];
|
|
612
|
+
}
|
|
613
|
+
async function browserStart(baseUrl, opts) {
|
|
614
|
+
const q = buildProfileQuery2(opts?.profile);
|
|
615
|
+
await fetchBrowserJson(withBaseUrl2(baseUrl, `/start${q}`), {
|
|
616
|
+
method: "POST",
|
|
617
|
+
timeoutMs: 15e3
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
async function browserStop(baseUrl, opts) {
|
|
621
|
+
const q = buildProfileQuery2(opts?.profile);
|
|
622
|
+
await fetchBrowserJson(withBaseUrl2(baseUrl, `/stop${q}`), {
|
|
623
|
+
method: "POST",
|
|
624
|
+
timeoutMs: 15e3
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
async function browserTabs(baseUrl, opts) {
|
|
628
|
+
const q = buildProfileQuery2(opts?.profile);
|
|
629
|
+
const res = await fetchBrowserJson(
|
|
630
|
+
withBaseUrl2(baseUrl, `/tabs${q}`),
|
|
631
|
+
{ timeoutMs: 3e3 }
|
|
632
|
+
);
|
|
633
|
+
return res.tabs ?? [];
|
|
634
|
+
}
|
|
635
|
+
async function browserOpenTab(baseUrl, url, opts) {
|
|
636
|
+
const q = buildProfileQuery2(opts?.profile);
|
|
637
|
+
return await fetchBrowserJson(withBaseUrl2(baseUrl, `/tabs/open${q}`), {
|
|
638
|
+
method: "POST",
|
|
639
|
+
headers: { "Content-Type": "application/json" },
|
|
640
|
+
body: JSON.stringify({ url }),
|
|
641
|
+
timeoutMs: 15e3
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
async function browserFocusTab(baseUrl, targetId, opts) {
|
|
645
|
+
const q = buildProfileQuery2(opts?.profile);
|
|
646
|
+
await fetchBrowserJson(withBaseUrl2(baseUrl, `/tabs/focus${q}`), {
|
|
647
|
+
method: "POST",
|
|
648
|
+
headers: { "Content-Type": "application/json" },
|
|
649
|
+
body: JSON.stringify({ targetId }),
|
|
650
|
+
timeoutMs: 5e3
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
async function browserCloseTab(baseUrl, targetId, opts) {
|
|
654
|
+
const q = buildProfileQuery2(opts?.profile);
|
|
655
|
+
await fetchBrowserJson(withBaseUrl2(baseUrl, `/tabs/${encodeURIComponent(targetId)}${q}`), {
|
|
656
|
+
method: "DELETE",
|
|
657
|
+
timeoutMs: 5e3
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
async function browserSnapshot(baseUrl, opts) {
|
|
661
|
+
const q = new URLSearchParams();
|
|
662
|
+
q.set("format", opts.format);
|
|
663
|
+
if (opts.targetId) {
|
|
664
|
+
q.set("targetId", opts.targetId);
|
|
665
|
+
}
|
|
666
|
+
if (typeof opts.limit === "number") {
|
|
667
|
+
q.set("limit", String(opts.limit));
|
|
668
|
+
}
|
|
669
|
+
if (typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars)) {
|
|
670
|
+
q.set("maxChars", String(opts.maxChars));
|
|
671
|
+
}
|
|
672
|
+
if (opts.refs === "aria" || opts.refs === "role") {
|
|
673
|
+
q.set("refs", opts.refs);
|
|
674
|
+
}
|
|
675
|
+
if (typeof opts.interactive === "boolean") {
|
|
676
|
+
q.set("interactive", String(opts.interactive));
|
|
677
|
+
}
|
|
678
|
+
if (typeof opts.compact === "boolean") {
|
|
679
|
+
q.set("compact", String(opts.compact));
|
|
680
|
+
}
|
|
681
|
+
if (typeof opts.depth === "number" && Number.isFinite(opts.depth)) {
|
|
682
|
+
q.set("depth", String(opts.depth));
|
|
683
|
+
}
|
|
684
|
+
if (opts.selector?.trim()) {
|
|
685
|
+
q.set("selector", opts.selector.trim());
|
|
686
|
+
}
|
|
687
|
+
if (opts.frame?.trim()) {
|
|
688
|
+
q.set("frame", opts.frame.trim());
|
|
689
|
+
}
|
|
690
|
+
if (opts.labels === true) {
|
|
691
|
+
q.set("labels", "1");
|
|
692
|
+
}
|
|
693
|
+
if (opts.mode) {
|
|
694
|
+
q.set("mode", opts.mode);
|
|
695
|
+
}
|
|
696
|
+
if (opts.profile) {
|
|
697
|
+
q.set("profile", opts.profile);
|
|
698
|
+
}
|
|
699
|
+
return await fetchBrowserJson(withBaseUrl2(baseUrl, `/snapshot?${q.toString()}`), {
|
|
700
|
+
timeoutMs: 5e4
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/agent-tools/tools/browser-tool.schema.ts
|
|
705
|
+
import { Type as Type2 } from "@sinclair/typebox";
|
|
706
|
+
|
|
707
|
+
// src/agent-tools/schema/typebox.ts
|
|
708
|
+
import { Type } from "@sinclair/typebox";
|
|
709
|
+
function stringEnum(values, options = {}) {
|
|
710
|
+
return Type.Unsafe({
|
|
711
|
+
type: "string",
|
|
712
|
+
enum: [...values],
|
|
713
|
+
...options
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
function optionalStringEnum(values, options = {}) {
|
|
717
|
+
return Type.Optional(stringEnum(values, options));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/agent-tools/tools/browser-tool.schema.ts
|
|
721
|
+
var BROWSER_ACT_KINDS = [
|
|
722
|
+
"click",
|
|
723
|
+
"type",
|
|
724
|
+
"press",
|
|
725
|
+
"hover",
|
|
726
|
+
"drag",
|
|
727
|
+
"select",
|
|
728
|
+
"fill",
|
|
729
|
+
"resize",
|
|
730
|
+
"wait",
|
|
731
|
+
"evaluate",
|
|
732
|
+
"close",
|
|
733
|
+
"mouse_click",
|
|
734
|
+
"scroll"
|
|
735
|
+
];
|
|
736
|
+
var BROWSER_TOOL_ACTIONS = [
|
|
737
|
+
"status",
|
|
738
|
+
"start",
|
|
739
|
+
"stop",
|
|
740
|
+
"profiles",
|
|
741
|
+
"tabs",
|
|
742
|
+
"open",
|
|
743
|
+
"focus",
|
|
744
|
+
"close",
|
|
745
|
+
"snapshot",
|
|
746
|
+
"screenshot",
|
|
747
|
+
"navigate",
|
|
748
|
+
"console",
|
|
749
|
+
"pdf",
|
|
750
|
+
"upload",
|
|
751
|
+
"dialog",
|
|
752
|
+
"act"
|
|
753
|
+
];
|
|
754
|
+
var BROWSER_TARGETS = ["sandbox", "host", "node"];
|
|
755
|
+
var BROWSER_SNAPSHOT_FORMATS = ["aria", "ai"];
|
|
756
|
+
var BROWSER_SNAPSHOT_MODES = ["efficient"];
|
|
757
|
+
var BROWSER_SNAPSHOT_REFS = ["role", "aria"];
|
|
758
|
+
var BROWSER_IMAGE_TYPES = ["png", "jpeg"];
|
|
759
|
+
var BrowserActSchema = Type2.Object({
|
|
760
|
+
kind: stringEnum(BROWSER_ACT_KINDS),
|
|
761
|
+
// Common fields
|
|
762
|
+
targetId: Type2.Optional(Type2.String()),
|
|
763
|
+
ref: Type2.Optional(Type2.String()),
|
|
764
|
+
// click
|
|
765
|
+
doubleClick: Type2.Optional(Type2.Boolean()),
|
|
766
|
+
button: Type2.Optional(Type2.String()),
|
|
767
|
+
modifiers: Type2.Optional(Type2.Array(Type2.String())),
|
|
768
|
+
// type
|
|
769
|
+
text: Type2.Optional(Type2.String()),
|
|
770
|
+
submit: Type2.Optional(Type2.Boolean()),
|
|
771
|
+
slowly: Type2.Optional(Type2.Boolean()),
|
|
772
|
+
// press
|
|
773
|
+
key: Type2.Optional(Type2.String()),
|
|
774
|
+
// drag
|
|
775
|
+
startRef: Type2.Optional(Type2.String()),
|
|
776
|
+
endRef: Type2.Optional(Type2.String()),
|
|
777
|
+
// select
|
|
778
|
+
values: Type2.Optional(Type2.Array(Type2.String())),
|
|
779
|
+
// fill - use permissive array of objects
|
|
780
|
+
fields: Type2.Optional(Type2.Array(Type2.Object({}, { additionalProperties: true }))),
|
|
781
|
+
// resize
|
|
782
|
+
width: Type2.Optional(Type2.Number()),
|
|
783
|
+
height: Type2.Optional(Type2.Number()),
|
|
784
|
+
// wait
|
|
785
|
+
timeMs: Type2.Optional(Type2.Number()),
|
|
786
|
+
textGone: Type2.Optional(Type2.String()),
|
|
787
|
+
// evaluate
|
|
788
|
+
fn: Type2.Optional(Type2.String()),
|
|
789
|
+
// mouse_click (coordinate-based clicking — fallback for Shadow DOM)
|
|
790
|
+
x: Type2.Optional(Type2.Number()),
|
|
791
|
+
y: Type2.Optional(Type2.Number()),
|
|
792
|
+
// scroll
|
|
793
|
+
deltaX: Type2.Optional(Type2.Number()),
|
|
794
|
+
deltaY: Type2.Optional(Type2.Number())
|
|
795
|
+
});
|
|
796
|
+
var BrowserToolSchema = Type2.Object({
|
|
797
|
+
action: stringEnum(BROWSER_TOOL_ACTIONS),
|
|
798
|
+
target: optionalStringEnum(BROWSER_TARGETS),
|
|
799
|
+
node: Type2.Optional(Type2.String()),
|
|
800
|
+
profile: Type2.Optional(Type2.String()),
|
|
801
|
+
targetUrl: Type2.Optional(Type2.String()),
|
|
802
|
+
targetId: Type2.Optional(Type2.String()),
|
|
803
|
+
limit: Type2.Optional(Type2.Number()),
|
|
804
|
+
maxChars: Type2.Optional(Type2.Number()),
|
|
805
|
+
mode: optionalStringEnum(BROWSER_SNAPSHOT_MODES),
|
|
806
|
+
snapshotFormat: optionalStringEnum(BROWSER_SNAPSHOT_FORMATS),
|
|
807
|
+
refs: optionalStringEnum(BROWSER_SNAPSHOT_REFS),
|
|
808
|
+
interactive: Type2.Optional(Type2.Boolean()),
|
|
809
|
+
compact: Type2.Optional(Type2.Boolean()),
|
|
810
|
+
depth: Type2.Optional(Type2.Number()),
|
|
811
|
+
selector: Type2.Optional(Type2.String()),
|
|
812
|
+
frame: Type2.Optional(Type2.String()),
|
|
813
|
+
labels: Type2.Optional(Type2.Boolean()),
|
|
814
|
+
fullPage: Type2.Optional(Type2.Boolean()),
|
|
815
|
+
ref: Type2.Optional(Type2.String()),
|
|
816
|
+
element: Type2.Optional(Type2.String()),
|
|
817
|
+
type: optionalStringEnum(BROWSER_IMAGE_TYPES),
|
|
818
|
+
level: Type2.Optional(Type2.String()),
|
|
819
|
+
paths: Type2.Optional(Type2.Array(Type2.String())),
|
|
820
|
+
inputRef: Type2.Optional(Type2.String()),
|
|
821
|
+
timeoutMs: Type2.Optional(Type2.Number()),
|
|
822
|
+
accept: Type2.Optional(Type2.Boolean()),
|
|
823
|
+
promptText: Type2.Optional(Type2.String()),
|
|
824
|
+
request: Type2.Optional(BrowserActSchema)
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
// src/agent-tools/tools/browser-snapshot-cleaner.ts
|
|
828
|
+
var NOISE_ROLES = /* @__PURE__ */ new Set([
|
|
829
|
+
"generic",
|
|
830
|
+
"none",
|
|
831
|
+
"unknown",
|
|
832
|
+
"presentation",
|
|
833
|
+
"separator",
|
|
834
|
+
"group"
|
|
835
|
+
// often just a wrapper div
|
|
836
|
+
]);
|
|
837
|
+
var CONDITIONAL_NOISE_ROLES = /* @__PURE__ */ new Set([
|
|
838
|
+
"region",
|
|
839
|
+
"section",
|
|
840
|
+
"banner",
|
|
841
|
+
"contentinfo",
|
|
842
|
+
"complementary",
|
|
843
|
+
"navigation"
|
|
844
|
+
]);
|
|
845
|
+
var MEANINGFUL_ROLES = /* @__PURE__ */ new Set([
|
|
846
|
+
"link",
|
|
847
|
+
"button",
|
|
848
|
+
"heading",
|
|
849
|
+
"textbox",
|
|
850
|
+
"text",
|
|
851
|
+
"statictext",
|
|
852
|
+
"paragraph",
|
|
853
|
+
"listitem",
|
|
854
|
+
"img",
|
|
855
|
+
"image",
|
|
856
|
+
"checkbox",
|
|
857
|
+
"radio",
|
|
858
|
+
"combobox",
|
|
859
|
+
"option",
|
|
860
|
+
"tab",
|
|
861
|
+
"tabpanel",
|
|
862
|
+
"menuitem",
|
|
863
|
+
"alert",
|
|
864
|
+
"dialog",
|
|
865
|
+
"article",
|
|
866
|
+
"cell",
|
|
867
|
+
"row",
|
|
868
|
+
"columnheader",
|
|
869
|
+
"rowheader",
|
|
870
|
+
"time",
|
|
871
|
+
"status",
|
|
872
|
+
"progressbar",
|
|
873
|
+
"slider",
|
|
874
|
+
"switch",
|
|
875
|
+
"searchbox"
|
|
876
|
+
]);
|
|
877
|
+
function isNoiseNode(node) {
|
|
878
|
+
const role = node.role.toLowerCase();
|
|
879
|
+
const hasName = Boolean(node.name?.trim());
|
|
880
|
+
const hasValue = Boolean(node.value?.trim());
|
|
881
|
+
if (MEANINGFUL_ROLES.has(role)) return false;
|
|
882
|
+
if (NOISE_ROLES.has(role) && !hasName && !hasValue) return true;
|
|
883
|
+
if (CONDITIONAL_NOISE_ROLES.has(role) && !hasName) return true;
|
|
884
|
+
if ((role === "img" || role === "image") && (node.name === "" || node.name === "decorative")) {
|
|
885
|
+
return true;
|
|
886
|
+
}
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
var twitterExtractor = {
|
|
890
|
+
name: "twitter",
|
|
891
|
+
match: (url) => /^https?:\/\/(x\.com|twitter\.com)/i.test(url),
|
|
892
|
+
transform: (nodes) => {
|
|
893
|
+
const navNoise = /* @__PURE__ */ new Set([
|
|
894
|
+
"Home",
|
|
895
|
+
"Explore",
|
|
896
|
+
"Notifications",
|
|
897
|
+
"Messages",
|
|
898
|
+
"Grok",
|
|
899
|
+
"Communities",
|
|
900
|
+
"Premium",
|
|
901
|
+
"Verified Orgs",
|
|
902
|
+
"Profile",
|
|
903
|
+
"More",
|
|
904
|
+
"Post",
|
|
905
|
+
"Lists",
|
|
906
|
+
"Bookmarks",
|
|
907
|
+
"Monetization",
|
|
908
|
+
"Ads",
|
|
909
|
+
"Jobs",
|
|
910
|
+
"Spaces",
|
|
911
|
+
"Settings",
|
|
912
|
+
"Sign out"
|
|
913
|
+
]);
|
|
914
|
+
return nodes.filter((n) => {
|
|
915
|
+
const name = n.name?.trim() || "";
|
|
916
|
+
if (n.role === "link" && navNoise.has(name)) return false;
|
|
917
|
+
if (n.role === "heading" && name.includes("keyboard shortcuts")) return false;
|
|
918
|
+
return true;
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
var facebookExtractor = {
|
|
923
|
+
name: "facebook",
|
|
924
|
+
match: (url) => /^https?:\/\/(www\.)?(facebook\.com|fb\.com|business\.facebook\.com)/i.test(url),
|
|
925
|
+
transform: (nodes) => {
|
|
926
|
+
const fbNavNoise = /* @__PURE__ */ new Set([
|
|
927
|
+
"Facebook",
|
|
928
|
+
"Search Facebook",
|
|
929
|
+
"Home",
|
|
930
|
+
"Watch",
|
|
931
|
+
"Marketplace",
|
|
932
|
+
"Groups",
|
|
933
|
+
"Gaming",
|
|
934
|
+
"Menu"
|
|
935
|
+
]);
|
|
936
|
+
return nodes.filter((n) => {
|
|
937
|
+
const name = n.name?.trim() || "";
|
|
938
|
+
if (n.role === "link" && fbNavNoise.has(name) && n.depth < 3) return false;
|
|
939
|
+
return true;
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
var linkedinExtractor = {
|
|
944
|
+
name: "linkedin",
|
|
945
|
+
match: (url) => /^https?:\/\/(www\.)?linkedin\.com/i.test(url),
|
|
946
|
+
transform: (nodes) => {
|
|
947
|
+
const liNavNoise = /* @__PURE__ */ new Set([
|
|
948
|
+
"LinkedIn",
|
|
949
|
+
"Home",
|
|
950
|
+
"My Network",
|
|
951
|
+
"Jobs",
|
|
952
|
+
"Messaging",
|
|
953
|
+
"Notifications",
|
|
954
|
+
"Me",
|
|
955
|
+
"For Business",
|
|
956
|
+
"Try Premium for \u20B90"
|
|
957
|
+
]);
|
|
958
|
+
return nodes.filter((n) => {
|
|
959
|
+
const name = n.name?.trim() || "";
|
|
960
|
+
if (n.role === "link" && liNavNoise.has(name) && n.depth < 3) return false;
|
|
961
|
+
return true;
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
var redditExtractor = {
|
|
966
|
+
name: "reddit",
|
|
967
|
+
match: (url) => /^https?:\/\/(old\.|www\.)?reddit\.com/i.test(url),
|
|
968
|
+
transform: (nodes) => nodes
|
|
969
|
+
// old.reddit is already clean
|
|
970
|
+
};
|
|
971
|
+
var googleExtractor = {
|
|
972
|
+
name: "google",
|
|
973
|
+
match: (url) => /^https?:\/\/(www\.)?google\.\w+\/search/i.test(url),
|
|
974
|
+
transform: (nodes) => {
|
|
975
|
+
return nodes.filter((n) => {
|
|
976
|
+
const name = n.name?.trim() || "";
|
|
977
|
+
if (n.role === "link" && ["Gmail", "Images", "Sign in", "About", "Store"].includes(name)) {
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
return true;
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
var SITE_EXTRACTORS = [
|
|
985
|
+
twitterExtractor,
|
|
986
|
+
facebookExtractor,
|
|
987
|
+
linkedinExtractor,
|
|
988
|
+
redditExtractor,
|
|
989
|
+
googleExtractor
|
|
990
|
+
];
|
|
991
|
+
function renderTreeText(nodes, opts) {
|
|
992
|
+
const includeRefs = opts.includeRefs !== false;
|
|
993
|
+
const lines = [];
|
|
994
|
+
for (const n of nodes) {
|
|
995
|
+
if (opts.minDepth !== void 0 && n.depth < opts.minDepth) continue;
|
|
996
|
+
if (opts.maxDepth !== void 0 && n.depth > opts.maxDepth) continue;
|
|
997
|
+
const indent = " ".repeat(n.depth);
|
|
998
|
+
const role = n.role.toLowerCase();
|
|
999
|
+
let line;
|
|
1000
|
+
if (role === "text" || role === "statictext") {
|
|
1001
|
+
const text = n.name || n.value || "";
|
|
1002
|
+
if (!text.trim()) continue;
|
|
1003
|
+
line = `${indent}"${text}"`;
|
|
1004
|
+
} else if (role === "heading") {
|
|
1005
|
+
const level = n.description?.match(/level (\d)/)?.[1] || "";
|
|
1006
|
+
const levelStr = level ? `[h${level}]` : "";
|
|
1007
|
+
line = `${indent}${levelStr} ${n.name || ""}`.trimEnd();
|
|
1008
|
+
} else if (role === "link") {
|
|
1009
|
+
line = `${indent}[link] ${n.name || "(unnamed)"}`;
|
|
1010
|
+
} else if (role === "button") {
|
|
1011
|
+
line = `${indent}[button] ${n.name || "(unnamed)"}`;
|
|
1012
|
+
} else if (role === "textbox" || role === "searchbox" || role === "combobox") {
|
|
1013
|
+
const val = n.value ? ` = "${n.value}"` : "";
|
|
1014
|
+
line = `${indent}[input] ${n.name || ""}${val}`;
|
|
1015
|
+
} else if (role === "img" || role === "image") {
|
|
1016
|
+
line = `${indent}[img] ${n.name || "(no alt)"}`;
|
|
1017
|
+
} else if (role === "checkbox" || role === "radio" || role === "switch") {
|
|
1018
|
+
const checked = n.value === "true" ? "\u2713" : "\u25CB";
|
|
1019
|
+
line = `${indent}[${checked}] ${n.name || ""}`;
|
|
1020
|
+
} else if (role === "article") {
|
|
1021
|
+
line = `${indent}--- article ---`;
|
|
1022
|
+
} else if (role === "time") {
|
|
1023
|
+
line = `${indent}${n.name || n.value || ""}`;
|
|
1024
|
+
} else {
|
|
1025
|
+
const nameStr = n.name ? ` ${n.name}` : "";
|
|
1026
|
+
const valStr = n.value ? ` = "${n.value}"` : "";
|
|
1027
|
+
line = `${indent}${role}${nameStr}${valStr}`;
|
|
1028
|
+
}
|
|
1029
|
+
if (includeRefs) {
|
|
1030
|
+
line += ` [${n.ref}]`;
|
|
1031
|
+
}
|
|
1032
|
+
lines.push(line);
|
|
1033
|
+
}
|
|
1034
|
+
return lines.join("\n");
|
|
1035
|
+
}
|
|
1036
|
+
function smartTruncate(text, maxChars) {
|
|
1037
|
+
if (text.length <= maxChars) {
|
|
1038
|
+
return { text, truncated: false };
|
|
1039
|
+
}
|
|
1040
|
+
const slice = text.slice(0, maxChars);
|
|
1041
|
+
const lastNewline = slice.lastIndexOf("\n");
|
|
1042
|
+
const lastArticle = slice.lastIndexOf("--- article ---");
|
|
1043
|
+
const cutPoint = lastArticle > maxChars * 0.5 ? lastArticle : lastNewline > maxChars * 0.7 ? lastNewline : maxChars;
|
|
1044
|
+
return {
|
|
1045
|
+
text: text.slice(0, cutPoint) + "\n... [truncated]",
|
|
1046
|
+
truncated: true
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
function cleanAriaSnapshot(nodes, opts = {}) {
|
|
1050
|
+
const maxChars = opts.maxChars ?? 1e4;
|
|
1051
|
+
const url = opts.url ?? "";
|
|
1052
|
+
const extractor = SITE_EXTRACTORS.find((e) => e.match(url));
|
|
1053
|
+
let processed = [...nodes];
|
|
1054
|
+
if (extractor) {
|
|
1055
|
+
processed = extractor.transform(processed);
|
|
1056
|
+
}
|
|
1057
|
+
const beforeCount = processed.length;
|
|
1058
|
+
processed = processed.filter((n) => !isNoiseNode(n));
|
|
1059
|
+
const noiseRemoved = beforeCount - processed.length;
|
|
1060
|
+
if (processed.length > 0) {
|
|
1061
|
+
let prevDepth = 0;
|
|
1062
|
+
for (let i = 0; i < processed.length; i++) {
|
|
1063
|
+
const node = processed[i];
|
|
1064
|
+
if (node.depth > prevDepth + 1) {
|
|
1065
|
+
node.depth = prevDepth + 1;
|
|
1066
|
+
}
|
|
1067
|
+
prevDepth = node.depth;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const rendered = extractor?.render ? extractor.render(processed, opts) : renderTreeText(processed, opts);
|
|
1071
|
+
const { text, truncated } = smartTruncate(rendered, maxChars);
|
|
1072
|
+
return {
|
|
1073
|
+
text,
|
|
1074
|
+
nodeCount: processed.length,
|
|
1075
|
+
noiseRemoved,
|
|
1076
|
+
siteExtractor: extractor?.name,
|
|
1077
|
+
truncated
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function cleanAiSnapshot(text, opts = {}) {
|
|
1081
|
+
const maxChars = opts.maxChars ?? 1e4;
|
|
1082
|
+
const { text: cleaned, truncated } = smartTruncate(text, maxChars);
|
|
1083
|
+
return {
|
|
1084
|
+
text: cleaned,
|
|
1085
|
+
nodeCount: 0,
|
|
1086
|
+
noiseRemoved: 0,
|
|
1087
|
+
truncated
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
function cleanSnapshot(data, opts = {}) {
|
|
1091
|
+
if (typeof data === "string") {
|
|
1092
|
+
return cleanAiSnapshot(data, opts);
|
|
1093
|
+
}
|
|
1094
|
+
if (Array.isArray(data)) {
|
|
1095
|
+
return cleanAriaSnapshot(data, opts);
|
|
1096
|
+
}
|
|
1097
|
+
if (data && typeof data === "object") {
|
|
1098
|
+
if (typeof data.snapshot === "string") {
|
|
1099
|
+
return cleanAiSnapshot(data.snapshot, opts);
|
|
1100
|
+
}
|
|
1101
|
+
if (Array.isArray(data.nodes)) {
|
|
1102
|
+
return cleanAriaSnapshot(data.nodes, opts);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return cleanAiSnapshot(JSON.stringify(data, null, 2), opts);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/agent-tools/tools/browser-tool.ts
|
|
1109
|
+
function wrapBrowserExternalJson(params) {
|
|
1110
|
+
const extractedText = JSON.stringify(params.payload, null, 2);
|
|
1111
|
+
const wrappedText = wrapExternalContent(extractedText, {
|
|
1112
|
+
source: "browser",
|
|
1113
|
+
includeWarning: params.includeWarning ?? true
|
|
1114
|
+
});
|
|
1115
|
+
return {
|
|
1116
|
+
wrappedText,
|
|
1117
|
+
safeDetails: {
|
|
1118
|
+
ok: true,
|
|
1119
|
+
externalContent: {
|
|
1120
|
+
untrusted: true,
|
|
1121
|
+
source: "browser",
|
|
1122
|
+
kind: params.kind,
|
|
1123
|
+
wrapped: true
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
function createEnterpriseBrowserTool(config) {
|
|
1129
|
+
const baseUrl = config?.baseUrl;
|
|
1130
|
+
const defaultProfile = config?.defaultProfile;
|
|
1131
|
+
return {
|
|
1132
|
+
label: "Browser",
|
|
1133
|
+
name: "browser",
|
|
1134
|
+
description: [
|
|
1135
|
+
"Control the browser for web automation \u2014 navigate, screenshot, snapshot (accessibility tree), click, type, hover, drag, fill forms, manage tabs, capture console logs, save PDFs, upload files, and handle dialogs.",
|
|
1136
|
+
"Actions: status, start, stop, profiles, tabs, open, focus, close, snapshot, screenshot, navigate, console, pdf, upload, dialog, act.",
|
|
1137
|
+
"Use snapshot+act for UI automation. snapshot returns the page accessibility tree; use refs from it with act to interact.",
|
|
1138
|
+
'snapshot format="ai" returns a text description; format="aria" returns structured nodes.',
|
|
1139
|
+
"act supports: click, type, press, hover, drag, select, fill, resize, wait, evaluate, close, dismiss, mouse_click, scroll.",
|
|
1140
|
+
'dismiss: SMART MODAL DISMISSAL \u2014 closes popups, dialogs, modals, overlays. Tries in order: Escape key \u2192 click outside modal \u2192 find Close/X/Cancel button \u2192 double Escape. Use this when stuck in a modal or when an unwanted popup appears (cookie consent, login prompt, "download app" popup). No ref needed.',
|
|
1141
|
+
"mouse_click: coordinate-based clicking (x, y) \u2014 use when ref-based click fails on Shadow DOM/custom components. Take a screenshot first to identify coordinates.",
|
|
1142
|
+
"scroll: scroll the page (deltaY positive=down, negative=up). Use to navigate long pages before taking snapshots.",
|
|
1143
|
+
"IMPORTANT: Use open(targetUrl) to create NEW tabs for each different site/URL. Do NOT reuse the same tab for different sites \u2014 open a new tab, get its targetId, then use that targetId for all actions on that site.",
|
|
1144
|
+
"Reddit URLs are auto-rewritten to old.reddit.com (avoids Shadow DOM issues).",
|
|
1145
|
+
'TWITTER/X RULES: (1) Non-Premium accounts have a 280 character limit per post/reply. ALWAYS keep tweets under 280 chars. Count carefully before posting. If your message is too long, shorten it \u2014 the Post/Reply button will be DISABLED if over the limit. (2) ALWAYS verify your post went through: after clicking Reply/Post, check for a "Your post was sent" confirmation alert or see your reply appear in the thread. If the button was disabled or no confirmation appeared, the post FAILED \u2014 shorten and retry. Never assume a post succeeded without verification. (3) For replies, navigate directly to the post URL (e.g. x.com/user/status/ID) instead of searching \u2014 this avoids loading the massive search results DOM.',
|
|
1146
|
+
"TOKEN EFFICIENCY: Snapshots can be large. Use compact=true to reduce size. Use maxChars (e.g. 5000) to limit output. Use selector to snapshot only a specific part of the page (e.g. the reply box or a single tweet). Avoid taking full-page snapshots repeatedly \u2014 each one adds thousands of tokens to your context.",
|
|
1147
|
+
'FALLBACK STRATEGY: If snapshot refs fail \u2192 try evaluate with document.querySelector(). If clicks fail \u2192 take screenshot, identify coordinates, use mouse_click(x, y). If page is too long \u2192 use scroll to navigate, then snapshot again. If STUCK IN A MODAL \u2192 use act kind="dismiss" to auto-close it (tries Escape, backdrop click, Close/X button). Never keep retrying actions that fail inside a modal \u2014 dismiss it first.'
|
|
1148
|
+
].join(" "),
|
|
1149
|
+
parameters: BrowserToolSchema,
|
|
1150
|
+
execute: async (_toolCallId, args) => {
|
|
1151
|
+
const executeWithRetry = async () => {
|
|
1152
|
+
try {
|
|
1153
|
+
return await executeInner(args);
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
const msg = String(err?.message || err || "");
|
|
1156
|
+
if (msg.includes("Can't reach") && !msg.includes("timed out")) {
|
|
1157
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
1158
|
+
return await executeInner(args);
|
|
1159
|
+
}
|
|
1160
|
+
throw err;
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
return await executeWithRetry();
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
async function executeInner(args) {
|
|
1167
|
+
const params = args;
|
|
1168
|
+
const action = readStringParam(params, "action", { required: true });
|
|
1169
|
+
const profile = readStringParam(params, "profile") || defaultProfile;
|
|
1170
|
+
switch (action) {
|
|
1171
|
+
case "status":
|
|
1172
|
+
return jsonResult(await browserStatus(baseUrl, { profile }));
|
|
1173
|
+
case "start":
|
|
1174
|
+
await browserStart(baseUrl, { profile });
|
|
1175
|
+
return jsonResult(await browserStatus(baseUrl, { profile }));
|
|
1176
|
+
case "stop":
|
|
1177
|
+
await browserStop(baseUrl, { profile });
|
|
1178
|
+
return jsonResult(await browserStatus(baseUrl, { profile }));
|
|
1179
|
+
case "profiles":
|
|
1180
|
+
return jsonResult({ profiles: await browserProfiles(baseUrl) });
|
|
1181
|
+
case "tabs": {
|
|
1182
|
+
const tabs = await browserTabs(baseUrl, { profile });
|
|
1183
|
+
const wrapped = wrapBrowserExternalJson({
|
|
1184
|
+
kind: "tabs",
|
|
1185
|
+
payload: { tabs },
|
|
1186
|
+
includeWarning: false
|
|
1187
|
+
});
|
|
1188
|
+
return {
|
|
1189
|
+
content: [{ type: "text", text: wrapped.wrappedText }],
|
|
1190
|
+
details: { ...wrapped.safeDetails, tabCount: tabs.length }
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
case "open": {
|
|
1194
|
+
const targetUrl = readStringParam(params, "targetUrl", { required: true });
|
|
1195
|
+
return jsonResult(await browserOpenTab(baseUrl, targetUrl, { profile }));
|
|
1196
|
+
}
|
|
1197
|
+
case "focus": {
|
|
1198
|
+
const targetId = readStringParam(params, "targetId", { required: true });
|
|
1199
|
+
await browserFocusTab(baseUrl, targetId, { profile });
|
|
1200
|
+
return jsonResult({ ok: true });
|
|
1201
|
+
}
|
|
1202
|
+
case "close": {
|
|
1203
|
+
const targetId = readStringParam(params, "targetId");
|
|
1204
|
+
if (targetId) {
|
|
1205
|
+
await browserCloseTab(baseUrl, targetId, { profile });
|
|
1206
|
+
} else {
|
|
1207
|
+
await browserAct(baseUrl, { kind: "close" }, { profile });
|
|
1208
|
+
}
|
|
1209
|
+
return jsonResult({ ok: true });
|
|
1210
|
+
}
|
|
1211
|
+
case "snapshot": {
|
|
1212
|
+
const format = params.snapshotFormat === "ai" || params.snapshotFormat === "aria" ? params.snapshotFormat : "ai";
|
|
1213
|
+
const mode = params.mode === "efficient" ? "efficient" : void 0;
|
|
1214
|
+
const labels = typeof params.labels === "boolean" ? params.labels : void 0;
|
|
1215
|
+
const refs = params.refs === "aria" || params.refs === "role" ? params.refs : void 0;
|
|
1216
|
+
const hasMaxChars = Object.hasOwn(params, "maxChars");
|
|
1217
|
+
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
|
|
1218
|
+
const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit : void 0;
|
|
1219
|
+
const maxChars = typeof params.maxChars === "number" && Number.isFinite(params.maxChars) && params.maxChars > 0 ? Math.floor(params.maxChars) : void 0;
|
|
1220
|
+
const resolvedMaxChars = hasMaxChars ? maxChars : mode === "efficient" ? void 0 : DEFAULT_AI_SNAPSHOT_MAX_CHARS;
|
|
1221
|
+
const interactive = typeof params.interactive === "boolean" ? params.interactive : void 0;
|
|
1222
|
+
const compact = typeof params.compact === "boolean" ? params.compact : void 0;
|
|
1223
|
+
const depth = typeof params.depth === "number" && Number.isFinite(params.depth) ? params.depth : void 0;
|
|
1224
|
+
const selector = typeof params.selector === "string" ? params.selector.trim() : void 0;
|
|
1225
|
+
const frame = typeof params.frame === "string" ? params.frame.trim() : void 0;
|
|
1226
|
+
const snapshot = await browserSnapshot(baseUrl, {
|
|
1227
|
+
format,
|
|
1228
|
+
targetId,
|
|
1229
|
+
limit,
|
|
1230
|
+
...typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {},
|
|
1231
|
+
refs,
|
|
1232
|
+
interactive,
|
|
1233
|
+
compact,
|
|
1234
|
+
depth,
|
|
1235
|
+
selector,
|
|
1236
|
+
frame,
|
|
1237
|
+
labels,
|
|
1238
|
+
mode,
|
|
1239
|
+
profile
|
|
1240
|
+
});
|
|
1241
|
+
if (snapshot.format === "ai") {
|
|
1242
|
+
const extractedText = snapshot.snapshot ?? "";
|
|
1243
|
+
const wrappedSnapshot = wrapExternalContent(extractedText, {
|
|
1244
|
+
source: "browser",
|
|
1245
|
+
includeWarning: true
|
|
1246
|
+
});
|
|
1247
|
+
const safeDetails = {
|
|
1248
|
+
ok: true,
|
|
1249
|
+
format: snapshot.format,
|
|
1250
|
+
targetId: snapshot.targetId,
|
|
1251
|
+
url: snapshot.url,
|
|
1252
|
+
truncated: snapshot.truncated,
|
|
1253
|
+
stats: snapshot.stats,
|
|
1254
|
+
refs: snapshot.refs ? Object.keys(snapshot.refs).length : void 0,
|
|
1255
|
+
labels: snapshot.labels,
|
|
1256
|
+
labelsCount: snapshot.labelsCount,
|
|
1257
|
+
labelsSkipped: snapshot.labelsSkipped,
|
|
1258
|
+
imagePath: snapshot.imagePath,
|
|
1259
|
+
imageType: snapshot.imageType,
|
|
1260
|
+
externalContent: {
|
|
1261
|
+
untrusted: true,
|
|
1262
|
+
source: "browser",
|
|
1263
|
+
kind: "snapshot",
|
|
1264
|
+
format: "ai",
|
|
1265
|
+
wrapped: true
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
if (labels && snapshot.imagePath) {
|
|
1269
|
+
return await imageResultFromFile({
|
|
1270
|
+
label: "browser:snapshot",
|
|
1271
|
+
path: snapshot.imagePath,
|
|
1272
|
+
extraText: wrappedSnapshot,
|
|
1273
|
+
details: safeDetails
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
return {
|
|
1277
|
+
content: [{ type: "text", text: wrappedSnapshot }],
|
|
1278
|
+
details: safeDetails
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
const cleaned = cleanSnapshot(snapshot, {
|
|
1282
|
+
url: snapshot.url,
|
|
1283
|
+
maxChars: resolvedMaxChars ?? void 0
|
|
1284
|
+
});
|
|
1285
|
+
const wrappedAria = wrapExternalContent(cleaned.text, {
|
|
1286
|
+
source: "browser",
|
|
1287
|
+
includeWarning: true
|
|
1288
|
+
});
|
|
1289
|
+
return {
|
|
1290
|
+
content: [{ type: "text", text: wrappedAria }],
|
|
1291
|
+
details: {
|
|
1292
|
+
ok: true,
|
|
1293
|
+
format: "aria",
|
|
1294
|
+
targetId: snapshot.targetId,
|
|
1295
|
+
url: snapshot.url,
|
|
1296
|
+
nodeCount: cleaned.nodeCount,
|
|
1297
|
+
noiseRemoved: cleaned.noiseRemoved,
|
|
1298
|
+
siteExtractor: cleaned.siteExtractor,
|
|
1299
|
+
truncated: cleaned.truncated,
|
|
1300
|
+
externalContent: {
|
|
1301
|
+
untrusted: true,
|
|
1302
|
+
source: "browser",
|
|
1303
|
+
kind: "snapshot",
|
|
1304
|
+
format: "aria",
|
|
1305
|
+
wrapped: true
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
case "screenshot": {
|
|
1311
|
+
const targetId = readStringParam(params, "targetId");
|
|
1312
|
+
const fullPage = Boolean(params.fullPage);
|
|
1313
|
+
const ref = readStringParam(params, "ref");
|
|
1314
|
+
const element = readStringParam(params, "element");
|
|
1315
|
+
const type = params.type === "jpeg" ? "jpeg" : "png";
|
|
1316
|
+
const result = await browserScreenshotAction(baseUrl, {
|
|
1317
|
+
targetId,
|
|
1318
|
+
fullPage,
|
|
1319
|
+
ref,
|
|
1320
|
+
element,
|
|
1321
|
+
type,
|
|
1322
|
+
profile
|
|
1323
|
+
});
|
|
1324
|
+
return await imageResultFromFile({
|
|
1325
|
+
label: "browser:screenshot",
|
|
1326
|
+
path: result.path,
|
|
1327
|
+
details: result
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
case "navigate": {
|
|
1331
|
+
const targetUrl = readStringParam(params, "targetUrl", { required: true });
|
|
1332
|
+
const targetId = readStringParam(params, "targetId");
|
|
1333
|
+
return jsonResult(
|
|
1334
|
+
await browserNavigate(baseUrl, {
|
|
1335
|
+
url: targetUrl,
|
|
1336
|
+
targetId,
|
|
1337
|
+
profile
|
|
1338
|
+
})
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
case "console": {
|
|
1342
|
+
const level = typeof params.level === "string" ? params.level.trim() : void 0;
|
|
1343
|
+
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
|
|
1344
|
+
const result = await browserConsoleMessages(baseUrl, { level, targetId, profile });
|
|
1345
|
+
const wrapped = wrapBrowserExternalJson({
|
|
1346
|
+
kind: "console",
|
|
1347
|
+
payload: result,
|
|
1348
|
+
includeWarning: false
|
|
1349
|
+
});
|
|
1350
|
+
return {
|
|
1351
|
+
content: [{ type: "text", text: wrapped.wrappedText }],
|
|
1352
|
+
details: {
|
|
1353
|
+
...wrapped.safeDetails,
|
|
1354
|
+
targetId: result.targetId,
|
|
1355
|
+
messageCount: result.messages.length
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
case "pdf": {
|
|
1360
|
+
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
|
|
1361
|
+
const result = await browserPdfSave(baseUrl, { targetId, profile });
|
|
1362
|
+
return {
|
|
1363
|
+
content: [{ type: "text", text: `FILE:${result.path}` }],
|
|
1364
|
+
details: result
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
case "upload": {
|
|
1368
|
+
const paths = Array.isArray(params.paths) ? params.paths.map((p) => String(p)) : [];
|
|
1369
|
+
if (paths.length === 0) throw new Error("paths required");
|
|
1370
|
+
const uploadDir = config?.uploadDir || DEFAULT_UPLOAD_DIR;
|
|
1371
|
+
const normalizedPaths = resolvePathsWithinRoot(uploadDir, ...paths);
|
|
1372
|
+
const ref = readStringParam(params, "ref");
|
|
1373
|
+
const inputRef = readStringParam(params, "inputRef");
|
|
1374
|
+
const element = readStringParam(params, "element");
|
|
1375
|
+
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
|
|
1376
|
+
const timeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? params.timeoutMs : void 0;
|
|
1377
|
+
return jsonResult(
|
|
1378
|
+
await browserArmFileChooser(baseUrl, {
|
|
1379
|
+
paths: normalizedPaths,
|
|
1380
|
+
ref,
|
|
1381
|
+
inputRef,
|
|
1382
|
+
element,
|
|
1383
|
+
targetId,
|
|
1384
|
+
timeoutMs,
|
|
1385
|
+
profile
|
|
1386
|
+
})
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
case "dialog": {
|
|
1390
|
+
const accept = Boolean(params.accept);
|
|
1391
|
+
const promptText = typeof params.promptText === "string" ? params.promptText : void 0;
|
|
1392
|
+
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
|
|
1393
|
+
const timeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? params.timeoutMs : void 0;
|
|
1394
|
+
return jsonResult(
|
|
1395
|
+
await browserArmDialog(baseUrl, {
|
|
1396
|
+
accept,
|
|
1397
|
+
promptText,
|
|
1398
|
+
targetId,
|
|
1399
|
+
timeoutMs,
|
|
1400
|
+
profile
|
|
1401
|
+
})
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
case "act": {
|
|
1405
|
+
const request = params.request;
|
|
1406
|
+
if (!request || typeof request !== "object") throw new Error("request required");
|
|
1407
|
+
if (request.kind === "evaluate" && config?.allowEvaluate === false) {
|
|
1408
|
+
throw new Error("JavaScript evaluation is disabled for this agent. Enable it in agent config.");
|
|
1409
|
+
}
|
|
1410
|
+
const result = await browserAct(baseUrl, request, {
|
|
1411
|
+
profile
|
|
1412
|
+
});
|
|
1413
|
+
return jsonResult(result);
|
|
1414
|
+
}
|
|
1415
|
+
default:
|
|
1416
|
+
throw new Error(`Unknown browser action: ${action}`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
export {
|
|
1421
|
+
createEnterpriseBrowserTool
|
|
1422
|
+
};
|