cmdop 0.1.20__py3-none-any.whl → 0.1.22__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 (31) hide show
  1. cmdop/__init__.py +1 -1
  2. cmdop/_generated/rpc_messages/browser_pb2.py +81 -81
  3. cmdop/_generated/rpc_messages/browser_pb2.pyi +4 -2
  4. cmdop/client.py +2 -8
  5. cmdop/services/browser/__init__.py +44 -31
  6. cmdop/services/browser/capabilities/__init__.py +15 -0
  7. cmdop/services/browser/capabilities/_base.py +28 -0
  8. cmdop/services/browser/capabilities/_helpers.py +16 -0
  9. cmdop/services/browser/capabilities/dom.py +76 -0
  10. cmdop/services/browser/capabilities/fetch.py +46 -0
  11. cmdop/services/browser/capabilities/input.py +49 -0
  12. cmdop/services/browser/capabilities/scroll.py +147 -0
  13. cmdop/services/browser/capabilities/timing.py +66 -0
  14. cmdop/services/browser/js/__init__.py +6 -4
  15. cmdop/services/browser/js/interaction.py +34 -0
  16. cmdop/services/browser/service/__init__.py +5 -0
  17. cmdop/services/browser/service/aio.py +30 -0
  18. cmdop/services/browser/{sync/service.py → service/sync.py} +9 -4
  19. cmdop/services/browser/session.py +166 -0
  20. {cmdop-0.1.20.dist-info → cmdop-0.1.22.dist-info}/METADATA +70 -41
  21. {cmdop-0.1.20.dist-info → cmdop-0.1.22.dist-info}/RECORD +24 -20
  22. cmdop/services/browser/aio/__init__.py +0 -6
  23. cmdop/services/browser/aio/service.py +0 -415
  24. cmdop/services/browser/aio/session.py +0 -358
  25. cmdop/services/browser/base/__init__.py +0 -6
  26. cmdop/services/browser/base/session.py +0 -124
  27. cmdop/services/browser/sync/__init__.py +0 -6
  28. cmdop/services/browser/sync/session.py +0 -580
  29. /cmdop/services/browser/{base/service.py → service/_helpers.py} +0 -0
  30. {cmdop-0.1.20.dist-info → cmdop-0.1.22.dist-info}/WHEEL +0 -0
  31. {cmdop-0.1.20.dist-info → cmdop-0.1.22.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,147 @@
1
+ """Scroll capability."""
2
+
3
+ import time
4
+ from typing import Any, Callable
5
+
6
+ from cmdop.services.browser.js import (
7
+ build_scroll_js,
8
+ build_scroll_to_bottom_js,
9
+ build_get_scroll_info_js,
10
+ build_infinite_scroll_js,
11
+ )
12
+ from cmdop.services.browser.models import ScrollResult, ScrollInfo, InfiniteScrollResult
13
+
14
+ from ._base import BaseCapability
15
+ from ._helpers import to_dict
16
+
17
+
18
+ class ScrollCapability(BaseCapability):
19
+ """Scroll operations.
20
+
21
+ Usage:
22
+ session.scroll.js("down", 500)
23
+ session.scroll.to_bottom()
24
+ info = session.scroll.info()
25
+ """
26
+
27
+ def js(
28
+ self,
29
+ direction: str = "down",
30
+ amount: int = 500,
31
+ selector: str | None = None,
32
+ smooth: bool = True,
33
+ human_like: bool = False,
34
+ container: str | None = None,
35
+ ) -> ScrollResult:
36
+ """Scroll using JavaScript. Use when native scroll doesn't work."""
37
+ js = build_scroll_js(direction, amount, selector, smooth, human_like, container)
38
+ data = to_dict(self._js(js))
39
+ return ScrollResult(
40
+ success=data.get("success", False),
41
+ scroll_y=int(data.get("scrollY", 0)),
42
+ scrolled_by=int(data.get("scrolledBy", 0)),
43
+ at_bottom=data.get("atBottom", False),
44
+ error=data.get("error"),
45
+ )
46
+
47
+ def to_bottom(self) -> ScrollResult:
48
+ """Scroll to page bottom."""
49
+ data = to_dict(self._js(build_scroll_to_bottom_js()))
50
+ return ScrollResult(
51
+ success=data.get("success", False),
52
+ scroll_y=int(data.get("scrollY", 0)),
53
+ scrolled_by=int(data.get("scrolledBy", 0)),
54
+ at_bottom=True,
55
+ )
56
+
57
+ def to_element(self, selector: str) -> ScrollResult:
58
+ """Scroll element into view."""
59
+ return self.js(selector=selector)
60
+
61
+ def info(self) -> ScrollInfo:
62
+ """Get scroll position and page dimensions."""
63
+ data = to_dict(self._js(build_get_scroll_info_js()))
64
+ return ScrollInfo(
65
+ scroll_x=int(data.get("scrollX", 0)),
66
+ scroll_y=int(data.get("scrollY", 0)),
67
+ page_height=int(data.get("pageHeight", 0)),
68
+ page_width=int(data.get("pageWidth", 0)),
69
+ viewport_height=int(data.get("viewportHeight", 0)),
70
+ viewport_width=int(data.get("viewportWidth", 0)),
71
+ at_bottom=data.get("atBottom", False),
72
+ at_top=data.get("atTop", True),
73
+ )
74
+
75
+ def native(
76
+ self,
77
+ direction: str = "down",
78
+ amount: int = 500,
79
+ selector: str | None = None,
80
+ smooth: bool = True,
81
+ ) -> ScrollResult:
82
+ """Scroll using native browser API."""
83
+ data = self._call("scroll", direction, amount, selector, smooth)
84
+ return ScrollResult(
85
+ success=True,
86
+ scroll_y=data.get("scroll_y", 0),
87
+ scrolled_by=data.get("scrolled_by", 0),
88
+ at_bottom=data.get("at_bottom", False),
89
+ )
90
+
91
+ def collect(
92
+ self,
93
+ seen_keys: set[str],
94
+ key_selector: str = "a[href]",
95
+ key_attr: str = "href",
96
+ container_selector: str = "body",
97
+ ) -> InfiniteScrollResult:
98
+ """Extract new keys for infinite scroll patterns. Updates seen_keys in-place."""
99
+ js = build_infinite_scroll_js(list(seen_keys), key_selector, key_attr, container_selector)
100
+ data = to_dict(self._js(js))
101
+ new_keys = data.get("new_keys", [])
102
+ seen_keys.update(new_keys)
103
+ return InfiniteScrollResult(
104
+ new_keys=new_keys,
105
+ at_bottom=data.get("at_bottom", False),
106
+ total_seen=data.get("total_seen", len(seen_keys)),
107
+ error=data.get("error"),
108
+ )
109
+
110
+ def infinite(
111
+ self,
112
+ extract_fn: Callable[[], list[Any]],
113
+ limit: int = 100,
114
+ max_scrolls: int = 50,
115
+ max_no_new: int = 3,
116
+ scroll_amount: int = 800,
117
+ delay: float = 1.0,
118
+ ) -> list[Any]:
119
+ """Smart infinite scroll with extraction.
120
+
121
+ Args:
122
+ extract_fn: Returns new items each call (dedup is caller's job)
123
+ limit: Stop after this many items
124
+ max_scrolls: Max scroll attempts
125
+ max_no_new: Stop after N scrolls with no new items
126
+ scroll_amount: Pixels per scroll
127
+ delay: Seconds between scrolls
128
+ """
129
+ items: list[Any] = []
130
+ no_new = 0
131
+
132
+ for _ in range(max_scrolls):
133
+ new = extract_fn()
134
+ if new:
135
+ items.extend(new)
136
+ no_new = 0
137
+ if len(items) >= limit:
138
+ break
139
+ else:
140
+ no_new += 1
141
+ if no_new >= max_no_new:
142
+ break
143
+
144
+ self.js("down", scroll_amount)
145
+ time.sleep(delay)
146
+
147
+ return items[:limit]
@@ -0,0 +1,66 @@
1
+ """Timing capability."""
2
+
3
+ import random
4
+ import time
5
+ import threading
6
+ from typing import Callable, TypeVar
7
+
8
+ from ._base import BaseCapability
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ class TimingCapability(BaseCapability):
14
+ """Timing operations: wait, delays, timeouts.
15
+
16
+ Usage:
17
+ session.timing.wait(1000)
18
+ session.timing.random(0.5, 2.0)
19
+ result, ok = session.timing.timeout(lambda: slow_fn(), 30)
20
+ """
21
+
22
+ def wait(self, ms: int, jitter: float = 0.1) -> None:
23
+ """Wait milliseconds with jitter (±10% by default)."""
24
+ actual = (ms / 1000) * (1 + random.uniform(-jitter, jitter))
25
+ time.sleep(actual)
26
+
27
+ def seconds(self, sec: float, jitter: float = 0.1) -> None:
28
+ """Wait seconds with jitter."""
29
+ self.wait(int(sec * 1000), jitter)
30
+
31
+ def random(self, min_sec: float = 0.5, max_sec: float = 1.5) -> None:
32
+ """Wait random time between min and max seconds."""
33
+ time.sleep(min_sec + random.random() * (max_sec - min_sec))
34
+
35
+ def timeout(
36
+ self,
37
+ fn: Callable[[], T],
38
+ seconds: float = 60.0,
39
+ on_timeout: Callable[[], None] | None = None,
40
+ ) -> tuple[T | None, bool]:
41
+ """Run function with timeout. Returns (result, success)."""
42
+ result: list[T | None] = [None]
43
+ error: list[Exception | None] = [None]
44
+ done = threading.Event()
45
+
46
+ def run():
47
+ try:
48
+ result[0] = fn()
49
+ except Exception as e:
50
+ error[0] = e
51
+ finally:
52
+ done.set()
53
+
54
+ threading.Thread(target=run, daemon=True).start()
55
+
56
+ if done.wait(timeout=seconds):
57
+ if error[0]:
58
+ raise error[0]
59
+ return result[0], True
60
+
61
+ if on_timeout:
62
+ try:
63
+ on_timeout()
64
+ except Exception:
65
+ pass
66
+ return None, False
@@ -7,29 +7,30 @@ This module provides JavaScript code generators for common browser operations:
7
7
  - Interaction: Hover, select, modals
8
8
  """
9
9
 
10
- from cmdop.services.browser.js.core import (
10
+ from .core import (
11
11
  parse_json_result,
12
12
  build_async_js,
13
13
  )
14
14
 
15
- from cmdop.services.browser.js.fetch import (
15
+ from .fetch import (
16
16
  build_fetch_js,
17
17
  build_fetch_all_js,
18
18
  )
19
19
 
20
- from cmdop.services.browser.js.scroll import (
20
+ from .scroll import (
21
21
  build_scroll_js,
22
22
  build_scroll_to_bottom_js,
23
23
  build_infinite_scroll_js,
24
24
  build_get_scroll_info_js,
25
25
  )
26
26
 
27
- from cmdop.services.browser.js.interaction import (
27
+ from .interaction import (
28
28
  build_hover_js,
29
29
  build_select_js,
30
30
  build_close_modal_js,
31
31
  build_click_all_by_text_js,
32
32
  build_press_key_js,
33
+ build_click_js,
33
34
  )
34
35
 
35
36
  __all__ = [
@@ -50,4 +51,5 @@ __all__ = [
50
51
  "build_close_modal_js",
51
52
  "build_click_all_by_text_js",
52
53
  "build_press_key_js",
54
+ "build_click_js",
53
55
  ]
@@ -178,3 +178,37 @@ def build_click_all_by_text_js(text: str, role: str = "button") -> str:
178
178
  return JSON.stringify({{ clicked: clicked }});
179
179
  }})()
180
180
  """
181
+
182
+
183
+ def build_click_js(selector: str, scroll_into_view: bool = True) -> str:
184
+ """
185
+ Build JS to click element via JavaScript (more reliable than CDP click).
186
+
187
+ This is useful when native CDP click hangs or doesn't work properly.
188
+ Uses document.querySelector to find element and calls .click() directly.
189
+
190
+ Args:
191
+ selector: CSS selector for the element to click
192
+ scroll_into_view: If True, scroll element into view before clicking (default: True)
193
+
194
+ Returns:
195
+ JS code that returns { success: true/false, error?: string }
196
+ """
197
+ scroll_code = 'el.scrollIntoView({block: "center", behavior: "instant"});' if scroll_into_view else ''
198
+ selector_escaped = json.dumps(selector)
199
+
200
+ return f"""
201
+ (function() {{
202
+ const el = document.querySelector({selector_escaped});
203
+ if (!el) {{
204
+ return JSON.stringify({{ success: false, error: 'Element not found' }});
205
+ }}
206
+ try {{
207
+ {scroll_code}
208
+ el.click();
209
+ return JSON.stringify({{ success: true }});
210
+ }} catch (e) {{
211
+ return JSON.stringify({{ success: false, error: e.message }});
212
+ }}
213
+ }})()
214
+ """
@@ -0,0 +1,5 @@
1
+ """Browser service layer (gRPC)."""
2
+
3
+ from .sync import BrowserService
4
+
5
+ __all__ = ["BrowserService"]
@@ -0,0 +1,30 @@
1
+ """Async browser service stub.
2
+
3
+ Async browser is not implemented yet. Use sync BrowserService instead.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING, NoReturn
9
+
10
+ if TYPE_CHECKING:
11
+ from cmdop.transport.base import BaseTransport
12
+
13
+
14
+ class AsyncBrowserService:
15
+ """
16
+ Async browser service stub.
17
+
18
+ Not implemented yet. Use sync CMDOPClient.browser instead.
19
+ """
20
+
21
+ def __init__(self, transport: BaseTransport) -> None:
22
+ self._transport = transport
23
+
24
+ def _not_implemented(self) -> NoReturn:
25
+ raise NotImplementedError(
26
+ "Async browser is not implemented. Use sync CMDOPClient.browser instead."
27
+ )
28
+
29
+ async def create_session(self, *args, **kwargs) -> NoReturn:
30
+ self._not_implemented()
@@ -6,7 +6,7 @@ import json
6
6
  from typing import TYPE_CHECKING, Any
7
7
 
8
8
  from cmdop.services.base import BaseService
9
- from cmdop.services.browser.base.service import BaseServiceMixin, cookie_to_pb, pb_to_cookie
9
+ from cmdop.services.browser.service._helpers import BaseServiceMixin, cookie_to_pb, pb_to_cookie
10
10
  from cmdop.services.browser.models import BrowserCookie, BrowserState, PageInfo, raise_browser_error
11
11
 
12
12
  if TYPE_CHECKING:
@@ -47,7 +47,7 @@ class BrowserService(BaseService, BaseServiceMixin):
47
47
  height: int = 800,
48
48
  ) -> "BrowserSession":
49
49
  from cmdop._generated.rpc_messages.browser_pb2 import BrowserCreateSessionRequest
50
- from cmdop.services.browser.sync.session import BrowserSession
50
+ from cmdop.services.browser.session import BrowserSession
51
51
 
52
52
  request = BrowserCreateSessionRequest(
53
53
  provider=provider,
@@ -88,11 +88,16 @@ class BrowserService(BaseService, BaseServiceMixin):
88
88
 
89
89
  return response.final_url
90
90
 
91
- def click(self, session_id: str, selector: str, timeout_ms: int = 5000) -> None:
91
+ def click(
92
+ self, session_id: str, selector: str, timeout_ms: int = 5000, move_cursor: bool = False
93
+ ) -> None:
92
94
  from cmdop._generated.rpc_messages.browser_pb2 import BrowserClickRequest
93
95
 
94
96
  request = BrowserClickRequest(
95
- browser_session_id=session_id, selector=selector, timeout_ms=timeout_ms
97
+ browser_session_id=session_id,
98
+ selector=selector,
99
+ timeout_ms=timeout_ms,
100
+ move_cursor=move_cursor,
96
101
  )
97
102
  response = self._call_sync(self._get_stub.BrowserClick, request)
98
103
 
@@ -0,0 +1,166 @@
1
+ """Browser session with capability-based API."""
2
+
3
+ from __future__ import annotations
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from cmdop.services.browser.models import BrowserCookie, BrowserState, PageInfo
7
+
8
+ from .capabilities import (
9
+ ScrollCapability,
10
+ InputCapability,
11
+ TimingCapability,
12
+ DOMCapability,
13
+ FetchCapability,
14
+ )
15
+
16
+ if TYPE_CHECKING:
17
+ from cmdop.services.browser.service.sync import BrowserService
18
+
19
+
20
+ class BrowserSession:
21
+ """Browser session with grouped capabilities.
22
+
23
+ Core methods (on session directly):
24
+ session.navigate(url)
25
+ session.click(selector)
26
+ session.type(selector, text)
27
+ session.wait_for(selector)
28
+ session.execute_script(js)
29
+
30
+ Capabilities (grouped by function):
31
+ session.scroll.js("down", 500)
32
+ session.scroll.to_bottom()
33
+ session.input.click_js(selector)
34
+ session.input.key("Escape")
35
+ session.timing.wait(1000)
36
+ session.dom.soup()
37
+ session.fetch.json("/api/data")
38
+
39
+ Usage:
40
+ with service.create_session() as session:
41
+ session.navigate("https://example.com")
42
+ session.scroll.js("down", 500)
43
+ session.input.click_js(".button")
44
+ """
45
+
46
+ __slots__ = (
47
+ "_service",
48
+ "_session_id",
49
+ "_scroll",
50
+ "_input",
51
+ "_timing",
52
+ "_dom",
53
+ "_fetch",
54
+ )
55
+
56
+ def __init__(self, service: "BrowserService", session_id: str) -> None:
57
+ self._service = service
58
+ self._session_id = session_id
59
+ self._scroll: ScrollCapability | None = None
60
+ self._input: InputCapability | None = None
61
+ self._timing: TimingCapability | None = None
62
+ self._dom: DOMCapability | None = None
63
+ self._fetch: FetchCapability | None = None
64
+
65
+ @property
66
+ def session_id(self) -> str:
67
+ return self._session_id
68
+
69
+ # === Capabilities (lazy init) ===
70
+
71
+ @property
72
+ def scroll(self) -> ScrollCapability:
73
+ """Scroll: js(), to_bottom(), to_element(), info(), native(), collect()"""
74
+ if self._scroll is None:
75
+ self._scroll = ScrollCapability(self)
76
+ return self._scroll
77
+
78
+ @property
79
+ def input(self) -> InputCapability:
80
+ """Input: click_js(), key(), click_all(), hover(), hover_js()"""
81
+ if self._input is None:
82
+ self._input = InputCapability(self)
83
+ return self._input
84
+
85
+ @property
86
+ def timing(self) -> TimingCapability:
87
+ """Timing: wait(), seconds(), random(), timeout()"""
88
+ if self._timing is None:
89
+ self._timing = TimingCapability(self)
90
+ return self._timing
91
+
92
+ @property
93
+ def dom(self) -> DOMCapability:
94
+ """DOM: html(), text(), soup(), parse(), select(), close_modal(), extract()"""
95
+ if self._dom is None:
96
+ self._dom = DOMCapability(self)
97
+ return self._dom
98
+
99
+ @property
100
+ def fetch(self) -> FetchCapability:
101
+ """Fetch: json(), all(), execute()"""
102
+ if self._fetch is None:
103
+ self._fetch = FetchCapability(self)
104
+ return self._fetch
105
+
106
+ # === Core Methods ===
107
+
108
+ def navigate(self, url: str, timeout_ms: int = 30000) -> str:
109
+ """Navigate to URL. Returns final URL."""
110
+ return self._service.navigate(self._session_id, url, timeout_ms)
111
+
112
+ def click(self, selector: str, timeout_ms: int = 5000, move_cursor: bool = False) -> None:
113
+ """Click element by CSS selector."""
114
+ self._service.click(self._session_id, selector, timeout_ms, move_cursor)
115
+
116
+ def type(self, selector: str, text: str, human_like: bool = False, clear_first: bool = True) -> None:
117
+ """Type text into element."""
118
+ self._service.type(self._session_id, selector, text, human_like, clear_first)
119
+
120
+ def wait_for(self, selector: str, timeout_ms: int = 30000) -> bool:
121
+ """Wait for element to appear."""
122
+ return self._service.wait_for(self._session_id, selector, timeout_ms)
123
+
124
+ def execute_script(self, script: str) -> str:
125
+ """Execute raw JavaScript."""
126
+ return self._service.execute_script(self._session_id, script)
127
+
128
+ # === State ===
129
+
130
+ def screenshot(self, full_page: bool = False) -> bytes:
131
+ """Take screenshot."""
132
+ return self._service.screenshot(self._session_id, full_page)
133
+
134
+ def get_state(self) -> BrowserState:
135
+ """Get browser state."""
136
+ return self._service.get_state(self._session_id)
137
+
138
+ def get_cookies(self, domain: str = "") -> list[BrowserCookie]:
139
+ """Get cookies."""
140
+ return self._service.get_cookies(self._session_id, domain)
141
+
142
+ def set_cookies(self, cookies: list[BrowserCookie | dict]) -> None:
143
+ """Set cookies."""
144
+ self._service.set_cookies(self._session_id, cookies)
145
+
146
+ def get_page_info(self) -> PageInfo:
147
+ """Get page info."""
148
+ return self._service.get_page_info(self._session_id)
149
+
150
+ # === Internal ===
151
+
152
+ def _call_service(self, method: str, *args: Any, **kwargs: Any) -> Any:
153
+ """Call service method (used by capabilities)."""
154
+ return getattr(self._service, method)(self._session_id, *args, **kwargs)
155
+
156
+ # === Context Manager ===
157
+
158
+ def close(self) -> None:
159
+ """Close session."""
160
+ self._service.close_session(self._session_id)
161
+
162
+ def __enter__(self) -> "BrowserSession":
163
+ return self
164
+
165
+ def __exit__(self, *args: Any) -> None:
166
+ self.close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cmdop
3
- Version: 0.1.20
3
+ Version: 0.1.22
4
4
  Summary: Python SDK for CMDOP agent interaction
5
5
  Project-URL: Homepage, https://cmdop.com
6
6
  Project-URL: Documentation, https://cmdop.com
@@ -145,71 +145,100 @@ health: Health = result.output # Typed!
145
145
 
146
146
  ## Browser
147
147
 
148
+ Capability-based API for browser automation.
149
+
148
150
  ```python
149
- with client.browser.create_session() as b:
150
- b.navigate("https://shop.com/products")
151
- b.close_modal() # Close popups
151
+ with client.browser.create_session() as s:
152
+ s.navigate("https://shop.com/products")
153
+ s.dom.close_modal() # Close popups
152
154
 
153
155
  # BeautifulSoup parsing
154
- soup = b.soup() # SoupWrapper with chainable API
156
+ soup = s.dom.soup() # SoupWrapper with chainable API
155
157
  for item in soup.select(".product"):
156
158
  title = item.select_one("h2").text()
157
159
  price = item.attr("data-price")
158
160
 
159
161
  # Scrolling with random delays
160
162
  for _ in range(10):
161
- soup = b.soup(".listings")
162
- # ... parse ...
163
- b.scroll("down", 700)
164
- b.wait_random(0.8, 1.5) # Random delay
163
+ soup = s.dom.soup(".listings")
164
+ s.scroll.js("down", 700)
165
+ s.timing.random(0.8, 1.5)
165
166
 
166
- # Click with cursor movement (human-like)
167
- b.click("button.buy", move_cursor=True) # Moves cursor before clicking
167
+ # Click with cursor movement
168
+ s.click("button.buy", move_cursor=True)
168
169
 
169
170
  # Click all "See more" buttons
170
- b.click_all_by_text("See more")
171
+ s.input.click_all("See more")
171
172
 
172
- # Native mouse operations
173
- b.mouse_move(500, 300) # Smooth cursor movement
174
- b.hover(".tooltip-trigger") # Hover to reveal tooltip
173
+ # Mouse operations
174
+ s.input.mouse_move(500, 300)
175
+ s.input.hover(".tooltip-trigger")
175
176
 
176
177
  # JS fetch (bypass CORS, inherit cookies)
177
- data = b.fetch_json("https://api.site.com/v1/items")
178
+ data = s.fetch.json("/api/items")
178
179
  ```
179
180
 
181
+ ### Core Methods (on session)
182
+
180
183
  | Method | Description |
181
184
  |--------|-------------|
182
185
  | `navigate(url)` | Go to URL |
183
- | `click(selector, move_cursor)` | Click element (optionally move cursor first) |
184
- | `click_all_by_text(text, role)` | Click all matching elements |
186
+ | `click(selector, move_cursor)` | Click element |
185
187
  | `type(selector, text)` | Type text |
186
- | `wait_for(selector, ms)` | Wait for element |
187
- | `wait_seconds(n)` | Sleep |
188
- | `wait_random(min, max)` | Random sleep |
189
- | `extract(selector, attr)` | Get text/attr |
190
- | `get_html(selector)` | Get HTML |
191
- | `soup(selector)` | → SoupWrapper |
192
- | `parse_html(html)` | → BeautifulSoup |
193
- | `fetch_json(url)` | JS fetch → dict |
194
- | `fetch_all(urls)` | Parallel fetch |
195
- | `execute_js(code)` | Run async JS |
188
+ | `wait_for(selector)` | Wait for element |
189
+ | `execute_script(js)` | Run JavaScript |
196
190
  | `screenshot()` | PNG bytes |
197
- | `scroll(dir, amount, ...)` | Native scroll page |
198
- | `scroll_to(selector)` | Scroll element into view |
199
- | `get_scroll_info()` | Position + page size |
200
- | `get_page_info()` | Comprehensive page info |
201
- | `mouse_move(x, y, steps)` | Move cursor to coordinates |
202
- | `hover(selector)` | Hover over element |
191
+ | `get_state()` | URL + title |
192
+ | `get_page_info()` | Full page info |
193
+ | `get/set_cookies()` | Cookie management |
194
+
195
+ ### Capabilities
196
+
197
+ **`session.scroll`** - Scrolling
198
+ | Method | Description |
199
+ |--------|-------------|
200
+ | `js(dir, amount)` | JS scroll (works on complex sites) |
201
+ | `native(dir, amount)` | Browser API scroll |
202
+ | `to_bottom()` | Scroll to page bottom |
203
+ | `to_element(selector)` | Scroll element into view |
204
+ | `info()` | Get scroll position |
205
+ | `infinite(extract_fn)` | Smart infinite scroll with extraction |
206
+
207
+ **`session.input`** - Input operations
208
+ | Method | Description |
209
+ |--------|-------------|
210
+ | `click_js(selector)` | JS click (reliable) |
211
+ | `click_all(text, role)` | Click all matching elements |
212
+ | `key(key, selector)` | Press keyboard key |
213
+ | `hover(selector)` | Hover over element (native) |
214
+ | `hover_js(selector)` | Hover via JS |
215
+ | `mouse_move(x, y)` | Move cursor to coordinates |
216
+
217
+ **`session.timing`** - Delays
218
+ | Method | Description |
219
+ |--------|-------------|
220
+ | `wait(ms)` | Wait milliseconds |
221
+ | `seconds(n)` | Wait seconds |
222
+ | `random(min, max)` | Random delay |
223
+ | `timeout(fn, sec, cleanup)` | Run with timeout |
224
+
225
+ **`session.dom`** - DOM operations
226
+ | Method | Description |
227
+ |--------|-------------|
228
+ | `html(selector)` | Get HTML |
229
+ | `text(selector)` | Get text content |
230
+ | `soup(selector)` | → SoupWrapper |
231
+ | `parse(html)` | → BeautifulSoup |
232
+ | `extract(selector, attr)` | Get text/attr list |
203
233
  | `select(selector, value)` | Dropdown select |
204
234
  | `close_modal()` | Close dialogs |
205
- | `press_key(key, selector)` | Press keyboard key |
206
- | `get/set_cookies()` | Cookie management |
207
235
 
208
- **scroll() parameters:**
209
- - `direction`: "up", "down", "left", "right"
210
- - `amount`: pixels to scroll
211
- - `smooth`: animate scroll (default True)
212
- - `selector`: scroll element into view (alternative to direction/amount)
236
+ **`session.fetch`** - HTTP from browser context
237
+ | Method | Description |
238
+ |--------|-------------|
239
+ | `json(url)` | Fetch JSON |
240
+ | `all(requests)` | Parallel fetch |
241
+ | `execute(method, url, ...)` | Custom request |
213
242
 
214
243
  ## SDKBaseModel
215
244