cmdop-browser 1.0.1__py3-none-any.whl

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 (83) hide show
  1. _skill.py +792 -0
  2. cmdop_browser-1.0.1.dist-info/METADATA +20 -0
  3. cmdop_browser-1.0.1.dist-info/RECORD +83 -0
  4. cmdop_browser-1.0.1.dist-info/WHEEL +4 -0
  5. cmdop_browser-1.0.1.dist-info/entry_points.txt +2 -0
  6. src/__init__.py +0 -0
  7. src/browsers/__init__.py +32 -0
  8. src/browsers/addon.py +405 -0
  9. src/browsers/base.py +136 -0
  10. src/browsers/camoufox.py +239 -0
  11. src/browsers/install.py +93 -0
  12. src/browsers/persistent.py +188 -0
  13. src/browsers/playwright.py +308 -0
  14. src/cdn/__init__.py +3 -0
  15. src/cdn/client.py +44 -0
  16. src/config.py +45 -0
  17. src/extraction/__init__.py +11 -0
  18. src/extraction/extractor.py +170 -0
  19. src/extraction/jsonld.py +112 -0
  20. src/extraction/meta.py +120 -0
  21. src/extraction/table.py +160 -0
  22. src/geo/__init__.py +9 -0
  23. src/network/__init__.py +3 -0
  24. src/network/capturer.py +172 -0
  25. src/perception/__init__.py +15 -0
  26. src/perception/axtree.py +225 -0
  27. src/perception/html_cleaner.py +188 -0
  28. src/perception/markdown.py +162 -0
  29. src/perception/pipeline.py +443 -0
  30. src/platforms/__init__.py +6 -0
  31. src/platforms/detector.py +16 -0
  32. src/platforms/facebook.py +18 -0
  33. src/platforms/linkedin.py +20 -0
  34. src/platforms/twitter.py +21 -0
  35. src/pool/__init__.py +4 -0
  36. src/pool/pool.py +231 -0
  37. src/profiles/__init__.py +3 -0
  38. src/profiles/store.py +135 -0
  39. src/recovery/__init__.py +7 -0
  40. src/recovery/manager.py +166 -0
  41. src/recovery/watchdog.py +134 -0
  42. src/stealth/__init__.py +23 -0
  43. src/stealth/behaviors/__init__.py +19 -0
  44. src/stealth/behaviors/fitts.py +37 -0
  45. src/stealth/behaviors/keyboard.py +86 -0
  46. src/stealth/behaviors/math_utils.py +36 -0
  47. src/stealth/behaviors/mouse.py +135 -0
  48. src/stealth/behaviors/ratelimit.py +35 -0
  49. src/stealth/behaviors/scroll.py +67 -0
  50. src/stealth/behaviors/targeting.py +29 -0
  51. src/stealth/behaviors/timing.py +79 -0
  52. src/stealth/fingerprints/__init__.py +14 -0
  53. src/stealth/fingerprints/fingerprint.py +120 -0
  54. src/stealth/manager.py +72 -0
  55. src/stealth/patches/__init__.py +15 -0
  56. src/stealth/patches/audio.py +41 -0
  57. src/stealth/patches/canvas.py +92 -0
  58. src/stealth/patches/clientrects.py +60 -0
  59. src/stealth/patches/fonts.py +32 -0
  60. src/stealth/patches/navigator.py +223 -0
  61. src/stealth/patches/webgl.py +67 -0
  62. src/tabs/__init__.py +3 -0
  63. src/tabs/manager.py +227 -0
  64. src/tools/__init__.py +169 -0
  65. src/tools/advanced.py +147 -0
  66. src/tools/control.py +35 -0
  67. src/tools/cookies.py +58 -0
  68. src/tools/data.py +142 -0
  69. src/tools/extraction.py +125 -0
  70. src/tools/interaction.py +59 -0
  71. src/tools/media.py +69 -0
  72. src/tools/models.py +373 -0
  73. src/tools/navigation.py +59 -0
  74. src/tools/network.py +103 -0
  75. src/tools/perception.py +70 -0
  76. src/tools/pool.py +49 -0
  77. src/tools/profiles.py +98 -0
  78. src/tools/tabs.py +144 -0
  79. src/tools/turnstile.py +66 -0
  80. src/tools/visual.py +33 -0
  81. src/tools/wait.py +32 -0
  82. src/turnstile/__init__.py +17 -0
  83. src/turnstile/solver.py +212 -0
