文件历史

提交图

17 次代码提交

作者 SHA1 备注 提交日期
Alezander9 71cf3a87a0 Merge remote-tracking branch 'origin/main' into fix/windows-ipc
# Conflicts:
#	SKILL.md
#	admin.py
#	helpers.py
2026-04-27 17:41:08 -07:00
Alezander9 991ab21f80 Windows support: route IPC through ipc.py (TCP on Windows, AF_UNIX on POSIX)
The harness was Linux/macOS-only because daemon IPC hardcoded AF_UNIX sockets
at /tmp/bu-*.sock paths and asyncio.start_unix_server, all of which are
unavailable or invalid on Windows. Worse, uv-managed Python on Windows
(python-build-standalone) ships without socket.AF_UNIX entirely (#124).

New ipc.py centralizes the platform fork:
  - POSIX: AF_UNIX socket at <tempdir>/bu-<NAME>.sock (chmod 0600), unchanged
    semantics from the prior /tmp-hardcoded path.
  - Windows: TCP loopback on 127.0.0.1:<ephemeral>, with the chosen port
    written to <tempdir>/bu-<NAME>.port so clients can find the daemon.
    Uses asyncio.start_server (stdlib, no obscure APIs, no third-party deps).

Path discipline: log/pid/port files all sit under tempfile.gettempdir() so
they land in /tmp on Linux, $TMPDIR on macOS, %TEMP% on Windows. helpers.py
screenshot() default also moves from /tmp/shot.png to tempfile.gettempdir().

subprocess detach uses start_new_session=True on POSIX and
DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP on Windows via ipc.spawn_kwargs().

run.py reconfigures stdout to UTF-8 on Windows so print(page_info()) doesn't
UnicodeEncodeError on the 🟢 marker that helpers prepend to tab titles
(#124 item 4). cp1252 (PowerShell default) can't encode it.

Verified end-to-end on Windows 11 with Chrome remote debugging:
  - daemon spawns, allocates port, writes .port file
  - goto + page_info + screenshot round-trip through TCP loopback
  - restart_daemon cleans up .port and .pid

POSIX path is logically equivalent to the prior code (same AF_UNIX call,
same socket-file semantics, same chmod 0600), routed through ipc.py.

Closes #124 items 1, 2, 4. Item 3 (Chrome 147 user-data-dir) is a separate
concern not addressed here.
2026-04-27 16:40:56 -07:00
Test User 263de47192 Fix IndexError when -c flag is passed without code argument
Running `browser-harness -c` without a code argument crashed with an
unhandled IndexError at `exec(args[1])`. Added a length check to
produce a proper usage message instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 19:58:12 +08:00
Sarath S Menon 216a2c9653 feat(cli): add --reload flag to restart the daemon (#200)
* fix(js): don't double-wrap IIFEs that contain return

The substring check `"return " in expression` incorrectly matched
expressions that already contained an IIFE with an internal return,
causing double-wrapping and a silent None result. Guard with
`not expression.strip().startswith("(")` so pre-wrapped expressions
pass through unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(cli): add --reload flag to restart the daemon

Kills the running daemon so the next call picks up code changes,
replacing the manual pkill workflow after editing helpers.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:32:22 +05:30
Sarath S Menon 5918ea7e4b feat(debug): add --debug-clicks mode with DPR-aware overlay (#189)
When BH_DEBUG_CLICKS=1 (or --debug-clicks CLI flag), click_at_xy
captures a screenshot before each click and draws a red crosshair
at the exact click location, scaled by window.devicePixelRatio so
the marker aligns correctly on HiDPI/Retina displays.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 16:54:50 +05:30
Sarath S Menon 05d6eb8c3e refactor(run): replace heredoc stdin with -c flag (#188)
* fix(js): auto-wrap top-level return expressions in an IIFE

Agents naturally write js() calls with top-level `return`, which is a
syntax error in Runtime.evaluate. js() now detects `return ` and wraps
the expression in an IIFE so both styles work without callers needing
to know the CDP constraint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(run): replace heredoc stdin with -c flag

Drops the <<'PY' heredoc pattern entirely. Agents now pass code via
`browser-harness -c "..."`, which is simpler to construct programmatically
and avoids heredoc quoting pitfalls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 15:53:37 +05:30
Gregor Žunič ba8b22f1ff fix(run): exec scoping for comprehensions + sharpen click guidance (#133)
* fix(run): pass globals to exec so comprehensions resolve free vars

exec(sys.stdin.read()) inside main() passes different dicts for globals
and locals. Python comprehensions and generator expressions compile to a
nested function whose free-variable lookups can only see the globals
dict, so code like

    for it in items:
        low = it['text'].lower()
        hit = any(k in low for k in KEYS)   # NameError: 'low'

fails at module level under exec. `low` is stored in the exec locals
dict which the generator's implicit function cannot see.

Passing globals() as the only extra arg makes exec use the same dict
for both globals and locals, and comprehensions resolve cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(skill): eyeball click coords from screenshots; no getBoundingClientRect

The previous Clicking bullet said "screenshot -> look -> click(x,y)" but
did not rule out roundtripping through js("...getBoundingClientRect()")
to compute coords. Agents coming from Playwright / Selenium habits tend
to locate-first-click-second even when the screenshot already shows the
target, which is slower and more brittle (hidden inputs placed at
x=-9999, CSS-transformed elements, pseudo-elements) than just reading
the pixel off the image.

Rewrite the bullet to explicitly suppress that reflex and scope the
DOM-fallback to elements with no visible geometry. Also replace the
loose "compositor level" phrasing with the actual mechanism: Chrome's
browser-process hit-testing, which is why clicks pass through iframes
/ shadow DOM / cross-origin without extra work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:06:16 -07:00
reformedot 8a0c981082 feat: self-update CLI, release workflow, and fetch-use routing
- Add --version, --doctor, --setup, --update[-y] commands in run.py;
  logic lives in admin.py (install-mode detection, GitHub-releases cache
  with 24h TTL, dirty-worktree guard, interactive Chrome-attach flow).
- Print a once-per-day startup banner telling agents to run
  `browser-harness --update -y` when a newer release is available.
- Rename project to browser-harness in pyproject.toml so PyPI installs
  (uv tool install browser-harness) work via the public package name.
- Add .github/workflows/release.yml: on v* tag push, verify the tag
  matches pyproject.toml, uv build, and publish to PyPI via trusted
  publishing.
- Wire helpers.http_get through fetch_use.fetch_sync(...).text when
  BROWSER_USE_API_KEY is set; falls back to the original urllib path
  otherwise, preserving the existing str return contract.
- Document the new commands and the agent's self-update duty in
  SKILL.md and install.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:49:07 -07:00
Magnus Müller 82a1f2f540 remote: stop_remote_daemon helper, skill updates from sub-agent testing (#96)
* remote: stop_remote_daemon helper, skill updates from sub-agent testing

Three fresh sub-agents ran typical-user prompts against the skill
("start a remote browser with my logged-in data" / "Stripe only, no
Google" / "refresh my existing profile"). All three completed
end-to-end — cookies made the round trip, filters scoped cleanly,
refresh was idempotent. But they hit a few doc/code seams worth
closing:

admin.py:
- Add stop_remote_daemon(name="remote"). Sub-agents kept reaching
  for restart_daemon() because there was no obvious way to end a
  remote session; the new alias pairs symmetrically with
  start_remote_daemon() and makes the intent explicit. Same
  underlying implementation — it's just naming for callers.

run.py:
- Pre-import stop_remote_daemon so it's usable from
  browser-harness <<'PY' ... PY without an extra import line.

interaction-skills/profile-sync.md:
- Drop the "close Chrome before syncing" trap at the top of the
  Traps section. Obsolete on profile-use v1.0.5+ — the tool copies
  the profile dir to a temp and syncs from the copy. Two sub-agents
  verified sync works with Chrome open on v1.0.5. Kept a small
  note at the bottom for anyone on older versions.
- Document the ♻️ / 📝 reuse-vs-create signal in sync_local_profile
  output, so agents can confirm cloud_profile_id was accepted
  without counting profiles.
- Clarify the API path convention for _browser_use and the raw
  examples: paths are relative to BU_API, not absolute
  /api/v3/... Sub-agents were copy-pasting the old doc form and
  getting 404s.
- Add a one-liner for looking up an existing cloud profile's UUID.
- Surface stop_remote_daemon in the Python API overview.

Housekeeping:
- Stopped 5 sub-agent-leftover remote browsers from today's testing
  burst (not committed; just an operations note).

* skill: UUID lookup pattern must handle 0 and >1 matches (cubic review)

cubic flagged the one-liner as unsafe: 'next(p["id"] for p in … if …)'
raises StopIteration on no match and silently picks the first duplicate
when names repeat. Profile names genuinely aren't unique — sub-3 in the
same testing round surfaced a duplicate on this account — so use a list
comprehension and require exactly one match before using the UUID.
2026-04-19 00:23:28 -07:00
Magnus Müller 96ccb7692b remote: Python API for remote browsers, profiles, and local-profile sync (#84)
* remote: Python API for remote browsers, profiles, and local-profile sync

No CLI, no new entrypoint. Every helper is a Python function callable from
inside a normal `browser-harness <<'PY'` block. run.py pre-imports them.

admin.py:
- start_remote_daemon(name, profileName=None, **create_kwargs)
  Now forwards every documented POST /browsers kwarg (profileId, profileName,
  proxyCountryCode, timeout, customProxy, browserScreenWidth/Height, ...).
  profileName is resolved client-side via list_cloud_profiles — no browser-use
  API change needed. Prints liveUrl and auto-opens it locally when a GUI is
  detected (macOS/Windows always; Linux needs $DISPLAY / $WAYLAND_DISPLAY);
  headless servers print only.
- list_cloud_profiles() — GET /api/v3/profiles + per-profile detail; returns
  [{id, name, cookieDomains, lastUsedAt, userId}]. Agents should report
  len(cookieDomains) not the full list — profiles can have 500 cookies across
  dozens of domains.
- list_local_profiles(), sync_local_profile(name) — shell out to `profile-use`.
  sync_local_profile returns the newly-created cloud UUID.

Profile-sync skill rewritten Python-first with the chat-driven flow (ask the
user which profile; summarize by domain count, never dump cookies) and calls
out the two upstream limitations (sync always creates a new cloud profile; no
per-domain filtering) that need a PR to browser-use/profile-use — they can't
be fixed in browser-harness because the Browser Use API has no cookie
upload/download endpoint.

SKILL.md remote-browsers section updated to match, leading with the parallel
sub-agent use case.

* remote: fix misleading 'no GUI' message when webbrowser.open raises

cubic flagged this on #84: if _has_local_gui() is True but webbrowser.open
raises (e.g. no default browser configured), the code fell through to the
final 'no local GUI — share the liveUrl' line, which is wrong on both counts.
Restructure so each branch produces exactly one accurate message.
2026-04-18 22:19:47 -07:00
Magnus Müller becec42ca4 [codex] Rename CLI to browser-harness run (#35)
* Rename CLI to browser-harness run

* Simplify browser-harness CLI
2026-04-17 22:33:05 -07:00
Magnus Müller c9910d426d Top-load browser harness usage guidance (#33)
* Top-load browser harness usage guidance

* Refine skill fast-start copy
2026-04-17 22:26:46 -07:00
Gregor Žunič e48235b32c Print usage hint when bh is run on a TTY (#34)
A bare `bh` invocation blocks forever on sys.stdin.read(). Detect a TTY
stdin and exit with a one-line usage hint instead, so the failure mode
is obvious.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 22:20:40 -07:00
Magnus Müller acda8c72a0 Simplify helper surface (#23)
* Simplify helper surface

* Trim common module and restore dispatch key
2026-04-17 21:37:58 -07:00
Magnus Müller fc3be06d78 add global bu launcher and shared skill guidance (#14) 2026-04-17 18:21:50 -07:00
Gregor Žunič 2366c226e9 self-managing daemon + document post-task self-improvement ritual
Connection management (addresses "shit ton of daemons" problem):
- Socket is the lock. Daemon refuses to start if another is already
  listening (5-line check).
- PID file at /tmp/harnesless.pid written on start, removed on exit.
- ensure_daemon() / kill_daemon() / daemon_alive() helpers.
- run.py auto-calls ensure_daemon() before exec — users never manage
  the daemon manually.
- kill_daemon uses PID file (robust) instead of pkill pattern matching
  (was silently missing because the process command line didn't contain
  the "harnesless/" prefix).

Post-task ritual added to SKILL.md: after every browser task, extract
ONE generalizable friction point and make the simplest possible
improvement (2-line helper, one-line gotcha, recipe correction).
This is how the harness sharpens itself over time.

Verified end-to-end: kill_daemon → 0 processes + files cleaned → next
run.py → auto-starts exactly one daemon. Second `uv run daemon.py`
exits with "daemon already running" message instead of racing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 19:00:38 -07:00
Gregor Žunič 2b8be642e0 initial commit: harnesless — LLM-first browser control via CDP
Three-process architecture: daemon.py holds one persistent CDP WebSocket
to the user's running Chrome (via chrome://inspect), short-lived run.py
processes talk to it over a Unix socket, helpers.py is the transparent
layer the LLM reads and edits at will.

Philosophy: no CLI, no fixed API surface. The LLM writes Python blocks
against ~13 tiny helpers (cdp, click, type_text, screenshot, get_dom,
etc.) and edits helpers.py on the fly when a pattern repeats. Coordinate
clicks default because they pass through iframes/shadow DOM/cross-origin
at the compositor level.

Uses cdp-use internally for send_raw only (ignores its 36k lines of
typed wrappers — raw CDP strings tokenize better than typed calls).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 17:51:31 -07:00