agent-browser 0.19.0 → 0.20.0

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 (86) hide show
  1. package/README.md +36 -82
  2. package/bin/agent-browser-darwin-arm64 +0 -0
  3. package/bin/agent-browser-darwin-x64 +0 -0
  4. package/bin/agent-browser-linux-arm64 +0 -0
  5. package/bin/agent-browser-linux-x64 +0 -0
  6. package/bin/agent-browser-win32-x64.exe +0 -0
  7. package/package.json +6 -49
  8. package/scripts/postinstall.js +12 -15
  9. package/skills/agent-browser/SKILL.md +9 -18
  10. package/skills/electron/SKILL.md +5 -5
  11. package/dist/action-policy.d.ts +0 -14
  12. package/dist/action-policy.d.ts.map +0 -1
  13. package/dist/action-policy.js +0 -253
  14. package/dist/action-policy.js.map +0 -1
  15. package/dist/actions.d.ts +0 -18
  16. package/dist/actions.d.ts.map +0 -1
  17. package/dist/actions.js +0 -2121
  18. package/dist/actions.js.map +0 -1
  19. package/dist/auth-cli.d.ts +0 -2
  20. package/dist/auth-cli.d.ts.map +0 -1
  21. package/dist/auth-cli.js +0 -97
  22. package/dist/auth-cli.js.map +0 -1
  23. package/dist/auth-vault.d.ts +0 -36
  24. package/dist/auth-vault.d.ts.map +0 -1
  25. package/dist/auth-vault.js +0 -125
  26. package/dist/auth-vault.js.map +0 -1
  27. package/dist/browser.d.ts +0 -629
  28. package/dist/browser.d.ts.map +0 -1
  29. package/dist/browser.js +0 -2342
  30. package/dist/browser.js.map +0 -1
  31. package/dist/confirmation.d.ts +0 -8
  32. package/dist/confirmation.d.ts.map +0 -1
  33. package/dist/confirmation.js +0 -30
  34. package/dist/confirmation.js.map +0 -1
  35. package/dist/daemon.d.ts +0 -71
  36. package/dist/daemon.d.ts.map +0 -1
  37. package/dist/daemon.js +0 -669
  38. package/dist/daemon.js.map +0 -1
  39. package/dist/diff.d.ts +0 -18
  40. package/dist/diff.d.ts.map +0 -1
  41. package/dist/diff.js +0 -271
  42. package/dist/diff.js.map +0 -1
  43. package/dist/domain-filter.d.ts +0 -28
  44. package/dist/domain-filter.d.ts.map +0 -1
  45. package/dist/domain-filter.js +0 -149
  46. package/dist/domain-filter.js.map +0 -1
  47. package/dist/encryption.d.ts +0 -73
  48. package/dist/encryption.d.ts.map +0 -1
  49. package/dist/encryption.js +0 -171
  50. package/dist/encryption.js.map +0 -1
  51. package/dist/index.d.ts +0 -7
  52. package/dist/index.d.ts.map +0 -1
  53. package/dist/index.js +0 -5
  54. package/dist/index.js.map +0 -1
  55. package/dist/inspect-server.d.ts +0 -26
  56. package/dist/inspect-server.d.ts.map +0 -1
  57. package/dist/inspect-server.js +0 -218
  58. package/dist/inspect-server.js.map +0 -1
  59. package/dist/ios-actions.d.ts +0 -11
  60. package/dist/ios-actions.d.ts.map +0 -1
  61. package/dist/ios-actions.js +0 -228
  62. package/dist/ios-actions.js.map +0 -1
  63. package/dist/ios-manager.d.ts +0 -266
  64. package/dist/ios-manager.d.ts.map +0 -1
  65. package/dist/ios-manager.js +0 -1073
  66. package/dist/ios-manager.js.map +0 -1
  67. package/dist/protocol.d.ts +0 -28
  68. package/dist/protocol.d.ts.map +0 -1
  69. package/dist/protocol.js +0 -988
  70. package/dist/protocol.js.map +0 -1
  71. package/dist/snapshot.d.ts +0 -67
  72. package/dist/snapshot.d.ts.map +0 -1
  73. package/dist/snapshot.js +0 -514
  74. package/dist/snapshot.js.map +0 -1
  75. package/dist/state-utils.d.ts +0 -77
  76. package/dist/state-utils.d.ts.map +0 -1
  77. package/dist/state-utils.js +0 -178
  78. package/dist/state-utils.js.map +0 -1
  79. package/dist/stream-server.d.ts +0 -117
  80. package/dist/stream-server.d.ts.map +0 -1
  81. package/dist/stream-server.js +0 -309
  82. package/dist/stream-server.js.map +0 -1
  83. package/dist/types.d.ts +0 -927
  84. package/dist/types.d.ts.map +0 -1
  85. package/dist/types.js +0 -2
  86. package/dist/types.js.map +0 -1
