@vellumai/assistant 0.4.21 → 0.4.23

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.
@@ -1,30 +1,30 @@
1
- import type { ImageContent } from '../../providers/types.js';
2
- import { getLogger } from '../../util/logger.js';
3
- import { truncate } from '../../util/truncate.js';
4
- import { credentialBroker } from '../credentials/broker.js';
1
+ import type { ImageContent } from "../../providers/types.js";
2
+ import { getLogger } from "../../util/logger.js";
3
+ import { truncate } from "../../util/truncate.js";
4
+ import { credentialBroker } from "../credentials/broker.js";
5
5
  import {
6
6
  isPrivateOrLocalHost,
7
7
  parseUrl,
8
8
  resolveHostAddresses,
9
9
  resolveRequestAddress,
10
10
  sanitizeUrlForOutput,
11
- } from '../network/url-safety.js';
12
- import type { ToolContext, ToolExecutionResult } from '../types.js';
13
- import { detectAuthChallenge, detectCaptchaChallenge, formatAuthChallenge } from './auth-detector.js';
14
- import type { PageResponse,RouteHandler } from './browser-manager.js';
15
- import { browserManager, SCREENCAST_HEIGHT,SCREENCAST_WIDTH } from './browser-manager.js';
11
+ } from "../network/url-safety.js";
12
+ import type { ToolContext, ToolExecutionResult } from "../types.js";
13
+ import {
14
+ detectAuthChallenge,
15
+ detectCaptchaChallenge,
16
+ formatAuthChallenge,
17
+ } from "./auth-detector.js";
18
+ import type { PageResponse, RouteHandler } from "./browser-manager.js";
19
+ import { browserManager } from "./browser-manager.js";
16
20
  import {
17
21
  ensureScreencast,
18
- getElementBounds,
19
22
  getSender,
20
23
  stopAllScreencasts,
21
24
  stopBrowserScreencast,
22
- updateBrowserStatus,
23
- updateHighlights,
24
- updatePagesList,
25
- } from './browser-screencast.js';
25
+ } from "./browser-screencast.js";
26
26
 
27
- const log = getLogger('headless-browser');
27
+ const log = getLogger("headless-browser");
28
28
 
29
29
  // ── Constants ────────────────────────────────────────────────────────
30
30
 
@@ -35,11 +35,11 @@ export const ACTION_TIMEOUT_MS = 10_000;
35
35
  export const MAX_SNAPSHOT_ELEMENTS = 150;
36
36
 
37
37
  export const INTERACTIVE_SELECTOR = [
38
- 'a[href]',
39
- 'button',
40
- 'input',
41
- 'select',
42
- 'textarea',
38
+ "a[href]",
39
+ "button",
40
+ "input",
41
+ "select",
42
+ "textarea",
43
43
  '[role="button"]',
44
44
  '[role="link"]',
45
45
  '[role="checkbox"]',
@@ -50,7 +50,7 @@ export const INTERACTIVE_SELECTOR = [
50
50
  '[role="combobox"]',
51
51
  '[role="listbox"]',
52
52
  '[contenteditable="true"]',
53
- ].join(', ');
53
+ ].join(", ");
54
54
 
