browser-tools 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- browser_tools/__init__.py +22 -0
- browser_tools/attach_chrome.sh +124 -0
- browser_tools/browser_state.py +157 -0
- browser_tools/browser_tools_session.py +1132 -0
- browser_tools/camoufox_session.py +332 -0
- browser_tools/cdp_client.py +253 -0
- browser_tools/cdp_constants.py +152 -0
- browser_tools/cdp_handler.py +1555 -0
- browser_tools/chrome_config.py +415 -0
- browser_tools/chrome_utils.py +559 -0
- browser_tools/daemon_client.py +125 -0
- browser_tools/detect_interstitial.js +267 -0
- browser_tools/frame_manager.py +408 -0
- browser_tools/interstitial.py +202 -0
- browser_tools/mcp_daemon.py +753 -0
- browser_tools/mcp_session.py +202 -0
- browser_tools/persistent-session-template.mjs +132 -0
- browser_tools/persistent_browser.py +1309 -0
- browser_tools/process_utils.py +373 -0
- browser_tools/profiler.py +289 -0
- browser_tools/screenshot_utils.py +126 -0
- browser_tools/stealth.js +179 -0
- browser_tools-0.1.0.dist-info/METADATA +153 -0
- browser_tools-0.1.0.dist-info/RECORD +27 -0
- browser_tools-0.1.0.dist-info/WHEEL +4 -0
- browser_tools-0.1.0.dist-info/entry_points.txt +3 -0
- browser_tools-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Browser Tools automation package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .browser_state import ActiveAttachConfig, BrowserState, ProjectBrowserConfig
|
|
6
|
+
from .camoufox_session import CamoufoxSession
|
|
7
|
+
from .cdp_client import CDPClient, CDPError
|
|
8
|
+
from .frame_manager import FrameManager
|
|
9
|
+
from .persistent_browser import PersistentChromeController
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ActiveAttachConfig",
|
|
13
|
+
"BrowserState",
|
|
14
|
+
"CDPClient",
|
|
15
|
+
"CDPError",
|
|
16
|
+
"CamoufoxSession",
|
|
17
|
+
"FrameManager",
|
|
18
|
+
"PersistentChromeController",
|
|
19
|
+
"ProjectBrowserConfig",
|
|
20
|
+
"__version__",
|
|
21
|
+
]
|
|
22
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# attach-chrome.sh — Launch Chrome with remote debugging for browser-tools attach
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./attach-chrome.sh [--port PORT] [--profile PROFILE] [URL]
|
|
6
|
+
#
|
|
7
|
+
# Examples:
|
|
8
|
+
# ./attach-chrome.sh # Default port 9222
|
|
9
|
+
# ./attach-chrome.sh --port 9333 # Custom port
|
|
10
|
+
# ./attach-chrome.sh --profile dev # Named profile
|
|
11
|
+
# ./attach-chrome.sh https://myapp.localhost # Open specific URL
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
PORT=9222
|
|
16
|
+
PROFILE=""
|
|
17
|
+
URL=""
|
|
18
|
+
CHROME_CANARY="/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
|
|
19
|
+
CHROME_STABLE="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
20
|
+
|
|
21
|
+
while [[ $# -gt 0 ]]; do
|
|
22
|
+
case "$1" in
|
|
23
|
+
--port)
|
|
24
|
+
PORT="$2"
|
|
25
|
+
shift 2
|
|
26
|
+
;;
|
|
27
|
+
--profile)
|
|
28
|
+
PROFILE="$2"
|
|
29
|
+
shift 2
|
|
30
|
+
;;
|
|
31
|
+
--help|-h)
|
|
32
|
+
echo "Usage: $0 [--port PORT] [--profile PROFILE] [URL]"
|
|
33
|
+
echo ""
|
|
34
|
+
echo "Launch Chrome with remote debugging enabled for browser-tools."
|
|
35
|
+
echo ""
|
|
36
|
+
echo "Options:"
|
|
37
|
+
echo " --port PORT Remote debugging port (default: 9222)"
|
|
38
|
+
echo " --profile NAME Named profile for persistent sessions"
|
|
39
|
+
echo " URL URL to open on launch"
|
|
40
|
+
echo ""
|
|
41
|
+
echo "Then in your agent, use:"
|
|
42
|
+
echo ' attach_browser(endpoint="http://127.0.0.1:PORT")'
|
|
43
|
+
exit 0
|
|
44
|
+
;;
|
|
45
|
+
*)
|
|
46
|
+
URL="$1"
|
|
47
|
+
shift
|
|
48
|
+
;;
|
|
49
|
+
esac
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
# Find Chrome executable
|
|
53
|
+
CHROME=""
|
|
54
|
+
if [[ -x "$CHROME_CANARY" ]]; then
|
|
55
|
+
CHROME="$CHROME_CANARY"
|
|
56
|
+
elif [[ -x "$CHROME_STABLE" ]]; then
|
|
57
|
+
CHROME="$CHROME_STABLE"
|
|
58
|
+
elif command -v google-chrome &>/dev/null; then
|
|
59
|
+
CHROME="google-chrome"
|
|
60
|
+
elif command -v chromium &>/dev/null; then
|
|
61
|
+
CHROME="chromium"
|
|
62
|
+
else
|
|
63
|
+
echo "Error: Chrome not found. Install Chrome or Chrome Canary." >&2
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Build user-data-dir path
|
|
68
|
+
CACHE_DIR="$HOME/.cache/tool-proxy/browser-tools"
|
|
69
|
+
if [[ -n "$PROFILE" ]]; then
|
|
70
|
+
USER_DATA_DIR="$CACHE_DIR/profiles/$PROFILE"
|
|
71
|
+
else
|
|
72
|
+
USER_DATA_DIR="$CACHE_DIR/profiles/attach-$PORT"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
mkdir -p "$USER_DATA_DIR"
|
|
76
|
+
chmod 700 "$USER_DATA_DIR"
|
|
77
|
+
|
|
78
|
+
echo "Launching Chrome with remote debugging on port $PORT..."
|
|
79
|
+
echo " Executable: $CHROME"
|
|
80
|
+
echo " Profile: $USER_DATA_DIR"
|
|
81
|
+
|
|
82
|
+
ARGS=(
|
|
83
|
+
"--remote-debugging-port=$PORT"
|
|
84
|
+
"--user-data-dir=$USER_DATA_DIR"
|
|
85
|
+
"--no-first-run"
|
|
86
|
+
"--no-default-browser-check"
|
|
87
|
+
"--disable-sync"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if [[ -n "$URL" ]]; then
|
|
91
|
+
ARGS+=("$URL")
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Detach Chrome into a new session so it survives if our parent shell is
|
|
95
|
+
# killed (e.g. an agent's Bash tool call hits its timeout). nohup alone is
|
|
96
|
+
# not enough on macOS: when the shell's process group is signaled, the
|
|
97
|
+
# child dies with it. Python's start_new_session=True calls os.setsid()
|
|
98
|
+
# in the child before exec, putting Chrome in its own session/process group.
|
|
99
|
+
python3 - "$CHROME" "${ARGS[@]}" <<'PY'
|
|
100
|
+
import subprocess, sys
|
|
101
|
+
subprocess.Popen(
|
|
102
|
+
sys.argv[1:],
|
|
103
|
+
stdin=subprocess.DEVNULL,
|
|
104
|
+
stdout=subprocess.DEVNULL,
|
|
105
|
+
stderr=subprocess.DEVNULL,
|
|
106
|
+
start_new_session=True,
|
|
107
|
+
)
|
|
108
|
+
PY
|
|
109
|
+
|
|
110
|
+
# Wait for the remote debugging endpoint to become reachable so the caller
|
|
111
|
+
# can attach immediately on return.
|
|
112
|
+
for _ in $(seq 1 60); do
|
|
113
|
+
if curl -fsS "http://127.0.0.1:$PORT/json/version" >/dev/null 2>&1; then
|
|
114
|
+
echo ""
|
|
115
|
+
echo "Chrome ready on http://127.0.0.1:$PORT"
|
|
116
|
+
echo "Connect with: attach_browser(endpoint=\"http://127.0.0.1:$PORT\")"
|
|
117
|
+
exit 0
|
|
118
|
+
fi
|
|
119
|
+
sleep 0.5
|
|
120
|
+
done
|
|
121
|
+
|
|
122
|
+
echo "Error: Chrome did not become ready on port $PORT within 30s." >&2
|
|
123
|
+
echo "If another Chrome is already using $USER_DATA_DIR, kill it first or use --profile NAME." >&2
|
|
124
|
+
exit 1
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Persisted browser state dataclasses for browser-tools.
|
|
2
|
+
|
|
3
|
+
Extracted from persistent_browser.py to keep the module under 800 lines.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from dataclasses import asdict, dataclass
|
|
10
|
+
from pathlib import Path # noqa: TC003
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class BrowserState:
|
|
15
|
+
"""Persisted browser state shared across wrapper invocations."""
|
|
16
|
+
|
|
17
|
+
browser_url: str
|
|
18
|
+
selected_page_id: int | None = None
|
|
19
|
+
selected_page_url: str | None = None
|
|
20
|
+
pid: int | None = None
|
|
21
|
+
user_data_dir: str | None = None
|
|
22
|
+
headless: bool = False
|
|
23
|
+
isolated: bool = True
|
|
24
|
+
channel: str = "canary"
|
|
25
|
+
viewport: str | None = None
|
|
26
|
+
last_used_at: float = 0.0
|
|
27
|
+
daemon_pid: int | None = None
|
|
28
|
+
daemon_socket: str | None = None
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_path(cls, path: Path) -> BrowserState | None:
|
|
32
|
+
"""Load browser state from disk if it exists and is valid.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
path: JSON file containing persisted browser state.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
BrowserState when the file exists and parses successfully, otherwise None.
|
|
39
|
+
"""
|
|
40
|
+
if not path.exists():
|
|
41
|
+
return None
|
|
42
|
+
try:
|
|
43
|
+
data = json.loads(path.read_text())
|
|
44
|
+
except (OSError, json.JSONDecodeError):
|
|
45
|
+
return None
|
|
46
|
+
try:
|
|
47
|
+
return cls(**data)
|
|
48
|
+
except TypeError:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def save(self, path: Path) -> None:
|
|
52
|
+
"""Persist browser state to disk.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
path: JSON file to write.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
None.
|
|
59
|
+
"""
|
|
60
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
path.write_text(json.dumps(asdict(self), indent=2, sort_keys=True))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ActiveAttachConfig:
|
|
66
|
+
"""Persisted configuration for the currently attached external Chrome.
|
|
67
|
+
|
|
68
|
+
Stored separately from BrowserState so future tool-proxy invocations can
|
|
69
|
+
recreate the correct controller after attach_browser has exited.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
browser_url: str
|
|
73
|
+
profile: str | None = None
|
|
74
|
+
mode: str = "full"
|
|
75
|
+
stealth: bool = False
|
|
76
|
+
saved_at: float = 0.0
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_path(cls, path: Path) -> ActiveAttachConfig | None:
|
|
80
|
+
"""Load config from disk.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
path: JSON file containing the saved attach config.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Parsed config, or None when the file is missing/invalid.
|
|
87
|
+
"""
|
|
88
|
+
if not path.exists():
|
|
89
|
+
return None
|
|
90
|
+
try:
|
|
91
|
+
data = json.loads(path.read_text())
|
|
92
|
+
except (OSError, json.JSONDecodeError):
|
|
93
|
+
return None
|
|
94
|
+
try:
|
|
95
|
+
return cls(**data)
|
|
96
|
+
except TypeError:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def save(self, path: Path) -> None:
|
|
100
|
+
"""Persist config to disk.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
path: JSON file to write.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
None.
|
|
107
|
+
"""
|
|
108
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
path.write_text(json.dumps(asdict(self), indent=2, sort_keys=True))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class ProjectBrowserConfig:
|
|
114
|
+
"""Preferred browser session configuration loaded from a project file."""
|
|
115
|
+
|
|
116
|
+
mode: str = "headless"
|
|
117
|
+
profile: str | None = None
|
|
118
|
+
endpoint: str | None = None
|
|
119
|
+
browser_url: str | None = None
|
|
120
|
+
headless: bool | None = None
|
|
121
|
+
isolated: bool | None = None
|
|
122
|
+
channel: str = "canary"
|
|
123
|
+
viewport: str | None = None
|
|
124
|
+
stealth: bool = False
|
|
125
|
+
saved_at: float = 0.0
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def from_path(cls, path: Path) -> ProjectBrowserConfig | None:
|
|
129
|
+
"""Load a project browser preference file.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
path: JSON project preference path.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Parsed project browser config, or None when missing/invalid.
|
|
136
|
+
"""
|
|
137
|
+
if not path.exists():
|
|
138
|
+
return None
|
|
139
|
+
try:
|
|
140
|
+
data = json.loads(path.read_text())
|
|
141
|
+
except (OSError, json.JSONDecodeError):
|
|
142
|
+
return None
|
|
143
|
+
if not isinstance(data, dict):
|
|
144
|
+
return None
|
|
145
|
+
if isinstance(data.get("preferred_session"), dict):
|
|
146
|
+
data = data["preferred_session"]
|
|
147
|
+
if isinstance(data.get("preferredSession"), dict):
|
|
148
|
+
data = data["preferredSession"]
|
|
149
|
+
allowed = {field.name for field in cls.__dataclass_fields__.values()}
|
|
150
|
+
return cls(**{key: value for key, value in data.items() if key in allowed})
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
__all__ = [
|
|
154
|
+
"ActiveAttachConfig",
|
|
155
|
+
"BrowserState",
|
|
156
|
+
"ProjectBrowserConfig",
|
|
157
|
+
]
|