@pencil-agent/nano-pencil 2.0.1 → 2.0.2

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