@pencil-agent/nano-pencil 2.0.0 → 2.0.1
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.
- package/README.md +267 -267
- package/dist/build-meta.json +3 -3
- package/dist/core/export-html/AGENT.md +11 -11
- package/dist/core/export-html/template.css +971 -971
- package/dist/core/export-html/template.html +54 -54
- package/dist/core/mcp/mcp-client.d.ts +3 -1
- package/dist/core/mcp/mcp-client.js +6 -6
- package/dist/core/mcp/mcp-config.d.ts +3 -3
- package/dist/core/mcp/mcp-config.js +1 -1
- package/dist/core/mcp/mcp-manager.d.ts +5 -1
- package/dist/core/mcp/mcp-manager.js +1 -1
- package/dist/core/platform/config/resource-loader.d.ts +2 -0
- package/dist/core/platform/config/resource-loader.js +2 -2
- package/dist/core/runtime/agent-session.d.ts +12 -0
- package/dist/core/runtime/agent-session.js +8 -8
- package/dist/core/runtime/sdk.d.ts +8 -0
- package/dist/core/runtime/sdk.js +1 -1
- package/dist/extensions/builtin/AGENT.md +115 -115
- package/dist/extensions/builtin/browser/AGENT.md +17 -17
- package/dist/extensions/builtin/browser/agent-workspace/agent_helpers.py +12 -12
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/amazon/product-search.md +198 -198
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/archive-org/scraping.md +341 -341
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv/scraping.md +311 -311
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv-bulk/scraping.md +333 -333
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/atlas/overview.md +70 -70
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/booking-com/scraping.md +578 -578
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/capterra/scraping.md +440 -440
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/centilebrain/generate-estimates.md +110 -110
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coingecko/scraping.md +325 -325
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coinmarketcap/scraping.md +463 -463
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coursera/scraping.md +360 -360
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/craigslist/scraping.md +390 -390
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/crossref/scraping.md +568 -568
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/dev-to/scraping.md +323 -323
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/duckduckgo/scraping.md +349 -349
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/ebay/scraping.md +435 -435
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/etsy/scraping.md +506 -506
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/eventbrite/scraping.md +363 -363
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/expedia/automation.md +168 -168
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/groups.md +236 -236
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/pages.md +295 -295
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/framer/editor.md +108 -108
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/fred/scraping.md +493 -493
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/g2/scraping.md +580 -580
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/genius/scraping.md +511 -511
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/repo-actions.md +65 -65
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/scraping.md +184 -184
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/glassdoor/scraping.md +543 -543
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gmail/compose.md +122 -122
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/goodreads/scraping.md +461 -461
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gutenberg/scraping.md +383 -383
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/hackernews/scraping.md +243 -243
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/howlongtobeat/scraping.md +473 -473
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/imdb/scraping.md +271 -271
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/itch-io/scraping.md +436 -436
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/job-boards/indeed-glassdoor.md +1021 -1021
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/letterboxd/scraping.md +349 -349
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/linkedin/invitation-manager.md +109 -109
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/loom/folder-enumeration.md +170 -170
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/macrotrends/scraping.md +537 -537
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/article-hydration.md +120 -120
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/scraping.md +414 -414
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/metacritic/scraping.md +477 -477
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/musicbrainz/scraping.md +478 -478
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/nasa/scraping.md +339 -339
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/news-aggregation/multi-source.md +205 -205
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/open-library/scraping.md +472 -472
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openalex/scraping.md +470 -470
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openstreetmap/scraping.md +490 -490
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/package-registries/npm-pypi.md +478 -478
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/polymarket/scraping.md +234 -234
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/producthunt/scraping.md +307 -307
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/pubmed/scraping.md +421 -421
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/quora/scraping.md +364 -364
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rawg/scraping.md +352 -352
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/reddit/scraping.md +124 -124
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rest-countries/scraping.md +233 -233
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/sec-edgar/scraping.md +361 -361
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/README.md +36 -36
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/embedded-apps.md +72 -72
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/knowledge-base.md +109 -109
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/polaris-inputs.md +137 -137
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/soundcloud/scraping.md +362 -362
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/spotify/scraping.md +339 -339
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/stackoverflow/scraping.md +435 -435
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/steam/scraping.md +575 -575
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/substack/scraping.md +338 -338
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/thetechgeeks/pricing.md +52 -52
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tiktok/upload.md +107 -107
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tradingview/scraping.md +309 -309
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trello/boards-and-lists.md +88 -88
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trustpilot/scraping.md +375 -375
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/walmart/scraping.md +444 -444
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wayback-machine/scraping.md +306 -306
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/weather/scraping.md +398 -398
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wellfound/scraping.md +596 -596
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/world-bank/scraping.md +356 -356
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/xiaohongshu/scraping.md +84 -84
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/youtube/scraping.md +418 -418
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/zillow/scraping.md +433 -433
- package/dist/extensions/builtin/browser/browser.md +73 -73
- package/dist/extensions/builtin/browser/install.md +142 -142
- package/dist/extensions/builtin/browser/interaction-skills/connection.md +48 -48
- package/dist/extensions/builtin/browser/interaction-skills/cookies.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/cross-origin-iframes.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/dialogs.md +64 -64
- package/dist/extensions/builtin/browser/interaction-skills/downloads.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/drag-and-drop.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/dropdowns.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/iframes.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/network-requests.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/print-as-pdf.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/profile-sync.md +90 -90
- package/dist/extensions/builtin/browser/interaction-skills/screenshots.md +17 -17
- package/dist/extensions/builtin/browser/interaction-skills/scrolling.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/shadow-dom.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/tabs.md +69 -69
- package/dist/extensions/builtin/browser/interaction-skills/uploads.md +1 -1
- package/dist/extensions/builtin/browser/interaction-skills/viewport.md +3 -3
- package/dist/extensions/builtin/browser/src/browser_harness/AGENT.md +15 -15
- package/dist/extensions/builtin/browser/src/browser_harness/__init__.py +8 -8
- package/dist/extensions/builtin/browser/src/browser_harness/_ipc.py +90 -90
- package/dist/extensions/builtin/browser/src/browser_harness/admin.py +722 -722
- package/dist/extensions/builtin/browser/src/browser_harness/daemon.py +328 -328
- package/dist/extensions/builtin/browser/src/browser_harness/helpers.py +396 -396
- package/dist/extensions/builtin/browser/src/browser_harness/run.py +103 -103
- package/dist/extensions/builtin/discipline/skills/brainstorming/SKILL.md +33 -33
- package/dist/extensions/builtin/discipline/skills/executing-plans/SKILL.md +25 -25
- package/dist/extensions/builtin/discipline/skills/finishing-development-branch/SKILL.md +25 -25
- package/dist/extensions/builtin/discipline/skills/receiving-code-review/SKILL.md +22 -22
- package/dist/extensions/builtin/discipline/skills/requesting-code-review/SKILL.md +31 -31
- package/dist/extensions/builtin/discipline/skills/systematic-debugging/SKILL.md +28 -28
- package/dist/extensions/builtin/discipline/skills/test-driven-development/SKILL.md +32 -32
- package/dist/extensions/builtin/discipline/skills/using-git-worktrees/SKILL.md +25 -25
- package/dist/extensions/builtin/discipline/skills/verification-before-completion/SKILL.md +27 -27
- package/dist/extensions/builtin/discipline/skills/writing-plans/SKILL.md +26 -26
- package/dist/extensions/builtin/goal/README.md +67 -67
- package/dist/extensions/builtin/grub/README.md +112 -112
- package/dist/extensions/builtin/link-world/agent-workspace/README.md +16 -16
- package/dist/extensions/builtin/link-world/internet-search/internet-search.md +65 -65
- package/dist/extensions/builtin/link-world/link-world-agent.md +82 -82
- package/dist/extensions/builtin/link-world/linkworld.md +313 -313
- package/dist/extensions/builtin/link-world/network-routing/network-routing.md +67 -67
- package/dist/extensions/builtin/loop/README.md +92 -92
- package/dist/extensions/builtin/mcp/figma-design.md +68 -68
- package/dist/extensions/builtin/mcp/mcp-management.md +85 -85
- package/dist/extensions/builtin/recap/AGENT.md +15 -15
- package/dist/extensions/builtin/sal/README.md +72 -72
- package/dist/extensions/builtin/security-audit/README.md +289 -289
- package/dist/extensions/builtin/team/AGENT.md +112 -112
- package/dist/extensions/builtin/team/TESTING.md +299 -299
- package/dist/extensions/builtin/token-save/README.md +56 -56
- package/dist/extensions/optional/AGENT.md +10 -10
- package/dist/modes/interactive/interactive-mode.js +36 -36
- package/dist/modes/interactive/theme/dark.json +85 -85
- package/dist/modes/interactive/theme/light.json +84 -84
- package/dist/modes/interactive/theme/theme-schema.json +335 -335
- package/dist/modes/interactive/theme/warm.json +81 -81
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop.js +3 -2
- package/dist/node_modules/@pencil-agent/agent-core/dist/structured-adaptive-agent-loop.js +2 -1
- package/dist/node_modules/@pencil-agent/ai/dist/cli.js +0 -0
- package/docs/cc-agent-design.md +1297 -0
- package/docs/cc-tui-design.md +1333 -0
- package/docs/codex-goal-command-impl.md +1055 -1055
- package/docs/codex-goal-vs-grub.md +500 -500
- package/docs/custom-provider.md +27 -27
- package/docs/extensions.md +27 -27
- package/docs/keybindings.md +27 -27
- package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +250 -250
- package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +122 -122
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -1222
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/256/236/347/216/260/346/212/245/345/221/212.md" +158 -158
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/257/271/346/257/224/345/210/206/346/236/220.md" +128 -128
- package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +320 -320
- package/docs/loop-usage-examples.md +214 -214
- package/docs/models.md +27 -27
- package/docs/nanoPencil-/345/255/246/344/271/240/350/256/241/345/210/222.md +170 -0
- package/docs/packages.md +27 -27
- package/docs/pi-design-philosophy.md +457 -457
- package/docs/planmode.md +1987 -1987
- package/docs/prompt-templates.md +27 -27
- package/docs/providers.md +27 -27
- package/docs/scan-report.md +3820 -0
- package/docs/sdk.md +27 -27
- package/docs/skills.md +27 -27
- package/docs/themes.md +27 -27
- package/docs/tui.md +27 -27
- package/docs//345/257/271/346/240/207Claude-Code.md +1775 -0
- package/docs//351/230/277/351/207/214/345/267/264/345/267/264/350/264/242/346/212/245/345/210/206/346/236/220/344/271/246.md +261 -0
- package/package.json +190 -190
- package/docs/ACP/345/215/217/350/256/256/351/233/206/346/210/220/345/274/200/345/217/221/346/226/207/346/241/243.md +0 -851
- package/docs/SDK-TESTING.md +0 -364
- package/docs/mem-core/346/212/200/346/234/257/346/226/207/346/241/243.md +0 -593
- package/docs/startup-performance-optimization.md +0 -301
- package/docs//350/256/244/347/237/245/345/234/260/345/233/276.md +0 -47
|
@@ -1,328 +1,328 @@
|
|
|
1
|
-
"""CDP WS holder + IPC relay (Unix socket on POSIX, TCP loopback on Windows). One daemon per BU_NAME.
|
|
2
|
-
|
|
3
|
-
[WHO]: Provides Browser Harness daemon process, CDP WebSocket attachment, target/session relay, event buffer, and shutdown handling
|
|
4
|
-
[FROM]: Depends on cdp_use.client.CDPClient, browser_harness._ipc, Chrome DevTools endpoints, and Browser Use cloud env vars
|
|
5
|
-
[TO]: Consumed by browser_harness.admin.ensure_daemon() and browser_harness.helpers.cdp()
|
|
6
|
-
[HERE]: extensions/builtin/browser/src/browser_harness/daemon.py within vendored Browser Harness package
|
|
7
|
-
"""
|
|
8
|
-
import asyncio, json, os, socket, sys, time, urllib.error, urllib.request
|
|
9
|
-
from collections import deque
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
from . import _ipc as ipc
|
|
13
|
-
from cdp_use.client import CDPClient
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _load_env():
|
|
17
|
-
repo_root = Path(__file__).resolve().parents[2]
|
|
18
|
-
workspace = Path(os.environ.get("BH_AGENT_WORKSPACE", repo_root / "agent-workspace")).expanduser()
|
|
19
|
-
for p in (repo_root / ".env", workspace / ".env"):
|
|
20
|
-
if not p.exists():
|
|
21
|
-
continue
|
|
22
|
-
_load_env_file(p)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _load_env_file(p):
|
|
26
|
-
for line in p.read_text().splitlines():
|
|
27
|
-
line = line.strip()
|
|
28
|
-
if not line or line.startswith("#") or "=" not in line:
|
|
29
|
-
continue
|
|
30
|
-
k, v = line.split("=", 1)
|
|
31
|
-
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
_load_env()
|
|
35
|
-
|
|
36
|
-
NAME = os.environ.get("BU_NAME", "default")
|
|
37
|
-
SOCK = ipc.sock_addr(NAME)
|
|
38
|
-
LOG = str(ipc.log_path(NAME))
|
|
39
|
-
PID = str(ipc.pid_path(NAME))
|
|
40
|
-
BUF = 500
|
|
41
|
-
PROFILES = [
|
|
42
|
-
Path.home() / "Library/Application Support/Google/Chrome",
|
|
43
|
-
Path.home() / "Library/Application Support/Comet",
|
|
44
|
-
Path.home() / "Library/Application Support/Arc/User Data",
|
|
45
|
-
Path.home() / "Library/Application Support/Microsoft Edge",
|
|
46
|
-
Path.home() / "Library/Application Support/Microsoft Edge Beta",
|
|
47
|
-
Path.home() / "Library/Application Support/Microsoft Edge Dev",
|
|
48
|
-
Path.home() / "Library/Application Support/Microsoft Edge Canary",
|
|
49
|
-
Path.home() / "Library/Application Support/BraveSoftware/Brave-Browser",
|
|
50
|
-
Path.home() / ".config/google-chrome",
|
|
51
|
-
Path.home() / ".config/chromium",
|
|
52
|
-
Path.home() / ".config/chromium-browser",
|
|
53
|
-
Path.home() / ".config/microsoft-edge",
|
|
54
|
-
Path.home() / ".config/microsoft-edge-beta",
|
|
55
|
-
Path.home() / ".config/microsoft-edge-dev",
|
|
56
|
-
Path.home() / ".var/app/org.chromium.Chromium/config/chromium",
|
|
57
|
-
Path.home() / ".var/app/com.google.Chrome/config/google-chrome",
|
|
58
|
-
Path.home() / ".var/app/com.brave.Browser/config/BraveSoftware/Brave-Browser",
|
|
59
|
-
Path.home() / ".var/app/com.microsoft.Edge/config/microsoft-edge",
|
|
60
|
-
Path.home() / "AppData/Local/Google/Chrome/User Data",
|
|
61
|
-
Path.home() / "AppData/Local/Chromium/User Data",
|
|
62
|
-
Path.home() / "AppData/Local/Microsoft/Edge/User Data",
|
|
63
|
-
Path.home() / "AppData/Local/Microsoft/Edge Beta/User Data",
|
|
64
|
-
Path.home() / "AppData/Local/Microsoft/Edge Dev/User Data",
|
|
65
|
-
Path.home() / "AppData/Local/Microsoft/Edge SxS/User Data",
|
|
66
|
-
]
|
|
67
|
-
INTERNAL = ("chrome://", "chrome-untrusted://", "devtools://", "chrome-extension://", "about:")
|
|
68
|
-
BU_API = "https://api.browser-use.com/api/v3"
|
|
69
|
-
REMOTE_ID = os.environ.get("BU_BROWSER_ID")
|
|
70
|
-
API_KEY = os.environ.get("BROWSER_USE_API_KEY")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def log(msg):
|
|
74
|
-
open(LOG, "a").write(f"{msg}\n")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
async def _silent(coro):
|
|
78
|
-
try:
|
|
79
|
-
await coro
|
|
80
|
-
except Exception:
|
|
81
|
-
pass
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def get_ws_url():
|
|
85
|
-
if url := os.environ.get("BU_CDP_WS"):
|
|
86
|
-
return url
|
|
87
|
-
if url := os.environ.get("BU_CDP_URL"):
|
|
88
|
-
# HTTP DevTools endpoint (e.g. http://127.0.0.1:9333) — resolve to ws via /json/version.
|
|
89
|
-
# Use this for a dedicated automation Chrome on a non-default profile, which avoids the
|
|
90
|
-
# M144 "Allow remote debugging" dialog and the M136 default-profile lockdown.
|
|
91
|
-
deadline = time.time() + 30
|
|
92
|
-
last_err = None
|
|
93
|
-
while time.time() < deadline:
|
|
94
|
-
try:
|
|
95
|
-
return json.loads(urllib.request.urlopen(f"{url}/json/version", timeout=5).read())["webSocketDebuggerUrl"]
|
|
96
|
-
except Exception as e:
|
|
97
|
-
last_err = e
|
|
98
|
-
time.sleep(1)
|
|
99
|
-
raise RuntimeError(f"BU_CDP_URL={url} unreachable after 30s: {last_err} -- is the dedicated automation Chrome running?")
|
|
100
|
-
for base in PROFILES:
|
|
101
|
-
try:
|
|
102
|
-
active = (base / "DevToolsActivePort").read_text().splitlines()
|
|
103
|
-
except (FileNotFoundError, NotADirectoryError):
|
|
104
|
-
continue
|
|
105
|
-
port = active[0].strip() if active else ""
|
|
106
|
-
ws_path = active[1].strip() if len(active) > 1 else ""
|
|
107
|
-
if not port:
|
|
108
|
-
continue
|
|
109
|
-
# Resolve the live WS URL via /json/version instead of trusting the path stored
|
|
110
|
-
# alongside the port in DevToolsActivePort: if Chrome was previously launched
|
|
111
|
-
# with a different --user-data-dir on the same port, that file is left behind
|
|
112
|
-
# with a stale browser UUID and the WS upgrade returns 404.
|
|
113
|
-
deadline = time.time() + 30
|
|
114
|
-
while time.time() < deadline:
|
|
115
|
-
try:
|
|
116
|
-
return json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=1).read())["webSocketDebuggerUrl"]
|
|
117
|
-
except urllib.error.HTTPError as e:
|
|
118
|
-
# Chrome 147+ disables /json/* HTTP discovery on the default user-data-dir;
|
|
119
|
-
# the ws path Chrome wrote to DevToolsActivePort still works.
|
|
120
|
-
if e.code == 404 and ws_path:
|
|
121
|
-
return f"ws://127.0.0.1:{port}{ws_path}"
|
|
122
|
-
time.sleep(1)
|
|
123
|
-
except (OSError, KeyError, ValueError):
|
|
124
|
-
time.sleep(1)
|
|
125
|
-
raise RuntimeError(
|
|
126
|
-
f"Chrome's remote-debugging page is open, but DevTools is not live yet on 127.0.0.1:{port} — if Chrome opened a profile picker, choose your normal profile first, then tick the checkbox and click Allow if shown"
|
|
127
|
-
)
|
|
128
|
-
for probe_port in (9222, 9223):
|
|
129
|
-
try:
|
|
130
|
-
with urllib.request.urlopen(f"http://127.0.0.1:{probe_port}/json/version", timeout=1) as r:
|
|
131
|
-
return json.loads(r.read())["webSocketDebuggerUrl"]
|
|
132
|
-
except (OSError, KeyError, ValueError):
|
|
133
|
-
continue
|
|
134
|
-
raise RuntimeError(f"DevToolsActivePort not found in {[str(p) for p in PROFILES]} — enable chrome://inspect/#remote-debugging, or set BU_CDP_WS for a remote browser")
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def stop_remote():
|
|
138
|
-
if not REMOTE_ID or not API_KEY: return
|
|
139
|
-
try:
|
|
140
|
-
req = urllib.request.Request(
|
|
141
|
-
f"{BU_API}/browsers/{REMOTE_ID}",
|
|
142
|
-
data=json.dumps({"action": "stop"}).encode(),
|
|
143
|
-
method="PATCH",
|
|
144
|
-
headers={"X-Browser-Use-API-Key": API_KEY, "Content-Type": "application/json"},
|
|
145
|
-
)
|
|
146
|
-
urllib.request.urlopen(req, timeout=15).read()
|
|
147
|
-
log(f"stopped remote browser {REMOTE_ID}")
|
|
148
|
-
except Exception as e:
|
|
149
|
-
log(f"stop_remote failed ({REMOTE_ID}): {e}")
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def is_real_page(t):
|
|
153
|
-
return t["type"] == "page" and not t.get("url", "").startswith(INTERNAL)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
class Daemon:
|
|
157
|
-
def __init__(self):
|
|
158
|
-
self.cdp = None
|
|
159
|
-
self.session = None
|
|
160
|
-
self.target_id = None
|
|
161
|
-
self.events = deque(maxlen=BUF)
|
|
162
|
-
self.dialog = None
|
|
163
|
-
self.stop = None # asyncio.Event, set inside start()
|
|
164
|
-
|
|
165
|
-
async def attach_first_page(self):
|
|
166
|
-
"""Attach to a real page (or any page). Sets self.session. Returns attached target or None."""
|
|
167
|
-
targets = (await self.cdp.send_raw("Target.getTargets"))["targetInfos"]
|
|
168
|
-
pages = [t for t in targets if is_real_page(t)]
|
|
169
|
-
if not pages:
|
|
170
|
-
# No real pages — create one instead of attaching to omnibox popup
|
|
171
|
-
tid = (await self.cdp.send_raw("Target.createTarget", {"url": "about:blank"}))["targetId"]
|
|
172
|
-
log(f"no real pages found, created about:blank ({tid})")
|
|
173
|
-
pages = [{"targetId": tid, "url": "about:blank", "type": "page"}]
|
|
174
|
-
self.session = (await self.cdp.send_raw(
|
|
175
|
-
"Target.attachToTarget", {"targetId": pages[0]["targetId"], "flatten": True}
|
|
176
|
-
))["sessionId"]
|
|
177
|
-
self.target_id = pages[0]["targetId"]
|
|
178
|
-
log(f"attached {pages[0]['targetId']} ({pages[0].get('url','')[:80]}) session={self.session}")
|
|
179
|
-
for d in ("Page", "DOM", "Runtime", "Network"):
|
|
180
|
-
try:
|
|
181
|
-
await asyncio.wait_for(
|
|
182
|
-
self.cdp.send_raw(f"{d}.enable", session_id=self.session),
|
|
183
|
-
timeout=5
|
|
184
|
-
)
|
|
185
|
-
except Exception as e:
|
|
186
|
-
log(f"enable {d}: {e}")
|
|
187
|
-
return pages[0]
|
|
188
|
-
|
|
189
|
-
async def start(self):
|
|
190
|
-
self.stop = asyncio.Event()
|
|
191
|
-
url = get_ws_url()
|
|
192
|
-
log(f"connecting to {url}")
|
|
193
|
-
self.cdp = CDPClient(url)
|
|
194
|
-
try:
|
|
195
|
-
await self.cdp.start()
|
|
196
|
-
except Exception as e:
|
|
197
|
-
if os.environ.get("BU_CDP_WS"):
|
|
198
|
-
raise RuntimeError(
|
|
199
|
-
f"CDP WS handshake failed: {e} -- remote browser WebSocket connection failed. "
|
|
200
|
-
"This can happen when network policy blocks the connection, the WS URL is wrong or expired, or the remote endpoint is down. "
|
|
201
|
-
"If you use Browser Use cloud, verify BROWSER_USE_API_KEY and get a fresh URL via start_remote_daemon()."
|
|
202
|
-
)
|
|
203
|
-
raise RuntimeError(f"CDP WS handshake failed: {e} -- click Allow in Chrome if prompted, then retry")
|
|
204
|
-
await self.attach_first_page()
|
|
205
|
-
orig = self.cdp._event_registry.handle_event
|
|
206
|
-
mark_js = "if(!document.title.startsWith('\U0001F7E2'))document.title='\U0001F7E2 '+document.title"
|
|
207
|
-
async def tap(method, params, session_id=None):
|
|
208
|
-
self.events.append({"method": method, "params": params, "session_id": session_id})
|
|
209
|
-
if method == "Page.javascriptDialogOpening":
|
|
210
|
-
self.dialog = params
|
|
211
|
-
elif method == "Page.javascriptDialogClosed":
|
|
212
|
-
self.dialog = None
|
|
213
|
-
elif method in ("Page.loadEventFired", "Page.domContentEventFired"):
|
|
214
|
-
asyncio.create_task(_silent(asyncio.wait_for(self.cdp.send_raw("Runtime.evaluate", {"expression": mark_js}, session_id=self.session), timeout=2)))
|
|
215
|
-
return await orig(method, params, session_id)
|
|
216
|
-
self.cdp._event_registry.handle_event = tap
|
|
217
|
-
|
|
218
|
-
async def handle(self, req):
|
|
219
|
-
meta = req.get("meta")
|
|
220
|
-
if meta == "drain_events":
|
|
221
|
-
out = list(self.events); self.events.clear()
|
|
222
|
-
return {"events": out}
|
|
223
|
-
if meta == "session": return {"session_id": self.session}
|
|
224
|
-
if meta == "connection_status":
|
|
225
|
-
if not self.target_id:
|
|
226
|
-
return {"error": "not_attached"}
|
|
227
|
-
try:
|
|
228
|
-
info = (await self.cdp.send_raw("Target.getTargetInfo", {"targetId": self.target_id}))["targetInfo"]
|
|
229
|
-
except Exception:
|
|
230
|
-
return {"error": "cdp_disconnected"}
|
|
231
|
-
page = None
|
|
232
|
-
if is_real_page(info):
|
|
233
|
-
page = {
|
|
234
|
-
"targetId": info.get("targetId"),
|
|
235
|
-
"title": info.get("title") or "(untitled)",
|
|
236
|
-
"url": info.get("url") or "",
|
|
237
|
-
}
|
|
238
|
-
return {"target_id": self.target_id, "session_id": self.session, "page": page}
|
|
239
|
-
if meta == "set_session":
|
|
240
|
-
self.session = req.get("session_id")
|
|
241
|
-
self.target_id = req.get("target_id") or self.target_id
|
|
242
|
-
try:
|
|
243
|
-
await asyncio.wait_for(self.cdp.send_raw("Page.enable", session_id=self.session), timeout=3)
|
|
244
|
-
await asyncio.wait_for(self.cdp.send_raw("Runtime.evaluate", {"expression": "if(!document.title.startsWith('\U0001F7E2'))document.title='\U0001F7E2 '+document.title"}, session_id=self.session), timeout=2)
|
|
245
|
-
except Exception: pass
|
|
246
|
-
return {"session_id": self.session}
|
|
247
|
-
if meta == "pending_dialog": return {"dialog": self.dialog}
|
|
248
|
-
if meta == "shutdown": self.stop.set(); return {"ok": True}
|
|
249
|
-
|
|
250
|
-
method = req["method"]
|
|
251
|
-
params = req.get("params") or {}
|
|
252
|
-
# Browser-level Target.* calls must not use a session (stale or otherwise).
|
|
253
|
-
# For everything else, explicit session in req wins; else default.
|
|
254
|
-
sid = None if method.startswith("Target.") else (req.get("session_id") or self.session)
|
|
255
|
-
try:
|
|
256
|
-
return {"result": await self.cdp.send_raw(method, params, session_id=sid)}
|
|
257
|
-
except Exception as e:
|
|
258
|
-
msg = str(e)
|
|
259
|
-
if "Session with given id not found" in msg and sid == self.session and sid:
|
|
260
|
-
log(f"stale session {sid}, re-attaching")
|
|
261
|
-
if await self.attach_first_page():
|
|
262
|
-
return {"result": await self.cdp.send_raw(method, params, session_id=self.session)}
|
|
263
|
-
return {"error": msg}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
async def serve(d):
|
|
267
|
-
async def handler(reader, writer):
|
|
268
|
-
try:
|
|
269
|
-
line = await reader.readline()
|
|
270
|
-
if not line: return
|
|
271
|
-
resp = await d.handle(json.loads(line))
|
|
272
|
-
writer.write((json.dumps(resp, default=str) + "\n").encode())
|
|
273
|
-
await writer.drain()
|
|
274
|
-
except Exception as e:
|
|
275
|
-
log(f"conn: {e}")
|
|
276
|
-
try:
|
|
277
|
-
writer.write((json.dumps({"error": str(e)}) + "\n").encode())
|
|
278
|
-
await writer.drain()
|
|
279
|
-
except Exception:
|
|
280
|
-
pass
|
|
281
|
-
finally:
|
|
282
|
-
writer.close()
|
|
283
|
-
|
|
284
|
-
serve_task = asyncio.create_task(ipc.serve(NAME, handler))
|
|
285
|
-
stop_task = asyncio.create_task(d.stop.wait())
|
|
286
|
-
await asyncio.sleep(0.05) # let serve() bind so sock_addr() resolves to the live endpoint
|
|
287
|
-
log(f"listening on {ipc.sock_addr(NAME)} (name={NAME}, remote={REMOTE_ID or 'local'})")
|
|
288
|
-
try:
|
|
289
|
-
await asyncio.wait({serve_task, stop_task}, return_when=asyncio.FIRST_COMPLETED)
|
|
290
|
-
if serve_task.done(): await serve_task # surfaces a serve crash
|
|
291
|
-
finally:
|
|
292
|
-
for t in (serve_task, stop_task):
|
|
293
|
-
t.cancel()
|
|
294
|
-
try: await t
|
|
295
|
-
except (asyncio.CancelledError, Exception): pass
|
|
296
|
-
ipc.cleanup_endpoint(NAME)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
async def main():
|
|
300
|
-
d = Daemon()
|
|
301
|
-
await d.start()
|
|
302
|
-
await serve(d)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def already_running():
|
|
306
|
-
try:
|
|
307
|
-
c = ipc.connect(NAME, timeout=1.0); c.close(); return True
|
|
308
|
-
except (FileNotFoundError, ConnectionRefusedError, TimeoutError, socket.timeout, OSError):
|
|
309
|
-
return False
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if __name__ == "__main__":
|
|
313
|
-
if already_running():
|
|
314
|
-
print(f"daemon already running on {SOCK}", file=sys.stderr)
|
|
315
|
-
sys.exit(0)
|
|
316
|
-
open(LOG, "w").close()
|
|
317
|
-
open(PID, "w").write(str(os.getpid()))
|
|
318
|
-
try:
|
|
319
|
-
asyncio.run(main())
|
|
320
|
-
except KeyboardInterrupt:
|
|
321
|
-
pass
|
|
322
|
-
except Exception as e:
|
|
323
|
-
log(f"fatal: {e}")
|
|
324
|
-
sys.exit(1)
|
|
325
|
-
finally:
|
|
326
|
-
stop_remote()
|
|
327
|
-
try: os.unlink(PID)
|
|
328
|
-
except FileNotFoundError: pass
|
|
1
|
+
"""CDP WS holder + IPC relay (Unix socket on POSIX, TCP loopback on Windows). One daemon per BU_NAME.
|
|
2
|
+
|
|
3
|
+
[WHO]: Provides Browser Harness daemon process, CDP WebSocket attachment, target/session relay, event buffer, and shutdown handling
|
|
4
|
+
[FROM]: Depends on cdp_use.client.CDPClient, browser_harness._ipc, Chrome DevTools endpoints, and Browser Use cloud env vars
|
|
5
|
+
[TO]: Consumed by browser_harness.admin.ensure_daemon() and browser_harness.helpers.cdp()
|
|
6
|
+
[HERE]: extensions/builtin/browser/src/browser_harness/daemon.py within vendored Browser Harness package
|
|
7
|
+
"""
|
|
8
|
+
import asyncio, json, os, socket, sys, time, urllib.error, urllib.request
|
|
9
|
+
from collections import deque
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from . import _ipc as ipc
|
|
13
|
+
from cdp_use.client import CDPClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_env():
|
|
17
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
18
|
+
workspace = Path(os.environ.get("BH_AGENT_WORKSPACE", repo_root / "agent-workspace")).expanduser()
|
|
19
|
+
for p in (repo_root / ".env", workspace / ".env"):
|
|
20
|
+
if not p.exists():
|
|
21
|
+
continue
|
|
22
|
+
_load_env_file(p)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_env_file(p):
|
|
26
|
+
for line in p.read_text().splitlines():
|
|
27
|
+
line = line.strip()
|
|
28
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
29
|
+
continue
|
|
30
|
+
k, v = line.split("=", 1)
|
|
31
|
+
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_load_env()
|
|
35
|
+
|
|
36
|
+
NAME = os.environ.get("BU_NAME", "default")
|
|
37
|
+
SOCK = ipc.sock_addr(NAME)
|
|
38
|
+
LOG = str(ipc.log_path(NAME))
|
|
39
|
+
PID = str(ipc.pid_path(NAME))
|
|
40
|
+
BUF = 500
|
|
41
|
+
PROFILES = [
|
|
42
|
+
Path.home() / "Library/Application Support/Google/Chrome",
|
|
43
|
+
Path.home() / "Library/Application Support/Comet",
|
|
44
|
+
Path.home() / "Library/Application Support/Arc/User Data",
|
|
45
|
+
Path.home() / "Library/Application Support/Microsoft Edge",
|
|
46
|
+
Path.home() / "Library/Application Support/Microsoft Edge Beta",
|
|
47
|
+
Path.home() / "Library/Application Support/Microsoft Edge Dev",
|
|
48
|
+
Path.home() / "Library/Application Support/Microsoft Edge Canary",
|
|
49
|
+
Path.home() / "Library/Application Support/BraveSoftware/Brave-Browser",
|
|
50
|
+
Path.home() / ".config/google-chrome",
|
|
51
|
+
Path.home() / ".config/chromium",
|
|
52
|
+
Path.home() / ".config/chromium-browser",
|
|
53
|
+
Path.home() / ".config/microsoft-edge",
|
|
54
|
+
Path.home() / ".config/microsoft-edge-beta",
|
|
55
|
+
Path.home() / ".config/microsoft-edge-dev",
|
|
56
|
+
Path.home() / ".var/app/org.chromium.Chromium/config/chromium",
|
|
57
|
+
Path.home() / ".var/app/com.google.Chrome/config/google-chrome",
|
|
58
|
+
Path.home() / ".var/app/com.brave.Browser/config/BraveSoftware/Brave-Browser",
|
|
59
|
+
Path.home() / ".var/app/com.microsoft.Edge/config/microsoft-edge",
|
|
60
|
+
Path.home() / "AppData/Local/Google/Chrome/User Data",
|
|
61
|
+
Path.home() / "AppData/Local/Chromium/User Data",
|
|
62
|
+
Path.home() / "AppData/Local/Microsoft/Edge/User Data",
|
|
63
|
+
Path.home() / "AppData/Local/Microsoft/Edge Beta/User Data",
|
|
64
|
+
Path.home() / "AppData/Local/Microsoft/Edge Dev/User Data",
|
|
65
|
+
Path.home() / "AppData/Local/Microsoft/Edge SxS/User Data",
|
|
66
|
+
]
|
|
67
|
+
INTERNAL = ("chrome://", "chrome-untrusted://", "devtools://", "chrome-extension://", "about:")
|
|
68
|
+
BU_API = "https://api.browser-use.com/api/v3"
|
|
69
|
+
REMOTE_ID = os.environ.get("BU_BROWSER_ID")
|
|
70
|
+
API_KEY = os.environ.get("BROWSER_USE_API_KEY")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def log(msg):
|
|
74
|
+
open(LOG, "a").write(f"{msg}\n")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def _silent(coro):
|
|
78
|
+
try:
|
|
79
|
+
await coro
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_ws_url():
|
|
85
|
+
if url := os.environ.get("BU_CDP_WS"):
|
|
86
|
+
return url
|
|
87
|
+
if url := os.environ.get("BU_CDP_URL"):
|
|
88
|
+
# HTTP DevTools endpoint (e.g. http://127.0.0.1:9333) — resolve to ws via /json/version.
|
|
89
|
+
# Use this for a dedicated automation Chrome on a non-default profile, which avoids the
|
|
90
|
+
# M144 "Allow remote debugging" dialog and the M136 default-profile lockdown.
|
|
91
|
+
deadline = time.time() + 30
|
|
92
|
+
last_err = None
|
|
93
|
+
while time.time() < deadline:
|
|
94
|
+
try:
|
|
95
|
+
return json.loads(urllib.request.urlopen(f"{url}/json/version", timeout=5).read())["webSocketDebuggerUrl"]
|
|
96
|
+
except Exception as e:
|
|
97
|
+
last_err = e
|
|
98
|
+
time.sleep(1)
|
|
99
|
+
raise RuntimeError(f"BU_CDP_URL={url} unreachable after 30s: {last_err} -- is the dedicated automation Chrome running?")
|
|
100
|
+
for base in PROFILES:
|
|
101
|
+
try:
|
|
102
|
+
active = (base / "DevToolsActivePort").read_text().splitlines()
|
|
103
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
104
|
+
continue
|
|
105
|
+
port = active[0].strip() if active else ""
|
|
106
|
+
ws_path = active[1].strip() if len(active) > 1 else ""
|
|
107
|
+
if not port:
|
|
108
|
+
continue
|
|
109
|
+
# Resolve the live WS URL via /json/version instead of trusting the path stored
|
|
110
|
+
# alongside the port in DevToolsActivePort: if Chrome was previously launched
|
|
111
|
+
# with a different --user-data-dir on the same port, that file is left behind
|
|
112
|
+
# with a stale browser UUID and the WS upgrade returns 404.
|
|
113
|
+
deadline = time.time() + 30
|
|
114
|
+
while time.time() < deadline:
|
|
115
|
+
try:
|
|
116
|
+
return json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=1).read())["webSocketDebuggerUrl"]
|
|
117
|
+
except urllib.error.HTTPError as e:
|
|
118
|
+
# Chrome 147+ disables /json/* HTTP discovery on the default user-data-dir;
|
|
119
|
+
# the ws path Chrome wrote to DevToolsActivePort still works.
|
|
120
|
+
if e.code == 404 and ws_path:
|
|
121
|
+
return f"ws://127.0.0.1:{port}{ws_path}"
|
|
122
|
+
time.sleep(1)
|
|
123
|
+
except (OSError, KeyError, ValueError):
|
|
124
|
+
time.sleep(1)
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
f"Chrome's remote-debugging page is open, but DevTools is not live yet on 127.0.0.1:{port} — if Chrome opened a profile picker, choose your normal profile first, then tick the checkbox and click Allow if shown"
|
|
127
|
+
)
|
|
128
|
+
for probe_port in (9222, 9223):
|
|
129
|
+
try:
|
|
130
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{probe_port}/json/version", timeout=1) as r:
|
|
131
|
+
return json.loads(r.read())["webSocketDebuggerUrl"]
|
|
132
|
+
except (OSError, KeyError, ValueError):
|
|
133
|
+
continue
|
|
134
|
+
raise RuntimeError(f"DevToolsActivePort not found in {[str(p) for p in PROFILES]} — enable chrome://inspect/#remote-debugging, or set BU_CDP_WS for a remote browser")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def stop_remote():
|
|
138
|
+
if not REMOTE_ID or not API_KEY: return
|
|
139
|
+
try:
|
|
140
|
+
req = urllib.request.Request(
|
|
141
|
+
f"{BU_API}/browsers/{REMOTE_ID}",
|
|
142
|
+
data=json.dumps({"action": "stop"}).encode(),
|
|
143
|
+
method="PATCH",
|
|
144
|
+
headers={"X-Browser-Use-API-Key": API_KEY, "Content-Type": "application/json"},
|
|
145
|
+
)
|
|
146
|
+
urllib.request.urlopen(req, timeout=15).read()
|
|
147
|
+
log(f"stopped remote browser {REMOTE_ID}")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
log(f"stop_remote failed ({REMOTE_ID}): {e}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def is_real_page(t):
|
|
153
|
+
return t["type"] == "page" and not t.get("url", "").startswith(INTERNAL)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Daemon:
|
|
157
|
+
def __init__(self):
|
|
158
|
+
self.cdp = None
|
|
159
|
+
self.session = None
|
|
160
|
+
self.target_id = None
|
|
161
|
+
self.events = deque(maxlen=BUF)
|
|
162
|
+
self.dialog = None
|
|
163
|
+
self.stop = None # asyncio.Event, set inside start()
|
|
164
|
+
|
|
165
|
+
async def attach_first_page(self):
|
|
166
|
+
"""Attach to a real page (or any page). Sets self.session. Returns attached target or None."""
|
|
167
|
+
targets = (await self.cdp.send_raw("Target.getTargets"))["targetInfos"]
|
|
168
|
+
pages = [t for t in targets if is_real_page(t)]
|
|
169
|
+
if not pages:
|
|
170
|
+
# No real pages — create one instead of attaching to omnibox popup
|
|
171
|
+
tid = (await self.cdp.send_raw("Target.createTarget", {"url": "about:blank"}))["targetId"]
|
|
172
|
+
log(f"no real pages found, created about:blank ({tid})")
|
|
173
|
+
pages = [{"targetId": tid, "url": "about:blank", "type": "page"}]
|
|
174
|
+
self.session = (await self.cdp.send_raw(
|
|
175
|
+
"Target.attachToTarget", {"targetId": pages[0]["targetId"], "flatten": True}
|
|
176
|
+
))["sessionId"]
|
|
177
|
+
self.target_id = pages[0]["targetId"]
|
|
178
|
+
log(f"attached {pages[0]['targetId']} ({pages[0].get('url','')[:80]}) session={self.session}")
|
|
179
|
+
for d in ("Page", "DOM", "Runtime", "Network"):
|
|
180
|
+
try:
|
|
181
|
+
await asyncio.wait_for(
|
|
182
|
+
self.cdp.send_raw(f"{d}.enable", session_id=self.session),
|
|
183
|
+
timeout=5
|
|
184
|
+
)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
log(f"enable {d}: {e}")
|
|
187
|
+
return pages[0]
|
|
188
|
+
|
|
189
|
+
async def start(self):
|
|
190
|
+
self.stop = asyncio.Event()
|
|
191
|
+
url = get_ws_url()
|
|
192
|
+
log(f"connecting to {url}")
|
|
193
|
+
self.cdp = CDPClient(url)
|
|
194
|
+
try:
|
|
195
|
+
await self.cdp.start()
|
|
196
|
+
except Exception as e:
|
|
197
|
+
if os.environ.get("BU_CDP_WS"):
|
|
198
|
+
raise RuntimeError(
|
|
199
|
+
f"CDP WS handshake failed: {e} -- remote browser WebSocket connection failed. "
|
|
200
|
+
"This can happen when network policy blocks the connection, the WS URL is wrong or expired, or the remote endpoint is down. "
|
|
201
|
+
"If you use Browser Use cloud, verify BROWSER_USE_API_KEY and get a fresh URL via start_remote_daemon()."
|
|
202
|
+
)
|
|
203
|
+
raise RuntimeError(f"CDP WS handshake failed: {e} -- click Allow in Chrome if prompted, then retry")
|
|
204
|
+
await self.attach_first_page()
|
|
205
|
+
orig = self.cdp._event_registry.handle_event
|
|
206
|
+
mark_js = "if(!document.title.startsWith('\U0001F7E2'))document.title='\U0001F7E2 '+document.title"
|
|
207
|
+
async def tap(method, params, session_id=None):
|
|
208
|
+
self.events.append({"method": method, "params": params, "session_id": session_id})
|
|
209
|
+
if method == "Page.javascriptDialogOpening":
|
|
210
|
+
self.dialog = params
|
|
211
|
+
elif method == "Page.javascriptDialogClosed":
|
|
212
|
+
self.dialog = None
|
|
213
|
+
elif method in ("Page.loadEventFired", "Page.domContentEventFired"):
|
|
214
|
+
asyncio.create_task(_silent(asyncio.wait_for(self.cdp.send_raw("Runtime.evaluate", {"expression": mark_js}, session_id=self.session), timeout=2)))
|
|
215
|
+
return await orig(method, params, session_id)
|
|
216
|
+
self.cdp._event_registry.handle_event = tap
|
|
217
|
+
|
|
218
|
+
async def handle(self, req):
|
|
219
|
+
meta = req.get("meta")
|
|
220
|
+
if meta == "drain_events":
|
|
221
|
+
out = list(self.events); self.events.clear()
|
|
222
|
+
return {"events": out}
|
|
223
|
+
if meta == "session": return {"session_id": self.session}
|
|
224
|
+
if meta == "connection_status":
|
|
225
|
+
if not self.target_id:
|
|
226
|
+
return {"error": "not_attached"}
|
|
227
|
+
try:
|
|
228
|
+
info = (await self.cdp.send_raw("Target.getTargetInfo", {"targetId": self.target_id}))["targetInfo"]
|
|
229
|
+
except Exception:
|
|
230
|
+
return {"error": "cdp_disconnected"}
|
|
231
|
+
page = None
|
|
232
|
+
if is_real_page(info):
|
|
233
|
+
page = {
|
|
234
|
+
"targetId": info.get("targetId"),
|
|
235
|
+
"title": info.get("title") or "(untitled)",
|
|
236
|
+
"url": info.get("url") or "",
|
|
237
|
+
}
|
|
238
|
+
return {"target_id": self.target_id, "session_id": self.session, "page": page}
|
|
239
|
+
if meta == "set_session":
|
|
240
|
+
self.session = req.get("session_id")
|
|
241
|
+
self.target_id = req.get("target_id") or self.target_id
|
|
242
|
+
try:
|
|
243
|
+
await asyncio.wait_for(self.cdp.send_raw("Page.enable", session_id=self.session), timeout=3)
|
|
244
|
+
await asyncio.wait_for(self.cdp.send_raw("Runtime.evaluate", {"expression": "if(!document.title.startsWith('\U0001F7E2'))document.title='\U0001F7E2 '+document.title"}, session_id=self.session), timeout=2)
|
|
245
|
+
except Exception: pass
|
|
246
|
+
return {"session_id": self.session}
|
|
247
|
+
if meta == "pending_dialog": return {"dialog": self.dialog}
|
|
248
|
+
if meta == "shutdown": self.stop.set(); return {"ok": True}
|
|
249
|
+
|
|
250
|
+
method = req["method"]
|
|
251
|
+
params = req.get("params") or {}
|
|
252
|
+
# Browser-level Target.* calls must not use a session (stale or otherwise).
|
|
253
|
+
# For everything else, explicit session in req wins; else default.
|
|
254
|
+
sid = None if method.startswith("Target.") else (req.get("session_id") or self.session)
|
|
255
|
+
try:
|
|
256
|
+
return {"result": await self.cdp.send_raw(method, params, session_id=sid)}
|
|
257
|
+
except Exception as e:
|
|
258
|
+
msg = str(e)
|
|
259
|
+
if "Session with given id not found" in msg and sid == self.session and sid:
|
|
260
|
+
log(f"stale session {sid}, re-attaching")
|
|
261
|
+
if await self.attach_first_page():
|
|
262
|
+
return {"result": await self.cdp.send_raw(method, params, session_id=self.session)}
|
|
263
|
+
return {"error": msg}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def serve(d):
|
|
267
|
+
async def handler(reader, writer):
|
|
268
|
+
try:
|
|
269
|
+
line = await reader.readline()
|
|
270
|
+
if not line: return
|
|
271
|
+
resp = await d.handle(json.loads(line))
|
|
272
|
+
writer.write((json.dumps(resp, default=str) + "\n").encode())
|
|
273
|
+
await writer.drain()
|
|
274
|
+
except Exception as e:
|
|
275
|
+
log(f"conn: {e}")
|
|
276
|
+
try:
|
|
277
|
+
writer.write((json.dumps({"error": str(e)}) + "\n").encode())
|
|
278
|
+
await writer.drain()
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
finally:
|
|
282
|
+
writer.close()
|
|
283
|
+
|
|
284
|
+
serve_task = asyncio.create_task(ipc.serve(NAME, handler))
|
|
285
|
+
stop_task = asyncio.create_task(d.stop.wait())
|
|
286
|
+
await asyncio.sleep(0.05) # let serve() bind so sock_addr() resolves to the live endpoint
|
|
287
|
+
log(f"listening on {ipc.sock_addr(NAME)} (name={NAME}, remote={REMOTE_ID or 'local'})")
|
|
288
|
+
try:
|
|
289
|
+
await asyncio.wait({serve_task, stop_task}, return_when=asyncio.FIRST_COMPLETED)
|
|
290
|
+
if serve_task.done(): await serve_task # surfaces a serve crash
|
|
291
|
+
finally:
|
|
292
|
+
for t in (serve_task, stop_task):
|
|
293
|
+
t.cancel()
|
|
294
|
+
try: await t
|
|
295
|
+
except (asyncio.CancelledError, Exception): pass
|
|
296
|
+
ipc.cleanup_endpoint(NAME)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
async def main():
|
|
300
|
+
d = Daemon()
|
|
301
|
+
await d.start()
|
|
302
|
+
await serve(d)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def already_running():
|
|
306
|
+
try:
|
|
307
|
+
c = ipc.connect(NAME, timeout=1.0); c.close(); return True
|
|
308
|
+
except (FileNotFoundError, ConnectionRefusedError, TimeoutError, socket.timeout, OSError):
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
if __name__ == "__main__":
|
|
313
|
+
if already_running():
|
|
314
|
+
print(f"daemon already running on {SOCK}", file=sys.stderr)
|
|
315
|
+
sys.exit(0)
|
|
316
|
+
open(LOG, "w").close()
|
|
317
|
+
open(PID, "w").write(str(os.getpid()))
|
|
318
|
+
try:
|
|
319
|
+
asyncio.run(main())
|
|
320
|
+
except KeyboardInterrupt:
|
|
321
|
+
pass
|
|
322
|
+
except Exception as e:
|
|
323
|
+
log(f"fatal: {e}")
|
|
324
|
+
sys.exit(1)
|
|
325
|
+
finally:
|
|
326
|
+
stop_remote()
|
|
327
|
+
try: os.unlink(PID)
|
|
328
|
+
except FileNotFoundError: pass
|