Skip to content

Releases: wighawag/webhands

webhands@0.7.0

Choose a tag to compare

@github-actions github-actions released this 12 Sep 16:19
b8a2e48

Minor Changes

  • c4c2312: Ship the use-webhands skill inside the published package, and stop flooding the skill list.

    webhands skills add advertised installing a skill the package did not contain. The tarball carried no skill at all (files was dist/src/README.md/LICENSE, and the skill was authored at the monorepo root, outside the published package), and the CLI passed no sync option to incur, so the command had no hand-authored skill to install in the first place. README.md has been telling users to "install the bundled skill" the whole time. Consumers worked around it by symlinking the skill out of a git CHECKOUT, which is mutable and unversioned: it describes whatever branch that clone sits on rather than the installed binary, and it cannot be linked from a store path beside the binary on Nix.

    • The skill now lives at skills/use-webhands/SKILL.md INSIDE the package and is listed in files, so npm pack carries it. Top-level skills/, not dist/skills/: it is hand-authored source, and dist/ is tsc output wiped on every build.
    • skills add and skills list resolve it from the MODULE's own location, not the caller's cwd, so a bare npx webhands skills add from any directory installs it with no clone present. (incur defaults sync() to a walked-up package root but list() to process.cwd(); passing an explicit cwd stops the two commands disagreeing about which skills exist.)
    • Generated per-verb skills are collapsed into ONE webhands command reference (depth: 0) instead of 26. They were near-duplicates of a reference use-webhands already carries in full, and they buried it. skills add now installs exactly two skills.

    The README now also documents LINKING the skill straight out of the installed package instead of syncing it (the read-only, version-pinned path for Nix and other declarative setups), including the clash: skills add clears its destination before writing, so running it after linking replaces the symlink with a mutable copy. Only use-webhands can be linked — the generated reference is rendered at sync time and has no file to point at.

    The skill's content needed no change: it already addressed a reader who has the CLI installed and invokes it as npx webhands <verb>.

webhands@0.6.0

Choose a tag to compare

@github-actions github-actions released this 11 Sep 19:24
fcb8409

