@web-auto/camo 0.1.26 → 0.2.1

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 (117) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +586 -586
  3. package/bin/browser-service.mjs +11 -11
  4. package/bin/camo.mjs +22 -22
  5. package/package.json +48 -48
  6. package/scripts/build.mjs +19 -19
  7. package/scripts/bump-version.mjs +34 -34
  8. package/scripts/check-file-size.mjs +80 -80
  9. package/scripts/file-size-policy.json +12 -2
  10. package/scripts/install.mjs +76 -76
  11. package/scripts/release.sh +54 -54
  12. package/src/autoscript/action-providers/index.mjs +6 -6
  13. package/src/autoscript/impact-engine.mjs +78 -78
  14. package/src/autoscript/runtime.mjs +1017 -1017
  15. package/src/autoscript/schema.mjs +376 -376
  16. package/src/cli.mjs +405 -405
  17. package/src/commands/attach.mjs +141 -141
  18. package/src/commands/autoscript.mjs +1011 -1011
  19. package/src/commands/browser.mjs +1255 -1257
  20. package/src/commands/container.mjs +401 -401
  21. package/src/commands/cookies.mjs +69 -69
  22. package/src/commands/create.mjs +98 -98
  23. package/src/commands/devtools.mjs +349 -349
  24. package/src/commands/events.mjs +152 -152
  25. package/src/commands/highlight-mode.mjs +24 -24
  26. package/src/commands/init.mjs +68 -68
  27. package/src/commands/lifecycle.mjs +275 -275
  28. package/src/commands/mouse.mjs +45 -45
  29. package/src/commands/profile.mjs +46 -46
  30. package/src/commands/record.mjs +115 -115
  31. package/src/commands/system.mjs +14 -14
  32. package/src/commands/window.mjs +123 -123
  33. package/src/container/change-notifier.mjs +362 -362
  34. package/src/container/element-filter.mjs +143 -143
  35. package/src/container/index.mjs +3 -3
  36. package/src/container/runtime-core/checkpoint.mjs +209 -209
  37. package/src/container/runtime-core/index.mjs +21 -21
  38. package/src/container/runtime-core/operations/index.mjs +774 -774
  39. package/src/container/runtime-core/operations/selector-scripts.mjs +277 -277
  40. package/src/container/runtime-core/operations/tab-pool.mjs +746 -746
  41. package/src/container/runtime-core/operations/viewport.mjs +189 -189
  42. package/src/container/runtime-core/search.mjs +190 -190
  43. package/src/container/runtime-core/subscription.mjs +224 -224
  44. package/src/container/runtime-core/utils.mjs +94 -94
  45. package/src/container/runtime-core/validation.mjs +127 -184
  46. package/src/container/runtime-core.mjs +1 -1
  47. package/src/container/subscription-registry.mjs +459 -459
  48. package/src/core/actions.mjs +561 -561
  49. package/src/core/browser.mjs +266 -266
  50. package/src/core/index.mjs +52 -52
  51. package/src/core/utils.mjs +91 -91
  52. package/src/events/daemon-entry.mjs +33 -33
  53. package/src/events/daemon.mjs +80 -80
  54. package/src/events/progress-log.mjs +109 -109
  55. package/src/events/ws-server.mjs +239 -239
  56. package/src/lib/client.mjs +200 -200
  57. package/src/lifecycle/cleanup.mjs +83 -83
  58. package/src/lifecycle/lock.mjs +126 -126
  59. package/src/lifecycle/session-registry.mjs +279 -279
  60. package/src/lifecycle/session-view.mjs +76 -76
  61. package/src/lifecycle/session-watchdog.mjs +281 -281
  62. package/src/services/browser-service/index.js +671 -674
  63. package/src/services/browser-service/internal/BrowserSession.input.test.js +389 -389
  64. package/src/services/browser-service/internal/BrowserSession.js +325 -336
  65. package/src/services/browser-service/internal/ElementRegistry.js +60 -60
  66. package/src/services/browser-service/internal/ProfileLock.js +84 -84
  67. package/src/services/browser-service/internal/SessionManager.js +184 -184
  68. package/src/services/browser-service/internal/SessionManager.test.js +39 -39
  69. package/src/services/browser-service/internal/browser-session/cookies.js +144 -144
  70. package/src/services/browser-service/internal/browser-session/input-ops.js +222 -219
  71. package/src/services/browser-service/internal/browser-session/input-pipeline.js +144 -144
  72. package/src/services/browser-service/internal/browser-session/logging.js +46 -46
  73. package/src/services/browser-service/internal/browser-session/navigation.js +38 -38
  74. package/src/services/browser-service/internal/browser-session/page-hooks.js +442 -442
  75. package/src/services/browser-service/internal/browser-session/page-management.js +302 -336
  76. package/src/services/browser-service/internal/browser-session/page-management.test.js +148 -148
  77. package/src/services/browser-service/internal/browser-session/recording.js +198 -198
  78. package/src/services/browser-service/internal/browser-session/runtime-events.js +61 -61
  79. package/src/services/browser-service/internal/browser-session/session-core.js +84 -84
  80. package/src/services/browser-service/internal/browser-session/session-state.js +38 -38
  81. package/src/services/browser-service/internal/browser-session/types.js +14 -14
  82. package/src/services/browser-service/internal/browser-session/utils.js +95 -95
  83. package/src/services/browser-service/internal/browser-session/viewport-manager.js +46 -46
  84. package/src/services/browser-service/internal/browser-session/viewport.js +215 -215
  85. package/src/services/browser-service/internal/container-matcher.js +851 -851
  86. package/src/services/browser-service/internal/container-registry.js +182 -182
  87. package/src/services/browser-service/internal/engine-manager.js +259 -259
  88. package/src/services/browser-service/internal/fingerprint.js +203 -203
  89. package/src/services/browser-service/internal/heartbeat.js +137 -137
  90. package/src/services/browser-service/internal/logging.js +46 -46
  91. package/src/services/browser-service/internal/page-runtime/runtime.js +1317 -1317
  92. package/src/services/browser-service/internal/pageRuntime.js +28 -28
  93. package/src/services/browser-service/internal/runtimeInjector.js +31 -31
  94. package/src/services/browser-service/internal/service-process-logger.js +140 -140
  95. package/src/services/browser-service/internal/state-bus.js +45 -45
  96. package/src/services/browser-service/internal/storage-paths.js +42 -42
  97. package/src/services/browser-service/internal/ws-server.js +1194 -1194
  98. package/src/services/browser-service/internal/ws-server.test.js +58 -58
  99. package/src/services/browser-service/server.mjs +6 -6
  100. package/src/services/controller/cli-bridge.js +93 -93
  101. package/src/services/controller/container-index.js +50 -50
  102. package/src/services/controller/container-storage.js +36 -36
  103. package/src/services/controller/controller-actions.js +207 -207
  104. package/src/services/controller/controller.js +1138 -1138
  105. package/src/services/controller/selectors.js +54 -54
  106. package/src/services/controller/transport.js +125 -125
  107. package/src/utils/args.mjs +26 -26
  108. package/src/utils/browser-service.mjs +544 -544
  109. package/src/utils/command-log.mjs +64 -64
  110. package/src/utils/config.mjs +214 -214
  111. package/src/utils/fingerprint.mjs +181 -181
  112. package/src/utils/help.mjs +216 -216
  113. package/src/utils/js-policy.mjs +13 -13
  114. package/src/utils/ws-client.mjs +30 -30
  115. package/src/container/runtime-core/operations/tab-pool.mjs.bak +0 -762
  116. package/src/container/runtime-core/operations/tab-pool.mjs.syntax-error +0 -762
  117. package/src/services/browser-service/index.js.bak +0 -671
