@bubstack/moe-glass 0.1.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 (62) hide show
  1. package/README.md +29 -0
  2. package/agents/browser-user.md +105 -0
  3. package/dist/LICENSE +25 -0
  4. package/dist/index.d.ts +9 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +22517 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/payload.d.ts +214 -0
  9. package/dist/payload.d.ts.map +1 -0
  10. package/dist/payload.js +325 -0
  11. package/dist/payload.js.map +1 -0
  12. package/package.json +59 -0
  13. package/skills/browsing/COMMANDLINE-USAGE.md +595 -0
  14. package/skills/browsing/EXAMPLES.md +717 -0
  15. package/skills/browsing/README.md +55 -0
  16. package/skills/browsing/SKILL.md +478 -0
  17. package/skills/browsing/chrome-ws +1021 -0
  18. package/skills/browsing/chrome-ws-lib.js +461 -0
  19. package/skills/browsing/host-override.js +98 -0
  20. package/skills/browsing/lib/browser-bridge.js +175 -0
  21. package/skills/browsing/lib/browser-session.js +137 -0
  22. package/skills/browsing/lib/capture.js +499 -0
  23. package/skills/browsing/lib/cdp-router.js +72 -0
  24. package/skills/browsing/lib/cdp-utils.js +18 -0
  25. package/skills/browsing/lib/chrome-launcher-helpers.js +374 -0
  26. package/skills/browsing/lib/chrome-process.js +464 -0
  27. package/skills/browsing/lib/console-logging.js +70 -0
  28. package/skills/browsing/lib/cookies.js +17 -0
  29. package/skills/browsing/lib/dialogs-render.js +154 -0
  30. package/skills/browsing/lib/dialogs-router.js +117 -0
  31. package/skills/browsing/lib/dialogs.js +254 -0
  32. package/skills/browsing/lib/element-selector.js +91 -0
  33. package/skills/browsing/lib/evaluation.js +85 -0
  34. package/skills/browsing/lib/extraction.js +55 -0
  35. package/skills/browsing/lib/file-upload.js +56 -0
  36. package/skills/browsing/lib/html-diff.js +122 -0
  37. package/skills/browsing/lib/key-definitions.js +149 -0
  38. package/skills/browsing/lib/keyboard-input.js +288 -0
  39. package/skills/browsing/lib/mouse.js +423 -0
  40. package/skills/browsing/lib/navigation.js +272 -0
  41. package/skills/browsing/lib/page-scripts/dom-summary.js +31 -0
  42. package/skills/browsing/lib/page-scripts/markdown.js +85 -0
  43. package/skills/browsing/lib/page-scripts/permission-shim.js +80 -0
  44. package/skills/browsing/lib/page-session.js +106 -0
  45. package/skills/browsing/lib/profile-lock.js +179 -0
  46. package/skills/browsing/lib/screenshot.js +171 -0
  47. package/skills/browsing/lib/select-option.js +99 -0
  48. package/skills/browsing/lib/session-state.js +66 -0
  49. package/skills/browsing/lib/tabs.js +144 -0
  50. package/skills/browsing/lib/viewport.js +103 -0
  51. package/skills/browsing/lib/websocket-client.js +162 -0
  52. package/skills/browsing/package.json +11 -0
  53. package/skills/browsing/test-chrome-args.js +81 -0
  54. package/skills/browsing/test-cookies.js +21 -0
  55. package/skills/browsing/test-e2e.sh +51 -0
  56. package/skills/browsing/test-extract.sh +17 -0
  57. package/skills/browsing/test-interact.sh +11 -0
  58. package/skills/browsing/test-navigate.sh +9 -0
  59. package/skills/browsing/test-raw.sh +8 -0
  60. package/skills/browsing/test-tabs.sh +15 -0
  61. package/skills/browsing/test-viewport.js +27 -0
  62. package/skills/browsing/test-wait.sh +9 -0
