ai-dev-browser 0.5.3__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 (155) hide show
  1. ai_dev_browser/__init__.py +110 -0
  2. ai_dev_browser/_cli.py +428 -0
  3. ai_dev_browser/_version.py +13 -0
  4. ai_dev_browser/cdp/__init__.py +6 -0
  5. ai_dev_browser/cdp/accessibility.py +668 -0
  6. ai_dev_browser/cdp/animation.py +494 -0
  7. ai_dev_browser/cdp/audits.py +1990 -0
  8. ai_dev_browser/cdp/autofill.py +292 -0
  9. ai_dev_browser/cdp/background_service.py +215 -0
  10. ai_dev_browser/cdp/bluetooth_emulation.py +626 -0
  11. ai_dev_browser/cdp/browser.py +821 -0
  12. ai_dev_browser/cdp/cache_storage.py +311 -0
  13. ai_dev_browser/cdp/cast.py +172 -0
  14. ai_dev_browser/cdp/console.py +107 -0
  15. ai_dev_browser/cdp/crash_report_context.py +55 -0
  16. ai_dev_browser/cdp/css.py +2710 -0
  17. ai_dev_browser/cdp/debugger.py +1405 -0
  18. ai_dev_browser/cdp/device_access.py +141 -0
  19. ai_dev_browser/cdp/device_orientation.py +45 -0
  20. ai_dev_browser/cdp/dom.py +2257 -0
  21. ai_dev_browser/cdp/dom_debugger.py +321 -0
  22. ai_dev_browser/cdp/dom_snapshot.py +876 -0
  23. ai_dev_browser/cdp/dom_storage.py +222 -0
  24. ai_dev_browser/cdp/emulation.py +1779 -0
  25. ai_dev_browser/cdp/event_breakpoints.py +56 -0
  26. ai_dev_browser/cdp/extensions.py +246 -0
  27. ai_dev_browser/cdp/fed_cm.py +283 -0
  28. ai_dev_browser/cdp/fetch.py +507 -0
  29. ai_dev_browser/cdp/file_system.py +115 -0
  30. ai_dev_browser/cdp/headless_experimental.py +115 -0
  31. ai_dev_browser/cdp/heap_profiler.py +401 -0
  32. ai_dev_browser/cdp/indexed_db.py +528 -0
  33. ai_dev_browser/cdp/input_.py +701 -0
  34. ai_dev_browser/cdp/inspector.py +95 -0
  35. ai_dev_browser/cdp/io.py +101 -0
  36. ai_dev_browser/cdp/layer_tree.py +464 -0
  37. ai_dev_browser/cdp/log.py +190 -0
  38. ai_dev_browser/cdp/media.py +313 -0
  39. ai_dev_browser/cdp/memory.py +305 -0
  40. ai_dev_browser/cdp/network.py +5317 -0
  41. ai_dev_browser/cdp/overlay.py +1468 -0
  42. ai_dev_browser/cdp/page.py +3970 -0
  43. ai_dev_browser/cdp/performance.py +124 -0
  44. ai_dev_browser/cdp/performance_timeline.py +200 -0
  45. ai_dev_browser/cdp/preload.py +575 -0
  46. ai_dev_browser/cdp/profiler.py +420 -0
  47. ai_dev_browser/cdp/pwa.py +278 -0
  48. ai_dev_browser/cdp/py.typed +0 -0
  49. ai_dev_browser/cdp/runtime.py +1589 -0
  50. ai_dev_browser/cdp/schema.py +50 -0
  51. ai_dev_browser/cdp/security.py +518 -0
  52. ai_dev_browser/cdp/service_worker.py +401 -0
  53. ai_dev_browser/cdp/smart_card_emulation.py +891 -0
  54. ai_dev_browser/cdp/storage.py +1573 -0
  55. ai_dev_browser/cdp/system_info.py +327 -0
  56. ai_dev_browser/cdp/target.py +822 -0
  57. ai_dev_browser/cdp/tethering.py +65 -0
  58. ai_dev_browser/cdp/tracing.py +377 -0
  59. ai_dev_browser/cdp/util.py +17 -0
  60. ai_dev_browser/cdp/web_audio.py +606 -0
  61. ai_dev_browser/cdp/web_authn.py +598 -0
  62. ai_dev_browser/cdp/web_mcp.py +219 -0
  63. ai_dev_browser/core/__init__.py +218 -0
  64. ai_dev_browser/core/_case.py +38 -0
  65. ai_dev_browser/core/_cf_template.py +84 -0
  66. ai_dev_browser/core/_element.py +431 -0
  67. ai_dev_browser/core/_tab.py +817 -0
  68. ai_dev_browser/core/_transport.py +362 -0
  69. ai_dev_browser/core/ax.py +605 -0
  70. ai_dev_browser/core/browser.py +323 -0
  71. ai_dev_browser/core/cdp.py +66 -0
  72. ai_dev_browser/core/chrome.py +253 -0
  73. ai_dev_browser/core/cloudflare.py +77 -0
  74. ai_dev_browser/core/config.py +95 -0
  75. ai_dev_browser/core/connection.py +403 -0
  76. ai_dev_browser/core/cookies.py +103 -0
  77. ai_dev_browser/core/dialog.py +165 -0
  78. ai_dev_browser/core/download.py +38 -0
  79. ai_dev_browser/core/elements.py +913 -0
  80. ai_dev_browser/core/human.py +612 -0
  81. ai_dev_browser/core/login.py +122 -0
  82. ai_dev_browser/core/mouse.py +140 -0
  83. ai_dev_browser/core/navigation.py +225 -0
  84. ai_dev_browser/core/overlays.py +148 -0
  85. ai_dev_browser/core/page.py +302 -0
  86. ai_dev_browser/core/port.py +465 -0
  87. ai_dev_browser/core/process.py +114 -0
  88. ai_dev_browser/core/snapshot.py +425 -0
  89. ai_dev_browser/core/storage.py +50 -0
  90. ai_dev_browser/core/tabs.py +125 -0
  91. ai_dev_browser/core/text_match.py +217 -0
  92. ai_dev_browser/core/window.py +84 -0
  93. ai_dev_browser/pool/__init__.py +34 -0
  94. ai_dev_browser/pool/job.py +140 -0
  95. ai_dev_browser/pool/persistence.py +154 -0
  96. ai_dev_browser/pool/pool.py +873 -0
  97. ai_dev_browser/pool/worker.py +171 -0
  98. ai_dev_browser/profile.py +107 -0
  99. ai_dev_browser/py.typed +0 -0
  100. ai_dev_browser/tools/__init__.py +9 -0
  101. ai_dev_browser/tools/_generate.py +209 -0
  102. ai_dev_browser/tools/browser_list.py +14 -0
  103. ai_dev_browser/tools/browser_start.py +14 -0
  104. ai_dev_browser/tools/browser_stop.py +14 -0
  105. ai_dev_browser/tools/cdp_send.py +14 -0
  106. ai_dev_browser/tools/click_by_html_id.py +14 -0
  107. ai_dev_browser/tools/click_by_ref.py +14 -0
  108. ai_dev_browser/tools/click_by_text.py +14 -0
  109. ai_dev_browser/tools/click_by_xpath.py +14 -0
  110. ai_dev_browser/tools/cloudflare_verify.py +14 -0
  111. ai_dev_browser/tools/cookies_list.py +14 -0
  112. ai_dev_browser/tools/cookies_load.py +14 -0
  113. ai_dev_browser/tools/cookies_save.py +14 -0
  114. ai_dev_browser/tools/dialog_respond.py +14 -0
  115. ai_dev_browser/tools/download.py +14 -0
  116. ai_dev_browser/tools/drag_by_ref.py +14 -0
  117. ai_dev_browser/tools/find_by_html_id.py +14 -0
  118. ai_dev_browser/tools/find_by_xpath.py +14 -0
  119. ai_dev_browser/tools/focus_by_ref.py +14 -0
  120. ai_dev_browser/tools/highlight_by_ref.py +14 -0
  121. ai_dev_browser/tools/hover_by_ref.py +14 -0
  122. ai_dev_browser/tools/html_by_ref.py +14 -0
  123. ai_dev_browser/tools/js_evaluate.py +14 -0
  124. ai_dev_browser/tools/login_interactive.py +14 -0
  125. ai_dev_browser/tools/mouse_click.py +14 -0
  126. ai_dev_browser/tools/mouse_drag.py +14 -0
  127. ai_dev_browser/tools/mouse_move.py +14 -0
  128. ai_dev_browser/tools/page_discover.py +14 -0
  129. ai_dev_browser/tools/page_emulate_focus.py +14 -0
  130. ai_dev_browser/tools/page_goto.py +14 -0
  131. ai_dev_browser/tools/page_html.py +14 -0
  132. ai_dev_browser/tools/page_info.py +14 -0
  133. ai_dev_browser/tools/page_reload.py +14 -0
  134. ai_dev_browser/tools/page_screenshot.py +14 -0
  135. ai_dev_browser/tools/page_scroll.py +14 -0
  136. ai_dev_browser/tools/page_wait_element.py +14 -0
  137. ai_dev_browser/tools/page_wait_ready.py +14 -0
  138. ai_dev_browser/tools/page_wait_url.py +14 -0
  139. ai_dev_browser/tools/screenshot_by_ref.py +14 -0
  140. ai_dev_browser/tools/select_by_ref.py +14 -0
  141. ai_dev_browser/tools/storage_get.py +14 -0
  142. ai_dev_browser/tools/storage_set.py +14 -0
  143. ai_dev_browser/tools/tab_close.py +14 -0
  144. ai_dev_browser/tools/tab_list.py +14 -0
  145. ai_dev_browser/tools/tab_new.py +14 -0
  146. ai_dev_browser/tools/tab_switch.py +14 -0
  147. ai_dev_browser/tools/type_by_ref.py +14 -0
  148. ai_dev_browser/tools/type_by_text.py +14 -0
  149. ai_dev_browser/tools/upload_by_ref.py +14 -0
  150. ai_dev_browser/tools/window_set.py +14 -0
  151. ai_dev_browser-0.5.3.dist-info/METADATA +177 -0
  152. ai_dev_browser-0.5.3.dist-info/RECORD +155 -0
  153. ai_dev_browser-0.5.3.dist-info/WHEEL +5 -0
  154. ai_dev_browser-0.5.3.dist-info/licenses/LICENSE +25 -0
  155. ai_dev_browser-0.5.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,110 @@