@@ -1,561 +1,561 @@
1
- /**
2
- * Browser actions module - Playwright-based page operations
3
- * No external browser-service dependency
4
- */
5
- import { getActiveBrowser, getCurrentPage, isBrowserRunning } from './browser.mjs';
6
-
7
- /**
8
- * Navigate to URL
9
- */
10
- export async function navigateTo(profileId, url) {
11
- if (!isBrowserRunning(profileId)) {
12
- throw new Error(`No browser running for profile: ${profileId}`);
13
- }
14
-
15
- const page = await getCurrentPage(profileId);
16
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
17
-
18
- return {
19
- ok: true,
20
- profileId,
21
- url: page.url(),
22
- title: await page.title(),
23
- };
24
- }
25
-
26
- /**
27
- * Go back
28
- */
29
- export async function goBack(profileId) {
30
- if (!isBrowserRunning(profileId)) {
31
- throw new Error(`No browser running for profile: ${profileId}`);
32
- }
33
-
34
- const page = await getCurrentPage(profileId);
35
- await page.goBack({ waitUntil: 'domcontentloaded' });
36
-
37
- return {
38
- ok: true,
39
- profileId,
40
- url: page.url(),
41
- };
42
- }
43
-
44
- /**
45
- * Take screenshot
46
- */
47
- export async function takeScreenshot(profileId, options = {}) {
48
- if (!isBrowserRunning(profileId)) {
49
- throw new Error(`No browser running for profile: ${profileId}`);
50
- }
51
-
52
- const page = await getCurrentPage(profileId);
53
- const buffer = await page.screenshot({
54
- fullPage: options.fullPage || false,
55
- type: 'png',
56
- });
57
-
58
- return {
59
- ok: true,
60
- profileId,
61
- data: buffer.toString('base64'),
62
- width: options.fullPage ? undefined : page.viewportSize()?.width,
63
- height: options.fullPage ? undefined : page.viewportSize()?.height,
64
- };
65
- }
66
-
67
- /**
68
- * Scroll page
69
- */
70
- export async function scrollPage(profileId, options = {}) {
71
- if (!isBrowserRunning(profileId)) {
72
- throw new Error(`No browser running for profile: ${profileId}`);
73
- }
74
-
75
- const page = await getCurrentPage(profileId);
76
- const direction = options.direction || 'down';
77
- const amount = options.amount || 300;
78
-
79
- const deltaX = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
80
- const deltaY = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
81
-
82
- await page.mouse.wheel(deltaX, deltaY);
83
-
84
- return {
85
- ok: true,
86
- profileId,
87
- direction,
88
- amount,
89
- scrolled: true,
90
- };
91
- }
92
-
93
- /**
94
- * Click element
95
- */
96
- export async function clickElement(profileId, selector, options = {}) {
97
- if (!isBrowserRunning(profileId)) {
98
- throw new Error(`No browser running for profile: ${profileId}`);
99
- }
100
-
101
- const page = await getCurrentPage(profileId);
102
-
103
- // Wait for element to be visible
104
- await page.waitForSelector(selector, { state: 'visible', timeout: options.timeout || 10000 });
105
-
106
- // Scroll into view if needed
107
- await page.locator(selector).scrollIntoViewIfNeeded();
108
-
109
- // Click with system-level mouse
110
- const element = await page.$(selector);
111
- const box = await element.boundingBox();
112
-
113
- if (!box) {
114
- throw new Error(`Element not visible: ${selector}`);
115
- }
116
-
117
- const x = box.x + box.width / 2;
118
- const y = box.y + box.height / 2;
119
-
120
- await page.mouse.click(x, y, { button: options.button || 'left', clickCount: options.clickCount || 1 });
121
-
122
- return {
123
- ok: true,
124
- profileId,
125
- selector,
126
- clicked: true,
127
- position: { x, y },
128
- };
129
- }
130
-
131
- /**
132
- * Type text
133
- */
134
- export async function typeText(profileId, selector, text, options = {}) {
135
- if (!isBrowserRunning(profileId)) {
136
- throw new Error(`No browser running for profile: ${profileId}`);
137
- }
138
-
139
- const page = await getCurrentPage(profileId);
140
-
141
- // Wait for element to be visible
142
- await page.waitForSelector(selector, { state: 'visible', timeout: options.timeout || 10000 });
143
-
144
- // Scroll into view if needed
145
- await page.locator(selector).scrollIntoViewIfNeeded();
146
-
147
- // Focus and type
148
- const element = await page.$(selector);
149
- await element.focus();
150
- await element.fill('');
151
-
152
- if (options.slowly) {
153
- await element.type(text, { delay: 50 });
154
- } else {
155
- await element.fill(text);
156
- }
157
-
158
- if (options.pressEnter) {
159
- await page.keyboard.press('Enter');
160
- }
161
-
162
- return {
163
- ok: true,
164
- profileId,
165
- selector,
166
- typed: text.length,
167
- };
168
- }
169
-
170
- /**
171
- * Press key
172
- */
173
- export async function pressKey(profileId, key) {
174
- if (!isBrowserRunning(profileId)) {
175
- throw new Error(`No browser running for profile: ${profileId}`);
176
- }
177
-
178
- const page = await getCurrentPage(profileId);
179
- await page.keyboard.press(key);
180
-
181
- return {
182
- ok: true,
183
- profileId,
184
- key,
185
- };
186
- }
187
-
188
- /**
189
- * Highlight element
190
- */
191
- export async function highlightElement(profileId, selector, options = {}) {
192
- if (!isBrowserRunning(profileId)) {
193
- throw new Error(`No browser running for profile: ${profileId}`);
194
- }
195
-
196
- const page = await getCurrentPage(profileId);
197
-
198
- const result = await page.evaluate((sel) => {
199
- const el = document.querySelector(sel);
200
- if (!el) return null;
201
-
202
- const prev = el.style.outline;
203
- el.style.outline = '3px solid #ff4444';
204
-
205
- const rect = el.getBoundingClientRect();
206
- return {
207
- highlighted: true,
208
- rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
209
- prevOutline: prev,
210
- };
211
- }, selector);
212
-
213
- if (!result) {
214
- throw new Error(`Element not found: ${selector}`);
215
- }
216
-
217
- // Auto-clear highlight after duration
218
- const duration = options.duration || 2000;
219
- setTimeout(async () => {
220
- try {
221
- const currentPage = await getCurrentPage(profileId);
222
- await currentPage.evaluate((sel) => {
223
- const el = document.querySelector(sel);
224
- if (el) el.style.outline = '';
225
- }, selector);
226
- } catch {
227
- // Browser may have been closed
228
- }
229
- }, duration);
230
-
231
- return {
232
- ok: true,
233
- profileId,
234
- selector,
235
- ...result,
236
- };
237
- }
238
-
239
- /**
240
- * Clear all highlights
241
- */
242
- export async function clearHighlights(profileId) {
243
- if (!isBrowserRunning(profileId)) {
244
- throw new Error(`No browser running for profile: ${profileId}`);
245
- }
246
-
247
- const page = await getCurrentPage(profileId);
248
- await page.evaluate(() => {
249
- document.querySelectorAll('[style*="outline"]').forEach((el) => {
250
- el.style.outline = '';
251
- });
252
- });
253
-
254
- return {
255
- ok: true,
256
- profileId,
257
- cleared: true,
258
- };
259
- }
260
-
261
- /**
262
- * Set viewport size
263
- */
264
- export async function setViewport(profileId, width, height) {
265
- if (!isBrowserRunning(profileId)) {
266
- throw new Error(`No browser running for profile: ${profileId}`);
267
- }
268
-
269
- const page = await getCurrentPage(profileId);
270
- await page.setViewportSize({ width, height });
271
-
272
- return {
273
- ok: true,
274
- profileId,
275
- viewport: { width, height },
276
- };
277
- }
278
-
279
- /**
280
- * Get page info (URL, title, etc)
281
- */
282
- export async function getPageInfo(profileId) {
283
- if (!isBrowserRunning(profileId)) {
284
- throw new Error(`No browser running for profile: ${profileId}`);
285
- }
286
-
287
- const page = await getCurrentPage(profileId);
288
-
289
- return {
290
- ok: true,
291
- profileId,
292
- url: page.url(),
293
- title: await page.title(),
294
- viewport: page.viewportSize(),
295
- };
296
- }
297
-
298
- /**
299
- * Get DOM snapshot
300
- */
301
- export async function getDOMSnapshot(profileId, options = {}) {
302
- if (!isBrowserRunning(profileId)) {
303
- throw new Error(`No browser running for profile: ${profileId}`);
304
- }
305
-
306
- const page = await getCurrentPage(profileId);
307
-
308
- const snapshot = await page.evaluate((opts) => {
309
- const collectNodes = (node, depth = 0, path = 'root') => {
310
- if (depth > 10) return null;
311
- if (!node) return null;
312
-
313
- const result = {
314
- tag: node.tagName?.toLowerCase() || node.nodeName?.toLowerCase(),
315
- id: node.id || null,
316
- classes: node.className ? node.className.split(' ').filter(Boolean) : [],
317
- path,
318
- };
319
-
320
- const rect = node.getBoundingClientRect?.() || null;
321
- if (rect) {
322
- result.rect = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
323
- }
324
-
325
- const text = node.textContent?.trim().slice(0, 50) || '';
326
- if (text && node.children?.length === 0) {
327
- result.text = text;
328
- }
329
-
330
- if (node.children && node.children.length > 0) {
331
- result.children = [];
332
- for (let i = 0; i < node.children.length; i++) {
333
- const child = collectNodes(node.children[i], depth + 1, `${path}/${i}`);
334
- if (child) result.children.push(child);
335
- }
336
- }
337
-
338
- return result;
339
- };
340
-
341
- const viewport = { width: window.innerWidth, height: window.innerHeight };
342
- const root = collectNodes(document.body, 0, 'root');
343
-
344
- return { viewport, dom_tree: root };
345
- }, options);
346
-
347
- return {
348
- ok: true,
349
- profileId,
350
- ...snapshot,
351
- };
352
- }
353
-
354
- /**
355
- * Query elements
356
- */
357
- export async function queryElements(profileId, selector, options = {}) {
358
- if (!isBrowserRunning(profileId)) {
359
- throw new Error(`No browser running for profile: ${profileId}`);
360
- }
361
-
362
- const page = await getCurrentPage(profileId);
363
-
364
- const elements = await page.evaluate((sel) => {
365
- const nodes = document.querySelectorAll(sel);
366
- return Array.from(nodes).map((el, idx) => {
367
- const rect = el.getBoundingClientRect();
368
- return {
369
- index: idx,
370
- tag: el.tagName.toLowerCase(),
371
- id: el.id || null,
372
- classes: el.className.split(' ').filter(Boolean),
373
- text: el.textContent?.trim().slice(0, 100) || '',
374
- rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
375
- visible: rect.width > 0 && rect.height > 0,
376
- };
377
- });
378
- }, selector);
379
-
380
- return {
381
- ok: true,
382
- profileId,
383
- selector,
384
- count: elements.length,
385
- elements,
386
- };
387
- }
388
-
389
- /**
390
- * Execute JavaScript
391
- */
392
- export async function evaluateJS(profileId, script) {
393
- if (!isBrowserRunning(profileId)) {
394
- throw new Error(`No browser running for profile: ${profileId}`);
395
- }
396
-
397
- const page = await getCurrentPage(profileId);
398
- const result = await page.evaluate(script);
399
-
400
- return {
401
- ok: true,
402
- profileId,
403
- result,
404
- };
405
- }
406
-
407
- /**
408
- * Create new page
409
- */
410
- export async function createNewPage(profileId, options = {}) {
411
- const browser = getActiveBrowser(profileId);
412
- if (!browser) {
413
- throw new Error(`No browser running for profile: ${profileId}`);
414
- }
415
-
416
- const context = browser.pwBrowser?.contexts()[0];
417
- if (!context) {
418
- throw new Error('No browser context available');
419
- }
420
-
421
- const page = await context.newPage();
422
- if (options.url) {
423
- await page.goto(options.url, { waitUntil: 'domcontentloaded' });
424
- }
425
-
426
- browser.pages.push(page);
427
- browser.currentPage = page;
428
-
429
- return {
430
- ok: true,
431
- profileId,
432
- pageIndex: browser.pages.length - 1,
433
- url: options.url || 'about:blank',
434
- };
435
- }
436
-
437
- /**
438
- * List pages
439
- */
440
- export async function listPages(profileId) {
441
- const browser = getActiveBrowser(profileId);
442
- if (!browser) {
443
- throw new Error(`No browser running for profile: ${profileId}`);
444
- }
445
-
446
- const context = browser.pwBrowser?.contexts()[0];
447
- if (!context) {
448
- throw new Error('No browser context available');
449
- }
450
-
451
- const pages = context.pages();
452
- const pageInfos = await Promise.all(pages.map(async (page, idx) => ({
453
- index: idx,
454
- url: page.url(),
455
- title: await page.title().catch(() => ''),
456
- })));
457
-
458
- return {
459
- ok: true,
460
- profileId,
461
- count: pageInfos.length,
462
- pages: pageInfos,
463
- currentPage: browser.currentPage?._guid || 0,
464
- };
465
- }
466
-
467
- /**
468
- * Switch page
469
- */
470
- export async function switchPage(profileId, pageIndex) {
471
- const browser = getActiveBrowser(profileId);
472
- if (!browser) {
473
- throw new Error(`No browser running for profile: ${profileId}`);
474
- }
475
-
476
- const context = browser.pwBrowser?.contexts()[0];
477
- if (!context) {
478
- throw new Error('No browser context available');
479
- }
480
-
481
- const pages = context.pages();
482
- if (pageIndex < 0 || pageIndex >= pages.length) {
483
- throw new Error(`Invalid page index: ${pageIndex}`);
484
- }
485
-
486
- browser.currentPage = pages[pageIndex];
487
- await pages[pageIndex].bringToFront();
488
-
489
- return {
490
- ok: true,
491
- profileId,
492
- pageIndex,
493
- url: pages[pageIndex].url(),
494
- };
495
- }
496
-
497
- /**
498
- * Close page
499
- */
500
- export async function closePage(profileId, pageIndex) {
501
- const browser = getActiveBrowser(profileId);
502
- if (!browser) {
503
- throw new Error(`No browser running for profile: ${profileId}`);
504
- }
505
-
506
- const context = browser.pwBrowser?.contexts()[0];
507
- if (!context) {
508
- throw new Error('No browser context available');
509
- }
510
-
511
- const pages = context.pages();
512
- const idx = pageIndex !== undefined ? pageIndex : pages.length - 1;
513
-
514
- if (idx < 0 || idx >= pages.length) {
515
- throw new Error(`Invalid page index: ${idx}`);
516
- }
517
-
518
- await pages[idx].close();
519
- browser.pages = pages.filter((_, i) => i !== idx);
520
-
521
- // Update current page if needed
522
- if (browser.currentPage === pages[idx]) {
523
- browser.currentPage = browser.pages[0] || null;
524
- }
525
-
526
- return {
527
- ok: true,
528
- profileId,
529
- closedIndex: idx,
530
- remaining: browser.pages.length,
531
- };
532
- }
533
-
534
- /**
535
- * Mouse operations
536
- */
537
- export async function mouseClick(profileId, x, y, options = {}) {
538
- if (!isBrowserRunning(profileId)) {
539
- throw new Error(`No browser running for profile: ${profileId}`);
540
- }
541
-
542
- const page = await getCurrentPage(profileId);
543
- await page.mouse.click(x, y, {
544
- button: options.button || 'left',
545
- clickCount: options.clickCount || 1,
546
- delay: options.delay || 0,
547
- });
548
-
549
- return { ok: true, profileId, x, y, button: options.button || 'left' };
550
- }
551
-
552
- export async function mouseWheel(profileId, deltaX, deltaY) {
553
- if (!isBrowserRunning(profileId)) {
554
- throw new Error(`No browser running for profile: ${profileId}`);
555
- }
556
-
557
- const page = await getCurrentPage(profileId);
558
- await page.mouse.wheel(deltaX, deltaY);
559
-
560
- return { ok: true, profileId, deltaX, deltaY };
561
- }
1
+ /**
2
+ * Browser actions module - Playwright-based page operations
3
+ * No external browser-service dependency
4
+ */
5
+ import { getActiveBrowser, getCurrentPage, isBrowserRunning } from './browser.mjs';
6
+
7
+ /**
8
+ * Navigate to URL
9
+ */
10
+ export async function navigateTo(profileId, url) {
11
+ if (!isBrowserRunning(profileId)) {
12
+ throw new Error(`No browser running for profile: ${profileId}`);
13
+ }
14
+
15
+ const page = await getCurrentPage(profileId);
16
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
17
+
18
+ return {
19
+ ok: true,
20
+ profileId,
21
+ url: page.url(),
22
+ title: await page.title(),
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Go back
28
+ */
29
+ export async function goBack(profileId) {
30
+ if (!isBrowserRunning(profileId)) {
31
+ throw new Error(`No browser running for profile: ${profileId}`);
32
+ }
33
+
34
+ const page = await getCurrentPage(profileId);
35
+ await page.goBack({ waitUntil: 'domcontentloaded' });
36
+
37
+ return {
38
+ ok: true,
39
+ profileId,
40
+ url: page.url(),
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Take screenshot
46
+ */
47
+ export async function takeScreenshot(profileId, options = {}) {
48
+ if (!isBrowserRunning(profileId)) {
49
+ throw new Error(`No browser running for profile: ${profileId}`);
50
+ }
51
+
52
+ const page = await getCurrentPage(profileId);
53
+ const buffer = await page.screenshot({
54
+ fullPage: options.fullPage || false,
55
+ type: 'png',
56
+ });
57
+
58
+ return {
59
+ ok: true,
60
+ profileId,
61
+ data: buffer.toString('base64'),
62
+ width: options.fullPage ? undefined : page.viewportSize()?.width,
63
+ height: options.fullPage ? undefined : page.viewportSize()?.height,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Scroll page
69
+ */
70
+ export async function scrollPage(profileId, options = {}) {
71
+ if (!isBrowserRunning(profileId)) {
72
+ throw new Error(`No browser running for profile: ${profileId}`);
73
+ }
74
+
75
+ const page = await getCurrentPage(profileId);
76
+ const direction = options.direction || 'down';
77
+ const amount = options.amount || 300;
78
+
79
+ const deltaX = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
80
+ const deltaY = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
81
+
82
+ await page.mouse.wheel(deltaX, deltaY);
83
+
84
+ return {
85
+ ok: true,
86
+ profileId,
87
+ direction,
88
+ amount,
89
+ scrolled: true,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Click element
95
+ */
96
+ export async function clickElement(profileId, selector, options = {}) {
97
+ if (!isBrowserRunning(profileId)) {
98
+ throw new Error(`No browser running for profile: ${profileId}`);
99
+ }
100
+
101
+ const page = await getCurrentPage(profileId);
102
+
103
+ // Wait for element to be visible
104
+ await page.waitForSelector(selector, { state: 'visible', timeout: options.timeout || 10000 });
105
+
106
+ // Scroll into view if needed
107
+ await page.locator(selector).scrollIntoViewIfNeeded();
108
+
109
+ // Click with system-level mouse
110
+ const element = await page.$(selector);
111
+ const box = await element.boundingBox();
112
+
113
+ if (!box) {
114
+ throw new Error(`Element not visible: ${selector}`);
115
+ }
116
+
117
+ const x = box.x + box.width / 2;
118
+ const y = box.y + box.height / 2;
119
+
120
+ await page.mouse.click(x, y, { button: options.button || 'left', clickCount: options.clickCount || 1 });
121
+
122
+ return {
123
+ ok: true,
124
+ profileId,
125
+ selector,
126
+ clicked: true,
127
+ position: { x, y },
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Type text
133
+ */
134
+ export async function typeText(profileId, selector, text, options = {}) {
135
+ if (!isBrowserRunning(profileId)) {
136
+ throw new Error(`No browser running for profile: ${profileId}`);
137
+ }
138
+
139
+ const page = await getCurrentPage(profileId);
140
+
141
+ // Wait for element to be visible
142
+ await page.waitForSelector(selector, { state: 'visible', timeout: options.timeout || 10000 });
143
+
144
+ // Scroll into view if needed
145
+ await page.locator(selector).scrollIntoViewIfNeeded();
146
+
147
+ // Focus and type
148
+ const element = await page.$(selector);
149
+ await element.focus();
150
+ await element.fill('');
151
+
152
+ if (options.slowly) {
153
+ await element.type(text, { delay: 50 });
154
+ } else {
155
+ await element.fill(text);
156
+ }
157
+
158
+ if (options.pressEnter) {
159
+ await page.keyboard.press('Enter');
160
+ }
161
+
162
+ return {
163
+ ok: true,
164
+ profileId,
165
+ selector,
166
+ typed: text.length,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Press key
172
+ */
173
+ export async function pressKey(profileId, key) {
174
+ if (!isBrowserRunning(profileId)) {
175
+ throw new Error(`No browser running for profile: ${profileId}`);
176
+ }
177
+
178
+ const page = await getCurrentPage(profileId);
179
+ await page.keyboard.press(key);
180
+
181
+ return {
182
+ ok: true,
183
+ profileId,
184
+ key,
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Highlight element
190
+ */
191
+ export async function highlightElement(profileId, selector, options = {}) {
192
+ if (!isBrowserRunning(profileId)) {
193
+ throw new Error(`No browser running for profile: ${profileId}`);
194
+ }
195
+
196
+ const page = await getCurrentPage(profileId);
197
+
198
+ const result = await page.evaluate((sel) => {
199
+ const el = document.querySelector(sel);
200
+ if (!el) return null;
201
+
202
+ const prev = el.style.outline;
203
+ el.style.outline = '3px solid #ff4444';
204
+
205
+ const rect = el.getBoundingClientRect();
206
+ return {
207
+ highlighted: true,
208
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
209
+ prevOutline: prev,
210
+ };
211
+ }, selector);
212
+
213
+ if (!result) {
214
+ throw new Error(`Element not found: ${selector}`);
215
+ }
216
+
217
+ // Auto-clear highlight after duration
218
+ const duration = options.duration || 2000;
219
+ setTimeout(async () => {
220
+ try {
221
+ const currentPage = await getCurrentPage(profileId);
222
+ await currentPage.evaluate((sel) => {
223
+ const el = document.querySelector(sel);
224
+ if (el) el.style.outline = '';
225
+ }, selector);
226
+ } catch {
227
+ // Browser may have been closed
228
+ }
229
+ }, duration);
230
+
231
+ return {
232
+ ok: true,
233
+ profileId,
234
+ selector,
235
+ ...result,
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Clear all highlights
241
+ */
242
+ export async function clearHighlights(profileId) {
243
+ if (!isBrowserRunning(profileId)) {
244
+ throw new Error(`No browser running for profile: ${profileId}`);
245
+ }
246
+
247
+ const page = await getCurrentPage(profileId);
248
+ await page.evaluate(() => {
249
+ document.querySelectorAll('[style*="outline"]').forEach((el) => {
250
+ el.style.outline = '';
251
+ });
252
+ });
253
+
254
+ return {
255
+ ok: true,
256
+ profileId,
257
+ cleared: true,
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Set viewport size
263
+ */
264
+ export async function setViewport(profileId, width, height) {
265
+ if (!isBrowserRunning(profileId)) {
266
+ throw new Error(`No browser running for profile: ${profileId}`);
267
+ }
268
+
269
+ const page = await getCurrentPage(profileId);
270
+ await page.setViewportSize({ width, height });
271
+
272
+ return {
273
+ ok: true,
274
+ profileId,
275
+ viewport: { width, height },
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Get page info (URL, title, etc)
281
+ */
282
+ export async function getPageInfo(profileId) {
283
+ if (!isBrowserRunning(profileId)) {
284
+ throw new Error(`No browser running for profile: ${profileId}`);
285
+ }
286
+
287
+ const page = await getCurrentPage(profileId);
288
+
289
+ return {
290
+ ok: true,
291
+ profileId,
292
+ url: page.url(),
293
+ title: await page.title(),
294
+ viewport: page.viewportSize(),
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Get DOM snapshot
300
+ */
301
+ export async function getDOMSnapshot(profileId, options = {}) {
302
+ if (!isBrowserRunning(profileId)) {
303
+ throw new Error(`No browser running for profile: ${profileId}`);
304
+ }
305
+
306
+ const page = await getCurrentPage(profileId);
307
+
308
+ const snapshot = await page.evaluate((opts) => {
309
+ const collectNodes = (node, depth = 0, path = 'root') => {
310
+ if (depth > 10) return null;
311
+ if (!node) return null;
312
+
313
+ const result = {
314
+ tag: node.tagName?.toLowerCase() || node.nodeName?.toLowerCase(),
315
+ id: node.id || null,
316
+ classes: node.className ? node.className.split(' ').filter(Boolean) : [],
317
+ path,
318
+ };
319
+
320
+ const rect = node.getBoundingClientRect?.() || null;
321
+ if (rect) {
322
+ result.rect = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
323
+ }
324
+
325
+ const text = node.textContent?.trim().slice(0, 50) || '';
326
+ if (text && node.children?.length === 0) {
327
+ result.text = text;
328
+ }
329
+
330
+ if (node.children && node.children.length > 0) {
331
+ result.children = [];
332
+ for (let i = 0; i < node.children.length; i++) {
333
+ const child = collectNodes(node.children[i], depth + 1, `${path}/${i}`);
334
+ if (child) result.children.push(child);
335
+ }
336
+ }
337
+
338
+ return result;
339
+ };
340
+
341
+ const viewport = { width: window.innerWidth, height: window.innerHeight };
342
+ const root = collectNodes(document.body, 0, 'root');
343
+
344
+ return { viewport, dom_tree: root };
345
+ }, options);
346
+
347
+ return {
348
+ ok: true,
349
+ profileId,
350
+ ...snapshot,
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Query elements
356
+ */
357
+ export async function queryElements(profileId, selector, options = {}) {
358
+ if (!isBrowserRunning(profileId)) {
359
+ throw new Error(`No browser running for profile: ${profileId}`);
360
+ }
361
+
362
+ const page = await getCurrentPage(profileId);
363
+
364
+ const elements = await page.evaluate((sel) => {
365
+ const nodes = document.querySelectorAll(sel);
366
+ return Array.from(nodes).map((el, idx) => {
367
+ const rect = el.getBoundingClientRect();
368
+ return {
369
+ index: idx,
370
+ tag: el.tagName.toLowerCase(),
371
+ id: el.id || null,
372
+ classes: el.className.split(' ').filter(Boolean),
373
+ text: el.textContent?.trim().slice(0, 100) || '',
374
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
375
+ visible: rect.width > 0 && rect.height > 0,
376
+ };
377
+ });
378
+ }, selector);
379
+
380
+ return {
381
+ ok: true,
382
+ profileId,
383
+ selector,
384
+ count: elements.length,
385
+ elements,
386
+ };
387
+ }
388
+
389
+ /**
390
+ * Execute JavaScript
391
+ */
392
+ export async function evaluateJS(profileId, script) {
393
+ if (!isBrowserRunning(profileId)) {
394
+ throw new Error(`No browser running for profile: ${profileId}`);
395
+ }
396
+
397
+ const page = await getCurrentPage(profileId);
398
+ const result = await page.evaluate(script);
399
+
400
+ return {
401
+ ok: true,
402
+ profileId,
403
+ result,
404
+ };
405
+ }
406
+
407
+ /**
408
+ * Create new page
409
+ */
410
+ export async function createNewPage(profileId, options = {}) {
411
+ const browser = getActiveBrowser(profileId);
412
+ if (!browser) {
413
+ throw new Error(`No browser running for profile: ${profileId}`);
414
+ }
415
+
416
+ const context = browser.pwBrowser?.contexts()[0];
417
+ if (!context) {
418
+ throw new Error('No browser context available');
419
+ }
420
+
421
+ const page = await context.newPage();
422
+ if (options.url) {
423
+ await page.goto(options.url, { waitUntil: 'domcontentloaded' });
424
+ }
425
+
426
+ browser.pages.push(page);
427
+ browser.currentPage = page;
428
+
429
+ return {
430
+ ok: true,
431
+ profileId,
432
+ pageIndex: browser.pages.length - 1,
433
+ url: options.url || 'about:blank',
434
+ };
435
+ }
436
+
437
+ /**
438
+ * List pages
439
+ */
440
+ export async function listPages(profileId) {
441
+ const browser = getActiveBrowser(profileId);
442
+ if (!browser) {
443
+ throw new Error(`No browser running for profile: ${profileId}`);
444
+ }
445
+
446
+ const context = browser.pwBrowser?.contexts()[0];
447
+ if (!context) {
448
+ throw new Error('No browser context available');
449
+ }
450
+
451
+ const pages = context.pages();
452
+ const pageInfos = await Promise.all(pages.map(async (page, idx) => ({
453
+ index: idx,
454
+ url: page.url(),
455
+ title: await page.title().catch(() => ''),
456
+ })));
457
+
458
+ return {
459
+ ok: true,
460
+ profileId,
461
+ count: pageInfos.length,
462
+ pages: pageInfos,
463
+ currentPage: browser.currentPage?._guid || 0,
464
+ };
465
+ }
466
+
467
+ /**
468
+ * Switch page
469
+ */
470
+ export async function switchPage(profileId, pageIndex) {
471
+ const browser = getActiveBrowser(profileId);
472
+ if (!browser) {
473
+ throw new Error(`No browser running for profile: ${profileId}`);
474
+ }
475
+
476
+ const context = browser.pwBrowser?.contexts()[0];
477
+ if (!context) {
478
+ throw new Error('No browser context available');
479
+ }
480
+
481
+ const pages = context.pages();
482
+ if (pageIndex < 0 || pageIndex >= pages.length) {
483
+ throw new Error(`Invalid page index: ${pageIndex}`);
484
+ }
485
+
486
+ browser.currentPage = pages[pageIndex];
487
+ await pages[pageIndex].bringToFront();
488
+
489
+ return {
490
+ ok: true,
491
+ profileId,
492
+ pageIndex,
493
+ url: pages[pageIndex].url(),
494
+ };
495
+ }
496
+
497
+ /**
498
+ * Close page
499
+ */
500
+ export async function closePage(profileId, pageIndex) {
501
+ const browser = getActiveBrowser(profileId);
502
+ if (!browser) {
503
+ throw new Error(`No browser running for profile: ${profileId}`);
504
+ }
505
+
506
+ const context = browser.pwBrowser?.contexts()[0];
507
+ if (!context) {
508
+ throw new Error('No browser context available');
509
+ }
510
+
511
+ const pages = context.pages();
512
+ const idx = pageIndex !== undefined ? pageIndex : pages.length - 1;
513
+
514
+ if (idx < 0 || idx >= pages.length) {
515
+ throw new Error(`Invalid page index: ${idx}`);
516
+ }
517
+
518
+ await pages[idx].close();
519
+ browser.pages = pages.filter((_, i) => i !== idx);
520
+
521
+ // Update current page if needed
522
+ if (browser.currentPage === pages[idx]) {
523
+ browser.currentPage = browser.pages[0] || null;
524
+ }
525
+
526
+ return {
527
+ ok: true,
528
+ profileId,
529
+ closedIndex: idx,
530
+ remaining: browser.pages.length,
531
+ };
532
+ }
533
+
534
+ /**
535
+ * Mouse operations
536
+ */
537
+ export async function mouseClick(profileId, x, y, options = {}) {
538
+ if (!isBrowserRunning(profileId)) {
539
+ throw new Error(`No browser running for profile: ${profileId}`);
540
+ }
541
+
542
+ const page = await getCurrentPage(profileId);
543
+ await page.mouse.click(x, y, {
544
+ button: options.button || 'left',
545
+ clickCount: options.clickCount || 1,
546
+ delay: options.delay || 0,
547
+ });
548
+
549
+ return { ok: true, profileId, x, y, button: options.button || 'left' };
550
+ }
551
+
552
+ export async function mouseWheel(profileId, deltaX, deltaY) {
553
+ if (!isBrowserRunning(profileId)) {
554
+ throw new Error(`No browser running for profile: ${profileId}`);
555
+ }
556
+
557
+ const page = await getCurrentPage(profileId);
558
+ await page.mouse.wheel(deltaX, deltaY);
559
+
560
+ return { ok: true, profileId, deltaX, deltaY };
561
+ }