package/dist/actions.js DELETED
@@ -1,2121 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { exec } from 'node:child_process';
4
- import { mkdirSync } from 'node:fs';
5
- import { getAppDir } from './daemon.js';
6
- import { checkPolicy, describeAction, getActionCategory, loadPolicyFile, initPolicyReloader, reloadPolicyIfChanged, } from './action-policy.js';
7
- import { requestConfirmation, getAndRemovePending } from './confirmation.js';
8
- import { getAuthProfile, updateLastLogin } from './auth-vault.js';
9
- import { getSessionsDir, readStateFile, isValidSessionName, isEncryptedPayload, listStateFiles, cleanupExpiredStates, } from './state-utils.js';
10
- import { successResponse, errorResponse, parseCommand } from './protocol.js';
11
- import { diffSnapshots, diffScreenshots } from './diff.js';
12
- import { getEnhancedSnapshot } from './snapshot.js';
13
- // Callback for screencast frames - will be set by the daemon when streaming is active
14
- let screencastFrameCallback = null;
15
- /**
16
- * Set the callback for screencast frames
17
- * This is called by the daemon to set up frame streaming
18
- */
19
- export function setScreencastFrameCallback(callback) {
20
- screencastFrameCallback = callback;
21
- }
22
- /**
23
- * Convert Playwright errors to AI-friendly messages
24
- * @internal Exported for testing
25
- */
26
- export function toAIFriendlyError(error, selector) {
27
- const message = error instanceof Error ? error.message : String(error);
28
- // Handle strict mode violation (multiple elements match)
29
- if (message.includes('strict mode violation')) {
30
- // Extract count if available
31
- const countMatch = message.match(/resolved to (\d+) elements/);
32
- const count = countMatch ? countMatch[1] : 'multiple';
33
- return new Error(`Selector "${selector}" matched ${count} elements. ` +
34
- `Run 'snapshot' to get updated refs, or use a more specific CSS selector.`);
35
- }
36
- // Handle element not interactable (must be checked BEFORE timeout case)
37
- // This includes cases where an overlay/modal blocks the element
38
- if (message.includes('intercepts pointer events')) {
39
- return new Error(`Element "${selector}" is blocked by another element (likely a modal or overlay). ` +
40
- `Try dismissing any modals/cookie banners first.`);
41
- }
42
- // Handle element not visible
43
- if (message.includes('not visible') && !message.includes('Timeout')) {
44
- return new Error(`Element "${selector}" is not visible. ` +
45
- `Try scrolling it into view or check if it's hidden.`);
46
- }
47
- // Handle general timeout (element exists but action couldn't complete)
48
- if (message.includes('Timeout') && message.includes('exceeded')) {
49
- return new Error(`Action on "${selector}" timed out. The element may be blocked, still loading, or not interactable. ` +
50
- `Run 'snapshot' to check the current page state.`);
51
- }
52
- // Handle element not found (timeout waiting for element)
53
- if (message.includes('waiting for') &&
54
- (message.includes('to be visible') || message.includes('Timeout'))) {
55
- return new Error(`Element "${selector}" not found or not visible. ` +
56
- `Run 'snapshot' to see current page elements.`);
57
- }
58
- // Return original error for unknown cases
59
- return error instanceof Error ? error : new Error(message);
60
- }
61
- let actionPolicy = null;
62
- let confirmCategories = new Set();
63
- export function initActionPolicy() {
64
- const policyPath = process.env.AGENT_BROWSER_ACTION_POLICY;
65
- if (policyPath) {
66
- try {
67
- actionPolicy = loadPolicyFile(policyPath);
68
- initPolicyReloader(policyPath, actionPolicy);
69
- }
70
- catch (err) {
71
- console.error(`[ERROR] Failed to load action policy from ${policyPath}: ${err instanceof Error ? err.message : err}`);
72
- process.exit(1);
73
- }
74
- }
75
- const confirmActionsEnv = process.env.AGENT_BROWSER_CONFIRM_ACTIONS;
76
- if (confirmActionsEnv) {
77
- confirmCategories = new Set(confirmActionsEnv
78
- .split(',')
79
- .map((c) => c.trim().toLowerCase())
80
- .filter((c) => c.length > 0));
81
- }
82
- }
83
- /**
84
- * Execute a command and return a response
85
- */
86
- export async function executeCommand(command, browser) {
87
- try {
88
- // Handle confirm/deny actions (bypass policy check)
89
- if (command.action === 'confirm') {
90
- return await handleConfirm(command, browser);
91
- }
92
- if (command.action === 'deny') {
93
- return handleDeny(command);
94
- }
95
- // Hot-reload policy file if it changed on disk
96
- actionPolicy = reloadPolicyIfChanged();
97
- // Policy enforcement
98
- const decision = checkPolicy(command.action, actionPolicy, confirmCategories);
99
- if (decision === 'deny') {
100
- const category = getActionCategory(command.action);
101
- return errorResponse(command.id, `Action denied by policy: '${category}' is not allowed`);
102
- }
103
- if (decision === 'confirm') {
104
- const category = getActionCategory(command.action);
105
- const description = describeAction(command.action, command);
106
- const { confirmationId } = requestConfirmation(command.action, category, description, command);
107
- return successResponse(command.id, {
108
- confirmation_required: true,
109
- action: command.action,
110
- category,
111
- description,
112
- confirmation_id: confirmationId,
113
- });
114
- }
115
- return await dispatchAction(command, browser);
116
- }
117
- catch (error) {
118
- const message = error instanceof Error ? error.message : String(error);
119
- return errorResponse(command.id, message);
120
- }
121
- }
122
- /**
123
- * Dispatch a command to its handler after policy checks have passed.
124
- */
125
- async function dispatchAction(command, browser) {
126
- switch (command.action) {
127
- case 'launch':
128
- return await handleLaunch(command, browser);
129
- case 'navigate':
130
- return await handleNavigate(command, browser);
131
- case 'click':
132
- return await handleClick(command, browser);
133
- case 'type':
134
- return await handleType(command, browser);
135
- case 'fill':
136
- return await handleFill(command, browser);
137
- case 'check':
138
- return await handleCheck(command, browser);
139
- case 'uncheck':
140
- return await handleUncheck(command, browser);
141
- case 'upload':
142
- return await handleUpload(command, browser);
143
- case 'dblclick':
144
- return await handleDoubleClick(command, browser);
145
- case 'focus':
146
- return await handleFocus(command, browser);
147
- case 'drag':
148
- return await handleDrag(command, browser);
149
- case 'frame':
150
- return await handleFrame(command, browser);
151
- case 'mainframe':
152
- return await handleMainFrame(command, browser);
153
- case 'getbyrole':
154
- return await handleGetByRole(command, browser);
155
- case 'getbytext':
156
- return await handleGetByText(command, browser);
157
- case 'getbylabel':
158
- return await handleGetByLabel(command, browser);
159
- case 'getbyplaceholder':
160
- return await handleGetByPlaceholder(command, browser);
161
- case 'press':
162
- return await handlePress(command, browser);
163
- case 'screenshot':
164
- return await handleScreenshot(command, browser);
165
- case 'snapshot':
166
- return await handleSnapshot(command, browser);
167
- case 'evaluate':
168
- return await handleEvaluate(command, browser);
169
- case 'wait':
170
- return await handleWait(command, browser);
171
- case 'scroll':
172
- return await handleScroll(command, browser);
173
- case 'select':
174
- return await handleSelect(command, browser);
175
- case 'hover':
176
- return await handleHover(command, browser);
177
- case 'content':
178
- return await handleContent(command, browser);
179
- case 'close':
180
- return await handleClose(command, browser);
181
- case 'tab_new':
182
- return await handleTabNew(command, browser);
183
- case 'tab_list':
184
- return await handleTabList(command, browser);
185
- case 'tab_switch':
186
- return await handleTabSwitch(command, browser);
187
- case 'tab_close':
188
- return await handleTabClose(command, browser);
189
- case 'window_new':
190
- return await handleWindowNew(command, browser);
191
- case 'cookies_get':
192
- return await handleCookiesGet(command, browser);
193
- case 'cookies_set':
194
- return await handleCookiesSet(command, browser);
195
- case 'cookies_clear':
196
- return await handleCookiesClear(command, browser);
197
- case 'storage_get':
198
- return await handleStorageGet(command, browser);
199
- case 'storage_set':
200
- return await handleStorageSet(command, browser);
201
- case 'storage_clear':
202
- return await handleStorageClear(command, browser);
203
- case 'dialog':
204
- return await handleDialog(command, browser);
205
- case 'pdf':
206
- return await handlePdf(command, browser);
207
- case 'route':
208
- return await handleRoute(command, browser);
209
- case 'unroute':
210
- return await handleUnroute(command, browser);
211
- case 'requests':
212
- return await handleRequests(command, browser);
213
- case 'download':
214
- return await handleDownload(command, browser);
215
- case 'geolocation':
216
- return await handleGeolocation(command, browser);
217
- case 'permissions':
218
- return await handlePermissions(command, browser);
219
- case 'viewport':
220
- return await handleViewport(command, browser);
221
- case 'useragent':
222
- return await handleUserAgent(command, browser);
223
- case 'device':
224
- return await handleDevice(command, browser);
225
- case 'back':
226
- return await handleBack(command, browser);
227
- case 'forward':
228
- return await handleForward(command, browser);
229
- case 'reload':
230
- return await handleReload(command, browser);
231
- case 'url':
232
- return await handleUrl(command, browser);
233
- case 'cdp_url':
234
- return handleCdpUrl(command, browser);
235
- case 'inspect':
236
- return await handleInspect(command, browser);
237
- case 'title':
238
- return await handleTitle(command, browser);
239
- case 'getattribute':
240
- return await handleGetAttribute(command, browser);
241
- case 'gettext':
242
- return await handleGetText(command, browser);
243
- case 'isvisible':
244
- return await handleIsVisible(command, browser);
245
- case 'isenabled':
246
- return await handleIsEnabled(command, browser);
247
- case 'ischecked':
248
- return await handleIsChecked(command, browser);
249
- case 'count':
250
- return await handleCount(command, browser);
251
- case 'boundingbox':
252
- return await handleBoundingBox(command, browser);
253
- case 'styles':
254
- return await handleStyles(command, browser);
255
- case 'video_start':
256
- return await handleVideoStart(command, browser);
257
- case 'video_stop':
258
- return await handleVideoStop(command, browser);
259
- case 'trace_start':
260
- return await handleTraceStart(command, browser);
261
- case 'trace_stop':
262
- return await handleTraceStop(command, browser);
263
- case 'profiler_start':
264
- return await handleProfilerStart(command, browser);
265
- case 'profiler_stop':
266
- return await handleProfilerStop(command, browser);
267
- case 'har_start':
268
- return await handleHarStart(command, browser);
269
- case 'har_stop':
270
- return await handleHarStop(command, browser);
271
- case 'state_save':
272
- return await handleStateSave(command, browser);
273
- case 'state_load':
274
- return await handleStateLoad(command, browser);
275
- case 'state_list':
276
- return await handleStateList(command);
277
- case 'state_clear':
278
- return await handleStateClear(command);
279
- case 'state_show':
280
- return await handleStateShow(command);
281
- case 'state_clean':
282
- return await handleStateClean(command);
283
- case 'state_rename':
284
- return await handleStateRename(command);
285
- case 'console':
286
- return await handleConsole(command, browser);
287
- case 'errors':
288
- return await handleErrors(command, browser);
289
- case 'keyboard':
290
- return await handleKeyboard(command, browser);
291
- case 'wheel':
292
- return await handleWheel(command, browser);
293
- case 'tap':
294
- return await handleTap(command, browser);
295
- case 'clipboard':
296
- return await handleClipboard(command, browser);
297
- case 'highlight':
298
- return await handleHighlight(command, browser);
299
- case 'clear':
300
- return await handleClear(command, browser);
301
- case 'selectall':
302
- return await handleSelectAll(command, browser);
303
- case 'innertext':
304
- return await handleInnerText(command, browser);
305
- case 'innerhtml':
306
- return await handleInnerHtml(command, browser);
307
- case 'inputvalue':
308
- return await handleInputValue(command, browser);
309
- case 'setvalue':
310
- return await handleSetValue(command, browser);
311
- case 'dispatch':
312
- return await handleDispatch(command, browser);
313
- case 'evalhandle':
314
- return await handleEvalHandle(command, browser);
315
- case 'expose':
316
- return await handleExpose(command, browser);
317
- case 'addscript':
318
- return await handleAddScript(command, browser);
319
- case 'addstyle':
320
- return await handleAddStyle(command, browser);
321
- case 'emulatemedia':
322
- return await handleEmulateMedia(command, browser);
323
- case 'offline':
324
- return await handleOffline(command, browser);
325
- case 'headers':
326
- return await handleHeaders(command, browser);
327
- case 'pause':
328
- return await handlePause(command, browser);
329
- case 'getbyalttext':
330
- return await handleGetByAltText(command, browser);
331
- case 'getbytitle':
332
- return await handleGetByTitle(command, browser);
333
- case 'getbytestid':
334
- return await handleGetByTestId(command, browser);
335
- case 'nth':
336
- return await handleNth(command, browser);
337
- case 'waitforurl':
338
- return await handleWaitForUrl(command, browser);
339
- case 'waitforloadstate':
340
- return await handleWaitForLoadState(command, browser);
341
- case 'setcontent':
342
- return await handleSetContent(command, browser);
343
- case 'timezone':
344
- return await handleTimezone(command, browser);
345
- case 'locale':
346
- return await handleLocale(command, browser);
347
- case 'credentials':
348
- return await handleCredentials(command, browser);
349
- case 'mousemove':
350
- return await handleMouseMove(command, browser);
351
- case 'mousedown':
352
- return await handleMouseDown(command, browser);
353
- case 'mouseup':
354
- return await handleMouseUp(command, browser);
355
- case 'bringtofront':
356
- return await handleBringToFront(command, browser);
357
- case 'waitforfunction':
358
- return await handleWaitForFunction(command, browser);
359
- case 'scrollintoview':
360
- return await handleScrollIntoView(command, browser);
361
- case 'addinitscript':
362
- return await handleAddInitScript(command, browser);
363
- case 'keydown':
364
- return await handleKeyDown(command, browser);
365
- case 'keyup':
366
- return await handleKeyUp(command, browser);
367
- case 'inserttext':
368
- return await handleInsertText(command, browser);
369
- case 'multiselect':
370
- return await handleMultiSelect(command, browser);
371
- case 'waitfordownload':
372
- return await handleWaitForDownload(command, browser);
373
- case 'responsebody':
374
- return await handleResponseBody(command, browser);
375
- case 'screencast_start':
376
- return await handleScreencastStart(command, browser);
377
- case 'screencast_stop':
378
- return await handleScreencastStop(command, browser);
379
- case 'input_mouse':
380
- return await handleInputMouse(command, browser);
381
- case 'input_keyboard':
382
- return await handleInputKeyboard(command, browser);
383
- case 'input_touch':
384
- return await handleInputTouch(command, browser);
385
- case 'recording_start':
386
- return await handleRecordingStart(command, browser);
387
- case 'recording_stop':
388
- return await handleRecordingStop(command, browser);
389
- case 'recording_restart':
390
- return await handleRecordingRestart(command, browser);
391
- case 'diff_snapshot':
392
- return await handleDiffSnapshot(command, browser);
393
- case 'diff_screenshot':
394
- return await handleDiffScreenshot(command, browser);
395
- case 'diff_url':
396
- return await handleDiffUrl(command, browser);
397
- case 'auth_login':
398
- return await handleAuthLogin(command, browser);
399
- default: {
400
- // TypeScript narrows to never here, but we handle it for safety
401
- const unknownCommand = command;
402
- return errorResponse(unknownCommand.id, `Unknown action: ${unknownCommand.action}`);
403
- }
404
- }
405
- }
406
- async function handleLaunch(command, browser) {
407
- if (command.engine === 'lightpanda') {
408
- return errorResponse(command.id, 'Lightpanda engine requires --native mode');
409
- }
410
- await browser.launch(command);
411
- return successResponse(command.id, { launched: true });
412
- }
413
- async function handleNavigate(command, browser) {
414
- const result = await browser.navigate(command.url, {
415
- headers: command.headers,
416
- waitUntil: command.waitUntil,
417
- });
418
- return successResponse(command.id, result);
419
- }
420
- async function handleClick(command, browser) {
421
- // Support both refs (@e1) and regular selectors
422
- const locator = browser.getLocator(command.selector);
423
- try {
424
- // If --new-tab flag is set, get the href and open in a new tab
425
- if (command.newTab) {
426
- const fullUrl = await locator.evaluate((el) => {
427
- const href = el.getAttribute('href');
428
- // URL and document.baseURI are available in the browser context
429
- return href
430
- ? new globalThis.URL(href, globalThis.document.baseURI).toString()
431
- : '';
432
- });
433
- if (!fullUrl) {
434
- throw new Error(`Element '${command.selector}' does not have an href attribute. --new-tab only works on links.`);
435
- }
436
- await browser.newTab();
437
- const newPage = browser.getPage();
438
- await newPage.goto(fullUrl);
439
- return successResponse(command.id, {
440
- clicked: true,
441
- newTab: true,
442
- url: fullUrl,
443
- });
444
- }
445
- await locator.click({
446
- button: command.button,
447
- clickCount: command.clickCount,
448
- delay: command.delay,
449
- });
450
- }
451
- catch (error) {
452
- throw toAIFriendlyError(error, command.selector);
453
- }
454
- return successResponse(command.id, { clicked: true });
455
- }
456
- async function handleType(command, browser) {
457
- const locator = browser.getLocator(command.selector);
458
- try {
459
- if (command.clear) {
460
- await locator.fill('');
461
- }
462
- await locator.pressSequentially(command.text, {
463
- delay: command.delay,
464
- });
465
- }
466
- catch (error) {
467
- throw toAIFriendlyError(error, command.selector);
468
- }
469
- return successResponse(command.id, { typed: true });
470
- }
471
- async function handlePress(command, browser) {
472
- const page = browser.getPage();
473
- if (command.selector) {
474
- await page.press(command.selector, command.key);
475
- }
476
- else {
477
- await page.keyboard.press(command.key);
478
- }
479
- return successResponse(command.id, { pressed: true });
480
- }
481
- const ANNOTATION_OVERLAY_ID = '__agent_browser_annotations__';
482
- async function removeAnnotationOverlay(page) {
483
- await page
484
- .evaluate(`(() => { const el = document.getElementById(${JSON.stringify(ANNOTATION_OVERLAY_ID)}); if (el) el.remove(); })()`)
485
- .catch(() => { });
486
- }
487
- async function handleScreenshot(command, browser) {
488
- const page = browser.getPage();
489
- const options = {
490
- fullPage: command.fullPage,
491
- type: command.format ?? 'png',
492
- };
493
- if (command.format === 'jpeg' && command.quality !== undefined) {
494
- options.quality = command.quality;
495
- }
496
- let target = page;
497
- if (command.selector) {
498
- target = browser.getLocator(command.selector);
499
- }
500
- let overlayInjected = false;
501
- try {
502
- let savePath = command.path;
503
- if (!savePath) {
504
- const ext = command.format === 'jpeg' ? 'jpg' : 'png';
505
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
506
- const random = Math.random().toString(36).substring(2, 8);
507
- const filename = `screenshot-${timestamp}-${random}.${ext}`;
508
- const screenshotDir = command.screenshotDir ?? path.join(getAppDir(), 'tmp', 'screenshots');
509
- mkdirSync(screenshotDir, { recursive: true });
510
- savePath = path.join(screenshotDir, filename);
511
- }
512
- let annotations;
513
- if (command.annotate) {
514
- const { refs } = await browser.getSnapshot({ interactive: true });
515
- const entries = Object.entries(refs);
516
- const results = await Promise.all(entries.map(async ([ref, data]) => {
517
- try {
518
- const locator = browser.getLocatorFromRef(ref);
519
- if (!locator)
520
- return null;
521
- const box = await locator.boundingBox();
522
- if (!box || box.width === 0 || box.height === 0)
523
- return null;
524
- const num = parseInt(ref.replace('e', ''), 10);
525
- return {
526
- ref,
527
- number: num,
528
- role: data.role,
529
- name: data.name || undefined,
530
- box: {
531
- x: Math.round(box.x),
532
- y: Math.round(box.y),
533
- width: Math.round(box.width),
534
- height: Math.round(box.height),
535
- },
536
- };
537
- }
538
- catch {
539
- return null;
540
- }
541
- }));
542
- // When a selector is provided the screenshot is cropped to that element,
543
- // so filter to annotations that overlap the target and shift coordinates.
544
- let targetBox = null;
545
- if (command.selector) {
546
- const raw = await browser.getLocator(command.selector).boundingBox();
547
- if (raw) {
548
- targetBox = {
549
- x: Math.round(raw.x),
550
- y: Math.round(raw.y),
551
- width: Math.round(raw.width),
552
- height: Math.round(raw.height),
553
- };
554
- }
555
- }
556
- const filtered = results.filter((a) => a !== null);
557
- // Filter by selector overlap if needed, but keep viewport-relative coords
558
- // for overlay positioning. Coordinate shifting happens later for metadata only.
559
- let overlayItems;
560
- if (targetBox) {
561
- const tb = targetBox;
562
- overlayItems = filtered
563
- .filter((a) => {
564
- const ax2 = a.box.x + a.box.width;
565
- const ay2 = a.box.y + a.box.height;
566
- const bx2 = tb.x + tb.width;
567
- const by2 = tb.y + tb.height;
568
- return a.box.x < bx2 && ax2 > tb.x && a.box.y < by2 && ay2 > tb.y;
569
- })
570
- .sort((a, b) => a.number - b.number);
571
- }
572
- else {
573
- overlayItems = filtered.sort((a, b) => a.number - b.number);
574
- }
575
- if (overlayItems.length > 0) {
576
- const overlayData = overlayItems.map((a) => ({
577
- number: a.number,
578
- x: a.box.x,
579
- y: a.box.y,
580
- width: a.box.width,
581
- height: a.box.height,
582
- }));
583
- // Uses position:absolute with document-relative coords so labels render
584
- // correctly for both viewport and fullPage screenshots, and when the
585
- // screenshot is scoped to a selector element.
586
- await page.evaluate(`(() => {
587
- var items = ${JSON.stringify(overlayData)};
588
- var id = ${JSON.stringify(ANNOTATION_OVERLAY_ID)};
589
- var sx = window.scrollX || 0;
590
- var sy = window.scrollY || 0;
591
- var c = document.createElement('div');
592
- c.id = id;
593
- c.style.cssText = 'position:absolute;top:0;left:0;width:0;height:0;pointer-events:none;z-index:2147483647;';
594
- for (var i = 0; i < items.length; i++) {
595
- var it = items[i];
596
- var dx = it.x + sx;
597
- var dy = it.y + sy;
598
- var b = document.createElement('div');
599
- b.style.cssText = 'position:absolute;left:' + dx + 'px;top:' + dy + 'px;width:' + it.width + 'px;height:' + it.height + 'px;border:2px solid rgba(255,0,0,0.8);box-sizing:border-box;pointer-events:none;';
600
- var l = document.createElement('div');
601
- l.textContent = String(it.number);
602
- var labelTop = dy < 14 ? '2px' : '-14px';
603
- l.style.cssText = 'position:absolute;top:' + labelTop + ';left:-2px;background:rgba(255,0,0,0.9);color:#fff;font:bold 11px/14px monospace;padding:0 4px;border-radius:2px;white-space:nowrap;';
604
- b.appendChild(l);
605
- c.appendChild(b);
606
- }
607
- document.documentElement.appendChild(c);
608
- })()`);
609
- overlayInjected = true;
610
- }
611
- // Build returned annotation metadata with image-relative coordinates.
612
- // Selector: shift to target-element-relative.
613
- // fullPage: convert to document-relative (matching fullPage image origin).
614
- // Default: viewport-relative (unchanged).
615
- if (targetBox) {
616
- const tb = targetBox;
617
- annotations = overlayItems.map((a) => ({
618
- ...a,
619
- box: {
620
- x: a.box.x - tb.x,
621
- y: a.box.y - tb.y,
622
- width: a.box.width,
623
- height: a.box.height,
624
- },
625
- }));
626
- }
627
- else if (command.fullPage) {
628
- const scroll = (await page.evaluate(`({x: window.scrollX || 0, y: window.scrollY || 0})`));
629
- annotations = overlayItems.map((a) => ({
630
- ...a,
631
- box: {
632
- x: a.box.x + scroll.x,
633
- y: a.box.y + scroll.y,
634
- width: a.box.width,
635
- height: a.box.height,
636
- },
637
- }));
638
- }
639
- else {
640
- annotations = overlayItems;
641
- }
642
- }
643
- await target.screenshot({ ...options, path: savePath });
644
- if (overlayInjected) {
645
- await removeAnnotationOverlay(page);
646
- }
647
- return successResponse(command.id, {
648
- path: savePath,
649
- ...(annotations && annotations.length > 0 ? { annotations } : {}),
650
- });
651
- }
652
- catch (error) {
653
- if (overlayInjected) {
654
- await removeAnnotationOverlay(page);
655
- }
656
- if (command.selector) {
657
- throw toAIFriendlyError(error, command.selector);
658
- }
659
- throw error;
660
- }
661
- }
662
- async function handleSnapshot(command, browser) {
663
- // Use enhanced snapshot with refs and optional filtering
664
- const { tree, refs } = await browser.getSnapshot({
665
- interactive: command.interactive,
666
- cursor: command.cursor,
667
- maxDepth: command.maxDepth,
668
- compact: command.compact,
669
- selector: command.selector,
670
- });
671
- // Simplify refs for output (just role and name)
672
- const simpleRefs = {};
673
- for (const [ref, data] of Object.entries(refs)) {
674
- simpleRefs[ref] = { role: data.role, name: data.name };
675
- }
676
- const page = browser.getPage();
677
- return successResponse(command.id, {
678
- snapshot: tree || 'Empty page',
679
- refs: Object.keys(simpleRefs).length > 0 ? simpleRefs : undefined,
680
- origin: page.url(),
681
- });
682
- }
683
- async function handleEvaluate(command, browser) {
684
- const page = browser.getPage();
685
- // Evaluate the script directly as a string expression
686
- const result = await page.evaluate(command.script);
687
- return successResponse(command.id, { result, origin: page.url() });
688
- }
689
- async function handleWait(command, browser) {
690
- const page = browser.getPage();
691
- if (command.text) {
692
- await page.waitForFunction(`(document.body.innerText || '').includes(${JSON.stringify(command.text)})`, { timeout: command.timeout });
693
- }
694
- else if (command.selector) {
695
- await page.waitForSelector(command.selector, {
696
- state: command.state ?? 'visible',
697
- timeout: command.timeout,
698
- });
699
- }
700
- else if (command.timeout) {
701
- await page.waitForTimeout(command.timeout);
702
- }
703
- else {
704
- await page.waitForLoadState('load');
705
- }
706
- return successResponse(command.id, { waited: true });
707
- }
708
- async function handleScroll(command, browser) {
709
- const page = browser.getPage();
710
- let deltaX = command.x ?? 0;
711
- let deltaY = command.y ?? 0;
712
- const hasExplicitDelta = command.x !== undefined || command.y !== undefined;
713
- if (command.direction) {
714
- const amount = command.amount ?? 100;
715
- switch (command.direction) {
716
- case 'up':
717
- deltaY = -amount;
718
- break;
719
- case 'down':
720
- deltaY = amount;
721
- break;
722
- case 'left':
723
- deltaX = -amount;
724
- break;
725
- case 'right':
726
- deltaX = amount;
727
- break;
728
- }
729
- }
730
- if (command.selector) {
731
- const element = browser.getLocator(command.selector);
732
- await element.scrollIntoViewIfNeeded();
733
- if (hasExplicitDelta || deltaX !== 0 || deltaY !== 0) {
734
- await element.evaluate((el, { x, y }) => {
735
- el.scrollBy(x, y);
736
- }, { x: deltaX, y: deltaY });
737
- }
738
- }
739
- else {
740
- await page.evaluate(`window.scrollBy(${deltaX}, ${deltaY})`);
741
- }
742
- return successResponse(command.id, { scrolled: true });
743
- }
744
- async function handleSelect(command, browser) {
745
- const locator = browser.getLocator(command.selector);
746
- const values = Array.isArray(command.values) ? command.values : [command.values];
747
- try {
748
- await locator.selectOption(values);
749
- }
750
- catch (error) {
751
- throw toAIFriendlyError(error, command.selector);
752
- }
753
- return successResponse(command.id, { selected: values });
754
- }
755
- async function handleHover(command, browser) {
756
- const locator = browser.getLocator(command.selector);
757
- try {
758
- await locator.hover();
759
- }
760
- catch (error) {
761
- throw toAIFriendlyError(error, command.selector);
762
- }
763
- return successResponse(command.id, { hovered: true });
764
- }
765
- async function handleContent(command, browser) {
766
- const page = browser.getPage();
767
- let html;
768
- if (command.selector) {
769
- const locator = browser.getLocator(command.selector);
770
- html = await locator.innerHTML();
771
- }
772
- else {
773
- html = await page.content();
774
- }
775
- return successResponse(command.id, { html, origin: page.url() });
776
- }
777
- async function handleClose(command, browser) {
778
- await browser.close();
779
- return successResponse(command.id, { closed: true });
780
- }
781
- async function handleTabNew(command, browser) {
782
- const result = await browser.newTab();
783
- // Navigate to URL if provided (same pattern as handleNavigate)
784
- if (command.url) {
785
- const page = browser.getPage();
786
- await page.goto(command.url, { waitUntil: 'domcontentloaded' });
787
- }
788
- return successResponse(command.id, result);
789
- }
790
- async function handleTabList(command, browser) {
791
- const tabs = await browser.listTabs();
792
- return successResponse(command.id, {
793
- tabs,
794
- active: browser.getActiveIndex(),
795
- });
796
- }
797
- async function handleTabSwitch(command, browser) {
798
- const result = await browser.switchTo(command.index);
799
- const page = browser.getPage();
800
- return successResponse(command.id, {
801
- ...result,
802
- title: await page.title(),
803
- });
804
- }
805
- async function handleTabClose(command, browser) {
806
- const result = await browser.closeTab(command.index);
807
- return successResponse(command.id, result);
808
- }
809
- async function handleWindowNew(command, browser) {
810
- const result = await browser.newWindow(command.viewport);
811
- return successResponse(command.id, result);
812
- }
813
- // New handlers for enhanced Playwright parity
814
- async function handleFill(command, browser) {
815
- const locator = browser.getLocator(command.selector);
816
- try {
817
- await locator.fill(command.value);
818
- }
819
- catch (error) {
820
- throw toAIFriendlyError(error, command.selector);
821
- }
822
- return successResponse(command.id, { filled: true });
823
- }
824
- async function handleCheck(command, browser) {
825
- const locator = browser.getLocator(command.selector);
826
- try {
827
- await locator.check();
828
- }
829
- catch (error) {
830
- throw toAIFriendlyError(error, command.selector);
831
- }
832
- return successResponse(command.id, { checked: true });
833
- }
834
- async function handleUncheck(command, browser) {
835
- const locator = browser.getLocator(command.selector);
836
- try {
837
- await locator.uncheck();
838
- }
839
- catch (error) {
840
- throw toAIFriendlyError(error, command.selector);
841
- }
842
- return successResponse(command.id, { unchecked: true });
843
- }
844
- async function handleUpload(command, browser) {
845
- const locator = browser.getLocator(command.selector);
846
- const files = Array.isArray(command.files) ? command.files : [command.files];
847
- try {
848
- await locator.setInputFiles(files);
849
- }
850
- catch (error) {
851
- throw toAIFriendlyError(error, command.selector);
852
- }
853
- return successResponse(command.id, { uploaded: files });
854
- }
855
- async function handleDoubleClick(command, browser) {
856
- const locator = browser.getLocator(command.selector);
857
- try {
858
- await locator.dblclick();
859
- }
860
- catch (error) {
861
- throw toAIFriendlyError(error, command.selector);
862
- }
863
- return successResponse(command.id, { clicked: true });
864
- }
865
- async function handleFocus(command, browser) {
866
- const locator = browser.getLocator(command.selector);
867
- try {
868
- await locator.focus();
869
- }
870
- catch (error) {
871
- throw toAIFriendlyError(error, command.selector);
872
- }
873
- return successResponse(command.id, { focused: true });
874
- }
875
- async function handleDrag(command, browser) {
876
- const frame = browser.getFrame();
877
- await frame.dragAndDrop(command.source, command.target);
878
- return successResponse(command.id, { dragged: true });
879
- }
880
- async function handleFrame(command, browser) {
881
- await browser.switchToFrame({
882
- selector: command.selector,
883
- name: command.name,
884
- url: command.url,
885
- });
886
- return successResponse(command.id, { switched: true });
887
- }
888
- async function handleMainFrame(command, browser) {
889
- browser.switchToMainFrame();
890
- return successResponse(command.id, { switched: true });
891
- }
892
- async function handleGetByRole(command, browser) {
893
- const page = browser.getPage();
894
- const locator = page.getByRole(command.role, { name: command.name, exact: command.exact });
895
- switch (command.subaction) {
896
- case 'click':
897
- await locator.click();
898
- return successResponse(command.id, { clicked: true });
899
- case 'fill':
900
- await locator.fill(command.value ?? '');
901
- return successResponse(command.id, { filled: true });
902
- case 'check':
903
- await locator.check();
904
- return successResponse(command.id, { checked: true });
905
- case 'hover':
906
- await locator.hover();
907
- return successResponse(command.id, { hovered: true });
908
- }
909
- }
910
- async function handleGetByText(command, browser) {
911
- const page = browser.getPage();
912
- const locator = page.getByText(command.text, { exact: command.exact });
913
- switch (command.subaction) {
914
- case 'click':
915
- await locator.click();
916
- return successResponse(command.id, { clicked: true });
917
- case 'hover':
918
- await locator.hover();
919
- return successResponse(command.id, { hovered: true });
920
- }
921
- }
922
- async function handleGetByLabel(command, browser) {
923
- const page = browser.getPage();
924
- const locator = page.getByLabel(command.label, { exact: command.exact });
925
- switch (command.subaction) {
926
- case 'click':
927
- await locator.click();
928
- return successResponse(command.id, { clicked: true });
929
- case 'fill':
930
- await locator.fill(command.value ?? '');
931
- return successResponse(command.id, { filled: true });
932
- case 'check':
933
- await locator.check();
934
- return successResponse(command.id, { checked: true });
935
- }
936
- }
937
- async function handleGetByPlaceholder(command, browser) {
938
- const page = browser.getPage();
939
- const locator = page.getByPlaceholder(command.placeholder, { exact: command.exact });
940
- switch (command.subaction) {
941
- case 'click':
942
- await locator.click();
943
- return successResponse(command.id, { clicked: true });
944
- case 'fill':
945
- await locator.fill(command.value ?? '');
946
- return successResponse(command.id, { filled: true });
947
- }
948
- }
949
- async function handleCookiesGet(command, browser) {
950
- const page = browser.getPage();
951
- const context = page.context();
952
- const cookies = await context.cookies(command.urls);
953
- return successResponse(command.id, { cookies });
954
- }
955
- async function handleCookiesSet(command, browser) {
956
- const page = browser.getPage();
957
- const context = page.context();
958
- // Auto-fill URL for cookies that don't have domain/path/url set
959
- const pageUrl = page.url();
960
- const cookies = command.cookies.map((cookie) => {
961
- if (!cookie.url && !cookie.domain && !cookie.path) {
962
- return { ...cookie, url: pageUrl };
963
- }
964
- return cookie;
965
- });
966
- await context.addCookies(cookies);
967
- return successResponse(command.id, { set: true });
968
- }
969
- async function handleCookiesClear(command, browser) {
970
- const page = browser.getPage();
971
- const context = page.context();
972
- await context.clearCookies();
973
- return successResponse(command.id, { cleared: true });
974
- }
975
- async function handleStorageGet(command, browser) {
976
- const page = browser.getPage();
977
- const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
978
- if (command.key) {
979
- const value = await page.evaluate(`${storageType}.getItem(${JSON.stringify(command.key)})`);
980
- return successResponse(command.id, { key: command.key, value });
981
- }
982
- else {
983
- const data = await page.evaluate(`
984
- (() => {
985
- const storage = ${storageType};
986
- const result = {};
987
- for (let i = 0; i < storage.length; i++) {
988
- const key = storage.key(i);
989
- if (key) result[key] = storage.getItem(key);
990
- }
991
- return result;
992
- })()
993
- `);
994
- return successResponse(command.id, { data });
995
- }
996
- }
997
- async function handleStorageSet(command, browser) {
998
- const page = browser.getPage();
999
- const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
1000
- await page.evaluate(`${storageType}.setItem(${JSON.stringify(command.key)}, ${JSON.stringify(command.value)})`);
1001
- return successResponse(command.id, { set: true });
1002
- }
1003
- async function handleStorageClear(command, browser) {
1004
- const page = browser.getPage();
1005
- const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
1006
- await page.evaluate(`${storageType}.clear()`);
1007
- return successResponse(command.id, { cleared: true });
1008
- }
1009
- async function handleDialog(command, browser) {
1010
- browser.setDialogHandler(command.response, command.promptText);
1011
- return successResponse(command.id, { handler: 'set', response: command.response });
1012
- }
1013
- async function handlePdf(command, browser) {
1014
- const page = browser.getPage();
1015
- await page.pdf({
1016
- path: command.path,
1017
- format: command.format ?? 'Letter',
1018
- });
1019
- return successResponse(command.id, { path: command.path });
1020
- }
1021
- // Network & Request handlers
1022
- async function handleRoute(command, browser) {
1023
- await browser.addRoute(command.url, {
1024
- response: command.response,
1025
- abort: command.abort,
1026
- });
1027
- return successResponse(command.id, { routed: command.url });
1028
- }
1029
- async function handleUnroute(command, browser) {
1030
- await browser.removeRoute(command.url);
1031
- return successResponse(command.id, { unrouted: command.url ?? 'all' });
1032
- }
1033
- async function handleRequests(command, browser) {
1034
- if (command.clear) {
1035
- browser.clearRequests();
1036
- return successResponse(command.id, { cleared: true });
1037
- }
1038
- // Start tracking if not already
1039
- browser.startRequestTracking();
1040
- const requests = browser.getRequests(command.filter);
1041
- return successResponse(command.id, { requests });
1042
- }
1043
- async function handleDownload(command, browser) {
1044
- const page = browser.getPage();
1045
- const locator = browser.getLocator(command.selector);
1046
- const [download] = await Promise.all([page.waitForEvent('download'), locator.click()]);
1047
- await download.saveAs(command.path);
1048
- return successResponse(command.id, {
1049
- path: command.path,
1050
- suggestedFilename: download.suggestedFilename(),
1051
- });
1052
- }
1053
- async function handleGeolocation(command, browser) {
1054
- await browser.setGeolocation(command.latitude, command.longitude, command.accuracy);
1055
- return successResponse(command.id, {
1056
- latitude: command.latitude,
1057
- longitude: command.longitude,
1058
- });
1059
- }
1060
- async function handlePermissions(command, browser) {
1061
- await browser.setPermissions(command.permissions, command.grant);
1062
- return successResponse(command.id, {
1063
- permissions: command.permissions,
1064
- granted: command.grant,
1065
- });
1066
- }
1067
- async function handleViewport(command, browser) {
1068
- if (command.deviceScaleFactor && command.deviceScaleFactor !== 1) {
1069
- await browser.setViewport(command.width, command.height);
1070
- await browser.setDeviceScaleFactor(command.deviceScaleFactor, command.width, command.height, false);
1071
- }
1072
- else {
1073
- // deviceScaleFactor is 1 or undefined -- clear any previously-set CDP
1074
- // Emulation.setDeviceMetricsOverride so stale DPR doesn't persist.
1075
- try {
1076
- await browser.clearDeviceMetricsOverride();
1077
- }
1078
- catch {
1079
- // Ignore if override was never set
1080
- }
1081
- await browser.setViewport(command.width, command.height);
1082
- }
1083
- const result = {
1084
- width: command.width,
1085
- height: command.height,
1086
- };
1087
- if (command.deviceScaleFactor !== undefined) {
1088
- result.deviceScaleFactor = command.deviceScaleFactor;
1089
- }
1090
- return successResponse(command.id, result);
1091
- }
1092
- async function handleUserAgent(command, browser) {
1093
- const page = browser.getPage();
1094
- const context = page.context();
1095
- // Note: Can't change user agent after context is created, but we can for new pages
1096
- return successResponse(command.id, {
1097
- note: 'User agent can only be set at launch time. Use device command instead.',
1098
- });
1099
- }
1100
- async function handleDevice(command, browser) {
1101
- const device = browser.getDevice(command.device);
1102
- if (!device) {
1103
- const available = browser.listDevices().slice(0, 10).join(', ');
1104
- throw new Error(`Unknown device: ${command.device}. Available: ${available}...`);
1105
- }
1106
- // Apply device viewport
1107
- await browser.setViewport(device.viewport.width, device.viewport.height);
1108
- // Apply or clear device scale factor
1109
- if (device.deviceScaleFactor && device.deviceScaleFactor !== 1) {
1110
- // Apply device scale factor for HiDPI/retina displays
1111
- await browser.setDeviceScaleFactor(device.deviceScaleFactor, device.viewport.width, device.viewport.height, device.isMobile ?? false);
1112
- }
1113
- else {
1114
- // Clear device scale factor override to restore default (1x)
1115
- try {
1116
- await browser.clearDeviceMetricsOverride();
1117
- }
1118
- catch {
1119
- // Ignore error if override was never set
1120
- }
1121
- }
1122
- return successResponse(command.id, {
1123
- device: command.device,
1124
- viewport: device.viewport,
1125
- userAgent: device.userAgent,
1126
- deviceScaleFactor: device.deviceScaleFactor,
1127
- });
1128
- }
1129
- async function handleBack(command, browser) {
1130
- const page = browser.getPage();
1131
- await page.goBack();
1132
- return successResponse(command.id, { url: page.url() });
1133
- }
1134
- async function handleForward(command, browser) {
1135
- const page = browser.getPage();
1136
- await page.goForward();
1137
- return successResponse(command.id, { url: page.url() });
1138
- }
1139
- async function handleReload(command, browser) {
1140
- const page = browser.getPage();
1141
- await page.reload();
1142
- return successResponse(command.id, { url: page.url() });
1143
- }
1144
- async function handleUrl(command, browser) {
1145
- const page = browser.getPage();
1146
- return successResponse(command.id, { url: page.url() });
1147
- }
1148
- function handleCdpUrl(command, browser) {
1149
- const cdpUrl = browser.getCdpUrl();
1150
- if (!cdpUrl) {
1151
- return errorResponse(command.id, 'CDP URL not available (browser may not be launched)');
1152
- }
1153
- return successResponse(command.id, { cdpUrl });
1154
- }
1155
- async function handleInspect(command, browser) {
1156
- const cdpUrl = browser.getCdpUrl();
1157
- if (!cdpUrl) {
1158
- return errorResponse(command.id, 'CDP URL not available (browser may not be launched)');
1159
- }
1160
- // Shut down any existing inspect server so we always target the current page
1161
- browser.stopInspectServer();
1162
- const stripped = cdpUrl.replace(/^(wss?|https?):\/\//, '');
1163
- const hostPort = stripped.split('/')[0];
1164
- // Get the target ID so the inspect server can create its own dedicated CDP session
1165
- const page = browser.getPage();
1166
- const context = page.context();
1167
- const tmpCdp = await context.newCDPSession(page);
1168
- let targetId = '';
1169
- try {
1170
- const info = await tmpCdp.send('Target.getTargetInfo');
1171
- targetId = info?.targetInfo?.targetId || '';
1172
- }
1173
- catch (err) {
1174
- console.error('[inspect] getTargetInfo failed:', err);
1175
- }
1176
- await tmpCdp.detach();
1177
- if (!targetId) {
1178
- return errorResponse(command.id, 'Could not determine target ID for active page');
1179
- }
1180
- const { InspectServer } = await import('./inspect-server.js');
1181
- const server = new InspectServer({
1182
- chromeHostPort: hostPort,
1183
- targetId,
1184
- chromeWsUrl: cdpUrl,
1185
- });
1186
- await server.start();
1187
- browser.setInspectServer(server);
1188
- const url = `http://127.0.0.1:${server.port}`;
1189
- openUrlInBrowser(url);
1190
- return successResponse(command.id, { opened: true, url });
1191
- }
1192
- function openUrlInBrowser(url) {
1193
- const platform = process.platform;
1194
- const cmd = platform === 'darwin'
1195
- ? `open "${url}"`
1196
- : platform === 'win32'
1197
- ? `start "" "${url}"`
1198
- : `xdg-open "${url}"`;
1199
- exec(cmd, (err) => {
1200
- if (err)
1201
- console.error('[inspect] Failed to open browser:', err.message);
1202
- });
1203
- }
1204
- async function handleTitle(command, browser) {
1205
- const page = browser.getPage();
1206
- const title = await page.title();
1207
- return successResponse(command.id, { title });
1208
- }
1209
- async function handleGetAttribute(command, browser) {
1210
- const page = browser.getPage();
1211
- const locator = browser.getLocator(command.selector);
1212
- const value = await locator.getAttribute(command.attribute);
1213
- return successResponse(command.id, { attribute: command.attribute, value, origin: page.url() });
1214
- }
1215
- async function handleGetText(command, browser) {
1216
- const page = browser.getPage();
1217
- const locator = browser.getLocator(command.selector);
1218
- const inner = await locator.innerText();
1219
- const text = inner || (await locator.textContent()) || '';
1220
- return successResponse(command.id, { text, origin: page.url() });
1221
- }
1222
- async function handleIsVisible(command, browser) {
1223
- const locator = browser.getLocator(command.selector);
1224
- const visible = await locator.isVisible();
1225
- return successResponse(command.id, { visible });
1226
- }
1227
- async function handleIsEnabled(command, browser) {
1228
- const locator = browser.getLocator(command.selector);
1229
- const enabled = await locator.isEnabled();
1230
- return successResponse(command.id, { enabled });
1231
- }
1232
- async function handleIsChecked(command, browser) {
1233
- const locator = browser.getLocator(command.selector);
1234
- const checked = await locator.isChecked();
1235
- return successResponse(command.id, { checked });
1236
- }
1237
- async function handleCount(command, browser) {
1238
- const locator = browser.getLocator(command.selector);
1239
- const count = await locator.count();
1240
- return successResponse(command.id, { count });
1241
- }
1242
- async function handleBoundingBox(command, browser) {
1243
- const locator = browser.getLocator(command.selector);
1244
- const box = await locator.boundingBox();
1245
- return successResponse(command.id, { box });
1246
- }
1247
- async function handleStyles(command, browser) {
1248
- const page = browser.getPage();
1249
- // Shared extraction logic as a string to be eval'd in browser context
1250
- const extractStylesScript = `(function(el) {
1251
- const s = getComputedStyle(el);
1252
- const r = el.getBoundingClientRect();
1253
- return {
1254
- tag: el.tagName.toLowerCase(),
1255
- text: el.innerText?.trim().slice(0, 80) || null,
1256
- box: {
1257
- x: Math.round(r.x),
1258
- y: Math.round(r.y),
1259
- width: Math.round(r.width),
1260
- height: Math.round(r.height),
1261
- },
1262
- styles: {
1263
- fontSize: s.fontSize,
1264
- fontWeight: s.fontWeight,
1265
- fontFamily: s.fontFamily.split(',')[0].trim().replace(/"/g, ''),
1266
- color: s.color,
1267
- backgroundColor: s.backgroundColor,
1268
- borderRadius: s.borderRadius,
1269
- border: s.border !== 'none' && s.borderWidth !== '0px' ? s.border : null,
1270
- boxShadow: s.boxShadow !== 'none' ? s.boxShadow : null,
1271
- padding: s.padding,
1272
- },
1273
- };
1274
- })`;
1275
- // Check if it's a ref - single element
1276
- if (browser.isRef(command.selector)) {
1277
- const locator = browser.getLocator(command.selector);
1278
- const element = (await locator.evaluate((el, script) => {
1279
- const fn = eval(script);
1280
- return fn(el);
1281
- }, extractStylesScript));
1282
- return successResponse(command.id, { elements: [element] });
1283
- }
1284
- // CSS selector - can match multiple elements
1285
- const elements = (await page.$$eval(command.selector, (els, script) => {
1286
- const fn = eval(script);
1287
- return els.map((el) => fn(el));
1288
- }, extractStylesScript));
1289
- return successResponse(command.id, { elements });
1290
- }
1291
- // Advanced handlers
1292
- async function handleVideoStart(command, browser) {
1293
- // Video recording requires context-level setup at launch
1294
- // For now, return a note about this limitation
1295
- return successResponse(command.id, {
1296
- note: 'Video recording must be enabled at browser launch. Use --video flag when starting.',
1297
- path: command.path,
1298
- });
1299
- }
1300
- async function handleVideoStop(command, browser) {
1301
- const page = browser.getPage();
1302
- const video = page.video();
1303
- if (video) {
1304
- const path = await video.path();
1305
- return successResponse(command.id, { path });
1306
- }
1307
- return successResponse(command.id, { note: 'No video recording active' });
1308
- }
1309
- async function handleTraceStart(command, browser) {
1310
- await browser.startTracing({
1311
- screenshots: command.screenshots,
1312
- snapshots: command.snapshots,
1313
- });
1314
- return successResponse(command.id, { started: true });
1315
- }
1316
- async function handleTraceStop(command, browser) {
1317
- await browser.stopTracing(command.path);
1318
- return successResponse(command.id, command.path ? { path: command.path } : { traceStopped: true });
1319
- }
1320
- async function handleProfilerStart(command, browser) {
1321
- await browser.startProfiling({ categories: command.categories });
1322
- return successResponse(command.id, { started: true });
1323
- }
1324
- async function handleProfilerStop(command, browser) {
1325
- let outputPath = command.path;
1326
- if (!outputPath) {
1327
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1328
- const random = Math.random().toString(36).substring(2, 8);
1329
- const filename = `profile-${timestamp}-${random}.json`;
1330
- const profileDir = path.join(getAppDir(), 'tmp', 'profiles');
1331
- mkdirSync(profileDir, { recursive: true });
1332
- outputPath = path.join(profileDir, filename);
1333
- }
1334
- const result = await browser.stopProfiling(outputPath);
1335
- return successResponse(command.id, result);
1336
- }
1337
- async function handleHarStart(command, browser) {
1338
- await browser.startHarRecording();
1339
- browser.startRequestTracking();
1340
- return successResponse(command.id, { started: true });
1341
- }
1342
- async function handleHarStop(command, browser) {
1343
- // HAR recording is handled at context level
1344
- // For now, we save tracked requests as a simplified HAR-like format
1345
- const requests = browser.getRequests();
1346
- return successResponse(command.id, {
1347
- path: command.path,
1348
- requestCount: requests.length,
1349
- });
1350
- }
1351
- async function handleStateSave(command, browser) {
1352
- await browser.saveStorageState(command.path);
1353
- return successResponse(command.id, { path: command.path });
1354
- }
1355
- async function handleStateLoad(command, browser) {
1356
- if (browser.isLaunched()) {
1357
- return errorResponse(command.id, 'Cannot load state while browser is running. Close browser first, then relaunch with loaded state.');
1358
- }
1359
- if (!fs.existsSync(command.path)) {
1360
- return errorResponse(command.id, `State file not found: ${command.path}`);
1361
- }
1362
- await browser.launch({
1363
- headless: true,
1364
- autoStateFilePath: command.path,
1365
- });
1366
- return successResponse(command.id, {
1367
- loaded: true,
1368
- path: command.path,
1369
- });
1370
- }
1371
- async function handleStateList(command) {
1372
- const sessionsDir = getSessionsDir();
1373
- const files = listStateFiles();
1374
- if (files.length === 0) {
1375
- return successResponse(command.id, { files: [], directory: sessionsDir });
1376
- }
1377
- const stateFiles = files
1378
- .map((filename) => {
1379
- const filepath = path.join(sessionsDir, filename);
1380
- const stats = fs.statSync(filepath);
1381
- let encrypted = false;
1382
- try {
1383
- const content = fs.readFileSync(filepath, 'utf-8');
1384
- const parsed = JSON.parse(content);
1385
- encrypted = isEncryptedPayload(parsed);
1386
- }
1387
- catch {
1388
- // Ignore parse errors
1389
- }
1390
- return {
1391
- filename,
1392
- path: filepath,
1393
- size: stats.size,
1394
- modified: stats.mtime.toISOString(),
1395
- encrypted,
1396
- };
1397
- })
1398
- .sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
1399
- return successResponse(command.id, { files: stateFiles, directory: sessionsDir });
1400
- }
1401
- async function handleStateClear(command) {
1402
- const sessionsDir = getSessionsDir();
1403
- if (command.sessionName && !isValidSessionName(command.sessionName)) {
1404
- return errorResponse(command.id, 'Invalid session name. Use only letters, numbers, dashes, and underscores.');
1405
- }
1406
- const files = listStateFiles();
1407
- if (files.length === 0) {
1408
- return successResponse(command.id, { cleared: 0, deleted: [] });
1409
- }
1410
- const deleted = [];
1411
- if (command.all) {
1412
- for (const file of files) {
1413
- fs.unlinkSync(path.join(sessionsDir, file));
1414
- deleted.push(file);
1415
- }
1416
- }
1417
- else if (command.sessionName) {
1418
- for (const file of files) {
1419
- if (file.startsWith(`${command.sessionName}-`)) {
1420
- fs.unlinkSync(path.join(sessionsDir, file));
1421
- deleted.push(file);
1422
- }
1423
- }
1424
- }
1425
- return successResponse(command.id, { cleared: deleted.length, deleted });
1426
- }
1427
- async function handleStateShow(command) {
1428
- const sessionsDir = getSessionsDir();
1429
- const baseName = command.filename.replace(/\.json$/, '');
1430
- if (!command.filename.endsWith('.json') || !isValidSessionName(baseName)) {
1431
- return errorResponse(command.id, 'Invalid filename. Use only letters, numbers, dashes, and underscores (with .json extension).');
1432
- }
1433
- const filepath = path.join(sessionsDir, command.filename);
1434
- if (!fs.existsSync(filepath)) {
1435
- return errorResponse(command.id, `State file not found: ${command.filename}`);
1436
- }
1437
- try {
1438
- const { data: state, wasEncrypted } = readStateFile(filepath);
1439
- const stats = fs.statSync(filepath);
1440
- const stateObj = state;
1441
- const cookies = stateObj.cookies?.length || 0;
1442
- const origins = stateObj.origins?.length || 0;
1443
- const domains = [...new Set((stateObj.cookies || []).map((c) => c.domain))];
1444
- return successResponse(command.id, {
1445
- filename: command.filename,
1446
- path: filepath,
1447
- size: stats.size,
1448
- modified: stats.mtime.toISOString(),
1449
- encrypted: wasEncrypted,
1450
- summary: {
1451
- cookies,
1452
- origins,
1453
- domains,
1454
- },
1455
- state,
1456
- });
1457
- }
1458
- catch (e) {
1459
- return errorResponse(command.id, `Failed to parse state file: ${e.message}`);
1460
- }
1461
- }
1462
- async function handleStateClean(command) {
1463
- const deleted = cleanupExpiredStates(command.days);
1464
- const keptCount = listStateFiles().length;
1465
- return successResponse(command.id, {
1466
- cleaned: deleted.length,
1467
- deleted,
1468
- keptCount,
1469
- days: command.days,
1470
- });
1471
- }
1472
- async function handleStateRename(command) {
1473
- const sessionsDir = getSessionsDir();
1474
- if (!isValidSessionName(command.oldName) || !isValidSessionName(command.newName)) {
1475
- return errorResponse(command.id, 'Invalid name. Use only letters, numbers, dashes, and underscores.');
1476
- }
1477
- const oldPath = path.join(sessionsDir, `${command.oldName}.json`);
1478
- const newPath = path.join(sessionsDir, `${command.newName}.json`);
1479
- if (!fs.existsSync(oldPath)) {
1480
- return errorResponse(command.id, `State file not found: ${command.oldName}.json`);
1481
- }
1482
- if (fs.existsSync(newPath)) {
1483
- return errorResponse(command.id, `Destination already exists: ${command.newName}.json`);
1484
- }
1485
- fs.renameSync(oldPath, newPath);
1486
- return successResponse(command.id, {
1487
- renamed: true,
1488
- oldName: `${command.oldName}.json`,
1489
- newName: `${command.newName}.json`,
1490
- path: newPath,
1491
- });
1492
- }
1493
- async function handleConsole(command, browser) {
1494
- if (command.clear) {
1495
- browser.clearConsoleMessages();
1496
- return successResponse(command.id, { cleared: true });
1497
- }
1498
- const page = browser.getPage();
1499
- const messages = browser.getConsoleMessages();
1500
- return successResponse(command.id, { messages, origin: page.url() });
1501
- }
1502
- async function handleErrors(command, browser) {
1503
- if (command.clear) {
1504
- browser.clearPageErrors();
1505
- return successResponse(command.id, { cleared: true });
1506
- }
1507
- const errors = browser.getPageErrors();
1508
- return successResponse(command.id, { errors });
1509
- }
1510
- async function handleKeyboard(command, browser) {
1511
- const page = browser.getPage();
1512
- const sub = command.subaction ?? 'press';
1513
- switch (sub) {
1514
- case 'type':
1515
- await page.keyboard.type(command.text ?? '', { delay: command.delay });
1516
- return successResponse(command.id, { typed: true, text: command.text });
1517
- case 'press':
1518
- await page.keyboard.press(command.keys ?? '');
1519
- return successResponse(command.id, { pressed: command.keys });
1520
- case 'insertText':
1521
- await page.keyboard.insertText(command.text ?? '');
1522
- return successResponse(command.id, { inserted: true, text: command.text });
1523
- default:
1524
- return errorResponse(command.id, `Unknown keyboard subaction: ${sub}`);
1525
- }
1526
- }
1527
- async function handleWheel(command, browser) {
1528
- const page = browser.getPage();
1529
- if (command.selector) {
1530
- const element = browser.getLocator(command.selector);
1531
- await element.hover();
1532
- }
1533
- await page.mouse.wheel(command.deltaX ?? 0, command.deltaY ?? 0);
1534
- return successResponse(command.id, { scrolled: true });
1535
- }
1536
- async function handleTap(command, browser) {
1537
- const page = browser.getPage();
1538
- await page.tap(command.selector);
1539
- return successResponse(command.id, { tapped: true });
1540
- }
1541
- async function handleClipboard(command, browser) {
1542
- const page = browser.getPage();
1543
- switch (command.operation) {
1544
- case 'copy':
1545
- await page.keyboard.press('ControlOrMeta+c');
1546
- return successResponse(command.id, { copied: true });
1547
- case 'paste':
1548
- await page.keyboard.press('ControlOrMeta+v');
1549
- return successResponse(command.id, { pasted: true });
1550
- case 'read': {
1551
- const text = await page.evaluate('navigator.clipboard.readText()');
1552
- return successResponse(command.id, { text });
1553
- }
1554
- case 'write': {
1555
- if (!command.text) {
1556
- return errorResponse(command.id, "Missing 'text' parameter for clipboard write");
1557
- }
1558
- await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(command.text)})`);
1559
- return successResponse(command.id, { written: command.text });
1560
- }
1561
- default:
1562
- return errorResponse(command.id, 'Unknown clipboard operation');
1563
- }
1564
- }
1565
- async function handleHighlight(command, browser) {
1566
- const locator = browser.getLocator(command.selector);
1567
- await locator.highlight();
1568
- return successResponse(command.id, { highlighted: true });
1569
- }
1570
- async function handleClear(command, browser) {
1571
- const locator = browser.getLocator(command.selector);
1572
- await locator.clear();
1573
- return successResponse(command.id, { cleared: true });
1574
- }
1575
- async function handleSelectAll(command, browser) {
1576
- const locator = browser.getLocator(command.selector);
1577
- await locator.selectText();
1578
- return successResponse(command.id, { selected: true });
1579
- }
1580
- async function handleInnerText(command, browser) {
1581
- const locator = browser.getLocator(command.selector);
1582
- const text = await locator.innerText();
1583
- return successResponse(command.id, { text });
1584
- }
1585
- async function handleInnerHtml(command, browser) {
1586
- const page = browser.getPage();
1587
- const locator = browser.getLocator(command.selector);
1588
- const html = await locator.innerHTML();
1589
- return successResponse(command.id, { html, origin: page.url() });
1590
- }
1591
- async function handleInputValue(command, browser) {
1592
- const page = browser.getPage();
1593
- const locator = browser.getLocator(command.selector);
1594
- const value = await locator.inputValue();
1595
- return successResponse(command.id, { value, origin: page.url() });
1596
- }
1597
- async function handleSetValue(command, browser) {
1598
- const locator = browser.getLocator(command.selector);
1599
- await locator.fill(command.value);
1600
- return successResponse(command.id, { set: true });
1601
- }
1602
- async function handleDispatch(command, browser) {
1603
- const locator = browser.getLocator(command.selector);
1604
- await locator.dispatchEvent(command.event, command.eventInit);
1605
- return successResponse(command.id, { dispatched: command.event });
1606
- }
1607
- async function handleEvalHandle(command, browser) {
1608
- const page = browser.getPage();
1609
- const handle = await page.evaluateHandle(command.script);
1610
- const result = await handle.jsonValue().catch(() => 'Handle (non-serializable)');
1611
- return successResponse(command.id, { result });
1612
- }
1613
- async function handleExpose(command, browser) {
1614
- const page = browser.getPage();
1615
- await page.exposeFunction(command.name, () => {
1616
- // Exposed function - can be extended
1617
- return `Function ${command.name} called`;
1618
- });
1619
- return successResponse(command.id, { exposed: command.name });
1620
- }
1621
- async function handleAddScript(command, browser) {
1622
- const page = browser.getPage();
1623
- if (command.content) {
1624
- await page.addScriptTag({ content: command.content });
1625
- }
1626
- else if (command.url) {
1627
- await page.addScriptTag({ url: command.url });
1628
- }
1629
- return successResponse(command.id, { added: true });
1630
- }
1631
- async function handleAddStyle(command, browser) {
1632
- const page = browser.getPage();
1633
- if (command.content) {
1634
- await page.addStyleTag({ content: command.content });
1635
- }
1636
- else if (command.url) {
1637
- await page.addStyleTag({ url: command.url });
1638
- }
1639
- return successResponse(command.id, { added: true });
1640
- }
1641
- async function handleEmulateMedia(command, browser) {
1642
- const page = browser.getPage();
1643
- await page.emulateMedia({
1644
- media: command.media,
1645
- colorScheme: command.colorScheme,
1646
- reducedMotion: command.reducedMotion,
1647
- forcedColors: command.forcedColors,
1648
- });
1649
- if (command.colorScheme) {
1650
- browser.setColorScheme(command.colorScheme);
1651
- }
1652
- return successResponse(command.id, { emulated: true });
1653
- }
1654
- async function handleOffline(command, browser) {
1655
- await browser.setOffline(command.offline);
1656
- return successResponse(command.id, { offline: command.offline });
1657
- }
1658
- async function handleHeaders(command, browser) {
1659
- await browser.setExtraHeaders(command.headers);
1660
- return successResponse(command.id, { set: true });
1661
- }
1662
- async function handlePause(command, browser) {
1663
- const page = browser.getPage();
1664
- await page.pause();
1665
- return successResponse(command.id, { paused: true });
1666
- }
1667
- async function handleGetByAltText(command, browser) {
1668
- const page = browser.getPage();
1669
- const locator = page.getByAltText(command.text, { exact: command.exact });
1670
- switch (command.subaction) {
1671
- case 'click':
1672
- await locator.click();
1673
- return successResponse(command.id, { clicked: true });
1674
- case 'hover':
1675
- await locator.hover();
1676
- return successResponse(command.id, { hovered: true });
1677
- }
1678
- }
1679
- async function handleGetByTitle(command, browser) {
1680
- const page = browser.getPage();
1681
- const locator = page.getByTitle(command.text, { exact: command.exact });
1682
- switch (command.subaction) {
1683
- case 'click':
1684
- await locator.click();
1685
- return successResponse(command.id, { clicked: true });
1686
- case 'hover':
1687
- await locator.hover();
1688
- return successResponse(command.id, { hovered: true });
1689
- }
1690
- }
1691
- async function handleGetByTestId(command, browser) {
1692
- const page = browser.getPage();
1693
- const locator = page.getByTestId(command.testId);
1694
- switch (command.subaction) {
1695
- case 'click':
1696
- await locator.click();
1697
- return successResponse(command.id, { clicked: true });
1698
- case 'fill':
1699
- await locator.fill(command.value ?? '');
1700
- return successResponse(command.id, { filled: true });
1701
- case 'check':
1702
- await locator.check();
1703
- return successResponse(command.id, { checked: true });
1704
- case 'hover':
1705
- await locator.hover();
1706
- return successResponse(command.id, { hovered: true });
1707
- }
1708
- }
1709
- async function handleNth(command, browser) {
1710
- const base = browser.getLocator(command.selector);
1711
- const locator = command.index === -1 ? base.last() : base.nth(command.index);
1712
- switch (command.subaction) {
1713
- case 'click':
1714
- await locator.click();
1715
- return successResponse(command.id, { clicked: true });
1716
- case 'fill':
1717
- await locator.fill(command.value ?? '');
1718
- return successResponse(command.id, { filled: true });
1719
- case 'check':
1720
- await locator.check();
1721
- return successResponse(command.id, { checked: true });
1722
- case 'hover':
1723
- await locator.hover();
1724
- return successResponse(command.id, { hovered: true });
1725
- case 'text':
1726
- const text = await locator.textContent();
1727
- return successResponse(command.id, { text });
1728
- }
1729
- }
1730
- async function handleWaitForUrl(command, browser) {
1731
- const page = browser.getPage();
1732
- await page.waitForURL(command.url, { timeout: command.timeout });
1733
- return successResponse(command.id, { url: page.url() });
1734
- }
1735
- async function handleWaitForLoadState(command, browser) {
1736
- const page = browser.getPage();
1737
- await page.waitForLoadState(command.state, { timeout: command.timeout });
1738
- return successResponse(command.id, { state: command.state });
1739
- }
1740
- async function handleSetContent(command, browser) {
1741
- const page = browser.getPage();
1742
- await page.setContent(command.html);
1743
- return successResponse(command.id, { set: true });
1744
- }
1745
- async function handleTimezone(command, browser) {
1746
- // Timezone must be set at context level before navigation
1747
- // This is a limitation - it sets for the current context
1748
- const page = browser.getPage();
1749
- await page.context().setGeolocation({ latitude: 0, longitude: 0 }); // Trigger context awareness
1750
- return successResponse(command.id, {
1751
- note: 'Timezone must be set at browser launch. Use --timezone flag.',
1752
- timezone: command.timezone,
1753
- });
1754
- }
1755
- async function handleLocale(command, browser) {
1756
- // Locale must be set at context creation
1757
- return successResponse(command.id, {
1758
- note: 'Locale must be set at browser launch. Use --locale flag.',
1759
- locale: command.locale,
1760
- });
1761
- }
1762
- async function handleCredentials(command, browser) {
1763
- const context = browser.getPage().context();
1764
- await context.setHTTPCredentials({
1765
- username: command.username,
1766
- password: command.password,
1767
- });
1768
- return successResponse(command.id, { set: true });
1769
- }
1770
- async function handleMouseMove(command, browser) {
1771
- const page = browser.getPage();
1772
- await page.mouse.move(command.x, command.y);
1773
- return successResponse(command.id, { moved: true, x: command.x, y: command.y });
1774
- }
1775
- async function handleMouseDown(command, browser) {
1776
- const page = browser.getPage();
1777
- await page.mouse.down({ button: command.button ?? 'left' });
1778
- return successResponse(command.id, { down: true });
1779
- }
1780
- async function handleMouseUp(command, browser) {
1781
- const page = browser.getPage();
1782
- await page.mouse.up({ button: command.button ?? 'left' });
1783
- return successResponse(command.id, { up: true });
1784
- }
1785
- async function handleBringToFront(command, browser) {
1786
- const page = browser.getPage();
1787
- await page.bringToFront();
1788
- return successResponse(command.id, { focused: true });
1789
- }
1790
- async function handleWaitForFunction(command, browser) {
1791
- const page = browser.getPage();
1792
- await page.waitForFunction(command.expression, { timeout: command.timeout });
1793
- return successResponse(command.id, { waited: true });
1794
- }
1795
- async function handleScrollIntoView(command, browser) {
1796
- await browser.getLocator(command.selector).scrollIntoViewIfNeeded();
1797
- return successResponse(command.id, { scrolled: true });
1798
- }
1799
- async function handleAddInitScript(command, browser) {
1800
- const context = browser.getPage().context();
1801
- await context.addInitScript(command.script);
1802
- return successResponse(command.id, { added: true });
1803
- }
1804
- async function handleKeyDown(command, browser) {
1805
- const page = browser.getPage();
1806
- await page.keyboard.down(command.key);
1807
- return successResponse(command.id, { down: true, key: command.key });
1808
- }
1809
- async function handleKeyUp(command, browser) {
1810
- const page = browser.getPage();
1811
- await page.keyboard.up(command.key);
1812
- return successResponse(command.id, { up: true, key: command.key });
1813
- }
1814
- async function handleInsertText(command, browser) {
1815
- const page = browser.getPage();
1816
- await page.keyboard.insertText(command.text);
1817
- return successResponse(command.id, { inserted: true });
1818
- }
1819
- async function handleMultiSelect(command, browser) {
1820
- const locator = browser.getLocator(command.selector);
1821
- const selected = await locator.selectOption(command.values);
1822
- return successResponse(command.id, { selected });
1823
- }
1824
- async function handleWaitForDownload(command, browser) {
1825
- const page = browser.getPage();
1826
- const download = await page.waitForEvent('download', { timeout: command.timeout });
1827
- let filePath;
1828
- if (command.path) {
1829
- filePath = command.path;
1830
- await download.saveAs(filePath);
1831
- }
1832
- else {
1833
- filePath = (await download.path()) || download.suggestedFilename();
1834
- }
1835
- return successResponse(command.id, {
1836
- path: filePath,
1837
- filename: download.suggestedFilename(),
1838
- url: download.url(),
1839
- });
1840
- }
1841
- async function handleResponseBody(command, browser) {
1842
- const page = browser.getPage();
1843
- const response = await page.waitForResponse((resp) => resp.url().includes(command.url), {
1844
- timeout: command.timeout,
1845
- });
1846
- const body = await response.text();
1847
- let parsed = body;
1848
- try {
1849
- parsed = JSON.parse(body);
1850
- }
1851
- catch {
1852
- // Keep as string if not JSON
1853
- }
1854
- return successResponse(command.id, {
1855
- url: response.url(),
1856
- status: response.status(),
1857
- body: parsed,
1858
- });
1859
- }
1860
- // Screencast and input injection handlers
1861
- async function handleScreencastStart(command, browser) {
1862
- if (!screencastFrameCallback) {
1863
- throw new Error('Screencast frame callback not set. Start the streaming server first.');
1864
- }
1865
- await browser.startScreencast(screencastFrameCallback, {
1866
- format: command.format,
1867
- quality: command.quality,
1868
- maxWidth: command.maxWidth,
1869
- maxHeight: command.maxHeight,
1870
- everyNthFrame: command.everyNthFrame,
1871
- });
1872
- return successResponse(command.id, {
1873
- started: true,
1874
- format: command.format ?? 'jpeg',
1875
- quality: command.quality ?? 80,
1876
- });
1877
- }
1878
- async function handleScreencastStop(command, browser) {
1879
- await browser.stopScreencast();
1880
- return successResponse(command.id, { stopped: true });
1881
- }
1882
- async function handleInputMouse(command, browser) {
1883
- await browser.injectMouseEvent({
1884
- type: command.type,
1885
- x: command.x,
1886
- y: command.y,
1887
- button: command.button,
1888
- clickCount: command.clickCount,
1889
- deltaX: command.deltaX,
1890
- deltaY: command.deltaY,
1891
- modifiers: command.modifiers,
1892
- });
1893
- return successResponse(command.id, { injected: true });
1894
- }
1895
- async function handleInputKeyboard(command, browser) {
1896
- await browser.injectKeyboardEvent({
1897
- type: command.type,
1898
- key: command.key,
1899
- code: command.code,
1900
- text: command.text,
1901
- modifiers: command.modifiers,
1902
- });
1903
- return successResponse(command.id, { injected: true });
1904
- }
1905
- async function handleInputTouch(command, browser) {
1906
- await browser.injectTouchEvent({
1907
- type: command.type,
1908
- touchPoints: command.touchPoints,
1909
- modifiers: command.modifiers,
1910
- });
1911
- return successResponse(command.id, { injected: true });
1912
- }
1913
- // Recording handlers (Playwright native video recording)
1914
- async function handleRecordingStart(command, browser) {
1915
- await browser.startRecording(command.path, command.url);
1916
- return successResponse(command.id, {
1917
- started: true,
1918
- path: command.path,
1919
- });
1920
- }
1921
- async function handleRecordingStop(command, browser) {
1922
- const result = await browser.stopRecording();
1923
- return successResponse(command.id, result);
1924
- }
1925
- async function handleRecordingRestart(command, browser) {
1926
- const result = await browser.restartRecording(command.path, command.url);
1927
- return successResponse(command.id, {
1928
- started: true,
1929
- path: command.path,
1930
- previousPath: result.previousPath,
1931
- stopped: result.stopped,
1932
- });
1933
- }
1934
- // Diff handlers
1935
- async function handleDiffSnapshot(command, browser) {
1936
- let before;
1937
- if (command.baseline) {
1938
- try {
1939
- before = fs.readFileSync(command.baseline, 'utf-8');
1940
- }
1941
- catch {
1942
- return errorResponse(command.id, `Cannot read baseline file: ${command.baseline}`);
1943
- }
1944
- }
1945
- else {
1946
- before = browser.getLastSnapshot();
1947
- if (!before) {
1948
- return errorResponse(command.id, 'No previous snapshot in this session. Take a snapshot first, or use --baseline <file>.');
1949
- }
1950
- }
1951
- const page = browser.getPage();
1952
- const { tree } = await getEnhancedSnapshot(page, {
1953
- selector: command.selector,
1954
- compact: command.compact,
1955
- maxDepth: command.maxDepth,
1956
- });
1957
- const after = tree || 'Empty page';
1958
- const result = diffSnapshots(before, after);
1959
- browser.setLastSnapshot(after);
1960
- return successResponse(command.id, result);
1961
- }
1962
- async function handleDiffScreenshot(command, browser) {
1963
- if (!fs.existsSync(command.baseline)) {
1964
- return errorResponse(command.id, `Baseline file not found: ${command.baseline}`);
1965
- }
1966
- const page = browser.getPage();
1967
- let screenshotBuffer;
1968
- if (command.selector) {
1969
- const locator = browser.getLocator(command.selector);
1970
- screenshotBuffer = await locator.screenshot({ type: 'png' });
1971
- }
1972
- else {
1973
- screenshotBuffer = await page.screenshot({ fullPage: command.fullPage, type: 'png' });
1974
- }
1975
- const baselineBuffer = fs.readFileSync(command.baseline);
1976
- const ext = path.extname(command.baseline).toLowerCase();
1977
- const baselineMime = ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' : 'image/png';
1978
- const result = await diffScreenshots(page.context(), baselineBuffer, screenshotBuffer, {
1979
- threshold: command.threshold,
1980
- outputPath: command.output,
1981
- baselineMime,
1982
- });
1983
- return successResponse(command.id, result);
1984
- }
1985
- async function handleDiffUrl(command, browser) {
1986
- const page = browser.getPage();
1987
- const waitUntil = command.waitUntil ?? 'load';
1988
- const snapshotOpts = {
1989
- selector: command.selector,
1990
- compact: command.compact,
1991
- maxDepth: command.maxDepth,
1992
- };
1993
- // Capture state of url1
1994
- await page.goto(command.url1, { waitUntil });
1995
- const { tree: tree1 } = await getEnhancedSnapshot(page, snapshotOpts);
1996
- const snapshot1 = tree1 || 'Empty page';
1997
- let screenshot1;
1998
- if (command.screenshot) {
1999
- screenshot1 = await page.screenshot({ fullPage: command.fullPage, type: 'png' });
2000
- }
2001
- // Capture state of url2
2002
- await page.goto(command.url2, { waitUntil });
2003
- const { tree: tree2 } = await getEnhancedSnapshot(page, snapshotOpts);
2004
- const snapshot2 = tree2 || 'Empty page';
2005
- const snapshotDiff = diffSnapshots(snapshot1, snapshot2);
2006
- const result = { snapshot: snapshotDiff };
2007
- if (command.screenshot && screenshot1) {
2008
- const screenshot2 = await page.screenshot({ fullPage: command.fullPage, type: 'png' });
2009
- result.screenshot = await diffScreenshots(page.context(), screenshot1, screenshot2, {});
2010
- }
2011
- return successResponse(command.id, result);
2012
- }
2013
- async function handleAuthLogin(command, browser) {
2014
- const profile = getAuthProfile(command.name);
2015
- if (!profile) {
2016
- return errorResponse(command.id, `Auth profile '${command.name}' not found`);
2017
- }
2018
- browser.checkDomainAllowed(profile.url);
2019
- const page = browser.getPage();
2020
- await page.goto(profile.url, { waitUntil: 'load' });
2021
- const usingAutoDetect = !profile.usernameSelector && !profile.passwordSelector && !profile.submitSelector;
2022
- if (usingAutoDetect) {
2023
- console.error(`[agent-browser] Auth login '${command.name}': using auto-detected form selectors. ` +
2024
- `If login fails, specify --username-selector/--password-selector/--submit-selector with auth save.`);
2025
- }
2026
- const passSel = profile.passwordSelector || 'input[type="password"]:visible';
2027
- // Auto-detect selectors ordered from most specific to broadest.
2028
- // Locale-dependent text matchers (e.g. "Sign in") are intentionally
2029
- // excluded -- they break on non-English pages.
2030
- const AUTO_USER_SELECTORS = [
2031
- 'input[autocomplete="username"]:visible',
2032
- 'input[type="email"]:visible',
2033
- 'input[name="username"]:visible',
2034
- 'input[name="email"]:visible',
2035
- ];
2036
- const AUTO_SUBMIT_SELECTORS = ['button[type="submit"]:visible', 'input[type="submit"]:visible'];
2037
- try {
2038
- // Resolve username field: custom selector or sequential auto-detect
2039
- let userLocator;
2040
- if (profile.usernameSelector) {
2041
- userLocator = page.locator(profile.usernameSelector).first();
2042
- }
2043
- else {
2044
- userLocator = null;
2045
- for (const sel of AUTO_USER_SELECTORS) {
2046
- const loc = page.locator(sel).first();
2047
- if (await loc.isVisible({ timeout: 1000 }).catch(() => false)) {
2048
- userLocator = loc;
2049
- break;
2050
- }
2051
- }
2052
- if (!userLocator) {
2053
- return errorResponse(command.id, `Auth login failed for '${command.name}': could not find username field. ` +
2054
- `Specify --username-selector with auth save.`);
2055
- }
2056
- }
2057
- // Resolve submit button: custom selector or sequential auto-detect
2058
- let submitLocator;
2059
- if (profile.submitSelector) {
2060
- submitLocator = page.locator(profile.submitSelector).first();
2061
- }
2062
- else {
2063
- submitLocator = null;
2064
- for (const sel of AUTO_SUBMIT_SELECTORS) {
2065
- const loc = page.locator(sel).first();
2066
- if (await loc.isVisible({ timeout: 1000 }).catch(() => false)) {
2067
- submitLocator = loc;
2068
- break;
2069
- }
2070
- }
2071
- if (!submitLocator) {
2072
- return errorResponse(command.id, `Auth login failed for '${command.name}': could not find submit button. ` +
2073
- `Specify --submit-selector with auth save.`);
2074
- }
2075
- }
2076
- await userLocator.fill(profile.username);
2077
- await page.locator(passSel).first().fill(profile.password);
2078
- await submitLocator.click();
2079
- await page.waitForLoadState('load');
2080
- }
2081
- catch (err) {
2082
- return errorResponse(command.id, `Auth login failed for '${command.name}': ${err instanceof Error ? err.message : err}. ` +
2083
- `Try specifying custom selectors with auth save --username-selector/--password-selector/--submit-selector`);
2084
- }
2085
- updateLastLogin(command.name);
2086
- return successResponse(command.id, {
2087
- loggedIn: true,
2088
- name: command.name,
2089
- url: page.url(),
2090
- title: await page.title(),
2091
- });
2092
- }
2093
- async function handleConfirm(command, browser) {
2094
- const entry = getAndRemovePending(command.confirmationId);
2095
- if (!entry) {
2096
- return errorResponse(command.id, `No pending confirmation with id '${command.confirmationId}'`);
2097
- }
2098
- // Re-validate the stored command through the schema to guard against
2099
- // shape drift between when the confirmation was issued and now.
2100
- const parseResult = parseCommand(JSON.stringify(entry.command));
2101
- if (!parseResult.success) {
2102
- return errorResponse(command.id, `Stored command is no longer valid: ${parseResult.error}`);
2103
- }
2104
- const originalCommand = parseResult.command;
2105
- // Re-check deny list in case policy was updated since the confirmation was issued
2106
- actionPolicy = reloadPolicyIfChanged();
2107
- const decision = checkPolicy(originalCommand.action, actionPolicy, new Set());
2108
- if (decision === 'deny') {
2109
- const category = getActionCategory(originalCommand.action);
2110
- return errorResponse(command.id, `Action denied by policy: '${category}' is not allowed`);
2111
- }
2112
- return await dispatchAction(originalCommand, browser);
2113
- }
2114
- function handleDeny(command) {
2115
- const entry = getAndRemovePending(command.confirmationId);
2116
- if (!entry) {
2117
- return errorResponse(command.id, `No pending confirmation with id '${command.confirmationId}'`);
2118
- }
2119
- return successResponse(command.id, { denied: true });
2120
- }
2121
- //# sourceMappingURL=actions.js.map