1
+ """ai-dev-browser: Browser automation toolkit with CDP WebSocket transport.
2
+
3
+ AI-first design for intuitive browser automation.
4
+
5
+ from ai_dev_browser import cdp, connect_browser, browser_start
6
+ """
7
+
8
+ from ._version import __version__
9
+
10
+ # Core operations (all shared code lives here)
11
+ # Core browser operations (shared by tools/ and Python code)
12
+ from . import core
13
+ from .core import (
14
+ # Config
15
+ DEFAULT_BASE_DIR,
16
+ DEFAULT_COOKIES_DIR,
17
+ DEFAULT_COOKIES_FILE,
18
+ DEFAULT_DEBUG_HOST,
19
+ DEFAULT_DEBUG_PORT,
20
+ DEFAULT_PORT_RANGE,
21
+ DEFAULT_PROFILE_DIR,
22
+ DEFAULT_PROFILE_PREFIX,
23
+ # Chrome detection and launching
24
+ find_chrome,
25
+ find_debug_chromes,
26
+ find_workspace_chromes,
27
+ get_available_port,
28
+ # Process management
29
+ get_pid_on_port,
30
+ # Port management
31
+ is_port_in_use,
32
+ launch_chrome,
33
+ )
34
+
35
+ # Cloudflare verification
36
+ from .core.cloudflare import cloudflare_verify
37
+
38
+ # Worker pool
39
+ from .pool import (
40
+ BrowserPool,
41
+ Job,
42
+ JobResult,
43
+ JobStatus,
44
+ PoolState,
45
+ Worker,
46
+ WorkerStats,
47
+ WorkerStatus,
48
+ load_state,
49
+ save_state,
50
+ )
51
+
52
+ # Profile management
53
+ from .profile import ProfileManager, ProfileMode
54
+
55
+
56
+ __all__ = [
57
+ # Version
58
+ "__version__",
59
+ # Config
60
+ "DEFAULT_BASE_DIR",
61
+ "DEFAULT_PROFILE_DIR",
62
+ "DEFAULT_COOKIES_FILE",
63
+ "DEFAULT_COOKIES_DIR",
64
+ "DEFAULT_PROFILE_PREFIX",
65
+ "DEFAULT_DEBUG_HOST",
66
+ "DEFAULT_DEBUG_PORT",
67
+ "DEFAULT_PORT_RANGE",
68
+ # Chrome
69
+ "find_chrome",
70
+ "launch_chrome",
71
+ # Port
72
+ "is_port_in_use",
73
+ "find_debug_chromes",
74
+ "find_workspace_chromes",
75
+ "get_available_port",
76
+ # Process
77
+ "get_pid_on_port",
78
+ # Worker pool
79
+ "BrowserPool",
80
+ "Job",
81
+ "JobResult",
82
+ "JobStatus",
83
+ "Worker",
84
+ "WorkerStats",
85
+ "WorkerStatus",
86
+ "PoolState",
87
+ "load_state",
88
+ "save_state",
89
+ # Profile management
90
+ "ProfileManager",
91
+ "ProfileMode",
92
+ # Cloudflare verification
93
+ "cloudflare_verify",
94
+ # Core operations module
95
+ "core",
96
+ # CDP protocol access (vendored)
97
+ "cdp",
98
+ # Tools
99
+ "browser_start",
100
+ "browser_stop",
101
+ "connect_browser",
102
+ ]
103
+
104
+ # Vendored CDP protocol module
105
+ from . import cdp
106
+
107
+ from .core import connect_browser
108
+
109
+ # Core functions also available at package level
110
+ from .core import browser_start, browser_stop
ai_dev_browser/_cli.py ADDED
@@ -0,0 +1,428 @@
1
+ """CLI decorator for tools.
2
+
3
+ Makes functions usable as both Python imports and CLI commands.
4
+
5
+ Usage:
6
+ # Tool that operates on existing browser (requires tab)
7
+ @as_cli()
8
+ async def click(tab, selector: str, text: str = None) -> dict:
9
+ '''Click an element.'''
10
+ ...
11
+
12
+ # Tool that manages browser lifecycle (no tab needed)
13
+ @as_cli(requires_tab=False)
14
+ def browser_start(port: int = None, headless: bool = False) -> dict:
15
+ '''Start a browser.'''
16
+ ...
17
+
18
+ # As CLI: python -m ai_dev_browser.tools.click --selector "button"
19
+ # As Python: from ai_dev_browser.tools import click; await click(tab, "button")
20
+ """
21
+
22
+ import argparse
23
+ import asyncio
24
+ import functools
25
+ import inspect
26
+ import json
27
+ import os
28
+ import sys
29
+ from collections.abc import Callable
30
+ from typing import Any, Literal, get_args, get_origin, get_type_hints
31
+
32
+
33
+ def _get_literal_choices(hint) -> list | None:
34
+ """Extract choices from Literal type hint."""
35
+ origin = get_origin(hint)
36
+ if origin is Literal:
37
+ return list(get_args(hint))
38
+ return None
39
+
40
+
41
+ def _parse_docstring_args(docstring: str) -> dict[str, str]:
42
+ """Extract arg descriptions from docstring Args section.
43
+
44
+ Parses Google-style docstrings:
45
+ Args:
46
+ param_name: Description here
47
+ another_param: Another description
48
+ """
49
+ if not docstring:
50
+ return {}
51
+
52
+ args_section = {}
53
+ in_args = False
54
+ current_arg = None
55
+ current_desc = []
56
+
57
+ for line in docstring.split("\n"):
58
+ stripped = line.strip()
59
+
60
+ if stripped == "Args:":
61
+ in_args = True
62
+ continue
63
+ elif (
64
+ in_args
65
+ and stripped
66
+ and not stripped[0].isspace()
67
+ and stripped.endswith(":")
68
+ ):
69
+ # New section like "Returns:" or "Raises:"
70
+ if current_arg:
71
+ args_section[current_arg] = " ".join(current_desc).strip()
72
+ break
73
+ elif in_args:
74
+ # Check if this is a new arg definition (name: description)
75
+ if ": " in stripped:
76
+ if current_arg:
77
+ args_section[current_arg] = " ".join(current_desc).strip()
78
+ parts = stripped.split(": ", 1)
79
+ current_arg = parts[0].strip()
80
+ current_desc = [parts[1].strip()] if len(parts) > 1 else []
81
+ elif current_arg and stripped:
82
+ # Continuation of previous arg description
83
+ current_desc.append(stripped)
84
+
85
+ if current_arg:
86
+ args_section[current_arg] = " ".join(current_desc).strip()
87
+
88
+ return args_section
89
+
90
+
91
+ def _get_param_type(hint) -> type | Callable[[str], bool]:
92
+ """Convert type hint to argparse type."""
93
+ import types
94
+ from typing import Union
95
+
96
+ if hint is bool:
97
+ return lambda x: x.lower() in ("true", "1", "yes")
98
+ if hint in (int, float, str):
99
+ return hint
100
+ # For Literal, use str (choices will constrain values)
101
+ if get_origin(hint) is Literal:
102
+ return str
103
+ # Handle Union types like int | None, str | None.
104
+ # PEP 604 `int | None` has origin types.UnionType; classic
105
+ # `Union[int, None]` has origin typing.Union — accept both.
106
+ origin = get_origin(hint)
107
+ union_origins = (Union, getattr(types, "UnionType", ()))
108
+ if origin in union_origins:
109
+ args = get_args(hint)
110
+ non_none_args = [a for a in args if a is not type(None)]
111
+ if len(non_none_args) == 1:
112
+ return _get_param_type(non_none_args[0])
113
+ return str
114
+
115
+
116
+ def _generate_parser(
117
+ func: Callable,
118
+ requires_tab: bool = True,
119
+ description: str = None,
120
+ ) -> argparse.ArgumentParser:
121
+ """Generate argparse parser from function signature."""
122
+ sig = inspect.signature(func)
123
+ hints = get_type_hints(func) if hasattr(func, "__annotations__") else {}
124
+ arg_descriptions = _parse_docstring_args(func.__doc__ or "")
125
+
126
+ parser = argparse.ArgumentParser(
127
+ description=description or func.__doc__,
128
+ formatter_class=argparse.RawDescriptionHelpFormatter,
129
+ )
130
+
131
+ # Add --port for browser connection (only when requires_tab)
132
+ if requires_tab:
133
+ parser.add_argument(
134
+ "--port",
135
+ "-p",
136
+ type=int,
137
+ default=None,
138
+ help="Chrome debugging port (auto-detects running Chrome if not specified)",
139
+ )
140
+
141
+ # Add arguments from function signature (skip 'tab' parameter)
142
+ for name, param in sig.parameters.items():
143
+ if name == "tab":
144
+ continue
145
+
146
+ hint = hints.get(name, str)
147
+ param_type = _get_param_type(hint)
148
+ required = param.default is inspect.Parameter.empty
149
+
150
+ # Get help text from docstring Args section, fallback to type hint
151
+ help_text = arg_descriptions.get(name)
152
+ if not help_text:
153
+ if hasattr(hint, "__name__"):
154
+ help_text = f"({hint.__name__})"
155
+ elif hasattr(hint, "__origin__"):
156
+ help_text = f"({str(hint)})"
157
+ else:
158
+ help_text = "(str)"
159
+
160
+ if hint is bool:
161
+ # For bool, use intuitive flag names:
162
+ # - default False: --flag to enable (store_true)
163
+ # - default True: --no-flag to disable (store_false)
164
+ if param.default is False or param.default is inspect.Parameter.empty:
165
+ parser.add_argument(
166
+ f"--{name.replace('_', '-')}",
167
+ action="store_true",
168
+ default=False,
169
+ help=help_text,
170
+ )
171
+ else:
172
+ # Default is True, use --no-xxx to disable
173
+ parser.add_argument(
174
+ f"--no-{name.replace('_', '-')}",
175
+ dest=name.replace("-", "_"),
176
+ action="store_false",
177
+ default=True,
178
+ help=f"Disable: {help_text}",
179
+ )
180
+ else:
181
+ kwargs: dict[str, Any] = {
182
+ "type": param_type,
183
+ "help": help_text,
184
+ }
185
+ if not required:
186
+ kwargs["default"] = param.default
187
+
188
+ # Handle Literal types with choices
189
+ choices = _get_literal_choices(hint)
190
+ if choices:
191
+ kwargs["choices"] = choices
192
+ kwargs["help"] = f"One of: {', '.join(str(c) for c in choices)}"
193
+
194
+ parser.add_argument(
195
+ f"--{name.replace('_', '-')}",
196
+ required=required,
197
+ **kwargs, # type: ignore[arg-type]
198
+ )
199
+
200
+ return parser
201
+
202
+
203
+ def as_cli(requires_tab: bool = True):
204
+ """Decorator that adds CLI capability to a function.
205
+
206
+ Args:
207
+ requires_tab: If True (default), the function requires a browser tab.
208
+ CLI will auto-connect to browser and pass tab as first arg.
209
+ If False, the function manages browser lifecycle itself.
210
+
211
+ The decorated function can be:
212
+ 1. Imported and called directly: await func(tab, ...) or func(...)
213
+ 2. Run as CLI: python -m module --arg value
214
+
215
+ Returns:
216
+ Decorator function
217
+ """
218
+
219
+ def decorator(func: Callable) -> Callable:
220
+ is_async = asyncio.iscoroutinefunction(func)
221
+
222
+ @functools.wraps(func)
223
+ async def async_wrapper(*args, **kwargs):
224
+ return await func(*args, **kwargs)
225
+
226
+ @functools.wraps(func)
227
+ def sync_wrapper(*args, **kwargs):
228
+ return func(*args, **kwargs)
229
+
230
+ wrapper = async_wrapper if is_async else sync_wrapper
231
+
232
+ def cli_main():
233
+ """Entry point for CLI usage."""
234
+ # When AI_DEV_BROWSER_REDIRECT is set, block direct access and
235
+ # print the redirect message (controlled by the embedding app).
236
+ redirect = os.environ.get("AI_DEV_BROWSER_REDIRECT")
237
+ if redirect:
238
+ print(redirect, file=sys.stderr)
239
+ sys.exit(1)
240
+
241
+ parser = _generate_parser(func, requires_tab=requires_tab)
242
+ args = parser.parse_args()
243
+
244
+ if requires_tab:
245
+ # Connect to browser and get tab
246
+ from ai_dev_browser.core import connect_browser, get_active_tab
247
+
248
+ async def run():
249
+ try:
250
+ port = args.port
251
+ if port is None:
252
+ # Check env var first (set by embedding apps like Sudowork)
253
+ env_port = os.environ.get("AI_DEV_BROWSER_PORT")
254
+ if env_port:
255
+ port = int(env_port)
256
+ else:
257
+ # Auto-detect: find a running Chrome in this workspace
258
+ from ai_dev_browser.core.port import (
259
+ find_workspace_chromes,
260
+ )
261
+
262
+ for candidate, _pid in find_workspace_chromes():
263
+ port = candidate
264
+ break
265
+
266
+ if port is None:
267
+ print(
268
+ json.dumps(
269
+ {
270
+ "error": "No available Chrome found. "
271
+ "Run browser-start first, or specify --port."
272
+ },
273
+ ensure_ascii=False,
274
+ indent=2,
275
+ )
276
+ )
277
+ sys.exit(1)
278
+
279
+ browser = await connect_browser(port=port)
280
+ tab = await get_active_tab(browser)
281
+
282
+ # Build kwargs from args, excluding 'port'
283
+ kwargs = {
284
+ k.replace("-", "_"): v
285
+ for k, v in vars(args).items()
286
+ if k != "port"
287
+ }
288
+
289
+ result = await func(tab, **kwargs)
290
+ print(json.dumps(result, ensure_ascii=False, indent=2))
291
+
292
+ except Exception as e:
293
+ print(
294
+ json.dumps({"error": str(e)}, ensure_ascii=False, indent=2)
295
+ )
296
+ sys.exit(1)
297
+
298
+ asyncio.run(run())
299
+ else:
300
+ # No browser connection needed
301
+ try:
302
+ kwargs = {k.replace("-", "_"): v for k, v in vars(args).items()}
303
+
304
+ result = asyncio.run(func(**kwargs)) if is_async else func(**kwargs)
305
+
306
+ print(json.dumps(result, ensure_ascii=False, indent=2))
307
+
308
+ except Exception as e:
309
+ print(json.dumps({"error": str(e)}, ensure_ascii=False, indent=2))
310
+ sys.exit(1)
311
+
312
+ # Attach CLI runner to the function
313
+ wrapper.cli_main = cli_main # type: ignore[attr-defined]
314
+ wrapper.__wrapped__ = func # type: ignore[attr-defined]
315
+
316
+ return wrapper
317
+
318
+ return decorator
319
+
320
+
321
+ def output(data: dict) -> None:
322
+ """Output JSON to stdout."""
323
+ print(json.dumps(data, ensure_ascii=False, indent=2))
324
+
325
+
326
+ def error(message: str, code: int = 1) -> None:
327
+ """Output error and exit."""
328
+ output({"error": message})
329
+ sys.exit(code)
330
+
331
+
332
+ def _json_serializable(obj: Any) -> bool:
333
+ """Check if object is JSON serializable."""
334
+ try:
335
+ json.dumps(obj)
336
+ return True
337
+ except (TypeError, ValueError):
338
+ return False
339
+
340
+
341
+ def _filter_dict_for_json(d: dict) -> dict:
342
+ """Filter dict to only JSON-serializable values."""
343
+ return {k: v for k, v in d.items() if _json_serializable(v)}
344
+
345
+
346
+ def wrap_core(core_func: Callable, result_key: str = "success") -> Callable:
347
+ """Wrap an async core function for CLI use, preserving its signature (SSOT).
348
+
349
+ This enables true SSOT: parameters are defined once in core function,
350
+ CLI automatically inherits them.
351
+
352
+ Non-JSON-serializable values (like Tab objects) are automatically filtered
353
+ from the output. Core functions can return them for programmatic use,
354
+ but CLI will only show serializable values.
355
+
356
+ Args:
357
+ core_func: The async core function to wrap
358
+ result_key: Key name for successful result (e.g., "clicked", "typed")
359
+
360
+ Returns:
361
+ Wrapped function with same signature, JSON-formatted output
362
+
363
+ Example:
364
+ # element_click.py - True SSOT
365
+ from ai_dev_browser.core import click
366
+ from .._cli import as_cli, wrap_core
367
+
368
+ element_click = as_cli()(wrap_core(click, "clicked"))
369
+ """
370
+
371
+ @functools.wraps(core_func)
372
+ async def wrapper(*args, **kwargs):
373
+ try:
374
+ result = await core_func(*args, **kwargs)
375
+ if isinstance(result, bool):
376
+ if result:
377
+ return {result_key: True}
378
+ else:
379
+ return {"error": "Operation failed"}
380
+ elif isinstance(result, dict):
381
+ # Filter out non-serializable values for CLI output
382
+ return _filter_dict_for_json(result)
383
+ else:
384
+ return {result_key: result}
385
+ except Exception as e:
386
+ return {"error": f"{core_func.__name__} failed: {e}"}
387
+
388
+ return wrapper
389
+
390
+
391
+ def wrap_core_sync(core_func: Callable, result_key: str = "success") -> Callable:
392
+ """Wrap a sync core function for CLI use, preserving its signature (SSOT).
393
+
394
+ Same as wrap_core but for synchronous functions.
395
+
396
+ Args:
397
+ core_func: The sync core function to wrap
398
+ result_key: Key name for successful result
399
+
400
+ Returns:
401
+ Wrapped sync function with same signature, JSON-formatted output
402
+
403
+ Example:
404
+ # browser_start.py - True SSOT
405
+ from ai_dev_browser.core import browser_start
406
+ from .._cli import as_cli, wrap_core_sync
407
+
408
+ browser_start = as_cli(requires_tab=False)(wrap_core_sync(browser_start, "port"))
409
+ """
410
+
411
+ @functools.wraps(core_func)
412
+ def wrapper(*args, **kwargs):
413
+ try:
414
+ result = core_func(*args, **kwargs)
415
+ if isinstance(result, bool):
416
+ if result:
417
+ return {result_key: True}
418
+ else:
419
+ return {"error": "Operation failed"}
420
+ elif isinstance(result, dict):
421
+ # Filter out non-serializable values for CLI output
422
+ return _filter_dict_for_json(result)
423
+ else:
424
+ return {result_key: result}
425
+ except Exception as e:
426
+ return {"error": f"{core_func.__name__} failed: {e}"}
427
+
428
+ return wrapper
@@ -0,0 +1,13 @@
1
+ """Package version — SSOT derived from git tag via setuptools-scm.
2
+
3
+ See [tool.setuptools_scm] in pyproject.toml. Release flow:
4
+ git tag vX.Y.Z && git push --tags
5
+ """
6
+
7
+ from importlib.metadata import PackageNotFoundError, version
8
+
9
+
10
+ try:
11
+ __version__ = version("ai-dev-browser")
12
+ except PackageNotFoundError:
13
+ __version__ = "0.0.0"
@@ -0,0 +1,6 @@
1
+ # DO NOT EDIT THIS FILE!
2
+ #
3
+ # This file is generated from the CDP specification. If you need to make
4
+ # changes, edit the generator and regenerate all of the modules.
5
+
6
+ from . import (accessibility, animation, audits, autofill, background_service, bluetooth_emulation, browser, css, cache_storage, cast, console, crash_report_context, dom, dom_debugger, dom_snapshot, dom_storage, debugger, device_access, device_orientation, emulation, event_breakpoints, extensions, fed_cm, fetch, file_system, headless_experimental, heap_profiler, io, indexed_db, input_, inspector, layer_tree, log, media, memory, network, overlay, pwa, page, performance, performance_timeline, preload, profiler, runtime, schema, security, service_worker, smart_card_emulation, storage, system_info, target, tethering, tracing, web_audio, web_authn, web_mcp)