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,941 @@
1
+ """Daemon application and action handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import traceback
7
+ from collections.abc import Awaitable, Callable
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from browser_cli import error_codes
12
+ from browser_cli.constants import DEFAULT_PUBLIC_AGENT_ID
13
+ from browser_cli.errors import (
14
+ BrowserCliError,
15
+ InvalidInputError,
16
+ NoActiveTabError,
17
+ OperationFailedError,
18
+ )
19
+ from browser_cli.profiles.discovery import ChromeEnvironment
20
+
21
+ from .models import DaemonRequest, DaemonResponse
22
+ from .runtime_presentation import build_runtime_presentation
23
+ from .state import DaemonState
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ Handler = Callable[[DaemonRequest], Awaitable[dict[str, Any]]]
28
+
29
+
30
+ class BrowserDaemonApp:
31
+ def __init__(self, state: DaemonState | None = None) -> None:
32
+ self._state = state or DaemonState()
33
+ self._handlers: dict[str, Handler] = {
34
+ "runtime-status": self._handle_runtime_status,
35
+ "open": self._handle_open,
36
+ "search": self._handle_search,
37
+ "tabs": self._handle_tabs,
38
+ "switch-tab": self._handle_switch_tab,
39
+ "new-tab": self._handle_new_tab,
40
+ "close-tab": self._handle_close_tab,
41
+ "close": self._handle_close,
42
+ "stop": self._handle_stop,
43
+ "info": self._handle_info,
44
+ "read-page": self._handle_read_page,
45
+ "html": self._handle_html,
46
+ "snapshot": self._handle_snapshot,
47
+ "reload": self._handle_reload,
48
+ "back": self._handle_back,
49
+ "forward": self._handle_forward,
50
+ "click": self._handle_click,
51
+ "double-click": self._handle_double_click,
52
+ "hover": self._handle_hover,
53
+ "focus": self._handle_focus,
54
+ "fill": self._handle_fill,
55
+ "fill-form": self._handle_fill_form,
56
+ "select": self._handle_select,
57
+ "options": self._handle_options,
58
+ "check": self._handle_check,
59
+ "uncheck": self._handle_uncheck,
60
+ "scroll-to": self._handle_scroll_to,
61
+ "drag": self._handle_drag,
62
+ "upload": self._handle_upload,
63
+ "type": self._handle_type,
64
+ "press": self._handle_press,
65
+ "key-down": self._handle_key_down,
66
+ "key-up": self._handle_key_up,
67
+ "scroll": self._handle_scroll,
68
+ "mouse-click": self._handle_mouse_click,
69
+ "mouse-move": self._handle_mouse_move,
70
+ "mouse-drag": self._handle_mouse_drag,
71
+ "mouse-down": self._handle_mouse_down,
72
+ "mouse-up": self._handle_mouse_up,
73
+ "eval": self._handle_eval,
74
+ "eval-on": self._handle_eval_on,
75
+ "wait": self._handle_wait,
76
+ "wait-network": self._handle_wait_network,
77
+ "screenshot": self._handle_screenshot,
78
+ "pdf": self._handle_pdf,
79
+ "console-start": self._handle_console_start,
80
+ "console": self._handle_console,
81
+ "console-stop": self._handle_console_stop,
82
+ "network-wait": self._handle_network_wait,
83
+ "network-start": self._handle_network_start,
84
+ "network": self._handle_network,
85
+ "network-stop": self._handle_network_stop,
86
+ "dialog-setup": self._handle_dialog_setup,
87
+ "dialog": self._handle_dialog,
88
+ "dialog-remove": self._handle_dialog_remove,
89
+ "cookies": self._handle_cookies,
90
+ "cookie-set": self._handle_cookie_set,
91
+ "cookies-clear": self._handle_cookies_clear,
92
+ "storage-save": self._handle_storage_save,
93
+ "storage-load": self._handle_storage_load,
94
+ "verify-text": self._handle_verify_text,
95
+ "verify-visible": self._handle_verify_visible,
96
+ "verify-url": self._handle_verify_url,
97
+ "verify-title": self._handle_verify_title,
98
+ "verify-state": self._handle_verify_state,
99
+ "verify-value": self._handle_verify_value,
100
+ "trace-start": self._handle_trace_start,
101
+ "trace-chunk": self._handle_trace_chunk,
102
+ "trace-stop": self._handle_trace_stop,
103
+ "video-start": self._handle_video_start,
104
+ "video-stop": self._handle_video_stop,
105
+ "resize": self._handle_resize,
106
+ }
107
+
108
+ @property
109
+ def state(self) -> DaemonState:
110
+ return self._state
111
+
112
+ async def execute(self, request: DaemonRequest) -> DaemonResponse:
113
+ agent_id = request.agent_id or DEFAULT_PUBLIC_AGENT_ID
114
+ request.agent_id = agent_id
115
+ handler = self._handlers.get(request.action)
116
+ if handler is None:
117
+ return self._error_response(
118
+ InvalidInputError(f"Unknown command: {request.action}"),
119
+ request=request,
120
+ )
121
+ runtime_meta: dict[str, Any] = {}
122
+ command_started = False
123
+ try:
124
+ self._maybe_configure_browser_environment(request.args)
125
+ if request.action not in {"runtime-status", "stop"}:
126
+ await self._state.browser_service.begin_command(request.action)
127
+ command_started = True
128
+ data = await handler(request)
129
+ meta = {
130
+ "action": request.action,
131
+ "agent_id": agent_id,
132
+ }
133
+ if command_started:
134
+ runtime_meta = await self._state.browser_service.end_command()
135
+ meta.update(runtime_meta)
136
+ if runtime_meta.get("driver_reason") == "extension-connected":
137
+ meta["runtime_note"] = "Browser CLI restored extension mode at a safe point."
138
+ elif runtime_meta.get("driver_reason") == "workspace-binding-rebuilt":
139
+ meta["runtime_note"] = "Browser CLI rebuilt its owned workspace binding."
140
+ else:
141
+ meta["driver"] = self._state.browser_service.active_driver_name
142
+ chrome_environment = self._state.browser_service.chrome_environment
143
+ if chrome_environment is not None:
144
+ meta["profile_source"] = chrome_environment.source
145
+ return DaemonResponse.success(data, meta=meta)
146
+ except BrowserCliError as exc:
147
+ if command_started:
148
+ runtime_meta = await self._state.browser_service.end_command()
149
+ return self._error_response(exc, request=request)
150
+ except Exception as exc: # pragma: no cover - last-resort daemon guard
151
+ if command_started:
152
+ runtime_meta = await self._state.browser_service.end_command()
153
+ logger.exception("Unexpected daemon failure in action=%s", request.action)
154
+ return self._error_response(
155
+ OperationFailedError(
156
+ f"Unexpected daemon failure: {exc}\n{traceback.format_exc()}",
157
+ error_code=error_codes.INTERNAL_ERROR,
158
+ ),
159
+ request=request,
160
+ )
161
+
162
+ def _error_response(self, exc: BrowserCliError, *, request: DaemonRequest) -> DaemonResponse:
163
+ return DaemonResponse.failure(
164
+ error_code=exc.error_code,
165
+ error_message=exc.message,
166
+ meta={
167
+ "action": request.action,
168
+ "agent_id": request.agent_id,
169
+ "driver": self._state.browser_service.active_driver_name,
170
+ },
171
+ )
172
+
173
+ async def _handle_stop(self, request: DaemonRequest) -> dict[str, Any]:
174
+ shutdown = await self._state.browser_service.stop()
175
+ self._state.shutdown_event.set()
176
+ return {"stopped": True, **shutdown}
177
+
178
+ async def _handle_runtime_status(self, request: DaemonRequest) -> dict[str, Any]:
179
+ warmup = bool(request.args.get("warmup"))
180
+ raw_status = await self._state.browser_service.runtime_status(warmup=warmup)
181
+ return {
182
+ **raw_status,
183
+ "presentation": build_runtime_presentation(raw_status),
184
+ }
185
+
186
+ async def _handle_open(self, request: DaemonRequest) -> dict[str, Any]:
187
+ url = self._require_str(request.args, "url")
188
+ page = await self._state.browser_service.new_tab(url=url)
189
+ await self._state.tabs.add_tab(
190
+ page_id=str(page["page_id"]),
191
+ owner_agent_id=request.agent_id,
192
+ url=str(page["url"]),
193
+ title=str(page["title"]),
194
+ )
195
+ return {"page": page}
196
+
197
+ async def _handle_search(self, request: DaemonRequest) -> dict[str, Any]:
198
+ query = self._require_str(request.args, "query")
199
+ engine = str(request.args.get("engine") or "duckduckgo")
200
+ page = await self._state.browser_service.search(query=query, engine=engine)
201
+ await self._state.tabs.add_tab(
202
+ page_id=str(page["page_id"]),
203
+ owner_agent_id=request.agent_id,
204
+ url=str(page["url"]),
205
+ title=str(page["title"]),
206
+ )
207
+ return {"page": page}
208
+
209
+ async def _handle_new_tab(self, request: DaemonRequest) -> dict[str, Any]:
210
+ url = str(request.args.get("url") or "").strip() or None
211
+ page = await self._state.browser_service.new_tab(url=url)
212
+ await self._state.tabs.add_tab(
213
+ page_id=str(page["page_id"]),
214
+ owner_agent_id=request.agent_id,
215
+ url=str(page["url"]),
216
+ title=str(page["title"]),
217
+ )
218
+ return {"page": page}
219
+
220
+ async def _handle_tabs(self, request: DaemonRequest) -> dict[str, Any]:
221
+ tabs = await self._state.tabs.list_tabs(request.agent_id)
222
+ current_active: str | None = None
223
+ try:
224
+ current_active = (await self._state.tabs.get_active_tab(request.agent_id)).page_id
225
+ except NoActiveTabError:
226
+ current_active = None
227
+ results: list[dict[str, Any]] = []
228
+ for record in tabs:
229
+ try:
230
+ latest = await self._state.browser_service.get_page_summary(record.page_id)
231
+ await self._state.tabs.update_tab(
232
+ record.page_id,
233
+ url=str(latest["url"]),
234
+ title=str(latest["title"]),
235
+ )
236
+ record.url = str(latest["url"])
237
+ record.title = str(latest["title"])
238
+ except BrowserCliError:
239
+ pass
240
+ results.append(
241
+ {
242
+ "page_id": record.page_id,
243
+ "url": record.url,
244
+ "title": record.title,
245
+ "active": record.page_id == current_active,
246
+ "busy": record.busy is not None,
247
+ }
248
+ )
249
+ return {"tabs": results}
250
+
251
+ async def _handle_switch_tab(self, request: DaemonRequest) -> dict[str, Any]:
252
+ page_id = self._require_str(request.args, "page_id")
253
+ claimed = await self._state.tabs.claim_page(
254
+ agent_id=request.agent_id,
255
+ page_id=page_id,
256
+ request_id=request.request_id,
257
+ command=request.action,
258
+ )
259
+ try:
260
+ await self._state.tabs.set_active_tab(request.agent_id, page_id)
261
+ finally:
262
+ await self._state.tabs.release_tab(
263
+ page_id=claimed.page_id, request_id=request.request_id
264
+ )
265
+ return {"page": await self._state.browser_service.get_page_summary(page_id)}
266
+
267
+ async def _handle_close_tab(self, request: DaemonRequest) -> dict[str, Any]:
268
+ page_id = str(request.args.get("page_id") or "").strip()
269
+ if not page_id:
270
+ page_id = await self._state.tabs.current_active_page_id(request.agent_id)
271
+ claimed = await self._state.tabs.claim_page(
272
+ agent_id=request.agent_id,
273
+ page_id=page_id,
274
+ request_id=request.request_id,
275
+ command=request.action,
276
+ )
277
+ try:
278
+ result = await self._state.browser_service.close_tab(page_id)
279
+ await self._state.tabs.remove_tab(request.agent_id, page_id)
280
+ return result
281
+ finally:
282
+ await self._state.tabs.release_tab(
283
+ page_id=claimed.page_id, request_id=request.request_id
284
+ )
285
+
286
+ async def _handle_close(self, request: DaemonRequest) -> dict[str, Any]:
287
+ return await self._handle_close_tab(request)
288
+
289
+ async def _handle_info(self, request: DaemonRequest) -> dict[str, Any]:
290
+ page = await self._run_active_page_action(
291
+ request, self._state.browser_service.get_page_info
292
+ )
293
+ return {"page": page}
294
+
295
+ async def _handle_read_page(self, request: DaemonRequest) -> dict[str, Any]:
296
+ url = self._require_str(request.args, "url")
297
+ output_mode = str(request.args.get("output_mode") or "html").strip() or "html"
298
+ scroll_bottom = bool(request.args.get("scroll_bottom"))
299
+ payload = await self._state.browser_service.read_page(
300
+ url=url,
301
+ output_mode=output_mode,
302
+ scroll_bottom=scroll_bottom,
303
+ )
304
+ return payload
305
+
306
+ async def _handle_html(self, request: DaemonRequest) -> dict[str, Any]:
307
+ payload = await self._run_active_page_action(
308
+ request, self._state.browser_service.capture_html
309
+ )
310
+ return payload
311
+
312
+ async def _handle_snapshot(self, request: DaemonRequest) -> dict[str, Any]:
313
+ interactive = bool(request.args.get("interactive"))
314
+ full_page = bool(request.args.get("full_page", True))
315
+
316
+ async def _snapshot(page_id: str) -> dict[str, Any]:
317
+ payload = await self._state.browser_service.capture_snapshot(
318
+ page_id,
319
+ interactive=interactive,
320
+ full_page=full_page,
321
+ )
322
+ refs_summary = payload.get("refs_summary", [])
323
+ ref_ids = {str(item.get("ref")) for item in refs_summary if item.get("ref")}
324
+ await self._state.tabs.update_tab(page_id, last_snapshot_refs=ref_ids)
325
+ await self._state.tabs.update_tab(
326
+ page_id,
327
+ last_snapshot_id=str(payload.get("snapshot_id") or ""),
328
+ last_snapshot_ref_count=len(ref_ids),
329
+ last_snapshot_url=str(payload.get("captured_url") or ""),
330
+ last_snapshot_at=float(payload.get("captured_at") or 0.0),
331
+ )
332
+ return payload
333
+
334
+ return await self._run_active_page_action(request, _snapshot)
335
+
336
+ async def _handle_reload(self, request: DaemonRequest) -> dict[str, Any]:
337
+ payload = await self._run_active_page_action(request, self._state.browser_service.reload)
338
+ return {"page": payload}
339
+
340
+ async def _handle_back(self, request: DaemonRequest) -> dict[str, Any]:
341
+ payload = await self._run_active_page_action(request, self._state.browser_service.go_back)
342
+ return {"page": payload}
343
+
344
+ async def _handle_forward(self, request: DaemonRequest) -> dict[str, Any]:
345
+ payload = await self._run_active_page_action(
346
+ request, self._state.browser_service.go_forward
347
+ )
348
+ return {"page": payload}
349
+
350
+ async def _handle_resize(self, request: DaemonRequest) -> dict[str, Any]:
351
+ width = self._require_int(request.args, "width")
352
+ height = self._require_int(request.args, "height")
353
+ if width <= 0 or height <= 0:
354
+ raise InvalidInputError("width and height must be positive integers.")
355
+ return await self._run_active_page_action(
356
+ request,
357
+ lambda page_id: self._state.browser_service.resize(page_id, width=width, height=height),
358
+ )
359
+
360
+ async def _handle_click(self, request: DaemonRequest) -> dict[str, Any]:
361
+ ref = self._require_str(request.args, "ref")
362
+ return await self._run_active_page_action(
363
+ request, lambda page_id: self._state.browser_service.click_ref(page_id, ref)
364
+ )
365
+
366
+ async def _handle_double_click(self, request: DaemonRequest) -> dict[str, Any]:
367
+ ref = self._require_str(request.args, "ref")
368
+ return await self._run_active_page_action(
369
+ request, lambda page_id: self._state.browser_service.double_click_ref(page_id, ref)
370
+ )
371
+
372
+ async def _handle_hover(self, request: DaemonRequest) -> dict[str, Any]:
373
+ ref = self._require_str(request.args, "ref")
374
+ return await self._run_active_page_action(
375
+ request, lambda page_id: self._state.browser_service.hover_ref(page_id, ref)
376
+ )
377
+
378
+ async def _handle_focus(self, request: DaemonRequest) -> dict[str, Any]:
379
+ ref = self._require_str(request.args, "ref")
380
+ return await self._run_active_page_action(
381
+ request, lambda page_id: self._state.browser_service.focus_ref(page_id, ref)
382
+ )
383
+
384
+ async def _handle_fill(self, request: DaemonRequest) -> dict[str, Any]:
385
+ ref = self._require_str(request.args, "ref")
386
+ text = self._require_str(request.args, "text")
387
+ submit = bool(request.args.get("submit"))
388
+ return await self._run_active_page_action(
389
+ request,
390
+ lambda page_id: self._state.browser_service.fill_ref(page_id, ref, text, submit=submit),
391
+ )
392
+
393
+ async def _handle_fill_form(self, request: DaemonRequest) -> dict[str, Any]:
394
+ fields = request.args.get("fields") or []
395
+ if not isinstance(fields, list):
396
+ raise InvalidInputError("fill-form expects a JSON array in --fields.")
397
+ submit = bool(request.args.get("submit"))
398
+ return await self._run_active_page_action(
399
+ request,
400
+ lambda page_id: self._state.browser_service.fill_form(page_id, fields, submit=submit),
401
+ )
402
+
403
+ async def _handle_select(self, request: DaemonRequest) -> dict[str, Any]:
404
+ ref = self._require_str(request.args, "ref")
405
+ text = self._require_str(request.args, "text")
406
+ return await self._run_active_page_action(
407
+ request, lambda page_id: self._state.browser_service.select_option(page_id, ref, text)
408
+ )
409
+
410
+ async def _handle_options(self, request: DaemonRequest) -> dict[str, Any]:
411
+ ref = self._require_str(request.args, "ref")
412
+ return await self._run_active_page_action(
413
+ request, lambda page_id: self._state.browser_service.list_options(page_id, ref)
414
+ )
415
+
416
+ async def _handle_check(self, request: DaemonRequest) -> dict[str, Any]:
417
+ ref = self._require_str(request.args, "ref")
418
+ return await self._run_active_page_action(
419
+ request, lambda page_id: self._state.browser_service.check_ref(page_id, ref)
420
+ )
421
+
422
+ async def _handle_uncheck(self, request: DaemonRequest) -> dict[str, Any]:
423
+ ref = self._require_str(request.args, "ref")
424
+ return await self._run_active_page_action(
425
+ request, lambda page_id: self._state.browser_service.uncheck_ref(page_id, ref)
426
+ )
427
+
428
+ async def _handle_scroll_to(self, request: DaemonRequest) -> dict[str, Any]:
429
+ ref = self._require_str(request.args, "ref")
430
+ return await self._run_active_page_action(
431
+ request, lambda page_id: self._state.browser_service.scroll_to_ref(page_id, ref)
432
+ )
433
+
434
+ async def _handle_drag(self, request: DaemonRequest) -> dict[str, Any]:
435
+ start_ref = self._require_str(request.args, "start_ref")
436
+ end_ref = self._require_str(request.args, "end_ref")
437
+ return await self._run_active_page_action(
438
+ request,
439
+ lambda page_id: self._state.browser_service.drag_ref(page_id, start_ref, end_ref),
440
+ )
441
+
442
+ async def _handle_upload(self, request: DaemonRequest) -> dict[str, Any]:
443
+ ref = self._require_str(request.args, "ref")
444
+ path = self._require_str(request.args, "path")
445
+ return await self._run_active_page_action(
446
+ request, lambda page_id: self._state.browser_service.upload_file(page_id, ref, path)
447
+ )
448
+
449
+ async def _handle_type(self, request: DaemonRequest) -> dict[str, Any]:
450
+ text = self._require_str(request.args, "text")
451
+ submit = bool(request.args.get("submit"))
452
+ return await self._run_active_page_action(
453
+ request,
454
+ lambda page_id: self._state.browser_service.type_text(page_id, text, submit=submit),
455
+ )
456
+
457
+ async def _handle_press(self, request: DaemonRequest) -> dict[str, Any]:
458
+ key = self._require_str(request.args, "key")
459
+ return await self._run_active_page_action(
460
+ request, lambda page_id: self._state.browser_service.press_key(page_id, key)
461
+ )
462
+
463
+ async def _handle_key_down(self, request: DaemonRequest) -> dict[str, Any]:
464
+ key = self._require_str(request.args, "key")
465
+ return await self._run_active_page_action(
466
+ request, lambda page_id: self._state.browser_service.key_down(page_id, key)
467
+ )
468
+
469
+ async def _handle_key_up(self, request: DaemonRequest) -> dict[str, Any]:
470
+ key = self._require_str(request.args, "key")
471
+ return await self._run_active_page_action(
472
+ request, lambda page_id: self._state.browser_service.key_up(page_id, key)
473
+ )
474
+
475
+ async def _handle_scroll(self, request: DaemonRequest) -> dict[str, Any]:
476
+ dx = self._optional_int(request.args, "dx") or 0
477
+ dy = self._optional_int(request.args, "dy") or 700
478
+ return await self._run_active_page_action(
479
+ request, lambda page_id: self._state.browser_service.wheel(page_id, dx=dx, dy=dy)
480
+ )
481
+
482
+ async def _handle_mouse_click(self, request: DaemonRequest) -> dict[str, Any]:
483
+ x = self._require_int(request.args, "x")
484
+ y = self._require_int(request.args, "y")
485
+ count = self._optional_int(request.args, "count") or 1
486
+ return await self._run_active_page_action(
487
+ request,
488
+ lambda page_id: self._state.browser_service.mouse_click(
489
+ page_id,
490
+ x=x,
491
+ y=y,
492
+ button=str(request.args.get("button") or "left"),
493
+ count=count,
494
+ ),
495
+ )
496
+
497
+ async def _handle_mouse_move(self, request: DaemonRequest) -> dict[str, Any]:
498
+ x = self._require_int(request.args, "x")
499
+ y = self._require_int(request.args, "y")
500
+ return await self._run_active_page_action(
501
+ request,
502
+ lambda page_id: self._state.browser_service.mouse_move(
503
+ page_id,
504
+ x=x,
505
+ y=y,
506
+ ),
507
+ )
508
+
509
+ async def _handle_mouse_drag(self, request: DaemonRequest) -> dict[str, Any]:
510
+ x1 = self._require_int(request.args, "x1")
511
+ y1 = self._require_int(request.args, "y1")
512
+ x2 = self._require_int(request.args, "x2")
513
+ y2 = self._require_int(request.args, "y2")
514
+ return await self._run_active_page_action(
515
+ request,
516
+ lambda page_id: self._state.browser_service.mouse_drag(
517
+ page_id,
518
+ x1=x1,
519
+ y1=y1,
520
+ x2=x2,
521
+ y2=y2,
522
+ ),
523
+ )
524
+
525
+ async def _handle_mouse_down(self, request: DaemonRequest) -> dict[str, Any]:
526
+ return await self._run_active_page_action(
527
+ request,
528
+ lambda page_id: self._state.browser_service.mouse_down(
529
+ page_id,
530
+ button=str(request.args.get("button") or "left"),
531
+ ),
532
+ )
533
+
534
+ async def _handle_mouse_up(self, request: DaemonRequest) -> dict[str, Any]:
535
+ return await self._run_active_page_action(
536
+ request,
537
+ lambda page_id: self._state.browser_service.mouse_up(
538
+ page_id,
539
+ button=str(request.args.get("button") or "left"),
540
+ ),
541
+ )
542
+
543
+ async def _handle_eval(self, request: DaemonRequest) -> dict[str, Any]:
544
+ code = self._require_str(request.args, "code")
545
+ return await self._run_active_page_action(
546
+ request, lambda page_id: self._state.browser_service.evaluate(page_id, code)
547
+ )
548
+
549
+ async def _handle_eval_on(self, request: DaemonRequest) -> dict[str, Any]:
550
+ ref = self._require_str(request.args, "ref")
551
+ code = self._require_str(request.args, "code")
552
+ return await self._run_active_page_action(
553
+ request, lambda page_id: self._state.browser_service.evaluate_on_ref(page_id, ref, code)
554
+ )
555
+
556
+ async def _handle_wait(self, request: DaemonRequest) -> dict[str, Any]:
557
+ seconds = self._optional_float(request.args, "seconds")
558
+ text = request.args.get("text")
559
+ gone = bool(request.args.get("gone"))
560
+ exact = bool(request.args.get("exact"))
561
+ return await self._run_active_page_action(
562
+ request,
563
+ lambda page_id: self._state.browser_service.wait(
564
+ page_id,
565
+ seconds=seconds,
566
+ text=str(text) if text else None,
567
+ gone=gone,
568
+ exact=exact,
569
+ ),
570
+ )
571
+
572
+ async def _handle_wait_network(self, request: DaemonRequest) -> dict[str, Any]:
573
+ timeout_seconds = self._optional_float(request.args, "timeout") or 30.0
574
+ return await self._run_active_page_action(
575
+ request,
576
+ lambda page_id: self._state.browser_service.wait_for_network_idle(
577
+ page_id, timeout_seconds=timeout_seconds
578
+ ),
579
+ )
580
+
581
+ async def _handle_screenshot(self, request: DaemonRequest) -> dict[str, Any]:
582
+ path = self._require_str(request.args, "path")
583
+ full_page = bool(request.args.get("full_page"))
584
+ return await self._run_active_page_action(
585
+ request,
586
+ lambda page_id: self._state.browser_service.screenshot(
587
+ page_id, path=path, full_page=full_page
588
+ ),
589
+ )
590
+
591
+ async def _handle_pdf(self, request: DaemonRequest) -> dict[str, Any]:
592
+ path = self._require_str(request.args, "path")
593
+ return await self._run_active_page_action(
594
+ request, lambda page_id: self._state.browser_service.save_pdf(page_id, path=path)
595
+ )
596
+
597
+ async def _handle_console_start(self, request: DaemonRequest) -> dict[str, Any]:
598
+ return await self._run_active_page_action(
599
+ request, self._state.browser_service.start_console_capture
600
+ )
601
+
602
+ async def _handle_console(self, request: DaemonRequest) -> dict[str, Any]:
603
+ message_type = str(request.args.get("message_type") or "").strip() or None
604
+ clear = not bool(request.args.get("no_clear"))
605
+ return await self._run_active_page_action(
606
+ request,
607
+ lambda page_id: self._state.browser_service.get_console_messages(
608
+ page_id, message_type=message_type, clear=clear
609
+ ),
610
+ )
611
+
612
+ async def _handle_console_stop(self, request: DaemonRequest) -> dict[str, Any]:
613
+ return await self._run_active_page_action(
614
+ request, self._state.browser_service.stop_console_capture
615
+ )
616
+
617
+ async def _handle_network_start(self, request: DaemonRequest) -> dict[str, Any]:
618
+ return await self._run_active_page_action(
619
+ request, self._state.browser_service.start_network_capture
620
+ )
621
+
622
+ async def _handle_network_wait(self, request: DaemonRequest) -> dict[str, Any]:
623
+ filters = self._network_filters_from_request(request.args)
624
+ timeout_seconds = self._optional_float(request.args, "timeout_seconds") or 30.0
625
+ return await self._run_active_page_action(
626
+ request,
627
+ lambda page_id: self._state.browser_service.wait_for_network_record(
628
+ page_id,
629
+ **filters,
630
+ timeout_seconds=timeout_seconds,
631
+ ),
632
+ )
633
+
634
+ async def _handle_network(self, request: DaemonRequest) -> dict[str, Any]:
635
+ filters = self._network_filters_from_request(request.args)
636
+ clear = not bool(request.args.get("no_clear"))
637
+ return await self._run_active_page_action(
638
+ request,
639
+ lambda page_id: self._state.browser_service.get_network_records(
640
+ page_id, clear=clear, **filters
641
+ ),
642
+ )
643
+
644
+ async def _handle_network_stop(self, request: DaemonRequest) -> dict[str, Any]:
645
+ return await self._run_active_page_action(
646
+ request, self._state.browser_service.stop_network_capture
647
+ )
648
+
649
+ async def _handle_dialog_setup(self, request: DaemonRequest) -> dict[str, Any]:
650
+ action = str(request.args.get("action") or "accept")
651
+ if action not in {"accept", "dismiss"}:
652
+ raise InvalidInputError("action must be accept or dismiss.")
653
+ return await self._run_active_page_action(
654
+ request,
655
+ lambda page_id: self._state.browser_service.setup_dialog_handler(
656
+ page_id,
657
+ default_action=action,
658
+ default_prompt_text=self._optional_str(request.args, "text"),
659
+ ),
660
+ )
661
+
662
+ async def _handle_dialog(self, request: DaemonRequest) -> dict[str, Any]:
663
+ accept = not bool(request.args.get("dismiss"))
664
+ return await self._run_active_page_action(
665
+ request,
666
+ lambda page_id: self._state.browser_service.handle_dialog(
667
+ page_id,
668
+ accept=accept,
669
+ prompt_text=self._optional_str(request.args, "text"),
670
+ ),
671
+ )
672
+
673
+ async def _handle_dialog_remove(self, request: DaemonRequest) -> dict[str, Any]:
674
+ return await self._run_active_page_action(
675
+ request, self._state.browser_service.remove_dialog_handler
676
+ )
677
+
678
+ async def _handle_cookies(self, request: DaemonRequest) -> dict[str, Any]:
679
+ return await self._run_active_page_action(
680
+ request,
681
+ lambda page_id: self._state.browser_service.get_cookies(
682
+ page_id,
683
+ name=self._optional_str(request.args, "name"),
684
+ domain=self._optional_str(request.args, "domain"),
685
+ path=self._optional_str(request.args, "path"),
686
+ ),
687
+ )
688
+
689
+ async def _handle_cookie_set(self, request: DaemonRequest) -> dict[str, Any]:
690
+ return await self._run_active_page_action(
691
+ request,
692
+ lambda page_id: self._state.browser_service.set_cookie(
693
+ page_id,
694
+ name=self._require_str(request.args, "name"),
695
+ value=self._require_str(request.args, "value"),
696
+ domain=self._optional_str(request.args, "domain"),
697
+ path=str(request.args.get("path") or "/"),
698
+ expires=self._optional_float(request.args, "expires"),
699
+ http_only=bool(request.args.get("http_only")),
700
+ secure=bool(request.args.get("secure")),
701
+ same_site=self._optional_str(request.args, "same_site"),
702
+ ),
703
+ )
704
+
705
+ async def _handle_cookies_clear(self, request: DaemonRequest) -> dict[str, Any]:
706
+ return await self._run_active_page_action(
707
+ request,
708
+ lambda page_id: self._state.browser_service.clear_cookies(
709
+ page_id,
710
+ name=self._optional_str(request.args, "name"),
711
+ domain=self._optional_str(request.args, "domain"),
712
+ path=self._optional_str(request.args, "path"),
713
+ ),
714
+ )
715
+
716
+ async def _handle_storage_save(self, request: DaemonRequest) -> dict[str, Any]:
717
+ return await self._run_active_page_action(
718
+ request,
719
+ lambda page_id: self._state.browser_service.save_storage_state(
720
+ page_id,
721
+ path=self._optional_str(request.args, "path"),
722
+ ),
723
+ )
724
+
725
+ async def _handle_storage_load(self, request: DaemonRequest) -> dict[str, Any]:
726
+ path = self._require_str(request.args, "path")
727
+ return await self._run_active_page_action(
728
+ request,
729
+ lambda page_id: self._state.browser_service.load_storage_state(page_id, path=path),
730
+ )
731
+
732
+ async def _handle_verify_text(self, request: DaemonRequest) -> dict[str, Any]:
733
+ text = self._require_str(request.args, "text")
734
+ exact = bool(request.args.get("exact"))
735
+ timeout = self._optional_float(request.args, "timeout") or 5.0
736
+ return await self._run_active_page_action(
737
+ request,
738
+ lambda page_id: self._state.browser_service.verify_text(
739
+ page_id, text=text, exact=exact, timeout_seconds=timeout
740
+ ),
741
+ )
742
+
743
+ async def _handle_verify_visible(self, request: DaemonRequest) -> dict[str, Any]:
744
+ role = self._require_str(request.args, "role")
745
+ name = self._require_str(request.args, "name")
746
+ timeout = self._optional_float(request.args, "timeout") or 5.0
747
+ return await self._run_active_page_action(
748
+ request,
749
+ lambda page_id: self._state.browser_service.verify_visible(
750
+ page_id, role=role, name=name, timeout_seconds=timeout
751
+ ),
752
+ )
753
+
754
+ async def _handle_verify_url(self, request: DaemonRequest) -> dict[str, Any]:
755
+ expected = self._require_str(request.args, "expected")
756
+ exact = bool(request.args.get("exact"))
757
+ return await self._run_active_page_action(
758
+ request,
759
+ lambda page_id: self._state.browser_service.verify_url(
760
+ page_id, expected=expected, exact=exact
761
+ ),
762
+ )
763
+
764
+ async def _handle_verify_title(self, request: DaemonRequest) -> dict[str, Any]:
765
+ expected = self._require_str(request.args, "expected")
766
+ exact = bool(request.args.get("exact"))
767
+ return await self._run_active_page_action(
768
+ request,
769
+ lambda page_id: self._state.browser_service.verify_title(
770
+ page_id, expected=expected, exact=exact
771
+ ),
772
+ )
773
+
774
+ async def _handle_verify_state(self, request: DaemonRequest) -> dict[str, Any]:
775
+ ref = self._require_str(request.args, "ref")
776
+ state = self._require_str(request.args, "state")
777
+ return await self._run_active_page_action(
778
+ request,
779
+ lambda page_id: self._state.browser_service.verify_state(page_id, ref=ref, state=state),
780
+ )
781
+
782
+ async def _handle_verify_value(self, request: DaemonRequest) -> dict[str, Any]:
783
+ ref = self._require_str(request.args, "ref")
784
+ expected = self._require_str(request.args, "expected")
785
+ return await self._run_active_page_action(
786
+ request,
787
+ lambda page_id: self._state.browser_service.verify_value(
788
+ page_id, ref=ref, expected=expected
789
+ ),
790
+ )
791
+
792
+ async def _handle_trace_start(self, request: DaemonRequest) -> dict[str, Any]:
793
+ screenshots = not bool(request.args.get("no_screenshots"))
794
+ snapshots = not bool(request.args.get("no_snapshots"))
795
+ sources = bool(request.args.get("sources"))
796
+ return await self._run_active_page_action(
797
+ request,
798
+ lambda page_id: self._state.browser_service.start_tracing(
799
+ page_id,
800
+ screenshots=screenshots,
801
+ snapshots=snapshots,
802
+ sources=sources,
803
+ ),
804
+ )
805
+
806
+ async def _handle_trace_chunk(self, request: DaemonRequest) -> dict[str, Any]:
807
+ title = self._optional_str(request.args, "title")
808
+ return await self._run_active_page_action(
809
+ request,
810
+ lambda page_id: self._state.browser_service.add_trace_chunk(page_id, title=title),
811
+ )
812
+
813
+ async def _handle_trace_stop(self, request: DaemonRequest) -> dict[str, Any]:
814
+ path = self._optional_str(request.args, "path")
815
+ return await self._run_active_page_action(
816
+ request,
817
+ lambda page_id: self._state.browser_service.stop_tracing(page_id, path=path),
818
+ )
819
+
820
+ async def _handle_video_start(self, request: DaemonRequest) -> dict[str, Any]:
821
+ width = self._optional_int(request.args, "width")
822
+ height = self._optional_int(request.args, "height")
823
+ return await self._run_active_page_action(
824
+ request,
825
+ lambda page_id: self._state.browser_service.start_video(
826
+ page_id,
827
+ width=width,
828
+ height=height,
829
+ ),
830
+ )
831
+
832
+ async def _handle_video_stop(self, request: DaemonRequest) -> dict[str, Any]:
833
+ path = self._optional_str(request.args, "path")
834
+ return await self._run_active_page_action(
835
+ request,
836
+ lambda page_id: self._state.browser_service.stop_video(page_id, path=path),
837
+ )
838
+
839
+ async def _run_active_page_action(
840
+ self,
841
+ request: DaemonRequest,
842
+ operation: Callable[[str], Awaitable[dict[str, Any]]],
843
+ ) -> dict[str, Any]:
844
+ async with self._state.tabs.claim_active_tab(
845
+ agent_id=request.agent_id,
846
+ request_id=request.request_id,
847
+ command=request.action,
848
+ ) as tab:
849
+ payload = await operation(tab.page_id)
850
+ try:
851
+ latest = await self._state.browser_service.get_page_summary(tab.page_id)
852
+ await self._state.tabs.update_tab(
853
+ tab.page_id,
854
+ url=str(latest["url"]),
855
+ title=str(latest["title"]),
856
+ )
857
+ except BrowserCliError:
858
+ pass
859
+ return payload
860
+
861
+ @staticmethod
862
+ def _require_str(args: dict[str, Any], key: str) -> str:
863
+ value = str(args.get(key) or "").strip()
864
+ if not value:
865
+ raise InvalidInputError(f"{key} is required.")
866
+ return value
867
+
868
+ @staticmethod
869
+ def _optional_str(args: dict[str, Any], key: str) -> str | None:
870
+ value = str(args.get(key) or "").strip()
871
+ return value or None
872
+
873
+ @classmethod
874
+ def _require_int(cls, args: dict[str, Any], key: str) -> int:
875
+ raw = args.get(key)
876
+ if raw is None or str(raw).strip() == "":
877
+ raise InvalidInputError(f"{key} is required.")
878
+ try:
879
+ return int(raw)
880
+ except (TypeError, ValueError) as exc:
881
+ raise InvalidInputError(f"{key} must be an integer.") from exc
882
+
883
+ @classmethod
884
+ def _optional_int(cls, args: dict[str, Any], key: str) -> int | None:
885
+ raw = args.get(key)
886
+ if raw is None or str(raw).strip() == "":
887
+ return None
888
+ try:
889
+ return int(raw)
890
+ except (TypeError, ValueError) as exc:
891
+ raise InvalidInputError(f"{key} must be an integer.") from exc
892
+
893
+ @classmethod
894
+ def _require_float(cls, args: dict[str, Any], key: str) -> float:
895
+ raw = args.get(key)
896
+ if raw is None or str(raw).strip() == "":
897
+ raise InvalidInputError(f"{key} is required.")
898
+ try:
899
+ return float(raw)
900
+ except (TypeError, ValueError) as exc:
901
+ raise InvalidInputError(f"{key} must be a number.") from exc
902
+
903
+ @classmethod
904
+ def _optional_float(cls, args: dict[str, Any], key: str) -> float | None:
905
+ raw = args.get(key)
906
+ if raw is None or str(raw).strip() == "":
907
+ return None
908
+ try:
909
+ return float(raw)
910
+ except (TypeError, ValueError) as exc:
911
+ raise InvalidInputError(f"{key} must be a number.") from exc
912
+
913
+ @classmethod
914
+ def _network_filters_from_request(cls, args: dict[str, Any]) -> dict[str, Any]:
915
+ return {
916
+ "url_contains": cls._optional_str(args, "url_contains"),
917
+ "url_regex": cls._optional_str(args, "url_regex"),
918
+ "method": cls._optional_str(args, "method"),
919
+ "status": cls._optional_int(args, "status"),
920
+ "resource_type": cls._optional_str(args, "resource_type"),
921
+ "mime_contains": cls._optional_str(args, "mime_contains"),
922
+ "include_static": bool(args.get("include_static")),
923
+ }
924
+
925
+ def _maybe_configure_browser_environment(self, args: dict[str, Any]) -> None:
926
+ chrome_environment_payload = args.get("chrome_environment")
927
+ if not isinstance(chrome_environment_payload, dict):
928
+ return
929
+ executable_path = self._optional_str(chrome_environment_payload, "executable_path")
930
+ self._state.browser_service.configure_environment(
931
+ ChromeEnvironment(
932
+ executable_path=Path(executable_path) if executable_path else None,
933
+ user_data_dir=Path(self._require_str(chrome_environment_payload, "user_data_dir")),
934
+ profile_directory=str(
935
+ chrome_environment_payload.get("profile_directory") or "Default"
936
+ ),
937
+ profile_name=self._optional_str(chrome_environment_payload, "profile_name"),
938
+ source=str(chrome_environment_payload.get("source") or "chrome"),
939
+ fallback_reason=self._optional_str(chrome_environment_payload, "fallback_reason"),
940
+ )
941
+ )