@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,1961 @@
1
+ import {
2
+ CONFIG_DIR,
3
+ createSubsystemLogger,
4
+ ensurePortAvailable,
5
+ isLoopbackAddress,
6
+ isLoopbackHost,
7
+ loadConfig,
8
+ rawDataToString,
9
+ resolvePinnedHostnameWithPolicy
10
+ } from "./chunk-A3PUJDNH.js";
11
+
12
+ // src/browser/constants.ts
13
+ var DEFAULT_AGENTICMAIL_BROWSER_ENABLED = true;
14
+ var DEFAULT_BROWSER_EVALUATE_ENABLED = true;
15
+ var DEFAULT_AGENTICMAIL_BROWSER_COLOR = "#E91E8C";
16
+ var DEFAULT_AGENTICMAIL_BROWSER_PROFILE_NAME = "AgenticMail";
17
+ var DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "chrome";
18
+ var DEFAULT_AI_SNAPSHOT_MAX_CHARS = 16e3;
19
+ var DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS = 1e4;
20
+ var DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH = 6;
21
+
22
+ // src/browser/extension-relay.ts
23
+ import { createServer } from "http";
24
+ import WebSocket, { WebSocketServer } from "ws";
25
+ var RELAY_AUTH_HEADER = "x-agenticmail-relay-token";
26
+ function headerValue(value) {
27
+ if (!value) {
28
+ return void 0;
29
+ }
30
+ if (Array.isArray(value)) {
31
+ return value[0];
32
+ }
33
+ return value;
34
+ }
35
+ function getHeader(req, name) {
36
+ return headerValue(req.headers[name.toLowerCase()]);
37
+ }
38
+ function getRelayAuthTokenFromRequest(req, url) {
39
+ const headerToken = getHeader(req, RELAY_AUTH_HEADER)?.trim();
40
+ if (headerToken) {
41
+ return headerToken;
42
+ }
43
+ const queryToken = url?.searchParams.get("token")?.trim();
44
+ if (queryToken) {
45
+ return queryToken;
46
+ }
47
+ return void 0;
48
+ }
49
+ function parseBaseUrl(raw) {
50
+ const parsed = new URL(raw.trim().replace(/\/$/, ""));
51
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
52
+ throw new Error(`extension relay cdpUrl must be http(s), got ${parsed.protocol}`);
53
+ }
54
+ const host = parsed.hostname;
55
+ const port = parsed.port?.trim() !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
56
+ if (!Number.isFinite(port) || port <= 0 || port > 65535) {
57
+ throw new Error(`extension relay cdpUrl has invalid port: ${parsed.port || "(empty)"}`);
58
+ }
59
+ return { host, port, baseUrl: parsed.toString().replace(/\/$/, "") };
60
+ }
61
+ function text(res, status, bodyText) {
62
+ const body = Buffer.from(bodyText);
63
+ res.write(
64
+ `HTTP/1.1 ${status} ${status === 200 ? "OK" : "ERR"}\r
65
+ Content-Type: text/plain; charset=utf-8\r
66
+ Content-Length: ${body.length}\r
67
+ Connection: close\r
68
+ \r
69
+ `
70
+ );
71
+ res.write(body);
72
+ res.end();
73
+ }
74
+ function rejectUpgrade(socket, status, bodyText) {
75
+ text(socket, status, bodyText);
76
+ try {
77
+ socket.destroy();
78
+ } catch {
79
+ }
80
+ }
81
+ var serversByPort = /* @__PURE__ */ new Map();
82
+ function resolveGatewayAuthToken() {
83
+ const envToken = process.env.AGENTICMAIL_GATEWAY_TOKEN?.trim() || process.env.CLAWDBOT_GATEWAY_TOKEN?.trim();
84
+ if (envToken) {
85
+ return envToken;
86
+ }
87
+ try {
88
+ const cfg = loadConfig();
89
+ const configToken = cfg.gateway?.auth?.token?.trim();
90
+ if (configToken) {
91
+ return configToken;
92
+ }
93
+ } catch {
94
+ }
95
+ return null;
96
+ }
97
+ function resolveRelayAuthToken() {
98
+ const gatewayToken = resolveGatewayAuthToken();
99
+ if (gatewayToken) {
100
+ return gatewayToken;
101
+ }
102
+ throw new Error(
103
+ "extension relay requires gateway auth token (set gateway.auth.token or AGENTICMAIL_GATEWAY_TOKEN)"
104
+ );
105
+ }
106
+ function isAddrInUseError(err) {
107
+ return typeof err === "object" && err !== null && "code" in err && err.code === "EADDRINUSE";
108
+ }
109
+ async function looksLikeAgenticMailRelay(baseUrl) {
110
+ const ctrl = new AbortController();
111
+ const timer = setTimeout(() => ctrl.abort(), 500);
112
+ try {
113
+ const statusUrl = new URL("/extension/status", `${baseUrl}/`).toString();
114
+ const res = await fetch(statusUrl, { signal: ctrl.signal });
115
+ if (!res.ok) {
116
+ return false;
117
+ }
118
+ const body = await res.json();
119
+ return typeof body.connected === "boolean";
120
+ } catch {
121
+ return false;
122
+ } finally {
123
+ clearTimeout(timer);
124
+ }
125
+ }
126
+ function relayAuthTokenForUrl(url) {
127
+ try {
128
+ const parsed = new URL(url);
129
+ if (!isLoopbackHost(parsed.hostname)) {
130
+ return null;
131
+ }
132
+ return resolveGatewayAuthToken();
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+ function getChromeExtensionRelayAuthHeaders(url) {
138
+ const token = relayAuthTokenForUrl(url);
139
+ if (!token) {
140
+ return {};
141
+ }
142
+ return { [RELAY_AUTH_HEADER]: token };
143
+ }
144
+ async function ensureChromeExtensionRelayServer(opts) {
145
+ const info = parseBaseUrl(opts.cdpUrl);
146
+ if (!isLoopbackHost(info.host)) {
147
+ throw new Error(`extension relay requires loopback cdpUrl host (got ${info.host})`);
148
+ }
149
+ const existing = serversByPort.get(info.port);
150
+ if (existing) {
151
+ return existing;
152
+ }
153
+ const relayAuthToken = resolveRelayAuthToken();
154
+ let extensionWs = null;
155
+ const cdpClients = /* @__PURE__ */ new Set();
156
+ const connectedTargets = /* @__PURE__ */ new Map();
157
+ const pendingExtension = /* @__PURE__ */ new Map();
158
+ let nextExtensionId = 1;
159
+ const sendToExtension = async (payload) => {
160
+ const ws = extensionWs;
161
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
162
+ throw new Error("Chrome extension not connected");
163
+ }
164
+ ws.send(JSON.stringify(payload));
165
+ return await new Promise((resolve, reject) => {
166
+ const timer = setTimeout(() => {
167
+ pendingExtension.delete(payload.id);
168
+ reject(new Error(`extension request timeout: ${payload.params.method}`));
169
+ }, 3e4);
170
+ pendingExtension.set(payload.id, { resolve, reject, timer });
171
+ });
172
+ };
173
+ const broadcastToCdpClients = (evt) => {
174
+ const msg = JSON.stringify(evt);
175
+ for (const ws of cdpClients) {
176
+ if (ws.readyState !== WebSocket.OPEN) {
177
+ continue;
178
+ }
179
+ ws.send(msg);
180
+ }
181
+ };
182
+ const sendResponseToCdp = (ws, res) => {
183
+ if (ws.readyState !== WebSocket.OPEN) {
184
+ return;
185
+ }
186
+ ws.send(JSON.stringify(res));
187
+ };
188
+ const ensureTargetEventsForClient = (ws, mode) => {
189
+ for (const target of connectedTargets.values()) {
190
+ if (mode === "autoAttach") {
191
+ ws.send(
192
+ JSON.stringify({
193
+ method: "Target.attachedToTarget",
194
+ params: {
195
+ sessionId: target.sessionId,
196
+ targetInfo: { ...target.targetInfo, attached: true },
197
+ waitingForDebugger: false
198
+ }
199
+ })
200
+ );
201
+ } else {
202
+ ws.send(
203
+ JSON.stringify({
204
+ method: "Target.targetCreated",
205
+ params: { targetInfo: { ...target.targetInfo, attached: true } }
206
+ })
207
+ );
208
+ }
209
+ }
210
+ };
211
+ const routeCdpCommand = async (cmd) => {
212
+ switch (cmd.method) {
213
+ case "Browser.getVersion":
214
+ return {
215
+ protocolVersion: "1.3",
216
+ product: "Chrome/AgenticMail-Extension-Relay",
217
+ revision: "0",
218
+ userAgent: "AgenticMail-Extension-Relay",
219
+ jsVersion: "V8"
220
+ };
221
+ case "Browser.setDownloadBehavior":
222
+ return {};
223
+ case "Target.setAutoAttach":
224
+ case "Target.setDiscoverTargets":
225
+ return {};
226
+ case "Target.getTargets":
227
+ return {
228
+ targetInfos: Array.from(connectedTargets.values()).map((t) => ({
229
+ ...t.targetInfo,
230
+ attached: true
231
+ }))
232
+ };
233
+ case "Target.getTargetInfo": {
234
+ const params = cmd.params ?? {};
235
+ const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
236
+ if (targetId) {
237
+ for (const t of connectedTargets.values()) {
238
+ if (t.targetId === targetId) {
239
+ return { targetInfo: t.targetInfo };
240
+ }
241
+ }
242
+ }
243
+ if (cmd.sessionId && connectedTargets.has(cmd.sessionId)) {
244
+ const t = connectedTargets.get(cmd.sessionId);
245
+ if (t) {
246
+ return { targetInfo: t.targetInfo };
247
+ }
248
+ }
249
+ const first = Array.from(connectedTargets.values())[0];
250
+ return { targetInfo: first?.targetInfo };
251
+ }
252
+ case "Target.attachToTarget": {
253
+ const params = cmd.params ?? {};
254
+ const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
255
+ if (!targetId) {
256
+ throw new Error("targetId required");
257
+ }
258
+ for (const t of connectedTargets.values()) {
259
+ if (t.targetId === targetId) {
260
+ return { sessionId: t.sessionId };
261
+ }
262
+ }
263
+ throw new Error("target not found");
264
+ }
265
+ default: {
266
+ const id = nextExtensionId++;
267
+ return await sendToExtension({
268
+ id,
269
+ method: "forwardCDPCommand",
270
+ params: {
271
+ method: cmd.method,
272
+ sessionId: cmd.sessionId,
273
+ params: cmd.params
274
+ }
275
+ });
276
+ }
277
+ }
278
+ };
279
+ const server = createServer((req, res) => {
280
+ const url = new URL(req.url ?? "/", info.baseUrl);
281
+ const path4 = url.pathname;
282
+ if (path4.startsWith("/json")) {
283
+ const token = getHeader(req, RELAY_AUTH_HEADER);
284
+ if (!token || token !== relayAuthToken) {
285
+ res.writeHead(401);
286
+ res.end("Unauthorized");
287
+ return;
288
+ }
289
+ }
290
+ if (req.method === "HEAD" && path4 === "/") {
291
+ res.writeHead(200);
292
+ res.end();
293
+ return;
294
+ }
295
+ if (path4 === "/") {
296
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
297
+ res.end("OK");
298
+ return;
299
+ }
300
+ if (path4 === "/extension/status") {
301
+ res.writeHead(200, { "Content-Type": "application/json" });
302
+ res.end(JSON.stringify({ connected: Boolean(extensionWs) }));
303
+ return;
304
+ }
305
+ const hostHeader = req.headers.host?.trim() || `${info.host}:${info.port}`;
306
+ const wsHost = `ws://${hostHeader}`;
307
+ const cdpWsUrl = `${wsHost}/cdp`;
308
+ if ((path4 === "/json/version" || path4 === "/json/version/") && (req.method === "GET" || req.method === "PUT")) {
309
+ const payload = {
310
+ Browser: "AgenticMail/extension-relay",
311
+ "Protocol-Version": "1.3"
312
+ };
313
+ if (extensionWs) {
314
+ payload.webSocketDebuggerUrl = cdpWsUrl;
315
+ }
316
+ res.writeHead(200, { "Content-Type": "application/json" });
317
+ res.end(JSON.stringify(payload));
318
+ return;
319
+ }
320
+ const listPaths = /* @__PURE__ */ new Set(["/json", "/json/", "/json/list", "/json/list/"]);
321
+ if (listPaths.has(path4) && (req.method === "GET" || req.method === "PUT")) {
322
+ const list = Array.from(connectedTargets.values()).map((t) => ({
323
+ id: t.targetId,
324
+ type: t.targetInfo.type ?? "page",
325
+ title: t.targetInfo.title ?? "",
326
+ description: t.targetInfo.title ?? "",
327
+ url: t.targetInfo.url ?? "",
328
+ webSocketDebuggerUrl: cdpWsUrl,
329
+ devtoolsFrontendUrl: `/devtools/inspector.html?ws=${cdpWsUrl.replace("ws://", "")}`
330
+ }));
331
+ res.writeHead(200, { "Content-Type": "application/json" });
332
+ res.end(JSON.stringify(list));
333
+ return;
334
+ }
335
+ const activateMatch = path4.match(/^\/json\/activate\/(.+)$/);
336
+ if (activateMatch && (req.method === "GET" || req.method === "PUT")) {
337
+ const targetId = decodeURIComponent(activateMatch[1] ?? "").trim();
338
+ if (!targetId) {
339
+ res.writeHead(400);
340
+ res.end("targetId required");
341
+ return;
342
+ }
343
+ void (async () => {
344
+ try {
345
+ await sendToExtension({
346
+ id: nextExtensionId++,
347
+ method: "forwardCDPCommand",
348
+ params: { method: "Target.activateTarget", params: { targetId } }
349
+ });
350
+ } catch {
351
+ }
352
+ })();
353
+ res.writeHead(200);
354
+ res.end("OK");
355
+ return;
356
+ }
357
+ const closeMatch = path4.match(/^\/json\/close\/(.+)$/);
358
+ if (closeMatch && (req.method === "GET" || req.method === "PUT")) {
359
+ const targetId = decodeURIComponent(closeMatch[1] ?? "").trim();
360
+ if (!targetId) {
361
+ res.writeHead(400);
362
+ res.end("targetId required");
363
+ return;
364
+ }
365
+ void (async () => {
366
+ try {
367
+ await sendToExtension({
368
+ id: nextExtensionId++,
369
+ method: "forwardCDPCommand",
370
+ params: { method: "Target.closeTarget", params: { targetId } }
371
+ });
372
+ } catch {
373
+ }
374
+ })();
375
+ res.writeHead(200);
376
+ res.end("OK");
377
+ return;
378
+ }
379
+ res.writeHead(404);
380
+ res.end("not found");
381
+ });
382
+ const wssExtension = new WebSocketServer({ noServer: true });
383
+ const wssCdp = new WebSocketServer({ noServer: true });
384
+ server.on("upgrade", (req, socket, head) => {
385
+ const url = new URL(req.url ?? "/", info.baseUrl);
386
+ const pathname = url.pathname;
387
+ const remote = req.socket.remoteAddress;
388
+ if (!remote || !isLoopbackAddress(remote)) {
389
+ rejectUpgrade(socket, 403, "Forbidden");
390
+ return;
391
+ }
392
+ const origin = headerValue(req.headers.origin);
393
+ if (origin && !origin.startsWith("chrome-extension://")) {
394
+ rejectUpgrade(socket, 403, "Forbidden: invalid origin");
395
+ return;
396
+ }
397
+ if (pathname === "/extension") {
398
+ const token = getRelayAuthTokenFromRequest(req, url);
399
+ if (!token || token !== relayAuthToken) {
400
+ rejectUpgrade(socket, 401, "Unauthorized");
401
+ return;
402
+ }
403
+ if (extensionWs) {
404
+ rejectUpgrade(socket, 409, "Extension already connected");
405
+ return;
406
+ }
407
+ wssExtension.handleUpgrade(req, socket, head, (ws) => {
408
+ wssExtension.emit("connection", ws, req);
409
+ });
410
+ return;
411
+ }
412
+ if (pathname === "/cdp") {
413
+ const token = getRelayAuthTokenFromRequest(req, url);
414
+ if (!token || token !== relayAuthToken) {
415
+ rejectUpgrade(socket, 401, "Unauthorized");
416
+ return;
417
+ }
418
+ if (!extensionWs) {
419
+ rejectUpgrade(socket, 503, "Extension not connected");
420
+ return;
421
+ }
422
+ wssCdp.handleUpgrade(req, socket, head, (ws) => {
423
+ wssCdp.emit("connection", ws, req);
424
+ });
425
+ return;
426
+ }
427
+ rejectUpgrade(socket, 404, "Not Found");
428
+ });
429
+ wssExtension.on("connection", (ws) => {
430
+ extensionWs = ws;
431
+ const ping = setInterval(() => {
432
+ if (ws.readyState !== WebSocket.OPEN) {
433
+ return;
434
+ }
435
+ ws.send(JSON.stringify({ method: "ping" }));
436
+ }, 5e3);
437
+ ws.on("message", (data) => {
438
+ let parsed = null;
439
+ try {
440
+ parsed = JSON.parse(rawDataToString(data));
441
+ } catch {
442
+ return;
443
+ }
444
+ if (parsed && typeof parsed === "object" && "id" in parsed && typeof parsed.id === "number") {
445
+ const pending = pendingExtension.get(parsed.id);
446
+ if (!pending) {
447
+ return;
448
+ }
449
+ pendingExtension.delete(parsed.id);
450
+ clearTimeout(pending.timer);
451
+ if ("error" in parsed && typeof parsed.error === "string" && parsed.error.trim()) {
452
+ pending.reject(new Error(parsed.error));
453
+ } else {
454
+ pending.resolve(parsed.result);
455
+ }
456
+ return;
457
+ }
458
+ if (parsed && typeof parsed === "object" && "method" in parsed) {
459
+ if (parsed.method === "pong") {
460
+ return;
461
+ }
462
+ if (parsed.method !== "forwardCDPEvent") {
463
+ return;
464
+ }
465
+ const evt = parsed;
466
+ const method = evt.params?.method;
467
+ const params = evt.params?.params;
468
+ const sessionId = evt.params?.sessionId;
469
+ if (!method || typeof method !== "string") {
470
+ return;
471
+ }
472
+ if (method === "Target.attachedToTarget") {
473
+ const attached = params ?? {};
474
+ const targetType = attached?.targetInfo?.type ?? "page";
475
+ if (targetType !== "page") {
476
+ return;
477
+ }
478
+ if (attached?.sessionId && attached?.targetInfo?.targetId) {
479
+ const prev = connectedTargets.get(attached.sessionId);
480
+ const nextTargetId = attached.targetInfo.targetId;
481
+ const prevTargetId = prev?.targetId;
482
+ const changedTarget = Boolean(prev && prevTargetId && prevTargetId !== nextTargetId);
483
+ connectedTargets.set(attached.sessionId, {
484
+ sessionId: attached.sessionId,
485
+ targetId: nextTargetId,
486
+ targetInfo: attached.targetInfo
487
+ });
488
+ if (changedTarget && prevTargetId) {
489
+ broadcastToCdpClients({
490
+ method: "Target.detachedFromTarget",
491
+ params: { sessionId: attached.sessionId, targetId: prevTargetId },
492
+ sessionId: attached.sessionId
493
+ });
494
+ }
495
+ if (!prev || changedTarget) {
496
+ broadcastToCdpClients({ method, params, sessionId });
497
+ }
498
+ return;
499
+ }
500
+ }
501
+ if (method === "Target.detachedFromTarget") {
502
+ const detached = params ?? {};
503
+ if (detached?.sessionId) {
504
+ connectedTargets.delete(detached.sessionId);
505
+ }
506
+ broadcastToCdpClients({ method, params, sessionId });
507
+ return;
508
+ }
509
+ if (method === "Target.targetInfoChanged") {
510
+ const changed = params ?? {};
511
+ const targetInfo = changed?.targetInfo;
512
+ const targetId = targetInfo?.targetId;
513
+ if (targetId && (targetInfo?.type ?? "page") === "page") {
514
+ for (const [sid, target] of connectedTargets) {
515
+ if (target.targetId !== targetId) {
516
+ continue;
517
+ }
518
+ connectedTargets.set(sid, {
519
+ ...target,
520
+ targetInfo: { ...target.targetInfo, ...targetInfo }
521
+ });
522
+ }
523
+ }
524
+ }
525
+ broadcastToCdpClients({ method, params, sessionId });
526
+ }
527
+ });
528
+ ws.on("close", () => {
529
+ clearInterval(ping);
530
+ extensionWs = null;
531
+ for (const [, pending] of pendingExtension) {
532
+ clearTimeout(pending.timer);
533
+ pending.reject(new Error("extension disconnected"));
534
+ }
535
+ pendingExtension.clear();
536
+ connectedTargets.clear();
537
+ for (const client of cdpClients) {
538
+ try {
539
+ client.close(1011, "extension disconnected");
540
+ } catch {
541
+ }
542
+ }
543
+ cdpClients.clear();
544
+ });
545
+ });
546
+ wssCdp.on("connection", (ws) => {
547
+ cdpClients.add(ws);
548
+ ws.on("message", async (data) => {
549
+ let cmd = null;
550
+ try {
551
+ cmd = JSON.parse(rawDataToString(data));
552
+ } catch {
553
+ return;
554
+ }
555
+ if (!cmd || typeof cmd !== "object") {
556
+ return;
557
+ }
558
+ if (typeof cmd.id !== "number" || typeof cmd.method !== "string") {
559
+ return;
560
+ }
561
+ if (!extensionWs) {
562
+ sendResponseToCdp(ws, {
563
+ id: cmd.id,
564
+ sessionId: cmd.sessionId,
565
+ error: { message: "Extension not connected" }
566
+ });
567
+ return;
568
+ }
569
+ try {
570
+ const result = await routeCdpCommand(cmd);
571
+ if (cmd.method === "Target.setAutoAttach" && !cmd.sessionId) {
572
+ ensureTargetEventsForClient(ws, "autoAttach");
573
+ }
574
+ if (cmd.method === "Target.setDiscoverTargets") {
575
+ const discover = cmd.params ?? {};
576
+ if (discover.discover === true) {
577
+ ensureTargetEventsForClient(ws, "discover");
578
+ }
579
+ }
580
+ if (cmd.method === "Target.attachToTarget") {
581
+ const params = cmd.params ?? {};
582
+ const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
583
+ if (targetId) {
584
+ const target = Array.from(connectedTargets.values()).find(
585
+ (t) => t.targetId === targetId
586
+ );
587
+ if (target) {
588
+ ws.send(
589
+ JSON.stringify({
590
+ method: "Target.attachedToTarget",
591
+ params: {
592
+ sessionId: target.sessionId,
593
+ targetInfo: { ...target.targetInfo, attached: true },
594
+ waitingForDebugger: false
595
+ }
596
+ })
597
+ );
598
+ }
599
+ }
600
+ }
601
+ sendResponseToCdp(ws, { id: cmd.id, sessionId: cmd.sessionId, result });
602
+ } catch (err) {
603
+ sendResponseToCdp(ws, {
604
+ id: cmd.id,
605
+ sessionId: cmd.sessionId,
606
+ error: { message: err instanceof Error ? err.message : String(err) }
607
+ });
608
+ }
609
+ });
610
+ ws.on("close", () => {
611
+ cdpClients.delete(ws);
612
+ });
613
+ });
614
+ try {
615
+ await new Promise((resolve, reject) => {
616
+ server.listen(info.port, info.host, () => resolve());
617
+ server.once("error", reject);
618
+ });
619
+ } catch (err) {
620
+ if (isAddrInUseError(err) && await looksLikeAgenticMailRelay(info.baseUrl)) {
621
+ const existingRelay = {
622
+ host: info.host,
623
+ port: info.port,
624
+ baseUrl: info.baseUrl,
625
+ cdpWsUrl: `ws://${info.host}:${info.port}/cdp`,
626
+ extensionConnected: () => false,
627
+ stop: async () => {
628
+ serversByPort.delete(info.port);
629
+ }
630
+ };
631
+ serversByPort.set(info.port, existingRelay);
632
+ return existingRelay;
633
+ }
634
+ throw err;
635
+ }
636
+ const addr = server.address();
637
+ const port = addr?.port ?? info.port;
638
+ const host = info.host;
639
+ const baseUrl = `${new URL(info.baseUrl).protocol}//${host}:${port}`;
640
+ const relay = {
641
+ host,
642
+ port,
643
+ baseUrl,
644
+ cdpWsUrl: `ws://${host}:${port}/cdp`,
645
+ extensionConnected: () => Boolean(extensionWs),
646
+ stop: async () => {
647
+ serversByPort.delete(port);
648
+ try {
649
+ extensionWs?.close(1001, "server stopping");
650
+ } catch {
651
+ }
652
+ for (const ws of cdpClients) {
653
+ try {
654
+ ws.close(1001, "server stopping");
655
+ } catch {
656
+ }
657
+ }
658
+ await new Promise((resolve) => {
659
+ server.close(() => resolve());
660
+ });
661
+ wssExtension.close();
662
+ wssCdp.close();
663
+ }
664
+ };
665
+ serversByPort.set(port, relay);
666
+ return relay;
667
+ }
668
+ async function stopChromeExtensionRelayServer(opts) {
669
+ const info = parseBaseUrl(opts.cdpUrl);
670
+ const existing = serversByPort.get(info.port);
671
+ if (!existing) {
672
+ return false;
673
+ }
674
+ await existing.stop();
675
+ return true;
676
+ }
677
+
678
+ // src/browser/cdp.helpers.ts
679
+ import WebSocket2 from "ws";
680
+ function getHeadersWithAuth(url, headers = {}) {
681
+ const relayHeaders = getChromeExtensionRelayAuthHeaders(url);
682
+ const mergedHeaders = { ...relayHeaders, ...headers };
683
+ try {
684
+ const parsed = new URL(url);
685
+ const hasAuthHeader = Object.keys(mergedHeaders).some(
686
+ (key) => key.toLowerCase() === "authorization"
687
+ );
688
+ if (hasAuthHeader) {
689
+ return mergedHeaders;
690
+ }
691
+ if (parsed.username || parsed.password) {
692
+ const auth = Buffer.from(`${parsed.username}:${parsed.password}`).toString("base64");
693
+ return { ...mergedHeaders, Authorization: `Basic ${auth}` };
694
+ }
695
+ } catch {
696
+ }
697
+ return mergedHeaders;
698
+ }
699
+ function appendCdpPath(cdpUrl, path4) {
700
+ const url = new URL(cdpUrl);
701
+ const basePath = url.pathname.replace(/\/$/, "");
702
+ const suffix = path4.startsWith("/") ? path4 : `/${path4}`;
703
+ url.pathname = `${basePath}${suffix}`;
704
+ return url.toString();
705
+ }
706
+ function createCdpSender(ws) {
707
+ let nextId = 1;
708
+ const pending = /* @__PURE__ */ new Map();
709
+ const send = (method, params, sessionId) => {
710
+ const id = nextId++;
711
+ const msg = { id, method, params, sessionId };
712
+ ws.send(JSON.stringify(msg));
713
+ return new Promise((resolve, reject) => {
714
+ pending.set(id, { resolve, reject });
715
+ });
716
+ };
717
+ const closeWithError = (err) => {
718
+ for (const [, p] of pending) {
719
+ p.reject(err);
720
+ }
721
+ pending.clear();
722
+ try {
723
+ ws.close();
724
+ } catch {
725
+ }
726
+ };
727
+ ws.on("error", (err) => {
728
+ closeWithError(err instanceof Error ? err : new Error(String(err)));
729
+ });
730
+ ws.on("message", (data) => {
731
+ try {
732
+ const parsed = JSON.parse(rawDataToString(data));
733
+ if (typeof parsed.id !== "number") {
734
+ return;
735
+ }
736
+ const p = pending.get(parsed.id);
737
+ if (!p) {
738
+ return;
739
+ }
740
+ pending.delete(parsed.id);
741
+ if (parsed.error?.message) {
742
+ p.reject(new Error(parsed.error.message));
743
+ return;
744
+ }
745
+ p.resolve(parsed.result);
746
+ } catch {
747
+ }
748
+ });
749
+ ws.on("close", () => {
750
+ closeWithError(new Error("CDP socket closed"));
751
+ });
752
+ return { send, closeWithError };
753
+ }
754
+ async function fetchJson(url, timeoutMs = 1500, init) {
755
+ const res = await fetchChecked(url, timeoutMs, init);
756
+ return await res.json();
757
+ }
758
+ async function fetchChecked(url, timeoutMs = 1500, init) {
759
+ const ctrl = new AbortController();
760
+ const t = setTimeout(ctrl.abort.bind(ctrl), timeoutMs);
761
+ try {
762
+ const headers = getHeadersWithAuth(url, init?.headers || {});
763
+ const res = await fetch(url, { ...init, headers, signal: ctrl.signal });
764
+ if (!res.ok) {
765
+ throw new Error(`HTTP ${res.status}`);
766
+ }
767
+ return res;
768
+ } finally {
769
+ clearTimeout(t);
770
+ }
771
+ }
772
+ async function fetchOk(url, timeoutMs = 1500, init) {
773
+ await fetchChecked(url, timeoutMs, init);
774
+ }
775
+ async function withCdpSocket(wsUrl, fn, opts) {
776
+ const headers = getHeadersWithAuth(wsUrl, opts?.headers ?? {});
777
+ const handshakeTimeoutMs = typeof opts?.handshakeTimeoutMs === "number" && Number.isFinite(opts.handshakeTimeoutMs) ? Math.max(1, Math.floor(opts.handshakeTimeoutMs)) : 5e3;
778
+ const ws = new WebSocket2(wsUrl, {
779
+ handshakeTimeout: handshakeTimeoutMs,
780
+ ...Object.keys(headers).length ? { headers } : {}
781
+ });
782
+ const { send, closeWithError } = createCdpSender(ws);
783
+ const openPromise = new Promise((resolve, reject) => {
784
+ ws.once("open", () => resolve());
785
+ ws.once("error", (err) => reject(err));
786
+ ws.once("close", () => reject(new Error("CDP socket closed")));
787
+ });
788
+ try {
789
+ await openPromise;
790
+ } catch (err) {
791
+ closeWithError(err instanceof Error ? err : new Error(String(err)));
792
+ throw err;
793
+ }
794
+ try {
795
+ return await fn(send);
796
+ } catch (err) {
797
+ closeWithError(err instanceof Error ? err : new Error(String(err)));
798
+ throw err;
799
+ } finally {
800
+ try {
801
+ ws.close();
802
+ } catch {
803
+ }
804
+ }
805
+ }
806
+
807
+ // src/browser/navigation-guard.ts
808
+ var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
809
+ var InvalidBrowserNavigationUrlError = class extends Error {
810
+ constructor(message) {
811
+ super(message);
812
+ this.name = "InvalidBrowserNavigationUrlError";
813
+ }
814
+ };
815
+ function withBrowserNavigationPolicy(ssrfPolicy) {
816
+ return ssrfPolicy ? { ssrfPolicy } : {};
817
+ }
818
+ async function assertBrowserNavigationAllowed(opts) {
819
+ const rawUrl = String(opts.url ?? "").trim();
820
+ if (!rawUrl) {
821
+ throw new InvalidBrowserNavigationUrlError("url is required");
822
+ }
823
+ let parsed;
824
+ try {
825
+ parsed = new URL(rawUrl);
826
+ } catch {
827
+ throw new InvalidBrowserNavigationUrlError(`Invalid URL: ${rawUrl}`);
828
+ }
829
+ if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
830
+ return;
831
+ }
832
+ await resolvePinnedHostnameWithPolicy(parsed.hostname, {
833
+ lookupFn: opts.lookupFn,
834
+ policy: opts.ssrfPolicy
835
+ });
836
+ }
837
+
838
+ // src/browser/cdp.ts
839
+ function normalizeCdpWsUrl(wsUrl, cdpUrl) {
840
+ const ws = new URL(wsUrl);
841
+ const cdp = new URL(cdpUrl);
842
+ if (isLoopbackHost(ws.hostname) && !isLoopbackHost(cdp.hostname)) {
843
+ ws.hostname = cdp.hostname;
844
+ const cdpPort = cdp.port || (cdp.protocol === "https:" ? "443" : "80");
845
+ if (cdpPort) {
846
+ ws.port = cdpPort;
847
+ }
848
+ ws.protocol = cdp.protocol === "https:" ? "wss:" : "ws:";
849
+ }
850
+ if (cdp.protocol === "https:" && ws.protocol === "ws:") {
851
+ ws.protocol = "wss:";
852
+ }
853
+ if (!ws.username && !ws.password && (cdp.username || cdp.password)) {
854
+ ws.username = cdp.username;
855
+ ws.password = cdp.password;
856
+ }
857
+ for (const [key, value] of cdp.searchParams.entries()) {
858
+ if (!ws.searchParams.has(key)) {
859
+ ws.searchParams.append(key, value);
860
+ }
861
+ }
862
+ return ws.toString();
863
+ }
864
+ async function captureScreenshot(opts) {
865
+ return await withCdpSocket(opts.wsUrl, async (send) => {
866
+ await send("Page.enable");
867
+ let clip;
868
+ if (opts.fullPage) {
869
+ const metrics = await send("Page.getLayoutMetrics");
870
+ const size = metrics?.cssContentSize ?? metrics?.contentSize;
871
+ const width = Number(size?.width ?? 0);
872
+ const height = Number(size?.height ?? 0);
873
+ if (width > 0 && height > 0) {
874
+ clip = { x: 0, y: 0, width, height, scale: 1 };
875
+ }
876
+ }
877
+ const format = opts.format ?? "png";
878
+ const quality = format === "jpeg" ? Math.max(0, Math.min(100, Math.round(opts.quality ?? 85))) : void 0;
879
+ const result = await send("Page.captureScreenshot", {
880
+ format,
881
+ ...quality !== void 0 ? { quality } : {},
882
+ fromSurface: true,
883
+ captureBeyondViewport: true,
884
+ ...clip ? { clip } : {}
885
+ });
886
+ const base64 = result?.data;
887
+ if (!base64) {
888
+ throw new Error("Screenshot failed: missing data");
889
+ }
890
+ return Buffer.from(base64, "base64");
891
+ });
892
+ }
893
+ async function createTargetViaCdp(opts) {
894
+ if (!opts.navigationChecked) {
895
+ await assertBrowserNavigationAllowed({
896
+ url: opts.url,
897
+ ...withBrowserNavigationPolicy(opts.ssrfPolicy)
898
+ });
899
+ }
900
+ const version = await fetchJson(
901
+ appendCdpPath(opts.cdpUrl, "/json/version"),
902
+ 1500
903
+ );
904
+ const wsUrlRaw = String(version?.webSocketDebuggerUrl ?? "").trim();
905
+ const wsUrl = wsUrlRaw ? normalizeCdpWsUrl(wsUrlRaw, opts.cdpUrl) : "";
906
+ if (!wsUrl) {
907
+ throw new Error("CDP /json/version missing webSocketDebuggerUrl");
908
+ }
909
+ return await withCdpSocket(wsUrl, async (send) => {
910
+ const created = await send("Target.createTarget", { url: opts.url });
911
+ const targetId = String(created?.targetId ?? "").trim();
912
+ if (!targetId) {
913
+ throw new Error("CDP Target.createTarget returned no targetId");
914
+ }
915
+ return { targetId };
916
+ });
917
+ }
918
+ function axValue(v) {
919
+ if (!v || typeof v !== "object") {
920
+ return "";
921
+ }
922
+ const value = v.value;
923
+ if (typeof value === "string") {
924
+ return value;
925
+ }
926
+ if (typeof value === "number" || typeof value === "boolean") {
927
+ return String(value);
928
+ }
929
+ return "";
930
+ }
931
+ function formatAriaSnapshot(nodes, limit) {
932
+ const byId = /* @__PURE__ */ new Map();
933
+ for (const n of nodes) {
934
+ if (n.nodeId) {
935
+ byId.set(n.nodeId, n);
936
+ }
937
+ }
938
+ const referenced = /* @__PURE__ */ new Set();
939
+ for (const n of nodes) {
940
+ for (const c of n.childIds ?? []) {
941
+ referenced.add(c);
942
+ }
943
+ }
944
+ const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0];
945
+ if (!root?.nodeId) {
946
+ return [];
947
+ }
948
+ const out = [];
949
+ const stack = [{ id: root.nodeId, depth: 0 }];
950
+ while (stack.length && out.length < limit) {
951
+ const popped = stack.pop();
952
+ if (!popped) {
953
+ break;
954
+ }
955
+ const { id, depth } = popped;
956
+ const n = byId.get(id);
957
+ if (!n) {
958
+ continue;
959
+ }
960
+ const role = axValue(n.role);
961
+ const name = axValue(n.name);
962
+ const value = axValue(n.value);
963
+ const description = axValue(n.description);
964
+ const ref = `ax${out.length + 1}`;
965
+ out.push({
966
+ ref,
967
+ role: role || "unknown",
968
+ name: name || "",
969
+ ...value ? { value } : {},
970
+ ...description ? { description } : {},
971
+ ...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
972
+ depth
973
+ });
974
+ const children = (n.childIds ?? []).filter((c) => byId.has(c));
975
+ for (let i = children.length - 1; i >= 0; i--) {
976
+ const child = children[i];
977
+ if (child) {
978
+ stack.push({ id: child, depth: depth + 1 });
979
+ }
980
+ }
981
+ }
982
+ return out;
983
+ }
984
+ async function snapshotAria(opts) {
985
+ const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
986
+ return await withCdpSocket(opts.wsUrl, async (send) => {
987
+ await send("Accessibility.enable").catch(() => {
988
+ });
989
+ const res = await send("Accessibility.getFullAXTree");
990
+ const nodes = Array.isArray(res?.nodes) ? res.nodes : [];
991
+ return { nodes: formatAriaSnapshot(nodes, limit) };
992
+ });
993
+ }
994
+
995
+ // src/browser/chrome.ts
996
+ import { spawn } from "child_process";
997
+ import fs3 from "fs";
998
+ import os2 from "os";
999
+ import path3 from "path";
1000
+ import WebSocket3 from "ws";
1001
+
1002
+ // src/browser/chrome.executables.ts
1003
+ import { execFileSync } from "child_process";
1004
+ import fs from "fs";
1005
+ import os from "os";
1006
+ import path from "path";
1007
+ var CHROMIUM_BUNDLE_IDS = /* @__PURE__ */ new Set([
1008
+ "com.google.Chrome",
1009
+ "com.google.Chrome.beta",
1010
+ "com.google.Chrome.canary",
1011
+ "com.google.Chrome.dev",
1012
+ "com.brave.Browser",
1013
+ "com.brave.Browser.beta",
1014
+ "com.brave.Browser.nightly",
1015
+ "com.microsoft.Edge",
1016
+ "com.microsoft.EdgeBeta",
1017
+ "com.microsoft.EdgeDev",
1018
+ "com.microsoft.EdgeCanary",
1019
+ "org.chromium.Chromium",
1020
+ "com.vivaldi.Vivaldi",
1021
+ "com.operasoftware.Opera",
1022
+ "com.operasoftware.OperaGX",
1023
+ "com.yandex.desktop.yandex-browser",
1024
+ "company.thebrowser.Browser"
1025
+ // Arc
1026
+ ]);
1027
+ var CHROMIUM_DESKTOP_IDS = /* @__PURE__ */ new Set([
1028
+ "google-chrome.desktop",
1029
+ "google-chrome-beta.desktop",
1030
+ "google-chrome-unstable.desktop",
1031
+ "brave-browser.desktop",
1032
+ "microsoft-edge.desktop",
1033
+ "microsoft-edge-beta.desktop",
1034
+ "microsoft-edge-dev.desktop",
1035
+ "microsoft-edge-canary.desktop",
1036
+ "chromium.desktop",
1037
+ "chromium-browser.desktop",
1038
+ "vivaldi.desktop",
1039
+ "vivaldi-stable.desktop",
1040
+ "opera.desktop",
1041
+ "opera-gx.desktop",
1042
+ "yandex-browser.desktop",
1043
+ "org.chromium.Chromium.desktop"
1044
+ ]);
1045
+ var CHROMIUM_EXE_NAMES = /* @__PURE__ */ new Set([
1046
+ "chrome.exe",
1047
+ "msedge.exe",
1048
+ "brave.exe",
1049
+ "brave-browser.exe",
1050
+ "chromium.exe",
1051
+ "vivaldi.exe",
1052
+ "opera.exe",
1053
+ "launcher.exe",
1054
+ "yandex.exe",
1055
+ "yandexbrowser.exe",
1056
+ // mac/linux names
1057
+ "google chrome",
1058
+ "google chrome canary",
1059
+ "brave browser",
1060
+ "microsoft edge",
1061
+ "chromium",
1062
+ "chrome",
1063
+ "brave",
1064
+ "msedge",
1065
+ "brave-browser",
1066
+ "google-chrome",
1067
+ "google-chrome-stable",
1068
+ "google-chrome-beta",
1069
+ "google-chrome-unstable",
1070
+ "microsoft-edge",
1071
+ "microsoft-edge-beta",
1072
+ "microsoft-edge-dev",
1073
+ "microsoft-edge-canary",
1074
+ "chromium-browser",
1075
+ "vivaldi",
1076
+ "vivaldi-stable",
1077
+ "opera",
1078
+ "opera-stable",
1079
+ "opera-gx",
1080
+ "yandex-browser"
1081
+ ]);
1082
+ function exists(filePath) {
1083
+ try {
1084
+ return fs.existsSync(filePath);
1085
+ } catch {
1086
+ return false;
1087
+ }
1088
+ }
1089
+ function execText(command, args, timeoutMs = 1200, maxBuffer = 1024 * 1024) {
1090
+ try {
1091
+ const output = execFileSync(command, args, {
1092
+ timeout: timeoutMs,
1093
+ encoding: "utf8",
1094
+ maxBuffer
1095
+ });
1096
+ return String(output ?? "").trim() || null;
1097
+ } catch {
1098
+ return null;
1099
+ }
1100
+ }
1101
+ function inferKindFromIdentifier(identifier) {
1102
+ const id = identifier.toLowerCase();
1103
+ if (id.includes("brave")) {
1104
+ return "brave";
1105
+ }
1106
+ if (id.includes("edge")) {
1107
+ return "edge";
1108
+ }
1109
+ if (id.includes("chromium")) {
1110
+ return "chromium";
1111
+ }
1112
+ if (id.includes("canary")) {
1113
+ return "canary";
1114
+ }
1115
+ if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser")) {
1116
+ return "chromium";
1117
+ }
1118
+ return "chrome";
1119
+ }
1120
+ function inferKindFromExecutableName(name) {
1121
+ const lower = name.toLowerCase();
1122
+ if (lower.includes("brave")) {
1123
+ return "brave";
1124
+ }
1125
+ if (lower.includes("edge") || lower.includes("msedge")) {
1126
+ return "edge";
1127
+ }
1128
+ if (lower.includes("chromium")) {
1129
+ return "chromium";
1130
+ }
1131
+ if (lower.includes("canary") || lower.includes("sxs")) {
1132
+ return "canary";
1133
+ }
1134
+ if (lower.includes("opera") || lower.includes("vivaldi") || lower.includes("yandex")) {
1135
+ return "chromium";
1136
+ }
1137
+ return "chrome";
1138
+ }
1139
+ function detectDefaultChromiumExecutable(platform) {
1140
+ if (platform === "darwin") {
1141
+ return detectDefaultChromiumExecutableMac();
1142
+ }
1143
+ if (platform === "linux") {
1144
+ return detectDefaultChromiumExecutableLinux();
1145
+ }
1146
+ if (platform === "win32") {
1147
+ return detectDefaultChromiumExecutableWindows();
1148
+ }
1149
+ return null;
1150
+ }
1151
+ function detectDefaultChromiumExecutableMac() {
1152
+ const bundleId = detectDefaultBrowserBundleIdMac();
1153
+ if (!bundleId || !CHROMIUM_BUNDLE_IDS.has(bundleId)) {
1154
+ return null;
1155
+ }
1156
+ const appPathRaw = execText("/usr/bin/osascript", [
1157
+ "-e",
1158
+ `POSIX path of (path to application id "${bundleId}")`
1159
+ ]);
1160
+ if (!appPathRaw) {
1161
+ return null;
1162
+ }
1163
+ const appPath = appPathRaw.trim().replace(/\/$/, "");
1164
+ const exeName = execText("/usr/bin/defaults", [
1165
+ "read",
1166
+ path.join(appPath, "Contents", "Info"),
1167
+ "CFBundleExecutable"
1168
+ ]);
1169
+ if (!exeName) {
1170
+ return null;
1171
+ }
1172
+ const exePath = path.join(appPath, "Contents", "MacOS", exeName.trim());
1173
+ if (!exists(exePath)) {
1174
+ return null;
1175
+ }
1176
+ return { kind: inferKindFromIdentifier(bundleId), path: exePath };
1177
+ }
1178
+ function detectDefaultBrowserBundleIdMac() {
1179
+ const plistPath = path.join(
1180
+ os.homedir(),
1181
+ "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
1182
+ );
1183
+ if (!exists(plistPath)) {
1184
+ return null;
1185
+ }
1186
+ const handlersRaw = execText(
1187
+ "/usr/bin/plutil",
1188
+ ["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath],
1189
+ 2e3,
1190
+ 5 * 1024 * 1024
1191
+ );
1192
+ if (!handlersRaw) {
1193
+ return null;
1194
+ }
1195
+ let handlers;
1196
+ try {
1197
+ handlers = JSON.parse(handlersRaw);
1198
+ } catch {
1199
+ return null;
1200
+ }
1201
+ if (!Array.isArray(handlers)) {
1202
+ return null;
1203
+ }
1204
+ const resolveScheme = (scheme) => {
1205
+ let candidate = null;
1206
+ for (const entry of handlers) {
1207
+ if (!entry || typeof entry !== "object") {
1208
+ continue;
1209
+ }
1210
+ const record = entry;
1211
+ if (record.LSHandlerURLScheme !== scheme) {
1212
+ continue;
1213
+ }
1214
+ const role = typeof record.LSHandlerRoleAll === "string" && record.LSHandlerRoleAll || typeof record.LSHandlerRoleViewer === "string" && record.LSHandlerRoleViewer || null;
1215
+ if (role) {
1216
+ candidate = role;
1217
+ }
1218
+ }
1219
+ return candidate;
1220
+ };
1221
+ return resolveScheme("http") ?? resolveScheme("https");
1222
+ }
1223
+ function detectDefaultChromiumExecutableLinux() {
1224
+ const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) || execText("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
1225
+ if (!desktopId) {
1226
+ return null;
1227
+ }
1228
+ const trimmed = desktopId.trim();
1229
+ if (!CHROMIUM_DESKTOP_IDS.has(trimmed)) {
1230
+ return null;
1231
+ }
1232
+ const desktopPath = findDesktopFilePath(trimmed);
1233
+ if (!desktopPath) {
1234
+ return null;
1235
+ }
1236
+ const execLine = readDesktopExecLine(desktopPath);
1237
+ if (!execLine) {
1238
+ return null;
1239
+ }
1240
+ const command = extractExecutableFromExecLine(execLine);
1241
+ if (!command) {
1242
+ return null;
1243
+ }
1244
+ const resolved = resolveLinuxExecutablePath(command);
1245
+ if (!resolved) {
1246
+ return null;
1247
+ }
1248
+ const exeName = path.posix.basename(resolved).toLowerCase();
1249
+ if (!CHROMIUM_EXE_NAMES.has(exeName)) {
1250
+ return null;
1251
+ }
1252
+ return { kind: inferKindFromExecutableName(exeName), path: resolved };
1253
+ }
1254
+ function detectDefaultChromiumExecutableWindows() {
1255
+ const progId = readWindowsProgId();
1256
+ const command = (progId ? readWindowsCommandForProgId(progId) : null) || readWindowsCommandForProgId("http");
1257
+ if (!command) {
1258
+ return null;
1259
+ }
1260
+ const expanded = expandWindowsEnvVars(command);
1261
+ const exePath = extractWindowsExecutablePath(expanded);
1262
+ if (!exePath) {
1263
+ return null;
1264
+ }
1265
+ if (!exists(exePath)) {
1266
+ return null;
1267
+ }
1268
+ const exeName = path.win32.basename(exePath).toLowerCase();
1269
+ if (!CHROMIUM_EXE_NAMES.has(exeName)) {
1270
+ return null;
1271
+ }
1272
+ return { kind: inferKindFromExecutableName(exeName), path: exePath };
1273
+ }
1274
+ function findDesktopFilePath(desktopId) {
1275
+ const candidates = [
1276
+ path.join(os.homedir(), ".local", "share", "applications", desktopId),
1277
+ path.join("/usr/local/share/applications", desktopId),
1278
+ path.join("/usr/share/applications", desktopId),
1279
+ path.join("/var/lib/snapd/desktop/applications", desktopId)
1280
+ ];
1281
+ for (const candidate of candidates) {
1282
+ if (exists(candidate)) {
1283
+ return candidate;
1284
+ }
1285
+ }
1286
+ return null;
1287
+ }
1288
+ function readDesktopExecLine(desktopPath) {
1289
+ try {
1290
+ const raw = fs.readFileSync(desktopPath, "utf8");
1291
+ const lines = raw.split(/\r?\n/);
1292
+ for (const line of lines) {
1293
+ if (line.startsWith("Exec=")) {
1294
+ return line.slice("Exec=".length).trim();
1295
+ }
1296
+ }
1297
+ } catch {
1298
+ }
1299
+ return null;
1300
+ }
1301
+ function extractExecutableFromExecLine(execLine) {
1302
+ const tokens = splitExecLine(execLine);
1303
+ for (const token of tokens) {
1304
+ if (!token) {
1305
+ continue;
1306
+ }
1307
+ if (token === "env") {
1308
+ continue;
1309
+ }
1310
+ if (token.includes("=") && !token.startsWith("/") && !token.includes("\\")) {
1311
+ continue;
1312
+ }
1313
+ return token.replace(/^["']|["']$/g, "");
1314
+ }
1315
+ return null;
1316
+ }
1317
+ function splitExecLine(line) {
1318
+ const tokens = [];
1319
+ let current = "";
1320
+ let inQuotes = false;
1321
+ let quoteChar = "";
1322
+ for (let i = 0; i < line.length; i += 1) {
1323
+ const ch = line[i];
1324
+ if ((ch === '"' || ch === "'") && (!inQuotes || ch === quoteChar)) {
1325
+ if (inQuotes) {
1326
+ inQuotes = false;
1327
+ quoteChar = "";
1328
+ } else {
1329
+ inQuotes = true;
1330
+ quoteChar = ch;
1331
+ }
1332
+ continue;
1333
+ }
1334
+ if (!inQuotes && /\s/.test(ch)) {
1335
+ if (current) {
1336
+ tokens.push(current);
1337
+ current = "";
1338
+ }
1339
+ continue;
1340
+ }
1341
+ current += ch;
1342
+ }
1343
+ if (current) {
1344
+ tokens.push(current);
1345
+ }
1346
+ return tokens;
1347
+ }
1348
+ function resolveLinuxExecutablePath(command) {
1349
+ const cleaned = command.trim().replace(/%[a-zA-Z]/g, "");
1350
+ if (!cleaned) {
1351
+ return null;
1352
+ }
1353
+ if (cleaned.startsWith("/")) {
1354
+ return cleaned;
1355
+ }
1356
+ const resolved = execText("which", [cleaned], 800);
1357
+ return resolved ? resolved.trim() : null;
1358
+ }
1359
+ function readWindowsProgId() {
1360
+ const output = execText("reg", [
1361
+ "query",
1362
+ "HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
1363
+ "/v",
1364
+ "ProgId"
1365
+ ]);
1366
+ if (!output) {
1367
+ return null;
1368
+ }
1369
+ const match = output.match(/ProgId\s+REG_\w+\s+(.+)$/im);
1370
+ return match?.[1]?.trim() || null;
1371
+ }
1372
+ function readWindowsCommandForProgId(progId) {
1373
+ const key = progId === "http" ? "HKCR\\http\\shell\\open\\command" : `HKCR\\${progId}\\shell\\open\\command`;
1374
+ const output = execText("reg", ["query", key, "/ve"]);
1375
+ if (!output) {
1376
+ return null;
1377
+ }
1378
+ const match = output.match(/REG_\w+\s+(.+)$/im);
1379
+ return match?.[1]?.trim() || null;
1380
+ }
1381
+ function expandWindowsEnvVars(value) {
1382
+ return value.replace(/%([^%]+)%/g, (_match, name) => {
1383
+ const key = String(name ?? "").trim();
1384
+ return key ? process.env[key] ?? `%${key}%` : _match;
1385
+ });
1386
+ }
1387
+ function extractWindowsExecutablePath(command) {
1388
+ const quoted = command.match(/"([^"]+\\.exe)"/i);
1389
+ if (quoted?.[1]) {
1390
+ return quoted[1];
1391
+ }
1392
+ const unquoted = command.match(/([^\\s]+\\.exe)/i);
1393
+ if (unquoted?.[1]) {
1394
+ return unquoted[1];
1395
+ }
1396
+ return null;
1397
+ }
1398
+ function findFirstExecutable(candidates) {
1399
+ for (const candidate of candidates) {
1400
+ if (exists(candidate.path)) {
1401
+ return candidate;
1402
+ }
1403
+ }
1404
+ return null;
1405
+ }
1406
+ function findChromeExecutableMac() {
1407
+ const candidates = [
1408
+ {
1409
+ kind: "chrome",
1410
+ path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
1411
+ },
1412
+ {
1413
+ kind: "chrome",
1414
+ path: path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
1415
+ },
1416
+ {
1417
+ kind: "brave",
1418
+ path: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
1419
+ },
1420
+ {
1421
+ kind: "brave",
1422
+ path: path.join(os.homedir(), "Applications/Brave Browser.app/Contents/MacOS/Brave Browser")
1423
+ },
1424
+ {
1425
+ kind: "edge",
1426
+ path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
1427
+ },
1428
+ {
1429
+ kind: "edge",
1430
+ path: path.join(
1431
+ os.homedir(),
1432
+ "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
1433
+ )
1434
+ },
1435
+ {
1436
+ kind: "chromium",
1437
+ path: "/Applications/Chromium.app/Contents/MacOS/Chromium"
1438
+ },
1439
+ {
1440
+ kind: "chromium",
1441
+ path: path.join(os.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium")
1442
+ },
1443
+ {
1444
+ kind: "canary",
1445
+ path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
1446
+ },
1447
+ {
1448
+ kind: "canary",
1449
+ path: path.join(
1450
+ os.homedir(),
1451
+ "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
1452
+ )
1453
+ }
1454
+ ];
1455
+ return findFirstExecutable(candidates);
1456
+ }
1457
+ function findChromeExecutableLinux() {
1458
+ const candidates = [
1459
+ { kind: "chrome", path: "/usr/bin/google-chrome" },
1460
+ { kind: "chrome", path: "/usr/bin/google-chrome-stable" },
1461
+ { kind: "chrome", path: "/usr/bin/chrome" },
1462
+ { kind: "brave", path: "/usr/bin/brave-browser" },
1463
+ { kind: "brave", path: "/usr/bin/brave-browser-stable" },
1464
+ { kind: "brave", path: "/usr/bin/brave" },
1465
+ { kind: "brave", path: "/snap/bin/brave" },
1466
+ { kind: "edge", path: "/usr/bin/microsoft-edge" },
1467
+ { kind: "edge", path: "/usr/bin/microsoft-edge-stable" },
1468
+ { kind: "chromium", path: "/usr/bin/chromium" },
1469
+ { kind: "chromium", path: "/usr/bin/chromium-browser" },
1470
+ { kind: "chromium", path: "/snap/bin/chromium" }
1471
+ ];
1472
+ return findFirstExecutable(candidates);
1473
+ }
1474
+ function findChromeExecutableWindows() {
1475
+ const localAppData = process.env.LOCALAPPDATA ?? "";
1476
+ const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
1477
+ const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
1478
+ const joinWin = path.win32.join;
1479
+ const candidates = [];
1480
+ if (localAppData) {
1481
+ candidates.push({
1482
+ kind: "chrome",
1483
+ path: joinWin(localAppData, "Google", "Chrome", "Application", "chrome.exe")
1484
+ });
1485
+ candidates.push({
1486
+ kind: "brave",
1487
+ path: joinWin(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
1488
+ });
1489
+ candidates.push({
1490
+ kind: "edge",
1491
+ path: joinWin(localAppData, "Microsoft", "Edge", "Application", "msedge.exe")
1492
+ });
1493
+ candidates.push({
1494
+ kind: "chromium",
1495
+ path: joinWin(localAppData, "Chromium", "Application", "chrome.exe")
1496
+ });
1497
+ candidates.push({
1498
+ kind: "canary",
1499
+ path: joinWin(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe")
1500
+ });
1501
+ }
1502
+ candidates.push({
1503
+ kind: "chrome",
1504
+ path: joinWin(programFiles, "Google", "Chrome", "Application", "chrome.exe")
1505
+ });
1506
+ candidates.push({
1507
+ kind: "chrome",
1508
+ path: joinWin(programFilesX86, "Google", "Chrome", "Application", "chrome.exe")
1509
+ });
1510
+ candidates.push({
1511
+ kind: "brave",
1512
+ path: joinWin(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
1513
+ });
1514
+ candidates.push({
1515
+ kind: "brave",
1516
+ path: joinWin(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
1517
+ });
1518
+ candidates.push({
1519
+ kind: "edge",
1520
+ path: joinWin(programFiles, "Microsoft", "Edge", "Application", "msedge.exe")
1521
+ });
1522
+ candidates.push({
1523
+ kind: "edge",
1524
+ path: joinWin(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe")
1525
+ });
1526
+ return findFirstExecutable(candidates);
1527
+ }
1528
+ function resolveBrowserExecutableForPlatform(resolved, platform) {
1529
+ if (resolved.executablePath) {
1530
+ if (!exists(resolved.executablePath)) {
1531
+ throw new Error(`browser.executablePath not found: ${resolved.executablePath}`);
1532
+ }
1533
+ return { kind: "custom", path: resolved.executablePath };
1534
+ }
1535
+ const detected = detectDefaultChromiumExecutable(platform);
1536
+ if (detected) {
1537
+ return detected;
1538
+ }
1539
+ if (platform === "darwin") {
1540
+ return findChromeExecutableMac();
1541
+ }
1542
+ if (platform === "linux") {
1543
+ return findChromeExecutableLinux();
1544
+ }
1545
+ if (platform === "win32") {
1546
+ return findChromeExecutableWindows();
1547
+ }
1548
+ return null;
1549
+ }
1550
+
1551
+ // src/browser/chrome.profile-decoration.ts
1552
+ import fs2 from "fs";
1553
+ import path2 from "path";
1554
+ function decoratedMarkerPath(userDataDir) {
1555
+ return path2.join(userDataDir, ".agenticmail-profile-decorated");
1556
+ }
1557
+ function safeReadJson(filePath) {
1558
+ try {
1559
+ if (!fs2.existsSync(filePath)) {
1560
+ return null;
1561
+ }
1562
+ const raw = fs2.readFileSync(filePath, "utf-8");
1563
+ const parsed = JSON.parse(raw);
1564
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1565
+ return null;
1566
+ }
1567
+ return parsed;
1568
+ } catch {
1569
+ return null;
1570
+ }
1571
+ }
1572
+ function safeWriteJson(filePath, data) {
1573
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
1574
+ fs2.writeFileSync(filePath, JSON.stringify(data, null, 2));
1575
+ }
1576
+ function setDeep(obj, keys, value) {
1577
+ let node = obj;
1578
+ for (const key of keys.slice(0, -1)) {
1579
+ const next = node[key];
1580
+ if (typeof next !== "object" || next === null || Array.isArray(next)) {
1581
+ node[key] = {};
1582
+ }
1583
+ node = node[key];
1584
+ }
1585
+ node[keys[keys.length - 1] ?? ""] = value;
1586
+ }
1587
+ function parseHexRgbToSignedArgbInt(hex) {
1588
+ const cleaned = hex.trim().replace(/^#/, "");
1589
+ if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) {
1590
+ return null;
1591
+ }
1592
+ const rgb = Number.parseInt(cleaned, 16);
1593
+ const argbUnsigned = 255 << 24 | rgb;
1594
+ return argbUnsigned > 2147483647 ? argbUnsigned - 4294967296 : argbUnsigned;
1595
+ }
1596
+ function isProfileDecorated(userDataDir, desiredName, desiredColorHex) {
1597
+ const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColorHex);
1598
+ const localStatePath = path2.join(userDataDir, "Local State");
1599
+ const preferencesPath = path2.join(userDataDir, "Default", "Preferences");
1600
+ const localState = safeReadJson(localStatePath);
1601
+ const profile = localState?.profile;
1602
+ const infoCache = typeof profile === "object" && profile !== null && !Array.isArray(profile) ? profile.info_cache : null;
1603
+ const info = typeof infoCache === "object" && infoCache !== null && !Array.isArray(infoCache) && typeof infoCache.Default === "object" && infoCache.Default !== null && !Array.isArray(infoCache.Default) ? infoCache.Default : null;
1604
+ const prefs = safeReadJson(preferencesPath);
1605
+ const browserTheme = (() => {
1606
+ const browser = prefs?.browser;
1607
+ const theme = typeof browser === "object" && browser !== null && !Array.isArray(browser) ? browser.theme : null;
1608
+ return typeof theme === "object" && theme !== null && !Array.isArray(theme) ? theme : null;
1609
+ })();
1610
+ const autogeneratedTheme = (() => {
1611
+ const autogenerated = prefs?.autogenerated;
1612
+ const theme = typeof autogenerated === "object" && autogenerated !== null && !Array.isArray(autogenerated) ? autogenerated.theme : null;
1613
+ return typeof theme === "object" && theme !== null && !Array.isArray(theme) ? theme : null;
1614
+ })();
1615
+ const nameOk = typeof info?.name === "string" ? info.name === desiredName : true;
1616
+ if (desiredColorInt == null) {
1617
+ return nameOk;
1618
+ }
1619
+ const localSeedOk = typeof info?.profile_color_seed === "number" ? info.profile_color_seed === desiredColorInt : false;
1620
+ const prefOk = typeof browserTheme?.user_color2 === "number" && browserTheme.user_color2 === desiredColorInt || typeof autogeneratedTheme?.color === "number" && autogeneratedTheme.color === desiredColorInt;
1621
+ return nameOk && localSeedOk && prefOk;
1622
+ }
1623
+ function decorateAgenticMailProfile(userDataDir, opts) {
1624
+ const desiredName = opts?.name ?? DEFAULT_AGENTICMAIL_BROWSER_PROFILE_NAME;
1625
+ const desiredColor = (opts?.color ?? DEFAULT_AGENTICMAIL_BROWSER_COLOR).toUpperCase();
1626
+ const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColor);
1627
+ const localStatePath = path2.join(userDataDir, "Local State");
1628
+ const preferencesPath = path2.join(userDataDir, "Default", "Preferences");
1629
+ const localState = safeReadJson(localStatePath) ?? {};
1630
+ setDeep(localState, ["profile", "info_cache", "Default", "name"], desiredName);
1631
+ setDeep(localState, ["profile", "info_cache", "Default", "shortcut_name"], desiredName);
1632
+ setDeep(localState, ["profile", "info_cache", "Default", "user_name"], desiredName);
1633
+ setDeep(localState, ["profile", "info_cache", "Default", "profile_color"], desiredColor);
1634
+ setDeep(localState, ["profile", "info_cache", "Default", "user_color"], desiredColor);
1635
+ if (desiredColorInt != null) {
1636
+ setDeep(
1637
+ localState,
1638
+ ["profile", "info_cache", "Default", "profile_color_seed"],
1639
+ desiredColorInt
1640
+ );
1641
+ setDeep(
1642
+ localState,
1643
+ ["profile", "info_cache", "Default", "profile_highlight_color"],
1644
+ desiredColorInt
1645
+ );
1646
+ setDeep(
1647
+ localState,
1648
+ ["profile", "info_cache", "Default", "default_avatar_fill_color"],
1649
+ desiredColorInt
1650
+ );
1651
+ setDeep(
1652
+ localState,
1653
+ ["profile", "info_cache", "Default", "default_avatar_stroke_color"],
1654
+ desiredColorInt
1655
+ );
1656
+ }
1657
+ safeWriteJson(localStatePath, localState);
1658
+ const prefs = safeReadJson(preferencesPath) ?? {};
1659
+ setDeep(prefs, ["profile", "name"], desiredName);
1660
+ setDeep(prefs, ["profile", "profile_color"], desiredColor);
1661
+ setDeep(prefs, ["profile", "user_color"], desiredColor);
1662
+ if (desiredColorInt != null) {
1663
+ setDeep(prefs, ["autogenerated", "theme", "color"], desiredColorInt);
1664
+ setDeep(prefs, ["browser", "theme", "user_color2"], desiredColorInt);
1665
+ }
1666
+ safeWriteJson(preferencesPath, prefs);
1667
+ try {
1668
+ fs2.writeFileSync(decoratedMarkerPath(userDataDir), `${Date.now()}
1669
+ `, "utf-8");
1670
+ } catch {
1671
+ }
1672
+ }
1673
+ function ensureProfileCleanExit(userDataDir) {
1674
+ const preferencesPath = path2.join(userDataDir, "Default", "Preferences");
1675
+ const prefs = safeReadJson(preferencesPath) ?? {};
1676
+ setDeep(prefs, ["exit_type"], "Normal");
1677
+ setDeep(prefs, ["exited_cleanly"], true);
1678
+ safeWriteJson(preferencesPath, prefs);
1679
+ }
1680
+
1681
+ // src/browser/chrome.ts
1682
+ var log = createSubsystemLogger("browser").child("chrome");
1683
+ function exists2(filePath) {
1684
+ try {
1685
+ return fs3.existsSync(filePath);
1686
+ } catch {
1687
+ return false;
1688
+ }
1689
+ }
1690
+ function resolveBrowserExecutable(resolved) {
1691
+ return resolveBrowserExecutableForPlatform(resolved, process.platform);
1692
+ }
1693
+ function resolveAgenticMailUserDataDir(profileName = DEFAULT_AGENTICMAIL_BROWSER_PROFILE_NAME) {
1694
+ return path3.join(CONFIG_DIR, "browser", profileName, "user-data");
1695
+ }
1696
+ function cdpUrlForPort(cdpPort) {
1697
+ return `http://127.0.0.1:${cdpPort}`;
1698
+ }
1699
+ async function isChromeReachable(cdpUrl, timeoutMs = 500) {
1700
+ const version = await fetchChromeVersion(cdpUrl, timeoutMs);
1701
+ return Boolean(version);
1702
+ }
1703
+ async function fetchChromeVersion(cdpUrl, timeoutMs = 500) {
1704
+ const ctrl = new AbortController();
1705
+ const t = setTimeout(ctrl.abort.bind(ctrl), timeoutMs);
1706
+ try {
1707
+ const versionUrl = appendCdpPath(cdpUrl, "/json/version");
1708
+ const res = await fetch(versionUrl, {
1709
+ signal: ctrl.signal,
1710
+ headers: getHeadersWithAuth(versionUrl)
1711
+ });
1712
+ if (!res.ok) {
1713
+ return null;
1714
+ }
1715
+ const data = await res.json();
1716
+ if (!data || typeof data !== "object") {
1717
+ return null;
1718
+ }
1719
+ return data;
1720
+ } catch {
1721
+ return null;
1722
+ } finally {
1723
+ clearTimeout(t);
1724
+ }
1725
+ }
1726
+ async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500) {
1727
+ const version = await fetchChromeVersion(cdpUrl, timeoutMs);
1728
+ const wsUrl = String(version?.webSocketDebuggerUrl ?? "").trim();
1729
+ if (!wsUrl) {
1730
+ return null;
1731
+ }
1732
+ return normalizeCdpWsUrl(wsUrl, cdpUrl);
1733
+ }
1734
+ async function canOpenWebSocket(wsUrl, timeoutMs = 800) {
1735
+ return await new Promise((resolve) => {
1736
+ const headers = getHeadersWithAuth(wsUrl);
1737
+ const ws = new WebSocket3(wsUrl, {
1738
+ handshakeTimeout: timeoutMs,
1739
+ ...Object.keys(headers).length ? { headers } : {}
1740
+ });
1741
+ const timer = setTimeout(
1742
+ () => {
1743
+ try {
1744
+ ws.terminate();
1745
+ } catch {
1746
+ }
1747
+ resolve(false);
1748
+ },
1749
+ Math.max(50, timeoutMs + 25)
1750
+ );
1751
+ ws.once("open", () => {
1752
+ clearTimeout(timer);
1753
+ try {
1754
+ ws.close();
1755
+ } catch {
1756
+ }
1757
+ resolve(true);
1758
+ });
1759
+ ws.once("error", () => {
1760
+ clearTimeout(timer);
1761
+ resolve(false);
1762
+ });
1763
+ });
1764
+ }
1765
+ async function isChromeCdpReady(cdpUrl, timeoutMs = 500, handshakeTimeoutMs = 800) {
1766
+ const wsUrl = await getChromeWebSocketUrl(cdpUrl, timeoutMs);
1767
+ if (!wsUrl) {
1768
+ return false;
1769
+ }
1770
+ return await canOpenWebSocket(wsUrl, handshakeTimeoutMs);
1771
+ }
1772
+ async function launchAgenticMailChrome(resolved, profile) {
1773
+ if (!profile.cdpIsLoopback) {
1774
+ throw new Error(`Profile "${profile.name}" is remote; cannot launch local Chrome.`);
1775
+ }
1776
+ await ensurePortAvailable(profile.cdpPort);
1777
+ const exe = resolveBrowserExecutable(resolved);
1778
+ if (!exe) {
1779
+ throw new Error(
1780
+ "No supported browser found (Chrome/Brave/Edge/Chromium on macOS, Linux, or Windows)."
1781
+ );
1782
+ }
1783
+ const userDataDir = resolveAgenticMailUserDataDir(profile.name);
1784
+ fs3.mkdirSync(userDataDir, { recursive: true });
1785
+ const needsDecorate = !isProfileDecorated(
1786
+ userDataDir,
1787
+ profile.name,
1788
+ (profile.color ?? DEFAULT_AGENTICMAIL_BROWSER_COLOR).toUpperCase()
1789
+ );
1790
+ const spawnOnce = () => {
1791
+ const args = [
1792
+ `--remote-debugging-port=${profile.cdpPort}`,
1793
+ `--user-data-dir=${userDataDir}`,
1794
+ "--no-first-run",
1795
+ "--no-default-browser-check",
1796
+ "--disable-sync",
1797
+ "--disable-background-networking",
1798
+ "--disable-component-update",
1799
+ "--disable-features=Translate,MediaRouter",
1800
+ "--disable-session-crashed-bubble",
1801
+ "--hide-crash-restore-bubble",
1802
+ "--password-store=basic",
1803
+ // Anti-throttling: prevent Chrome from throttling background tabs/pages
1804
+ // Critical for multi-tab agent workflows where tabs go inactive
1805
+ "--disable-background-timer-throttling",
1806
+ "--disable-backgrounding-occluded-windows",
1807
+ "--disable-renderer-backgrounding"
1808
+ ];
1809
+ if (resolved.headless) {
1810
+ args.push("--headless=new");
1811
+ args.push("--disable-gpu");
1812
+ }
1813
+ if (resolved.noSandbox) {
1814
+ args.push("--no-sandbox");
1815
+ args.push("--disable-setuid-sandbox");
1816
+ }
1817
+ if (process.platform === "linux") {
1818
+ args.push("--disable-dev-shm-usage");
1819
+ }
1820
+ args.push("--disable-blink-features=AutomationControlled");
1821
+ if (resolved.extraArgs.length > 0) {
1822
+ args.push(...resolved.extraArgs);
1823
+ }
1824
+ args.push("about:blank");
1825
+ return spawn(exe.path, args, {
1826
+ stdio: "pipe",
1827
+ env: {
1828
+ ...process.env,
1829
+ // Reduce accidental sharing with the user's env.
1830
+ HOME: os2.homedir()
1831
+ }
1832
+ });
1833
+ };
1834
+ const startedAt = Date.now();
1835
+ const localStatePath = path3.join(userDataDir, "Local State");
1836
+ const preferencesPath = path3.join(userDataDir, "Default", "Preferences");
1837
+ const needsBootstrap = !exists2(localStatePath) || !exists2(preferencesPath);
1838
+ if (needsBootstrap) {
1839
+ const bootstrap = spawnOnce();
1840
+ const deadline = Date.now() + 1e4;
1841
+ while (Date.now() < deadline) {
1842
+ if (exists2(localStatePath) && exists2(preferencesPath)) {
1843
+ break;
1844
+ }
1845
+ await new Promise((r) => setTimeout(r, 100));
1846
+ }
1847
+ try {
1848
+ bootstrap.kill("SIGTERM");
1849
+ } catch {
1850
+ }
1851
+ const exitDeadline = Date.now() + 5e3;
1852
+ while (Date.now() < exitDeadline) {
1853
+ if (bootstrap.exitCode != null) {
1854
+ break;
1855
+ }
1856
+ await new Promise((r) => setTimeout(r, 50));
1857
+ }
1858
+ }
1859
+ if (needsDecorate) {
1860
+ try {
1861
+ decorateAgenticMailProfile(userDataDir, {
1862
+ name: profile.name,
1863
+ color: profile.color
1864
+ });
1865
+ log.info(`\u{1F99E} agenticmail browser profile decorated (${profile.color})`);
1866
+ } catch (err) {
1867
+ log.warn(`agenticmail browser profile decoration failed: ${String(err)}`);
1868
+ }
1869
+ }
1870
+ try {
1871
+ ensureProfileCleanExit(userDataDir);
1872
+ } catch (err) {
1873
+ log.warn(`agenticmail browser clean-exit prefs failed: ${String(err)}`);
1874
+ }
1875
+ const proc = spawnOnce();
1876
+ const readyDeadline = Date.now() + 15e3;
1877
+ while (Date.now() < readyDeadline) {
1878
+ if (await isChromeReachable(profile.cdpUrl, 500)) {
1879
+ break;
1880
+ }
1881
+ await new Promise((r) => setTimeout(r, 200));
1882
+ }
1883
+ if (!await isChromeReachable(profile.cdpUrl, 500)) {
1884
+ try {
1885
+ proc.kill("SIGKILL");
1886
+ } catch {
1887
+ }
1888
+ throw new Error(
1889
+ `Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}".`
1890
+ );
1891
+ }
1892
+ const pid = proc.pid ?? -1;
1893
+ log.info(
1894
+ `Agenticmail browser started (${exe.kind}) profile "${profile.name}" on 127.0.0.1:${profile.cdpPort} (pid ${pid})`
1895
+ );
1896
+ return {
1897
+ pid,
1898
+ exe,
1899
+ userDataDir,
1900
+ cdpPort: profile.cdpPort,
1901
+ startedAt,
1902
+ proc
1903
+ };
1904
+ }
1905
+ async function stopAgenticMailChrome(running, timeoutMs = 2500) {
1906
+ const proc = running.proc;
1907
+ if (proc.killed) {
1908
+ return;
1909
+ }
1910
+ try {
1911
+ proc.kill("SIGTERM");
1912
+ } catch {
1913
+ }
1914
+ const start = Date.now();
1915
+ while (Date.now() - start < timeoutMs) {
1916
+ if (!proc.exitCode && proc.killed) {
1917
+ break;
1918
+ }
1919
+ if (!await isChromeReachable(cdpUrlForPort(running.cdpPort), 200)) {
1920
+ return;
1921
+ }
1922
+ await new Promise((r) => setTimeout(r, 100));
1923
+ }
1924
+ try {
1925
+ proc.kill("SIGKILL");
1926
+ } catch {
1927
+ }
1928
+ }
1929
+
1930
+ export {
1931
+ DEFAULT_AGENTICMAIL_BROWSER_ENABLED,
1932
+ DEFAULT_BROWSER_EVALUATE_ENABLED,
1933
+ DEFAULT_AGENTICMAIL_BROWSER_COLOR,
1934
+ DEFAULT_AGENTICMAIL_BROWSER_PROFILE_NAME,
1935
+ DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
1936
+ DEFAULT_AI_SNAPSHOT_MAX_CHARS,
1937
+ DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS,
1938
+ DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH,
1939
+ ensureChromeExtensionRelayServer,
1940
+ stopChromeExtensionRelayServer,
1941
+ getHeadersWithAuth,
1942
+ appendCdpPath,
1943
+ fetchJson,
1944
+ fetchOk,
1945
+ withCdpSocket,
1946
+ InvalidBrowserNavigationUrlError,
1947
+ withBrowserNavigationPolicy,
1948
+ assertBrowserNavigationAllowed,
1949
+ normalizeCdpWsUrl,
1950
+ captureScreenshot,
1951
+ createTargetViaCdp,
1952
+ formatAriaSnapshot,
1953
+ snapshotAria,
1954
+ resolveBrowserExecutableForPlatform,
1955
+ resolveAgenticMailUserDataDir,
1956
+ isChromeReachable,
1957
+ getChromeWebSocketUrl,
1958
+ isChromeCdpReady,
1959
+ launchAgenticMailChrome,
1960
+ stopAgenticMailChrome
1961
+ };