@agenticmail/enterprise 0.5.78 → 0.5.79

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.
Files changed (95) hide show
  1. package/dist/chunk-7RNT4O5T.js +15198 -0
  2. package/dist/chunk-AGFOJCSB.js +2191 -0
  3. package/dist/chunk-F4GSFCM3.js +898 -0
  4. package/dist/chunk-PZA7YOJE.js +898 -0
  5. package/dist/chunk-Q3V7VZFQ.js +2191 -0
  6. package/dist/chunk-RRFB6G6M.js +15198 -0
  7. package/dist/chunk-VX3VFMVB.js +409 -0
  8. package/dist/cli.js +1 -1
  9. package/dist/dashboard/pages/agent-detail.js +313 -1
  10. package/dist/index.js +4 -3
  11. package/dist/pw-ai-KPETTB25.js +2212 -0
  12. package/dist/routes-PDHMCIXU.js +6676 -0
  13. package/dist/runtime-7HW4GX5L.js +48 -0
  14. package/dist/runtime-XXDCZZIK.js +48 -0
  15. package/dist/server-FMP4BFGW.js +12 -0
  16. package/dist/server-JRHDUNII.js +12 -0
  17. package/dist/setup-O5FPRLK4.js +20 -0
  18. package/dist/setup-S4Z4PPIJ.js +20 -0
  19. package/package.json +15 -2
  20. package/src/agent-tools/common.ts +25 -0
  21. package/src/agent-tools/index.ts +3 -0
  22. package/src/agent-tools/schema/typebox.ts +25 -0
  23. package/src/agent-tools/tools/browser-tool.schema.ts +112 -0
  24. package/src/agent-tools/tools/browser-tool.ts +388 -0
  25. package/src/agent-tools/tools/gateway.ts +126 -0
  26. package/src/agent-tools/tools/nodes-utils.ts +80 -0
  27. package/src/browser/bridge-auth-registry.ts +34 -0
  28. package/src/browser/bridge-server.ts +93 -0
  29. package/src/browser/cdp.helpers.ts +180 -0
  30. package/src/browser/cdp.ts +466 -0
  31. package/src/browser/chrome.executables.ts +625 -0
  32. package/src/browser/chrome.profile-decoration.ts +198 -0
  33. package/src/browser/chrome.ts +349 -0
  34. package/src/browser/client-actions-core.ts +259 -0
  35. package/src/browser/client-actions-observe.ts +184 -0
  36. package/src/browser/client-actions-state.ts +284 -0
  37. package/src/browser/client-actions-types.ts +16 -0
  38. package/src/browser/client-actions-url.ts +11 -0
  39. package/src/browser/client-actions.ts +4 -0
  40. package/src/browser/client-fetch.ts +253 -0
  41. package/src/browser/client.ts +337 -0
  42. package/src/browser/config.ts +296 -0
  43. package/src/browser/constants.ts +8 -0
  44. package/src/browser/control-auth.ts +94 -0
  45. package/src/browser/control-service.ts +81 -0
  46. package/src/browser/csrf.ts +87 -0
  47. package/src/browser/enterprise-compat.ts +518 -0
  48. package/src/browser/extension-relay.ts +834 -0
  49. package/src/browser/http-auth.ts +63 -0
  50. package/src/browser/navigation-guard.ts +50 -0
  51. package/src/browser/paths.ts +49 -0
  52. package/src/browser/profiles-service.ts +187 -0
  53. package/src/browser/profiles.ts +113 -0
  54. package/src/browser/proxy-files.ts +41 -0
  55. package/src/browser/pw-ai-module.ts +52 -0
  56. package/src/browser/pw-ai-state.ts +9 -0
  57. package/src/browser/pw-ai.ts +65 -0
  58. package/src/browser/pw-role-snapshot.ts +434 -0
  59. package/src/browser/pw-session.ts +810 -0
  60. package/src/browser/pw-tools-core.activity.ts +68 -0
  61. package/src/browser/pw-tools-core.downloads.ts +281 -0
  62. package/src/browser/pw-tools-core.interactions.ts +646 -0
  63. package/src/browser/pw-tools-core.responses.ts +124 -0
  64. package/src/browser/pw-tools-core.shared.ts +70 -0
  65. package/src/browser/pw-tools-core.snapshot.ts +213 -0
  66. package/src/browser/pw-tools-core.state.ts +209 -0
  67. package/src/browser/pw-tools-core.storage.ts +128 -0
  68. package/src/browser/pw-tools-core.trace.ts +37 -0
  69. package/src/browser/pw-tools-core.ts +8 -0
  70. package/src/browser/resolved-config-refresh.ts +59 -0
  71. package/src/browser/routes/agent.act.shared.ts +52 -0
  72. package/src/browser/routes/agent.act.ts +575 -0
  73. package/src/browser/routes/agent.debug.ts +149 -0
  74. package/src/browser/routes/agent.shared.ts +143 -0
  75. package/src/browser/routes/agent.snapshot.ts +333 -0
  76. package/src/browser/routes/agent.storage.ts +451 -0
  77. package/src/browser/routes/agent.ts +13 -0
  78. package/src/browser/routes/basic.ts +202 -0
  79. package/src/browser/routes/dispatcher.ts +126 -0
  80. package/src/browser/routes/index.ts +11 -0
  81. package/src/browser/routes/path-output.ts +1 -0
  82. package/src/browser/routes/tabs.ts +217 -0
  83. package/src/browser/routes/types.ts +26 -0
  84. package/src/browser/routes/utils.ts +73 -0
  85. package/src/browser/screenshot.ts +54 -0
  86. package/src/browser/server-context.ts +688 -0
  87. package/src/browser/server-context.types.ts +65 -0
  88. package/src/browser/server-lifecycle.ts +48 -0
  89. package/src/browser/server-middleware.ts +37 -0
  90. package/src/browser/server.ts +110 -0
  91. package/src/browser/target-id.ts +30 -0
  92. package/src/browser/trash.ts +21 -0
  93. package/src/dashboard/pages/agent-detail.js +313 -1
  94. package/src/engine/agent-routes.ts +46 -0
  95. package/src/security/external-content.ts +299 -0
