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.
- ai_dev_browser/__init__.py +110 -0
- ai_dev_browser/_cli.py +428 -0
- ai_dev_browser/_version.py +13 -0
- ai_dev_browser/cdp/__init__.py +6 -0
- ai_dev_browser/cdp/accessibility.py +668 -0
- ai_dev_browser/cdp/animation.py +494 -0
- ai_dev_browser/cdp/audits.py +1990 -0
- ai_dev_browser/cdp/autofill.py +292 -0
- ai_dev_browser/cdp/background_service.py +215 -0
- ai_dev_browser/cdp/bluetooth_emulation.py +626 -0
- ai_dev_browser/cdp/browser.py +821 -0
- ai_dev_browser/cdp/cache_storage.py +311 -0
- ai_dev_browser/cdp/cast.py +172 -0
- ai_dev_browser/cdp/console.py +107 -0
- ai_dev_browser/cdp/crash_report_context.py +55 -0
- ai_dev_browser/cdp/css.py +2710 -0
- ai_dev_browser/cdp/debugger.py +1405 -0
- ai_dev_browser/cdp/device_access.py +141 -0
- ai_dev_browser/cdp/device_orientation.py +45 -0
- ai_dev_browser/cdp/dom.py +2257 -0
- ai_dev_browser/cdp/dom_debugger.py +321 -0
- ai_dev_browser/cdp/dom_snapshot.py +876 -0
- ai_dev_browser/cdp/dom_storage.py +222 -0
- ai_dev_browser/cdp/emulation.py +1779 -0
- ai_dev_browser/cdp/event_breakpoints.py +56 -0
- ai_dev_browser/cdp/extensions.py +246 -0
- ai_dev_browser/cdp/fed_cm.py +283 -0
- ai_dev_browser/cdp/fetch.py +507 -0
- ai_dev_browser/cdp/file_system.py +115 -0
- ai_dev_browser/cdp/headless_experimental.py +115 -0
- ai_dev_browser/cdp/heap_profiler.py +401 -0
- ai_dev_browser/cdp/indexed_db.py +528 -0
- ai_dev_browser/cdp/input_.py +701 -0
- ai_dev_browser/cdp/inspector.py +95 -0
- ai_dev_browser/cdp/io.py +101 -0
- ai_dev_browser/cdp/layer_tree.py +464 -0
- ai_dev_browser/cdp/log.py +190 -0
- ai_dev_browser/cdp/media.py +313 -0
- ai_dev_browser/cdp/memory.py +305 -0
- ai_dev_browser/cdp/network.py +5317 -0
- ai_dev_browser/cdp/overlay.py +1468 -0
- ai_dev_browser/cdp/page.py +3970 -0
- ai_dev_browser/cdp/performance.py +124 -0
- ai_dev_browser/cdp/performance_timeline.py +200 -0
- ai_dev_browser/cdp/preload.py +575 -0
- ai_dev_browser/cdp/profiler.py +420 -0
- ai_dev_browser/cdp/pwa.py +278 -0
- ai_dev_browser/cdp/py.typed +0 -0
- ai_dev_browser/cdp/runtime.py +1589 -0
- ai_dev_browser/cdp/schema.py +50 -0
- ai_dev_browser/cdp/security.py +518 -0
- ai_dev_browser/cdp/service_worker.py +401 -0
- ai_dev_browser/cdp/smart_card_emulation.py +891 -0
- ai_dev_browser/cdp/storage.py +1573 -0
- ai_dev_browser/cdp/system_info.py +327 -0
- ai_dev_browser/cdp/target.py +822 -0
- ai_dev_browser/cdp/tethering.py +65 -0
- ai_dev_browser/cdp/tracing.py +377 -0
- ai_dev_browser/cdp/util.py +17 -0
- ai_dev_browser/cdp/web_audio.py +606 -0
- ai_dev_browser/cdp/web_authn.py +598 -0
- ai_dev_browser/cdp/web_mcp.py +219 -0
- ai_dev_browser/core/__init__.py +218 -0
- ai_dev_browser/core/_case.py +38 -0
- ai_dev_browser/core/_cf_template.py +84 -0
- ai_dev_browser/core/_element.py +431 -0
- ai_dev_browser/core/_tab.py +817 -0
- ai_dev_browser/core/_transport.py +362 -0
- ai_dev_browser/core/ax.py +605 -0
- ai_dev_browser/core/browser.py +323 -0
- ai_dev_browser/core/cdp.py +66 -0
- ai_dev_browser/core/chrome.py +253 -0
- ai_dev_browser/core/cloudflare.py +77 -0
- ai_dev_browser/core/config.py +95 -0
- ai_dev_browser/core/connection.py +403 -0
- ai_dev_browser/core/cookies.py +103 -0
- ai_dev_browser/core/dialog.py +165 -0
- ai_dev_browser/core/download.py +38 -0
- ai_dev_browser/core/elements.py +913 -0
- ai_dev_browser/core/human.py +612 -0
- ai_dev_browser/core/login.py +122 -0
- ai_dev_browser/core/mouse.py +140 -0
- ai_dev_browser/core/navigation.py +225 -0
- ai_dev_browser/core/overlays.py +148 -0
- ai_dev_browser/core/page.py +302 -0
- ai_dev_browser/core/port.py +465 -0
- ai_dev_browser/core/process.py +114 -0
- ai_dev_browser/core/snapshot.py +425 -0
- ai_dev_browser/core/storage.py +50 -0
- ai_dev_browser/core/tabs.py +125 -0
- ai_dev_browser/core/text_match.py +217 -0
- ai_dev_browser/core/window.py +84 -0
- ai_dev_browser/pool/__init__.py +34 -0
- ai_dev_browser/pool/job.py +140 -0
- ai_dev_browser/pool/persistence.py +154 -0
- ai_dev_browser/pool/pool.py +873 -0
- ai_dev_browser/pool/worker.py +171 -0
- ai_dev_browser/profile.py +107 -0
- ai_dev_browser/py.typed +0 -0
- ai_dev_browser/tools/__init__.py +9 -0
- ai_dev_browser/tools/_generate.py +209 -0
- ai_dev_browser/tools/browser_list.py +14 -0
- ai_dev_browser/tools/browser_start.py +14 -0
- ai_dev_browser/tools/browser_stop.py +14 -0
- ai_dev_browser/tools/cdp_send.py +14 -0
- ai_dev_browser/tools/click_by_html_id.py +14 -0
- ai_dev_browser/tools/click_by_ref.py +14 -0
- ai_dev_browser/tools/click_by_text.py +14 -0
- ai_dev_browser/tools/click_by_xpath.py +14 -0
- ai_dev_browser/tools/cloudflare_verify.py +14 -0
- ai_dev_browser/tools/cookies_list.py +14 -0
- ai_dev_browser/tools/cookies_load.py +14 -0
- ai_dev_browser/tools/cookies_save.py +14 -0
- ai_dev_browser/tools/dialog_respond.py +14 -0
- ai_dev_browser/tools/download.py +14 -0
- ai_dev_browser/tools/drag_by_ref.py +14 -0
- ai_dev_browser/tools/find_by_html_id.py +14 -0
- ai_dev_browser/tools/find_by_xpath.py +14 -0
- ai_dev_browser/tools/focus_by_ref.py +14 -0
- ai_dev_browser/tools/highlight_by_ref.py +14 -0
- ai_dev_browser/tools/hover_by_ref.py +14 -0
- ai_dev_browser/tools/html_by_ref.py +14 -0
- ai_dev_browser/tools/js_evaluate.py +14 -0
- ai_dev_browser/tools/login_interactive.py +14 -0
- ai_dev_browser/tools/mouse_click.py +14 -0
- ai_dev_browser/tools/mouse_drag.py +14 -0
- ai_dev_browser/tools/mouse_move.py +14 -0
- ai_dev_browser/tools/page_discover.py +14 -0
- ai_dev_browser/tools/page_emulate_focus.py +14 -0
- ai_dev_browser/tools/page_goto.py +14 -0
- ai_dev_browser/tools/page_html.py +14 -0
- ai_dev_browser/tools/page_info.py +14 -0
- ai_dev_browser/tools/page_reload.py +14 -0
- ai_dev_browser/tools/page_screenshot.py +14 -0
- ai_dev_browser/tools/page_scroll.py +14 -0
- ai_dev_browser/tools/page_wait_element.py +14 -0
- ai_dev_browser/tools/page_wait_ready.py +14 -0
- ai_dev_browser/tools/page_wait_url.py +14 -0
- ai_dev_browser/tools/screenshot_by_ref.py +14 -0
- ai_dev_browser/tools/select_by_ref.py +14 -0
- ai_dev_browser/tools/storage_get.py +14 -0
- ai_dev_browser/tools/storage_set.py +14 -0
- ai_dev_browser/tools/tab_close.py +14 -0
- ai_dev_browser/tools/tab_list.py +14 -0
- ai_dev_browser/tools/tab_new.py +14 -0
- ai_dev_browser/tools/tab_switch.py +14 -0
- ai_dev_browser/tools/type_by_ref.py +14 -0
- ai_dev_browser/tools/type_by_text.py +14 -0
- ai_dev_browser/tools/upload_by_ref.py +14 -0
- ai_dev_browser/tools/window_set.py +14 -0
- ai_dev_browser-0.5.3.dist-info/METADATA +177 -0
- ai_dev_browser-0.5.3.dist-info/RECORD +155 -0
- ai_dev_browser-0.5.3.dist-info/WHEEL +5 -0
- ai_dev_browser-0.5.3.dist-info/licenses/LICENSE +25 -0
- 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)
|