Releases: jkudish/jev-browser
Release list
0.5.0
Install:
npx -y @jkudish/jev-browser
-
Bot-protection interstitials are detected and named instead of endured. When a Cloudflare challenge ("Just a moment...", "Performing security verification") or hard block is on the page, the run stops with status
blockedinstead of spending further Jev calls or steps on the wall, and every result carriesbot_protection: { provider, kind, evidence, guidance }(kindischallengeorblock). A detected challenge first gets a short bounded window (8s, within the run budget) to clear itself; one that auto-passes lets the run proceed normally, andblockedis declared only when the challenge was still there after the window. A hard block stops immediately. A wall that only becomes visible on the final page (after the last action, or under a done/goal judgment) gets the same settle window and flips the outcome toblockedwhen it persists, so a confidentdonecan no longer wrap a challenge page; when it clears in the window the outcome stands, no wall is annotated from the pre-settle page, and the reported final page is the content that actually painted. When the budget is too exhausted to run the settle window at all, the deadline outcome (timeout) keeps precedence and the wall is annotated as evidence instead of claimed as the outcome. Detection from the page (the only evidence that can stop a run, DOM-only and self-sufficient, so the run probes and the detector agree on everything that stops) requires a Cloudflare-signature or branded challenge title (generic wordings like "Verify you are human" need a Cloudflare-brand body marker on the page) or three distinct body markers including at least one Cloudflare-brand phrase, with overlapping phrases counted once;from_pagein the library result means the DOM evidence met that threshold on its own, never a header plus an incidental body phrase. Cloudflare'scf-mitigated: challenge|blockedresponse header is recorded as evidence and annotation but never corroborates stopping, because it is last-seen state and a challenge that auto-passed still answers with the header. The guidance text states the operative facts:cf_clearanceis bound to the browser and IP that earned it, so seeded cookies do not clear challenges; the reliable paths are running from the browser session that earned the clearance (reuse its page) or the site's API. -
Cookie seeding (PR #9, remediated):
cookiesonnavigate()and the MCP tool, and--cookie-file name=@pathon the CLI, add cookies to the run's own browser context before the first navigation so a run can start behind a login (password fill is the other way; the agent never types into password fields). Cookie values are credentials of the same rank as the password value. Delivery is strictly by reference: the CLI reads the value from a file (there is deliberately no--cookie name=valueflag, which would put the token in argv, shell history, and the process list), and the MCP tool takescookie_file(a one-shot handoff file inside~/.jev-browser/handoff, validated and consumed exactly likepassword_file) orcookie_env(onlyJEV_COOKIE_*names, rejected before lookup); no argument ever carries a cookie value. Each value is validated like a password (empty, over 4096 bytes, under 4 characters, control characters, and normalization-collapsing values are refused before the run starts) and is redacted from every model-facing state, trace, error, URL, console event, and result payload through the same machinery, one redactor covering the password plus every cookie value, applied longest first so a value that is a prefix of another still redacts. Cookie runs refuse an injectedpage(context.addCookieswould mutate a caller-owned context) and refuse video recording, both before any timer, listener, or browser is armed, and the final screenshot is suppressed from run start (the first rendered page can already reflect a value into pixels). Attributes are no longer weakened in transit: with nodomainsupplied the cookie is host-only on the start URL's exact host (a dotless domain through Playwright'saddCookies, verified host-only in Chromium; only a caller-supplied leading dot opts into subdomain matching),pathdefaults to/,httpOnlyto true (page scripts cannot read the value; set false only when the site's own JavaScript must),sameSitetoLax, andsecureto true on https start URLs, forced true for__Host-/__Secure-names and forsameSite: "None";__Host-cookies with an explicit domain or a non-root path are rejected because the browser would drop them anyway, and an explicitsecure: falsecannot strip the forced flag. No error path, from argument parsing through cookie resolution to handoff validation, ever quotes a cookie value. -
CLI argument errors now print a one-line message and exit 1 instead of an uncaught stack trace (from PR #9).
-
Typing degradation is now visible instead of silent (#2). Every result, success or error, carries
degraded,warnings,typing_provider, andtyping_model; each warning is{ code, step, message, provider, model, finish_reason?, fallback? }with codestyping_fallback_no_provider,typing_generator_empty,typing_generator_error, andtyping_configuration_error. Warning messages are short (capped at 200 chars) and never contain raw provider response bodies. A degraded run is not a tool error and does not change the CLI exit code when a fallback completed; the CLI prints one concise stderr line, andnavigate()itself stays side-effect free (no console output). -
JEV_BROWSER_TYPE_PROVIDERis now strict: it selects only that provider, and an unknown value, or a missing or malformed key for the named provider, is a configuration error raised before any timer, listener, or browser is armed. Previously it only reordered auto-detection candidates and silently fell through to another provider. Runs withallow_typing: falseignore typing configuration entirely, and auto-detection without the variable keeps its order (openai, openrouter, anthropic, google). -
Typing fallbacks split by field kind (#2). A failed, empty, or provider-less generation on an ordinary
type_eNfield types nothing and records a step action error ("typing generator failed; nothing was typed") plus a warning, instead of filling the field with task-keyword soup.search_eNfields keep the search-tuned keyword heuristic fallback, now reported withfallback: "keyword-heuristic"and the underlyingtyping_generator_emptyortyping_generator_errorcode. -
OpenRouter typing calls disable reasoning (
providerOptions: { openrouter: { reasoning: { enabled: false } } }) and raise the output budget from 48 to 256 tokens, because reasoning models can spend the whole cap on hidden reasoning tokens and return an empty message. The providerOptions namespace is sent only when the resolved typing provider is openrouter; other providers keep the 48-token cap. No retry machinery was added. -
JEV_BROWSER_TYPE_BASE_URLon its own still selects a compatible endpoint, and when combined withJEV_BROWSER_TYPE_PROVIDERit becomes the named provider's endpoint; it is validated as an absolute http(s) URL up front. -
Node.js 22 or newer is required (was 20): the locked
ai@7dependency declares>=22. -
JEV_BROWSER_TYPE_BASE_URLis honored by every named provider (openai, openrouter, anthropic, google), not only openrouter, so private gateways work uniformly.@ai-sdk/googleis upgraded to 2.x: 1.x emitted model spec v1, whichai@7rejects at runtime, so the google typing provider can now actually generate (previously every google generation failed). -
README corrections: the OpenRouter typing default model is
google/gemini-2.5-flash-lite(the table said a stale id), theJEV_BROWSER_TYPE_MODELexample is a plain model id passed through unchanged (noopenrouter:prefix), and the typing-cost claim now says one call per typed field instead of once or twice per task. -
Form controls now resolve an accessible name (AccName 1.2 precedence:
aria-labelledbyrefs,aria-label, all associated native labels via the.labelsAPI, then placeholder and title). Plain<label for>login forms no longer drop out of the action space, which previously made their inputs invisible and untypeable (#1). -
A successful type action now records a truthful outcome (
typed into "Username"; no visible page change) instead ofno visible change, so the stuck watcher no longer misreads a filled field as a no-op. Repeat recovery now keys off machine state (a repeated action whose only effect was the fill, or a genuine no-op) instead of matching the display string, and no longer terminates on adonealternate with negligible probability. -
Native
<select>dropdowns are now selected by DOM option index instead of by label string, with each option's live index carried alongside its label. Blank options are filtered from the model's list without shifting the indexes used for selection, and a scrubbed or truncated label can never become the selection key. -
Password fill for logins: the model never sees or types the password. Delivery is CLI
--password-file <path|->(stdin supported), MCPpassword_file(one-shot file directly inside~/.jev-browser/handoff, validated: owner, mode 0600, single link, no symlinks, unlinked at run start), or MCPpassword_env(onlyJEV_PASSWORD_*names, rejected before lookup). Fills are bound to an exact trusted origin (JEV_BROWSER_PASSWORD_ORIGIN,--password-origin), and the type/origin check and the fill run as a single in-page task on the resolved element, so no navigation or DOM swap can interleave. Fills never submit. Echoes of the value (raw, percent-encoded, form-encoded, HTML-entity, markdown-escaped, whitespace- and zero-width-normalized, aria-YAML-escaped) are redacted from every model-facing state, trace, error, URL, and payload, including values a page reflects into labels, attributes, con...
0.4.1
Fixes the Chromium download on install. The postinstall script in 0.4.0 sat outside the scripts object in package.json, so it never ran and the first launch failed with: Executable doesn't exist. It now runs on install and downloads Chromium once. Set JEV_BROWSER_SKIP_BROWSER_DOWNLOAD=1 to skip it.
Install: npx -y @jkudish/jev-browser
v0.4.0
Recording support (--record out.webm, per-step t_ms, video_path in results), DOM-stability settle that cuts per-step time roughly 2-3x on JS-heavy sites, 4s action timeouts, OpenRouter typing default moved to google/gemini-2.5-flash-lite (openai/gpt-5.6-luna returns empty output through OpenRouter), README demo GIF. Install: npx -y @jkudish/jev-browser
v0.3.0
Cloudflare Workers AI transport (typesafe/jev via /ai/run) and Vercel AI Gateway transport (typesafe-ai/jev via the AI SDK evaluate API, confidence included); AI SDK bumped to v7 for the typing path with no behavior change. Resolution order: TypeSafe direct, OpenRouter, Cloudflare, Vercel; JEV_PROVIDER forces any. Install: npx -y @jkudish/jev-browser
v0.2.0
OpenRouter support: one OPENROUTER_API_KEY can power the entire package, typing included. JEV_PROVIDER forces typesafe or openrouter; jev-latest maps to typesafe/jev-1.13 on OpenRouter. Results now report the transport used (jev_provider, resolved model). Install: npx -y @jkudish/jev-browser