_skill.py ADDED
@@ -0,0 +1,792 @@
1
+ """cmdop-browser — CMDOP skill wrapper for browser automation."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any
6
+
7
+ from cmdop_skill import Arg, Skill
8
+
9
+ from src.browsers import create_browser
10
+ from src.browsers.base import BrowserDriver
11
+ from src.config import BrowserConfig
12
+ from src.tools import ALL_TOOLS
13
+
14
+ skill = Skill(
15
+ name="browser",
16
+ description="Playwright + Camoufox browser automation — 56 tools, stealth mode, network capture, profiles",
17
+ version="1.0.0",
18
+ )
19
+
20
+ # ── Singleton browser ────────────────────────────────────────────────────────
21
+
22
+ _browser: BrowserDriver | None = None
23
+
24
+
25
+ async def _get_browser() -> BrowserDriver:
26
+ global _browser
27
+ if _browser is not None and _browser.is_running():
28
+ return _browser
29
+ config = BrowserConfig()
30
+ _browser = create_browser(config)
31
+ await _browser.start()
32
+ return _browser
33
+
34
+
35
+ @skill.teardown
36
+ async def _teardown() -> None:
37
+ global _browser
38
+ if _browser is not None and _browser.is_running():
39
+ await _browser.close()
40
+ _browser = None
41
+
42
+
43
+ # ── Helpers ──────────────────────────────────────────────────────────────────
44
+
45
+ def _to_dict(result: Any) -> dict:
46
+ from pydantic import BaseModel
47
+ if isinstance(result, BaseModel):
48
+ return result.model_dump()
49
+ if isinstance(result, dict):
50
+ return result
51
+ return {"ok": True, "result": result}
52
+
53
+
54
+ # ── Setup ────────────────────────────────────────────────────────────────────
55
+
56
+ @skill.command
57
+ def setup(
58
+ force: bool = Arg("--force", action="store_true", default=False, help="Re-install even if already present"),
59
+ ) -> dict:
60
+ """Download and install browser binaries."""
61
+ from src.browsers.install import install_browsers
62
+ from src.config import BROWSER_PROVIDER
63
+ provider = BROWSER_PROVIDER or "all"
64
+ install_browsers(provider=provider, force=force)
65
+ return {"provider": provider}
66
+
67
+
68
+ # ── Navigation (6) ───────────────────────────────────────────────────────────
69
+
70
+ @skill.command
71
+ async def navigate(
72
+ url: str = Arg(help="URL to navigate to", required=True),
73
+ timeout: float = Arg("--timeout", default=30.0, help="Navigation timeout in seconds"),
74
+ ) -> dict:
75
+ """Navigate to a URL."""
76
+ browser = await _get_browser()
77
+ result = await ALL_TOOLS["navigate"](browser, url=url, timeout=timeout)
78
+ return _to_dict(result)
79
+
80
+
81
+ @skill.command
82
+ async def refresh() -> dict:
83
+ """Refresh the current page."""
84
+ browser = await _get_browser()
85
+ return _to_dict(await ALL_TOOLS["refresh"](browser))
86
+
87
+
88
+ @skill.command
89
+ async def back() -> dict:
90
+ """Go back in browser history."""
91
+ browser = await _get_browser()
92
+ return _to_dict(await ALL_TOOLS["back"](browser))
93
+
94
+
95
+ @skill.command
96
+ async def forward() -> dict:
97
+ """Go forward in browser history."""
98
+ browser = await _get_browser()
99
+ return _to_dict(await ALL_TOOLS["forward"](browser))
100
+
101
+
102
+ @skill.command
103
+ async def get_url() -> dict:
104
+ """Get the current page URL."""
105
+ browser = await _get_browser()
106
+ return _to_dict(await ALL_TOOLS["get_url"](browser))
107
+
108
+
109
+ @skill.command
110
+ async def get_title() -> dict:
111
+ """Get the current page title."""
112
+ browser = await _get_browser()
113
+ return _to_dict(await ALL_TOOLS["get_title"](browser))
114
+
115
+
116
+ # ── Interaction (7) ──────────────────────────────────────────────────────────
117
+
118
+ @skill.command
119
+ async def click(
120
+ selector: str = Arg(help="CSS selector to click", required=True),
121
+ human: bool = Arg("--human", action="store_true", default=False, help="Move cursor like a human"),
122
+ ) -> dict:
123
+ """Click an element."""
124
+ browser = await _get_browser()
125
+ return _to_dict(await ALL_TOOLS["click"](browser, selector=selector, human=human))
126
+
127
+
128
+ @skill.command
129
+ async def type_text(
130
+ selector: str = Arg(help="CSS selector to type into", required=True),
131
+ text: str = Arg(help="Text to type", required=True),
132
+ clear: bool = Arg("--clear", action="store_true", default=False, help="Clear field before typing"),
133
+ ) -> dict:
134
+ """Type text into an element."""
135
+ browser = await _get_browser()
136
+ return _to_dict(await ALL_TOOLS["type"](browser, selector=selector, text=text, clear=clear))
137
+
138
+
139
+ @skill.command
140
+ async def type_human(
141
+ selector: str = Arg(help="CSS selector to type into", required=True),
142
+ text: str = Arg(help="Text to type with human-like delays", required=True),
143
+ ) -> dict:
144
+ """Type text with human-like timing."""
145
+ browser = await _get_browser()
146
+ return _to_dict(await ALL_TOOLS["type_human"](browser, selector=selector, text=text))
147
+
148
+
149
+ @skill.command
150
+ async def clear(
151
+ selector: str = Arg(help="CSS selector to clear", required=True),
152
+ ) -> dict:
153
+ """Clear an input field."""
154
+ browser = await _get_browser()
155
+ return _to_dict(await ALL_TOOLS["clear"](browser, selector=selector))
156
+
157
+
158
+ @skill.command
159
+ async def hover(
160
+ selector: str = Arg(help="CSS selector to hover over", required=True),
161
+ ) -> dict:
162
+ """Hover over an element."""
163
+ browser = await _get_browser()
164
+ return _to_dict(await ALL_TOOLS["hover"](browser, selector=selector))
165
+
166
+
167
+ @skill.command
168
+ async def press_key(
169
+ key: str = Arg(help="Key to press", required=True),
170
+ modifiers: str = Arg("--modifiers", default=None, nargs="*", help="Modifier keys"),
171
+ ) -> dict:
172
+ """Press a keyboard key."""
173
+ browser = await _get_browser()
174
+ return _to_dict(await ALL_TOOLS["press_key"](browser, key=key, modifiers=modifiers))
175
+
176
+
177
+ @skill.command
178
+ async def mouse_move(
179
+ x: int = Arg(help="X coordinate", required=True),
180
+ y: int = Arg(help="Y coordinate", required=True),
181
+ steps: int = Arg("--steps", default=20, help="Number of intermediate steps"),
182
+ ) -> dict:
183
+ """Move the mouse cursor."""
184
+ browser = await _get_browser()
185
+ return _to_dict(await ALL_TOOLS["mouse_move"](browser, x=x, y=y, steps=steps))
186
+
187
+
188
+ # ── Wait (3) ─────────────────────────────────────────────────────────────────
189
+
190
+ @skill.command
191
+ async def wait_for_selector(
192
+ selector: str = Arg(help="CSS selector to wait for", required=True),
193
+ timeout: float = Arg("--timeout", default=45.0, help="Timeout in seconds"),
194
+ visible: bool = Arg("--visible", action="store_true", default=True, help="Wait for visible element"),
195
+ ) -> dict:
196
+ """Wait for an element to appear."""
197
+ browser = await _get_browser()
198
+ return _to_dict(await ALL_TOOLS["wait_for_selector"](browser, selector=selector, timeout=timeout, visible=visible))
199
+
200
+
201
+ @skill.command
202
+ async def wait_for_idle(
203
+ timeout: float = Arg("--timeout", default=5.0, help="Timeout in seconds"),
204
+ ) -> dict:
205
+ """Wait for the page to become idle."""
206
+ browser = await _get_browser()
207
+ return _to_dict(await ALL_TOOLS["wait_for_idle"](browser, timeout=timeout))
208
+
209
+
210
+ @skill.command
211
+ async def wait(
212
+ seconds: float = Arg(help="Seconds to wait", required=True),
213
+ ) -> dict:
214
+ """Wait for a specified number of seconds."""
215
+ return _to_dict(await ALL_TOOLS["wait"](seconds=seconds))
216
+
217
+
218
+ # ── Control (3) ──────────────────────────────────────────────────────────────
219
+
220
+ @skill.command
221
+ async def set_viewport(
222
+ width: int = Arg(help="Viewport width", required=True),
223
+ height: int = Arg(help="Viewport height", required=True),
224
+ ) -> dict:
225
+ """Set the browser viewport size."""
226
+ browser = await _get_browser()
227
+ return _to_dict(await ALL_TOOLS["set_viewport"](browser, width=width, height=height))
228
+
229
+
230
+ @skill.command
231
+ async def scroll(
232
+ direction: str = Arg(help="Scroll direction", required=True, choices=["up", "down", "left", "right"]),
233
+ amount: int = Arg("--amount", default=300, help="Scroll amount in pixels"),
234
+ selector: str = Arg("--selector", default="", help="Element to scroll within"),
235
+ smooth: bool = Arg("--no-smooth", dest="smooth", action="store_false", default=True, help="Disable smooth scrolling"),
236
+ ) -> dict:
237
+ """Scroll the page or element."""
238
+ browser = await _get_browser()
239
+ return _to_dict(await ALL_TOOLS["scroll"](browser, direction=direction, amount=amount, selector=selector, smooth=smooth))
240
+
241
+
242
+ @skill.command
243
+ async def close_browser() -> dict:
244
+ """Close the browser."""
245
+ browser = await _get_browser()
246
+ return _to_dict(await ALL_TOOLS["close_browser"](browser))
247
+
248
+
249
+ # ── Data (9) ─────────────────────────────────────────────────────────────────
250
+
251
+ @skill.command
252
+ async def screenshot(
253
+ full_page: bool = Arg("--full-page", action="store_true", default=False, help="Capture full page"),
254
+ selector: str = Arg("--selector", default="", help="Element to screenshot"),
255
+ ) -> dict:
256
+ """Take a screenshot."""
257
+ browser = await _get_browser()
258
+ return _to_dict(await ALL_TOOLS["screenshot"](browser, full_page=full_page, selector=selector))
259
+
260
+
261
+ @skill.command
262
+ async def save_screenshot(
263
+ path: str = Arg("--path", default="", help="File path to save screenshot"),
264
+ full_page: bool = Arg("--full-page", action="store_true", default=False, help="Capture full page"),
265
+ ) -> dict:
266
+ """Save a screenshot to a file."""
267
+ browser = await _get_browser()
268
+ return _to_dict(await ALL_TOOLS["save_screenshot"](browser, path=path, full_page=full_page))
269
+
270
+
271
+ @skill.command
272
+ async def get_html(
273
+ mode: str = Arg("--mode", default="agent", choices=["agent", "raw"], help="Output mode"),
274
+ selector: str = Arg("--selector", default="", help="Element selector"),
275
+ ) -> dict:
276
+ """Get page HTML."""
277
+ browser = await _get_browser()
278
+ return _to_dict(await ALL_TOOLS["get_html"](browser, mode=mode, selector=selector))
279
+
280
+
281
+ @skill.command
282
+ async def get_text(
283
+ selector: str = Arg(help="CSS selector to extract text from", required=True),
284
+ ) -> dict:
285
+ """Get text content of an element."""
286
+ browser = await _get_browser()
287
+ return _to_dict(await ALL_TOOLS["get_text"](browser, selector=selector))
288
+
289
+
290
+ @skill.command
291
+ async def execute_script(
292
+ script: str = Arg(help="JavaScript code to execute", required=True),
293
+ args: str = Arg("--args", default=None, nargs="*", help="Script arguments"),
294
+ ) -> dict:
295
+ """Execute JavaScript in the browser."""
296
+ browser = await _get_browser()
297
+ return _to_dict(await ALL_TOOLS["execute_script"](browser, script=script, args=args))
298
+
299
+
300
+ @skill.command
301
+ async def extract(
302
+ selector: str = Arg(help="CSS selector to extract from", required=True),
303
+ attribute: str = Arg("--attribute", default="text", help="Attribute to extract"),
304
+ limit: int = Arg("--limit", default=100, help="Maximum number of elements"),
305
+ ) -> dict:
306
+ """Extract data from elements matching a selector."""
307
+ browser = await _get_browser()
308
+ return _to_dict(await ALL_TOOLS["extract"](browser, selector=selector, attribute=attribute, limit=limit))
309
+
310
+
311
+ @skill.command
312
+ async def get_attribute(
313
+ selector: str = Arg(help="CSS selector", required=True),
314
+ attribute: str = Arg(help="Attribute name", required=True),
315
+ ) -> dict:
316
+ """Get an attribute value from an element."""
317
+ browser = await _get_browser()
318
+ return _to_dict(await ALL_TOOLS["get_attribute"](browser, selector=selector, attribute=attribute))
319
+
320
+
321
+ @skill.command
322
+ async def exists(
323
+ selector: str = Arg(help="CSS selector to check", required=True),
324
+ ) -> dict:
325
+ """Check if an element exists."""
326
+ browser = await _get_browser()
327
+ return _to_dict(await ALL_TOOLS["exists"](browser, selector=selector))
328
+
329
+
330
+ @skill.command
331
+ async def count(
332
+ selector: str = Arg(help="CSS selector to count", required=True),
333
+ ) -> dict:
334
+ """Count elements matching a selector."""
335
+ browser = await _get_browser()
336
+ return _to_dict(await ALL_TOOLS["count"](browser, selector=selector))
337
+
338
+
339
+ # ── Perception (4) ───────────────────────────────────────────────────────────
340
+
341
+ @skill.command
342
+ async def get_page(
343
+ mode: str = Arg("--mode", default="full", help="Page capture mode"),
344
+ ) -> dict:
345
+ """Get annotated page HTML for LLM automation."""
346
+ browser = await _get_browser()
347
+ return _to_dict(await ALL_TOOLS["get_page"](browser, mode=mode))
348
+
349
+
350
+ @skill.command
351
+ async def get_state() -> dict:
352
+ """Get text representation of page state."""
353
+ browser = await _get_browser()
354
+ return _to_dict(await ALL_TOOLS["get_state"](browser))
355
+
356
+
357
+ @skill.command
358
+ async def get_interactive(
359
+ visible_only: bool = Arg("--visible-only", action="store_true", default=True, help="Only visible elements"),
360
+ ) -> dict:
361
+ """Get list of interactive elements."""
362
+ browser = await _get_browser()
363
+ return _to_dict(await ALL_TOOLS["get_interactive"](browser, visible_only=visible_only))
364
+
365
+
366
+ @skill.command
367
+ async def get_diff() -> dict:
368
+ """Get page changes since last call."""
369
+ browser = await _get_browser()
370
+ return _to_dict(await ALL_TOOLS["get_diff"](browser))
371
+
372
+
373
+ # ── Extraction (4) ───────────────────────────────────────────────────────────
374
+
375
+ @skill.command
376
+ async def validate_selectors(
377
+ selectors: str = Arg(help="CSS selectors to validate", required=True, nargs="+"),
378
+ ) -> dict:
379
+ """Validate CSS selectors against the page."""
380
+ browser = await _get_browser()
381
+ return _to_dict(await ALL_TOOLS["validate_selectors"](browser, selectors=selectors))
382
+
383
+
384
+ @skill.command
385
+ async def extract_regex(
386
+ pattern: str = Arg(help="Regex pattern", required=True),
387
+ source: str = Arg("--source", default="text", choices=["text", "html"], help="Source to search"),
388
+ selector: str = Arg("--selector", default="", help="Limit to element"),
389
+ group: int = Arg("--group", default=0, help="Capture group number"),
390
+ ) -> dict:
391
+ """Extract text matching a regex pattern."""
392
+ browser = await _get_browser()
393
+ return _to_dict(await ALL_TOOLS["extract_regex"](browser, pattern=pattern, source=source, selector=selector, group=group))
394
+
395
+
396
+ @skill.command
397
+ async def extract_data(
398
+ mappings: str = Arg(help='JSON mapping: {"field": "selector"}', required=True),
399
+ attribute: str = Arg("--attribute", default="text", help="Attribute to extract"),
400
+ multiple: bool = Arg("--multiple", action="store_true", default=False, help="Extract multiple matches"),
401
+ ) -> dict:
402
+ """Extract structured data using selector mappings."""
403
+ browser = await _get_browser()
404
+ parsed = json.loads(mappings)
405
+ return _to_dict(await ALL_TOOLS["extract_data"](browser, mappings=parsed, attribute=attribute, multiple=multiple))
406
+
407
+
408
+ @skill.command
409
+ async def check_regex(
410
+ selector: str = Arg(help="CSS selector", required=True),
411
+ pattern: str = Arg(help="Regex pattern to check", required=True),
412
+ attribute: str = Arg("--attribute", default="text", help="Attribute to check against"),
413
+ ) -> dict:
414
+ """Check if element content matches a regex."""
415
+ browser = await _get_browser()
416
+ return _to_dict(await ALL_TOOLS["check_regex"](browser, selector=selector, pattern=pattern, attribute=attribute))
417
+
418
+
419
+ # ── Cookies (1) ──────────────────────────────────────────────────────────────
420
+
421
+ @skill.command
422
+ async def import_cookies(
423
+ domain: str = Arg(help="Domain to import cookies for", required=True),
424
+ source: str = Arg("--source", default="chrome", help="Cookie source"),
425
+ ) -> dict:
426
+ """Import cookies from local browser."""
427
+ browser = await _get_browser()
428
+ return _to_dict(await ALL_TOOLS["import_cookies"](browser, domain=domain, source=source))
429
+
430
+
431
+ # ── Media (2) ────────────────────────────────────────────────────────────────
432
+
433
+ @skill.command
434
+ async def extract_video_urls(
435
+ wait_for_play: bool = Arg("--wait-for-play", action="store_true", default=False, help="Wait for video to play"),
436
+ ) -> dict:
437
+ """Extract video source URLs from the page."""
438
+ browser = await _get_browser()
439
+ return _to_dict(await ALL_TOOLS["extract_video_urls"](browser, wait_for_play=wait_for_play))
440
+
441
+
442
+ @skill.command
443
+ async def get_video_info(
444
+ selector: str = Arg("--selector", default="video", help="Video element selector"),
445
+ ) -> dict:
446
+ """Get video metadata."""
447
+ browser = await _get_browser()
448
+ return _to_dict(await ALL_TOOLS["get_video_info"](browser, selector=selector))
449
+
450
+
451
+ # ── Profiles (6) ─────────────────────────────────────────────────────────────
452
+
453
+ @skill.command
454
+ async def create_profile(
455
+ name: str = Arg("--name", default="", help="Profile name"),
456
+ ) -> dict:
457
+ """Create a new browser profile."""
458
+ browser = await _get_browser()
459
+ return _to_dict(await ALL_TOOLS["create_profile"](browser, name=name))
460
+
461
+
462
+ @skill.command
463
+ async def switch_profile(
464
+ profile_id: str = Arg(help="Profile ID to switch to", required=True),
465
+ ) -> dict:
466
+ """Switch to a browser profile."""
467
+ browser = await _get_browser()
468
+ return _to_dict(await ALL_TOOLS["switch_profile"](browser, profile_id=profile_id))
469
+
470
+
471
+ @skill.command
472
+ async def list_profiles() -> dict:
473
+ """List all browser profiles."""
474
+ browser = await _get_browser()
475
+ return _to_dict(await ALL_TOOLS["list_profiles"](browser))
476
+
477
+
478
+ @skill.command
479
+ async def current_profile() -> dict:
480
+ """Get the current browser profile."""
481
+ browser = await _get_browser()
482
+ return _to_dict(await ALL_TOOLS["current_profile"](browser))
483
+
484
+
485
+ @skill.command
486
+ async def save_profile() -> dict:
487
+ """Save the current browser profile."""
488
+ browser = await _get_browser()
489
+ return _to_dict(await ALL_TOOLS["save_profile"](browser))
490
+
491
+
492
+ @skill.command
493
+ async def delete_profile(
494
+ profile_id: str = Arg(help="Profile ID to delete", required=True),
495
+ ) -> dict:
496
+ """Delete a browser profile."""
497
+ browser = await _get_browser()
498
+ return _to_dict(await ALL_TOOLS["delete_profile"](browser, profile_id=profile_id))
499
+
500
+
501
+ # ── Visual (1) ───────────────────────────────────────────────────────────────
502
+
503
+ @skill.command
504
+ async def show_toast(
505
+ message: str = Arg(help="Toast message", required=True),
506
+ type: str = Arg("--type", default="info", choices=["info", "success", "warning", "error"], help="Toast type"),
507
+ duration: int = Arg("--duration", default=3000, help="Duration in milliseconds"),
508
+ ) -> dict:
509
+ """Show a toast notification in the browser."""
510
+ browser = await _get_browser()
511
+ return _to_dict(await ALL_TOOLS["show_toast"](browser, message=message, type=type, duration=duration))
512
+
513
+
514
+ # ── Network (6) ──────────────────────────────────────────────────────────────
515
+
516
+ @skill.command
517
+ async def network_enable(
518
+ enabled: bool = Arg("--enabled", action="store_true", default=True, help="Enable network capture"),
519
+ ) -> dict:
520
+ """Enable or disable network request capture."""
521
+ browser = await _get_browser()
522
+ return _to_dict(await ALL_TOOLS["network_enable"](browser, enabled=enabled))
523
+
524
+
525
+ @skill.command
526
+ async def network_get(
527
+ url_pattern: str = Arg("--url-pattern", default="", help="Filter by URL pattern"),
528
+ methods: str = Arg("--methods", default=None, nargs="*", help="Filter by HTTP methods"),
529
+ types: str = Arg("--types", default=None, nargs="*", help="Filter by request types"),
530
+ limit: int = Arg("--limit", default=100, help="Maximum number of requests"),
531
+ ) -> dict:
532
+ """Get captured network requests."""
533
+ browser = await _get_browser()
534
+ return _to_dict(await ALL_TOOLS["network_get"](browser, url_pattern=url_pattern, methods=methods, types=types, limit=limit))
535
+
536
+
537
+ @skill.command
538
+ async def network_clear() -> dict:
539
+ """Clear captured network requests."""
540
+ browser = await _get_browser()
541
+ return _to_dict(await ALL_TOOLS["network_clear"](browser))
542
+
543
+
544
+ @skill.command
545
+ async def network_stats() -> dict:
546
+ """Get network capture statistics."""
547
+ browser = await _get_browser()
548
+ return _to_dict(await ALL_TOOLS["network_stats"](browser))
549
+
550
+
551
+ @skill.command
552
+ async def network_get_json(
553
+ url_pattern: str = Arg("--url-pattern", default="", help="Filter by URL pattern"),
554
+ ) -> dict:
555
+ """Extract JSON from captured network responses."""
556
+ browser = await _get_browser()
557
+ return _to_dict(await ALL_TOOLS["network_get_json"](browser, url_pattern=url_pattern))
558
+
559
+
560
+ @skill.command
561
+ async def network_export_har(
562
+ path: str = Arg("--path", default="", help="File path for HAR export"),
563
+ ) -> dict:
564
+ """Export network requests as HAR file."""
565
+ browser = await _get_browser()
566
+ return _to_dict(await ALL_TOOLS["network_export_har"](browser, path=path))
567
+
568
+
569
+ # ── Tabs (11) ────────────────────────────────────────────────────────────────
570
+
571
+ @skill.command
572
+ async def tab_new() -> dict:
573
+ """Open a new empty tab."""
574
+ browser = await _get_browser()
575
+ return _to_dict(await ALL_TOOLS["tab_new"](browser))
576
+
577
+
578
+ @skill.command
579
+ async def tab_new_with_url(
580
+ url: str = Arg(help="URL to open in new tab", required=True),
581
+ ) -> dict:
582
+ """Open a new tab with a URL."""
583
+ browser = await _get_browser()
584
+ return _to_dict(await ALL_TOOLS["tab_new_with_url"](browser, url=url))
585
+
586
+
587
+ @skill.command
588
+ async def tab_switch(
589
+ tab_id: str = Arg(help="Tab ID to switch to", required=True),
590
+ ) -> dict:
591
+ """Switch to a tab by ID."""
592
+ browser = await _get_browser()
593
+ return _to_dict(await ALL_TOOLS["tab_switch"](browser, tab_id=tab_id))
594
+
595
+
596
+ @skill.command
597
+ async def tab_switch_index(
598
+ index: int = Arg(help="Tab index to switch to", required=True),
599
+ ) -> dict:
600
+ """Switch to a tab by index."""
601
+ browser = await _get_browser()
602
+ return _to_dict(await ALL_TOOLS["tab_switch_index"](browser, index=index))
603
+
604
+
605
+ @skill.command
606
+ async def tab_close(
607
+ tab_id: str = Arg(help="Tab ID to close", required=True),
608
+ ) -> dict:
609
+ """Close a tab by ID."""
610
+ browser = await _get_browser()
611
+ return _to_dict(await ALL_TOOLS["tab_close"](browser, tab_id=tab_id))
612
+
613
+
614
+ @skill.command
615
+ async def tab_close_all_except(
616
+ tab_id: str = Arg(help="Tab ID to keep", required=True),
617
+ ) -> dict:
618
+ """Close all tabs except the specified one."""
619
+ browser = await _get_browser()
620
+ return _to_dict(await ALL_TOOLS["tab_close_all_except"](browser, tab_id=tab_id))
621
+
622
+
623
+ @skill.command
624
+ async def tab_list() -> dict:
625
+ """List all open tabs."""
626
+ browser = await _get_browser()
627
+ return _to_dict(await ALL_TOOLS["tab_list"](browser))
628
+
629
+
630
+ @skill.command
631
+ async def tab_current() -> dict:
632
+ """Get the current tab info."""
633
+ browser = await _get_browser()
634
+ return _to_dict(await ALL_TOOLS["tab_current"](browser))
635
+
636
+
637
+ @skill.command
638
+ async def tab_next() -> dict:
639
+ """Switch to the next tab."""
640
+ browser = await _get_browser()
641
+ return _to_dict(await ALL_TOOLS["tab_next"](browser))
642
+
643
+
644
+ @skill.command
645
+ async def tab_previous() -> dict:
646
+ """Switch to the previous tab."""
647
+ browser = await _get_browser()
648
+ return _to_dict(await ALL_TOOLS["tab_previous"](browser))
649
+
650
+
651
+ @skill.command
652
+ async def tab_count() -> dict:
653
+ """Get the number of open tabs."""
654
+ browser = await _get_browser()
655
+ return _to_dict(await ALL_TOOLS["tab_count"](browser))
656
+
657
+
658
+ # ── Turnstile + Markdown (4) ─────────────────────────────────────────────────
659
+
660
+ @skill.command
661
+ async def solve_turnstile(
662
+ max_attempts: int = Arg("--max-attempts", default=3, help="Maximum solve attempts"),
663
+ token_timeout: float = Arg("--token-timeout", default=30.0, help="Token wait timeout"),
664
+ ) -> dict:
665
+ """Solve a Cloudflare Turnstile challenge."""
666
+ browser = await _get_browser()
667
+ return _to_dict(await ALL_TOOLS["solve_turnstile"](browser, max_attempts=max_attempts, token_timeout=token_timeout))
668
+
669
+
670
+ @skill.command
671
+ async def is_turnstile_present() -> dict:
672
+ """Check if a Turnstile challenge is present."""
673
+ browser = await _get_browser()
674
+ return _to_dict(await ALL_TOOLS["is_turnstile_present"](browser))
675
+
676
+
677
+ @skill.command
678
+ async def wait_turnstile(
679
+ timeout: float = Arg("--timeout", default=30.0, help="Timeout in seconds"),
680
+ ) -> dict:
681
+ """Wait for and solve a Turnstile challenge."""
682
+ browser = await _get_browser()
683
+ return _to_dict(await ALL_TOOLS["wait_turnstile"](browser, timeout=timeout))
684
+
685
+
686
+ @skill.command
687
+ async def get_markdown() -> dict:
688
+ """Convert current page to markdown."""
689
+ browser = await _get_browser()
690
+ return _to_dict(await ALL_TOOLS["get_markdown"](browser))
691
+
692
+
693
+ # ── Advanced (9) ─────────────────────────────────────────────────────────────
694
+
695
+ @skill.command
696
+ async def extract_meta() -> dict:
697
+ """Extract meta tags from the page."""
698
+ browser = await _get_browser()
699
+ return _to_dict(await ALL_TOOLS["extract_meta"](browser))
700
+
701
+
702
+ @skill.command
703
+ async def extract_tables() -> dict:
704
+ """Extract all tables from the page."""
705
+ browser = await _get_browser()
706
+ return _to_dict(await ALL_TOOLS["extract_tables"](browser))
707
+
708
+
709
+ @skill.command
710
+ async def extract_table(
711
+ selector: str = Arg(help="Table selector", required=True),
712
+ ) -> dict:
713
+ """Extract a specific table."""
714
+ browser = await _get_browser()
715
+ return _to_dict(await ALL_TOOLS["extract_table"](browser, selector=selector))
716
+
717
+
718
+ @skill.command
719
+ async def extract_jsonld() -> dict:
720
+ """Extract JSON-LD structured data."""
721
+ browser = await _get_browser()
722
+ return _to_dict(await ALL_TOOLS["extract_jsonld"](browser))
723
+
724
+
725
+ @skill.command
726
+ async def extract_jsonld_type(
727
+ schema_type: str = Arg(help="Schema.org type to filter", required=True),
728
+ ) -> dict:
729
+ """Extract JSON-LD data by schema type."""
730
+ browser = await _get_browser()
731
+ return _to_dict(await ALL_TOOLS["extract_jsonld_type"](browser, schema_type=schema_type))
732
+
733
+
734
+ @skill.command
735
+ async def extract_all() -> dict:
736
+ """Extract all structured data from the page."""
737
+ browser = await _get_browser()
738
+ return _to_dict(await ALL_TOOLS["extract_all"](browser))
739
+
740
+
741
+ @skill.command
742
+ async def get_ax_tree(
743
+ max_depth: int = Arg("--max-depth", default=10, help="Maximum tree depth"),
744
+ ) -> dict:
745
+ """Get the accessibility tree."""
746
+ browser = await _get_browser()
747
+ return _to_dict(await ALL_TOOLS["get_ax_tree"](browser, max_depth=max_depth))
748
+
749
+
750
+ @skill.command
751
+ async def get_ax_text(
752
+ max_depth: int = Arg("--max-depth", default=10, help="Maximum tree depth"),
753
+ ) -> dict:
754
+ """Get accessibility tree as text."""
755
+ browser = await _get_browser()
756
+ return _to_dict(await ALL_TOOLS["get_ax_text"](browser, max_depth=max_depth))
757
+
758
+
759
+ @skill.command
760
+ async def get_full_state() -> dict:
761
+ """Get complete page state."""
762
+ browser = await _get_browser()
763
+ return _to_dict(await ALL_TOOLS["get_full_state"](browser))
764
+
765
+
766
+ # ── Pool (2) ─────────────────────────────────────────────────────────────────
767
+
768
+ @skill.command
769
+ async def pool_navigate(
770
+ urls: str = Arg(help="URLs to navigate in parallel", required=True, nargs="+"),
771
+ timeout: float = Arg("--timeout", default=30.0, help="Navigation timeout"),
772
+ ) -> dict:
773
+ """Navigate multiple URLs in parallel."""
774
+ browser = await _get_browser()
775
+ return _to_dict(await ALL_TOOLS["pool_navigate"](browser, urls=urls, timeout=timeout))
776
+
777
+
778
+ @skill.command
779
+ async def pool_stats() -> dict:
780
+ """Get pool statistics."""
781
+ browser = await _get_browser()
782
+ return _to_dict(await ALL_TOOLS["pool_stats"](browser))
783
+
784
+
785
+ # ── Entry point ──────────────────────────────────────────────────────────────
786
+
787
+ def main() -> None:
788
+ skill.run()
789
+
790
+
791
+ if __name__ == "__main__":
792
+ main()