@agenticmail/enterprise 0.5.417 → 0.5.418

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.
@@ -0,0 +1,2102 @@
1
+ import {
2
+ allocateCdpPort,
3
+ allocateColor,
4
+ getPwAiModule,
5
+ getUsedColors,
6
+ getUsedPorts,
7
+ isValidProfileName,
8
+ movePathToTrash,
9
+ parseHttpUrl,
10
+ resolveProfile
11
+ } from "./chunk-C2LZJ26G.js";
12
+ import {
13
+ DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH,
14
+ DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS,
15
+ DEFAULT_AI_SNAPSHOT_MAX_CHARS,
16
+ DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
17
+ captureScreenshot,
18
+ resolveAgenticMailUserDataDir,
19
+ resolveBrowserExecutableForPlatform,
20
+ snapshotAria,
21
+ withBrowserNavigationPolicy
22
+ } from "./chunk-PB4RGLMS.js";
23
+ import {
24
+ IMAGE_REDUCE_QUALITY_STEPS,
25
+ buildImageResizeSideGrid,
26
+ deriveDefaultBrowserCdpPortRange,
27
+ ensureMediaDir,
28
+ getImageMetadata,
29
+ loadConfig,
30
+ parseBooleanValue,
31
+ resizeToJpeg,
32
+ resolvePreferredAgenticMailTmpDir,
33
+ saveMediaBuffer,
34
+ writeConfigFile
35
+ } from "./chunk-A3PUJDNH.js";
36
+
37
+ // src/browser/routes/agent.act.shared.ts
38
+ var ACT_KINDS = [
39
+ "click",
40
+ "close",
41
+ "drag",
42
+ "evaluate",
43
+ "fill",
44
+ "hover",
45
+ "mouse_click",
46
+ "scroll",
47
+ "scrollIntoView",
48
+ "press",
49
+ "resize",
50
+ "select",
51
+ "type",
52
+ "wait"
53
+ ];
54
+ function isActKind(value) {
55
+ if (typeof value !== "string") {
56
+ return false;
57
+ }
58
+ return ACT_KINDS.includes(value);
59
+ }
60
+ var ALLOWED_CLICK_MODIFIERS = /* @__PURE__ */ new Set([
61
+ "Alt",
62
+ "Control",
63
+ "ControlOrMeta",
64
+ "Meta",
65
+ "Shift"
66
+ ]);
67
+ function parseClickButton(raw) {
68
+ if (raw === "left" || raw === "right" || raw === "middle") {
69
+ return raw;
70
+ }
71
+ return void 0;
72
+ }
73
+ function parseClickModifiers(raw) {
74
+ const invalid = raw.filter((m) => !ALLOWED_CLICK_MODIFIERS.has(m));
75
+ if (invalid.length) {
76
+ return { error: "modifiers must be Alt|Control|ControlOrMeta|Meta|Shift" };
77
+ }
78
+ return { modifiers: raw.length ? raw : void 0 };
79
+ }
80
+
81
+ // src/browser/routes/utils.ts
82
+ function getProfileContext(req, ctx) {
83
+ let profileName;
84
+ if (typeof req.query.profile === "string") {
85
+ profileName = req.query.profile.trim() || void 0;
86
+ }
87
+ if (!profileName && req.body && typeof req.body === "object") {
88
+ const body = req.body;
89
+ if (typeof body.profile === "string") {
90
+ profileName = body.profile.trim() || void 0;
91
+ }
92
+ }
93
+ try {
94
+ return ctx.forProfile(profileName);
95
+ } catch (err) {
96
+ return { error: String(err), status: 404 };
97
+ }
98
+ }
99
+ function jsonError(res, status, message) {
100
+ res.status(status).json({ error: message });
101
+ }
102
+ function toStringOrEmpty(value) {
103
+ if (typeof value === "string") {
104
+ return value.trim();
105
+ }
106
+ if (typeof value === "number" || typeof value === "boolean") {
107
+ return String(value).trim();
108
+ }
109
+ return "";
110
+ }
111
+ function toNumber(value) {
112
+ if (typeof value === "number" && Number.isFinite(value)) {
113
+ return value;
114
+ }
115
+ if (typeof value === "string" && value.trim()) {
116
+ const parsed = Number(value);
117
+ return Number.isFinite(parsed) ? parsed : void 0;
118
+ }
119
+ return void 0;
120
+ }
121
+ function toBoolean(value) {
122
+ return parseBooleanValue(value, {
123
+ truthy: ["true", "1", "yes"],
124
+ falsy: ["false", "0", "no"]
125
+ });
126
+ }
127
+ function toStringArray(value) {
128
+ if (!Array.isArray(value)) {
129
+ return void 0;
130
+ }
131
+ const strings = value.map((v) => toStringOrEmpty(v)).filter(Boolean);
132
+ return strings.length ? strings : void 0;
133
+ }
134
+
135
+ // src/browser/routes/agent.shared.ts
136
+ var SELECTOR_UNSUPPORTED_MESSAGE = [
137
+ "Error: 'selector' is not supported. Use 'ref' from snapshot instead.",
138
+ "",
139
+ "Example workflow:",
140
+ "1. snapshot action to get page state with refs",
141
+ '2. act with ref: "e123" to interact with element',
142
+ "",
143
+ "This is more reliable for modern SPAs."
144
+ ].join("\n");
145
+ function readBody(req) {
146
+ const body = req.body;
147
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
148
+ return {};
149
+ }
150
+ return body;
151
+ }
152
+ function resolveTargetIdFromBody(body) {
153
+ const targetId = typeof body.targetId === "string" ? body.targetId.trim() : "";
154
+ return targetId || void 0;
155
+ }
156
+ function resolveTargetIdFromQuery(query) {
157
+ const targetId = typeof query.targetId === "string" ? query.targetId.trim() : "";
158
+ return targetId || void 0;
159
+ }
160
+ function handleRouteError(ctx, res, err) {
161
+ const mapped = ctx.mapTabError(err);
162
+ if (mapped) {
163
+ return jsonError(res, mapped.status, mapped.message);
164
+ }
165
+ jsonError(res, 500, String(err));
166
+ }
167
+ function resolveProfileContext(req, res, ctx) {
168
+ const profileCtx = getProfileContext(req, ctx);
169
+ if ("error" in profileCtx) {
170
+ jsonError(res, profileCtx.status, profileCtx.error);
171
+ return null;
172
+ }
173
+ return profileCtx;
174
+ }
175
+ async function getPwAiModule2() {
176
+ return await getPwAiModule({ mode: "soft" });
177
+ }
178
+ async function requirePwAi(res, feature) {
179
+ const mod = await getPwAiModule2();
180
+ if (mod) {
181
+ return mod;
182
+ }
183
+ jsonError(
184
+ res,
185
+ 501,
186
+ [
187
+ `Playwright is not available in this gateway build; '${feature}' is unsupported.`,
188
+ "Install the full Playwright package (not playwright-core) and restart the gateway, or reinstall with browser support.",
189
+ "Docs: /tools/browser#playwright-requirement"
190
+ ].join("\n")
191
+ );
192
+ return null;
193
+ }
194
+ async function withRouteTabContext(params) {
195
+ const profileCtx = resolveProfileContext(params.req, params.res, params.ctx);
196
+ if (!profileCtx) {
197
+ return void 0;
198
+ }
199
+ try {
200
+ const tab = await profileCtx.ensureTabAvailable(params.targetId);
201
+ return await params.run({
202
+ profileCtx,
203
+ tab,
204
+ cdpUrl: profileCtx.profile.cdpUrl
205
+ });
206
+ } catch (err) {
207
+ handleRouteError(params.ctx, params.res, err);
208
+ return void 0;
209
+ }
210
+ }
211
+ async function withPlaywrightRouteContext(params) {
212
+ return await withRouteTabContext({
213
+ req: params.req,
214
+ res: params.res,
215
+ ctx: params.ctx,
216
+ targetId: params.targetId,
217
+ run: async ({ profileCtx, tab, cdpUrl }) => {
218
+ const pw = await requirePwAi(params.res, params.feature);
219
+ if (!pw) {
220
+ return void 0;
221
+ }
222
+ return await params.run({ profileCtx, tab, cdpUrl, pw });
223
+ }
224
+ });
225
+ }
226
+
227
+ // src/browser/paths.ts
228
+ import path from "path";
229
+ var DEFAULT_BROWSER_TMP_DIR = resolvePreferredAgenticMailTmpDir();
230
+ var DEFAULT_TRACE_DIR = DEFAULT_BROWSER_TMP_DIR;
231
+ var DEFAULT_DOWNLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "downloads");
232
+ var DEFAULT_UPLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "uploads");
233
+ function resolvePathWithinRoot(params) {
234
+ const root = path.resolve(params.rootDir);
235
+ const raw = params.requestedPath.trim();
236
+ if (!raw) {
237
+ if (!params.defaultFileName) {
238
+ return { ok: false, error: "path is required" };
239
+ }
240
+ return { ok: true, path: path.join(root, params.defaultFileName) };
241
+ }
242
+ const resolved = path.resolve(root, raw);
243
+ const rel = path.relative(root, resolved);
244
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
245
+ return { ok: false, error: `Invalid path: must stay within ${params.scopeLabel}` };
246
+ }
247
+ return { ok: true, path: resolved };
248
+ }
249
+ function resolvePathsWithinRoot(params) {
250
+ const resolvedPaths = [];
251
+ for (const raw of params.requestedPaths) {
252
+ const pathResult = resolvePathWithinRoot({
253
+ rootDir: params.rootDir,
254
+ requestedPath: raw,
255
+ scopeLabel: params.scopeLabel
256
+ });
257
+ if (!pathResult.ok) {
258
+ return { ok: false, error: pathResult.error };
259
+ }
260
+ resolvedPaths.push(pathResult.path);
261
+ }
262
+ return { ok: true, paths: resolvedPaths };
263
+ }
264
+
265
+ // src/browser/routes/agent.act.ts
266
+ function resolveDownloadPathOrRespond(res, requestedPath) {
267
+ const downloadPathResult = resolvePathWithinRoot({
268
+ rootDir: DEFAULT_DOWNLOAD_DIR,
269
+ requestedPath,
270
+ scopeLabel: "downloads directory"
271
+ });
272
+ if (!downloadPathResult.ok) {
273
+ res.status(400).json({ error: downloadPathResult.error });
274
+ return null;
275
+ }
276
+ return downloadPathResult.path;
277
+ }
278
+ function buildDownloadRequestBase(cdpUrl, targetId, timeoutMs) {
279
+ return {
280
+ cdpUrl,
281
+ targetId,
282
+ timeoutMs: timeoutMs ?? void 0
283
+ };
284
+ }
285
+ function respondWithDownloadResult(res, targetId, result) {
286
+ res.json({ ok: true, targetId, download: result });
287
+ }
288
+ function registerBrowserAgentActRoutes(app, ctx) {
289
+ app.post("/act", async (req, res) => {
290
+ const body = readBody(req);
291
+ const kindRaw = toStringOrEmpty(body.kind);
292
+ if (!isActKind(kindRaw)) {
293
+ return jsonError(res, 400, "kind is required");
294
+ }
295
+ const kind = kindRaw;
296
+ const targetId = resolveTargetIdFromBody(body);
297
+ if (Object.hasOwn(body, "selector") && kind !== "wait") {
298
+ return jsonError(res, 400, SELECTOR_UNSUPPORTED_MESSAGE);
299
+ }
300
+ const actTimeout = new Promise(
301
+ (_, reject) => setTimeout(() => reject(new Error(`act:${kind} timed out on server after 35s`)), 35e3)
302
+ );
303
+ const actExecution = withPlaywrightRouteContext({
304
+ req,
305
+ res,
306
+ ctx,
307
+ targetId,
308
+ feature: `act:${kind}`,
309
+ run: async ({ cdpUrl, tab, pw }) => {
310
+ const evaluateEnabled = ctx.state().resolved.evaluateEnabled;
311
+ switch (kind) {
312
+ case "click": {
313
+ const ref = toStringOrEmpty(body.ref);
314
+ if (!ref) {
315
+ return jsonError(res, 400, "ref is required");
316
+ }
317
+ const doubleClick = toBoolean(body.doubleClick) ?? false;
318
+ const timeoutMs = toNumber(body.timeoutMs);
319
+ const buttonRaw = toStringOrEmpty(body.button) || "";
320
+ const button = buttonRaw ? parseClickButton(buttonRaw) : void 0;
321
+ if (buttonRaw && !button) {
322
+ return jsonError(res, 400, "button must be left|right|middle");
323
+ }
324
+ const modifiersRaw = toStringArray(body.modifiers) ?? [];
325
+ const parsedModifiers = parseClickModifiers(modifiersRaw);
326
+ if (parsedModifiers.error) {
327
+ return jsonError(res, 400, parsedModifiers.error);
328
+ }
329
+ const modifiers = parsedModifiers.modifiers;
330
+ const clickRequest = {
331
+ cdpUrl,
332
+ targetId: tab.targetId,
333
+ ref,
334
+ doubleClick
335
+ };
336
+ if (button) {
337
+ clickRequest.button = button;
338
+ }
339
+ if (modifiers) {
340
+ clickRequest.modifiers = modifiers;
341
+ }
342
+ if (timeoutMs) {
343
+ clickRequest.timeoutMs = timeoutMs;
344
+ }
345
+ await pw.clickViaPlaywright(clickRequest);
346
+ return res.json({ ok: true, targetId: tab.targetId, url: tab.url });
347
+ }
348
+ case "type": {
349
+ const ref = toStringOrEmpty(body.ref);
350
+ if (!ref) {
351
+ return jsonError(res, 400, "ref is required");
352
+ }
353
+ if (typeof body.text !== "string") {
354
+ return jsonError(res, 400, "text is required");
355
+ }
356
+ const text = body.text;
357
+ const submit = toBoolean(body.submit) ?? false;
358
+ const slowly = toBoolean(body.slowly) ?? false;
359
+ const timeoutMs = toNumber(body.timeoutMs);
360
+ const typeRequest = {
361
+ cdpUrl,
362
+ targetId: tab.targetId,
363
+ ref,
364
+ text,
365
+ submit,
366
+ slowly
367
+ };
368
+ if (timeoutMs) {
369
+ typeRequest.timeoutMs = timeoutMs;
370
+ }
371
+ await pw.typeViaPlaywright(typeRequest);
372
+ return res.json({ ok: true, targetId: tab.targetId });
373
+ }
374
+ case "press": {
375
+ const key = toStringOrEmpty(body.key);
376
+ if (!key) {
377
+ return jsonError(res, 400, "key is required");
378
+ }
379
+ const delayMs = toNumber(body.delayMs);
380
+ await pw.pressKeyViaPlaywright({
381
+ cdpUrl,
382
+ targetId: tab.targetId,
383
+ key,
384
+ delayMs: delayMs ?? void 0
385
+ });
386
+ return res.json({ ok: true, targetId: tab.targetId });
387
+ }
388
+ case "hover": {
389
+ const ref = toStringOrEmpty(body.ref);
390
+ if (!ref) {
391
+ return jsonError(res, 400, "ref is required");
392
+ }
393
+ const timeoutMs = toNumber(body.timeoutMs);
394
+ await pw.hoverViaPlaywright({
395
+ cdpUrl,
396
+ targetId: tab.targetId,
397
+ ref,
398
+ timeoutMs: timeoutMs ?? void 0
399
+ });
400
+ return res.json({ ok: true, targetId: tab.targetId });
401
+ }
402
+ case "scrollIntoView": {
403
+ const ref = toStringOrEmpty(body.ref);
404
+ if (!ref) {
405
+ return jsonError(res, 400, "ref is required");
406
+ }
407
+ const timeoutMs = toNumber(body.timeoutMs);
408
+ const scrollRequest = {
409
+ cdpUrl,
410
+ targetId: tab.targetId,
411
+ ref
412
+ };
413
+ if (timeoutMs) {
414
+ scrollRequest.timeoutMs = timeoutMs;
415
+ }
416
+ await pw.scrollIntoViewViaPlaywright(scrollRequest);
417
+ return res.json({ ok: true, targetId: tab.targetId });
418
+ }
419
+ case "drag": {
420
+ const startRef = toStringOrEmpty(body.startRef);
421
+ const endRef = toStringOrEmpty(body.endRef);
422
+ if (!startRef || !endRef) {
423
+ return jsonError(res, 400, "startRef and endRef are required");
424
+ }
425
+ const timeoutMs = toNumber(body.timeoutMs);
426
+ await pw.dragViaPlaywright({
427
+ cdpUrl,
428
+ targetId: tab.targetId,
429
+ startRef,
430
+ endRef,
431
+ timeoutMs: timeoutMs ?? void 0
432
+ });
433
+ return res.json({ ok: true, targetId: tab.targetId });
434
+ }
435
+ case "select": {
436
+ const ref = toStringOrEmpty(body.ref);
437
+ const values = toStringArray(body.values);
438
+ if (!ref || !values?.length) {
439
+ return jsonError(res, 400, "ref and values are required");
440
+ }
441
+ const timeoutMs = toNumber(body.timeoutMs);
442
+ await pw.selectOptionViaPlaywright({
443
+ cdpUrl,
444
+ targetId: tab.targetId,
445
+ ref,
446
+ values,
447
+ timeoutMs: timeoutMs ?? void 0
448
+ });
449
+ return res.json({ ok: true, targetId: tab.targetId });
450
+ }
451
+ case "fill": {
452
+ const rawFields = Array.isArray(body.fields) ? body.fields : [];
453
+ const fields = rawFields.map((field) => {
454
+ if (!field || typeof field !== "object") {
455
+ return null;
456
+ }
457
+ const rec = field;
458
+ const ref = toStringOrEmpty(rec.ref);
459
+ const type = toStringOrEmpty(rec.type);
460
+ if (!ref || !type) {
461
+ return null;
462
+ }
463
+ const value = typeof rec.value === "string" || typeof rec.value === "number" || typeof rec.value === "boolean" ? rec.value : void 0;
464
+ const parsed = value === void 0 ? { ref, type } : { ref, type, value };
465
+ return parsed;
466
+ }).filter((field) => field !== null);
467
+ if (!fields.length) {
468
+ return jsonError(res, 400, "fields are required");
469
+ }
470
+ const timeoutMs = toNumber(body.timeoutMs);
471
+ await pw.fillFormViaPlaywright({
472
+ cdpUrl,
473
+ targetId: tab.targetId,
474
+ fields,
475
+ timeoutMs: timeoutMs ?? void 0
476
+ });
477
+ return res.json({ ok: true, targetId: tab.targetId });
478
+ }
479
+ case "resize": {
480
+ const width = toNumber(body.width);
481
+ const height = toNumber(body.height);
482
+ if (!width || !height) {
483
+ return jsonError(res, 400, "width and height are required");
484
+ }
485
+ await pw.resizeViewportViaPlaywright({
486
+ cdpUrl,
487
+ targetId: tab.targetId,
488
+ width,
489
+ height
490
+ });
491
+ return res.json({ ok: true, targetId: tab.targetId, url: tab.url });
492
+ }
493
+ case "wait": {
494
+ const timeMs = toNumber(body.timeMs);
495
+ const text = toStringOrEmpty(body.text) || void 0;
496
+ const textGone = toStringOrEmpty(body.textGone) || void 0;
497
+ const selector = toStringOrEmpty(body.selector) || void 0;
498
+ const url = toStringOrEmpty(body.url) || void 0;
499
+ const loadStateRaw = toStringOrEmpty(body.loadState);
500
+ const loadState = loadStateRaw === "load" || loadStateRaw === "domcontentloaded" || loadStateRaw === "networkidle" ? loadStateRaw : void 0;
501
+ const fn = toStringOrEmpty(body.fn) || void 0;
502
+ const timeoutMs = toNumber(body.timeoutMs) ?? void 0;
503
+ if (fn && !evaluateEnabled) {
504
+ return jsonError(
505
+ res,
506
+ 403,
507
+ [
508
+ "wait --fn is disabled by config (browser.evaluateEnabled=false).",
509
+ "Docs: /gateway/configuration#browser-agenticmail-managed-browser"
510
+ ].join("\n")
511
+ );
512
+ }
513
+ if (timeMs === void 0 && !text && !textGone && !selector && !url && !loadState && !fn) {
514
+ return jsonError(
515
+ res,
516
+ 400,
517
+ "wait requires at least one of: timeMs, text, textGone, selector, url, loadState, fn"
518
+ );
519
+ }
520
+ await pw.waitForViaPlaywright({
521
+ cdpUrl,
522
+ targetId: tab.targetId,
523
+ timeMs,
524
+ text,
525
+ textGone,
526
+ selector,
527
+ url,
528
+ loadState,
529
+ fn,
530
+ timeoutMs
531
+ });
532
+ return res.json({ ok: true, targetId: tab.targetId });
533
+ }
534
+ case "evaluate": {
535
+ if (!evaluateEnabled) {
536
+ return jsonError(
537
+ res,
538
+ 403,
539
+ [
540
+ "act:evaluate is disabled by config (browser.evaluateEnabled=false).",
541
+ "Docs: /gateway/configuration#browser-agenticmail-managed-browser"
542
+ ].join("\n")
543
+ );
544
+ }
545
+ const fn = toStringOrEmpty(body.fn);
546
+ if (!fn) {
547
+ return jsonError(res, 400, "fn is required");
548
+ }
549
+ const ref = toStringOrEmpty(body.ref) || void 0;
550
+ const evalTimeoutMs = toNumber(body.timeoutMs);
551
+ const evalRequest = {
552
+ cdpUrl,
553
+ targetId: tab.targetId,
554
+ fn,
555
+ ref,
556
+ signal: req.signal
557
+ };
558
+ if (evalTimeoutMs !== void 0) {
559
+ evalRequest.timeoutMs = evalTimeoutMs;
560
+ }
561
+ const result = await pw.evaluateViaPlaywright(evalRequest);
562
+ return res.json({
563
+ ok: true,
564
+ targetId: tab.targetId,
565
+ url: tab.url,
566
+ result
567
+ });
568
+ }
569
+ case "mouse_click": {
570
+ const x = typeof body.x === "number" ? body.x : void 0;
571
+ const y = typeof body.y === "number" ? body.y : void 0;
572
+ if (x === void 0 || y === void 0) {
573
+ return jsonError(res, 400, "x and y coordinates are required for mouse_click");
574
+ }
575
+ const button = body.button === "right" || body.button === "middle" ? body.button : "left";
576
+ await pw.mouseClickViaPlaywright({
577
+ cdpUrl,
578
+ targetId: tab.targetId,
579
+ x,
580
+ y,
581
+ button,
582
+ doubleClick: !!body.doubleClick
583
+ });
584
+ return res.json({ ok: true, targetId: tab.targetId, url: tab.url });
585
+ }
586
+ case "scroll": {
587
+ const deltaX = typeof body.deltaX === "number" ? body.deltaX : 0;
588
+ const deltaY = typeof body.deltaY === "number" ? body.deltaY : typeof body.delta === "number" ? body.delta : 0;
589
+ await pw.scrollViaPlaywright({
590
+ cdpUrl,
591
+ targetId: tab.targetId,
592
+ x: typeof body.x === "number" ? body.x : void 0,
593
+ y: typeof body.y === "number" ? body.y : void 0,
594
+ deltaX,
595
+ deltaY
596
+ });
597
+ return res.json({ ok: true, targetId: tab.targetId, url: tab.url });
598
+ }
599
+ case "close": {
600
+ await pw.closePageViaPlaywright({ cdpUrl, targetId: tab.targetId });
601
+ return res.json({ ok: true, targetId: tab.targetId });
602
+ }
603
+ default: {
604
+ return jsonError(res, 400, "unsupported kind");
605
+ }
606
+ }
607
+ }
608
+ });
609
+ try {
610
+ await Promise.race([actExecution, actTimeout]);
611
+ } catch (err) {
612
+ if (!res.headersSent) {
613
+ return jsonError(res, 504, err?.message || "act operation timed out");
614
+ }
615
+ }
616
+ });
617
+ app.post("/hooks/file-chooser", async (req, res) => {
618
+ const body = readBody(req);
619
+ const targetId = resolveTargetIdFromBody(body);
620
+ const ref = toStringOrEmpty(body.ref) || void 0;
621
+ const inputRef = toStringOrEmpty(body.inputRef) || void 0;
622
+ const element = toStringOrEmpty(body.element) || void 0;
623
+ const paths = toStringArray(body.paths) ?? [];
624
+ const timeoutMs = toNumber(body.timeoutMs);
625
+ if (!paths.length) {
626
+ return jsonError(res, 400, "paths are required");
627
+ }
628
+ await withPlaywrightRouteContext({
629
+ req,
630
+ res,
631
+ ctx,
632
+ targetId,
633
+ feature: "file chooser hook",
634
+ run: async ({ cdpUrl, tab, pw }) => {
635
+ const uploadPathsResult = resolvePathsWithinRoot({
636
+ rootDir: DEFAULT_UPLOAD_DIR,
637
+ requestedPaths: paths,
638
+ scopeLabel: `uploads directory (${DEFAULT_UPLOAD_DIR})`
639
+ });
640
+ if (!uploadPathsResult.ok) {
641
+ res.status(400).json({ error: uploadPathsResult.error });
642
+ return;
643
+ }
644
+ const resolvedPaths = uploadPathsResult.paths;
645
+ if (inputRef || element) {
646
+ if (ref) {
647
+ return jsonError(res, 400, "ref cannot be combined with inputRef/element");
648
+ }
649
+ await pw.setInputFilesViaPlaywright({
650
+ cdpUrl,
651
+ targetId: tab.targetId,
652
+ inputRef,
653
+ element,
654
+ paths: resolvedPaths
655
+ });
656
+ } else {
657
+ await pw.armFileUploadViaPlaywright({
658
+ cdpUrl,
659
+ targetId: tab.targetId,
660
+ paths: resolvedPaths,
661
+ timeoutMs: timeoutMs ?? void 0
662
+ });
663
+ if (ref) {
664
+ await pw.clickViaPlaywright({
665
+ cdpUrl,
666
+ targetId: tab.targetId,
667
+ ref
668
+ });
669
+ }
670
+ }
671
+ res.json({ ok: true });
672
+ }
673
+ });
674
+ });
675
+ app.post("/hooks/dialog", async (req, res) => {
676
+ const body = readBody(req);
677
+ const targetId = resolveTargetIdFromBody(body);
678
+ const accept = toBoolean(body.accept);
679
+ const promptText = toStringOrEmpty(body.promptText) || void 0;
680
+ const timeoutMs = toNumber(body.timeoutMs);
681
+ if (accept === void 0) {
682
+ return jsonError(res, 400, "accept is required");
683
+ }
684
+ await withPlaywrightRouteContext({
685
+ req,
686
+ res,
687
+ ctx,
688
+ targetId,
689
+ feature: "dialog hook",
690
+ run: async ({ cdpUrl, tab, pw }) => {
691
+ await pw.armDialogViaPlaywright({
692
+ cdpUrl,
693
+ targetId: tab.targetId,
694
+ accept,
695
+ promptText,
696
+ timeoutMs: timeoutMs ?? void 0
697
+ });
698
+ res.json({ ok: true });
699
+ }
700
+ });
701
+ });
702
+ app.post("/wait/download", async (req, res) => {
703
+ const body = readBody(req);
704
+ const targetId = resolveTargetIdFromBody(body);
705
+ const out = toStringOrEmpty(body.path) || "";
706
+ const timeoutMs = toNumber(body.timeoutMs);
707
+ await withPlaywrightRouteContext({
708
+ req,
709
+ res,
710
+ ctx,
711
+ targetId,
712
+ feature: "wait for download",
713
+ run: async ({ cdpUrl, tab, pw }) => {
714
+ let downloadPath;
715
+ if (out.trim()) {
716
+ const resolvedDownloadPath = resolveDownloadPathOrRespond(res, out);
717
+ if (!resolvedDownloadPath) {
718
+ return;
719
+ }
720
+ downloadPath = resolvedDownloadPath;
721
+ }
722
+ const requestBase = buildDownloadRequestBase(cdpUrl, tab.targetId, timeoutMs);
723
+ const result = await pw.waitForDownloadViaPlaywright({
724
+ ...requestBase,
725
+ path: downloadPath
726
+ });
727
+ respondWithDownloadResult(res, tab.targetId, result);
728
+ }
729
+ });
730
+ });
731
+ app.post("/download", async (req, res) => {
732
+ const body = readBody(req);
733
+ const targetId = resolveTargetIdFromBody(body);
734
+ const ref = toStringOrEmpty(body.ref);
735
+ const out = toStringOrEmpty(body.path);
736
+ const timeoutMs = toNumber(body.timeoutMs);
737
+ if (!ref) {
738
+ return jsonError(res, 400, "ref is required");
739
+ }
740
+ if (!out) {
741
+ return jsonError(res, 400, "path is required");
742
+ }
743
+ await withPlaywrightRouteContext({
744
+ req,
745
+ res,
746
+ ctx,
747
+ targetId,
748
+ feature: "download",
749
+ run: async ({ cdpUrl, tab, pw }) => {
750
+ const downloadPath = resolveDownloadPathOrRespond(res, out);
751
+ if (!downloadPath) {
752
+ return;
753
+ }
754
+ const requestBase = buildDownloadRequestBase(cdpUrl, tab.targetId, timeoutMs);
755
+ const result = await pw.downloadViaPlaywright({
756
+ ...requestBase,
757
+ ref,
758
+ path: downloadPath
759
+ });
760
+ respondWithDownloadResult(res, tab.targetId, result);
761
+ }
762
+ });
763
+ });
764
+ app.post("/response/body", async (req, res) => {
765
+ const body = readBody(req);
766
+ const targetId = resolveTargetIdFromBody(body);
767
+ const url = toStringOrEmpty(body.url);
768
+ const timeoutMs = toNumber(body.timeoutMs);
769
+ const maxChars = toNumber(body.maxChars);
770
+ if (!url) {
771
+ return jsonError(res, 400, "url is required");
772
+ }
773
+ await withPlaywrightRouteContext({
774
+ req,
775
+ res,
776
+ ctx,
777
+ targetId,
778
+ feature: "response body",
779
+ run: async ({ cdpUrl, tab, pw }) => {
780
+ const result = await pw.responseBodyViaPlaywright({
781
+ cdpUrl,
782
+ targetId: tab.targetId,
783
+ url,
784
+ timeoutMs: timeoutMs ?? void 0,
785
+ maxChars: maxChars ?? void 0
786
+ });
787
+ res.json({ ok: true, targetId: tab.targetId, response: result });
788
+ }
789
+ });
790
+ });
791
+ app.post("/highlight", async (req, res) => {
792
+ const body = readBody(req);
793
+ const targetId = resolveTargetIdFromBody(body);
794
+ const ref = toStringOrEmpty(body.ref);
795
+ if (!ref) {
796
+ return jsonError(res, 400, "ref is required");
797
+ }
798
+ await withPlaywrightRouteContext({
799
+ req,
800
+ res,
801
+ ctx,
802
+ targetId,
803
+ feature: "highlight",
804
+ run: async ({ cdpUrl, tab, pw }) => {
805
+ await pw.highlightViaPlaywright({
806
+ cdpUrl,
807
+ targetId: tab.targetId,
808
+ ref
809
+ });
810
+ res.json({ ok: true, targetId: tab.targetId });
811
+ }
812
+ });
813
+ });
814
+ }
815
+
816
+ // src/browser/routes/agent.debug.ts
817
+ import crypto from "crypto";
818
+ import fs from "fs/promises";
819
+ import path2 from "path";
820
+ function registerBrowserAgentDebugRoutes(app, ctx) {
821
+ app.get("/console", async (req, res) => {
822
+ const targetId = resolveTargetIdFromQuery(req.query);
823
+ const level = typeof req.query.level === "string" ? req.query.level : "";
824
+ await withPlaywrightRouteContext({
825
+ req,
826
+ res,
827
+ ctx,
828
+ targetId,
829
+ feature: "console messages",
830
+ run: async ({ cdpUrl, tab, pw }) => {
831
+ const messages = await pw.getConsoleMessagesViaPlaywright({
832
+ cdpUrl,
833
+ targetId: tab.targetId,
834
+ level: level.trim() || void 0
835
+ });
836
+ res.json({ ok: true, messages, targetId: tab.targetId });
837
+ }
838
+ });
839
+ });
840
+ app.get("/errors", async (req, res) => {
841
+ const targetId = resolveTargetIdFromQuery(req.query);
842
+ const clear = toBoolean(req.query.clear) ?? false;
843
+ await withPlaywrightRouteContext({
844
+ req,
845
+ res,
846
+ ctx,
847
+ targetId,
848
+ feature: "page errors",
849
+ run: async ({ cdpUrl, tab, pw }) => {
850
+ const result = await pw.getPageErrorsViaPlaywright({
851
+ cdpUrl,
852
+ targetId: tab.targetId,
853
+ clear
854
+ });
855
+ res.json({ ok: true, targetId: tab.targetId, ...result });
856
+ }
857
+ });
858
+ });
859
+ app.get("/requests", async (req, res) => {
860
+ const targetId = resolveTargetIdFromQuery(req.query);
861
+ const filter = typeof req.query.filter === "string" ? req.query.filter : "";
862
+ const clear = toBoolean(req.query.clear) ?? false;
863
+ await withPlaywrightRouteContext({
864
+ req,
865
+ res,
866
+ ctx,
867
+ targetId,
868
+ feature: "network requests",
869
+ run: async ({ cdpUrl, tab, pw }) => {
870
+ const result = await pw.getNetworkRequestsViaPlaywright({
871
+ cdpUrl,
872
+ targetId: tab.targetId,
873
+ filter: filter.trim() || void 0,
874
+ clear
875
+ });
876
+ res.json({ ok: true, targetId: tab.targetId, ...result });
877
+ }
878
+ });
879
+ });
880
+ app.post("/trace/start", async (req, res) => {
881
+ const body = readBody(req);
882
+ const targetId = resolveTargetIdFromBody(body);
883
+ const screenshots = toBoolean(body.screenshots) ?? void 0;
884
+ const snapshots = toBoolean(body.snapshots) ?? void 0;
885
+ const sources = toBoolean(body.sources) ?? void 0;
886
+ await withPlaywrightRouteContext({
887
+ req,
888
+ res,
889
+ ctx,
890
+ targetId,
891
+ feature: "trace start",
892
+ run: async ({ cdpUrl, tab, pw }) => {
893
+ await pw.traceStartViaPlaywright({
894
+ cdpUrl,
895
+ targetId: tab.targetId,
896
+ screenshots,
897
+ snapshots,
898
+ sources
899
+ });
900
+ res.json({ ok: true, targetId: tab.targetId });
901
+ }
902
+ });
903
+ });
904
+ app.post("/trace/stop", async (req, res) => {
905
+ const body = readBody(req);
906
+ const targetId = resolveTargetIdFromBody(body);
907
+ const out = toStringOrEmpty(body.path) || "";
908
+ await withPlaywrightRouteContext({
909
+ req,
910
+ res,
911
+ ctx,
912
+ targetId,
913
+ feature: "trace stop",
914
+ run: async ({ cdpUrl, tab, pw }) => {
915
+ const id = crypto.randomUUID();
916
+ const dir = DEFAULT_TRACE_DIR;
917
+ await fs.mkdir(dir, { recursive: true });
918
+ const tracePathResult = resolvePathWithinRoot({
919
+ rootDir: dir,
920
+ requestedPath: out,
921
+ scopeLabel: "trace directory",
922
+ defaultFileName: `browser-trace-${id}.zip`
923
+ });
924
+ if (!tracePathResult.ok) {
925
+ res.status(400).json({ error: tracePathResult.error });
926
+ return;
927
+ }
928
+ const tracePath = tracePathResult.path;
929
+ await pw.traceStopViaPlaywright({
930
+ cdpUrl,
931
+ targetId: tab.targetId,
932
+ path: tracePath
933
+ });
934
+ res.json({
935
+ ok: true,
936
+ targetId: tab.targetId,
937
+ path: path2.resolve(tracePath)
938
+ });
939
+ }
940
+ });
941
+ });
942
+ }
943
+
944
+ // src/browser/routes/agent.snapshot.ts
945
+ import path3 from "path";
946
+
947
+ // src/browser/screenshot.ts
948
+ var DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE = 2e3;
949
+ var DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES = 5 * 1024 * 1024;
950
+ async function normalizeBrowserScreenshot(buffer, opts) {
951
+ const maxSide = Math.max(1, Math.round(opts?.maxSide ?? DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE));
952
+ const maxBytes = Math.max(1, Math.round(opts?.maxBytes ?? DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES));
953
+ const meta = await getImageMetadata(buffer);
954
+ const width = Number(meta?.width ?? 0);
955
+ const height = Number(meta?.height ?? 0);
956
+ const maxDim = Math.max(width, height);
957
+ if (buffer.byteLength <= maxBytes && (maxDim === 0 || width <= maxSide && height <= maxSide)) {
958
+ return { buffer };
959
+ }
960
+ const sideStart = maxDim > 0 ? Math.min(maxSide, maxDim) : maxSide;
961
+ const sideGrid = buildImageResizeSideGrid(maxSide, sideStart);
962
+ let smallest = null;
963
+ for (const side of sideGrid) {
964
+ for (const quality of IMAGE_REDUCE_QUALITY_STEPS) {
965
+ const out = await resizeToJpeg({
966
+ buffer,
967
+ maxSide: side,
968
+ quality,
969
+ withoutEnlargement: true
970
+ });
971
+ if (!smallest || out.byteLength < smallest.size) {
972
+ smallest = { buffer: out, size: out.byteLength };
973
+ }
974
+ if (out.byteLength <= maxBytes) {
975
+ return { buffer: out, contentType: "image/jpeg" };
976
+ }
977
+ }
978
+ }
979
+ const best = smallest?.buffer ?? buffer;
980
+ throw new Error(
981
+ `Browser screenshot could not be reduced below ${(maxBytes / (1024 * 1024)).toFixed(0)}MB (got ${(best.byteLength / (1024 * 1024)).toFixed(2)}MB)`
982
+ );
983
+ }
984
+
985
+ // src/browser/routes/agent.snapshot.ts
986
+ async function saveBrowserMediaResponse(params) {
987
+ await ensureMediaDir();
988
+ const saved = await saveMediaBuffer(
989
+ params.buffer,
990
+ params.contentType,
991
+ "browser",
992
+ params.maxBytes
993
+ );
994
+ params.res.json({
995
+ ok: true,
996
+ path: path3.resolve(saved.path),
997
+ targetId: params.targetId,
998
+ url: params.url
999
+ });
1000
+ }
1001
+ function registerBrowserAgentSnapshotRoutes(app, ctx) {
1002
+ app.post("/navigate", async (req, res) => {
1003
+ const body = readBody(req);
1004
+ let url = toStringOrEmpty(body.url);
1005
+ const targetId = toStringOrEmpty(body.targetId) || void 0;
1006
+ if (!url) {
1007
+ return jsonError(res, 400, "url is required");
1008
+ }
1009
+ url = url.replace(/^(https?:\/\/)(?:www\.)?reddit\.com(\/|$)/, "$1old.reddit.com$2");
1010
+ await withPlaywrightRouteContext({
1011
+ req,
1012
+ res,
1013
+ ctx,
1014
+ targetId,
1015
+ feature: "navigate",
1016
+ run: async ({ cdpUrl, tab, pw }) => {
1017
+ const result = await pw.navigateViaPlaywright({
1018
+ cdpUrl,
1019
+ targetId: tab.targetId,
1020
+ url,
1021
+ ...withBrowserNavigationPolicy(ctx.state().resolved.ssrfPolicy)
1022
+ });
1023
+ res.json({ ok: true, targetId: tab.targetId, ...result });
1024
+ }
1025
+ });
1026
+ });
1027
+ app.post("/pdf", async (req, res) => {
1028
+ const body = readBody(req);
1029
+ const targetId = toStringOrEmpty(body.targetId) || void 0;
1030
+ await withPlaywrightRouteContext({
1031
+ req,
1032
+ res,
1033
+ ctx,
1034
+ targetId,
1035
+ feature: "pdf",
1036
+ run: async ({ cdpUrl, tab, pw }) => {
1037
+ const pdf = await pw.pdfViaPlaywright({
1038
+ cdpUrl,
1039
+ targetId: tab.targetId
1040
+ });
1041
+ await saveBrowserMediaResponse({
1042
+ res,
1043
+ buffer: pdf.buffer,
1044
+ contentType: "application/pdf",
1045
+ maxBytes: pdf.buffer.byteLength,
1046
+ targetId: tab.targetId,
1047
+ url: tab.url
1048
+ });
1049
+ }
1050
+ });
1051
+ });
1052
+ app.post("/screenshot", async (req, res) => {
1053
+ const body = readBody(req);
1054
+ const targetId = toStringOrEmpty(body.targetId) || void 0;
1055
+ const fullPage = toBoolean(body.fullPage) ?? false;
1056
+ const ref = toStringOrEmpty(body.ref) || void 0;
1057
+ const element = toStringOrEmpty(body.element) || void 0;
1058
+ const type = body.type === "jpeg" ? "jpeg" : "png";
1059
+ if (fullPage && (ref || element)) {
1060
+ return jsonError(res, 400, "fullPage is not supported for element screenshots");
1061
+ }
1062
+ await withRouteTabContext({
1063
+ req,
1064
+ res,
1065
+ ctx,
1066
+ targetId,
1067
+ run: async ({ profileCtx, tab, cdpUrl }) => {
1068
+ let buffer;
1069
+ const shouldUsePlaywright = profileCtx.profile.driver === "extension" || !tab.wsUrl || Boolean(ref) || Boolean(element);
1070
+ if (shouldUsePlaywright) {
1071
+ const pw = await requirePwAi(res, "screenshot");
1072
+ if (!pw) {
1073
+ return;
1074
+ }
1075
+ const snap = await pw.takeScreenshotViaPlaywright({
1076
+ cdpUrl,
1077
+ targetId: tab.targetId,
1078
+ ref,
1079
+ element,
1080
+ fullPage,
1081
+ type
1082
+ });
1083
+ buffer = snap.buffer;
1084
+ } else {
1085
+ buffer = await captureScreenshot({
1086
+ wsUrl: tab.wsUrl ?? "",
1087
+ fullPage,
1088
+ format: type,
1089
+ quality: type === "jpeg" ? 85 : void 0
1090
+ });
1091
+ }
1092
+ const normalized = await normalizeBrowserScreenshot(buffer, {
1093
+ maxSide: DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
1094
+ maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES
1095
+ });
1096
+ await saveBrowserMediaResponse({
1097
+ res,
1098
+ buffer: normalized.buffer,
1099
+ contentType: normalized.contentType ?? `image/${type}`,
1100
+ maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES,
1101
+ targetId: tab.targetId,
1102
+ url: tab.url
1103
+ });
1104
+ }
1105
+ });
1106
+ });
1107
+ app.get("/snapshot", async (req, res) => {
1108
+ const profileCtx = resolveProfileContext(req, res, ctx);
1109
+ if (!profileCtx) {
1110
+ return;
1111
+ }
1112
+ const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : "";
1113
+ const mode = req.query.mode === "efficient" ? "efficient" : void 0;
1114
+ const labels = toBoolean(req.query.labels) ?? void 0;
1115
+ const explicitFormat = req.query.format === "aria" ? "aria" : req.query.format === "ai" ? "ai" : void 0;
1116
+ const format = explicitFormat ?? (mode ? "ai" : await getPwAiModule2() ? "ai" : "aria");
1117
+ const limitRaw = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
1118
+ const hasMaxChars = Object.hasOwn(req.query, "maxChars");
1119
+ const maxCharsRaw = typeof req.query.maxChars === "string" ? Number(req.query.maxChars) : void 0;
1120
+ const limit = Number.isFinite(limitRaw) ? limitRaw : void 0;
1121
+ const maxChars = typeof maxCharsRaw === "number" && Number.isFinite(maxCharsRaw) && maxCharsRaw > 0 ? Math.floor(maxCharsRaw) : void 0;
1122
+ const resolvedMaxChars = format === "ai" ? hasMaxChars ? maxChars : mode === "efficient" ? DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS : DEFAULT_AI_SNAPSHOT_MAX_CHARS : void 0;
1123
+ const interactiveRaw = toBoolean(req.query.interactive);
1124
+ const compactRaw = toBoolean(req.query.compact);
1125
+ const depthRaw = toNumber(req.query.depth);
1126
+ const refsModeRaw = toStringOrEmpty(req.query.refs).trim();
1127
+ const refsMode = refsModeRaw === "aria" ? "aria" : refsModeRaw === "role" ? "role" : void 0;
1128
+ const interactive = interactiveRaw ?? (mode === "efficient" ? true : void 0);
1129
+ const compact = compactRaw ?? (mode === "efficient" ? true : void 0);
1130
+ const depth = depthRaw ?? (mode === "efficient" ? DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH : void 0);
1131
+ const selector = toStringOrEmpty(req.query.selector);
1132
+ const frameSelector = toStringOrEmpty(req.query.frame);
1133
+ const selectorValue = selector.trim() || void 0;
1134
+ const frameSelectorValue = frameSelector.trim() || void 0;
1135
+ try {
1136
+ const tab = await profileCtx.ensureTabAvailable(targetId || void 0);
1137
+ if ((labels || mode === "efficient") && format === "aria") {
1138
+ return jsonError(res, 400, "labels/mode=efficient require format=ai");
1139
+ }
1140
+ if (format === "ai") {
1141
+ const pw = await requirePwAi(res, "ai snapshot");
1142
+ if (!pw) {
1143
+ return;
1144
+ }
1145
+ const wantsRoleSnapshot = labels === true || mode === "efficient" || interactive === true || compact === true || depth !== void 0 || Boolean(selectorValue) || Boolean(frameSelectorValue);
1146
+ const roleSnapshotArgs = {
1147
+ cdpUrl: profileCtx.profile.cdpUrl,
1148
+ targetId: tab.targetId,
1149
+ selector: selectorValue,
1150
+ frameSelector: frameSelectorValue,
1151
+ refsMode,
1152
+ options: {
1153
+ interactive: interactive ?? void 0,
1154
+ compact: compact ?? void 0,
1155
+ maxDepth: depth ?? void 0
1156
+ }
1157
+ };
1158
+ const snap2 = wantsRoleSnapshot ? await pw.snapshotRoleViaPlaywright(roleSnapshotArgs) : await pw.snapshotAiViaPlaywright({
1159
+ cdpUrl: profileCtx.profile.cdpUrl,
1160
+ targetId: tab.targetId,
1161
+ ...typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}
1162
+ }).catch(async (err) => {
1163
+ if (String(err).toLowerCase().includes("_snapshotforai")) {
1164
+ return await pw.snapshotRoleViaPlaywright(roleSnapshotArgs);
1165
+ }
1166
+ throw err;
1167
+ });
1168
+ if (labels) {
1169
+ const labeled = await pw.screenshotWithLabelsViaPlaywright({
1170
+ cdpUrl: profileCtx.profile.cdpUrl,
1171
+ targetId: tab.targetId,
1172
+ refs: "refs" in snap2 ? snap2.refs : {},
1173
+ type: "png"
1174
+ });
1175
+ const normalized = await normalizeBrowserScreenshot(labeled.buffer, {
1176
+ maxSide: DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
1177
+ maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES
1178
+ });
1179
+ await ensureMediaDir();
1180
+ const saved = await saveMediaBuffer(
1181
+ normalized.buffer,
1182
+ normalized.contentType ?? "image/png",
1183
+ "browser",
1184
+ DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES
1185
+ );
1186
+ const imageType = normalized.contentType?.includes("jpeg") ? "jpeg" : "png";
1187
+ return res.json({
1188
+ ok: true,
1189
+ format,
1190
+ targetId: tab.targetId,
1191
+ url: tab.url,
1192
+ labels: true,
1193
+ labelsCount: labeled.labels,
1194
+ labelsSkipped: labeled.skipped,
1195
+ imagePath: path3.resolve(saved.path),
1196
+ imageType,
1197
+ ...snap2
1198
+ });
1199
+ }
1200
+ return res.json({
1201
+ ok: true,
1202
+ format,
1203
+ targetId: tab.targetId,
1204
+ url: tab.url,
1205
+ ...snap2
1206
+ });
1207
+ }
1208
+ const snap = profileCtx.profile.driver === "extension" || !tab.wsUrl ? (() => {
1209
+ return requirePwAi(res, "aria snapshot").then(async (pw) => {
1210
+ if (!pw) {
1211
+ return null;
1212
+ }
1213
+ return await pw.snapshotAriaViaPlaywright({
1214
+ cdpUrl: profileCtx.profile.cdpUrl,
1215
+ targetId: tab.targetId,
1216
+ limit
1217
+ });
1218
+ });
1219
+ })() : snapshotAria({ wsUrl: tab.wsUrl ?? "", limit });
1220
+ const resolved = await Promise.resolve(snap);
1221
+ if (!resolved) {
1222
+ return;
1223
+ }
1224
+ return res.json({
1225
+ ok: true,
1226
+ format,
1227
+ targetId: tab.targetId,
1228
+ url: tab.url,
1229
+ ...resolved
1230
+ });
1231
+ } catch (err) {
1232
+ handleRouteError(ctx, res, err);
1233
+ }
1234
+ });
1235
+ }
1236
+
1237
+ // src/browser/routes/agent.storage.ts
1238
+ function parseStorageKind(raw) {
1239
+ if (raw === "local" || raw === "session") {
1240
+ return raw;
1241
+ }
1242
+ return null;
1243
+ }
1244
+ function parseStorageMutationRequest(kindParam, body) {
1245
+ return {
1246
+ kind: parseStorageKind(toStringOrEmpty(kindParam)),
1247
+ targetId: resolveTargetIdFromBody(body)
1248
+ };
1249
+ }
1250
+ function parseRequiredStorageMutationRequest(kindParam, body) {
1251
+ const parsed = parseStorageMutationRequest(kindParam, body);
1252
+ if (!parsed.kind) {
1253
+ return null;
1254
+ }
1255
+ return {
1256
+ kind: parsed.kind,
1257
+ targetId: parsed.targetId
1258
+ };
1259
+ }
1260
+ function parseStorageMutationOrRespond(res, kindParam, body) {
1261
+ const parsed = parseRequiredStorageMutationRequest(kindParam, body);
1262
+ if (!parsed) {
1263
+ jsonError(res, 400, "kind must be local|session");
1264
+ return null;
1265
+ }
1266
+ return parsed;
1267
+ }
1268
+ function parseStorageMutationFromRequest(req, res) {
1269
+ const body = readBody(req);
1270
+ const parsed = parseStorageMutationOrRespond(res, req.params.kind, body);
1271
+ if (!parsed) {
1272
+ return null;
1273
+ }
1274
+ return { body, parsed };
1275
+ }
1276
+ function registerBrowserAgentStorageRoutes(app, ctx) {
1277
+ app.get("/cookies", async (req, res) => {
1278
+ const targetId = resolveTargetIdFromQuery(req.query);
1279
+ await withPlaywrightRouteContext({
1280
+ req,
1281
+ res,
1282
+ ctx,
1283
+ targetId,
1284
+ feature: "cookies",
1285
+ run: async ({ cdpUrl, tab, pw }) => {
1286
+ const result = await pw.cookiesGetViaPlaywright({
1287
+ cdpUrl,
1288
+ targetId: tab.targetId
1289
+ });
1290
+ res.json({ ok: true, targetId: tab.targetId, ...result });
1291
+ }
1292
+ });
1293
+ });
1294
+ app.post("/cookies/set", async (req, res) => {
1295
+ const body = readBody(req);
1296
+ const targetId = resolveTargetIdFromBody(body);
1297
+ const cookie = body.cookie && typeof body.cookie === "object" && !Array.isArray(body.cookie) ? body.cookie : null;
1298
+ if (!cookie) {
1299
+ return jsonError(res, 400, "cookie is required");
1300
+ }
1301
+ await withPlaywrightRouteContext({
1302
+ req,
1303
+ res,
1304
+ ctx,
1305
+ targetId,
1306
+ feature: "cookies set",
1307
+ run: async ({ cdpUrl, tab, pw }) => {
1308
+ await pw.cookiesSetViaPlaywright({
1309
+ cdpUrl,
1310
+ targetId: tab.targetId,
1311
+ cookie: {
1312
+ name: toStringOrEmpty(cookie.name),
1313
+ value: toStringOrEmpty(cookie.value),
1314
+ url: toStringOrEmpty(cookie.url) || void 0,
1315
+ domain: toStringOrEmpty(cookie.domain) || void 0,
1316
+ path: toStringOrEmpty(cookie.path) || void 0,
1317
+ expires: toNumber(cookie.expires) ?? void 0,
1318
+ httpOnly: toBoolean(cookie.httpOnly) ?? void 0,
1319
+ secure: toBoolean(cookie.secure) ?? void 0,
1320
+ sameSite: cookie.sameSite === "Lax" || cookie.sameSite === "None" || cookie.sameSite === "Strict" ? cookie.sameSite : void 0
1321
+ }
1322
+ });
1323
+ res.json({ ok: true, targetId: tab.targetId });
1324
+ }
1325
+ });
1326
+ });
1327
+ app.post("/cookies/clear", async (req, res) => {
1328
+ const body = readBody(req);
1329
+ const targetId = resolveTargetIdFromBody(body);
1330
+ await withPlaywrightRouteContext({
1331
+ req,
1332
+ res,
1333
+ ctx,
1334
+ targetId,
1335
+ feature: "cookies clear",
1336
+ run: async ({ cdpUrl, tab, pw }) => {
1337
+ await pw.cookiesClearViaPlaywright({
1338
+ cdpUrl,
1339
+ targetId: tab.targetId
1340
+ });
1341
+ res.json({ ok: true, targetId: tab.targetId });
1342
+ }
1343
+ });
1344
+ });
1345
+ app.get("/storage/:kind", async (req, res) => {
1346
+ const kind = parseStorageKind(toStringOrEmpty(req.params.kind));
1347
+ if (!kind) {
1348
+ return jsonError(res, 400, "kind must be local|session");
1349
+ }
1350
+ const targetId = resolveTargetIdFromQuery(req.query);
1351
+ const key = toStringOrEmpty(req.query.key);
1352
+ await withPlaywrightRouteContext({
1353
+ req,
1354
+ res,
1355
+ ctx,
1356
+ targetId,
1357
+ feature: "storage get",
1358
+ run: async ({ cdpUrl, tab, pw }) => {
1359
+ const result = await pw.storageGetViaPlaywright({
1360
+ cdpUrl,
1361
+ targetId: tab.targetId,
1362
+ kind,
1363
+ key: key.trim() || void 0
1364
+ });
1365
+ res.json({ ok: true, targetId: tab.targetId, ...result });
1366
+ }
1367
+ });
1368
+ });
1369
+ app.post("/storage/:kind/set", async (req, res) => {
1370
+ const mutation = parseStorageMutationFromRequest(req, res);
1371
+ if (!mutation) {
1372
+ return;
1373
+ }
1374
+ const key = toStringOrEmpty(mutation.body.key);
1375
+ if (!key) {
1376
+ return jsonError(res, 400, "key is required");
1377
+ }
1378
+ const value = typeof mutation.body.value === "string" ? mutation.body.value : "";
1379
+ await withPlaywrightRouteContext({
1380
+ req,
1381
+ res,
1382
+ ctx,
1383
+ targetId: mutation.parsed.targetId,
1384
+ feature: "storage set",
1385
+ run: async ({ cdpUrl, tab, pw }) => {
1386
+ await pw.storageSetViaPlaywright({
1387
+ cdpUrl,
1388
+ targetId: tab.targetId,
1389
+ kind: mutation.parsed.kind,
1390
+ key,
1391
+ value
1392
+ });
1393
+ res.json({ ok: true, targetId: tab.targetId });
1394
+ }
1395
+ });
1396
+ });
1397
+ app.post("/storage/:kind/clear", async (req, res) => {
1398
+ const mutation = parseStorageMutationFromRequest(req, res);
1399
+ if (!mutation) {
1400
+ return;
1401
+ }
1402
+ await withPlaywrightRouteContext({
1403
+ req,
1404
+ res,
1405
+ ctx,
1406
+ targetId: mutation.parsed.targetId,
1407
+ feature: "storage clear",
1408
+ run: async ({ cdpUrl, tab, pw }) => {
1409
+ await pw.storageClearViaPlaywright({
1410
+ cdpUrl,
1411
+ targetId: tab.targetId,
1412
+ kind: mutation.parsed.kind
1413
+ });
1414
+ res.json({ ok: true, targetId: tab.targetId });
1415
+ }
1416
+ });
1417
+ });
1418
+ app.post("/set/offline", async (req, res) => {
1419
+ const body = readBody(req);
1420
+ const targetId = resolveTargetIdFromBody(body);
1421
+ const offline = toBoolean(body.offline);
1422
+ if (offline === void 0) {
1423
+ return jsonError(res, 400, "offline is required");
1424
+ }
1425
+ await withPlaywrightRouteContext({
1426
+ req,
1427
+ res,
1428
+ ctx,
1429
+ targetId,
1430
+ feature: "offline",
1431
+ run: async ({ cdpUrl, tab, pw }) => {
1432
+ await pw.setOfflineViaPlaywright({
1433
+ cdpUrl,
1434
+ targetId: tab.targetId,
1435
+ offline
1436
+ });
1437
+ res.json({ ok: true, targetId: tab.targetId });
1438
+ }
1439
+ });
1440
+ });
1441
+ app.post("/set/headers", async (req, res) => {
1442
+ const body = readBody(req);
1443
+ const targetId = resolveTargetIdFromBody(body);
1444
+ const headers = body.headers && typeof body.headers === "object" && !Array.isArray(body.headers) ? body.headers : null;
1445
+ if (!headers) {
1446
+ return jsonError(res, 400, "headers is required");
1447
+ }
1448
+ const parsed = {};
1449
+ for (const [k, v] of Object.entries(headers)) {
1450
+ if (typeof v === "string") {
1451
+ parsed[k] = v;
1452
+ }
1453
+ }
1454
+ await withPlaywrightRouteContext({
1455
+ req,
1456
+ res,
1457
+ ctx,
1458
+ targetId,
1459
+ feature: "headers",
1460
+ run: async ({ cdpUrl, tab, pw }) => {
1461
+ await pw.setExtraHTTPHeadersViaPlaywright({
1462
+ cdpUrl,
1463
+ targetId: tab.targetId,
1464
+ headers: parsed
1465
+ });
1466
+ res.json({ ok: true, targetId: tab.targetId });
1467
+ }
1468
+ });
1469
+ });
1470
+ app.post("/set/credentials", async (req, res) => {
1471
+ const body = readBody(req);
1472
+ const targetId = resolveTargetIdFromBody(body);
1473
+ const clear = toBoolean(body.clear) ?? false;
1474
+ const username = toStringOrEmpty(body.username) || void 0;
1475
+ const password = typeof body.password === "string" ? body.password : void 0;
1476
+ await withPlaywrightRouteContext({
1477
+ req,
1478
+ res,
1479
+ ctx,
1480
+ targetId,
1481
+ feature: "http credentials",
1482
+ run: async ({ cdpUrl, tab, pw }) => {
1483
+ await pw.setHttpCredentialsViaPlaywright({
1484
+ cdpUrl,
1485
+ targetId: tab.targetId,
1486
+ username,
1487
+ password,
1488
+ clear
1489
+ });
1490
+ res.json({ ok: true, targetId: tab.targetId });
1491
+ }
1492
+ });
1493
+ });
1494
+ app.post("/set/geolocation", async (req, res) => {
1495
+ const body = readBody(req);
1496
+ const targetId = resolveTargetIdFromBody(body);
1497
+ const clear = toBoolean(body.clear) ?? false;
1498
+ const latitude = toNumber(body.latitude);
1499
+ const longitude = toNumber(body.longitude);
1500
+ const accuracy = toNumber(body.accuracy) ?? void 0;
1501
+ const origin = toStringOrEmpty(body.origin) || void 0;
1502
+ await withPlaywrightRouteContext({
1503
+ req,
1504
+ res,
1505
+ ctx,
1506
+ targetId,
1507
+ feature: "geolocation",
1508
+ run: async ({ cdpUrl, tab, pw }) => {
1509
+ await pw.setGeolocationViaPlaywright({
1510
+ cdpUrl,
1511
+ targetId: tab.targetId,
1512
+ latitude,
1513
+ longitude,
1514
+ accuracy,
1515
+ origin,
1516
+ clear
1517
+ });
1518
+ res.json({ ok: true, targetId: tab.targetId });
1519
+ }
1520
+ });
1521
+ });
1522
+ app.post("/set/media", async (req, res) => {
1523
+ const body = readBody(req);
1524
+ const targetId = resolveTargetIdFromBody(body);
1525
+ const schemeRaw = toStringOrEmpty(body.colorScheme);
1526
+ const colorScheme = schemeRaw === "dark" || schemeRaw === "light" || schemeRaw === "no-preference" ? schemeRaw : schemeRaw === "none" ? null : void 0;
1527
+ if (colorScheme === void 0) {
1528
+ return jsonError(res, 400, "colorScheme must be dark|light|no-preference|none");
1529
+ }
1530
+ await withPlaywrightRouteContext({
1531
+ req,
1532
+ res,
1533
+ ctx,
1534
+ targetId,
1535
+ feature: "media emulation",
1536
+ run: async ({ cdpUrl, tab, pw }) => {
1537
+ await pw.emulateMediaViaPlaywright({
1538
+ cdpUrl,
1539
+ targetId: tab.targetId,
1540
+ colorScheme
1541
+ });
1542
+ res.json({ ok: true, targetId: tab.targetId });
1543
+ }
1544
+ });
1545
+ });
1546
+ app.post("/set/timezone", async (req, res) => {
1547
+ const body = readBody(req);
1548
+ const targetId = resolveTargetIdFromBody(body);
1549
+ const timezoneId = toStringOrEmpty(body.timezoneId);
1550
+ if (!timezoneId) {
1551
+ return jsonError(res, 400, "timezoneId is required");
1552
+ }
1553
+ await withPlaywrightRouteContext({
1554
+ req,
1555
+ res,
1556
+ ctx,
1557
+ targetId,
1558
+ feature: "timezone",
1559
+ run: async ({ cdpUrl, tab, pw }) => {
1560
+ await pw.setTimezoneViaPlaywright({
1561
+ cdpUrl,
1562
+ targetId: tab.targetId,
1563
+ timezoneId
1564
+ });
1565
+ res.json({ ok: true, targetId: tab.targetId });
1566
+ }
1567
+ });
1568
+ });
1569
+ app.post("/set/locale", async (req, res) => {
1570
+ const body = readBody(req);
1571
+ const targetId = resolveTargetIdFromBody(body);
1572
+ const locale = toStringOrEmpty(body.locale);
1573
+ if (!locale) {
1574
+ return jsonError(res, 400, "locale is required");
1575
+ }
1576
+ await withPlaywrightRouteContext({
1577
+ req,
1578
+ res,
1579
+ ctx,
1580
+ targetId,
1581
+ feature: "locale",
1582
+ run: async ({ cdpUrl, tab, pw }) => {
1583
+ await pw.setLocaleViaPlaywright({
1584
+ cdpUrl,
1585
+ targetId: tab.targetId,
1586
+ locale
1587
+ });
1588
+ res.json({ ok: true, targetId: tab.targetId });
1589
+ }
1590
+ });
1591
+ });
1592
+ app.post("/set/device", async (req, res) => {
1593
+ const body = readBody(req);
1594
+ const targetId = resolveTargetIdFromBody(body);
1595
+ const name = toStringOrEmpty(body.name);
1596
+ if (!name) {
1597
+ return jsonError(res, 400, "name is required");
1598
+ }
1599
+ await withPlaywrightRouteContext({
1600
+ req,
1601
+ res,
1602
+ ctx,
1603
+ targetId,
1604
+ feature: "device emulation",
1605
+ run: async ({ cdpUrl, tab, pw }) => {
1606
+ await pw.setDeviceViaPlaywright({
1607
+ cdpUrl,
1608
+ targetId: tab.targetId,
1609
+ name
1610
+ });
1611
+ res.json({ ok: true, targetId: tab.targetId });
1612
+ }
1613
+ });
1614
+ });
1615
+ }
1616
+
1617
+ // src/browser/routes/agent.ts
1618
+ function registerBrowserAgentRoutes(app, ctx) {
1619
+ registerBrowserAgentSnapshotRoutes(app, ctx);
1620
+ registerBrowserAgentActRoutes(app, ctx);
1621
+ registerBrowserAgentDebugRoutes(app, ctx);
1622
+ registerBrowserAgentStorageRoutes(app, ctx);
1623
+ }
1624
+
1625
+ // src/browser/profiles-service.ts
1626
+ import fs2 from "fs";
1627
+ import path4 from "path";
1628
+ var HEX_COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
1629
+ function createBrowserProfilesService(ctx) {
1630
+ const listProfiles = async () => {
1631
+ return await ctx.listProfiles();
1632
+ };
1633
+ const createProfile = async (params) => {
1634
+ const name = params.name.trim();
1635
+ const rawCdpUrl = params.cdpUrl?.trim() || void 0;
1636
+ const driver = params.driver === "extension" ? "extension" : void 0;
1637
+ if (!isValidProfileName(name)) {
1638
+ throw new Error("invalid profile name: use lowercase letters, numbers, and hyphens only");
1639
+ }
1640
+ const state = ctx.state();
1641
+ const resolvedProfiles = state.resolved.profiles;
1642
+ if (name in resolvedProfiles) {
1643
+ throw new Error(`profile "${name}" already exists`);
1644
+ }
1645
+ const cfg = loadConfig();
1646
+ const rawProfiles = cfg.browser?.profiles ?? {};
1647
+ if (name in rawProfiles) {
1648
+ throw new Error(`profile "${name}" already exists`);
1649
+ }
1650
+ const usedColors = getUsedColors(resolvedProfiles);
1651
+ const profileColor = params.color && HEX_COLOR_RE.test(params.color) ? params.color : allocateColor(usedColors);
1652
+ let profileConfig;
1653
+ if (rawCdpUrl) {
1654
+ const parsed = parseHttpUrl(rawCdpUrl, "browser.profiles.cdpUrl");
1655
+ profileConfig = {
1656
+ cdpUrl: parsed.normalized,
1657
+ ...driver ? { driver } : {},
1658
+ color: profileColor
1659
+ };
1660
+ } else {
1661
+ const usedPorts = getUsedPorts(resolvedProfiles);
1662
+ const range = deriveDefaultBrowserCdpPortRange(state.resolved.controlPort);
1663
+ const cdpPort = allocateCdpPort(usedPorts, range);
1664
+ if (cdpPort === null) {
1665
+ throw new Error("no available CDP ports in range");
1666
+ }
1667
+ profileConfig = {
1668
+ cdpPort,
1669
+ ...driver ? { driver } : {},
1670
+ color: profileColor
1671
+ };
1672
+ }
1673
+ const nextConfig = {
1674
+ ...cfg,
1675
+ browser: {
1676
+ ...cfg.browser,
1677
+ profiles: {
1678
+ ...rawProfiles,
1679
+ [name]: profileConfig
1680
+ }
1681
+ }
1682
+ };
1683
+ await writeConfigFile(nextConfig);
1684
+ state.resolved.profiles[name] = profileConfig;
1685
+ const resolved = resolveProfile(state.resolved, name);
1686
+ if (!resolved) {
1687
+ throw new Error(`profile "${name}" not found after creation`);
1688
+ }
1689
+ return {
1690
+ ok: true,
1691
+ profile: name,
1692
+ cdpPort: resolved.cdpPort,
1693
+ cdpUrl: resolved.cdpUrl,
1694
+ color: resolved.color,
1695
+ isRemote: !resolved.cdpIsLoopback
1696
+ };
1697
+ };
1698
+ const deleteProfile = async (nameRaw) => {
1699
+ const name = nameRaw.trim();
1700
+ if (!name) {
1701
+ throw new Error("profile name is required");
1702
+ }
1703
+ if (!isValidProfileName(name)) {
1704
+ throw new Error("invalid profile name");
1705
+ }
1706
+ const cfg = loadConfig();
1707
+ const profiles = cfg.browser?.profiles ?? {};
1708
+ if (!(name in profiles)) {
1709
+ throw new Error(`profile "${name}" not found`);
1710
+ }
1711
+ const defaultProfile = cfg.browser?.defaultProfile ?? DEFAULT_BROWSER_DEFAULT_PROFILE_NAME;
1712
+ if (name === defaultProfile) {
1713
+ throw new Error(
1714
+ `cannot delete the default profile "${name}"; change browser.defaultProfile first`
1715
+ );
1716
+ }
1717
+ let deleted = false;
1718
+ const state = ctx.state();
1719
+ const resolved = resolveProfile(state.resolved, name);
1720
+ if (resolved?.cdpIsLoopback) {
1721
+ try {
1722
+ await ctx.forProfile(name).stopRunningBrowser();
1723
+ } catch {
1724
+ }
1725
+ const userDataDir = resolveAgenticMailUserDataDir(name);
1726
+ const profileDir = path4.dirname(userDataDir);
1727
+ if (fs2.existsSync(profileDir)) {
1728
+ await movePathToTrash(profileDir);
1729
+ deleted = true;
1730
+ }
1731
+ }
1732
+ const { [name]: _removed, ...remainingProfiles } = profiles;
1733
+ const nextConfig = {
1734
+ ...cfg,
1735
+ browser: {
1736
+ ...cfg.browser,
1737
+ profiles: remainingProfiles
1738
+ }
1739
+ };
1740
+ await writeConfigFile(nextConfig);
1741
+ delete state.resolved.profiles[name];
1742
+ state.profiles.delete(name);
1743
+ return { ok: true, profile: name, deleted };
1744
+ };
1745
+ return {
1746
+ listProfiles,
1747
+ createProfile,
1748
+ deleteProfile
1749
+ };
1750
+ }
1751
+
1752
+ // src/browser/routes/basic.ts
1753
+ async function withBasicProfileRoute(params) {
1754
+ const profileCtx = resolveProfileContext(params.req, params.res, params.ctx);
1755
+ if (!profileCtx) {
1756
+ return;
1757
+ }
1758
+ try {
1759
+ await params.run(profileCtx);
1760
+ } catch (err) {
1761
+ jsonError(params.res, 500, String(err));
1762
+ }
1763
+ }
1764
+ function registerBrowserBasicRoutes(app, ctx) {
1765
+ app.get("/profiles", async (_req, res) => {
1766
+ try {
1767
+ const service = createBrowserProfilesService(ctx);
1768
+ const profiles = await service.listProfiles();
1769
+ res.json({ profiles });
1770
+ } catch (err) {
1771
+ jsonError(res, 500, String(err));
1772
+ }
1773
+ });
1774
+ app.get("/", async (req, res) => {
1775
+ let current;
1776
+ try {
1777
+ current = ctx.state();
1778
+ } catch {
1779
+ return jsonError(res, 503, "browser server not started");
1780
+ }
1781
+ const profileCtx = getProfileContext(req, ctx);
1782
+ if ("error" in profileCtx) {
1783
+ return jsonError(res, profileCtx.status, profileCtx.error);
1784
+ }
1785
+ const [cdpHttp, cdpReady] = await Promise.all([
1786
+ profileCtx.isHttpReachable(300),
1787
+ profileCtx.isReachable(600)
1788
+ ]);
1789
+ const profileState = current.profiles.get(profileCtx.profile.name);
1790
+ let detectedBrowser = null;
1791
+ let detectedExecutablePath = null;
1792
+ let detectError = null;
1793
+ try {
1794
+ const detected = resolveBrowserExecutableForPlatform(current.resolved, process.platform);
1795
+ if (detected) {
1796
+ detectedBrowser = detected.kind;
1797
+ detectedExecutablePath = detected.path;
1798
+ }
1799
+ } catch (err) {
1800
+ detectError = String(err);
1801
+ }
1802
+ res.json({
1803
+ enabled: current.resolved.enabled,
1804
+ profile: profileCtx.profile.name,
1805
+ running: cdpReady,
1806
+ cdpReady,
1807
+ cdpHttp,
1808
+ pid: profileState?.running?.pid ?? null,
1809
+ cdpPort: profileCtx.profile.cdpPort,
1810
+ cdpUrl: profileCtx.profile.cdpUrl,
1811
+ chosenBrowser: profileState?.running?.exe.kind ?? null,
1812
+ detectedBrowser,
1813
+ detectedExecutablePath,
1814
+ detectError,
1815
+ userDataDir: profileState?.running?.userDataDir ?? null,
1816
+ color: profileCtx.profile.color,
1817
+ headless: current.resolved.headless,
1818
+ noSandbox: current.resolved.noSandbox,
1819
+ executablePath: current.resolved.executablePath ?? null,
1820
+ attachOnly: current.resolved.attachOnly
1821
+ });
1822
+ });
1823
+ app.post("/start", async (req, res) => {
1824
+ await withBasicProfileRoute({
1825
+ req,
1826
+ res,
1827
+ ctx,
1828
+ run: async (profileCtx) => {
1829
+ await profileCtx.ensureBrowserAvailable();
1830
+ res.json({ ok: true, profile: profileCtx.profile.name });
1831
+ }
1832
+ });
1833
+ });
1834
+ app.post("/stop", async (req, res) => {
1835
+ await withBasicProfileRoute({
1836
+ req,
1837
+ res,
1838
+ ctx,
1839
+ run: async (profileCtx) => {
1840
+ const result = await profileCtx.stopRunningBrowser();
1841
+ res.json({
1842
+ ok: true,
1843
+ stopped: result.stopped,
1844
+ profile: profileCtx.profile.name
1845
+ });
1846
+ }
1847
+ });
1848
+ });
1849
+ app.post("/reset-profile", async (req, res) => {
1850
+ await withBasicProfileRoute({
1851
+ req,
1852
+ res,
1853
+ ctx,
1854
+ run: async (profileCtx) => {
1855
+ const result = await profileCtx.resetProfile();
1856
+ res.json({ ok: true, profile: profileCtx.profile.name, ...result });
1857
+ }
1858
+ });
1859
+ });
1860
+ app.post("/profiles/create", async (req, res) => {
1861
+ const name = toStringOrEmpty(req.body?.name);
1862
+ const color = toStringOrEmpty(req.body?.color);
1863
+ const cdpUrl = toStringOrEmpty(req.body?.cdpUrl);
1864
+ const driver = toStringOrEmpty(req.body?.driver);
1865
+ if (!name) {
1866
+ return jsonError(res, 400, "name is required");
1867
+ }
1868
+ try {
1869
+ const service = createBrowserProfilesService(ctx);
1870
+ const result = await service.createProfile({
1871
+ name,
1872
+ color: color || void 0,
1873
+ cdpUrl: cdpUrl || void 0,
1874
+ driver: driver === "extension" ? "extension" : void 0
1875
+ });
1876
+ res.json(result);
1877
+ } catch (err) {
1878
+ const msg = String(err);
1879
+ if (msg.includes("already exists")) {
1880
+ return jsonError(res, 409, msg);
1881
+ }
1882
+ if (msg.includes("invalid profile name")) {
1883
+ return jsonError(res, 400, msg);
1884
+ }
1885
+ if (msg.includes("no available CDP ports")) {
1886
+ return jsonError(res, 507, msg);
1887
+ }
1888
+ if (msg.includes("cdpUrl")) {
1889
+ return jsonError(res, 400, msg);
1890
+ }
1891
+ jsonError(res, 500, msg);
1892
+ }
1893
+ });
1894
+ app.delete("/profiles/:name", async (req, res) => {
1895
+ const name = toStringOrEmpty(req.params.name);
1896
+ if (!name) {
1897
+ return jsonError(res, 400, "profile name is required");
1898
+ }
1899
+ try {
1900
+ const service = createBrowserProfilesService(ctx);
1901
+ const result = await service.deleteProfile(name);
1902
+ res.json(result);
1903
+ } catch (err) {
1904
+ const msg = String(err);
1905
+ if (msg.includes("invalid profile name")) {
1906
+ return jsonError(res, 400, msg);
1907
+ }
1908
+ if (msg.includes("default profile")) {
1909
+ return jsonError(res, 400, msg);
1910
+ }
1911
+ if (msg.includes("not found")) {
1912
+ return jsonError(res, 404, msg);
1913
+ }
1914
+ jsonError(res, 500, msg);
1915
+ }
1916
+ });
1917
+ }
1918
+
1919
+ // src/browser/routes/tabs.ts
1920
+ function resolveTabsProfileContext(req, res, ctx) {
1921
+ const profileCtx = getProfileContext(req, ctx);
1922
+ if ("error" in profileCtx) {
1923
+ jsonError(res, profileCtx.status, profileCtx.error);
1924
+ return null;
1925
+ }
1926
+ return profileCtx;
1927
+ }
1928
+ function handleTabsRouteError(ctx, res, err, opts) {
1929
+ if (opts?.mapTabError) {
1930
+ const mapped = ctx.mapTabError(err);
1931
+ if (mapped) {
1932
+ return jsonError(res, mapped.status, mapped.message);
1933
+ }
1934
+ }
1935
+ return jsonError(res, 500, String(err));
1936
+ }
1937
+ async function withTabsProfileRoute(params) {
1938
+ const profileCtx = resolveTabsProfileContext(params.req, params.res, params.ctx);
1939
+ if (!profileCtx) {
1940
+ return;
1941
+ }
1942
+ try {
1943
+ await params.run(profileCtx);
1944
+ } catch (err) {
1945
+ handleTabsRouteError(params.ctx, params.res, err, { mapTabError: params.mapTabError });
1946
+ }
1947
+ }
1948
+ async function ensureBrowserRunning(profileCtx, res) {
1949
+ if (!await profileCtx.isReachable(300)) {
1950
+ jsonError(res, 409, "browser not running");
1951
+ return false;
1952
+ }
1953
+ return true;
1954
+ }
1955
+ function resolveIndexedTab(tabs, index) {
1956
+ return typeof index === "number" ? tabs[index] : tabs.at(0);
1957
+ }
1958
+ function parseRequiredTargetId(res, rawTargetId) {
1959
+ const targetId = toStringOrEmpty(rawTargetId);
1960
+ if (!targetId) {
1961
+ jsonError(res, 400, "targetId is required");
1962
+ return null;
1963
+ }
1964
+ return targetId;
1965
+ }
1966
+ async function runTabTargetMutation(params) {
1967
+ await withTabsProfileRoute({
1968
+ req: params.req,
1969
+ res: params.res,
1970
+ ctx: params.ctx,
1971
+ mapTabError: true,
1972
+ run: async (profileCtx) => {
1973
+ if (!await ensureBrowserRunning(profileCtx, params.res)) {
1974
+ return;
1975
+ }
1976
+ await params.mutate(profileCtx, params.targetId);
1977
+ params.res.json({ ok: true });
1978
+ }
1979
+ });
1980
+ }
1981
+ function registerBrowserTabRoutes(app, ctx) {
1982
+ app.get("/tabs", async (req, res) => {
1983
+ await withTabsProfileRoute({
1984
+ req,
1985
+ res,
1986
+ ctx,
1987
+ run: async (profileCtx) => {
1988
+ const reachable = await profileCtx.isReachable(300);
1989
+ if (!reachable) {
1990
+ return res.json({ running: false, tabs: [] });
1991
+ }
1992
+ const tabs = await profileCtx.listTabs();
1993
+ res.json({ running: true, tabs });
1994
+ }
1995
+ });
1996
+ });
1997
+ app.post("/tabs/open", async (req, res) => {
1998
+ const url = toStringOrEmpty(req.body?.url);
1999
+ if (!url) {
2000
+ return jsonError(res, 400, "url is required");
2001
+ }
2002
+ await withTabsProfileRoute({
2003
+ req,
2004
+ res,
2005
+ ctx,
2006
+ mapTabError: true,
2007
+ run: async (profileCtx) => {
2008
+ await profileCtx.ensureBrowserAvailable();
2009
+ const tab = await profileCtx.openTab(url);
2010
+ res.json(tab);
2011
+ }
2012
+ });
2013
+ });
2014
+ app.post("/tabs/focus", async (req, res) => {
2015
+ const targetId = parseRequiredTargetId(res, req.body?.targetId);
2016
+ if (!targetId) {
2017
+ return;
2018
+ }
2019
+ await runTabTargetMutation({
2020
+ req,
2021
+ res,
2022
+ ctx,
2023
+ targetId,
2024
+ mutate: async (profileCtx, id) => {
2025
+ await profileCtx.focusTab(id);
2026
+ }
2027
+ });
2028
+ });
2029
+ app.delete("/tabs/:targetId", async (req, res) => {
2030
+ const targetId = parseRequiredTargetId(res, req.params.targetId);
2031
+ if (!targetId) {
2032
+ return;
2033
+ }
2034
+ await runTabTargetMutation({
2035
+ req,
2036
+ res,
2037
+ ctx,
2038
+ targetId,
2039
+ mutate: async (profileCtx, id) => {
2040
+ await profileCtx.closeTab(id);
2041
+ }
2042
+ });
2043
+ });
2044
+ app.post("/tabs/action", async (req, res) => {
2045
+ const action = toStringOrEmpty(req.body?.action);
2046
+ const index = toNumber(req.body?.index);
2047
+ await withTabsProfileRoute({
2048
+ req,
2049
+ res,
2050
+ ctx,
2051
+ mapTabError: true,
2052
+ run: async (profileCtx) => {
2053
+ if (action === "list") {
2054
+ const reachable = await profileCtx.isReachable(300);
2055
+ if (!reachable) {
2056
+ return res.json({ ok: true, tabs: [] });
2057
+ }
2058
+ const tabs = await profileCtx.listTabs();
2059
+ return res.json({ ok: true, tabs });
2060
+ }
2061
+ if (action === "new") {
2062
+ await profileCtx.ensureBrowserAvailable();
2063
+ const tab = await profileCtx.openTab("about:blank");
2064
+ return res.json({ ok: true, tab });
2065
+ }
2066
+ if (action === "close") {
2067
+ const tabs = await profileCtx.listTabs();
2068
+ const target = resolveIndexedTab(tabs, index);
2069
+ if (!target) {
2070
+ return jsonError(res, 404, "tab not found");
2071
+ }
2072
+ await profileCtx.closeTab(target.targetId);
2073
+ return res.json({ ok: true, targetId: target.targetId });
2074
+ }
2075
+ if (action === "select") {
2076
+ if (typeof index !== "number") {
2077
+ return jsonError(res, 400, "index is required");
2078
+ }
2079
+ const tabs = await profileCtx.listTabs();
2080
+ const target = tabs[index];
2081
+ if (!target) {
2082
+ return jsonError(res, 404, "tab not found");
2083
+ }
2084
+ await profileCtx.focusTab(target.targetId);
2085
+ return res.json({ ok: true, targetId: target.targetId });
2086
+ }
2087
+ return jsonError(res, 400, "unknown tab action");
2088
+ }
2089
+ });
2090
+ });
2091
+ }
2092
+
2093
+ // src/browser/routes/index.ts
2094
+ function registerBrowserRoutes(app, ctx) {
2095
+ registerBrowserBasicRoutes(app, ctx);
2096
+ registerBrowserTabRoutes(app, ctx);
2097
+ registerBrowserAgentRoutes(app, ctx);
2098
+ }
2099
+
2100
+ export {
2101
+ registerBrowserRoutes
2102
+ };