@@ -0,0 +1,717 @@
1
+ # Chrome Direct Access Examples (MCP Tool)
2
+
3
+ Examples using the `use_browser` MCP tool. For command-line bash examples, see [COMMANDLINE-USAGE.md](COMMANDLINE-USAGE.md).
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Basic Operations](#basic-operations)
8
+ 2. [Form Automation](#form-automation)
9
+ 3. [Web Scraping](#web-scraping)
10
+ 4. [Multi-Tab Workflows](#multi-tab-workflows)
11
+ 5. [Dynamic Content](#dynamic-content)
12
+ 6. [Dialogs](#dialogs) — basic-auth, JS confirm, popup-with-confirm
13
+ 7. [Recovery](#recovery) — auto-restart, kill/restart cycle
14
+ 8. [Multi-MCP isolation](#multi-mcp-isolation)
15
+ 9. [Advanced Patterns](#advanced-patterns)
16
+
17
+ ---
18
+
19
+ ## Basic Operations
20
+
21
+ ### Extract Page Content
22
+
23
+ Navigate to a page and extract various elements:
24
+
25
+ ```
26
+ {action: "navigate", payload: "https://example.com"}
27
+ {action: "await_element", selector: "h1"}
28
+
29
+ // Get page title
30
+ {action: "eval", payload: "document.title"}
31
+
32
+ // Get main heading text
33
+ {action: "extract", payload: "text", selector: "h1"}
34
+
35
+ // Get first link URL
36
+ {action: "attr", selector: "a", payload: "href"}
37
+ ```
38
+
39
+ ### Get All Links
40
+
41
+ Use JavaScript evaluation to get structured data:
42
+
43
+ ```
44
+ {action: "navigate", payload: "https://example.com"}
45
+ {action: "eval", payload: "Array.from(document.querySelectorAll('a')).map(a => ({ text: a.textContent.trim(), href: a.href }))"}
46
+ ```
47
+
48
+ ### Extract Table Data
49
+
50
+ Convert HTML table to structured data:
51
+
52
+ ```
53
+ {action: "navigate", payload: "https://example.com/data"}
54
+ {action: "await_element", selector: "table"}
55
+
56
+ // Convert table to JSON array
57
+ {action: "eval", payload: "Array.from(document.querySelectorAll('table tr')).map(row => Array.from(row.cells).map(cell => cell.textContent.trim()))"}
58
+ ```
59
+
60
+ ### Get Page as Markdown
61
+
62
+ Extract entire page content in markdown format:
63
+
64
+ ```
65
+ {action: "navigate", payload: "https://example.com"}
66
+ {action: "await_element", selector: "body"}
67
+ {action: "extract", payload: "markdown"}
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Form Automation
73
+
74
+ ### Simple Login
75
+
76
+ Navigate, fill credentials, and submit:
77
+
78
+ ```
79
+ {action: "navigate", payload: "https://app.example.com/login"}
80
+ {action: "await_element", selector: "input[name=email]"}
81
+
82
+ // Fill credentials
83
+ {action: "type", selector: "input[name=email]", payload: "user@example.com"}
84
+ {action: "type", selector: "input[name=password]", payload: "securepass123\n"}
85
+
86
+ // Wait for successful login
87
+ {action: "await_text", payload: "Dashboard"}
88
+ ```
89
+
90
+ Note: The `\n` at the end of the password submits the form.
91
+
92
+ ### Multi-Step Form
93
+
94
+ Handle forms that show steps progressively:
95
+
96
+ ```
97
+ {action: "navigate", payload: "https://example.com/register"}
98
+
99
+ // Step 1: Personal information
100
+ {action: "type", selector: "input[name=firstName]", payload: "John"}
101
+ {action: "type", selector: "input[name=lastName]", payload: "Doe"}
102
+ {action: "type", selector: "input[name=email]", payload: "john@example.com"}
103
+ {action: "click", selector: "button.next"}
104
+
105
+ // Wait for step 2 to load
106
+ {action: "await_element", selector: "input[name=address]"}
107
+
108
+ // Step 2: Address
109
+ {action: "type", selector: "input[name=address]", payload: "123 Main St"}
110
+ {action: "select", selector: "select[name=state]", payload: "IL"}
111
+ {action: "type", selector: "input[name=zip]", payload: "62701"}
112
+ {action: "click", selector: "button.submit"}
113
+
114
+ {action: "await_text", payload: "Registration complete"}
115
+ ```
116
+
117
+ ### Search with Filters
118
+
119
+ Use dropdowns and text inputs together:
120
+
121
+ ```
122
+ {action: "navigate", payload: "https://library.example.com/search"}
123
+ {action: "await_element", selector: "form"}
124
+
125
+ // Select category dropdown
126
+ {action: "select", selector: "select[name=category]", payload: "books"}
127
+
128
+ // Fill search term
129
+ {action: "type", selector: "input[name=query]", payload: "chrome devtools"}
130
+
131
+ // Submit and count results
132
+ {action: "click", selector: "button[type=submit]"}
133
+ {action: "await_element", selector: ".results"}
134
+
135
+ // Count results
136
+ {action: "eval", payload: "document.querySelectorAll('.result').length"}
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Web Scraping
142
+
143
+ ### Article Content
144
+
145
+ Extract article metadata and content:
146
+
147
+ ```
148
+ {action: "navigate", payload: "https://blog.example.com/article"}
149
+ {action: "await_element", selector: "article"}
150
+
151
+ // Extract metadata
152
+ {action: "extract", payload: "text", selector: "article h1"}
153
+ {action: "extract", payload: "text", selector: ".author-name"}
154
+ {action: "extract", payload: "text", selector: "time"}
155
+ {action: "extract", payload: "text", selector: "article .content"}
156
+ ```
157
+
158
+ ### Product Information
159
+
160
+ Scrape product details from e-commerce site:
161
+
162
+ ```
163
+ {action: "navigate", payload: "https://shop.example.com/product/123"}
164
+ {action: "await_element", selector: ".product-details"}
165
+
166
+ // Extract product data
167
+ {action: "extract", payload: "text", selector: "h1.product-name"}
168
+ {action: "extract", payload: "text", selector: ".price"}
169
+ {action: "attr", selector: ".product-image img", payload: "src"}
170
+ {action: "extract", payload: "text", selector: ".stock-status"}
171
+ ```
172
+
173
+ ### Batch Extract Structured Data
174
+
175
+ Get multiple products at once using JavaScript:
176
+
177
+ ```
178
+ {action: "navigate", payload: "https://shop.example.com/category/electronics"}
179
+ {action: "await_element", selector: ".product-grid"}
180
+
181
+ // Extract all products as structured data
182
+ {action: "eval", payload: `
183
+ Array.from(document.querySelectorAll('.product-card')).map(card => ({
184
+ name: card.querySelector('.product-name').textContent,
185
+ price: card.querySelector('.price').textContent,
186
+ image: card.querySelector('img').src,
187
+ url: card.querySelector('a').href
188
+ }))
189
+ `}
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Multi-Tab Workflows
195
+
196
+ ### Email Extraction
197
+
198
+ List tabs, then switch to the correct tab and extract data:
199
+
200
+ ```
201
+ // Find email tab
202
+ {action: "list_tabs"}
203
+
204
+ // Switch to tab 2 (from list_tabs output), then operate on active tab
205
+ {action: "switch_tab", payload: 2}
206
+ {action: "click", selector: "a[title*='Organization receipt']"}
207
+ {action: "await_element", selector: ".email-body"}
208
+
209
+ // Extract donation amount
210
+ {action: "extract", payload: "text", selector: ".donation-amount"}
211
+ ```
212
+
213
+ ### Price Comparison
214
+
215
+ Open multiple stores and compare prices:
216
+
217
+ ```
218
+ // Navigate first tab (already active)
219
+ {action: "navigate", payload: "https://store1.com/product"}
220
+
221
+ // Open additional tabs and navigate each
222
+ {action: "new_tab"}
223
+ {action: "navigate", payload: "https://store2.com/product"}
224
+
225
+ {action: "new_tab"}
226
+ {action: "navigate", payload: "https://store3.com/product"}
227
+
228
+ // Switch back to each tab and extract prices
229
+ {action: "switch_tab", payload: "store1.com"}
230
+ {action: "await_element", selector: ".price"}
231
+ {action: "extract", payload: "text", selector: ".price"}
232
+
233
+ {action: "switch_tab", payload: "store2.com"}
234
+ {action: "await_element", selector: ".price"}
235
+ {action: "extract", payload: "text", selector: ".price"}
236
+
237
+ {action: "switch_tab", payload: "store3.com"}
238
+ {action: "await_element", selector: ".price"}
239
+ {action: "extract", payload: "text", selector: ".price"}
240
+ ```
241
+
242
+ ### Cross-Reference Between Sites
243
+
244
+ Extract data from one site and use in another:
245
+
246
+ ```
247
+ // Get phone number from company site
248
+ {action: "navigate", payload: "https://company.com/contact"}
249
+ {action: "await_element", selector: ".phone"}
250
+ {action: "extract", payload: "text", selector: ".phone"}
251
+
252
+ // Store the result, then open verification site in a new tab
253
+ {action: "new_tab"}
254
+ {action: "navigate", payload: "https://lookup.com"}
255
+ {action: "await_element", selector: "input[name=phone]"}
256
+
257
+ // Fill with extracted phone number (new tab is already active)
258
+ {action: "type", selector: "input[name=phone]", payload: "<phone-from-previous-extract>"}
259
+ {action: "click", selector: "button.search"}
260
+ {action: "await_element", selector: ".results"}
261
+ {action: "extract", payload: "text", selector: ".verification-status"}
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Dynamic Content
267
+
268
+ ### Wait for AJAX to Complete
269
+
270
+ Wait for loading spinner to disappear:
271
+
272
+ ```
273
+ {action: "navigate", payload: "https://app.com/dashboard"}
274
+
275
+ // Wait for spinner to disappear using custom JavaScript
276
+ {action: "eval", payload: `
277
+ new Promise(resolve => {
278
+ const check = () => {
279
+ if (!document.querySelector('.spinner')) {
280
+ resolve(true);
281
+ } else {
282
+ setTimeout(check, 100);
283
+ }
284
+ };
285
+ check();
286
+ })
287
+ `}
288
+
289
+ // Now safe to extract
290
+ {action: "extract", payload: "text", selector: ".dashboard-data"}
291
+ ```
292
+
293
+ ### Infinite Scroll
294
+
295
+ Scroll to load more content:
296
+
297
+ ```
298
+ {action: "navigate", payload: "https://example.com/feed"}
299
+ {action: "await_element", selector: ".feed-item"}
300
+
301
+ // Scroll multiple times
302
+ {action: "eval", payload: "window.scrollTo(0, document.body.scrollHeight)"}
303
+ {action: "await_element", selector: ".feed-item", timeout: 2000}
304
+
305
+ {action: "eval", payload: "window.scrollTo(0, document.body.scrollHeight)"}
306
+ {action: "await_element", selector: ".feed-item", timeout: 2000}
307
+
308
+ {action: "eval", payload: "window.scrollTo(0, document.body.scrollHeight)"}
309
+ {action: "await_element", selector: ".feed-item", timeout: 2000}
310
+
311
+ // Count loaded items
312
+ {action: "eval", payload: "document.querySelectorAll('.feed-item').length"}
313
+ ```
314
+
315
+ ### Wait for Element to Become Enabled
316
+
317
+ Wait for button to be clickable:
318
+
319
+ ```
320
+ {action: "click", selector: "button.start"}
321
+
322
+ // Wait for continue button to enable
323
+ {action: "eval", payload: `
324
+ new Promise(resolve => {
325
+ const check = () => {
326
+ const btn = document.querySelector('button.continue');
327
+ if (btn && !btn.disabled) {
328
+ resolve(true);
329
+ } else {
330
+ setTimeout(check, 100);
331
+ }
332
+ };
333
+ check();
334
+ })
335
+ `}
336
+
337
+ {action: "click", selector: "button.continue"}
338
+ ```
339
+
340
+ ---
341
+
342
+ ## Dialogs
343
+
344
+ ### HTTP basic-auth (dialog surfaces during navigate)
345
+
346
+ When the page returns 401 + `WWW-Authenticate`, Chrome stages a basic-auth
347
+ dialog. The bridge intercepts the `Fetch.authRequired` event, holds the
348
+ navigation, and surfaces a dialog refusal — `navigate` throws with the
349
+ dialog grammar in the message. The response text contains `basic-auth`
350
+ and lists the `dialog::username`/`dialog::password`/`dialog::accept`
351
+ selectors.
352
+
353
+ ```
354
+ # Step 1: navigate fails with the dialog payload
355
+ {action: "navigate", payload: "http://localhost:8766/", timeout: 15000}
356
+ # (response includes "basic-auth", "dialog::username", "dialog::password")
357
+
358
+ # Step 2-4: stage credentials and submit
359
+ {action: "type", selector: "dialog::username", payload: "alice"}
360
+ {action: "type", selector: "dialog::password", payload: "secret"}
361
+ {action: "click", selector: "dialog::accept"}
362
+
363
+ # Step 5: the original navigation completes; the page is now loaded
364
+ {action: "extract", selector: "h1", payload: "text"}
365
+ # → "hi alice"
366
+ ```
367
+
368
+ ### JS confirm/alert dispatched by a click
369
+
370
+ A button whose `onclick` calls `confirm()` opens a dialog as soon as the
371
+ click event fires. The click itself may report a CDP timeout (Chrome
372
+ pauses the main thread on the dialog); that's expected. The bridge has
373
+ already populated `state.dialogs[sid]` and any subsequent page-targeted
374
+ call gets refused with the dialog grammar.
375
+
376
+ ```
377
+ {action: "navigate", payload: "<page with onclick=confirm('Proceed?')>"}
378
+ {action: "click", selector: "#ask"}
379
+ # Click times out — expected.
380
+
381
+ {action: "extract", selector: "#result", payload: "text"}
382
+ # Refused: response contains "Page is behind a dialog", "dialog::accept",
383
+ # "dialog::dismiss", and the prompt "Proceed?".
384
+
385
+ {action: "click", selector: "dialog::accept"}
386
+ # Dialog accepted; state.dialogs cleared eagerly.
387
+
388
+ {action: "eval", payload: "window.__userChoice"}
389
+ # → true
390
+ ```
391
+
392
+ ### Popup with synchronous dialog (Phase F headline case)
393
+
394
+ A page that opens a popup whose first inline script calls
395
+ `confirm()` works without races. The bridge attaches to the popup
396
+ target via `Target.setAutoAttach({waitForDebuggerOnStart: true})`,
397
+ installs the dialog shim, then resumes execution — so the synchronous
398
+ confirm is observed.
399
+
400
+ ```
401
+ {action: "navigate", payload: "http://localhost:8765/popup-opener.html"}
402
+ {action: "click", selector: "#open"} # opens window.open('popup.html')
403
+ {action: "list_tabs"} # popup is enumerated
404
+ {action: "switch_tab", payload: "Popup"} # route to the popup tab
405
+ {action: "extract", selector: "*", payload: "text"}
406
+ # Refused with dialog grammar — the popup's confirm was caught.
407
+
408
+ {action: "click", selector: "dialog::accept"}
409
+ {action: "eval", payload: "window.__userChoice"}
410
+ # → true
411
+ ```
412
+
413
+ ## Recovery
414
+
415
+ ### Chrome killed externally
416
+
417
+ If something kills your Chrome (`kill -9 <pid>`, OOM killer, a user
418
+ closing the headed window), the bridge auto-restarts on the next page
419
+ action. The response is prefixed with a banner so you know the previous
420
+ URL/tab state is gone.
421
+
422
+ ```
423
+ {action: "navigate", payload: "https://example.com"}
424
+ {action: "extract", selector: "h1", payload: "text"} # → "Example Domain"
425
+ {action: "browser_mode"} # records pid=N
426
+
427
+ # (from your shell or another process: kill -9 N)
428
+
429
+ {action: "navigate", payload: "https://example.com"}
430
+ # Response starts with:
431
+ # [Chrome auto-restarted; URL reset to about:blank. Re-navigate to continue.]
432
+ # Navigated to https://example.com
433
+ # ...
434
+ ```
435
+
436
+ `browser_mode` also reports the real PID even when the bridge adopted a
437
+ Chrome it didn't spawn (a leftover from a previous MCP session). So
438
+ "get pid, kill -9 it, watch the restart" works regardless of how Chrome
439
+ got there.
440
+
441
+ ### Explicit kill + restart cycle
442
+
443
+ ```
444
+ {action: "kill_chrome"} # Chrome killed.
445
+ {action: "restart_chrome"} # Chrome restarted in headless mode.
446
+ {action: "navigate", payload: "data:text/html,<h1>fresh</h1>"}
447
+ ```
448
+
449
+ ## Multi-MCP isolation
450
+
451
+ By default the bridge handles parallel MCP servers on the same host
452
+ automatically: the first claims `moe-glass:9222`, the next
453
+ silently falls through to `moe-glass-2:9223`, then `-3:9224`,
454
+ etc. Each MCP drives its own Chrome with its own profile directory.
455
+
456
+ To intentionally **share** a Chrome between processes (e.g., a
457
+ long-lived `chrome-ws start` from the shell + a Claude MCP attaching to
458
+ it), pick a fixed profile name on both sides:
459
+
460
+ ```
461
+ # Shell:
462
+ CHROME_WS_PROFILE=shared chrome-ws start
463
+
464
+ # In the MCP, on first call:
465
+ {action: "set_profile", payload: "shared"}
466
+ {action: "navigate", payload: "https://example.com"}
467
+ # Reconnects to the shell-started Chrome — same tabs, same cookies.
468
+ ```
469
+
470
+ Either set `CHROME_WS_PROFILE=shared` in the MCP's environment, or call
471
+ `set_profile` at runtime. Both mark the profile as explicit, so the
472
+ bridge shares rather than disambiguates.
473
+
474
+ ---
475
+
476
+ ## Advanced Patterns
477
+
478
+ ### Multi-Step Workflow
479
+
480
+ Complete booking flow with validation:
481
+
482
+ ```
483
+ {action: "navigate", payload: "https://booking.example.com"}
484
+
485
+ // Search
486
+ {action: "type", selector: "input[name=destination]", payload: "San Francisco"}
487
+ {action: "type", selector: "input[name=checkin]", payload: "2025-12-01"}
488
+ {action: "click", selector: "button.search"}
489
+
490
+ // Select hotel
491
+ {action: "await_element", selector: ".hotel-results"}
492
+ {action: "click", selector: ".hotel-card:first-child .select"}
493
+
494
+ // Choose room
495
+ {action: "await_element", selector: ".room-options"}
496
+ {action: "click", selector: ".room[data-type=deluxe] .book"}
497
+
498
+ // Fill guest info
499
+ {action: "await_element", selector: "form.guest-info"}
500
+ {action: "type", selector: "input[name=firstName]", payload: "Jane"}
501
+ {action: "type", selector: "input[name=lastName]", payload: "Smith"}
502
+ {action: "type", selector: "input[name=email]", payload: "jane@example.com"}
503
+
504
+ // Review (don't complete)
505
+ {action: "click", selector: "button.review"}
506
+ {action: "await_element", selector: ".summary"}
507
+
508
+ // Extract confirmation details
509
+ {action: "extract", payload: "text", selector: ".hotel-name"}
510
+ {action: "extract", payload: "text", selector: ".total-price"}
511
+ ```
512
+
513
+ ### Cookies and LocalStorage
514
+
515
+ Access browser storage:
516
+
517
+ ```
518
+ // Get cookies
519
+ {action: "eval", payload: "document.cookie"}
520
+
521
+ // Set cookie
522
+ {action: "eval", payload: "document.cookie = 'theme=dark; path=/'"}
523
+
524
+ // Get localStorage
525
+ {action: "eval", payload: "JSON.stringify(localStorage)"}
526
+
527
+ // Set localStorage
528
+ {action: "eval", payload: "localStorage.setItem('lastVisit', new Date().toISOString())"}
529
+ ```
530
+
531
+ ### Handle Modals
532
+
533
+ Interact with modal dialogs:
534
+
535
+ ```
536
+ {action: "click", selector: "button.open-modal"}
537
+ {action: "await_element", selector: ".modal.visible"}
538
+
539
+ // Fill modal form
540
+ {action: "type", selector: ".modal input[name=username]", payload: "testuser"}
541
+ {action: "click", selector: ".modal button.submit"}
542
+
543
+ // Wait for modal to close
544
+ {action: "eval", payload: `
545
+ new Promise(resolve => {
546
+ const check = () => {
547
+ if (!document.querySelector('.modal.visible')) {
548
+ resolve(true);
549
+ } else {
550
+ setTimeout(check, 100);
551
+ }
552
+ };
553
+ check();
554
+ })
555
+ `}
556
+ ```
557
+
558
+ ### Screenshots
559
+
560
+ Capture full page or specific elements:
561
+
562
+ ```
563
+ // Full page screenshot
564
+ {action: "navigate", payload: "https://example.com"}
565
+ {action: "await_element", selector: "body"}
566
+ {action: "screenshot", payload: "/tmp/page.png"}
567
+
568
+ // Element-specific screenshot
569
+ {action: "screenshot", payload: "/tmp/element.png", selector: ".important-section"}
570
+ ```
571
+
572
+ ### Check Element State
573
+
574
+ Verify element properties before interaction:
575
+
576
+ ```
577
+ // Check if button is disabled
578
+ {action: "eval", payload: "document.querySelector('button.submit').disabled"}
579
+
580
+ // Check if element is visible
581
+ {action: "eval", payload: "!!document.querySelector('.important-button') && window.getComputedStyle(document.querySelector('.important-button')).display !== 'none'"}
582
+
583
+ // Check element exists
584
+ {action: "eval", payload: "!!document.querySelector('.important-button')"}
585
+ ```
586
+
587
+ ---
588
+
589
+ ## Tips and Best Practices
590
+
591
+ ### Always Wait Before Interaction
592
+
593
+ Don't interact with elements immediately after navigation:
594
+
595
+ ```
596
+ // BAD - might fail if page slow to load
597
+ {action: "navigate", payload: "https://example.com"}
598
+ {action: "click", selector: "button"} // May fail!
599
+
600
+ // GOOD - wait for element first
601
+ {action: "navigate", payload: "https://example.com"}
602
+ {action: "await_element", selector: "button"}
603
+ {action: "click", selector: "button"}
604
+ ```
605
+
606
+ ### Use Specific Selectors
607
+
608
+ Avoid generic selectors that match multiple elements:
609
+
610
+ ```
611
+ // BAD - matches first button on page
612
+ {action: "click", selector: "button"}
613
+
614
+ // GOOD - specific selector
615
+ {action: "click", selector: "button[type=submit]"}
616
+ {action: "click", selector: "button.login-button"}
617
+ {action: "click", selector: "#submit-form"}
618
+ ```
619
+
620
+ ### Verify Selectors First
621
+
622
+ Check page structure before building workflow:
623
+
624
+ ```
625
+ // Check page HTML
626
+ {action: "extract", payload: "html"}
627
+
628
+ // Or check specific element
629
+ {action: "extract", payload: "html", selector: "form"}
630
+ ```
631
+
632
+ ### Handle Dynamic Content
633
+
634
+ Wait for content to load before extraction:
635
+
636
+ ```
637
+ // BAD - tries to extract before content loads
638
+ {action: "navigate", payload: "https://app.com"}
639
+ {action: "extract", payload: "text", selector: ".user-name"} // Might be empty!
640
+
641
+ // GOOD - wait for content
642
+ {action: "navigate", payload: "https://app.com"}
643
+ {action: "await_element", selector: ".user-name"}
644
+ {action: "extract", payload: "text", selector: ".user-name"}
645
+ ```
646
+
647
+ ### Use \n for Form Submission
648
+
649
+ Append newline to auto-submit forms:
650
+
651
+ ```
652
+ // Submit search without explicit click
653
+ {action: "type", selector: "#search-input", payload: "my query\n"}
654
+
655
+ // Submit login form
656
+ {action: "type", selector: "input[name=email]", payload: "user@example.com"}
657
+ {action: "type", selector: "input[name=password]", payload: "password123\n"}
658
+ ```
659
+
660
+ ---
661
+
662
+ ## Common Pitfalls
663
+
664
+ ### Don't Rely on Tab Indices
665
+
666
+ Tab indices change when tabs close — use URL or title substrings for reliable switching:
667
+
668
+ ```
669
+ // BAD - index might be stale after closing tabs
670
+ {action: "switch_tab", payload: 2}
671
+ {action: "click", selector: "button"}
672
+
673
+ // GOOD - switch by URL or title substring (stable across tab changes)
674
+ {action: "switch_tab", payload: "example.com"}
675
+ {action: "click", selector: "button"}
676
+
677
+ // Or list tabs first to confirm the index
678
+ {action: "list_tabs"}
679
+ {action: "switch_tab", payload: 2}
680
+ {action: "click", selector: "button"}
681
+ ```
682
+
683
+ ### Increase Timeout for Slow Pages
684
+
685
+ Default timeout is 5000ms, increase if needed:
686
+
687
+ ```
688
+ // For slow-loading elements
689
+ {action: "await_element", selector: ".lazy-content", timeout: 30000}
690
+
691
+ // For slow AJAX requests
692
+ {action: "await_text", payload: "Data loaded", timeout: 15000}
693
+ ```
694
+
695
+ ### Extract Structured Data with JavaScript
696
+
697
+ For complex data extraction, use JavaScript evaluation:
698
+
699
+ ```
700
+ // Instead of multiple extract calls, use one eval
701
+ {action: "eval", payload: `
702
+ {
703
+ title: document.querySelector('h1').textContent,
704
+ author: document.querySelector('.author').textContent,
705
+ date: document.querySelector('time').textContent,
706
+ links: Array.from(document.querySelectorAll('a')).map(a => a.href)
707
+ }
708
+ `}
709
+ ```
710
+
711
+ ---
712
+
713
+ ## Reference
714
+
715
+ - [SKILL.md](SKILL.md) - Complete tool reference
716
+ - [COMMANDLINE-USAGE.md](COMMANDLINE-USAGE.md) - Command-line bash examples
717
+ - [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) - Full protocol documentation