Releases: wighawag/webhands
Release list
webhands@0.7.0
Minor Changes
-
c4c2312: Ship the
use-webhandsskill inside the published package, and stop flooding the skill list.webhands skills addadvertised installing a skill the package did not contain. The tarball carried no skill at all (fileswasdist/src/README.md/LICENSE, and the skill was authored at the monorepo root, outside the published package), and the CLI passed nosyncoption toincur, so the command had no hand-authored skill to install in the first place.README.mdhas 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.mdINSIDE the package and is listed infiles, sonpm packcarries it. Top-levelskills/, notdist/skills/: it is hand-authored source, anddist/is tsc output wiped on every build. skills addandskills listresolve it from the MODULE's own location, not the caller's cwd, so a barenpx webhands skills addfrom any directory installs it with no clone present. (incurdefaultssync()to a walked-up package root butlist()toprocess.cwd(); passing an explicitcwdstops the two commands disagreeing about which skills exist.)- Generated per-verb skills are collapsed into ONE
webhandscommand reference (depth: 0) instead of 26. They were near-duplicates of a referenceuse-webhandsalready carries in full, and they buried it.skills addnow 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 addclears its destination before writing, so running it after linking replaces the symlink with a mutable copy. Onlyuse-webhandscan 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>. - The skill now lives at
webhands@0.6.0
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 thecookiesverb did export/import only, so the only way to do that was to drop intoscript.New seam verb
clearCookies(filter)(names/domain/path/all, exact-match strings, every field must match) available in-process and over the session RPC, pluswebhands cookies clear [--name <n>]… [--domain <d>] [--path <p>] [--all]where--nameis repeatable. It returns the number of cookies the browser actually removed, computed as a before/after difference, socleared: 0visibly 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).distillreplays 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 indocs/adr/0016.New exports from
@webhands/core:CookieFilterandvalidateCookieFilter. -
b88e30b: An explicit
--domescape for controls hidden behind styled labels,--timeouton the acting verbs, andclicknow 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.
clickalready had a fallback; the problem was everything around it.--domonclick,type,press,hoverandselectskips the actionability check and fires the event directly. Each verb documents exactly what it fires and how faithful that is:clickdispatches a real click event (which still toggles a radio),selectsets the value and fireschange,typesets the value and firesinput/change(no keystrokes, so masked inputs may differ),pressfireskeydown/keypress/keyup(handlers run, no text inserted),hoverfires the pointer-enter events (no pointer position, so CSS:hoverdoes not react).dragdeliberately has NO--dom: a synthetic drag needs aDataTransferthat 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,selectandhovereach burned Playwright's full 30s default with no escape and no way to shorten it.clickreturns (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 isdispatchEvent(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
--timeouthelp 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--timeoutwhen clicking something that becomes ready after a request.ActionOptionsgainsdomandtimeoutMs, carried over the session RPC in both directions.WebHandsPage.clickreturnsClickResultinstead ofvoid: additive for callers (an ignored return value), but breaking for anyone IMPLEMENTINGWebHandsPage/Transportoutside this repo, and for aHandthat wrapsclick, since the method must now return a result. New exports:ClickResult,ClickVia. The decision and its reasoning are recorded indocs/adr/0015. -
b88e30b: BREAKING for anyone relying on
serveadvertising acdpEndpointby default: pass--expose-cdpto restore it.serveno longer opens a remote-debugging port unless asked. CDP exposure was hard-coded ON in theservewiring, 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), andservereports a warning in its output envelope (not just stderr, so an agent caller sees it too) when--stealth --expose-cdpare combined.cdpEndpointis present in theserveoutput only when exposed. The eval harness, which needs the shared driving surface for its Playwright-baseline leg, now asks for it explicitly.setup-profileaccepts the same browser-selection flags aslaunch/serve(--stealth,--use-system-browser,--proxy,--no-viewport), where it used to reject them as "Unknown flag". That mattered becausesetup-profileis 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 withserve --use-system-browser chromepoints 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'ssetupProfiletakes the policy as alaunchoption, and the verb now reports thesystemBrowser/stealthit 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 chromeand a fresh profile, while attaching to a user-started Chrome drove a nine-screen authenticated flow.--stealthis one tell removed, not the anti-bot answer. -
b88e30b:
--proxynow 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-rulesno-leak catch-all forsocks5h). It reuses the existingparseSocksProxy, sosocks5hvssocks5means the same thing in both modes and a malformed value is still the typedInvalidProxyErrorrather than an unproxied browser. This matters more in this mode than on the launch path:--real-chromeexists 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.serveno longer warns that--proxyis inapplicable under--real-chrome, because it is not.Three limits, taken from Chromium's
net/docs/proxy.mdand 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 auser: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
buildRealChromeArgsmakes the flag construction testable without spawning a browser. - Credentials are REFUSED in this mode, with a new typed
-
b88e30b:
scriptaccepts 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 withUnexpected token ';'and a top-levelconst CONFIG = {...}before the function failed withUnexpected token 'const': messages that name the punctuation and never the cause. ...
@webhands/core@0.8.0
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 thecookiesverb did export/import only, so the only way to do that was to drop intoscript.New seam verb
clearCookies(filter)(names/domain/path/all, exact-match strings, every field must match) available in-process and over the session RPC, pluswebhands cookies clear [--name <n>]… [--domain <d>] [--path <p>] [--all]where--nameis repeatable. It returns the number of cookies the browser actually removed, computed as a before/after difference, socleared: 0visibly 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).distillreplays 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 indocs/adr/0016.New exports from
@webhands/core:CookieFilterandvalidateCookieFilter. -
b88e30b: An explicit
--domescape for controls hidden behind styled labels,--timeouton the acting verbs, andclicknow 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.
clickalready had a fallback; the problem was everything around it.--domonclick,type,press,hoverandselectskips the actionability check and fires the event directly. Each verb documents exactly what it fires and how faithful that is:clickdispatches a real click event (which still toggles a radio),selectsets the value and fireschange,typesets the value and firesinput/change(no keystrokes, so masked inputs may differ),pressfireskeydown/keypress/keyup(handlers run, no text inserted),hoverfires the pointer-enter events (no pointer position, so CSS:hoverdoes not react).dragdeliberately has NO--dom: a synthetic drag needs aDataTransferthat 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,selectandhovereach burned Playwright's full 30s default with no escape and no way to shorten it.clickreturns (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 isdispatchEvent(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
--timeouthelp 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--timeoutwhen clicking something that becomes ready after a request.ActionOptionsgainsdomandtimeoutMs, carried over the session RPC in both directions.WebHandsPage.clickreturnsClickResultinstead ofvoid: additive for callers (an ignored return value), but breaking for anyone IMPLEMENTINGWebHandsPage/Transportoutside this repo, and for aHandthat wrapsclick, since the method must now return a result. New exports:ClickResult,ClickVia. The decision and its reasoning are recorded indocs/adr/0015. -
b88e30b: BREAKING for anyone relying on
serveadvertising acdpEndpointby default: pass--expose-cdpto restore it.serveno longer opens a remote-debugging port unless asked. CDP exposure was hard-coded ON in theservewiring, 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), andservereports a warning in its output envelope (not just stderr, so an agent caller sees it too) when--stealth --expose-cdpare combined.cdpEndpointis present in theserveoutput only when exposed. The eval harness, which needs the shared driving surface for its Playwright-baseline leg, now asks for it explicitly.setup-profileaccepts the same browser-selection flags aslaunch/serve(--stealth,--use-system-browser,--proxy,--no-viewport), where it used to reject them as "Unknown flag". That mattered becausesetup-profileis 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 withserve --use-system-browser chromepoints 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'ssetupProfiletakes the policy as alaunchoption, and the verb now reports thesystemBrowser/stealthit 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 chromeand a fresh profile, while attaching to a user-started Chrome drove a nine-screen authenticated flow.--stealthis one tell removed, not the anti-bot answer. -
b88e30b:
--proxynow 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-rulesno-leak catch-all forsocks5h). It reuses the existingparseSocksProxy, sosocks5hvssocks5means the same thing in both modes and a malformed value is still the typedInvalidProxyErrorrather than an unproxied browser. This matters more in this mode than on the launch path:--real-chromeexists 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.serveno longer warns that--proxyis inapplicable under--real-chrome, because it is not.Three limits, taken from Chromium's
net/docs/proxy.mdand 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 auser: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
buildRealChromeArgsmakes the flag construction testable without spawning a browser. - Credentials are REFUSED in this mode, with a new typed
-
b88e30b:
scriptaccepts 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 withUnexpected token ';'and a top-levelconst CONFIG = {...}before the function failed withUnexpected token 'const': messages that name the punctuation and never the cause. ...