pi-jev-browser
Browser automation tools for pi: Jev (TypeSafe System One) chooses each action in a bounded DOM loop, with a deterministic selector layer for precise work.
Package details
Install pi-jev-browser from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-jev-browser- Package
pi-jev-browser- Version
0.1.0- Published
- Sep 19, 2026
- Downloads
- not available
- Author
- laihenyi
- License
- Apache-2.0
- Types
- extension
- Size
- 303.9 KB
- Dependencies
- 2 dependencies · 3 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
Pi Jev Browser
Isolated Playwright browser tools for pi, with Jev (TypeSafe System One) choosing each browser action over a structured DOM observation instead of a screenshot-per-step loop.
This is a port of cline/plugins/plugins/jev-browser
to the pi extension API. The observation pipeline, Jev prompt, run statuses, and
safety contract are carried over; the host plumbing, the decision transport, and
the text helper changed. See Differences from the Cline plugin.
Install
From npm
pi install npm:pi-jev-browser
Pi installs the package under ~/.pi/agent/npm/ and reads this manifest:
"pi": { "extensions": ["./index.ts"] }
Installing pulls in playwright and @typesafe-ai/sdk, and Playwright's own
install step downloads Chromium. That download is roughly 150 MB, so the first
install takes a moment. typebox, @earendil-works/pi-ai, and
@earendil-works/pi-coding-agent are peerDependencies because pi provides
them as virtual modules, so nothing is bundled twice.
From a git checkout or a local directory
pi install git:github.com/laihenyi/pi-Jev-browser # latest main
pi install git:github.com/laihenyi/pi-Jev-browser@v0.1.0 # a pinned tag
pi install /absolute/path/to/pi-Jev-browser # a working copy
Auto-discovery in ~/.pi/agent/extensions
A checkout placed at ~/.pi/agent/extensions/pi-Jev-browser/ is discovered
automatically, which is how this repository is used during development. Restart
pi (or /reload) after changing it, and install its dependencies once:
cd ~/.pi/agent/extensions/pi-Jev-browser
npm install
node node_modules/playwright/cli.js install chromium # or: npm run install-browser
Chromium setup also happens automatically on the first jev_run, bounded to
two minutes. A failed setup is reported by the tool that needed it, and a later
call retries. On Linux, system browser libraries remain an administrator-managed
prerequisite; the extension never runs sudo.
Provide a TypeSafe API key from console.typesafe.ai in the environment or the
config file:
export TYPESAFE_API_KEY=... # or typesafe.apiKey in pi-jev-browser.config.json
chmod 600 ~/.pi/agent/pi-jev-browser.config.json
jev_run resolves this before Chromium starts, so a missing key fails
immediately with the file and variable to set instead of a browser-shaped error.
Nothing else is required: field text for typed inputs is generated with the pi model the session is already using.
The Jev loop
jev_run starts or reuses a browser, captures a before screenshot, and then
runs a bounded decision loop. Each evaluation sends one TypeSafe System One
request:
POST https://api.typesafe.ai/v1/systemone
Authorization: Bearer <TYPESAFE_API_KEY>
{ "model": "jev-latest", "state": "<page observation + recent actions>", "questions": { "action": { ... } } }
Jev answers a single choice question whose criteria are the concrete operations
offered for the current page: one entry per visible action target
(CLICK:<id>, TYPE_TEXT:<id>, SELECT:<id>), plus SCROLL_UP, SCROLL_DOWN,
WAIT, BLOCKED, REVIEW, and DONE. Comparing every concrete action against
scrolling, waiting, and stopping in one pass is what keeps the loop cheap; there
is no screenshot in the decision path.
Jev sees indexed visible action targets, selected form options (including offscreen selections), and summaries of controls above and below the viewport. It does not receive screenshots. The agent receives screenshots to verify outcomes.
Jev chooses actions but cannot generate arbitrary text. When it chooses
TYPE_TEXT, the extension calls the active pi model through
ctx.modelRegistry.complete() to produce the field value. Set
textHelper.model (or PI_JEV_BROWSER_TEXT_MODEL) to "provider/modelId" to pin a
different model. Its token usage is reported back to pi on the tool result.
The helper is a one-line extraction, so it uses a 4,096-token budget (reasoning
models share that budget with their thinking tokens) and two transport retries,
and it tolerates a code fence or surrounding prose around the JSON object. The
value itself is still validated strictly: exactly one non-empty text string of
at most 2,000 characters, otherwise nothing is typed and the run reports
text_helper_invalid_output.
Example tool sequence
jev_run({ "url": "https://en.wikipedia.org", "goal": "Find and open the article about Ada Lovelace. Stop when the article is visible.", "maxSteps": 20 })- Verify the returned final screenshot (or read
finalScreenshotPathif the image is not displayed). jev_stop({})to release the browser and finalize the video.
jev_run starts a browser automatically, captures the initial screen, runs
Jev, and returns a final image plus both screenshot paths and page states.
Further calls reuse the browser; omit url to continue or provide it to navigate
first. Launch options (headless, recordVideo, showCursor,
showClickIndicators) apply when creating a browser. The browser stays available
for follow-up runs until stopped.
Supported automatic operations: click, replace text, native dropdown selection,
page scroll, and short loading waits. Runs default to 20 steps (maximum 60) and
have a 100-second cancellation deadline. There is no default probability cutoff.
Optional minProbability gates Jev's calibrated probability for the selected
choice; it is not provider confidence. A browser operation already in flight can
take up to its bounded Playwright timeout to settle after cancellation. Browser
mutations are not retried.
Results include status, steps, elapsedMs, and a JSONL tracePath. Statuses
are done_unverified, blocked, needs_review, uncertain, step_limit,
evaluation_limit, or interrupted. A failed run also reports failure
(stage, category, detail) and an errorsLogPath:
failure.category |
Meaning |
|---|---|
configuration |
Missing or unreadable configuration; the message names the file and the variable to set. Reported before Chromium starts. |
text_helper_invalid_output |
The active pi model did not return a usable {"text": ...} value. Nothing was typed. |
cancelled |
The run was aborted or hit its deadline. |
navigation_context |
The document changed while it was being read. |
timeout |
A bounded browser or model timeout elapsed. |
document_not_ready |
The page never produced a readable document. |
unexpected_error |
Anything else, usually a provider error. |
stopReason explains why the loop stopped, independent of the model's status:
stopReason |
Meaning |
|---|---|
model_done |
Jev reported DONE; the status is done_unverified because a claim is not proof. |
model_blocked |
Jev reported BLOCKED: no supported action can progress. |
model_review |
Jev reported REVIEW: the next step needs sensitive data, submits something, or crosses a safety barrier such as a CAPTCHA. |
min_probability |
The selected choice fell below the requested minProbability. |
step_limit / evaluation_limit |
The action or evaluation budget ran out. |
repeated_action |
The same action on the same target executed three times without advancing. |
scroll_oscillation |
SCROLL_UP and SCROLL_DOWN alternated repeatedly, a two-cycle that is not exploration. |
stale_observations |
Four consecutive observations were invalidated before an action could run. |
no_progress |
Three actions produced no observable change. |
text_unavailable |
The pi text helper declined to produce a value, so nothing was typed. |
cancelled / error |
The run was aborted, or it failed; see failure. |
failure.detail is a bounded summary (error name, HTTP status when known)
and errors.log in the run directory holds the full error, including a stack.
Provider errors can quote the outgoing request body, so the full text is written
only to that local file and never returned to the model. An attempted action in
a failed run might already have taken effect: inspect before continuing. Traces
record decisions (including terminal and rejected decisions), stale observations,
action attempts, completion, and the final result. Provider confidence is
recorded separately when supplied. Traces omit generated field text but may
contain page labels.
The loop retains the last ten actions, entered text, and observed progress across
runs of the same goal in one browser session. Text stays in memory and is not
written to traces. Three non-wait actions without observable progress stop the
run. Stale decisions are re-evaluated within a budget of twice maxSteps;
executed mutations are never retried.
The loop retains observed DOM nodes and checks page semantics, node identity, and
occlusion before acting. Frames, shadow DOM, canvas controls, nested scrolling,
uploads, and arbitrary keyboard widgets are outside this DOM loop; use
jev_actions where appropriate. Model context is capped at 200 action targets
and 6,000 visible text characters, plus 50 selected options and up to 50 offscreen
control labels in each direction. This can omit controls on dense pages.
Page text and visible field values are sent to TypeSafe api.typesafe.ai, and the
same content is sent to your pi model provider when a field value must be
generated. Password and file fields are excluded, but other sensitive content is
not automatically redacted. Jev is instructed to return REVIEW before
consequential actions; this is model guidance, not a deterministic security
boundary. Delegate only narrowly scoped tasks suitable for autonomous browser
interaction. The agent must handle any review and independently verify
done_unverified.
This integration avoids an LLM reasoning round trip and a screenshot per browser step. Actual end-to-end speed and live-model reliability have not been benchmarked.
Features
- screenshots returned as image tool results, so the agent can verify outcomes
- batched
click,double_click,scroll,type,wait,keypress,drag,move,navigate,back,forward,reload, andscreenshotactions - browser console, page-error, failed-request, navigation, download, and security logs
- per-run PNG artifacts and optional WebM video recording with a visible agent cursor and animated click pulses
- a tokenized live screenshot/log viewer bound to
127.0.0.1 - session isolation, blocked downloads, blocked service workers, no inherited host environment, disabled extensions and browser file-system access
- bounded browser launch, navigation, video-finalization, and cleanup timeouts so unavailable apps fail with their underlying error instead of hanging
- automatic visible-browser fallback on macOS when headless Chromium cannot start
- a live footer status line while a browser run streams decisions
- prompt guidelines covering prompt injection, sensitive data, and consequential actions
This is a browser harness, not unrestricted control of the host desktop.
Tools
jev_run— startup, before/after screenshots, and the Jev loop.jev_actions— manual actions without Jev; returns an updated screenshot.jev_extract— deterministic read of text, table rows, links, or an attribute, with no model call.
Element targets instead of coordinates
A manual click, fill, or select can address an element instead of a pixel, so the same call keeps working after a layout change:
[
{ "type": "click", "target": { "role": "link", "name": "部落格" } },
{ "type": "fill", "target": { "role": "textbox", "name": "Email" }, "value": "a@b.c" },
{ "type": "select", "target": { "role": "combobox", "name": "Plan" }, "value": "pro" },
{ "type": "click", "target": { "selector": "#submit" } },
{ "type": "click", "target": { "role": "button", "name": "Buy", "nth": 1 } }
]
A target accepts role, name (needs role), text, selector, and nth.
Matching waits up to five seconds for the element to exist, so a batch can click a
link and act on the page it navigates to. A target that matches nothing fails with
No element matched … instead of clicking whatever happens to be under the cursor.
x/y coordinates remain available for canvas-like surfaces.
A coordinate click is a raw event, so the browser will route it into an iframe even
though the automatic loop never sees frame content. When a click or drag lands on a
frame, the result carries a warnings entry and a matching security entry appears
in jev_logs, naming the frame origin and flagging known anti-bot providers
(reCAPTCHA, hCaptcha, Cloudflare challenge):
A click at (160, 165) landed inside a frame from https://www.google.com [reCAPTCHA].
The automatic Jev loop never interacts with frame content; this was a raw coordinate click.
This is a record, not a block: it makes coordinate clicks auditable and tells the agent to stop clicking there. Keep it in mind when deciding whether to auto-approve browser tools.
Tabs
Sites open new tabs constantly (target="_blank" on login and outbound links). Pi
Browser never switches the observed page silently:
popups: "stay"(default) keeps the run on the original page, records atabentry injev_logs, and returns a warning naming the new URL.popups: "follow"adopts the new tab as the observed page, and says so.jev_actionscan then move deliberately withactivate_tab/close_tab, andjev_state.activePageIndexreports which tab is being observed.
If the observed tab closes, the run falls back to another open tab instead of keeping a reference to a closed page.
jev_logs— captured console, error, request, navigation, tab, and security logs.jev_stream— start, inspect, or stop the localhost live viewer.jev_state— active state, tabs, URL, title, viewport, start time.jev_stop— close the browser, stop the stream, finalize the video.
Every tool runs sequentially (executionMode: "sequential") because they mutate
one shared browser, and one browser operation at a time is allowed per pi session.
Configuration
No configuration is required. Defaults: all HTTP/HTTPS origins, headless
Chromium, a 1280 × 720 viewport, WebM recording enabled, cursor and click
indicators enabled, live viewer disabled until jev_stream starts it, and
artifacts under ~/.pi/agent/pi-jev-browser/.
Copy pi-jev-browser.config.example.json to ~/.pi/agent/pi-jev-browser.config.json to
change defaults, or point PI_JEV_BROWSER_CONFIG at another file. Restrict
allowedOrigins with * wildcards (for example https://*.example.com) for
authenticated or sensitive workflows.
{
"allowedOrigins": ["https://*.example.com"],
"denyOrigins": [],
"requireConfirmation": [],
"headless": true,
"recordVideo": true,
"viewport": { "width": 1280, "height": 720 },
"stream": { "enabled": false, "intervalMs": 1000 },
"popups": "stay",
"profile": "session",
"typesafe": { "apiKey": "", "baseUrl": "https://api.typesafe.ai", "model": "jev-latest" },
"textHelper": { "model": "" }
}
Per-site policy
denyOriginsis checked beforeallowedOrigins, so a deny rule always wins and blocks navigation before Chromium starts.requireConfirmationlists origins that need an explicit yes in a pi dialog beforejev_runorjev_actionstouches them. Without a dialog-capable UI (for example a headless run) the call fails instead of proceeding silently.
Persistent profile and human-in-the-loop login
profile decides how much browser state survives between runs:
"session"(default) — one profile per pi session, under~/.pi/agent/pi-jev-browser/profiles/<session id>. Log in once in the visible window and later runs in the same pi session reuse the cookies. Two pi sessions never fight over one Chrome profile."shared"— one profile for every session, atprofileDir. Convenient, but two concurrent pi sessions cannot use it at the same time."off"— a clean browser every run, the pre-profile behavior.
Only cookies with an expiry persist; Chromium drops session cookies on exit by
design. Note that a persistent profile also keeps localStorage and IndexedDB, so
point profileDir at something you are comfortable reusing.
Environment overrides: TYPESAFE_API_KEY, TYPESAFE_BASE_URL,
TYPESAFE_DEFAULT_MODEL, PI_JEV_BROWSER_TEXT_MODEL, PI_JEV_BROWSER_CONFIG.
Credentials are read on every run, are never passed to Chromium, and never appear
in tool results. The config path is resolved once when pi starts, so restart pi
after changing PI_JEV_BROWSER_CONFIG.
Safety
Keep pi's tool approval enabled and do not auto-approve browser tools for authenticated, financial, medical, destructive, or otherwise high-impact workflows. The extension enforces navigation allowlists, but the user must still approve consequential actions at the point of risk. Treat webpage text and screenshots as untrusted input, not user instructions.
The browser starts isolated. Jev uses DOM observations for its action loop; the agent receives screenshots to independently verify the outcome.
Development
npm install
npm run install-browser
npm run check # tsc --noEmit
npm test # node --test --experimental-strip-types
npm run benchmark # capability suite; see benchmarks/README.md
Tests use local HTML, a local stub for the TypeSafe System One endpoint, and
mocked pi models. They make no paid model calls. Browser tests require installed
Chromium and permission to launch it; set PI_JEV_BROWSER_TEST_BROWSER to use a
specific executable.
Returned page evidence
A jev_run result carries the page state captured when the loop stopped: the
JSON summary (status, stopReason, failure, steps, tracePath,
errorsLogPath, finalPageUrl, finalPageTitle), a readable block with the
final URL, title and visible page text, and the before/after screenshots. The
text block is what a model without image input uses to verify the outcome, so
the loop returns it instead of forcing a screenshot read. The full record of
every run also lands in the trace's final result entry.
When a site blocks automated access
Google and similar sites rate-limit repeated automated visits and serve an
anti-bot verification page (google.com/sorry/). Pi Jev Browser does not solve or
bypass it: Jev returns blocked with stopReason: "model_blocked" on the first
observation, typically in under a second, and the guidelines tell the agent to
stop and ask you rather than retry.
Because a blocked or needs_review run leaves the browser open, you can clear
the verification yourself:
- Keep
headless: falseso the window is visible. - Let the run return
blocked(the browser stays open; the agent must not calljev_stop). - Solve the verification in the visible window.
- Continue in the same pi session — browsers are keyed to
ctx.sessionManager.getSessionId(), so a new pi session starts a fresh browser.
Bursts of automated probing are what trigger the block; normal use rarely does. If a site blocks you persistently, prefer another source over escalating.
Manual smoke test
A three-page fixture (search box → results list → detail page) is enough to
exercise the full loop, including a real TYPE_TEXT through the pi model:
# serve a fixture, then in pi:
# jev_run url http://127.0.0.1:4599/ goal "Search for zebra and open the
# Zebra result. Stop when the page shows the Zebra heading."
Cancellation
pi transports a real AbortSignal to tool execution, so the extension combines
that signal with its own per-session controller. Pressing Escape aborts an active
run or action batch, and jev_stop cancels one explicitly. Run deadlines and
model timeouts are still enforced locally. A browser mutation already in flight
can finish before cancellation takes effect.
Chromium setup starts lazily on the first browser tool call rather than at
extension load, and session_shutdown closes any browser left open.
Differences from the Cline plugin
| Area | Cline plugin | Pi Jev Browser |
|---|---|---|
| Host API | plugin.setup(api) with JSON Schema inputSchema |
export default (pi) with TypeBox parameters |
| Safety rules | api.registerRule() |
promptSnippet + promptGuidelines (verified in the built system prompt) |
| Decision transport | @ai-sdk/gateway + experimental_evaluate |
direct POST https://api.typesafe.ai/v1/systemone |
| Decision model | typesafe-ai/jev via AI Gateway |
jev-latest (configurable) direct |
| Text helper | google/gemini-2.5-flash-lite via Gateway |
active pi model via ctx.modelRegistry.complete() |
| Credential | AI_GATEWAY_API_KEY |
TYPESAFE_API_KEY |
| Config | ~/.cline/plugins/cline-jev-browser.config.json |
~/.pi/agent/pi-jev-browser.config.json |
| Artifacts | ~/.cline/data/jev-browser/ |
~/.pi/agent/pi-jev-browser/ |
| Session identity | context.sessionId over JSON IPC |
ctx.sessionManager.getSessionId() |
| Cancellation | local controller only (signals were not transported) | pi AbortSignal + local controller |
| Failure reporting | one opaque interrupted message |
classified failure + errors.log with the full provider error |
| Setup | kicked off eagerly when the plugin loaded | lazily on first browser tool use |
| Tool names | jev_run, jev_actions, jev_state, jev_logs, jev_stream, jev_stop |
the same names, plus jev_extract for deterministic reads |
| Tool results | { result: [...] } interpreted by Cline |
{ content, details, usage }; nested model usage is reported to pi |
| Schemas | JSON Schema with additionalProperties: false |
TypeBox with additionalProperties: false, plus per-action field validation |
| Concurrency | host-defined | executionMode: "sequential" for all browser tools |
| Cleanup | plugin teardown | session_shutdown handler calls stopAll() |
Benchmarks
benchmarks/ is a repeatable capability suite in three tiers: local (offline
fixtures, no credential), model (real Jev decisions on local pages), and live
(third-party sites). Every scenario verifies its outcome against the fixture
server's request log, the run trace, or an independent read, so a run cannot pass
by claiming success.
npm run benchmark # local tier
npm run benchmark -- --suite=all # every tier, needs a credential and internet
Latest results: 14 of 14 executable scenarios pass, with one documented gap
reported as GAP rather than hidden — Jev's REVIEW rule stops at a real CAPTCHA
because the widget is in an unobservable iframe, but it clicks straight through an
ordinary DOM human-verification gate. benchmarks/README.md has the full table,
measured metrics, and the list of what the suite deliberately does not measure.
License
Apache-2.0. This project is a port of
cline/plugins/plugins/jev-browser,
which is Apache-2.0 licensed; the original LICENSE text is kept in this
repository and the port keeps that license. The observation pipeline, Jev prompt,
run statuses, and safety contract come from the original work; the pi host
plumbing, decision transport, text helper, selector layer, benchmark suite, and
subsequent fixes are new.