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.
- browser_cli/__init__.py +7 -0
- browser_cli/actions/__init__.py +5 -0
- browser_cli/actions/cli_specs.py +868 -0
- browser_cli/agent_scope/__init__.py +14 -0
- browser_cli/automation/__init__.py +11 -0
- browser_cli/automation/api/__init__.py +5 -0
- browser_cli/automation/api/server.py +321 -0
- browser_cli/automation/hooks.py +42 -0
- browser_cli/automation/loader.py +134 -0
- browser_cli/automation/models.py +127 -0
- browser_cli/automation/persistence/__init__.py +5 -0
- browser_cli/automation/persistence/store.py +682 -0
- browser_cli/automation/projections.py +279 -0
- browser_cli/automation/publisher.py +153 -0
- browser_cli/automation/scheduler/__init__.py +5 -0
- browser_cli/automation/scheduler/schedule.py +113 -0
- browser_cli/automation/service/__init__.py +8 -0
- browser_cli/automation/service/__main__.py +64 -0
- browser_cli/automation/service/client.py +232 -0
- browser_cli/automation/service/runtime.py +388 -0
- browser_cli/automation/toml.py +35 -0
- browser_cli/automation/web/__init__.py +5 -0
- browser_cli/automation/web/assets.py +377 -0
- browser_cli/browser/__init__.py +1 -0
- browser_cli/browser/models.py +44 -0
- browser_cli/browser/network_capture.py +392 -0
- browser_cli/browser/service.py +1519 -0
- browser_cli/browser/session.py +158 -0
- browser_cli/browser/snapshot.py +35 -0
- browser_cli/browser/stealth.py +450 -0
- browser_cli/cli/__init__.py +1 -0
- browser_cli/cli/error_hints.py +26 -0
- browser_cli/cli/main.py +298 -0
- browser_cli/commands/__init__.py +1 -0
- browser_cli/commands/action.py +25 -0
- browser_cli/commands/automation.py +304 -0
- browser_cli/commands/doctor.py +336 -0
- browser_cli/commands/install_skills.py +138 -0
- browser_cli/commands/paths.py +31 -0
- browser_cli/commands/read.py +33 -0
- browser_cli/commands/reload.py +51 -0
- browser_cli/commands/status.py +444 -0
- browser_cli/commands/task.py +71 -0
- browser_cli/constants.py +97 -0
- browser_cli/daemon/__init__.py +1 -0
- browser_cli/daemon/__main__.py +34 -0
- browser_cli/daemon/app.py +941 -0
- browser_cli/daemon/browser_service.py +998 -0
- browser_cli/daemon/client.py +280 -0
- browser_cli/daemon/models.py +63 -0
- browser_cli/daemon/runtime_presentation.py +167 -0
- browser_cli/daemon/server.py +103 -0
- browser_cli/daemon/state.py +15 -0
- browser_cli/daemon/transport.py +77 -0
- browser_cli/drivers/__init__.py +6 -0
- browser_cli/drivers/_extension/__init__.py +1 -0
- browser_cli/drivers/_extension/artifact_actions.py +154 -0
- browser_cli/drivers/_extension/dialog_actions.py +35 -0
- browser_cli/drivers/_extension/input_actions.py +63 -0
- browser_cli/drivers/_extension/locator_actions.py +100 -0
- browser_cli/drivers/_extension/observe_actions.py +285 -0
- browser_cli/drivers/_extension/page_actions.py +210 -0
- browser_cli/drivers/_extension/state_actions.py +134 -0
- browser_cli/drivers/base.py +391 -0
- browser_cli/drivers/extension_driver.py +33 -0
- browser_cli/drivers/models.py +21 -0
- browser_cli/drivers/playwright_driver.py +499 -0
- browser_cli/error_codes.py +27 -0
- browser_cli/errors.py +146 -0
- browser_cli/exit_codes.py +9 -0
- browser_cli/extension/__init__.py +32 -0
- browser_cli/extension/protocol.py +222 -0
- browser_cli/extension/session.py +393 -0
- browser_cli/network.py +147 -0
- browser_cli/outputs/__init__.py +1 -0
- browser_cli/outputs/json.py +10 -0
- browser_cli/outputs/render.py +7 -0
- browser_cli/packaged_skills/__init__.py +1 -0
- browser_cli/packaged_skills/browser-cli-converge/SKILL.md +62 -0
- browser_cli/packaged_skills/browser-cli-delivery/SKILL.md +61 -0
- browser_cli/packaged_skills/browser-cli-explore/SKILL.md +77 -0
- browser_cli/profiles/__init__.py +1 -0
- browser_cli/profiles/discovery.py +185 -0
- browser_cli/refs/__init__.py +17 -0
- browser_cli/refs/generator.py +386 -0
- browser_cli/refs/models.py +127 -0
- browser_cli/refs/registry.py +51 -0
- browser_cli/refs/resolver.py +214 -0
- browser_cli/tabs/__init__.py +5 -0
- browser_cli/tabs/registry.py +283 -0
- browser_cli/task_runtime/__init__.py +31 -0
- browser_cli/task_runtime/client.py +107 -0
- browser_cli/task_runtime/entrypoint.py +110 -0
- browser_cli/task_runtime/errors.py +17 -0
- browser_cli/task_runtime/flow.py +171 -0
- browser_cli/task_runtime/models.py +97 -0
- browser_cli/task_runtime/read.py +71 -0
- browser_cli/task_runtime/templates.py +58 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/METADATA +340 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/RECORD +104 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/WHEEL +5 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/entry_points.txt +2 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/licenses/LICENSE +21 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ExtensionDriverObserveActionsMixin:
|
|
8
|
+
async def wait(
|
|
9
|
+
self,
|
|
10
|
+
page_id: str,
|
|
11
|
+
*,
|
|
12
|
+
seconds: float | None = None,
|
|
13
|
+
text: str | None = None,
|
|
14
|
+
gone: bool = False,
|
|
15
|
+
exact: bool = False,
|
|
16
|
+
) -> dict[str, object]:
|
|
17
|
+
payload = await self._page_command(
|
|
18
|
+
page_id,
|
|
19
|
+
"wait",
|
|
20
|
+
{
|
|
21
|
+
"seconds": seconds,
|
|
22
|
+
"text": text,
|
|
23
|
+
"gone": gone,
|
|
24
|
+
"exact": exact,
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
return {"page_id": page_id, **payload}
|
|
28
|
+
|
|
29
|
+
async def wait_for_network_idle(
|
|
30
|
+
self, page_id: str, *, timeout_seconds: float = 30.0
|
|
31
|
+
) -> dict[str, object]:
|
|
32
|
+
payload = await self._page_command(
|
|
33
|
+
page_id, "wait-network", {"timeout_seconds": timeout_seconds}
|
|
34
|
+
)
|
|
35
|
+
return {"page_id": page_id, **payload}
|
|
36
|
+
|
|
37
|
+
async def start_console_capture(self, page_id: str) -> dict[str, object]:
|
|
38
|
+
payload = await self._page_command(page_id, "console-start", {})
|
|
39
|
+
return {"page_id": page_id, **payload}
|
|
40
|
+
|
|
41
|
+
async def get_console_messages(
|
|
42
|
+
self,
|
|
43
|
+
page_id: str,
|
|
44
|
+
*,
|
|
45
|
+
message_type: str | None = None,
|
|
46
|
+
clear: bool = True,
|
|
47
|
+
) -> dict[str, object]:
|
|
48
|
+
payload = await self._page_command(
|
|
49
|
+
page_id,
|
|
50
|
+
"console-get",
|
|
51
|
+
{"message_type": message_type, "clear": clear},
|
|
52
|
+
)
|
|
53
|
+
return {"page_id": page_id, **payload}
|
|
54
|
+
|
|
55
|
+
async def stop_console_capture(self, page_id: str) -> dict[str, object]:
|
|
56
|
+
payload = await self._page_command(page_id, "console-stop", {})
|
|
57
|
+
return {"page_id": page_id, **payload}
|
|
58
|
+
|
|
59
|
+
async def start_network_capture(self, page_id: str) -> dict[str, object]:
|
|
60
|
+
payload = await self._page_command(page_id, "network-start", {})
|
|
61
|
+
return {"page_id": page_id, **payload}
|
|
62
|
+
|
|
63
|
+
async def wait_for_network_record(
|
|
64
|
+
self,
|
|
65
|
+
page_id: str,
|
|
66
|
+
*,
|
|
67
|
+
url_contains: str | None = None,
|
|
68
|
+
url_regex: str | None = None,
|
|
69
|
+
method: str | None = None,
|
|
70
|
+
status: int | None = None,
|
|
71
|
+
resource_type: str | None = None,
|
|
72
|
+
mime_contains: str | None = None,
|
|
73
|
+
include_static: bool = False,
|
|
74
|
+
timeout_seconds: float = 30.0,
|
|
75
|
+
) -> dict[str, object]:
|
|
76
|
+
payload = await self._page_command(
|
|
77
|
+
page_id,
|
|
78
|
+
"network-wait",
|
|
79
|
+
{
|
|
80
|
+
"url_contains": url_contains,
|
|
81
|
+
"url_regex": url_regex,
|
|
82
|
+
"method": method,
|
|
83
|
+
"status": status,
|
|
84
|
+
"resource_type": resource_type,
|
|
85
|
+
"mime_contains": mime_contains,
|
|
86
|
+
"include_static": include_static,
|
|
87
|
+
"timeout_seconds": timeout_seconds,
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
await self._materialize_network_body_artifacts(payload, page_id=page_id)
|
|
91
|
+
return {"page_id": page_id, **payload}
|
|
92
|
+
|
|
93
|
+
async def get_network_records(
|
|
94
|
+
self,
|
|
95
|
+
page_id: str,
|
|
96
|
+
*,
|
|
97
|
+
url_contains: str | None = None,
|
|
98
|
+
url_regex: str | None = None,
|
|
99
|
+
method: str | None = None,
|
|
100
|
+
status: int | None = None,
|
|
101
|
+
resource_type: str | None = None,
|
|
102
|
+
mime_contains: str | None = None,
|
|
103
|
+
include_static: bool = False,
|
|
104
|
+
clear: bool = True,
|
|
105
|
+
) -> dict[str, object]:
|
|
106
|
+
payload = await self._page_command(
|
|
107
|
+
page_id,
|
|
108
|
+
"network",
|
|
109
|
+
{
|
|
110
|
+
"url_contains": url_contains,
|
|
111
|
+
"url_regex": url_regex,
|
|
112
|
+
"method": method,
|
|
113
|
+
"status": status,
|
|
114
|
+
"resource_type": resource_type,
|
|
115
|
+
"mime_contains": mime_contains,
|
|
116
|
+
"include_static": include_static,
|
|
117
|
+
"clear": clear,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
await self._materialize_network_body_artifacts(payload, page_id=page_id)
|
|
121
|
+
return {"page_id": page_id, **payload}
|
|
122
|
+
|
|
123
|
+
async def stop_network_capture(self, page_id: str) -> dict[str, object]:
|
|
124
|
+
payload = await self._page_command(page_id, "network-stop", {})
|
|
125
|
+
return {"page_id": page_id, **payload}
|
|
126
|
+
|
|
127
|
+
async def _materialize_network_body_artifacts(
|
|
128
|
+
self, payload: dict[str, object], *, page_id: str
|
|
129
|
+
) -> None:
|
|
130
|
+
artifacts = list(payload.get("_artifacts") or [])
|
|
131
|
+
artifact_paths: dict[str, str] = {}
|
|
132
|
+
for artifact in artifacts:
|
|
133
|
+
if str(artifact.get("artifact_kind") or "") != "network-body":
|
|
134
|
+
continue
|
|
135
|
+
artifact_id = str(artifact.get("artifact_id") or "").strip()
|
|
136
|
+
if not artifact_id:
|
|
137
|
+
continue
|
|
138
|
+
resolved = self._resolve_artifact_output_path(
|
|
139
|
+
artifact,
|
|
140
|
+
page_id=page_id,
|
|
141
|
+
path=None,
|
|
142
|
+
suffix="",
|
|
143
|
+
)
|
|
144
|
+
self._write_artifact_bytes(artifact, resolved)
|
|
145
|
+
artifact_paths[artifact_id] = str(resolved)
|
|
146
|
+
if "record" in payload and isinstance(payload.get("record"), dict):
|
|
147
|
+
self._rewrite_network_body_path(payload["record"], artifact_paths)
|
|
148
|
+
records = payload.get("records")
|
|
149
|
+
if isinstance(records, list):
|
|
150
|
+
for record in records:
|
|
151
|
+
if isinstance(record, dict):
|
|
152
|
+
self._rewrite_network_body_path(record, artifact_paths)
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _rewrite_network_body_path(
|
|
156
|
+
record: dict[str, object], artifact_paths: dict[str, str]
|
|
157
|
+
) -> None:
|
|
158
|
+
body = record.get("body")
|
|
159
|
+
if not isinstance(body, dict):
|
|
160
|
+
return
|
|
161
|
+
raw_path = str(body.get("path") or "")
|
|
162
|
+
if not raw_path.startswith("artifact://"):
|
|
163
|
+
return
|
|
164
|
+
artifact_id = raw_path.removeprefix("artifact://")
|
|
165
|
+
resolved_path = artifact_paths.get(artifact_id)
|
|
166
|
+
if resolved_path:
|
|
167
|
+
body["path"] = resolved_path
|
|
168
|
+
|
|
169
|
+
async def get_cookies(
|
|
170
|
+
self,
|
|
171
|
+
page_id: str,
|
|
172
|
+
*,
|
|
173
|
+
name: str | None = None,
|
|
174
|
+
domain: str | None = None,
|
|
175
|
+
path: str | None = None,
|
|
176
|
+
) -> dict[str, object]:
|
|
177
|
+
payload = await self._page_command(
|
|
178
|
+
page_id,
|
|
179
|
+
"cookies-get",
|
|
180
|
+
{"name": name, "domain": domain, "path": path},
|
|
181
|
+
)
|
|
182
|
+
return {"page_id": page_id, **payload}
|
|
183
|
+
|
|
184
|
+
async def set_cookie(
|
|
185
|
+
self,
|
|
186
|
+
page_id: str,
|
|
187
|
+
*,
|
|
188
|
+
name: str,
|
|
189
|
+
value: str,
|
|
190
|
+
domain: str | None = None,
|
|
191
|
+
path: str = "/",
|
|
192
|
+
expires: float | None = None,
|
|
193
|
+
http_only: bool = False,
|
|
194
|
+
secure: bool = False,
|
|
195
|
+
same_site: str | None = None,
|
|
196
|
+
) -> dict[str, object]:
|
|
197
|
+
payload = await self._page_command(
|
|
198
|
+
page_id,
|
|
199
|
+
"cookie-set",
|
|
200
|
+
{
|
|
201
|
+
"name": name,
|
|
202
|
+
"value": value,
|
|
203
|
+
"domain": domain,
|
|
204
|
+
"path": path,
|
|
205
|
+
"expires": expires,
|
|
206
|
+
"http_only": http_only,
|
|
207
|
+
"secure": secure,
|
|
208
|
+
"same_site": same_site,
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
return {"page_id": page_id, **payload}
|
|
212
|
+
|
|
213
|
+
async def clear_cookies(
|
|
214
|
+
self,
|
|
215
|
+
page_id: str,
|
|
216
|
+
*,
|
|
217
|
+
name: str | None = None,
|
|
218
|
+
domain: str | None = None,
|
|
219
|
+
path: str | None = None,
|
|
220
|
+
) -> dict[str, object]:
|
|
221
|
+
payload = await self._page_command(
|
|
222
|
+
page_id,
|
|
223
|
+
"cookies-clear",
|
|
224
|
+
{"name": name, "domain": domain, "path": path},
|
|
225
|
+
)
|
|
226
|
+
return {"page_id": page_id, **payload}
|
|
227
|
+
|
|
228
|
+
async def save_storage_state(
|
|
229
|
+
self, page_id: str, *, path: str | None = None
|
|
230
|
+
) -> dict[str, object]:
|
|
231
|
+
payload = await self._page_command(page_id, "storage-get", {})
|
|
232
|
+
output_path = self._resolve_output_path(path, page_id=page_id, suffix=".json")
|
|
233
|
+
output_path.write_text(json.dumps(payload, ensure_ascii=True, indent=2), encoding="utf-8")
|
|
234
|
+
return {"page_id": page_id, "path": str(output_path)}
|
|
235
|
+
|
|
236
|
+
async def load_storage_state(self, page_id: str, *, path: str) -> dict[str, object]:
|
|
237
|
+
payload = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
238
|
+
result = await self._page_command(page_id, "storage-set", payload)
|
|
239
|
+
return {"page_id": page_id, **result}
|
|
240
|
+
|
|
241
|
+
async def verify_text(
|
|
242
|
+
self,
|
|
243
|
+
page_id: str,
|
|
244
|
+
*,
|
|
245
|
+
text: str,
|
|
246
|
+
exact: bool = False,
|
|
247
|
+
timeout_seconds: float = 5.0,
|
|
248
|
+
) -> dict[str, object]:
|
|
249
|
+
payload = await self._page_command(
|
|
250
|
+
page_id,
|
|
251
|
+
"verify-text",
|
|
252
|
+
{"text": text, "exact": exact, "timeout_seconds": timeout_seconds},
|
|
253
|
+
)
|
|
254
|
+
return {"page_id": page_id, **payload}
|
|
255
|
+
|
|
256
|
+
async def verify_url(
|
|
257
|
+
self, page_id: str, *, expected: str, exact: bool = False
|
|
258
|
+
) -> dict[str, object]:
|
|
259
|
+
payload = await self._page_command(
|
|
260
|
+
page_id, "verify-url", {"expected": expected, "exact": exact}
|
|
261
|
+
)
|
|
262
|
+
return {"page_id": page_id, **payload}
|
|
263
|
+
|
|
264
|
+
async def verify_title(
|
|
265
|
+
self, page_id: str, *, expected: str, exact: bool = False
|
|
266
|
+
) -> dict[str, object]:
|
|
267
|
+
payload = await self._page_command(
|
|
268
|
+
page_id, "verify-title", {"expected": expected, "exact": exact}
|
|
269
|
+
)
|
|
270
|
+
return {"page_id": page_id, **payload}
|
|
271
|
+
|
|
272
|
+
async def verify_visible(
|
|
273
|
+
self,
|
|
274
|
+
page_id: str,
|
|
275
|
+
*,
|
|
276
|
+
role: str,
|
|
277
|
+
name: str,
|
|
278
|
+
timeout_seconds: float = 5.0,
|
|
279
|
+
) -> dict[str, object]:
|
|
280
|
+
payload = await self._page_command(
|
|
281
|
+
page_id,
|
|
282
|
+
"verify-visible",
|
|
283
|
+
{"role": role, "name": name, "timeout_seconds": timeout_seconds},
|
|
284
|
+
)
|
|
285
|
+
return {"page_id": page_id, **payload}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from browser_cli.refs.models import SnapshotInput
|
|
7
|
+
|
|
8
|
+
from ..models import TabState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExtensionDriverPageActionsMixin:
|
|
12
|
+
async def new_tab(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
page_id: str,
|
|
16
|
+
url: str | None = None,
|
|
17
|
+
wait_until: str = "load",
|
|
18
|
+
timeout_seconds: float | None = None,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
session = await self._require_session()
|
|
21
|
+
payload = await session.send_request(
|
|
22
|
+
"open-tab",
|
|
23
|
+
{
|
|
24
|
+
"page_id": page_id,
|
|
25
|
+
"url": url,
|
|
26
|
+
"wait_until": wait_until,
|
|
27
|
+
"timeout_seconds": timeout_seconds,
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
tab_id = int(payload["tab_id"])
|
|
31
|
+
self._page_to_tab[page_id] = tab_id
|
|
32
|
+
self._tab_to_page[tab_id] = page_id
|
|
33
|
+
self._active_page_id = page_id
|
|
34
|
+
return {
|
|
35
|
+
"page_id": page_id,
|
|
36
|
+
"url": str(payload.get("url") or url or ""),
|
|
37
|
+
"title": str(payload.get("title") or ""),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async def close_tab(self, page_id: str) -> dict[str, Any]:
|
|
41
|
+
tab_id = self._require_tab_id(page_id)
|
|
42
|
+
session = await self._require_session()
|
|
43
|
+
payload = await session.send_request("close-tab", {"tab_id": tab_id})
|
|
44
|
+
self._page_to_tab.pop(page_id, None)
|
|
45
|
+
self._tab_to_page.pop(tab_id, None)
|
|
46
|
+
if self._active_page_id == page_id:
|
|
47
|
+
self._active_page_id = next(iter(self._page_to_tab.keys()), None)
|
|
48
|
+
result = {
|
|
49
|
+
"page_id": page_id,
|
|
50
|
+
"closed": True,
|
|
51
|
+
"url": str(payload.get("url") or ""),
|
|
52
|
+
"title": str(payload.get("title") or ""),
|
|
53
|
+
}
|
|
54
|
+
video_paths = await self._materialize_video_artifacts(payload.get("_artifacts") or [])
|
|
55
|
+
if video_paths:
|
|
56
|
+
result["video_path"] = video_paths[0]
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
async def list_tabs(self) -> list[TabState]:
|
|
60
|
+
session = await self._require_session()
|
|
61
|
+
payload = await session.send_request("list-tabs", {})
|
|
62
|
+
results: list[TabState] = []
|
|
63
|
+
for item in payload.get("tabs", []):
|
|
64
|
+
try:
|
|
65
|
+
tab_id = int(item["tab_id"])
|
|
66
|
+
except Exception:
|
|
67
|
+
continue
|
|
68
|
+
page_id = self._tab_to_page.get(tab_id)
|
|
69
|
+
if not page_id:
|
|
70
|
+
continue
|
|
71
|
+
results.append(
|
|
72
|
+
TabState(
|
|
73
|
+
page_id=page_id,
|
|
74
|
+
url=str(item.get("url") or ""),
|
|
75
|
+
title=str(item.get("title") or ""),
|
|
76
|
+
active=bool(item.get("active")),
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
if item.get("active"):
|
|
80
|
+
self._active_page_id = page_id
|
|
81
|
+
return results
|
|
82
|
+
|
|
83
|
+
async def switch_tab(self, page_id: str) -> dict[str, Any]:
|
|
84
|
+
tab_id = self._require_tab_id(page_id)
|
|
85
|
+
session = await self._require_session()
|
|
86
|
+
payload = await session.send_request("activate-tab", {"tab_id": tab_id})
|
|
87
|
+
self._active_page_id = page_id
|
|
88
|
+
return {
|
|
89
|
+
"page_id": page_id,
|
|
90
|
+
"url": str(payload.get("url") or ""),
|
|
91
|
+
"title": str(payload.get("title") or ""),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async def get_page_summary(self, page_id: str) -> dict[str, Any]:
|
|
95
|
+
tab_id = self._require_tab_id(page_id)
|
|
96
|
+
session = await self._require_session()
|
|
97
|
+
payload = await session.send_request("page-summary", {"tab_id": tab_id})
|
|
98
|
+
return {
|
|
99
|
+
"page_id": page_id,
|
|
100
|
+
"url": str(payload.get("url") or ""),
|
|
101
|
+
"title": str(payload.get("title") or ""),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async def get_page_info(self, page_id: str) -> dict[str, Any]:
|
|
105
|
+
tab_id = self._require_tab_id(page_id)
|
|
106
|
+
session = await self._require_session()
|
|
107
|
+
payload = await session.send_request("page-info", {"tab_id": tab_id})
|
|
108
|
+
return {"page_id": page_id, **payload}
|
|
109
|
+
|
|
110
|
+
async def capture_html(self, page_id: str) -> dict[str, Any]:
|
|
111
|
+
tab_id = self._require_tab_id(page_id)
|
|
112
|
+
session = await self._require_session()
|
|
113
|
+
payload = await session.send_request("capture-html", {"tab_id": tab_id})
|
|
114
|
+
return {"page_id": page_id, "html": str(payload.get("html") or "")}
|
|
115
|
+
|
|
116
|
+
async def capture_snapshot_input(
|
|
117
|
+
self,
|
|
118
|
+
page_id: str,
|
|
119
|
+
*,
|
|
120
|
+
interactive: bool = False,
|
|
121
|
+
full_page: bool = True,
|
|
122
|
+
) -> SnapshotInput:
|
|
123
|
+
tab_id = self._require_tab_id(page_id)
|
|
124
|
+
session = await self._require_session()
|
|
125
|
+
payload = await session.send_request(
|
|
126
|
+
"capture-snapshot-input",
|
|
127
|
+
{
|
|
128
|
+
"tab_id": tab_id,
|
|
129
|
+
"interactive": interactive,
|
|
130
|
+
"full_page": full_page,
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
return SnapshotInput(
|
|
134
|
+
raw_snapshot=str(payload.get("raw_snapshot") or ""),
|
|
135
|
+
captured_url=str(payload.get("captured_url") or ""),
|
|
136
|
+
captured_at=float(payload.get("captured_at") or time.time()),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def navigate(
|
|
140
|
+
self,
|
|
141
|
+
page_id: str,
|
|
142
|
+
url: str,
|
|
143
|
+
*,
|
|
144
|
+
wait_until: str = "load",
|
|
145
|
+
timeout_seconds: float = 30.0,
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
tab_id = self._require_tab_id(page_id)
|
|
148
|
+
session = await self._require_session()
|
|
149
|
+
payload = await session.send_request(
|
|
150
|
+
"navigate",
|
|
151
|
+
{
|
|
152
|
+
"tab_id": tab_id,
|
|
153
|
+
"url": url,
|
|
154
|
+
"wait_until": wait_until,
|
|
155
|
+
"timeout_seconds": timeout_seconds,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
return {
|
|
159
|
+
"page_id": page_id,
|
|
160
|
+
"url": str(payload.get("url") or url),
|
|
161
|
+
"title": str(payload.get("title") or ""),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async def reload(
|
|
165
|
+
self,
|
|
166
|
+
page_id: str,
|
|
167
|
+
*,
|
|
168
|
+
wait_until: str = "load",
|
|
169
|
+
timeout_seconds: float = 30.0,
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
return await self._simple_page_command(
|
|
172
|
+
page_id,
|
|
173
|
+
"reload",
|
|
174
|
+
{"wait_until": wait_until, "timeout_seconds": timeout_seconds},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
async def go_back(self, page_id: str) -> dict[str, Any]:
|
|
178
|
+
return await self._simple_page_command(page_id, "go-back", {})
|
|
179
|
+
|
|
180
|
+
async def go_forward(self, page_id: str) -> dict[str, Any]:
|
|
181
|
+
return await self._simple_page_command(page_id, "go-forward", {})
|
|
182
|
+
|
|
183
|
+
async def resize(self, page_id: str, *, width: int, height: int) -> dict[str, Any]:
|
|
184
|
+
payload = await self._page_command(page_id, "resize", {"width": width, "height": height})
|
|
185
|
+
return {
|
|
186
|
+
"page_id": page_id,
|
|
187
|
+
"width": int(payload.get("width") or width),
|
|
188
|
+
"height": int(payload.get("height") or height),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async def evaluate(self, page_id: str, code: str) -> dict[str, Any]:
|
|
192
|
+
payload = await self._page_command(page_id, "eval", {"code": code})
|
|
193
|
+
return {"page_id": page_id, "result": payload.get("result")}
|
|
194
|
+
|
|
195
|
+
async def _simple_page_command(
|
|
196
|
+
self, page_id: str, action: str, payload: dict[str, Any]
|
|
197
|
+
) -> dict[str, Any]:
|
|
198
|
+
payload = await self._page_command(page_id, action, payload)
|
|
199
|
+
return {
|
|
200
|
+
"page_id": page_id,
|
|
201
|
+
"url": str(payload.get("url") or ""),
|
|
202
|
+
"title": str(payload.get("title") or ""),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async def _page_command(
|
|
206
|
+
self, page_id: str, action: str, payload: dict[str, Any]
|
|
207
|
+
) -> dict[str, Any]:
|
|
208
|
+
tab_id = self._require_tab_id(page_id)
|
|
209
|
+
session = await self._require_session()
|
|
210
|
+
return await session.send_request(action, {"tab_id": tab_id, **payload})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from browser_cli.errors import OperationFailedError, TabNotFoundError
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from browser_cli.extension import ExtensionHub
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExtensionDriverStateMixin:
|
|
13
|
+
_hub: ExtensionHub
|
|
14
|
+
_page_to_tab: dict[str, int]
|
|
15
|
+
_tab_to_page: dict[int, str]
|
|
16
|
+
_active_page_id: str | None
|
|
17
|
+
|
|
18
|
+
async def ensure_started(self) -> None:
|
|
19
|
+
await self._hub.ensure_started()
|
|
20
|
+
if self._hub.session is None:
|
|
21
|
+
raise OperationFailedError(
|
|
22
|
+
"Browser CLI extension is not connected.",
|
|
23
|
+
error_code="EXTENSION_UNAVAILABLE",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
async def stop(self) -> dict[str, Any]:
|
|
27
|
+
closed_pages = sorted(self._page_to_tab.keys())
|
|
28
|
+
session = self._hub.session
|
|
29
|
+
payload: dict[str, Any] = {}
|
|
30
|
+
cleanup_error: str | None = None
|
|
31
|
+
if session is not None:
|
|
32
|
+
try:
|
|
33
|
+
payload = await session.send_request("workspace-close", {})
|
|
34
|
+
except Exception as exc:
|
|
35
|
+
cleanup_error = str(exc)
|
|
36
|
+
payload = {}
|
|
37
|
+
video_paths = await self._materialize_video_artifacts(payload.get("_artifacts") or [])
|
|
38
|
+
self._page_to_tab.clear()
|
|
39
|
+
self._tab_to_page.clear()
|
|
40
|
+
self._active_page_id = None
|
|
41
|
+
result = {
|
|
42
|
+
"closed_pages": closed_pages,
|
|
43
|
+
"extension_connected": session is not None,
|
|
44
|
+
"video_paths": video_paths,
|
|
45
|
+
}
|
|
46
|
+
if cleanup_error:
|
|
47
|
+
result["cleanup_error"] = cleanup_error
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
async def workspace_status(self) -> dict[str, Any]:
|
|
51
|
+
session = await self._require_session()
|
|
52
|
+
payload = await session.send_request("workspace-status", {})
|
|
53
|
+
return {
|
|
54
|
+
"window_id": payload.get("window_id"),
|
|
55
|
+
"tab_count": int(payload.get("tab_count") or 0),
|
|
56
|
+
"managed_tab_count": int(payload.get("managed_tab_count") or 0),
|
|
57
|
+
"binding_state": str(payload.get("binding_state") or "absent"),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async def rebuild_workspace_binding(self) -> dict[str, Any]:
|
|
61
|
+
session = await self._require_session()
|
|
62
|
+
payload = await session.send_request("workspace-rebuild-binding", {})
|
|
63
|
+
video_paths = await self._materialize_video_artifacts(payload.get("_artifacts") or [])
|
|
64
|
+
self._page_to_tab.clear()
|
|
65
|
+
self._tab_to_page.clear()
|
|
66
|
+
self._active_page_id = None
|
|
67
|
+
return {
|
|
68
|
+
"rebuilt": bool(payload.get("rebuilt")),
|
|
69
|
+
"workspace_window_state": {
|
|
70
|
+
"window_id": payload.get("window_id"),
|
|
71
|
+
"tab_count": int(payload.get("tab_count") or 0),
|
|
72
|
+
"managed_tab_count": int(payload.get("managed_tab_count") or 0),
|
|
73
|
+
"binding_state": str(payload.get("binding_state") or "absent"),
|
|
74
|
+
},
|
|
75
|
+
"video_paths": video_paths,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async def health(self):
|
|
79
|
+
session = self._hub.session
|
|
80
|
+
if session is None:
|
|
81
|
+
from ..models import DriverHealth
|
|
82
|
+
|
|
83
|
+
return DriverHealth(name=self.name, available=False, details={"connected": False})
|
|
84
|
+
hello = session.hello
|
|
85
|
+
from ..models import DriverHealth
|
|
86
|
+
|
|
87
|
+
return DriverHealth(
|
|
88
|
+
name=self.name,
|
|
89
|
+
available=hello.has_required_capabilities(),
|
|
90
|
+
details={
|
|
91
|
+
"connected": True,
|
|
92
|
+
"extension_version": hello.extension_version,
|
|
93
|
+
"browser_name": hello.browser_name,
|
|
94
|
+
"browser_version": hello.browser_version,
|
|
95
|
+
"capabilities": sorted(hello.capabilities),
|
|
96
|
+
"capability_complete": hello.has_required_capabilities(),
|
|
97
|
+
"missing_capabilities": hello.missing_required_capabilities(),
|
|
98
|
+
"workspace_window_state": dict(hello.workspace_window_state),
|
|
99
|
+
"extension_instance_id": hello.extension_instance_id,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async def _require_session(self):
|
|
104
|
+
await self.ensure_started()
|
|
105
|
+
session = self._hub.session
|
|
106
|
+
if session is None:
|
|
107
|
+
raise OperationFailedError(
|
|
108
|
+
"Browser CLI extension is not connected.", error_code="EXTENSION_UNAVAILABLE"
|
|
109
|
+
)
|
|
110
|
+
return session
|
|
111
|
+
|
|
112
|
+
def _require_tab_id(self, page_id: str) -> int:
|
|
113
|
+
tab_id = self._page_to_tab.get(page_id)
|
|
114
|
+
if tab_id is None:
|
|
115
|
+
raise TabNotFoundError()
|
|
116
|
+
return tab_id
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _resolve_output_path(path: str | None, *, page_id: str, suffix: str) -> Path:
|
|
120
|
+
if not path:
|
|
121
|
+
from browser_cli.constants import get_app_paths
|
|
122
|
+
|
|
123
|
+
output = get_app_paths().artifacts_dir / f"{page_id}{suffix}"
|
|
124
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
return output.resolve()
|
|
126
|
+
raw = Path(path).expanduser()
|
|
127
|
+
if not raw.is_absolute():
|
|
128
|
+
from browser_cli.constants import get_app_paths
|
|
129
|
+
|
|
130
|
+
raw = (get_app_paths().artifacts_dir / raw).resolve()
|
|
131
|
+
if raw.suffix.lower() != suffix:
|
|
132
|
+
raw = raw.with_suffix(suffix)
|
|
133
|
+
raw.parent.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
return raw
|