Stay productive. Focus uses TypeSafe's Jev System One classification model (docs) through OpenRouter's Decisions API to decide whether a domain is distracting, and blocks it.
- On each top-frame navigation (
webNavigation.onBeforeNavigate+tabs.onUpdatedfallback for SPAs), the background service worker extracts the domain. - Allowlist / blocklist overrides win first. Fresh cached verdicts (7 days) win second.
- Otherwise it calls Jev via OpenRouter:
POST https://openrouter.ai/api/alpha/decisions
Authorization: Bearer <OPENROUTER_API_KEY>
Content-Type: application/json{
"model": "typesafe/jev-1.13",
"state": "Domain: youtube.com\nFull URL: https://www.youtube.com/\nPage title: YouTube\nUser goal: stay productive…",
"questions": {
"site_category": {
"type": "choice",
"instructions": "Is visiting this website productive for focused work, or is it distracting?",
"criteria": {
"productive": "Work, study, documentation, coding, email, calendar, maps, …",
"distracting": "Social media feeds, short-video doomscrolling, entertainment, gaming, gambling, …"
}
}
}
}- If
answers.site_category.choice === "distracting"andP(distracting) >= threshold(default 0.6), the tab is redirected toblocked.html, which shows the probability, confidence, and model. - Without an API key (or if the API fails), a small offline heuristic list keeps the extension useful until you configure a key.
This mirrors the TypeSafe POST /v1/systemone shape ({ state, model, questions }
with a Choice classification primitive); OpenRouter just serves it at
/api/alpha/decisions with OpenRouter auth.
| File | Purpose |
|---|---|
manifest.json |
Shared MV3 manifest (Chrome, Firefox, and Safari), permissions, options page |
background.js |
Navigation interception, Jev classification, caching, blocking |
popup.html / popup.js |
Toolbar popup: current-site verdict, enable toggle, allow/block, re-check |
options.html / options.js |
Extension dialog: OpenRouter API key, model, threshold, lists, cache |
blocked.html / blocked.js |
Block interstitial |
styles.css |
Shared dark theme |
icons/ |
Extension icons |
scripts/package-safari.sh |
Generates the macOS Safari host app and Xcode project |
- Open
chrome://extensions, enable Developer mode. - Load unpacked → select this
Focusfolder. - Click the 🎯 toolbar icon → Settings & API key.
- Paste your OpenRouter key (from
openrouter.ai → Keys) → Save settings → Test API key. - Browse. Distracting domains redirect to the Focus blocked page.
- Open
about:debugging#/runtime/this-firefox→ Load Temporary Add-on → pickmanifest.json. - Open Add-ons → Focus → Preferences to set the API key.
The Safari version uses the same JavaScript, HTML, and CSS as Chrome, so its
filtering, settings, cache, popup, and blocked page behave identically. The
packaging script removes Safari's unsupported options_ui.open_in_tab preference
from its staged manifest only.
- In Safari, open Settings → Advanced and enable Show features for web developers.
- Open Settings → Developer, enable Allow unsigned extensions, then click
Add Temporary Extension… and select this
Focusfolder. - Open Settings → Extensions → Focus and allow access to All Websites.
- Open the Focus toolbar popup and configure the API key as above.
Safari removes temporary extensions after 24 hours or when Safari quits.
Install the full Xcode app, then run:
BUNDLE_IDENTIFIER=com.example.Focus ./scripts/package-safari.shThis creates a macOS host app and Xcode project under build/safari. Open the
generated project, select your development team if signing is required, and run
the Focus (macOS) scheme. Enable Focus and grant All Websites access in
Safari's extension settings.
Use a unique reverse-DNS BUNDLE_IDENTIFIER ending in .Focus that belongs to
your Apple developer account. The script refuses to overwrite an existing output
directory so it does not discard Xcode signing configuration. To generate a fresh
project, remove the old output directory and run the command again.
The script stages only extension runtime files and asks Apple's
safari-web-extension-packager (or its older safari-web-extension-converter name)
to generate the native wrapper. The wrapper is generated rather than committed so
all browsers continue to share one behavior implementation.
- Model:
typesafe/jev-1.13(pinned, default) or~typesafe/jev-latest(auto-update). - Threshold: minimum
P(distracting)to block. Lower = stricter. - Allowlist / blocklist: one domain per line,
www.stripped, subdomains match. - Cache: per-domain verdicts are cached for 7 days.
- The API key is stored in
chrome.storage.syncand sent only tohttps://openrouter.ai. Safari implements this storage area locally but does not sync it between devices. - Each new domain sends
{ domain, full URL, page title }as Jevstate. No page content is sent.