55
55
  export type SnapshotElement = {
56
56
  eid: string;
@@ -69,15 +69,23 @@ export function resolveSelector(
69
69
  sessionId: string,
70
70
  input: Record<string, unknown>,
71
71
  ): { selector: string | null; error: string | null } {
72
- const elementId = typeof input.element_id === 'string' ? input.element_id : null;
73
- const rawSelector = typeof input.selector === 'string' ? input.selector : null;
72
+ const elementId =
73
+ typeof input.element_id === "string" ? input.element_id : null;
74
+ const rawSelector =
75
+ typeof input.selector === "string" ? input.selector : null;
74
76
 
75
77
  if (!elementId && !rawSelector) {
76
- return { selector: null, error: 'Error: Either element_id or selector is required.' };
78
+ return {
79
+ selector: null,
80
+ error: "Error: Either element_id or selector is required.",
81
+ };
77
82
  }
78
83
 
79
84
  if (elementId) {
80
- const resolved = browserManager.resolveSnapshotSelector(sessionId, elementId);
85
+ const resolved = browserManager.resolveSnapshotSelector(
86
+ sessionId,
87
+ elementId,
88
+ );
81
89
  if (!resolved) {
82
90
  return {
83
91
  selector: null,
@@ -97,15 +105,18 @@ export async function executeBrowserNavigate(
97
105
  context: ToolContext,
98
106
  ): Promise<ToolExecutionResult> {
99
107
  if (context.signal?.aborted) {
100
- return { content: 'Error: operation was cancelled', isError: true };
108
+ return { content: "Error: operation was cancelled", isError: true };
101
109
  }
102
110
 
103
111
  const parsedUrl = parseUrl(input.url);
104
112
  if (!parsedUrl) {
105
- return { content: 'Error: url is required and must be a valid HTTP(S) URL', isError: true };
113
+ return {
114
+ content: "Error: url is required and must be a valid HTTP(S) URL",
115
+ isError: true,
116
+ };
106
117
  }
107
- if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
108
- return { content: 'Error: url must use http or https', isError: true };
118
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
119
+ return { content: "Error: url must use http or https", isError: true };
109
120
  }
110
121
 
111
122
  const allowPrivateNetwork = input.allow_private_network === true;
@@ -140,13 +151,15 @@ export async function executeBrowserNavigate(
140
151
  // Start screencast if a sender is registered for this session
141
152
  const sender = getSender(context.sessionId);
142
153
  if (sender) {
143
- await ensureScreencast(context.sessionId, sender);
144
- updateBrowserStatus(context.sessionId, sender, 'navigating', `Navigating to ${safeRequestedUrl}`);
154
+ await ensureScreencast(context.sessionId);
145
155
  }
146
156
 
147
157
  try {
148
158
  const page = await browserManager.getOrCreateSessionPage(context.sessionId);
149
- log.debug({ url: safeRequestedUrl, sessionId: context.sessionId }, 'Navigating');
159
+ log.debug(
160
+ { url: safeRequestedUrl, sessionId: context.sessionId },
161
+ "Navigating",
162
+ );
150
163
 
151
164
  // Install request interception to block redirects/sub-requests to private networks.
152
165
  // This prevents SSRF bypass via server-side redirects and DNS rebinding attacks,
@@ -159,7 +172,10 @@ export async function executeBrowserNavigate(
159
172
  // resolves to a public IP then later to a private one. Blocked results are
160
173
  // never cached so they are always re-resolved.
161
174
  const DNS_CACHE_TTL_MS = 5_000;
162
- const dnsCache = new Map<string, { addresses: string[]; blockedAddress?: string; cachedAt: number }>();
175
+ const dnsCache = new Map<
176
+ string,
177
+ { addresses: string[]; blockedAddress?: string; cachedAt: number }
178
+ >();
163
179
  routeHandler = async (route, request) => {
164
180
  try {
165
181
  const reqUrl = request.url();
@@ -174,8 +190,11 @@ export async function executeBrowserNavigate(
174
190
  // Check hostname against private/local patterns
175
191
  if (isPrivateOrLocalHost(reqParsed.hostname)) {
176
192
  blockedUrl = sanitizeUrlForOutput(reqParsed);
177
- log.warn({ blockedUrl }, 'Blocked navigation to private network target via redirect');
178
- await route.abort('blockedbyclient');
193
+ log.warn(
194
+ { blockedUrl },
195
+ "Blocked navigation to private network target via redirect",
196
+ );
197
+ await route.abort("blockedbyclient");
179
198
  return;
180
199
  }
181
200
 
@@ -184,36 +203,44 @@ export async function executeBrowserNavigate(
184
203
  // DNS rebinding where a hostname flips from public to private IP.
185
204
  let cached = dnsCache.get(reqParsed.hostname);
186
205
  const now = Date.now();
187
- if (cached && (now - cached.cachedAt > DNS_CACHE_TTL_MS)) {
206
+ if (cached && now - cached.cachedAt > DNS_CACHE_TTL_MS) {
188
207
  dnsCache.delete(reqParsed.hostname);
189
208
  cached = undefined;
190
209
  }
191
- const resolution = cached ?? await (async () => {
192
- const res = await resolveRequestAddress(
193
- reqParsed.hostname,
194
- resolveHostAddresses,
195
- false,
196
- );
197
- // Only cache allowed results; blocked results must be re-resolved
198
- if (!res.blockedAddress) {
199
- dnsCache.set(reqParsed.hostname, { ...res, cachedAt: now });
200
- }
201
- return res;
202
- })();
210
+ const resolution =
211
+ cached ??
212
+ (await (async () => {
213
+ const res = await resolveRequestAddress(
214
+ reqParsed.hostname,
215
+ resolveHostAddresses,
216
+ false,
217
+ );
218
+ // Only cache allowed results; blocked results must be re-resolved
219
+ if (!res.blockedAddress) {
220
+ dnsCache.set(reqParsed.hostname, { ...res, cachedAt: now });
221
+ }
222
+ return res;
223
+ })());
203
224
  if (resolution.blockedAddress) {
204
225
  blockedUrl = sanitizeUrlForOutput(reqParsed);
205
- log.warn({ blockedUrl, resolvedTo: resolution.blockedAddress }, 'Blocked navigation: DNS resolves to private address');
206
- await route.abort('blockedbyclient');
226
+ log.warn(
227
+ { blockedUrl, resolvedTo: resolution.blockedAddress },
228
+ "Blocked navigation: DNS resolves to private address",
229
+ );
230
+ await route.abort("blockedbyclient");
207
231
  return;
208
232
  }
209
233
 
210
234
  await route.continue();
211
235
  } catch (err) {
212
236
  // Route may already be handled if the page navigated or was closed
213
- log.debug({ err }, 'Route handler error (route likely already handled)');
237
+ log.debug(
238
+ { err },
239
+ "Route handler error (route likely already handled)",
240
+ );
214
241
  }
215
242
  };
216
- await page.route('**/*', routeHandler);
243
+ await page.route("**/*", routeHandler);
217
244
  }
218
245
 
219
246
  // Use domcontentloaded but with a shorter timeout — if it times out,
@@ -224,19 +251,22 @@ export async function executeBrowserNavigate(
224
251
  const urlBeforeNav = page.url();
225
252
  try {
226
253
  response = await page.goto(parsedUrl.href, {
227
- waitUntil: 'domcontentloaded',
254
+ waitUntil: "domcontentloaded",
228
255
  timeout: NAVIGATE_TIMEOUT_MS,
229
256
  });
230
257
  } catch (navErr) {
231
258
  const navMsg = navErr instanceof Error ? navErr.message : String(navErr);
232
- if (navMsg.includes('Timeout') || navMsg.includes('timeout')) {
259
+ if (navMsg.includes("Timeout") || navMsg.includes("timeout")) {
233
260
  // If the page URL never changed from before navigation, the page
234
261
  // never actually loaded — re-throw instead of reporting success.
235
262
  if (page.url() === urlBeforeNav && urlBeforeNav !== parsedUrl.href) {
236
263
  throw navErr;
237
264
  }
238
265
  navigationTimedOut = true;
239
- log.info({ url: safeRequestedUrl }, 'Navigation timed out waiting for domcontentloaded, continuing with partial load');
266
+ log.info(
267
+ { url: safeRequestedUrl },
268
+ "Navigation timed out waiting for domcontentloaded, continuing with partial load",
269
+ );
240
270
  } else {
241
271
  throw navErr;
242
272
  }
@@ -244,7 +274,7 @@ export async function executeBrowserNavigate(
244
274
 
245
275
  // Remove the route handler now that navigation is complete
246
276
  if (routeHandler) {
247
- await page.unroute('**/*', routeHandler);
277
+ await page.unroute("**/*", routeHandler);
248
278
  routeHandler = null;
249
279
  }
250
280
 
@@ -255,9 +285,6 @@ export async function executeBrowserNavigate(
255
285
  }
256
286
 
257
287
  if (blockedUrl) {
258
- if (sender) {
259
- updateBrowserStatus(context.sessionId, sender, 'idle');
260
- }
261
288
  return {
262
289
  content: `Error: Navigation blocked. A request targeted a local/private network address (${blockedUrl}). Set allow_private_network=true if you explicitly need it.`,
263
290
  isError: true,
@@ -300,12 +327,14 @@ export async function executeBrowserNavigate(
300
327
  const lines: string[] = [
301
328
  `Requested URL: ${safeRequestedUrl}`,
302
329
  `Final URL: ${safeFinalUrl}`,
303
- `Status: ${status ?? 'unknown'}`,
304
- `Title: ${title || '(none)'}`,
330
+ `Status: ${status ?? "unknown"}`,
331
+ `Title: ${title || "(none)"}`,
305
332
  ];
306
333
 
307
334
  if (navigationTimedOut) {
308
- lines.push(`Note: Page is still loading (domcontentloaded timed out). The page should still be interactive — use browser_snapshot to check.`);
335
+ lines.push(
336
+ `Note: Page is still loading (domcontentloaded timed out). The page should still be interactive — use browser_snapshot to check.`,
337
+ );
309
338
  }
310
339
 
311
340
  if (finalUrl !== parsedUrl.href) {
@@ -321,17 +350,16 @@ export async function executeBrowserNavigate(
321
350
 
322
351
  // Many CAPTCHA interstitials (e.g. Cloudflare "Just a moment") auto-resolve
323
352
  // within a few seconds. Wait and re-check before handing off to the user.
324
- if (challenge?.type === 'captcha') {
325
- log.info('CAPTCHA detected, waiting up to 5s for auto-resolve');
353
+ if (challenge?.type === "captcha") {
354
+ log.info("CAPTCHA detected, waiting up to 5s for auto-resolve");
326
355
  for (let i = 0; i < 5; i++) {
327
356
  if (context.signal?.aborted) {
328
- if (sender) updateBrowserStatus(context.sessionId, sender, 'idle');
329
- return { content: 'Navigation cancelled.', isError: true };
357
+ return { content: "Navigation cancelled.", isError: true };
330
358
  }
331
359
  await new Promise((r) => setTimeout(r, 1000));
332
360
  const still = await detectCaptchaChallenge(page);
333
361
  if (!still) {
334
- log.info('CAPTCHA auto-resolved');
362
+ log.info("CAPTCHA auto-resolved");
335
363
  // Re-check for auth challenge now that CAPTCHA is gone —
336
364
  // the page may have loaded a login form behind it.
337
365
  challenge = await detectAuthChallenge(page);
@@ -341,76 +369,99 @@ export async function executeBrowserNavigate(
341
369
  }
342
370
 
343
371
  if (challenge) {
344
- if (challenge.type === 'captcha') {
372
+ if (challenge.type === "captcha") {
345
373
  // CAPTCHA persisted after auto-resolve wait — hand off to user
346
- if (browserManager.browserMode === 'cdp' && sender) {
347
- const { startHandoff } = await import('./browser-handoff.js');
348
- await startHandoff(context.sessionId, sender, {
349
- reason: 'captcha',
350
- message: 'Cloudflare verification detected. Please solve the CAPTCHA in the Chrome window that just opened (not the preview panel — CAPTCHA providers detect preview clicks as automated). Click "Hand back" when done.',
374
+ if (sender) {
375
+ const { startHandoff } = await import("./browser-handoff.js");
376
+ await startHandoff(context.sessionId, {
377
+ reason: "captcha",
378
+ message:
379
+ "Cloudflare verification detected. Please solve the CAPTCHA in the Chrome window. The browser will automatically detect when you're done and resume.",
351
380
  bringToFront: true,
352
381
  });
353
382
  const newUrl = page.url();
354
383
  const newTitle = await page.title();
355
- lines.push('');
356
- lines.push(`CAPTCHA solved by user. Current page: ${newTitle} (${newUrl})`);
384
+ lines.push("");
385
+ lines.push(
386
+ `CAPTCHA solved by user. Current page: ${newTitle} (${newUrl})`,
387
+ );
357
388
 
358
389
  // Re-check for auth challenges — the page behind the CAPTCHA may have a login form
359
390
  const postCaptchaAuth = await detectAuthChallenge(page);
360
391
  if (postCaptchaAuth) {
361
- lines.push('');
392
+ lines.push("");
362
393
  lines.push(formatAuthChallenge(postCaptchaAuth));
363
- lines.push('');
364
- lines.push('Handle this by using browser tools to interact with the login form:');
365
- lines.push('1. Use browser_snapshot to find the sign-in form elements');
366
- lines.push('2. Use browser_fill_credential to fill email/password from credential_store');
367
- lines.push('3. For SMS/email verification codes, use ui_show with a form to ask the user for the code mid-turn');
368
- lines.push('4. Do NOT give up or tell the user to sign in manually — handle the login flow yourself');
394
+ lines.push("");
395
+ lines.push(
396
+ "Handle this by using browser tools to interact with the login form:",
397
+ );
398
+ lines.push(
399
+ "1. Use browser_snapshot to find the sign-in form elements",
400
+ );
401
+ lines.push(
402
+ "2. Use browser_fill_credential to fill email/password from credential_store",
403
+ );
404
+ lines.push(
405
+ "3. For SMS/email verification codes, use ui_show with a form to ask the user for the code mid-turn",
406
+ );
407
+ lines.push(
408
+ "4. Do NOT give up or tell the user to sign in manually — handle the login flow yourself",
409
+ );
369
410
  }
370
411
  } else {
371
- lines.push('');
372
- lines.push('⚠️ CAPTCHA/Cloudflare verification detected on this page.');
373
- lines.push('The user needs to solve this challenge manually. Please inform the user that the page requires human verification before the content can be accessed.');
412
+ lines.push("");
413
+ lines.push(
414
+ "⚠️ CAPTCHA/Cloudflare verification detected on this page.",
415
+ );
416
+ lines.push(
417
+ "The user needs to solve this challenge manually. Please inform the user that the page requires human verification before the content can be accessed.",
418
+ );
374
419
  }
375
420
  } else {
376
421
  // Login / 2FA / OAuth — the agent should handle these itself
377
422
  // using browser tools + credential_store. Don't hand off.
378
- lines.push('');
423
+ lines.push("");
379
424
  lines.push(formatAuthChallenge(challenge));
380
- lines.push('');
381
- lines.push('Handle this by using browser tools to interact with the login form:');
382
- lines.push('1. Use browser_snapshot to find the sign-in form elements');
383
- lines.push('2. Use browser_fill_credential to fill email/password from credential_store');
384
- lines.push('3. For SMS/email verification codes, use ui_show with a form to ask the user for the code mid-turn');
385
- lines.push('4. Do NOT give up or tell the user to sign in manually — handle the login flow yourself');
425
+ lines.push("");
426
+ lines.push(
427
+ "Handle this by using browser tools to interact with the login form:",
428
+ );
429
+ lines.push(
430
+ "1. Use browser_snapshot to find the sign-in form elements",
431
+ );
432
+ lines.push(
433
+ "2. Use browser_fill_credential to fill email/password from credential_store",
434
+ );
435
+ lines.push(
436
+ "3. For SMS/email verification codes, use ui_show with a form to ask the user for the code mid-turn",
437
+ );
438
+ lines.push(
439
+ "4. Do NOT give up or tell the user to sign in manually — handle the login flow yourself",
440
+ );
386
441
  }
387
442
  }
388
443
  } catch {
389
444
  // Auth/CAPTCHA detection is best-effort; don't fail navigation
390
445
  }
391
446
 
392
- if (sender) {
393
- updateBrowserStatus(context.sessionId, sender, 'idle');
394
- await updatePagesList(context.sessionId, sender);
395
- }
396
-
397
- return { content: lines.join('\n'), isError: false };
447
+ return { content: lines.join("\n"), isError: false };
398
448
  } catch (err) {
399
449
  // Best-effort cleanup of route handler on error
400
450
  if (routeHandler) {
401
451
  try {
402
- const page = await browserManager.getOrCreateSessionPage(context.sessionId);
403
- await page.unroute('**/*', routeHandler);
404
- } catch { /* ignore cleanup errors */ }
452
+ const page = await browserManager.getOrCreateSessionPage(
453
+ context.sessionId,
454
+ );
455
+ await page.unroute("**/*", routeHandler);
456
+ } catch {
457
+ /* ignore cleanup errors */
458
+ }
405
459
  }
406
460
 
407
461
  // If the route handler blocked a redirect to a private network address,
408
462
  // page.goto() throws. Return the clear security message instead of the
409
463
  // raw Playwright error (which could leak credentials from the URL).
410
464
  if (blockedUrl) {
411
- if (sender) {
412
- updateBrowserStatus(context.sessionId, sender, 'idle');
413
- }
414
465
  return {
415
466
  content: `Error: Navigation blocked. A request targeted a local/private network address (${blockedUrl}). Set allow_private_network=true if you explicitly need it.`,
416
467
  isError: true,
@@ -418,10 +469,7 @@ export async function executeBrowserNavigate(
418
469
  }
419
470
 
420
471
  const msg = err instanceof Error ? err.message : String(err);
421
- log.error({ err, url: safeRequestedUrl }, 'Navigation failed');
422
- if (sender) {
423
- updateBrowserStatus(context.sessionId, sender, 'idle');
424
- }
472
+ log.error({ err, url: safeRequestedUrl }, "Navigation failed");
425
473
  return { content: `Error: Navigation failed: ${msg}`, isError: true };
426
474
  }
427
475
  }
@@ -472,32 +520,34 @@ export async function executeBrowserSnapshot(
472
520
  // Format output
473
521
  const lines: string[] = [
474
522
  `URL: ${currentUrl}`,
475
- `Title: ${title || '(none)'}`,
476
- '',
523
+ `Title: ${title || "(none)"}`,
524
+ "",
477
525
  ];
478
526
 
479
527
  if (elements.length === 0) {
480
- lines.push('(no interactive elements found)');
528
+ lines.push("(no interactive elements found)");
481
529
  } else {
482
530
  for (const el of elements) {
483
531
  let desc = `<${el.tag}`;
484
532
  for (const [key, val] of Object.entries(el.attrs)) {
485
533
  desc += ` ${key}="${val}"`;
486
534
  }
487
- desc += '>';
535
+ desc += ">";
488
536
  if (el.text) {
489
537
  desc += ` ${el.text}`;
490
538
  }
491
539
  lines.push(`[${el.eid}] ${desc}`);
492
540
  }
493
- lines.push('');
494
- lines.push(`${elements.length} interactive element${elements.length === 1 ? '' : 's'} found.`);
541
+ lines.push("");
542
+ lines.push(
543
+ `${elements.length} interactive element${elements.length === 1 ? "" : "s"} found.`,
544
+ );
495
545
  }
496
546
 
497
- return { content: lines.join('\n'), isError: false };
547
+ return { content: lines.join("\n"), isError: false };
498
548
  } catch (err) {
499
549
  const msg = err instanceof Error ? err.message : String(err);
500
- log.error({ err }, 'Snapshot failed');
550
+ log.error({ err }, "Snapshot failed");
501
551
  return { content: `Error: Snapshot failed: ${msg}`, isError: true };
502
552
  }
503
553
  }
@@ -512,26 +562,30 @@ export async function executeBrowserScreenshot(
512
562
 
513
563
  try {
514
564
  const page = await browserManager.getOrCreateSessionPage(context.sessionId);
515
- const buffer = await page.screenshot({ type: 'jpeg', quality: 80, fullPage });
516
- const base64Data = buffer.toString('base64');
565
+ const buffer = await page.screenshot({
566
+ type: "jpeg",
567
+ quality: 80,
568
+ fullPage,
569
+ });
570
+ const base64Data = buffer.toString("base64");
517
571
 
518
572
  const imageBlock: ImageContent = {
519
- type: 'image' as const,
573
+ type: "image" as const,
520
574
  source: {
521
- type: 'base64' as const,
522
- media_type: 'image/jpeg',
575
+ type: "base64" as const,
576
+ media_type: "image/jpeg",
523
577
  data: base64Data,
524
578
  },
525
579
  };
526
580
 
527
581
  return {
528
- content: `Screenshot captured (${buffer.length} bytes, ${fullPage ? 'full page' : 'viewport'})`,
582
+ content: `Screenshot captured (${buffer.length} bytes, ${fullPage ? "full page" : "viewport"})`,
529
583
  isError: false,
530
584
  contentBlocks: [imageBlock],
531
585
  };
532
586
  } catch (err) {
533
587
  const msg = err instanceof Error ? err.message : String(err);
534
- log.error({ err }, 'Screenshot failed');
588
+ log.error({ err }, "Screenshot failed");
535
589
  return { content: `Error: Screenshot failed: ${msg}`, isError: true };
536
590
  }
537
591
  }
@@ -545,19 +599,22 @@ export async function executeBrowserClose(
545
599
  try {
546
600
  const sender = getSender(context.sessionId);
547
601
  if (sender) {
548
- await stopBrowserScreencast(context.sessionId, sender);
602
+ await stopBrowserScreencast(context.sessionId);
549
603
  }
550
604
 
551
605
  if (input.close_all_pages === true) {
552
606
  await stopAllScreencasts();
553
607
  await browserManager.closeAllPages();
554
- return { content: 'All browser pages and context closed.', isError: false };
608
+ return {
609
+ content: "All browser pages and context closed.",
610
+ isError: false,
611
+ };
555
612
  }
556
613
  await browserManager.closeSessionPage(context.sessionId);
557
- return { content: 'Browser page closed for this session.', isError: false };
614
+ return { content: "Browser page closed for this session.", isError: false };
558
615
  } catch (err) {
559
616
  const msg = err instanceof Error ? err.message : String(err);
560
- log.error({ err }, 'Close failed');
617
+ log.error({ err }, "Close failed");
561
618
  return { content: `Error: Close failed: ${msg}`, isError: true };
562
619
  }
563
620
  }
@@ -571,33 +628,16 @@ export async function executeBrowserClick(
571
628
  const { selector, error } = resolveSelector(context.sessionId, input);
572
629
  if (error) return { content: error, isError: true };
573
630
 
574
- const sender = getSender(context.sessionId);
575
- if (sender) {
576
- await ensureScreencast(context.sessionId, sender);
577
- updateBrowserStatus(context.sessionId, sender, 'interacting', 'Clicking element');
578
- const bounds = await getElementBounds(context.sessionId, selector!);
579
- if (bounds) {
580
- updateHighlights(context.sessionId, sender, [{ ...bounds, label: 'Clicking element' }]);
581
- }
582
- }
583
-
584
- const timeout = typeof input.timeout === 'number' ? input.timeout : ACTION_TIMEOUT_MS;
631
+ const timeout =
632
+ typeof input.timeout === "number" ? input.timeout : ACTION_TIMEOUT_MS;
585
633
 
586
634
  try {
587
635
  const page = await browserManager.getOrCreateSessionPage(context.sessionId);
588
636
  await page.click(selector!, { timeout });
589
- if (sender) {
590
- updateHighlights(context.sessionId, sender, []);
591
- updateBrowserStatus(context.sessionId, sender, 'idle');
592
- }
593
637
  return { content: `Clicked element: ${selector}`, isError: false };
594
638
  } catch (err) {
595
639
  const msg = err instanceof Error ? err.message : String(err);
596
- log.error({ err, selector }, 'Click failed');
597
- if (sender) {
598
- updateHighlights(context.sessionId, sender, []);
599
- updateBrowserStatus(context.sessionId, sender, 'idle');
600
- }
640
+ log.error({ err, selector }, "Click failed");
601
641
  return { content: `Error: Click failed: ${msg}`, isError: true };
602
642
  }
603
643
  }
@@ -611,28 +651,19 @@ export async function executeBrowserType(
611
651
  const { selector, error } = resolveSelector(context.sessionId, input);
612
652
  if (error) return { content: error, isError: true };
613
653
 
614
- const text = typeof input.text === 'string' ? input.text : '';
654
+ const text = typeof input.text === "string" ? input.text : "";
615
655
  if (!text) {
616
- return { content: 'Error: text is required.', isError: true };
656
+ return { content: "Error: text is required.", isError: true };
617
657
  }
618
658
 
619
659
  const clearFirst = input.clear_first !== false; // default true
620
660
  const pressEnter = input.press_enter === true;
621
661
 
622
- const sender = getSender(context.sessionId);
623
- if (sender) {
624
- await ensureScreencast(context.sessionId, sender);
625
- updateBrowserStatus(context.sessionId, sender, 'interacting', 'Typing text');
626
- const bounds = await getElementBounds(context.sessionId, selector!);
627
- if (bounds) {
628
- updateHighlights(context.sessionId, sender, [{ ...bounds, label: 'Typing' }]);
629
- }
630
- }
631
-
632
662
  try {
633
663
  const page = await browserManager.getOrCreateSessionPage(context.sessionId);
634
664
 
635
- const fillTimeout = typeof input.timeout === 'number' ? input.timeout : ACTION_TIMEOUT_MS;
665
+ const fillTimeout =
666
+ typeof input.timeout === "number" ? input.timeout : ACTION_TIMEOUT_MS;
636
667
 
637
668
  if (clearFirst) {
638
669
  await page.fill(selector!, text, { timeout: fillTimeout });
@@ -647,25 +678,16 @@ export async function executeBrowserType(
647
678
  }
648
679
 
649
680
  if (pressEnter) {
650
- await page.press(selector!, 'Enter');
651
- }
652
-
653
- if (sender) {
654
- updateHighlights(context.sessionId, sender, []);
655
- updateBrowserStatus(context.sessionId, sender, 'idle');
681
+ await page.press(selector!, "Enter");
656
682
  }
657
683
 
658
684
  const lines = [`Typed into element: ${selector}`];
659
- if (clearFirst) lines.push('(cleared existing content first)');
660
- if (pressEnter) lines.push('(pressed Enter after typing)');
661
- return { content: lines.join('\n'), isError: false };
685
+ if (clearFirst) lines.push("(cleared existing content first)");
686
+ if (pressEnter) lines.push("(pressed Enter after typing)");
687
+ return { content: lines.join("\n"), isError: false };
662
688
  } catch (err) {
663
689
  const msg = err instanceof Error ? err.message : String(err);
664
- log.error({ err, selector }, 'Type failed');
665
- if (sender) {
666
- updateHighlights(context.sessionId, sender, []);
667
- updateBrowserStatus(context.sessionId, sender, 'idle');
668
- }
690
+ log.error({ err, selector }, "Type failed");
669
691
  return { content: `Error: Type failed: ${msg}`, isError: true };
670
692
  }
671
693
  }
@@ -676,129 +698,86 @@ export async function executeBrowserPressKey(
676
698
  input: Record<string, unknown>,
677
699
  context: ToolContext,
678
700
  ): Promise<ToolExecutionResult> {
679
- const key = typeof input.key === 'string' ? input.key : '';
701
+ const key = typeof input.key === "string" ? input.key : "";
680
702
  if (!key) {
681
- return { content: 'Error: key is required.', isError: true };
682
- }
683
-
684
- const sender = getSender(context.sessionId);
685
- if (sender) {
686
- await ensureScreencast(context.sessionId, sender);
687
- updateBrowserStatus(context.sessionId, sender, 'interacting', `Pressing ${key}`);
703
+ return { content: "Error: key is required.", isError: true };
688
704
  }
689
705
 
690
706
  try {
691
707
  const page = await browserManager.getOrCreateSessionPage(context.sessionId);
692
708
 
693
709
  // If element_id or selector is provided, press key on that element
694
- const elementId = typeof input.element_id === 'string' ? input.element_id : null;
695
- const rawSelector = typeof input.selector === 'string' ? input.selector : null;
710
+ const elementId =
711
+ typeof input.element_id === "string" ? input.element_id : null;
712
+ const rawSelector =
713
+ typeof input.selector === "string" ? input.selector : null;
696
714
 
697
715
  if (elementId || rawSelector) {
698
716
  const { selector, error } = resolveSelector(context.sessionId, input);
699
717
  if (error) {
700
- if (sender) {
701
- updateBrowserStatus(context.sessionId, sender, 'idle');
702
- }
703
718
  return { content: error, isError: true };
704
719
  }
705
- if (sender) {
706
- const bounds = await getElementBounds(context.sessionId, selector!);
707
- if (bounds) {
708
- updateHighlights(context.sessionId, sender, [{ ...bounds, label: `Pressing ${key}` }]);
709
- }
710
- }
711
720
  await page.press(selector!, key);
712
- if (sender) {
713
- updateHighlights(context.sessionId, sender, []);
714
- updateBrowserStatus(context.sessionId, sender, 'idle');
715
- }
716
- return { content: `Pressed "${key}" on element: ${selector}`, isError: false };
721
+ return {
722
+ content: `Pressed "${key}" on element: ${selector}`,
723
+ isError: false,
724
+ };
717
725
  }
718
726
 
719
727
  // No target -> press key on the page (focused element)
720
728
  await page.keyboard.press(key);
721
- if (sender) {
722
- updateBrowserStatus(context.sessionId, sender, 'idle');
723
- }
724
729
  return { content: `Pressed "${key}"`, isError: false };
725
730
  } catch (err) {
726
731
  const msg = err instanceof Error ? err.message : String(err);
727
- log.error({ err, key }, 'Press key failed');
728
- if (sender) {
729
- updateHighlights(context.sessionId, sender, []);
730
- updateBrowserStatus(context.sessionId, sender, 'idle');
731
- }
732
+ log.error({ err, key }, "Press key failed");
732
733
  return { content: `Error: Press key failed: ${msg}`, isError: true };
733
734
  }
734
735
  }
735
736
 
736
-
737
737
  // ── browser_scroll ───────────────────────────────────────────────────
738
738
 
739
739
  export async function executeBrowserScroll(
740
740
  input: Record<string, unknown>,
741
741
  context: ToolContext,
742
742
  ): Promise<ToolExecutionResult> {
743
- const direction = typeof input.direction === 'string' ? input.direction : '';
744
- if (!direction || !['up', 'down', 'left', 'right'].includes(direction)) {
745
- return { content: 'Error: direction is required and must be one of: up, down, left, right.', isError: true };
743
+ const direction = typeof input.direction === "string" ? input.direction : "";
744
+ if (!direction || !["up", "down", "left", "right"].includes(direction)) {
745
+ return {
746
+ content:
747
+ "Error: direction is required and must be one of: up, down, left, right.",
748
+ isError: true,
749
+ };
746
750
  }
747
751
 
748
- const amount = typeof input.amount === 'number' ? Math.abs(input.amount) : 500;
749
-
750
- const sender = getSender(context.sessionId);
751
- if (sender) {
752
- await ensureScreencast(context.sessionId, sender);
753
- updateBrowserStatus(context.sessionId, sender, 'interacting', `Scrolling ${direction}`);
754
- }
752
+ const amount =
753
+ typeof input.amount === "number" ? Math.abs(input.amount) : 500;
755
754
 
756
755
  try {
757
756
  const page = await browserManager.getOrCreateSessionPage(context.sessionId);
758
757
 
759
- // If element_id or selector is provided, scroll within that element
760
- const elementId = typeof input.element_id === 'string' ? input.element_id : null;
761
- const rawSelector = typeof input.selector === 'string' ? input.selector : null;
762
-
763
- if (elementId || rawSelector) {
764
- const { selector, error } = resolveSelector(context.sessionId, input);
765
- if (error) {
766
- if (sender) updateBrowserStatus(context.sessionId, sender, 'idle');
767
- return { content: error, isError: true };
768
- }
769
- // Move mouse to element center before scrolling
770
- const bounds = await getElementBounds(context.sessionId, selector!);
771
- if (bounds) {
772
- // Convert screencast coords back to page coords for mouse.move
773
- const result = await page.evaluate(`(() => ({ vw: window.innerWidth, vh: window.innerHeight }))()`) as { vw: number; vh: number };
774
- const scale = Math.min(SCREENCAST_WIDTH / result.vw, SCREENCAST_HEIGHT / result.vh);
775
- const pageX = (bounds.x + bounds.w / 2) / scale;
776
- const pageY = (bounds.y + bounds.h / 2) / scale;
777
- await page.mouse.move(pageX, pageY);
778
- }
779
- }
780
-
781
758
  let deltaX = 0;
782
759
  let deltaY = 0;
783
760
  switch (direction) {
784
- case 'up': deltaY = -amount; break;
785
- case 'down': deltaY = amount; break;
786
- case 'left': deltaX = -amount; break;
787
- case 'right': deltaX = amount; break;
761
+ case "up":
762
+ deltaY = -amount;
763
+ break;
764
+ case "down":
765
+ deltaY = amount;
766
+ break;
767
+ case "left":
768
+ deltaX = -amount;
769
+ break;
770
+ case "right":
771
+ deltaX = amount;
772
+ break;
788
773
  }
789
774
 
790
775
  await page.mouse.wheel(deltaX, deltaY);
791
776
 
792
- if (sender) {
793
- updateBrowserStatus(context.sessionId, sender, 'idle');
794
- }
795
777
  return { content: `Scrolled ${direction} by ${amount}px`, isError: false };
796
778
  } catch (err) {
797
779
  const msg = err instanceof Error ? err.message : String(err);
798
- log.error({ err, direction }, 'Scroll failed');
799
- if (sender) {
800
- updateBrowserStatus(context.sessionId, sender, 'idle');
801
- }
780
+ log.error({ err, direction }, "Scroll failed");
802
781
  return { content: `Error: Scroll failed: ${msg}`, isError: true };
803
782
  }
804
783
  }
@@ -812,22 +791,15 @@ export async function executeBrowserSelectOption(
812
791
  const { selector, error } = resolveSelector(context.sessionId, input);
813
792
  if (error) return { content: error, isError: true };
814
793
 
815
- const value = typeof input.value === 'string' ? input.value : undefined;
816
- const label = typeof input.label === 'string' ? input.label : undefined;
817
- const index = typeof input.index === 'number' ? input.index : undefined;
794
+ const value = typeof input.value === "string" ? input.value : undefined;
795
+ const label = typeof input.label === "string" ? input.label : undefined;
796
+ const index = typeof input.index === "number" ? input.index : undefined;
818
797
 
819
798
  if (value === undefined && label === undefined && index === undefined) {
820
- return { content: 'Error: One of value, label, or index is required.', isError: true };
821
- }
822
-
823
- const sender = getSender(context.sessionId);
824
- if (sender) {
825
- await ensureScreencast(context.sessionId, sender);
826
- updateBrowserStatus(context.sessionId, sender, 'interacting', 'Selecting option');
827
- const bounds = await getElementBounds(context.sessionId, selector!);
828
- if (bounds) {
829
- updateHighlights(context.sessionId, sender, [{ ...bounds, label: 'Selecting option' }]);
830
- }
799
+ return {
800
+ content: "Error: One of value, label, or index is required.",
801
+ isError: true,
802
+ };
831
803
  }
832
804
 
833
805
  try {
@@ -840,20 +812,19 @@ export async function executeBrowserSelectOption(
840
812
 
841
813
  await page.selectOption(selector!, option);
842
814
 
843
- if (sender) {
844
- updateHighlights(context.sessionId, sender, []);
845
- updateBrowserStatus(context.sessionId, sender, 'idle');
846
- }
847
-
848
- const desc = value !== undefined ? `value="${value}"` : label !== undefined ? `label="${label}"` : `index=${index}`;
849
- return { content: `Selected option (${desc}) on element: ${selector}`, isError: false };
815
+ const desc =
816
+ value !== undefined
817
+ ? `value="${value}"`
818
+ : label !== undefined
819
+ ? `label="${label}"`
820
+ : `index=${index}`;
821
+ return {
822
+ content: `Selected option (${desc}) on element: ${selector}`,
823
+ isError: false,
824
+ };
850
825
  } catch (err) {
851
826
  const msg = err instanceof Error ? err.message : String(err);
852
- log.error({ err, selector }, 'Select option failed');
853
- if (sender) {
854
- updateHighlights(context.sessionId, sender, []);
855
- updateBrowserStatus(context.sessionId, sender, 'idle');
856
- }
827
+ log.error({ err, selector }, "Select option failed");
857
828
  return { content: `Error: Select option failed: ${msg}`, isError: true };
858
829
  }
859
830
  }
@@ -867,32 +838,14 @@ export async function executeBrowserHover(
867
838
  const { selector, error } = resolveSelector(context.sessionId, input);
868
839
  if (error) return { content: error, isError: true };
869
840
 
870
- const sender = getSender(context.sessionId);
871
- if (sender) {
872
- await ensureScreencast(context.sessionId, sender);
873
- updateBrowserStatus(context.sessionId, sender, 'interacting', 'Hovering element');
874
- const bounds = await getElementBounds(context.sessionId, selector!);
875
- if (bounds) {
876
- updateHighlights(context.sessionId, sender, [{ ...bounds, label: 'Hovering' }]);
877
- }
878
- }
879
-
880
841
  try {
881
842
  const page = await browserManager.getOrCreateSessionPage(context.sessionId);
882
843
  await page.hover(selector!, { timeout: ACTION_TIMEOUT_MS });
883
844
 
884
- if (sender) {
885
- updateHighlights(context.sessionId, sender, []);
886
- updateBrowserStatus(context.sessionId, sender, 'idle');
887
- }
888
845
  return { content: `Hovered element: ${selector}`, isError: false };
889
846
  } catch (err) {
890
847
  const msg = err instanceof Error ? err.message : String(err);
891
- log.error({ err, selector }, 'Hover failed');
892
- if (sender) {
893
- updateHighlights(context.sessionId, sender, []);
894
- updateBrowserStatus(context.sessionId, sender, 'idle');
895
- }
848
+ log.error({ err, selector }, "Hover failed");
896
849
  return { content: `Error: Hover failed: ${msg}`, isError: true };
897
850
  }
898
851
  }
@@ -904,31 +857,45 @@ export async function executeBrowserWaitFor(
904
857
  context: ToolContext,
905
858
  ): Promise<ToolExecutionResult> {
906
859
  if (context.signal?.aborted) {
907
- return { content: 'Error: operation was cancelled', isError: true };
860
+ return { content: "Error: operation was cancelled", isError: true };
908
861
  }
909
862
 
910
- const selector = typeof input.selector === 'string' && input.selector ? input.selector : null;
911
- const text = typeof input.text === 'string' && input.text ? input.text : null;
912
- const duration = typeof input.duration === 'number' ? input.duration : null;
863
+ const selector =
864
+ typeof input.selector === "string" && input.selector
865
+ ? input.selector
866
+ : null;
867
+ const text = typeof input.text === "string" && input.text ? input.text : null;
868
+ const duration = typeof input.duration === "number" ? input.duration : null;
913
869
 
914
870
  const modeCount = [selector, text, duration].filter((v) => v != null).length;
915
871
  if (modeCount === 0) {
916
- return { content: 'Error: Exactly one of selector, text, or duration is required.', isError: true };
872
+ return {
873
+ content: "Error: Exactly one of selector, text, or duration is required.",
874
+ isError: true,
875
+ };
917
876
  }
918
877
  if (modeCount > 1) {
919
- return { content: 'Error: Provide exactly one of selector, text, or duration (not multiple).', isError: true };
878
+ return {
879
+ content:
880
+ "Error: Provide exactly one of selector, text, or duration (not multiple).",
881
+ isError: true,
882
+ };
920
883
  }
921
884
 
922
- const timeout = typeof input.timeout === 'number'
923
- ? Math.min(input.timeout, MAX_WAIT_MS)
924
- : MAX_WAIT_MS;
885
+ const timeout =
886
+ typeof input.timeout === "number"
887
+ ? Math.min(input.timeout, MAX_WAIT_MS)
888
+ : MAX_WAIT_MS;
925
889
 
926
890
  try {
927
891
  const page = await browserManager.getOrCreateSessionPage(context.sessionId);
928
892
 
929
893
  if (selector) {
930
894
  await page.waitForSelector(selector, { timeout });
931
- return { content: `Element matching "${selector}" appeared.`, isError: false };
895
+ return {
896
+ content: `Element matching "${selector}" appeared.`,
897
+ isError: false,
898
+ };
932
899
  }
933
900
 
934
901
  if (text) {
@@ -937,7 +904,10 @@ export async function executeBrowserWaitFor(
937
904
  `document.body?.innerText?.includes(${escaped})`,
938
905
  { timeout },
939
906
  );
940
- return { content: `Text "${truncate(text, 80)}" appeared on page.`, isError: false };
907
+ return {
908
+ content: `Text "${truncate(text, 80)}" appeared on page.`,
909
+ isError: false,
910
+ };
941
911
  }
942
912
 
943
913
  // duration mode (milliseconds)
@@ -946,7 +916,7 @@ export async function executeBrowserWaitFor(
946
916
  return { content: `Waited ${waitMs}ms.`, isError: false };
947
917
  } catch (err) {
948
918
  const msg = err instanceof Error ? err.message : String(err);
949
- log.error({ err }, 'Wait failed');
919
+ log.error({ err }, "Wait failed");
950
920
  return { content: `Error: Wait failed: ${msg}`, isError: true };
951
921
  }
952
922
  }
@@ -969,14 +939,15 @@ export async function executeBrowserExtract(
969
939
  )) as string;
970
940
 
971
941
  if (textContent.length > MAX_EXTRACT_LENGTH) {
972
- textContent = textContent.slice(0, MAX_EXTRACT_LENGTH) + '\n... (truncated)';
942
+ textContent =
943
+ textContent.slice(0, MAX_EXTRACT_LENGTH) + "\n... (truncated)";
973
944
  }
974
945
 
975
946
  const lines: string[] = [
976
947
  `URL: ${currentUrl}`,
977
- `Title: ${title || '(none)'}`,
978
- '',
979
- textContent || '(empty page)',
948
+ `Title: ${title || "(none)"}`,
949
+ "",
950
+ textContent || "(empty page)",
980
951
  ];
981
952
 
982
953
  if (includeLinks) {
@@ -991,18 +962,18 @@ export async function executeBrowserExtract(
991
962
  `)) as Array<{ text: string; href: string }>;
992
963
 
993
964
  if (links.length > 0) {
994
- lines.push('');
995
- lines.push('Links:');
965
+ lines.push("");
966
+ lines.push("Links:");
996
967
  for (const link of links) {
997
- lines.push(` [${link.text || '(no text)'}](${link.href})`);
968
+ lines.push(` [${link.text || "(no text)"}](${link.href})`);
998
969
  }
999
970
  }
1000
971
  }
1001
972
 
1002
- return { content: lines.join('\n'), isError: false };
973
+ return { content: lines.join("\n"), isError: false };
1003
974
  } catch (err) {
1004
975
  const msg = err instanceof Error ? err.message : String(err);
1005
- log.error({ err }, 'Extract failed');
976
+ log.error({ err }, "Extract failed");
1006
977
  return { content: `Error: Extract failed: ${msg}`, isError: true };
1007
978
  }
1008
979
  }
@@ -1013,14 +984,14 @@ export async function executeBrowserFillCredential(
1013
984
  input: Record<string, unknown>,
1014
985
  context: ToolContext,
1015
986
  ): Promise<ToolExecutionResult> {
1016
- const service = typeof input.service === 'string' ? input.service : '';
1017
- const field = typeof input.field === 'string' ? input.field : '';
987
+ const service = typeof input.service === "string" ? input.service : "";
988
+ const field = typeof input.field === "string" ? input.field : "";
1018
989
 
1019
990
  if (!service) {
1020
- return { content: 'Error: service is required.', isError: true };
991
+ return { content: "Error: service is required.", isError: true };
1021
992
  }
1022
993
  if (!field) {
1023
- return { content: 'Error: field is required.', isError: true };
994
+ return { content: "Error: field is required.", isError: true };
1024
995
  }
1025
996
 
1026
997
  const { selector, error } = resolveSelector(context.sessionId, input);
@@ -1035,7 +1006,7 @@ export async function executeBrowserFillCredential(
1035
1006
  let pageDomain: string | undefined;
1036
1007
  try {
1037
1008
  const pageUrl = page.url();
1038
- if (pageUrl && pageUrl !== 'about:blank') {
1009
+ if (pageUrl && pageUrl !== "about:blank") {
1039
1010
  const parsed = new URL(pageUrl);
1040
1011
  pageDomain = parsed.hostname;
1041
1012
  }
@@ -1046,7 +1017,7 @@ export async function executeBrowserFillCredential(
1046
1017
  const result = await credentialBroker.browserFill({
1047
1018
  service,
1048
1019
  field,
1049
- toolName: 'browser_fill_credential',
1020
+ toolName: "browser_fill_credential",
1050
1021
  domain: pageDomain,
1051
1022
  fill: async (value) => {
1052
1023
  await page.fill(selector!, value);
@@ -1054,37 +1025,49 @@ export async function executeBrowserFillCredential(
1054
1025
  });
1055
1026
 
1056
1027
  if (!result.success) {
1057
- const reason = result.reason ?? 'unknown error';
1058
- if (reason.includes('No credential found') || reason.includes('no stored value')) {
1028
+ const reason = result.reason ?? "unknown error";
1029
+ if (
1030
+ reason.includes("No credential found") ||
1031
+ reason.includes("no stored value")
1032
+ ) {
1059
1033
  return {
1060
1034
  content: `No credential stored for ${service}/${field}. Use credential_store to save it first.`,
1061
1035
  isError: true,
1062
1036
  };
1063
1037
  }
1064
- if (reason.includes('not allowed to use credential')) {
1038
+ if (reason.includes("not allowed to use credential")) {
1065
1039
  return {
1066
1040
  content: `Policy denied: ${reason} Update the credential's allowed_tools via credential_store if this tool should have access.`,
1067
1041
  isError: true,
1068
1042
  };
1069
1043
  }
1070
- if (reason.includes('not allowed for credential') || reason.includes('no page domain was provided')) {
1044
+ if (
1045
+ reason.includes("not allowed for credential") ||
1046
+ reason.includes("no page domain was provided")
1047
+ ) {
1071
1048
  return {
1072
1049
  content: `Domain policy denied: ${reason} Navigate to an allowed domain before filling this credential.`,
1073
1050
  isError: true,
1074
1051
  };
1075
1052
  }
1076
- log.error({ selector, reason }, 'Fill credential failed');
1077
- return { content: `Error: Fill credential failed: ${reason}`, isError: true };
1053
+ log.error({ selector, reason }, "Fill credential failed");
1054
+ return {
1055
+ content: `Error: Fill credential failed: ${reason}`,
1056
+ isError: true,
1057
+ };
1078
1058
  }
1079
1059
 
1080
1060
  if (pressEnter) {
1081
- await page.press(selector!, 'Enter');
1061
+ await page.press(selector!, "Enter");
1082
1062
  }
1083
1063
 
1084
- return { content: `Filled ${field} for ${service} into the target element.`, isError: false };
1064
+ return {
1065
+ content: `Filled ${field} for ${service} into the target element.`,
1066
+ isError: false,
1067
+ };
1085
1068
  } catch (err) {
1086
1069
  const msg = err instanceof Error ? err.message : String(err);
1087
- log.error({ err }, 'Fill credential failed');
1070
+ log.error({ err }, "Fill credential failed");
1088
1071
  return { content: `Error: Fill credential failed: ${msg}`, isError: true };
1089
1072
  }
1090
1073
  }