browser-control-and-automation-cli 0.1.1.dev3__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 (104) hide show
  1. browser_cli/__init__.py +7 -0
  2. browser_cli/actions/__init__.py +5 -0
  3. browser_cli/actions/cli_specs.py +868 -0
  4. browser_cli/agent_scope/__init__.py +14 -0
  5. browser_cli/automation/__init__.py +11 -0
  6. browser_cli/automation/api/__init__.py +5 -0
  7. browser_cli/automation/api/server.py +321 -0
  8. browser_cli/automation/hooks.py +42 -0
  9. browser_cli/automation/loader.py +134 -0
  10. browser_cli/automation/models.py +127 -0
  11. browser_cli/automation/persistence/__init__.py +5 -0
  12. browser_cli/automation/persistence/store.py +682 -0
  13. browser_cli/automation/projections.py +279 -0
  14. browser_cli/automation/publisher.py +153 -0
  15. browser_cli/automation/scheduler/__init__.py +5 -0
  16. browser_cli/automation/scheduler/schedule.py +113 -0
  17. browser_cli/automation/service/__init__.py +8 -0
  18. browser_cli/automation/service/__main__.py +64 -0
  19. browser_cli/automation/service/client.py +232 -0
  20. browser_cli/automation/service/runtime.py +388 -0
  21. browser_cli/automation/toml.py +35 -0
  22. browser_cli/automation/web/__init__.py +5 -0
  23. browser_cli/automation/web/assets.py +377 -0
  24. browser_cli/browser/__init__.py +1 -0
  25. browser_cli/browser/models.py +44 -0
  26. browser_cli/browser/network_capture.py +392 -0
  27. browser_cli/browser/service.py +1519 -0
  28. browser_cli/browser/session.py +158 -0
  29. browser_cli/browser/snapshot.py +35 -0
  30. browser_cli/browser/stealth.py +450 -0
  31. browser_cli/cli/__init__.py +1 -0
  32. browser_cli/cli/error_hints.py +26 -0
  33. browser_cli/cli/main.py +298 -0
  34. browser_cli/commands/__init__.py +1 -0
  35. browser_cli/commands/action.py +25 -0
  36. browser_cli/commands/automation.py +304 -0
  37. browser_cli/commands/doctor.py +336 -0
  38. browser_cli/commands/install_skills.py +138 -0
  39. browser_cli/commands/paths.py +31 -0
  40. browser_cli/commands/read.py +33 -0
  41. browser_cli/commands/reload.py +51 -0
  42. browser_cli/commands/status.py +444 -0
  43. browser_cli/commands/task.py +71 -0
  44. browser_cli/constants.py +97 -0
  45. browser_cli/daemon/__init__.py +1 -0
  46. browser_cli/daemon/__main__.py +34 -0
  47. browser_cli/daemon/app.py +941 -0
  48. browser_cli/daemon/browser_service.py +998 -0
  49. browser_cli/daemon/client.py +280 -0
  50. browser_cli/daemon/models.py +63 -0
  51. browser_cli/daemon/runtime_presentation.py +167 -0
  52. browser_cli/daemon/server.py +103 -0
  53. browser_cli/daemon/state.py +15 -0
  54. browser_cli/daemon/transport.py +77 -0
  55. browser_cli/drivers/__init__.py +6 -0
  56. browser_cli/drivers/_extension/__init__.py +1 -0
  57. browser_cli/drivers/_extension/artifact_actions.py +154 -0
  58. browser_cli/drivers/_extension/dialog_actions.py +35 -0
  59. browser_cli/drivers/_extension/input_actions.py +63 -0
  60. browser_cli/drivers/_extension/locator_actions.py +100 -0
  61. browser_cli/drivers/_extension/observe_actions.py +285 -0
  62. browser_cli/drivers/_extension/page_actions.py +210 -0
  63. browser_cli/drivers/_extension/state_actions.py +134 -0
  64. browser_cli/drivers/base.py +391 -0
  65. browser_cli/drivers/extension_driver.py +33 -0
  66. browser_cli/drivers/models.py +21 -0
  67. browser_cli/drivers/playwright_driver.py +499 -0
  68. browser_cli/error_codes.py +27 -0
  69. browser_cli/errors.py +146 -0
  70. browser_cli/exit_codes.py +9 -0
  71. browser_cli/extension/__init__.py +32 -0
  72. browser_cli/extension/protocol.py +222 -0
  73. browser_cli/extension/session.py +393 -0
  74. browser_cli/network.py +147 -0
  75. browser_cli/outputs/__init__.py +1 -0
  76. browser_cli/outputs/json.py +10 -0
  77. browser_cli/outputs/render.py +7 -0
  78. browser_cli/packaged_skills/__init__.py +1 -0
  79. browser_cli/packaged_skills/browser-cli-converge/SKILL.md +62 -0
  80. browser_cli/packaged_skills/browser-cli-delivery/SKILL.md +61 -0
  81. browser_cli/packaged_skills/browser-cli-explore/SKILL.md +77 -0
  82. browser_cli/profiles/__init__.py +1 -0
  83. browser_cli/profiles/discovery.py +185 -0
  84. browser_cli/refs/__init__.py +17 -0
  85. browser_cli/refs/generator.py +386 -0
  86. browser_cli/refs/models.py +127 -0
  87. browser_cli/refs/registry.py +51 -0
  88. browser_cli/refs/resolver.py +214 -0
  89. browser_cli/tabs/__init__.py +5 -0
  90. browser_cli/tabs/registry.py +283 -0
  91. browser_cli/task_runtime/__init__.py +31 -0
  92. browser_cli/task_runtime/client.py +107 -0
  93. browser_cli/task_runtime/entrypoint.py +110 -0
  94. browser_cli/task_runtime/errors.py +17 -0
  95. browser_cli/task_runtime/flow.py +171 -0
  96. browser_cli/task_runtime/models.py +97 -0
  97. browser_cli/task_runtime/read.py +71 -0
  98. browser_cli/task_runtime/templates.py +58 -0
  99. browser_control_and_automation_cli-0.1.1.dev3.dist-info/METADATA +340 -0
  100. browser_control_and_automation_cli-0.1.1.dev3.dist-info/RECORD +104 -0
  101. browser_control_and_automation_cli-0.1.1.dev3.dist-info/WHEEL +5 -0
  102. browser_control_and_automation_cli-0.1.1.dev3.dist-info/entry_points.txt +2 -0
  103. browser_control_and_automation_cli-0.1.1.dev3.dist-info/licenses/LICENSE +21 -0
  104. browser_control_and_automation_cli-0.1.1.dev3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,77 @@