@@ -0,0 +1,451 @@
1
+ import type { BrowserRouteContext } from "../server-context.js";
2
+ import {
3
+ readBody,
4
+ resolveTargetIdFromBody,
5
+ resolveTargetIdFromQuery,
6
+ withPlaywrightRouteContext,
7
+ } from "./agent.shared.js";
8
+ import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js";
9
+ import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js";
10
+
11
+ type StorageKind = "local" | "session";
12
+
13
+ export function parseStorageKind(raw: string): StorageKind | null {
14
+ if (raw === "local" || raw === "session") {
15
+ return raw;
16
+ }
17
+ return null;
18
+ }
19
+
20
+ export function parseStorageMutationRequest(
21
+ kindParam: unknown,
22
+ body: Record<string, unknown>,
23
+ ): { kind: StorageKind | null; targetId: string | undefined } {
24
+ return {
25
+ kind: parseStorageKind(toStringOrEmpty(kindParam)),
26
+ targetId: resolveTargetIdFromBody(body),
27
+ };
28
+ }
29
+
30
+ export function parseRequiredStorageMutationRequest(
31
+ kindParam: unknown,
32
+ body: Record<string, unknown>,
33
+ ): { kind: StorageKind; targetId: string | undefined } | null {
34
+ const parsed = parseStorageMutationRequest(kindParam, body);
35
+ if (!parsed.kind) {
36
+ return null;
37
+ }
38
+ return {
39
+ kind: parsed.kind,
40
+ targetId: parsed.targetId,
41
+ };
42
+ }
43
+
44
+ function parseStorageMutationOrRespond(
45
+ res: BrowserResponse,
46
+ kindParam: unknown,
47
+ body: Record<string, unknown>,
48
+ ) {
49
+ const parsed = parseRequiredStorageMutationRequest(kindParam, body);
50
+ if (!parsed) {
51
+ jsonError(res, 400, "kind must be local|session");
52
+ return null;
53
+ }
54
+ return parsed;
55
+ }
56
+
57
+ function parseStorageMutationFromRequest(req: BrowserRequest, res: BrowserResponse) {
58
+ const body = readBody(req);
59
+ const parsed = parseStorageMutationOrRespond(res, req.params.kind, body);
60
+ if (!parsed) {
61
+ return null;
62
+ }
63
+ return { body, parsed };
64
+ }
65
+
66
+ export function registerBrowserAgentStorageRoutes(
67
+ app: BrowserRouteRegistrar,
68
+ ctx: BrowserRouteContext,
69
+ ) {
70
+ app.get("/cookies", async (req, res) => {
71
+ const targetId = resolveTargetIdFromQuery(req.query);
72
+ await withPlaywrightRouteContext({
73
+ req,
74
+ res,
75
+ ctx,
76
+ targetId,
77
+ feature: "cookies",
78
+ run: async ({ cdpUrl, tab, pw }) => {
79
+ const result = await pw.cookiesGetViaPlaywright({
80
+ cdpUrl,
81
+ targetId: tab.targetId,
82
+ });
83
+ res.json({ ok: true, targetId: tab.targetId, ...result });
84
+ },
85
+ });
86
+ });
87
+
88
+ app.post("/cookies/set", async (req, res) => {
89
+ const body = readBody(req);
90
+ const targetId = resolveTargetIdFromBody(body);
91
+ const cookie =
92
+ body.cookie && typeof body.cookie === "object" && !Array.isArray(body.cookie)
93
+ ? (body.cookie as Record<string, unknown>)
94
+ : null;
95
+ if (!cookie) {
96
+ return jsonError(res, 400, "cookie is required");
97
+ }
98
+
99
+ await withPlaywrightRouteContext({
100
+ req,
101
+ res,
102
+ ctx,
103
+ targetId,
104
+ feature: "cookies set",
105
+ run: async ({ cdpUrl, tab, pw }) => {
106
+ await pw.cookiesSetViaPlaywright({
107
+ cdpUrl,
108
+ targetId: tab.targetId,
109
+ cookie: {
110
+ name: toStringOrEmpty(cookie.name),
111
+ value: toStringOrEmpty(cookie.value),
112
+ url: toStringOrEmpty(cookie.url) || undefined,
113
+ domain: toStringOrEmpty(cookie.domain) || undefined,
114
+ path: toStringOrEmpty(cookie.path) || undefined,
115
+ expires: toNumber(cookie.expires) ?? undefined,
116
+ httpOnly: toBoolean(cookie.httpOnly) ?? undefined,
117
+ secure: toBoolean(cookie.secure) ?? undefined,
118
+ sameSite:
119
+ cookie.sameSite === "Lax" ||
120
+ cookie.sameSite === "None" ||
121
+ cookie.sameSite === "Strict"
122
+ ? cookie.sameSite
123
+ : undefined,
124
+ },
125
+ });
126
+ res.json({ ok: true, targetId: tab.targetId });
127
+ },
128
+ });
129
+ });
130
+
131
+ app.post("/cookies/clear", async (req, res) => {
132
+ const body = readBody(req);
133
+ const targetId = resolveTargetIdFromBody(body);
134
+
135
+ await withPlaywrightRouteContext({
136
+ req,
137
+ res,
138
+ ctx,
139
+ targetId,
140
+ feature: "cookies clear",
141
+ run: async ({ cdpUrl, tab, pw }) => {
142
+ await pw.cookiesClearViaPlaywright({
143
+ cdpUrl,
144
+ targetId: tab.targetId,
145
+ });
146
+ res.json({ ok: true, targetId: tab.targetId });
147
+ },
148
+ });
149
+ });
150
+
151
+ app.get("/storage/:kind", async (req, res) => {
152
+ const kind = parseStorageKind(toStringOrEmpty(req.params.kind));
153
+ if (!kind) {
154
+ return jsonError(res, 400, "kind must be local|session");
155
+ }
156
+ const targetId = resolveTargetIdFromQuery(req.query);
157
+ const key = toStringOrEmpty(req.query.key);
158
+
159
+ await withPlaywrightRouteContext({
160
+ req,
161
+ res,
162
+ ctx,
163
+ targetId,
164
+ feature: "storage get",
165
+ run: async ({ cdpUrl, tab, pw }) => {
166
+ const result = await pw.storageGetViaPlaywright({
167
+ cdpUrl,
168
+ targetId: tab.targetId,
169
+ kind,
170
+ key: key.trim() || undefined,
171
+ });
172
+ res.json({ ok: true, targetId: tab.targetId, ...result });
173
+ },
174
+ });
175
+ });
176
+
177
+ app.post("/storage/:kind/set", async (req, res) => {
178
+ const mutation = parseStorageMutationFromRequest(req, res);
179
+ if (!mutation) {
180
+ return;
181
+ }
182
+ const key = toStringOrEmpty(mutation.body.key);
183
+ if (!key) {
184
+ return jsonError(res, 400, "key is required");
185
+ }
186
+ const value = typeof mutation.body.value === "string" ? mutation.body.value : "";
187
+
188
+ await withPlaywrightRouteContext({
189
+ req,
190
+ res,
191
+ ctx,
192
+ targetId: mutation.parsed.targetId,
193
+ feature: "storage set",
194
+ run: async ({ cdpUrl, tab, pw }) => {
195
+ await pw.storageSetViaPlaywright({
196
+ cdpUrl,
197
+ targetId: tab.targetId,
198
+ kind: mutation.parsed.kind,
199
+ key,
200
+ value,
201
+ });
202
+ res.json({ ok: true, targetId: tab.targetId });
203
+ },
204
+ });
205
+ });
206
+
207
+ app.post("/storage/:kind/clear", async (req, res) => {
208
+ const mutation = parseStorageMutationFromRequest(req, res);
209
+ if (!mutation) {
210
+ return;
211
+ }
212
+
213
+ await withPlaywrightRouteContext({
214
+ req,
215
+ res,
216
+ ctx,
217
+ targetId: mutation.parsed.targetId,
218
+ feature: "storage clear",
219
+ run: async ({ cdpUrl, tab, pw }) => {
220
+ await pw.storageClearViaPlaywright({
221
+ cdpUrl,
222
+ targetId: tab.targetId,
223
+ kind: mutation.parsed.kind,
224
+ });
225
+ res.json({ ok: true, targetId: tab.targetId });
226
+ },
227
+ });
228
+ });
229
+
230
+ app.post("/set/offline", async (req, res) => {
231
+ const body = readBody(req);
232
+ const targetId = resolveTargetIdFromBody(body);
233
+ const offline = toBoolean(body.offline);
234
+ if (offline === undefined) {
235
+ return jsonError(res, 400, "offline is required");
236
+ }
237
+
238
+ await withPlaywrightRouteContext({
239
+ req,
240
+ res,
241
+ ctx,
242
+ targetId,
243
+ feature: "offline",
244
+ run: async ({ cdpUrl, tab, pw }) => {
245
+ await pw.setOfflineViaPlaywright({
246
+ cdpUrl,
247
+ targetId: tab.targetId,
248
+ offline,
249
+ });
250
+ res.json({ ok: true, targetId: tab.targetId });
251
+ },
252
+ });
253
+ });
254
+
255
+ app.post("/set/headers", async (req, res) => {
256
+ const body = readBody(req);
257
+ const targetId = resolveTargetIdFromBody(body);
258
+ const headers =
259
+ body.headers && typeof body.headers === "object" && !Array.isArray(body.headers)
260
+ ? (body.headers as Record<string, unknown>)
261
+ : null;
262
+ if (!headers) {
263
+ return jsonError(res, 400, "headers is required");
264
+ }
265
+
266
+ const parsed: Record<string, string> = {};
267
+ for (const [k, v] of Object.entries(headers)) {
268
+ if (typeof v === "string") {
269
+ parsed[k] = v;
270
+ }
271
+ }
272
+
273
+ await withPlaywrightRouteContext({
274
+ req,
275
+ res,
276
+ ctx,
277
+ targetId,
278
+ feature: "headers",
279
+ run: async ({ cdpUrl, tab, pw }) => {
280
+ await pw.setExtraHTTPHeadersViaPlaywright({
281
+ cdpUrl,
282
+ targetId: tab.targetId,
283
+ headers: parsed,
284
+ });
285
+ res.json({ ok: true, targetId: tab.targetId });
286
+ },
287
+ });
288
+ });
289
+
290
+ app.post("/set/credentials", async (req, res) => {
291
+ const body = readBody(req);
292
+ const targetId = resolveTargetIdFromBody(body);
293
+ const clear = toBoolean(body.clear) ?? false;
294
+ const username = toStringOrEmpty(body.username) || undefined;
295
+ const password = typeof body.password === "string" ? body.password : undefined;
296
+
297
+ await withPlaywrightRouteContext({
298
+ req,
299
+ res,
300
+ ctx,
301
+ targetId,
302
+ feature: "http credentials",
303
+ run: async ({ cdpUrl, tab, pw }) => {
304
+ await pw.setHttpCredentialsViaPlaywright({
305
+ cdpUrl,
306
+ targetId: tab.targetId,
307
+ username,
308
+ password,
309
+ clear,
310
+ });
311
+ res.json({ ok: true, targetId: tab.targetId });
312
+ },
313
+ });
314
+ });
315
+
316
+ app.post("/set/geolocation", async (req, res) => {
317
+ const body = readBody(req);
318
+ const targetId = resolveTargetIdFromBody(body);
319
+ const clear = toBoolean(body.clear) ?? false;
320
+ const latitude = toNumber(body.latitude);
321
+ const longitude = toNumber(body.longitude);
322
+ const accuracy = toNumber(body.accuracy) ?? undefined;
323
+ const origin = toStringOrEmpty(body.origin) || undefined;
324
+
325
+ await withPlaywrightRouteContext({
326
+ req,
327
+ res,
328
+ ctx,
329
+ targetId,
330
+ feature: "geolocation",
331
+ run: async ({ cdpUrl, tab, pw }) => {
332
+ await pw.setGeolocationViaPlaywright({
333
+ cdpUrl,
334
+ targetId: tab.targetId,
335
+ latitude,
336
+ longitude,
337
+ accuracy,
338
+ origin,
339
+ clear,
340
+ });
341
+ res.json({ ok: true, targetId: tab.targetId });
342
+ },
343
+ });
344
+ });
345
+
346
+ app.post("/set/media", async (req, res) => {
347
+ const body = readBody(req);
348
+ const targetId = resolveTargetIdFromBody(body);
349
+ const schemeRaw = toStringOrEmpty(body.colorScheme);
350
+ const colorScheme =
351
+ schemeRaw === "dark" || schemeRaw === "light" || schemeRaw === "no-preference"
352
+ ? schemeRaw
353
+ : schemeRaw === "none"
354
+ ? null
355
+ : undefined;
356
+ if (colorScheme === undefined) {
357
+ return jsonError(res, 400, "colorScheme must be dark|light|no-preference|none");
358
+ }
359
+
360
+ await withPlaywrightRouteContext({
361
+ req,
362
+ res,
363
+ ctx,
364
+ targetId,
365
+ feature: "media emulation",
366
+ run: async ({ cdpUrl, tab, pw }) => {
367
+ await pw.emulateMediaViaPlaywright({
368
+ cdpUrl,
369
+ targetId: tab.targetId,
370
+ colorScheme,
371
+ });
372
+ res.json({ ok: true, targetId: tab.targetId });
373
+ },
374
+ });
375
+ });
376
+
377
+ app.post("/set/timezone", async (req, res) => {
378
+ const body = readBody(req);
379
+ const targetId = resolveTargetIdFromBody(body);
380
+ const timezoneId = toStringOrEmpty(body.timezoneId);
381
+ if (!timezoneId) {
382
+ return jsonError(res, 400, "timezoneId is required");
383
+ }
384
+
385
+ await withPlaywrightRouteContext({
386
+ req,
387
+ res,
388
+ ctx,
389
+ targetId,
390
+ feature: "timezone",
391
+ run: async ({ cdpUrl, tab, pw }) => {
392
+ await pw.setTimezoneViaPlaywright({
393
+ cdpUrl,
394
+ targetId: tab.targetId,
395
+ timezoneId,
396
+ });
397
+ res.json({ ok: true, targetId: tab.targetId });
398
+ },
399
+ });
400
+ });
401
+
402
+ app.post("/set/locale", async (req, res) => {
403
+ const body = readBody(req);
404
+ const targetId = resolveTargetIdFromBody(body);
405
+ const locale = toStringOrEmpty(body.locale);
406
+ if (!locale) {
407
+ return jsonError(res, 400, "locale is required");
408
+ }
409
+
410
+ await withPlaywrightRouteContext({
411
+ req,
412
+ res,
413
+ ctx,
414
+ targetId,
415
+ feature: "locale",
416
+ run: async ({ cdpUrl, tab, pw }) => {
417
+ await pw.setLocaleViaPlaywright({
418
+ cdpUrl,
419
+ targetId: tab.targetId,
420
+ locale,
421
+ });
422
+ res.json({ ok: true, targetId: tab.targetId });
423
+ },
424
+ });
425
+ });
426
+
427
+ app.post("/set/device", async (req, res) => {
428
+ const body = readBody(req);
429
+ const targetId = resolveTargetIdFromBody(body);
430
+ const name = toStringOrEmpty(body.name);
431
+ if (!name) {
432
+ return jsonError(res, 400, "name is required");
433
+ }
434
+
435
+ await withPlaywrightRouteContext({
436
+ req,
437
+ res,
438
+ ctx,
439
+ targetId,
440
+ feature: "device emulation",
441
+ run: async ({ cdpUrl, tab, pw }) => {
442
+ await pw.setDeviceViaPlaywright({
443
+ cdpUrl,
444
+ targetId: tab.targetId,
445
+ name,
446
+ });
447
+ res.json({ ok: true, targetId: tab.targetId });
448
+ },
449
+ });
450
+ });
451
+ }
@@ -0,0 +1,13 @@
1
+ import type { BrowserRouteContext } from "../server-context.js";
2
+ import { registerBrowserAgentActRoutes } from "./agent.act.js";
3
+ import { registerBrowserAgentDebugRoutes } from "./agent.debug.js";
4
+ import { registerBrowserAgentSnapshotRoutes } from "./agent.snapshot.js";
5
+ import { registerBrowserAgentStorageRoutes } from "./agent.storage.js";
6
+ import type { BrowserRouteRegistrar } from "./types.js";
7
+
8
+ export function registerBrowserAgentRoutes(app: BrowserRouteRegistrar, ctx: BrowserRouteContext) {
9
+ registerBrowserAgentSnapshotRoutes(app, ctx);
10
+ registerBrowserAgentActRoutes(app, ctx);
11
+ registerBrowserAgentDebugRoutes(app, ctx);
12
+ registerBrowserAgentStorageRoutes(app, ctx);
13
+ }
@@ -0,0 +1,202 @@
1
+ import { resolveBrowserExecutableForPlatform } from "../chrome.executables.js";
2
+ import { createBrowserProfilesService } from "../profiles-service.js";
3
+ import type { BrowserRouteContext, ProfileContext } from "../server-context.js";
4
+ import { resolveProfileContext } from "./agent.shared.js";
5
+ import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js";
6
+ import { getProfileContext, jsonError, toStringOrEmpty } from "./utils.js";
7
+
8
+ async function withBasicProfileRoute(params: {
9
+ req: BrowserRequest;
10
+ res: BrowserResponse;
11
+ ctx: BrowserRouteContext;
12
+ run: (profileCtx: ProfileContext) => Promise<void>;
13
+ }) {
14
+ const profileCtx = resolveProfileContext(params.req, params.res, params.ctx);
15
+ if (!profileCtx) {
16
+ return;
17
+ }
18
+ try {
19
+ await params.run(profileCtx);
20
+ } catch (err) {
21
+ jsonError(params.res, 500, String(err));
22
+ }
23
+ }
24
+
25
+ export function registerBrowserBasicRoutes(app: BrowserRouteRegistrar, ctx: BrowserRouteContext) {
26
+ // List all profiles with their status
27
+ app.get("/profiles", async (_req, res) => {
28
+ try {
29
+ const service = createBrowserProfilesService(ctx);
30
+ const profiles = await service.listProfiles();
31
+ res.json({ profiles });
32
+ } catch (err) {
33
+ jsonError(res, 500, String(err));
34
+ }
35
+ });
36
+
37
+ // Get status (profile-aware)
38
+ app.get("/", async (req, res) => {
39
+ let current: ReturnType<typeof ctx.state>;
40
+ try {
41
+ current = ctx.state();
42
+ } catch {
43
+ return jsonError(res, 503, "browser server not started");
44
+ }
45
+
46
+ const profileCtx = getProfileContext(req, ctx);
47
+ if ("error" in profileCtx) {
48
+ return jsonError(res, profileCtx.status, profileCtx.error);
49
+ }
50
+
51
+ const [cdpHttp, cdpReady] = await Promise.all([
52
+ profileCtx.isHttpReachable(300),
53
+ profileCtx.isReachable(600),
54
+ ]);
55
+
56
+ const profileState = current.profiles.get(profileCtx.profile.name);
57
+ let detectedBrowser: string | null = null;
58
+ let detectedExecutablePath: string | null = null;
59
+ let detectError: string | null = null;
60
+
61
+ try {
62
+ const detected = resolveBrowserExecutableForPlatform(current.resolved, process.platform);
63
+ if (detected) {
64
+ detectedBrowser = detected.kind;
65
+ detectedExecutablePath = detected.path;
66
+ }
67
+ } catch (err) {
68
+ detectError = String(err);
69
+ }
70
+
71
+ res.json({
72
+ enabled: current.resolved.enabled,
73
+ profile: profileCtx.profile.name,
74
+ running: cdpReady,
75
+ cdpReady,
76
+ cdpHttp,
77
+ pid: profileState?.running?.pid ?? null,
78
+ cdpPort: profileCtx.profile.cdpPort,
79
+ cdpUrl: profileCtx.profile.cdpUrl,
80
+ chosenBrowser: profileState?.running?.exe.kind ?? null,
81
+ detectedBrowser,
82
+ detectedExecutablePath,
83
+ detectError,
84
+ userDataDir: profileState?.running?.userDataDir ?? null,
85
+ color: profileCtx.profile.color,
86
+ headless: current.resolved.headless,
87
+ noSandbox: current.resolved.noSandbox,
88
+ executablePath: current.resolved.executablePath ?? null,
89
+ attachOnly: current.resolved.attachOnly,
90
+ });
91
+ });
92
+
93
+ // Start browser (profile-aware)
94
+ app.post("/start", async (req, res) => {
95
+ await withBasicProfileRoute({
96
+ req,
97
+ res,
98
+ ctx,
99
+ run: async (profileCtx) => {
100
+ await profileCtx.ensureBrowserAvailable();
101
+ res.json({ ok: true, profile: profileCtx.profile.name });
102
+ },
103
+ });
104
+ });
105
+
106
+ // Stop browser (profile-aware)
107
+ app.post("/stop", async (req, res) => {
108
+ await withBasicProfileRoute({
109
+ req,
110
+ res,
111
+ ctx,
112
+ run: async (profileCtx) => {
113
+ const result = await profileCtx.stopRunningBrowser();
114
+ res.json({
115
+ ok: true,
116
+ stopped: result.stopped,
117
+ profile: profileCtx.profile.name,
118
+ });
119
+ },
120
+ });
121
+ });
122
+
123
+ // Reset profile (profile-aware)
124
+ app.post("/reset-profile", async (req, res) => {
125
+ await withBasicProfileRoute({
126
+ req,
127
+ res,
128
+ ctx,
129
+ run: async (profileCtx) => {
130
+ const result = await profileCtx.resetProfile();
131
+ res.json({ ok: true, profile: profileCtx.profile.name, ...result });
132
+ },
133
+ });
134
+ });
135
+
136
+ // Create a new profile
137
+ app.post("/profiles/create", async (req, res) => {
138
+ const name = toStringOrEmpty((req.body as { name?: unknown })?.name);
139
+ const color = toStringOrEmpty((req.body as { color?: unknown })?.color);
140
+ const cdpUrl = toStringOrEmpty((req.body as { cdpUrl?: unknown })?.cdpUrl);
141
+ const driver = toStringOrEmpty((req.body as { driver?: unknown })?.driver) as
142
+ | "openclaw"
143
+ | "extension"
144
+ | "";
145
+
146
+ if (!name) {
147
+ return jsonError(res, 400, "name is required");
148
+ }
149
+
150
+ try {
151
+ const service = createBrowserProfilesService(ctx);
152
+ const result = await service.createProfile({
153
+ name,
154
+ color: color || undefined,
155
+ cdpUrl: cdpUrl || undefined,
156
+ driver: driver === "extension" ? "extension" : undefined,
157
+ });
158
+ res.json(result);
159
+ } catch (err) {
160
+ const msg = String(err);
161
+ if (msg.includes("already exists")) {
162
+ return jsonError(res, 409, msg);
163
+ }
164
+ if (msg.includes("invalid profile name")) {
165
+ return jsonError(res, 400, msg);
166
+ }
167
+ if (msg.includes("no available CDP ports")) {
168
+ return jsonError(res, 507, msg);
169
+ }
170
+ if (msg.includes("cdpUrl")) {
171
+ return jsonError(res, 400, msg);
172
+ }
173
+ jsonError(res, 500, msg);
174
+ }
175
+ });
176
+
177
+ // Delete a profile
178
+ app.delete("/profiles/:name", async (req, res) => {
179
+ const name = toStringOrEmpty(req.params.name);
180
+ if (!name) {
181
+ return jsonError(res, 400, "profile name is required");
182
+ }
183
+
184
+ try {
185
+ const service = createBrowserProfilesService(ctx);
186
+ const result = await service.deleteProfile(name);
187
+ res.json(result);
188
+ } catch (err) {
189
+ const msg = String(err);
190
+ if (msg.includes("invalid profile name")) {
191
+ return jsonError(res, 400, msg);
192
+ }
193
+ if (msg.includes("default profile")) {
194
+ return jsonError(res, 400, msg);
195
+ }
196
+ if (msg.includes("not found")) {
197
+ return jsonError(res, 404, msg);
198
+ }
199
+ jsonError(res, 500, msg);
200
+ }
201
+ });
202
+ }