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