If an agent runs the interceptor snippet before GTM has initialized,
window.dataLayer is undefined and .push.bind throws. Seeding with [] is
safe — GTM picks up a pre-existing array on init.
Dia is a Chromium-based browser by The Browser Company (same makers as
Arc). It ships with CDP enabled by default on its standard user-data-dir
at ~/Library/Application Support/Dia/User Data, so adding the path is
all that's needed for browser-harness to attach without any per-profile
opt-in.
Placed next to Arc since both are Browser Company products. Existing
browser priority (Chrome first) is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
press_key emits a Input.dispatchKeyEvent('char', text='a') after every
single-character keyDown. With Cmd/Ctrl held, that char event makes Chrome
treat the input as a printable letter "a" rather than firing the
select-all shortcut, so the field never gets cleared (Backspace would
then delete the literal "a" and leave the original value untouched).
Dispatch the rawKeyDown/keyUp pair directly with the modifier set and no
text/char event. Tightened the test to assert (a) the modifier is on the
'a' event with the platform-correct value and (b) no char event with
text='a' is emitted.
The computed-style check measures the element itself, so an element nested
inside a display:none or visibility:hidden ancestor is reported as visible
(getComputedStyle returns the descendant's own non-none value, not the
inherited "is rendered" state). checkVisibility walks the ancestor chain
and is the right primitive on modern Chrome. Kept the per-element CSS
check as a fallback for older Chrome that lacks checkVisibility.
- ly-com: read Vue ref via stable .value with _rawValue fallback
rather than the brittle internal _rawValue path.
- wehotel: ancestor walk now identifies the hotel-name anchor by
identity/innerText instead of an href-based negation against text
(查看详情 lives on the link body, not in href), which previously
let the loop stop on the detail anchor itself.
- ctrip: raise an explicit error when the destination input or 搜索
button isn't found, instead of dereferencing None and crashing
with a confusing TypeError.
- write_text calls now pin encoding="utf-8" so non-ASCII transcripts
don't depend on the locale default.
- share-export.md schema now lists source_url, matching what the script
actually emits.
* domain-skills: add bilibili navigation and structure skill
Covers site architecture, URL patterns, content channels, video
interaction features (三连/投币/弹幕), and navigation quirks for
the Chinese video platform.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* domain-skills: fix PR review — correct history URL and ranking tab count
- Fix history URL from bilibili.com/history to https://www.bilibili.com/account/history
- Remove incorrect "also contains 稍后再看 tab" note (watch later is a separate SPA)
- Fix ranking category tab count from 23 to 24
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* domain-skills: add BOSS直聘 (zhipin.com) navigation and job search skills
Verified content only — navigation structure and job card extraction
confirmed via browser CDP.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* domain-skills: verify zhipin job search with API-first approach
Browser-verified: the /wapi/zpgeek/pc/recommend/job/list.json API
returns real salary numbers ("18-22K") unlike the DOM which uses
font-encoded PUA characters. Added full API reference for job list,
job detail, and filter conditions endpoints.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* domain-skills: verify zhipin chat with WebSocket + REST API
Browser-verified: conversation list loads via WebSocket (ws6.zhipin.com),
message history via /wapi/zpchat/geek/historyMsg API. Documented message
body types (text=1, job card=8, system=16), DOM selectors, pagination,
and bossId extraction.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* domain-skills: add zhipin chat filter tabs with clickable UI control
Browser-verified: 全部/未读/新招呼/更多/有交换/有面试/不感兴趣/AI筛选
filters with click_filter() and click_more_filter() functions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* domain-skills: browser-verify job-search.md and fix API field names
Verified all functions against live zhipin.com. Fixed brandInfo→brandComInfo
with correct sub-fields (brandName/logo/industryName), corrected city codes
(101280100=Guangzhou not Shenzhen), documented page-based pagination as
unreliable, added encryptExpectId default params, and noted anti-bot timing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* domain-skills: fix PR review — add API response validation, narrow bossId extraction, clarify city slug
- Guard all d.zpData accesses with d.code !== 0 checks across job-search.md and chat.md
- Filter bossId extraction to /wapi/zpchat/geek/historyMsg URLs only
- Correct "Chinese" to "pinyin" for city slug and add API code cross-reference
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
#266 made `browser-harness -c '...'` auto-provision a Browser Use cloud browser
whenever BROWSER_USE_API_KEY was set and no local Chrome / daemon was alive.
That gating signal is too broad — the API key is commonly set for unrelated
reasons (profile sync, cloud API calls, parent agents managing their own
session), so users with the key in env quietly got a *new* (billed) browser
spawned every time they ran a script.
Make auto-bootstrap opt-in via BU_AUTOSPAWN. Setting it alongside
BROWSER_USE_API_KEY restores #266's behaviour for fresh headless boxes;
anyone else is unaffected.
Refs #181, #183, #266.
Three improvements to the cross-platform IPC layer, lifted from #104:
- meta:'ping' handshake replaces bare TCP connect in daemon_alive() and already_running(). A connect-only check on Windows can succeed against an unrelated process that grabbed our ephemeral port after a daemon crash; the ping/pong response confirms the listener is actually our daemon.
- Per-daemon random token (secrets.token_hex(32)) gates every request on Windows. AF_UNIX + chmod 600 is the boundary on POSIX, but TCP loopback has no chmod-equivalent; without a token any local process could connect and issue CDP commands.
- Atomic .port write (write .port.tmp, os.replace) so a concurrent reader never sees a half-written file.
Adds rohitdutt108 to VOUCHED.td.
Co-authored-by: Rohit Dutt <rohit.dutt@iyc.ishafoundation.org>
Domain skills auto-injected by goto_url() are community-contributed and quality varies; defaulting them off avoids polluting the average run while preserving the contribution loop. Set BH_DOMAIN_SKILLS=1 to opt in.
bug-report.yml: 4 required fields plus a 4-box preflight (searched issues, ran --doctor, read install.md, this is a bug not a question/FR/cloud issue). feature-request.yml: 3 required fields plus a 2-box preflight. config.yml disables blank issues and links to Discussions Q&A and install.md.
Same behavior matrix: HTTPError 404 with a ws_path returns the file's ws URL immediately; 404 without ws_path or any other error keeps polling until the 30s deadline. Removes the flag, the break, the post-loop check, and shrinks the explanatory comment.
Captures durable selectors and gotchas for exporting transcripts from
claude.ai/share/<uuid> URLs:
- [data-testid=user-message] for user turns, .font-claude-response for
assistant turns, [data-testid=page-header] for the chat title
- Container-walk pattern (no semantic wrapper — turns are alternating
siblings of a common ancestor)
- The H2.sr-only "Claude responded:" trap that pollutes innerText if you
select the outer turn div instead of .font-claude-response
- Auth requirement: share pages render the conversation only for
signed-in viewers; logged-out sessions get an empty app shell
Includes a working extract-share-transcript.py that emits both JSON and
LLM-friendly Markdown ({Human|Assistant} headers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cubic flagged that the original socket.create_connection probe matches any process on 9222/9223, not just Chrome. Mirror daemon.py's fallback by hitting /json/version, so a stale or unrelated listener does not skip the cloud bootstrap.
Drop the three boolean-table tests that mocked every collaborator and re-asserted the literal if-condition. Add a focused test for _local_chrome_listening that covers the false-positive case directly.
On headless servers (VPS, Docker) with no local Chrome, ensure_daemon() fires
before any user script runs and raises immediately — start_remote_daemon() can
never be reached from within a -c script.
Add a pre-check in main(): if no daemon is alive, Chrome is not listening on
known debugging ports (9222/9223), and BROWSER_USE_API_KEY is set, auto-
provision a Browser Use cloud browser before falling through to ensure_daemon().
_local_chrome_listening() probes ports 9222/9223 with a 0.3s timeout rather
than relying on _is_local_chrome_mode(), which only checks for absence of
BU_CDP_WS and would incorrectly trigger cloud bootstrap on a local machine
where Chrome is running but BROWSER_USE_API_KEY is also set (e.g. for profile
sync).
Fixes the behaviour reported in issues #181 and #183.
Tested on a headless Hostinger VPS running hermes-agent in Docker — browser-
harness -c '...' now works without any manual daemon setup when BROWSER_USE_API_KEY
is set.
Chrome 147+ silently disables /json/* HTTP discovery when remote debugging
runs on the default user-data-dir (IsUsingDefaultDataDirectory). The websocket
itself still works and DevToolsActivePort still holds the live ws path, but
get_ws_url() polls /json/version for 30s and dies with a misleading
"DevTools is not live yet" error.
PR #260 deliberately stopped trusting the path stored in DevToolsActivePort
because a stale port file (left behind after switching --user-data-dir on the
same port) had a dead UUID. That fix is correct, but it never falls back to
the file's UUID even when Chrome just wrote the file in the current run.
Read both port and ws path. Try /json/version first (preserves #260's
stale-UUID behavior). On HTTP 404 specifically — which is distinct from
connection-refused/timeout meaning "Chrome still starting" — fall back to
ws://127.0.0.1:{port}{ws_path} from the file. The file's UUID is fresh
because Chrome rewrites it on every launch, and the only persistent reason
/json/version 404s on a live port is the default-profile lockdown. If the WS
upgrade then 404s (true stale UUID), the existing CDP handshake error
surfaces unchanged.
Closes#264.