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,158 @@
1
+ """Playwright-backed browser session wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from types import TracebackType
6
+ from typing import Any
7
+
8
+ from browser_cli.browser.models import BrowserLaunchConfig
9
+ from browser_cli.browser.snapshot import capture_snapshot
10
+ from browser_cli.browser.stealth import (
11
+ build_context_options,
12
+ build_ignore_default_args,
13
+ build_init_script,
14
+ build_launch_args,
15
+ )
16
+ from browser_cli.errors import BrowserUnavailableError, ProfileUnavailableError, TemporaryReadError
17
+
18
+
19
+ class BrowserSession:
20
+ def __init__(self, config: BrowserLaunchConfig) -> None:
21
+ self._config = config
22
+ self._playwright: Any | None = None
23
+ self._context: Any | None = None
24
+ self._page: Any | None = None
25
+
26
+ async def __aenter__(self) -> BrowserSession:
27
+ await self.start()
28
+ return self
29
+
30
+ async def __aexit__(
31
+ self,
32
+ exc_type: type[BaseException] | None,
33
+ exc: BaseException | None,
34
+ tb: TracebackType | None,
35
+ ) -> None:
36
+ await self.close()
37
+
38
+ async def start(self) -> None:
39
+ try:
40
+ from playwright.async_api import async_playwright
41
+ except ImportError as exc: # pragma: no cover - dependency problem
42
+ raise BrowserUnavailableError(
43
+ "Playwright is not installed in this repo environment. Run: uv sync --dev"
44
+ ) from exc
45
+
46
+ try:
47
+ self._playwright = await async_playwright().start()
48
+ chromium = self._playwright.chromium
49
+ context_options = build_context_options(
50
+ viewport_width=self._config.viewport_width,
51
+ viewport_height=self._config.viewport_height,
52
+ locale=self._config.locale,
53
+ )
54
+ self._context = await chromium.launch_persistent_context(
55
+ user_data_dir=str(self._config.user_data_dir),
56
+ executable_path=str(self._config.executable_path)
57
+ if self._config.executable_path
58
+ else None,
59
+ headless=self._config.headless,
60
+ viewport={
61
+ "width": self._config.viewport_width,
62
+ "height": self._config.viewport_height,
63
+ },
64
+ ignore_default_args=build_ignore_default_args(),
65
+ args=[
66
+ *build_launch_args(
67
+ headless=self._config.headless,
68
+ viewport_width=self._config.viewport_width,
69
+ viewport_height=self._config.viewport_height,
70
+ locale=self._config.locale,
71
+ ),
72
+ f"--profile-directory={self._config.profile_directory}",
73
+ ],
74
+ **context_options,
75
+ )
76
+ init_script = build_init_script(
77
+ headless=self._config.headless, locale=self._config.locale
78
+ )
79
+ if init_script:
80
+ await self._context.add_init_script(init_script)
81
+ self._page = (
82
+ self._context.pages[0] if self._context.pages else await self._context.new_page()
83
+ )
84
+ self._page.set_default_navigation_timeout(self._config.navigation_timeout_ms)
85
+ except Exception as exc:
86
+ await self.close()
87
+ self._raise_launch_error(exc)
88
+
89
+ async def close(self) -> None:
90
+ if self._context is not None:
91
+ await self._context.close()
92
+ self._context = None
93
+ if self._playwright is not None:
94
+ await self._playwright.stop()
95
+ self._playwright = None
96
+ self._page = None
97
+
98
+ async def navigate(self, url: str) -> None:
99
+ self._ensure_started()
100
+ try:
101
+ await self._page.goto(
102
+ url, wait_until="load", timeout=self._config.navigation_timeout_ms
103
+ )
104
+ except Exception as exc:
105
+ raise TemporaryReadError(f"Failed to navigate to {url}: {exc}") from exc
106
+
107
+ async def settle(self) -> None:
108
+ self._ensure_started()
109
+ await self._page.wait_for_timeout(self._config.settle_timeout_ms)
110
+
111
+ async def scroll_to_bottom(self) -> None:
112
+ self._ensure_started()
113
+ stable_rounds = 0
114
+ previous_height = -1
115
+ for _ in range(8):
116
+ current_height = await self._page.evaluate(
117
+ "() => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)"
118
+ )
119
+ await self._page.evaluate(
120
+ "() => window.scrollTo({ top: document.body.scrollHeight, behavior: 'instant' })"
121
+ )
122
+ await self._page.wait_for_timeout(450)
123
+ next_height = await self._page.evaluate(
124
+ "() => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)"
125
+ )
126
+ if next_height == current_height == previous_height:
127
+ stable_rounds += 1
128
+ else:
129
+ stable_rounds = 0
130
+ previous_height = next_height
131
+ if stable_rounds >= 1:
132
+ break
133
+
134
+ async def capture_html(self) -> str:
135
+ self._ensure_started()
136
+ return await self._page.content()
137
+
138
+ async def capture_snapshot(self) -> str:
139
+ self._ensure_started()
140
+ snapshot = await capture_snapshot(self._page, page_id="read_page")
141
+ return snapshot.tree
142
+
143
+ def _ensure_started(self) -> None:
144
+ if self._page is None:
145
+ raise TemporaryReadError("Browser session was not started.")
146
+
147
+ def _raise_launch_error(self, exc: Exception) -> None:
148
+ message = str(exc)
149
+ lowered = message.lower()
150
+ if (
151
+ "singleton" in lowered
152
+ or "profile" in lowered
153
+ or "user data directory is already in use" in lowered
154
+ ):
155
+ raise ProfileUnavailableError(message) from exc
156
+ if "executable" in lowered or "browser" in lowered or "failed to launch" in lowered:
157
+ raise BrowserUnavailableError(message) from exc
158
+ raise TemporaryReadError(message) from exc
@@ -0,0 +1,35 @@
1
+ """Compatibility wrapper around semantic snapshot generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from browser_cli.refs import SemanticSnapshotGenerator
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class SnapshotCapture:
13
+ tree: str
14
+ refs: dict[str, dict[str, Any]]
15
+ snapshot_id: str
16
+
17
+
18
+ async def capture_snapshot(
19
+ page: Any,
20
+ *,
21
+ page_id: str,
22
+ interactive: bool = False,
23
+ full_page: bool = True,
24
+ ) -> SnapshotCapture:
25
+ snapshot = await SemanticSnapshotGenerator().get_snapshot(
26
+ page,
27
+ page_id=page_id,
28
+ interactive=interactive,
29
+ full_page=full_page,
30
+ )
31
+ return SnapshotCapture(
32
+ tree=snapshot.tree,
33
+ refs={ref: data.to_summary() for ref, data in snapshot.refs.items()},
34
+ snapshot_id=snapshot.metadata.snapshot_id,
35
+ )
@@ -0,0 +1,450 @@
1
+ """Stealth helpers adapted from bridgic-browser concepts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ CHROME_DISABLED_COMPONENTS = [
12
+ "AcceptCHFrame",
13
+ "AutoExpandDetailsElement",
14
+ "AvoidUnnecessaryBeforeUnloadCheckSync",
15
+ "CertificateTransparencyComponentUpdater",
16
+ "DestroyProfileOnBrowserClose",
17
+ "DialMediaRouteProvider",
18
+ "ExtensionManifestV2Disabled",
19
+ "GlobalMediaControls",
20
+ "HttpsUpgrades",
21
+ "ImprovedCookieControls",
22
+ "LazyFrameLoading",
23
+ "LensOverlay",
24
+ "MediaRouter",
25
+ "PaintHolding",
26
+ "ThirdPartyStoragePartitioning",
27
+ "Translate",
28
+ "AutomationControlled",
29
+ "BackForwardCache",
30
+ "OptimizationHints",
31
+ "ProcessPerSiteUpToMainFrameThreshold",
32
+ "InterestFeedContentSuggestions",
33
+ "CalculateNativeWinOcclusion",
34
+ "HeavyAdPrivacyMitigations",
35
+ "PrivacySandboxSettings4",
36
+ "AutofillServerCommunication",
37
+ "CrashReporting",
38
+ "OverscrollHistoryNavigation",
39
+ "InfiniteSessionRestore",
40
+ "ExtensionDisableUnsupportedDeveloper",
41
+ "ExtensionManifestV2Unsupported",
42
+ ]
43
+
44
+ CHROME_DISABLED_COMPONENTS_HEADED = [
45
+ "AutomationControlled",
46
+ "InfiniteSessionRestore",
47
+ ]
48
+
49
+ CHROME_STEALTH_ARGS = [
50
+ "--disable-field-trial-config",
51
+ "--disable-background-networking",
52
+ "--disable-background-timer-throttling",
53
+ "--disable-backgrounding-occluded-windows",
54
+ "--disable-back-forward-cache",
55
+ "--disable-breakpad",
56
+ "--disable-client-side-phishing-detection",
57
+ "--disable-component-extensions-with-background-pages",
58
+ "--disable-component-update",
59
+ "--no-default-browser-check",
60
+ "--disable-hang-monitor",
61
+ "--disable-ipc-flooding-protection",
62
+ "--disable-popup-blocking",
63
+ "--disable-prompt-on-repost",
64
+ "--disable-renderer-backgrounding",
65
+ "--metrics-recording-only",
66
+ "--no-first-run",
67
+ "--no-service-autorun",
68
+ "--export-tagged-pdf",
69
+ "--disable-search-engine-choice-screen",
70
+ "--unsafely-disable-devtools-self-xss-warnings",
71
+ "--disable-sync",
72
+ "--allow-legacy-extension-manifests",
73
+ "--allow-pre-commit-input",
74
+ "--disable-blink-features=AutomationControlled",
75
+ "--log-level=2",
76
+ "--lang=en-US",
77
+ "--disable-focus-on-load",
78
+ "--disable-window-activation",
79
+ "--generate-pdf-document-outline",
80
+ "--no-pings",
81
+ "--disable-infobars",
82
+ "--hide-crash-restore-bubble",
83
+ "--disable-domain-reliability",
84
+ "--disable-datasaver-prompt",
85
+ "--disable-speech-synthesis-api",
86
+ "--disable-speech-api",
87
+ "--disable-print-preview",
88
+ "--safebrowsing-disable-auto-update",
89
+ "--disable-external-intent-requests",
90
+ "--disable-desktop-notifications",
91
+ "--noerrdialogs",
92
+ "--silent-debugger-extension-api",
93
+ "--disable-extensions-http-throttling",
94
+ "--extensions-on-chrome-urls",
95
+ "--disable-default-apps",
96
+ ]
97
+
98
+ CHROME_STEALTH_ARGS_HEADED = [
99
+ "--disable-blink-features=AutomationControlled",
100
+ "--no-first-run",
101
+ "--no-default-browser-check",
102
+ "--disable-infobars",
103
+ "--hide-crash-restore-bubble",
104
+ "--no-service-autorun",
105
+ "--log-level=2",
106
+ "--disable-search-engine-choice-screen",
107
+ "--unsafely-disable-devtools-self-xss-warnings",
108
+ "--disable-popup-blocking",
109
+ ]
110
+
111
+ CHROME_LINUX_ONLY_ARGS = [
112
+ "--disable-dev-shm-usage",
113
+ "--ash-no-nudges",
114
+ "--suppress-message-center-popups",
115
+ ]
116
+
117
+ CHROME_IGNORE_DEFAULT_ARGS = [
118
+ "--enable-automation",
119
+ "--hide-scrollbars",
120
+ ]
121
+
122
+ DEFAULT_PERMISSIONS = [
123
+ "clipboard-read",
124
+ "clipboard-write",
125
+ "notifications",
126
+ ]
127
+
128
+ _STEALTH_INIT_SCRIPT_TEMPLATE = r"""
129
+ (function () {
130
+ const _nativeFns = new WeakSet();
131
+ const _nativeFnNames = new WeakMap();
132
+ const _mkNative = (fn, name) => {
133
+ _nativeFns.add(fn);
134
+ if (name !== undefined) _nativeFnNames.set(fn, name);
135
+ return fn;
136
+ };
137
+
138
+ const _origFnToString = Function.prototype.toString;
139
+ Function.prototype.toString = _mkNative(function toString() {
140
+ if (_nativeFns.has(this)) {
141
+ const _n = _nativeFnNames.has(this) ? _nativeFnNames.get(this) : (this.name || '');
142
+ return `function ${_n}() { [native code] }`;
143
+ }
144
+ return _origFnToString.call(this);
145
+ }, 'toString');
146
+
147
+ const _wdDesc = Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver');
148
+ if (_wdDesc) {
149
+ Object.defineProperty(Navigator.prototype, 'webdriver', {
150
+ get: _mkNative(function () { return undefined; }, ''),
151
+ configurable: true,
152
+ });
153
+ } else if (navigator.webdriver !== undefined) {
154
+ Object.defineProperty(navigator, 'webdriver', {
155
+ get: _mkNative(function () { return undefined; }, ''),
156
+ configurable: true,
157
+ });
158
+ }
159
+
160
+ const _makeMime = (type, suffixes, description) =>
161
+ ({ type, suffixes, description, enabledPlugin: null });
162
+
163
+ const _pdfMimes = [
164
+ _makeMime('application/pdf', 'pdf', 'Portable Document Format'),
165
+ _makeMime('text/pdf', 'pdf', 'Portable Document Format'),
166
+ ];
167
+
168
+ const _makePlugin = (name, description) => {
169
+ const p = { name, description, filename: 'internal-pdf-viewer', length: _pdfMimes.length };
170
+ _pdfMimes.forEach((m, i) => {
171
+ const localMime = { type: m.type, suffixes: m.suffixes, description: m.description, enabledPlugin: p };
172
+ Object.defineProperty(p, i, { value: localMime, enumerable: true });
173
+ });
174
+ p.item = _mkNative(function item(i) { return p[i] ?? null; }, 'item');
175
+ p.namedItem = _mkNative(function namedItem(n) {
176
+ const idx = _pdfMimes.findIndex(m => m.type === n);
177
+ return idx >= 0 ? p[idx] : null;
178
+ }, 'namedItem');
179
+ return p;
180
+ };
181
+
182
+ const _plugins = [
183
+ _makePlugin('PDF Viewer', 'Portable Document Format'),
184
+ _makePlugin('Chrome PDF Viewer', 'Portable Document Format'),
185
+ _makePlugin('Chromium PDF Viewer', 'Portable Document Format'),
186
+ _makePlugin('Microsoft Edge PDF Viewer', 'Portable Document Format'),
187
+ _makePlugin('WebKit built-in PDF', 'Portable Document Format'),
188
+ ];
189
+
190
+ _pdfMimes.forEach((m) => { m.enabledPlugin = _plugins[0]; });
191
+
192
+ const _pluginList = Object.assign([..._plugins], {
193
+ item: _mkNative(function item(i) { return _plugins[i] ?? null; }, 'item'),
194
+ namedItem: _mkNative(function namedItem(n) { return _plugins.find(p => p.name === n) ?? null; }, 'namedItem'),
195
+ refresh: _mkNative(function refresh() {}, 'refresh'),
196
+ length: _plugins.length,
197
+ });
198
+
199
+ Object.defineProperty(navigator, 'plugins', {
200
+ get: _mkNative(function () { return _pluginList; }, ''),
201
+ configurable: true,
202
+ });
203
+
204
+ const _mimeList = Object.assign([..._pdfMimes], {
205
+ item: _mkNative(function item(i) { return _pdfMimes[i] ?? null; }, 'item'),
206
+ namedItem: _mkNative(function namedItem(n) { return _pdfMimes.find(m => m.type === n) ?? null; }, 'namedItem'),
207
+ length: _pdfMimes.length,
208
+ });
209
+
210
+ Object.defineProperty(navigator, 'mimeTypes', {
211
+ get: _mkNative(function () { return _mimeList; }, ''),
212
+ configurable: true,
213
+ });
214
+
215
+ try {
216
+ Object.defineProperty(navigator, 'languages', {
217
+ get: _mkNative(function () { return __BROWSER_CLI_LANGS__; }, ''),
218
+ configurable: true,
219
+ });
220
+ } catch (_) {}
221
+
222
+ if (!window.chrome || !window.chrome.runtime || !window.chrome.csi || !window.chrome.loadTimes) {
223
+ const _chrome = {
224
+ app: {
225
+ isInstalled: false,
226
+ getDetails: _mkNative(function getDetails() { return null; }, 'getDetails'),
227
+ getIsInstalled: _mkNative(function getIsInstalled() { return false; }, 'getIsInstalled'),
228
+ installState: _mkNative(function installState() { return 'not_installed'; }, 'installState'),
229
+ },
230
+ runtime: {
231
+ connect: _mkNative(function connect() {}, 'connect'),
232
+ sendMessage: _mkNative(function sendMessage() {}, 'sendMessage'),
233
+ },
234
+ csi: _mkNative(function csi() {
235
+ return {
236
+ onloadT: Date.now(),
237
+ pageT: Date.now() - (performance.timeOrigin ?? performance.timing?.navigationStart ?? 0),
238
+ startE: Date.now() - 1000,
239
+ tran: 15,
240
+ };
241
+ }, 'csi'),
242
+ loadTimes: _mkNative(function loadTimes() {
243
+ return {
244
+ commitLoadTime: Date.now() / 1000 - 1,
245
+ connectionInfo: 'h2',
246
+ finishDocumentLoadTime: Date.now() / 1000,
247
+ finishLoadTime: Date.now() / 1000,
248
+ firstPaintAfterLoadTime: 0,
249
+ firstPaintTime: Date.now() / 1000 - 0.5,
250
+ navigationType: 'Other',
251
+ npnNegotiatedProtocol: 'h2',
252
+ requestTime: Date.now() / 1000 - 1,
253
+ startLoadTime: Date.now() / 1000 - 1,
254
+ wasAlternateProtocolAvailable: false,
255
+ wasFetchedViaSpdy: true,
256
+ wasNpnNegotiated: true,
257
+ };
258
+ }, 'loadTimes'),
259
+ };
260
+ try {
261
+ Object.defineProperty(window, 'chrome', {
262
+ value: _chrome, writable: false, enumerable: true, configurable: false,
263
+ });
264
+ } catch (_) { window.chrome = _chrome; }
265
+ }
266
+
267
+ if (navigator.permissions && navigator.permissions.query) {
268
+ const _origQuery = navigator.permissions.query.bind(navigator.permissions);
269
+ navigator.permissions.query = _mkNative(function query(params) {
270
+ if (params && params.name === 'notifications') {
271
+ return Promise.resolve({ state: Notification.permission === 'denied' ? 'default' : Notification.permission, onchange: null });
272
+ }
273
+ return _origQuery(params);
274
+ }, 'query');
275
+ }
276
+
277
+ if (window.Notification && Notification.permission === 'denied') {
278
+ try {
279
+ Object.defineProperty(Notification, 'permission', {
280
+ get: _mkNative(function () { return 'default'; }, ''),
281
+ configurable: true,
282
+ });
283
+ } catch (_) {}
284
+ }
285
+
286
+ try { document.hasFocus = _mkNative(function hasFocus() { return true; }, 'hasFocus'); } catch (_) {}
287
+ try { Object.defineProperty(document, 'hidden', { get: _mkNative(function () { return false; }, ''), configurable: true }); } catch (_) {}
288
+ try { Object.defineProperty(document, 'visibilityState', { get: _mkNative(function () { return 'visible'; }, ''), configurable: true }); } catch (_) {}
289
+
290
+ if (window.outerWidth === 0) {
291
+ try { Object.defineProperty(window, 'outerWidth', { get: _mkNative(function () { return window.innerWidth; }, ''), configurable: true }); } catch (_) {}
292
+ }
293
+ if (window.outerHeight === 0) {
294
+ try { Object.defineProperty(window, 'outerHeight', { get: _mkNative(function () { return window.innerHeight; }, ''), configurable: true }); } catch (_) {}
295
+ }
296
+
297
+ if (navigator.deviceMemory === undefined) {
298
+ try { Object.defineProperty(navigator, 'deviceMemory', { get: _mkNative(function () { return 8; }, ''), configurable: true }); } catch (_) {}
299
+ }
300
+
301
+ if (!navigator.hardwareConcurrency || navigator.hardwareConcurrency < 2) {
302
+ try { Object.defineProperty(navigator, 'hardwareConcurrency', { get: _mkNative(function () { return 8; }, ''), configurable: true }); } catch (_) {}
303
+ }
304
+
305
+ if (!navigator.connection) {
306
+ try {
307
+ Object.defineProperty(navigator, 'connection', {
308
+ get: _mkNative(function () { return { effectiveType: '4g', downlink: 10, rtt: 100, saveData: false }; }, ''),
309
+ configurable: true,
310
+ });
311
+ } catch (_) {}
312
+ }
313
+
314
+ (function () {
315
+ const _patchWebGL = (Ctx) => {
316
+ if (!Ctx) return;
317
+ const _orig = Ctx.prototype.getParameter;
318
+ Ctx.prototype.getParameter = _mkNative(function getParameter(parameter) {
319
+ const _val = _orig.call(this, parameter);
320
+ if (parameter === 37445) {
321
+ if (_val && (_val.includes('Google') || _val === '')) return 'Intel Inc.';
322
+ return _val;
323
+ }
324
+ if (parameter === 37446) {
325
+ if (_val && (_val.includes('SwiftShader') || _val === '')) return 'Intel Iris OpenGL Engine';
326
+ return _val;
327
+ }
328
+ return _val;
329
+ }, 'getParameter');
330
+ };
331
+ _patchWebGL(window.WebGLRenderingContext);
332
+ _patchWebGL(window.WebGL2RenderingContext);
333
+ })();
334
+ })();
335
+ """
336
+
337
+
338
+ def _normalize_locale(raw: str | None) -> str | None:
339
+ if raw is None:
340
+ return None
341
+ value = raw.strip().split(".", 1)[0]
342
+ if not value:
343
+ return None
344
+ return value.replace("_", "-")
345
+
346
+
347
+ def _languages_for_locale(locale: str | None) -> list[str]:
348
+ normalized = _normalize_locale(locale)
349
+ if not normalized:
350
+ return ["en-US", "en"]
351
+ parts = normalized.split("-")
352
+ base = parts[0]
353
+ langs = [normalized]
354
+ if base != normalized:
355
+ langs.append(base)
356
+ if base.lower() != "en" and "en" not in langs:
357
+ langs.append("en")
358
+ return langs
359
+
360
+
361
+ def _get_playwright_disabled_features() -> list[str]:
362
+ try:
363
+ import playwright as playwright_pkg
364
+ except Exception:
365
+ return []
366
+ try:
367
+ switches_js = (
368
+ Path(playwright_pkg.__file__).parent
369
+ / "driver/package/lib/server/chromium/chromiumSwitches.js"
370
+ )
371
+ text = switches_js.read_text(encoding="utf-8")
372
+ match = re.search(r"const disabledFeatures\s*=.*?\[(.+?)\]\.filter", text, re.DOTALL)
373
+ if not match:
374
+ return []
375
+ return [feature for feature in re.findall(r'"([^"]+)"', match.group(1)) if feature]
376
+ except Exception:
377
+ return []
378
+
379
+
380
+ def _build_disable_features(headless: bool) -> str:
381
+ playright_features = _get_playwright_disabled_features()
382
+ browser_cli_features = (
383
+ CHROME_DISABLED_COMPONENTS if headless else CHROME_DISABLED_COMPONENTS_HEADED
384
+ )
385
+ combined = list(dict.fromkeys(playright_features + list(browser_cli_features)))
386
+ return f"--disable-features={','.join(combined)}"
387
+
388
+
389
+ def build_launch_args(
390
+ *,
391
+ headless: bool,
392
+ viewport_width: int,
393
+ viewport_height: int,
394
+ locale: str | None = None,
395
+ ) -> list[str]:
396
+ normalized_locale = _normalize_locale(locale) or "en-US"
397
+ if headless:
398
+ args = [arg for arg in CHROME_STEALTH_ARGS if not arg.startswith("--lang=")]
399
+ args.append(f"--lang={normalized_locale}")
400
+ if sys.platform == "linux":
401
+ args.extend(CHROME_LINUX_ONLY_ARGS)
402
+ args.append(_build_disable_features(headless=True))
403
+ args.append(f"--window-size={viewport_width},{viewport_height}")
404
+ args.extend(
405
+ [
406
+ "--headless=new",
407
+ "--hide-scrollbars",
408
+ "--mute-audio",
409
+ "--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4",
410
+ ]
411
+ )
412
+ return args
413
+
414
+ args = list(CHROME_STEALTH_ARGS_HEADED)
415
+ if sys.platform == "linux":
416
+ args.append("--disable-dev-shm-usage")
417
+ args.append(_build_disable_features(headless=False))
418
+ args.append(f"--lang={normalized_locale}")
419
+ args.append(f"--window-size={viewport_width},{viewport_height}")
420
+ return args
421
+
422
+
423
+ def build_ignore_default_args() -> list[str]:
424
+ return list(CHROME_IGNORE_DEFAULT_ARGS)
425
+
426
+
427
+ def build_context_options(
428
+ *,
429
+ viewport_width: int,
430
+ viewport_height: int,
431
+ locale: str | None = None,
432
+ ) -> dict[str, Any]:
433
+ options: dict[str, Any] = {
434
+ "accept_downloads": True,
435
+ "permissions": list(DEFAULT_PERMISSIONS),
436
+ "screen": {"width": viewport_width, "height": viewport_height},
437
+ }
438
+ normalized_locale = _normalize_locale(locale)
439
+ if normalized_locale:
440
+ options["locale"] = normalized_locale
441
+ return options
442
+
443
+
444
+ def build_init_script(*, headless: bool, locale: str | None = None) -> str | None:
445
+ if not headless:
446
+ return None
447
+ return _STEALTH_INIT_SCRIPT_TEMPLATE.replace(
448
+ "__BROWSER_CLI_LANGS__",
449
+ json.dumps(_languages_for_locale(locale)),
450
+ )
@@ -0,0 +1 @@
1
+ """CLI package."""
@@ -0,0 +1,26 @@
1
+ """Short recovery hints for common CLI failures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from browser_cli import error_codes
6
+ from browser_cli.errors import BrowserCliError
7
+
8
+
9
+ def next_hint_for_error(exc: BrowserCliError) -> str | None:
10
+ if exc.error_code == error_codes.BROWSER_UNAVAILABLE:
11
+ return "install stable Google Chrome and re-run browser-cli doctor"
12
+ if exc.error_code == error_codes.PROFILE_UNAVAILABLE:
13
+ return "close Browser CLI-owned Chrome windows or inspect browser-cli status"
14
+ if exc.error_code in {
15
+ error_codes.DAEMON_NOT_AVAILABLE,
16
+ error_codes.AUTOMATION_SERVICE_NOT_AVAILABLE,
17
+ }:
18
+ return "run browser-cli reload"
19
+ message = str(exc).lower()
20
+ if exc.error_code == error_codes.INVALID_INPUT and "task" in message:
21
+ return "run browser-cli task validate <task-dir>"
22
+ if "version" in message and "not found" in message:
23
+ return "run browser-cli automation versions <automation-id>"
24
+ if "automation" in message and "not found" in message:
25
+ return "run browser-cli automation list"
26
+ return None