@pencil-agent/nano-pencil 2.0.0-beta.8 → 2.0.0

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 (241) hide show
  1. package/README.md +267 -267
  2. package/dist/build-meta.json +3 -3
  3. package/dist/core/export-html/AGENT.md +11 -11
  4. package/dist/core/export-html/template.css +971 -971
  5. package/dist/core/export-html/template.html +54 -54
  6. package/dist/core/extensions-host/index.d.ts +1 -1
  7. package/dist/core/extensions-host/loader.js +1 -1
  8. package/dist/core/extensions-host/runner.d.ts +1 -0
  9. package/dist/core/extensions-host/runner.js +2 -2
  10. package/dist/core/extensions-host/types.d.ts +17 -22
  11. package/dist/core/lib/ai/src/types.d.ts +12 -2
  12. package/dist/core/persona/persona-manager.js +5 -2
  13. package/dist/core/runtime/agent-session.js +3 -3
  14. package/dist/core/runtime/extension-core-bindings.d.ts +1 -0
  15. package/dist/core/runtime/extension-core-bindings.js +2 -2
  16. package/dist/extensions/builtin/AGENT.md +115 -115
  17. package/dist/extensions/builtin/browser/AGENT.md +17 -17
  18. package/dist/extensions/builtin/browser/agent-workspace/agent_helpers.py +12 -12
  19. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/amazon/product-search.md +198 -198
  20. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/archive-org/scraping.md +341 -341
  21. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv/scraping.md +311 -311
  22. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv-bulk/scraping.md +333 -333
  23. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/atlas/overview.md +70 -70
  24. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/booking-com/scraping.md +578 -578
  25. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/capterra/scraping.md +440 -440
  26. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/centilebrain/generate-estimates.md +110 -110
  27. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coingecko/scraping.md +325 -325
  28. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coinmarketcap/scraping.md +463 -463
  29. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coursera/scraping.md +360 -360
  30. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/craigslist/scraping.md +390 -390
  31. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/crossref/scraping.md +568 -568
  32. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/dev-to/scraping.md +323 -323
  33. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/duckduckgo/scraping.md +349 -349
  34. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/ebay/scraping.md +435 -435
  35. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/etsy/scraping.md +506 -506
  36. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/eventbrite/scraping.md +363 -363
  37. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/expedia/automation.md +168 -168
  38. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/groups.md +236 -236
  39. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/pages.md +295 -295
  40. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/framer/editor.md +108 -108
  41. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/fred/scraping.md +493 -493
  42. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/g2/scraping.md +580 -580
  43. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/genius/scraping.md +511 -511
  44. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/repo-actions.md +65 -65
  45. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/scraping.md +184 -184
  46. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/glassdoor/scraping.md +543 -543
  47. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gmail/compose.md +122 -122
  48. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/goodreads/scraping.md +461 -461
  49. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gutenberg/scraping.md +383 -383
  50. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/hackernews/scraping.md +243 -243
  51. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/howlongtobeat/scraping.md +473 -473
  52. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/imdb/scraping.md +271 -271
  53. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/itch-io/scraping.md +436 -436
  54. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/job-boards/indeed-glassdoor.md +1021 -1021
  55. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/letterboxd/scraping.md +349 -349
  56. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/linkedin/invitation-manager.md +109 -109
  57. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/loom/folder-enumeration.md +170 -170
  58. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/macrotrends/scraping.md +537 -537
  59. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/article-hydration.md +120 -120
  60. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/scraping.md +414 -414
  61. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/metacritic/scraping.md +477 -477
  62. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/musicbrainz/scraping.md +478 -478
  63. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/nasa/scraping.md +339 -339
  64. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/news-aggregation/multi-source.md +205 -205
  65. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/open-library/scraping.md +472 -472
  66. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openalex/scraping.md +470 -470
  67. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openstreetmap/scraping.md +490 -490
  68. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/package-registries/npm-pypi.md +478 -478
  69. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/polymarket/scraping.md +234 -234
  70. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/producthunt/scraping.md +307 -307
  71. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/pubmed/scraping.md +421 -421
  72. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/quora/scraping.md +364 -364
  73. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rawg/scraping.md +352 -352
  74. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/reddit/scraping.md +124 -124
  75. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rest-countries/scraping.md +233 -233
  76. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/sec-edgar/scraping.md +361 -361
  77. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/README.md +36 -36
  78. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/embedded-apps.md +72 -72
  79. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/knowledge-base.md +109 -109
  80. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/polaris-inputs.md +137 -137
  81. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/soundcloud/scraping.md +362 -362
  82. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/spotify/scraping.md +339 -339
  83. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/stackoverflow/scraping.md +435 -435
  84. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/steam/scraping.md +575 -575
  85. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/substack/scraping.md +338 -338
  86. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/thetechgeeks/pricing.md +52 -52
  87. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tiktok/upload.md +107 -107
  88. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tradingview/scraping.md +309 -309
  89. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trello/boards-and-lists.md +88 -88
  90. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trustpilot/scraping.md +375 -375
  91. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/walmart/scraping.md +444 -444
  92. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wayback-machine/scraping.md +306 -306
  93. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/weather/scraping.md +398 -398
  94. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wellfound/scraping.md +596 -596
  95. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/world-bank/scraping.md +356 -356
  96. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/xiaohongshu/scraping.md +84 -84
  97. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/youtube/scraping.md +418 -418
  98. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/zillow/scraping.md +433 -433
  99. package/dist/extensions/builtin/browser/browser.md +73 -73
  100. package/dist/extensions/builtin/browser/install.md +142 -142
  101. package/dist/extensions/builtin/browser/interaction-skills/connection.md +48 -48
  102. package/dist/extensions/builtin/browser/interaction-skills/cookies.md +3 -3
  103. package/dist/extensions/builtin/browser/interaction-skills/cross-origin-iframes.md +3 -3
  104. package/dist/extensions/builtin/browser/interaction-skills/dialogs.md +64 -64
  105. package/dist/extensions/builtin/browser/interaction-skills/downloads.md +3 -3
  106. package/dist/extensions/builtin/browser/interaction-skills/drag-and-drop.md +3 -3
  107. package/dist/extensions/builtin/browser/interaction-skills/dropdowns.md +3 -3
  108. package/dist/extensions/builtin/browser/interaction-skills/iframes.md +3 -3
  109. package/dist/extensions/builtin/browser/interaction-skills/network-requests.md +3 -3
  110. package/dist/extensions/builtin/browser/interaction-skills/print-as-pdf.md +3 -3
  111. package/dist/extensions/builtin/browser/interaction-skills/profile-sync.md +90 -90
  112. package/dist/extensions/builtin/browser/interaction-skills/screenshots.md +17 -17
  113. package/dist/extensions/builtin/browser/interaction-skills/scrolling.md +3 -3
  114. package/dist/extensions/builtin/browser/interaction-skills/shadow-dom.md +3 -3
  115. package/dist/extensions/builtin/browser/interaction-skills/tabs.md +69 -69
  116. package/dist/extensions/builtin/browser/interaction-skills/uploads.md +1 -1
  117. package/dist/extensions/builtin/browser/interaction-skills/viewport.md +3 -3
  118. package/dist/extensions/builtin/browser/src/browser_harness/AGENT.md +15 -15
  119. package/dist/extensions/builtin/browser/src/browser_harness/__init__.py +8 -8
  120. package/dist/extensions/builtin/browser/src/browser_harness/_ipc.py +90 -90
  121. package/dist/extensions/builtin/browser/src/browser_harness/admin.py +722 -722
  122. package/dist/extensions/builtin/browser/src/browser_harness/daemon.py +328 -328
  123. package/dist/extensions/builtin/browser/src/browser_harness/helpers.py +396 -396
  124. package/dist/extensions/builtin/browser/src/browser_harness/run.py +103 -103
  125. package/dist/extensions/builtin/discipline/skills/brainstorming/SKILL.md +33 -33
  126. package/dist/extensions/builtin/discipline/skills/executing-plans/SKILL.md +25 -25
  127. package/dist/extensions/builtin/discipline/skills/finishing-development-branch/SKILL.md +25 -25
  128. package/dist/extensions/builtin/discipline/skills/receiving-code-review/SKILL.md +22 -22
  129. package/dist/extensions/builtin/discipline/skills/requesting-code-review/SKILL.md +31 -31
  130. package/dist/extensions/builtin/discipline/skills/systematic-debugging/SKILL.md +28 -28
  131. package/dist/extensions/builtin/discipline/skills/test-driven-development/SKILL.md +32 -32
  132. package/dist/extensions/builtin/discipline/skills/using-git-worktrees/SKILL.md +25 -25
  133. package/dist/extensions/builtin/discipline/skills/verification-before-completion/SKILL.md +27 -27
  134. package/dist/extensions/builtin/discipline/skills/writing-plans/SKILL.md +26 -26
  135. package/dist/extensions/builtin/goal/README.md +67 -67
  136. package/dist/extensions/builtin/goal/goal-controller.d.ts +39 -10
  137. package/dist/extensions/builtin/goal/goal-controller.js +1 -1
  138. package/dist/extensions/builtin/goal/goal-format.js +1 -1
  139. package/dist/extensions/builtin/goal/goal-prompts.d.ts +2 -0
  140. package/dist/extensions/builtin/goal/goal-prompts.js +5 -4
  141. package/dist/extensions/builtin/goal/goal-store.js +1 -1
  142. package/dist/extensions/builtin/goal/index.d.ts +1 -1
  143. package/dist/extensions/builtin/goal/index.js +10 -7
  144. package/dist/extensions/builtin/grub/README.md +112 -112
  145. package/dist/extensions/builtin/link-world/agent-workspace/README.md +16 -16
  146. package/dist/extensions/builtin/link-world/index.js +6 -6
  147. package/dist/extensions/builtin/link-world/internet-search/internet-search.md +65 -65
  148. package/dist/extensions/builtin/link-world/link-world-agent.md +82 -82
  149. package/dist/extensions/builtin/link-world/linkworld.md +313 -313
  150. package/dist/extensions/builtin/link-world/{network-routing.md → network-routing/network-routing.md} +67 -67
  151. package/dist/extensions/builtin/loop/README.md +92 -92
  152. package/dist/extensions/builtin/mcp/figma-design.md +68 -68
  153. package/dist/extensions/builtin/mcp/mcp-management.md +85 -85
  154. package/dist/extensions/builtin/plan/index.js +1 -1
  155. package/dist/extensions/builtin/recap/AGENT.md +15 -15
  156. package/dist/extensions/builtin/sal/README.md +72 -72
  157. package/dist/extensions/builtin/security-audit/README.md +289 -289
  158. package/dist/extensions/builtin/task/task-store.d.ts +4 -0
  159. package/dist/extensions/builtin/task/task-store.js +1 -1
  160. package/dist/extensions/builtin/team/AGENT.md +112 -112
  161. package/dist/extensions/builtin/team/TESTING.md +299 -299
  162. package/dist/extensions/builtin/token-save/README.md +56 -56
  163. package/dist/extensions/optional/AGENT.md +10 -10
  164. package/dist/index.d.ts +5 -30
  165. package/dist/index.js +1 -1
  166. package/dist/models.d.ts +7 -0
  167. package/dist/models.js +1 -0
  168. package/dist/modes/interactive/components/footer.js +1 -1
  169. package/dist/modes/interactive/components/task-status-panel.d.ts +36 -0
  170. package/dist/modes/interactive/components/task-status-panel.js +1 -0
  171. package/dist/modes/interactive/controllers/stream-render-controller.d.ts +7 -0
  172. package/dist/modes/interactive/controllers/stream-render-controller.js +2 -2
  173. package/dist/modes/interactive/interactive-mode.js +40 -40
  174. package/dist/modes/interactive/state/interactive-state.d.ts +2 -0
  175. package/dist/modes/interactive/state/interactive-state.js +1 -1
  176. package/dist/modes/interactive/theme/dark.json +85 -85
  177. package/dist/modes/interactive/theme/light.json +84 -84
  178. package/dist/modes/interactive/theme/theme-schema.json +335 -335
  179. package/dist/modes/interactive/theme/warm.json +81 -81
  180. package/dist/node_modules/@pencil-agent/ai/dist/cli.js +0 -0
  181. package/dist/node_modules/@pencil-agent/ai/dist/models.generated.js +1 -1
  182. package/dist/node_modules/@pencil-agent/ai/dist/providers/anthropic.js +2 -2
  183. package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-completions.js +5 -5
  184. package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-responses.js +1 -1
  185. package/dist/node_modules/@pencil-agent/ai/dist/stream.js +1 -1
  186. package/dist/packages/protocol/src/commands.d.ts +33 -0
  187. package/dist/packages/protocol/src/flags.d.ts +20 -0
  188. package/dist/packages/protocol/src/hooks.d.ts +17 -0
  189. package/dist/packages/protocol/src/hooks.js +0 -0
  190. package/dist/packages/{extension-sdk → protocol}/src/index.d.ts +7 -4
  191. package/dist/packages/protocol/src/index.js +1 -0
  192. package/dist/packages/{extension-sdk → protocol}/src/lifecycle.d.ts +15 -27
  193. package/dist/packages/protocol/src/lifecycle.js +0 -0
  194. package/dist/packages/{extension-sdk → protocol}/src/tools.d.ts +1 -1
  195. package/dist/packages/protocol/src/tools.js +0 -0
  196. package/dist/public-config.d.ts +12 -0
  197. package/dist/public-config.js +1 -0
  198. package/dist/runtime.d.ts +9 -0
  199. package/dist/runtime.js +1 -0
  200. package/dist/session-compaction.d.ts +7 -0
  201. package/dist/session-compaction.js +1 -0
  202. package/dist/session.d.ts +7 -0
  203. package/dist/session.js +1 -0
  204. package/dist/skills.d.ts +7 -0
  205. package/dist/skills.js +1 -0
  206. package/dist/tools.d.ts +7 -0
  207. package/dist/tools.js +1 -0
  208. 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 +851 -0
  209. package/docs/SDK-TESTING.md +364 -0
  210. package/docs/codex-goal-command-impl.md +1055 -1055
  211. package/docs/codex-goal-vs-grub.md +500 -500
  212. package/docs/custom-provider.md +27 -27
  213. package/docs/extensions.md +27 -27
  214. package/docs/keybindings.md +27 -27
  215. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +250 -250
  216. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +122 -122
  217. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -1222
  218. 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
  219. 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
  220. package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +320 -320
  221. package/docs/loop-usage-examples.md +214 -214
  222. package/docs/mem-core/346/212/200/346/234/257/346/226/207/346/241/243.md +593 -0
  223. package/docs/models.md +27 -27
  224. package/docs/packages.md +27 -27
  225. package/docs/pi-design-philosophy.md +457 -457
  226. package/docs/planmode.md +1987 -1987
  227. package/docs/prompt-templates.md +27 -27
  228. package/docs/providers.md +27 -27
  229. package/docs/sdk.md +27 -27
  230. package/docs/skills.md +27 -27
  231. package/docs/startup-performance-optimization.md +301 -0
  232. package/docs/themes.md +27 -27
  233. package/docs/tui.md +27 -27
  234. package/docs//350/256/244/347/237/245/345/234/260/345/233/276.md +47 -0
  235. package/package.json +190 -162
  236. package/dist/packages/extension-sdk/src/index.js +0 -1
  237. package/docs/cc-agent-design.md +0 -1297
  238. package/docs/cc-tui-design.md +0 -1333
  239. package/docs//345/257/271/346/240/207Claude-Code.md +0 -1775
  240. /package/dist/packages/{extension-sdk/src/lifecycle.js → protocol/src/commands.js} +0 -0
  241. /package/dist/packages/{extension-sdk/src/tools.js → protocol/src/flags.js} +0 -0
@@ -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