文件历史

提交图

17 次代码提交

作者 SHA1 备注 提交日期
opensesamenext1-netizen 761da289c8 Support Flatpak browser profile paths 2026-04-21 14:59:36 +00:00
Sybren Geel ca2ee56bf8 fix: detect Chromium profiles on Linux 2026-04-19 17:43:35 +02:00
Magnus Müller 4a7f7633f1 fix tab indicator: re-enable Page + mark on session switch (#70)
The merged tab indicator only marked on Page.loadEventFired,
but Page events weren't enabled for new sessions created by
switch_tab(). Added Page.enable + immediate mark in set_session
handler so the 🟢 appears on every tab switch and survives
goto() navigation.

6/6 tests pass: new_tab, switch_tab, goto, click link,
cmd+click, switch back.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 16:17:15 -07:00
Prabir Shrestha 4e562620be Add Microsoft Edge support (#63)
* Add Microsoft Edge support

Edge is Chromium, so the daemon's CDP bootstrap works unchanged once it
knows where Edge's DevToolsActivePort lives. This adds the macOS, Linux,
and Windows Edge profile paths to the discovery list, plus a one-line
note in install.md clarifying that `chrome://inspect/#remote-debugging`
and the rest of the setup flow apply to Edge too.

* Edge Beta/Dev/Canary, drop Edge from main README

Cover the non-stable Edge channels on macOS, Linux, and Windows so
users on insider builds don't fall through to 'DevToolsActivePort not
found'. Edge SxS (Side-by-Side) is Canary's install dir on Windows.

Keep README's Chrome-only pitch; Edge stays documented in install.md
where setup details belong.

* install.md: move Edge note to bottom, one line

Top-of-section carve-out distracts from the Chrome bootstrap steps.
A one-liner in the cold-start reminders is enough for users who need it.

---------

Co-authored-by: MagMueller <mamagnus00@gmail.com>
2026-04-18 15:37:43 -07:00
Magnus Müller 65015e57fa try WS handshake once, not 12 times (#67)
Each retry created a new CDPClient which opened a new WebSocket
connection, triggering Chrome's "Allow debugging" dialog again.
12 retries = 12 stacked popups the user has to dismiss.

Now tries once. If it fails, tells the user to click Allow and
retry — no popup spam.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 15:20:46 -07:00
Magnus Müller 6a6081538a add 5s timeout on domain enable calls, add nuclear recovery gotcha (#66)
daemon.py: Page/DOM/Runtime/Network.enable calls now have a 5s
timeout. Previously they could hang indefinitely on heavy pages
(TikTok FYP), preventing the daemon from reaching its socket
listener.

SKILL.md: added one gotcha for when restart_daemon() itself hangs
(kill Chrome entirely and reconnect).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 15:06:33 -07:00
Magnus Müller 5d06079d7f page_info: surface pending native dialog (#65)
Agents rarely know to check for open alert/confirm/prompt/beforeunload
dialogs — they freeze the JS thread, so page_info()'s Runtime.evaluate
returns a stale or misleading viewport dict while the page is blocked.

Track the latest unhandled Page.javascriptDialogOpening in the daemon
(cleared on Page.javascriptDialogClosed) and, when one is open, return
{dialog: {type, message, ...}} from page_info() instead of the usual
viewport dict. The dialogs skill now points agents at this signal.
2026-04-18 14:58:18 -07:00
Magnus Müller 23ce2ec464 Fix daemon attaching to invisible omnibox popup (#60)
* fix daemon attaching to invisible omnibox popup on fresh Chrome

When Chrome opens fresh, the only page targets are chrome://
internal pages and the omnibox popup (1px invisible viewport).
The daemon's attach_first_page() fell back to the popup, making
all subsequent work invisible to the user.

Fix: when no real pages exist, create an about:blank tab via
Target.createTarget instead of attaching to the omnibox popup.

Tested configurations:
- Fresh start with no real tabs → creates about:blank (1112x817)
- Navigate without AppleScript → works, tab visible
- Recovery from stale socket → auto-reconnects
- Chrome restart from scratch → creates about:blank

Also adds interaction-skills/connection.md documenting the
omnibox popup problem and startup sequence.

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

* add connection skill reference to main SKILL.md

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:07:43 -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 0702b83759 Clarify Chrome remote debugging bootstrap (#22) 2026-04-17 21:12:50 -07:00
Magnus Müller 35ae22bdec [codex] Make setup work from anywhere (#17)
* Make setup work from anywhere

* Rename launcher to bh

* Remove extra helper commands

* Document global skill setup
2026-04-17 19:32:18 -07:00
Gregor Žunič 5ab5a958bb add remote browser support (Browser Use cloud) + multi-daemon + rename to bu (#1)
* 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>
2026-04-17 00:28:02 -07:00
Gregor Žunič 4552e7b42f prune eval-specific helpers; keep primitives + document gotchas
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>
2026-04-16 19:42:29 -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č df92b0c4e7 handle stale sessions + add iframe support (Azure billing admin task)
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>
2026-04-16 18:56:35 -07:00
Gregor Žunič da8257c586 simplify: drop 14 unused helpers, compress daemon, tighten docs
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>
2026-04-16 18:41:27 -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