Minor Changes

  • b88e30b: Add cookies clear: remove a NAMED SUBSET of the active session's cookies and report how many actually went.

    Motivated by a live Akamai-protected site that flipped from serving to blocking part way through an authenticated session. That verdict is persisted in a handful of named cookies (_abck, bm_sz, bm_sv, ak_bmsc), so every dynamic endpoint 403s until they are gone, while the login lives in the site's own separate session cookies. Clearing just those names restores access and keeps the human signed in. Until now the cookies verb did export/import only, so the only way to do that was to drop into script.

    New seam verb clearCookies(filter) (names / domain / path / all, exact-match strings, every field must match) available in-process and over the session RPC, plus webhands cookies clear [--name <n>]… [--domain <d>] [--path <p>] [--all] where --name is repeatable. It returns the number of cookies the browser actually removed, computed as a before/after difference, so cleared: 0 visibly means "nothing matched" rather than silently passing.

    The count is over the MATCHING cookies, not the whole jar, so a background XHR writing a cookie mid-clear cannot skew it (a jar-wide difference could under-report, report a successful clear as 0, or even go negative).

    An EMPTY filter is REFUSED at the CLI, in the page verb and again on the RPC server. Playwright's own clearCookies() reads no-filter as "clear everything", which aimed at a live logged-in session is an irreversible silent logout, so clearing everything has to be asked for with --all (and cannot be combined with a narrowing flag). distill replays a clear faithfully rather than leaving it as a TODO, since the filter carries no secrets, except a filterless entry, which becomes an annotated TODO rather than generated wipe-everything code. The bot-block recovery recipe is documented in the skill and deliberately NOT shipped as a vendor-specific preset flag; that boundary decision, and the empty-filter refusal, are recorded in docs/adr/0016.

    New exports from @webhands/core: CookieFilter and validateCookieFilter.

  • b88e30b: An explicit --dom escape for controls hidden behind styled labels, --timeout on the acting verbs, and click now reports HOW it clicked.

    Real sites hide radios and checkboxes behind styled labels constantly. Playwright is right to refuse to act on what a human could not reach, so the verb waits out its timeout even though the control is functional. click already had a fallback; the problem was everything around it.

    • --dom on click, type, press, hover and select skips the actionability check and fires the event directly. Each verb documents exactly what it fires and how faithful that is: click dispatches a real click event (which still toggles a radio), select sets the value and fires change, type sets the value and fires input/change (no keystrokes, so masked inputs may differ), press fires keydown/keypress/keyup (handlers run, no text inserted), hover fires the pointer-enter events (no pointer position, so CSS :hover does not react). drag deliberately has NO --dom: a synthetic drag needs a DataTransfer that real drop targets often ignore, so it would fail quietly more often than it worked.
    • It stays OPT-IN and the sibling defaults are unchanged (real auto-wait, fail-loud), because an invisible control a human cannot reach is sometimes a honeypot, and because the siblings' synthetic forms are approximations of a different action rather than "the same action without the check".
    • --timeout <ms> on the acting verbs bounds the actionability wait. It changes how long you WAIT, never what is performed. Measured motivation: on a hidden element, type, select and hover each burned Playwright's full 30s default with no escape and no way to shorten it.
    • click returns (and the CLI reports) via: "click" | "dispatch", so the fallback is no longer silent: an agent can tell a real actionability-checked click from an event dispatched at something a user could not have clicked.

    Also fixes a latent bug in the existing fallback. It called dispatchEvent('click', {timeout}), but Playwright's signature is dispatchEvent(type, eventInit?, options?), so the bound was passed as an event FIELD and the 30s default silently applied. A locator matching nothing hung for 30.4s in the dispatch while the code comment above it promised fast failure. The bound now goes in the options argument, with a test pinning the failure under 10s so the 30s regression cannot return.

    Note the resulting asymmetry, which is documented in the --timeout help and the skill: click's default actionability budget is about 1s (short, so its fallback is reachable), while the other verbs keep Playwright's 30s. Pass --timeout when clicking something that becomes ready after a request.

    ActionOptions gains dom and timeoutMs, carried over the session RPC in both directions. WebHandsPage.click returns ClickResult instead of void: additive for callers (an ignored return value), but breaking for anyone IMPLEMENTING WebHandsPage/Transport outside this repo, and for a Hand that wraps click, since the method must now return a result. New exports: ClickResult, ClickVia. The decision and its reasoning are recorded in docs/adr/0015.

  • b88e30b: BREAKING for anyone relying on serve advertising a cdpEndpoint by default: pass --expose-cdp to restore it.

    serve no longer opens a remote-debugging port unless asked. CDP exposure was hard-coded ON in the serve wiring, so every launch appended --remote-debugging-port=0: a code-execution surface on your logged-in page AND an automation tell that anti-bot WAFs look for, added even under --stealth, where it partly undoes what Patchright is there for. It is now the opt-in --expose-cdp (default OFF), and serve reports a warning in its output envelope (not just stderr, so an agent caller sees it too) when --stealth --expose-cdp are combined. cdpEndpoint is present in the serve output only when exposed. The eval harness, which needs the shared driving surface for its Playwright-baseline leg, now asks for it explicitly.

    setup-profile accepts the same browser-selection flags as launch/serve (--stealth, --use-system-browser, --proxy, --no-viewport), where it used to reject them as "Unknown flag". That mattered because setup-profile is what CREATES the profile directory, and a Chromium user-data dir is written by a specific browser build: setting a profile up with the bundled Chromium and then driving it with serve --use-system-browser chrome points a different build at the same dir, which is both a fingerprint discrepancy and a real risk of Chrome migrating or refusing a profile another build wrote. core's setupProfile takes the policy as a launch option, and the verb now reports the systemBrowser/stealth it set the profile up with (and carries that selection into its suggested next command).

    README's stealth guidance is corrected with measured evidence: against Akamai Bot Manager, a Playwright-launched browser was blocked identically with default launch, --stealth, --stealth --use-system-browser chrome and a fresh profile, while attaching to a user-started Chrome drove a nine-screen authenticated flow. --stealth is one tell removed, not the anti-bot answer.

  • b88e30b: --proxy now works with --real-chrome, routing the spawned browser's traffic and DNS through a SOCKS proxy via Chromium's own --proxy-server (plus the --host-resolver-rules no-leak catch-all for socks5h). It reuses the existing parseSocksProxy, so socks5h vs socks5 means the same thing in both modes and a malformed value is still the typed InvalidProxyError rather than an unproxied browser. This matters more in this mode than on the launch path: --real-chrome exists to present a real browser to an anti-bot system, the exit IP is part of what such a system weighs, and a proxy is the only lever on it there. serve no longer warns that --proxy is inapplicable under --real-chrome, because it is not.

    Three limits, taken from Chromium's net/docs/proxy.md and verified locally rather than assumed:

    • Credentials are REFUSED in this mode, with a new typed ProxyAuthUnsupportedError. Chrome "supports no authentication methods for SOCKSv5" and "will not use any credentials embedded in the proxy settings", so passing a user:pass@ URL through would fail every request, and stripping it silently would leave the user believing their traffic was authenticated and proxied. The fix command points at terminating auth locally (ssh -D 1080) and the message redacts the password. The default Playwright launch path still accepts credentials, since Playwright answers the auth challenge itself.
    • Loopback is never proxied (Chromium's implicit bypass list), so "all traffic" means all non-loopback traffic. Left as-is, because nobody wants their local dev server proxied, and documented instead.
    • It fails closed: an unreachable proxy means the navigation does not complete, rather than quietly leaving via the real IP.

    Tested end to end against a real local SOCKS5 server that observes the browser's connections, so the claim asserted is "the page traffic went through the proxy", not merely "the flag was forwarded". A new pure buildRealChromeArgs makes the flag construction testable without spawning a browser.

  • b88e30b: script accepts the module-style file a human actually writes. The loader compiled the source by wrapping it in an expression position (return (<source>)), so a trailing semicolon failed with Unexpected token ';' and a top-level const CONFIG = {...} before the function failed with Unexpected token 'const': messages that name the punctuation and never the cause. ...

Read more

@webhands/core@0.8.0

Choose a tag to compare

Minor Changes

  • b88e30b: Add cookies clear: remove a NAMED SUBSET of the active session's cookies and report how many actually went.

    Motivated by a live Akamai-protected site that flipped from serving to blocking part way through an authenticated session. That verdict is persisted in a handful of named cookies (_abck, bm_sz, bm_sv, ak_bmsc), so every dynamic endpoint 403s until they are gone, while the login lives in the site's own separate session cookies. Clearing just those names restores access and keeps the human signed in. Until now the cookies verb did export/import only, so the only way to do that was to drop into script.

    New seam verb clearCookies(filter) (names / domain / path / all, exact-match strings, every field must match) available in-process and over the session RPC, plus webhands cookies clear [--name <n>]… [--domain <d>] [--path <p>] [--all] where --name is repeatable. It returns the number of cookies the browser actually removed, computed as a before/after difference, so cleared: 0 visibly means "nothing matched" rather than silently passing.

    The count is over the MATCHING cookies, not the whole jar, so a background XHR writing a cookie mid-clear cannot skew it (a jar-wide difference could under-report, report a successful clear as 0, or even go negative).

    An EMPTY filter is REFUSED at the CLI, in the page verb and again on the RPC server. Playwright's own clearCookies() reads no-filter as "clear everything", which aimed at a live logged-in session is an irreversible silent logout, so clearing everything has to be asked for with --all (and cannot be combined with a narrowing flag). distill replays a clear faithfully rather than leaving it as a TODO, since the filter carries no secrets, except a filterless entry, which becomes an annotated TODO rather than generated wipe-everything code. The bot-block recovery recipe is documented in the skill and deliberately NOT shipped as a vendor-specific preset flag; that boundary decision, and the empty-filter refusal, are recorded in docs/adr/0016.

    New exports from @webhands/core: CookieFilter and validateCookieFilter.

  • b88e30b: An explicit --dom escape for controls hidden behind styled labels, --timeout on the acting verbs, and click now reports HOW it clicked.

    Real sites hide radios and checkboxes behind styled labels constantly. Playwright is right to refuse to act on what a human could not reach, so the verb waits out its timeout even though the control is functional. click already had a fallback; the problem was everything around it.

    • --dom on click, type, press, hover and select skips the actionability check and fires the event directly. Each verb documents exactly what it fires and how faithful that is: click dispatches a real click event (which still toggles a radio), select sets the value and fires change, type sets the value and fires input/change (no keystrokes, so masked inputs may differ), press fires keydown/keypress/keyup (handlers run, no text inserted), hover fires the pointer-enter events (no pointer position, so CSS :hover does not react). drag deliberately has NO --dom: a synthetic drag needs a DataTransfer that real drop targets often ignore, so it would fail quietly more often than it worked.
    • It stays OPT-IN and the sibling defaults are unchanged (real auto-wait, fail-loud), because an invisible control a human cannot reach is sometimes a honeypot, and because the siblings' synthetic forms are approximations of a different action rather than "the same action without the check".
    • --timeout <ms> on the acting verbs bounds the actionability wait. It changes how long you WAIT, never what is performed. Measured motivation: on a hidden element, type, select and hover each burned Playwright's full 30s default with no escape and no way to shorten it.
    • click returns (and the CLI reports) via: "click" | "dispatch", so the fallback is no longer silent: an agent can tell a real actionability-checked click from an event dispatched at something a user could not have clicked.

    Also fixes a latent bug in the existing fallback. It called dispatchEvent('click', {timeout}), but Playwright's signature is dispatchEvent(type, eventInit?, options?), so the bound was passed as an event FIELD and the 30s default silently applied. A locator matching nothing hung for 30.4s in the dispatch while the code comment above it promised fast failure. The bound now goes in the options argument, with a test pinning the failure under 10s so the 30s regression cannot return.

    Note the resulting asymmetry, which is documented in the --timeout help and the skill: click's default actionability budget is about 1s (short, so its fallback is reachable), while the other verbs keep Playwright's 30s. Pass --timeout when clicking something that becomes ready after a request.

    ActionOptions gains dom and timeoutMs, carried over the session RPC in both directions. WebHandsPage.click returns ClickResult instead of void: additive for callers (an ignored return value), but breaking for anyone IMPLEMENTING WebHandsPage/Transport outside this repo, and for a Hand that wraps click, since the method must now return a result. New exports: ClickResult, ClickVia. The decision and its reasoning are recorded in docs/adr/0015.

  • b88e30b: BREAKING for anyone relying on serve advertising a cdpEndpoint by default: pass --expose-cdp to restore it.

    serve no longer opens a remote-debugging port unless asked. CDP exposure was hard-coded ON in the serve wiring, so every launch appended --remote-debugging-port=0: a code-execution surface on your logged-in page AND an automation tell that anti-bot WAFs look for, added even under --stealth, where it partly undoes what Patchright is there for. It is now the opt-in --expose-cdp (default OFF), and serve reports a warning in its output envelope (not just stderr, so an agent caller sees it too) when --stealth --expose-cdp are combined. cdpEndpoint is present in the serve output only when exposed. The eval harness, which needs the shared driving surface for its Playwright-baseline leg, now asks for it explicitly.

    setup-profile accepts the same browser-selection flags as launch/serve (--stealth, --use-system-browser, --proxy, --no-viewport), where it used to reject them as "Unknown flag". That mattered because setup-profile is what CREATES the profile directory, and a Chromium user-data dir is written by a specific browser build: setting a profile up with the bundled Chromium and then driving it with serve --use-system-browser chrome points a different build at the same dir, which is both a fingerprint discrepancy and a real risk of Chrome migrating or refusing a profile another build wrote. core's setupProfile takes the policy as a launch option, and the verb now reports the systemBrowser/stealth it set the profile up with (and carries that selection into its suggested next command).

    README's stealth guidance is corrected with measured evidence: against Akamai Bot Manager, a Playwright-launched browser was blocked identically with default launch, --stealth, --stealth --use-system-browser chrome and a fresh profile, while attaching to a user-started Chrome drove a nine-screen authenticated flow. --stealth is one tell removed, not the anti-bot answer.

  • b88e30b: --proxy now works with --real-chrome, routing the spawned browser's traffic and DNS through a SOCKS proxy via Chromium's own --proxy-server (plus the --host-resolver-rules no-leak catch-all for socks5h). It reuses the existing parseSocksProxy, so socks5h vs socks5 means the same thing in both modes and a malformed value is still the typed InvalidProxyError rather than an unproxied browser. This matters more in this mode than on the launch path: --real-chrome exists to present a real browser to an anti-bot system, the exit IP is part of what such a system weighs, and a proxy is the only lever on it there. serve no longer warns that --proxy is inapplicable under --real-chrome, because it is not.

    Three limits, taken from Chromium's net/docs/proxy.md and verified locally rather than assumed:

    • Credentials are REFUSED in this mode, with a new typed ProxyAuthUnsupportedError. Chrome "supports no authentication methods for SOCKSv5" and "will not use any credentials embedded in the proxy settings", so passing a user:pass@ URL through would fail every request, and stripping it silently would leave the user believing their traffic was authenticated and proxied. The fix command points at terminating auth locally (ssh -D 1080) and the message redacts the password. The default Playwright launch path still accepts credentials, since Playwright answers the auth challenge itself.
    • Loopback is never proxied (Chromium's implicit bypass list), so "all traffic" means all non-loopback traffic. Left as-is, because nobody wants their local dev server proxied, and documented instead.
    • It fails closed: an unreachable proxy means the navigation does not complete, rather than quietly leaving via the real IP.

    Tested end to end against a real local SOCKS5 server that observes the browser's connections, so the claim asserted is "the page traffic went through the proxy", not merely "the flag was forwarded". A new pure buildRealChromeArgs makes the flag construction testable without spawning a browser.

  • b88e30b: script accepts the module-style file a human actually writes. The loader compiled the source by wrapping it in an expression position (return (<source>)), so a trailing semicolon failed with Unexpected token ';' and a top-level const CONFIG = {...} before the function failed with Unexpected token 'const': messages that name the punctuation and never the cause. ...

Read more