1
+ """Unix-socket transport helpers for the daemon."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import json
7
+ import os
8
+ import socket
9
+ import stat
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from browser_cli.constants import get_app_paths
14
+
15
+ DAEMON_RUNTIME_VERSION = "2026-04-10-dual-driver-extension-v1"
16
+
17
+
18
+ def ensure_run_dir() -> Path:
19
+ run_dir = get_app_paths().run_dir
20
+ run_dir.mkdir(parents=True, exist_ok=True)
21
+ with contextlib.suppress(OSError):
22
+ os.chmod(run_dir, 0o700)
23
+ return run_dir
24
+
25
+
26
+ def write_run_info(info: dict[str, Any]) -> None:
27
+ ensure_run_dir()
28
+ run_info_path = get_app_paths().run_info_path
29
+ tmp_path = run_info_path.with_suffix(".tmp")
30
+ tmp_path.write_text(json.dumps(info), encoding="utf-8")
31
+ tmp_path.replace(run_info_path)
32
+ with contextlib.suppress(OSError):
33
+ os.chmod(run_info_path, 0o600)
34
+
35
+
36
+ def read_run_info() -> dict[str, Any] | None:
37
+ run_info_path = get_app_paths().run_info_path
38
+ try:
39
+ return json.loads(run_info_path.read_text(encoding="utf-8"))
40
+ except (FileNotFoundError, OSError, json.JSONDecodeError):
41
+ return None
42
+
43
+
44
+ def remove_run_info() -> None:
45
+ with contextlib.suppress(OSError):
46
+ get_app_paths().run_info_path.unlink()
47
+
48
+
49
+ def safe_remove_socket(path: Path | None = None) -> None:
50
+ socket_path = path or get_app_paths().socket_path
51
+ try:
52
+ st = socket_path.stat()
53
+ except FileNotFoundError:
54
+ return
55
+ if not stat.S_ISSOCK(st.st_mode):
56
+ raise RuntimeError(f"Refusing to remove non-socket path: {socket_path}")
57
+ if hasattr(os, "getuid") and st.st_uid != os.getuid():
58
+ raise PermissionError(
59
+ f"Socket path {socket_path} is owned by uid={st.st_uid}, current uid={os.getuid()}"
60
+ )
61
+ try:
62
+ socket_path.unlink()
63
+ except FileNotFoundError:
64
+ return
65
+
66
+
67
+ def probe_socket(path: Path | None = None) -> bool:
68
+ socket_path = str(path or get_app_paths().socket_path)
69
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
70
+ try:
71
+ client.settimeout(1.0)
72
+ client.connect(socket_path)
73
+ return True
74
+ except OSError:
75
+ return False
76
+ finally:
77
+ client.close()
@@ -0,0 +1,6 @@
1
+ """Driver interfaces and implementations."""
2
+
3
+ from .base import BrowserDriver
4
+ from .models import DriverHealth, TabState
5
+
6
+ __all__ = ["BrowserDriver", "DriverHealth", "TabState"]
@@ -0,0 +1 @@
1
+ """Internal helpers for the extension-backed browser driver."""
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ from typing import Any
5
+
6
+
7
+ class ExtensionDriverArtifactActionsMixin:
8
+ async def screenshot(
9
+ self, page_id: str, *, path: str, full_page: bool = False
10
+ ) -> dict[str, Any]:
11
+ payload = await self._page_command(
12
+ page_id,
13
+ "screenshot",
14
+ {"full_page": full_page},
15
+ )
16
+ output_path = await self._materialize_single_artifact(
17
+ payload,
18
+ page_id=page_id,
19
+ path=path,
20
+ suffix=".png",
21
+ artifact_kind="screenshot",
22
+ inline_key="data_base64",
23
+ )
24
+ return {"page_id": page_id, "path": output_path, "full_page": full_page}
25
+
26
+ async def save_pdf(self, page_id: str, *, path: str) -> dict[str, Any]:
27
+ payload = await self._page_command(page_id, "pdf", {})
28
+ output_path = await self._materialize_single_artifact(
29
+ payload,
30
+ page_id=page_id,
31
+ path=path,
32
+ suffix=".pdf",
33
+ artifact_kind="pdf",
34
+ inline_key="data_base64",
35
+ )
36
+ return {"page_id": page_id, "path": output_path}
37
+
38
+ async def start_tracing(
39
+ self,
40
+ page_id: str,
41
+ *,
42
+ screenshots: bool = True,
43
+ snapshots: bool = True,
44
+ sources: bool = False,
45
+ ) -> dict[str, Any]:
46
+ payload = await self._page_command(
47
+ page_id,
48
+ "trace-start",
49
+ {"screenshots": screenshots, "snapshots": snapshots, "sources": sources},
50
+ )
51
+ return {"page_id": page_id, **payload}
52
+
53
+ async def add_trace_chunk(self, page_id: str, *, title: str | None = None) -> dict[str, Any]:
54
+ payload = await self._page_command(page_id, "trace-chunk", {"title": title})
55
+ return {"page_id": page_id, **payload}
56
+
57
+ async def stop_tracing(self, page_id: str, *, path: str | None = None) -> dict[str, Any]:
58
+ payload = await self._page_command(page_id, "trace-stop", {"path": path})
59
+ output_path = await self._materialize_single_artifact(
60
+ payload,
61
+ page_id=page_id,
62
+ path=path,
63
+ suffix=".zip",
64
+ artifact_kind="trace",
65
+ )
66
+ return {"page_id": page_id, "path": output_path}
67
+
68
+ async def start_video(
69
+ self, page_id: str, *, width: int | None = None, height: int | None = None
70
+ ) -> dict[str, Any]:
71
+ payload = await self._page_command(
72
+ page_id, "video-start", {"width": width, "height": height}
73
+ )
74
+ return {"page_id": page_id, **payload}
75
+
76
+ async def stop_video(self, page_id: str, *, path: str | None = None) -> dict[str, Any]:
77
+ payload = await self._page_command(page_id, "video-stop", {"path": path})
78
+ return {"page_id": page_id, **payload}
79
+
80
+ async def _materialize_single_artifact(
81
+ self,
82
+ payload: dict[str, Any],
83
+ *,
84
+ page_id: str,
85
+ path: str | None,
86
+ suffix: str,
87
+ artifact_kind: str,
88
+ inline_key: str | None = None,
89
+ ) -> str:
90
+ artifacts = list(payload.get("_artifacts") or [])
91
+ if artifacts:
92
+ for artifact in artifacts:
93
+ if str(artifact.get("artifact_kind") or "") != artifact_kind:
94
+ continue
95
+ resolved = self._resolve_artifact_output_path(
96
+ artifact,
97
+ page_id=page_id,
98
+ path=path,
99
+ suffix=suffix,
100
+ )
101
+ self._write_artifact_bytes(artifact, resolved)
102
+ return str(resolved)
103
+ if inline_key and payload.get(inline_key):
104
+ output_path = self._resolve_output_path(path, page_id=page_id, suffix=suffix)
105
+ output_path.write_bytes(
106
+ base64.b64decode(str(payload.get(inline_key) or "").encode("ascii"))
107
+ )
108
+ return str(output_path)
109
+ if path:
110
+ return str(self._resolve_output_path(path, page_id=page_id, suffix=suffix))
111
+ raise RuntimeError(f"Expected extension artifact kind {artifact_kind}.")
112
+
113
+ async def _materialize_video_artifacts(self, artifacts: list[dict[str, Any]]) -> list[str]:
114
+ paths: list[str] = []
115
+ for artifact in artifacts:
116
+ if str(artifact.get("artifact_kind") or "") != "video":
117
+ continue
118
+ page_id = str(artifact.get("page_id") or "page")
119
+ resolved = self._resolve_artifact_output_path(
120
+ artifact,
121
+ page_id=page_id,
122
+ path=str((artifact.get("metadata") or {}).get("requested_path") or "").strip()
123
+ or None,
124
+ suffix=".webm",
125
+ )
126
+ self._write_artifact_bytes(artifact, resolved)
127
+ paths.append(str(resolved))
128
+ return paths
129
+
130
+ def _resolve_artifact_output_path(
131
+ self,
132
+ artifact: dict[str, Any],
133
+ *,
134
+ page_id: str,
135
+ path: str | None,
136
+ suffix: str,
137
+ ):
138
+ if path:
139
+ return self._resolve_output_path(path, page_id=page_id, suffix=suffix)
140
+ metadata = dict(artifact.get("metadata") or {})
141
+ requested = str(metadata.get("requested_path") or "").strip()
142
+ if requested:
143
+ return self._resolve_output_path(requested, page_id=page_id, suffix=suffix)
144
+ filename = str(artifact.get("filename") or "").strip()
145
+ if filename:
146
+ return self._resolve_output_path(filename, page_id=page_id, suffix=suffix)
147
+ return self._resolve_output_path(None, page_id=page_id, suffix=suffix)
148
+
149
+ @staticmethod
150
+ def _write_artifact_bytes(artifact: dict[str, Any], output_path) -> None:
151
+ content = artifact.get("content")
152
+ if not isinstance(content, bytes | bytearray):
153
+ raise RuntimeError("Extension artifact payload is missing binary content.")
154
+ output_path.write_bytes(bytes(content))
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ExtensionDriverDialogActionsMixin:
5
+ async def setup_dialog_handler(
6
+ self,
7
+ page_id: str,
8
+ *,
9
+ default_action: str = "accept",
10
+ default_prompt_text: str | None = None,
11
+ ) -> dict[str, object]:
12
+ payload = await self._page_command(
13
+ page_id,
14
+ "dialog-setup",
15
+ {"action": default_action, "text": default_prompt_text},
16
+ )
17
+ return {"page_id": page_id, **payload}
18
+
19
+ async def handle_dialog(
20
+ self,
21
+ page_id: str,
22
+ *,
23
+ accept: bool,
24
+ prompt_text: str | None = None,
25
+ ) -> dict[str, object]:
26
+ payload = await self._page_command(
27
+ page_id,
28
+ "dialog",
29
+ {"dismiss": not accept, "text": prompt_text},
30
+ )
31
+ return {"page_id": page_id, **payload}
32
+
33
+ async def remove_dialog_handler(self, page_id: str) -> dict[str, object]:
34
+ payload = await self._page_command(page_id, "dialog-remove", {})
35
+ return {"page_id": page_id, **payload}
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ExtensionDriverInputActionsMixin:
5
+ async def type_text(
6
+ self, page_id: str, text: str, *, submit: bool = False
7
+ ) -> dict[str, object]:
8
+ payload = await self._page_command(page_id, "type", {"text": text, "submit": submit})
9
+ return {"page_id": page_id, **payload}
10
+
11
+ async def press_key(self, page_id: str, key: str) -> dict[str, object]:
12
+ payload = await self._page_command(page_id, "press", {"key": key})
13
+ return {"page_id": page_id, **payload}
14
+
15
+ async def key_down(self, page_id: str, key: str) -> dict[str, object]:
16
+ payload = await self._page_command(page_id, "key-down", {"key": key})
17
+ return {"page_id": page_id, **payload}
18
+
19
+ async def key_up(self, page_id: str, key: str) -> dict[str, object]:
20
+ payload = await self._page_command(page_id, "key-up", {"key": key})
21
+ return {"page_id": page_id, **payload}
22
+
23
+ async def wheel(self, page_id: str, *, dx: int = 0, dy: int = 700) -> dict[str, object]:
24
+ payload = await self._page_command(page_id, "scroll", {"dx": dx, "dy": dy})
25
+ return {"page_id": page_id, **payload}
26
+
27
+ async def mouse_move(self, page_id: str, *, x: int, y: int) -> dict[str, object]:
28
+ payload = await self._page_command(page_id, "mouse-move", {"x": x, "y": y})
29
+ return {"page_id": page_id, **payload}
30
+
31
+ async def mouse_click(
32
+ self,
33
+ page_id: str,
34
+ *,
35
+ x: int,
36
+ y: int,
37
+ button: str = "left",
38
+ count: int = 1,
39
+ ) -> dict[str, object]:
40
+ payload = await self._page_command(
41
+ page_id,
42
+ "mouse-click",
43
+ {"x": x, "y": y, "button": button, "count": count},
44
+ )
45
+ return {"page_id": page_id, **payload}
46
+
47
+ async def mouse_drag(
48
+ self, page_id: str, *, x1: int, y1: int, x2: int, y2: int
49
+ ) -> dict[str, object]:
50
+ payload = await self._page_command(
51
+ page_id,
52
+ "mouse-drag",
53
+ {"x1": x1, "y1": y1, "x2": x2, "y2": y2},
54
+ )
55
+ return {"page_id": page_id, **payload}
56
+
57
+ async def mouse_down(self, page_id: str, *, button: str = "left") -> dict[str, object]:
58
+ payload = await self._page_command(page_id, "mouse-down", {"button": button})
59
+ return {"page_id": page_id, **payload}
60
+
61
+ async def mouse_up(self, page_id: str, *, button: str = "left") -> dict[str, object]:
62
+ payload = await self._page_command(page_id, "mouse-up", {"button": button})
63
+ return {"page_id": page_id, **payload}
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from browser_cli.refs.models import LocatorSpec
6
+
7
+
8
+ class ExtensionDriverLocatorActionsMixin:
9
+ async def click(self, page_id: str, locator: LocatorSpec) -> dict[str, Any]:
10
+ await self._locator_command(page_id, "click", locator)
11
+ return {"page_id": page_id, "ref": locator.ref, "action": "click"}
12
+
13
+ async def double_click(self, page_id: str, locator: LocatorSpec) -> dict[str, Any]:
14
+ await self._locator_command(page_id, "double-click", locator)
15
+ return {"page_id": page_id, "ref": locator.ref, "action": "double-click"}
16
+
17
+ async def hover(self, page_id: str, locator: LocatorSpec) -> dict[str, Any]:
18
+ await self._locator_command(page_id, "hover", locator)
19
+ return {"page_id": page_id, "ref": locator.ref, "action": "hover"}
20
+
21
+ async def focus(self, page_id: str, locator: LocatorSpec) -> dict[str, Any]:
22
+ await self._locator_command(page_id, "focus", locator)
23
+ return {"page_id": page_id, "ref": locator.ref, "action": "focus"}
24
+
25
+ async def fill(
26
+ self,
27
+ page_id: str,
28
+ locator: LocatorSpec,
29
+ text: str,
30
+ *,
31
+ submit: bool = False,
32
+ ) -> dict[str, Any]:
33
+ await self._locator_command(page_id, "fill", locator, {"text": text, "submit": submit})
34
+ return {"page_id": page_id, "ref": locator.ref, "filled": True, "submitted": submit}
35
+
36
+ async def select_option(self, page_id: str, locator: LocatorSpec, text: str) -> dict[str, Any]:
37
+ await self._locator_command(page_id, "select", locator, {"text": text})
38
+ return {"page_id": page_id, "ref": locator.ref, "selected": text}
39
+
40
+ async def list_options(self, page_id: str, locator: LocatorSpec) -> dict[str, Any]:
41
+ payload = await self._locator_command(page_id, "options", locator)
42
+ return {"page_id": page_id, "ref": locator.ref, **payload}
43
+
44
+ async def check(self, page_id: str, locator: LocatorSpec, *, checked: bool) -> dict[str, Any]:
45
+ await self._locator_command(page_id, "check", locator, {"checked": checked})
46
+ return {"page_id": page_id, "ref": locator.ref, "checked": checked}
47
+
48
+ async def scroll_to(self, page_id: str, locator: LocatorSpec) -> dict[str, Any]:
49
+ await self._locator_command(page_id, "scroll-to", locator)
50
+ return {"page_id": page_id, "ref": locator.ref, "scrolled": True}
51
+
52
+ async def drag(
53
+ self, page_id: str, start_locator: LocatorSpec, end_locator: LocatorSpec
54
+ ) -> dict[str, Any]:
55
+ payload = await self._page_command(
56
+ page_id,
57
+ "drag",
58
+ {"start_locator": start_locator.to_dict(), "end_locator": end_locator.to_dict()},
59
+ )
60
+ return {
61
+ "page_id": page_id,
62
+ "start_ref": start_locator.ref,
63
+ "end_ref": end_locator.ref,
64
+ **payload,
65
+ }
66
+
67
+ async def upload(self, page_id: str, locator: LocatorSpec, file_path: str) -> dict[str, Any]:
68
+ payload = await self._locator_command(page_id, "upload", locator, {"path": file_path})
69
+ return {"page_id": page_id, "ref": locator.ref, **payload}
70
+
71
+ async def evaluate_on(self, page_id: str, locator: LocatorSpec, code: str) -> dict[str, Any]:
72
+ payload = await self._locator_command(page_id, "eval-on", locator, {"code": code})
73
+ return {"page_id": page_id, "ref": locator.ref, "result": payload.get("result")}
74
+
75
+ async def verify_state(
76
+ self, page_id: str, *, locator: LocatorSpec, state: str
77
+ ) -> dict[str, Any]:
78
+ payload = await self._locator_command(page_id, "verify-state", locator, {"state": state})
79
+ return {"page_id": page_id, "ref": locator.ref, **payload}
80
+
81
+ async def verify_value(
82
+ self, page_id: str, *, locator: LocatorSpec, expected: str
83
+ ) -> dict[str, Any]:
84
+ payload = await self._locator_command(
85
+ page_id, "verify-value", locator, {"expected": expected}
86
+ )
87
+ return {"page_id": page_id, "ref": locator.ref, **payload}
88
+
89
+ async def _locator_command(
90
+ self,
91
+ page_id: str,
92
+ action: str,
93
+ locator: LocatorSpec,
94
+ payload: dict[str, Any] | None = None,
95
+ ) -> dict[str, Any]:
96
+ return await self._page_command(
97
+ page_id,
98
+ action,
99
+ {"locator": locator.to_dict(), **(payload or {})},
100
+ )