Collapsed multi-line docstrings on fill/clickables/field_info/type_text/press_key
to one-liners, dropped block comments above CLICKABLE_SELECTOR/FORMAT_HINTS,
and removed inline how-it-works comments in press_key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small helpers that close the gap with verb-style CLIs on form-filling
benchmarks, without bundling them into a monolithic "state" command:
- fill(selector, value): focus + Input.insertText + Tab blur. Fires native
beforeinput/input/change/blur events so React/Vue/Angular/Formik inputs
all register the value. ~4 CDP calls regardless of value length (vs the
per-char press_key loop which is 3 calls × char-count).
- clickables(): indexed interactive elements — cheaper than screenshot+Read
for verification. CLICKABLE_SELECTOR module constant is overridable per
call for sites using custom [data-*] patterns.
- field_info(selector): all validation attrs + format_hint for date/time
inputs + live validity/error from HTML5 validity API. Codifies the
"what format does this widget expect" table (FORMAT_HINTS) so agents
don't guess YYYY-MM-DD vs mm/dd/yyyy.
type_text() gets a docstring noting it does NOT fire keydown/keyup events,
pointing users to fill() for framework-controlled inputs.
Benchmarked on a 38-task browser-automation eval: total cost dropped from
$8.91 → $6.51 (−27%), pass rate went from 33/38 → 38/38 (v2 had 5 tasks
hit the 30-turn cap on framework form fields; fill() brought them all in
under cap).
Previously, printable chars like 'a' or '.' sent keyDown with text AND
char with text, each of which inserts the character. Typing 'test' gave
'tteesstt'. Control keys (Enter, Tab, arrows) weren't affected because
their text is '\r', '\t', or empty and doesn't visibly double-insert.
Fix: for printable chars, use rawKeyDown (no text) + char (with text) —
the canonical CDP pattern for keyboard input. Control keys unchanged.
* add remote browser support via Browser Use cloud + multi-daemon
HARNESLESS_NAME suffixes socket/pid/log — daemons are independent, no
supervisor. start_remote_daemon() creates a Browser Use cloud browser and
launches a daemon attached to it; kill_daemon() stops both. Local Chrome
path is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* rename env vars to BU_ prefix (shorter, less noisy in tool calls)
HARNESLESS_NAME → BU_NAME
HARNESLESS_CDP_WS → BU_CDP_WS
HARNESLESS_REMOTE_BROWSER_ID → BU_BROWSER_ID
Socket/pid/log files keep the harnesless- prefix on disk so they're
recognizable in /tmp. BROWSER_USE_API_KEY unchanged (external convention).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* rename project from harnesless to bu
Socket/pid/log paths now /tmp/bu-<name>.{sock,pid,log}. pyproject package
name updated, uv.lock regenerated. Slash command now /bu.
Note: the repo directory itself is still named harnesless on disk. Rename
manually (mv harnesless bu) so the absolute paths in docs line up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous commit added fill_form / mui_select_first / is_success which are
eval-harness logic, not harnesless primitives (task-specific defaults like
"dumbledore"/"Harry Potter", MUI-only shim). Per AGENTS.md — "could the LLM
rewrite this from scratch after reading it once" — the LLM should pick field
values + the submit strategy per-task, not inherit eval defaults.
Removed: fill_form, _FILL_JS, mui_select_first, is_success (~100 lines)
Kept: dispatch_key, upload_file, capture_dialogs/dialogs (universal needs)
Insights from the eval captured as gotchas in SKILL.md instead:
- React controlled inputs need native-setter + input event
- Radios/checkboxes: el.click() over el.checked=true for React
- MUI / UI-library overlays: real CDP click, not JS .click()
- CDP char event ≠ DOM keypress for special keys → dispatch_key
- Same-origin iframes: contentDocument walk, not CDP targets
- Shadow DOM: querySelector doesn't pierce, walk .shadowRoot
- Form success signals vary: element / alert / body text
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Observed friction during an Azure portal task:
- Daemon's default session went stale (user closed the attached tab),
which broke every subsequent call including browser-level Target.*.
- new_tab() had been removed in the previous simplification pass but
was needed to recover.
- Azure portal renders blade panels in iframes; js() on the main page
returned nothing for picker contents.
Changes:
- daemon: browser-level Target.* calls now bypass self.session entirely
(so a stale session doesn't poison them). On "Session with given id
not found" for session-scoped calls, clear + re-attach + retry once.
Merged start() and the new recovery path into attach_first_page().
- helpers: add new_tab(); js() accepts target_id for iframe queries;
iframe_target(substr) to find blade iframes; ensure_real_tab() now
resilient to stale-session exceptions.
- SKILL.md: one-line note on iframe-site workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Session of 7 real tasks (Upwork, X, Google Flights, HN, Netflix, iPhone
comparison) used only half the original helper surface — every DOM
interaction went through js() + a bespoke selector, never through the
indexed-DOM helpers. Dropped 14 that never fired: get_dom,
click_element, type_in, element_pos, save_cookies, load_cookies,
set_viewport, screenshot_full (folded into screenshot full=True),
double_click, right_click, move_mouse, new_tab, close_tab,
handle_dialog, back, reload.
Also:
- daemon: shorter identifiers, collapsed boilerplate, TOCTOU fix via
try/except on read_text, is_real_page module-level, no variable
shadowing of `url`.
- helpers: one shared INTERNAL_URL_SCHEMES tuple instead of three
subtly-different ones; current_tab() collapsed from 4 round-trips
with dead fallback to one; wait_for_load() stops draining the shared
event buffer.
- SKILL.md / AGENTS.md: cut recipes and anything the LLM already knows
(what click does, what CDP is). Kept only project-specific context.
Net: 8600 -> 4030 tokens across the project (53% reduction); the file
the LLM reads every skill invocation (helpers.py) dropped from 2193
to 1219 tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>