npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

okati

v0.1.0

Published

Act on the web from a CLI, so an agent never has to open a browser.

Readme

okati

Act on the web from a CLI, so an agent never has to open a browser.

Browsers corrupt context. The moment an agent opens DevTools it burns thousands of tokens on DOM it does not care about. okati takes that job: it drives the browser, and hands back a few hundred tokens of JSON.

okati amazon.com                      what can I do here
okati amazon.com search guitars       do it
okati amazon.com lucky "guitar cable" search, take the first real result, cart it

Every command answers with one ok field and an exit code that agrees with it. ok means the command did what it says on the tin, not that the process survived: an add that cannot be confirmed against the cart exits non-zero, because believing that one is how you buy two.

Install

Needs node 20 or newer.

curl -fsSL https://okati.sh/install | sh

That is the whole setup. There is no account to open, no key to paste, and nothing to configure: the first site command enrols this machine with the okati service on its own, under a brand new account, and the only secret this machine will ever hold is one okati token in ~/.okati/credentials.json, mode 600. Nothing on this machine is signed in to any site until you sign in.

okati amazon.com                          what can I do here
okati amazon.com search "guitar capo"     do it
okati wikipedia.org search hypertext      any site, the same way

The installer stages into a temp directory and proves the download loads before it writes the shim. An install that claims success and then dies on its first import reads as "okati is broken" rather than "okati is absent", which is worse.

Any site, learned on first use

okati has no list of supported sites. The first time anyone runs a subcommand on a site, it runs in a real browser; okati then proves the same request replays from a plain process and reads the same rows, and keeps that route only if it does. The next run, on any machine, is one HTTP request.

Measured 2026-09-18 on wikipedia.org from an empty home directory:

$ okati wikipedia.org
  enrolling this machine with okati
  discovered wikipedia.org in 4s

wikipedia.org  Wikipedia

commands:
  search <query>   find things

$ okati wikipedia.org search hypertext
{"ok":true,"command":"search","kind":"page","title":"Hypertext", ..., "via":"browser","ms":14800}

$ okati wikipedia.org search hypertext
{"ok":true,"command":"search","kind":"page","title":"Hypertext", ..., "via":"replay","ms":475}

The two answers are the same JSON. The only fields that differ are via and ms, and they are always reported, because a slow first run that looks like a hang is worse than one that says why it was slow.

What one machine learns is what every machine gets. The site's menu is fetched from the service before a browser ever opens, so the second person on a site skips discovery, and menus are ranked by how often each subcommand is run across everyone. Menus are paged and searchable (--page=2, --find <word>), never cut to a fixed number.

The rules, from docs/vision.md:

  • The browser is the truth. A replay that returns a non-200, a robot wall, or rows that do not match what the browser showed is a fallback reason on the response (fellBackTo), never a result. The browser answers instead.
  • Nothing personal enters the shared index. Requests are stored as names and placeholders, never values. One machine's own results, cookies and selectors never leave it.
  • Verified means reproduced. A route is shared only after a replay from this process returned what the browser returned.

Counting

okati.sh shows three numbers, live: installs, commands run, sites indexed. Every command reports its domain, subcommand, path (replay, browser or local), whether it succeeded and how long it took, through a detached child process that costs the command nothing. Never the arguments, never who. One device counts a subcommand at most once every ten seconds, so a loop cannot spin the public number. OKATI_NO_USAGE=1 turns reporting off.

Two logins, named apart

Everything below exists because these two were both called "login" until 2026-09-16, and a brand new install was therefore already signed in to an Amazon account nobody on that machine had ever typed a password for.

| command | what it is | |---|---| | okati auth login <invite> | join an EXISTING okati account from this machine (a fresh install makes its own account with no code) | | okati auth status | which okati account and device this machine is | | okati auth logout | forget okati on this machine; site logins untouched | | okati amazon.com login <email> | sign THE ACCOUNT in to amazon.com | | okati amazon.com login status | is the account still signed in to amazon.com | | okati amazon.com logout | sign the account out of amazon.com, everywhere |

okati login, okati logout and okati whoami still mean the first three. They are the original spellings and nothing that used them has to change.

The Amazon login is account-scoped, not device-scoped

This is the single most surprising thing about okati and it is deliberate:

  • The Amazon session lives in a browser context the service owns, keyed to your okati account. It is never copied onto a laptop.
  • Every device enrolled on one account shares it. Enrol a second machine and it is already signed in, with nothing copied across. An earlier version of this file told you to scp ~/.okati/db/amazon.com.json between machines. That was wrong, and it is the reason the service exists.
  • A new okati account starts blank. Contexts are keyed by account, so a new account cannot inherit another one's login. There is a test that asserts two accounts get two different contexts.
  • okati amazon.com logout therefore signs out every device, including ones that are switched off. It does three things, and reports each separately:
    1. tells Amazon to end the session, in the browser, so Amazon's own token dies
    2. destroys the context holding the cookies, so the next browser opens blank
    3. moves the account's auth epoch, which is how the sign-out reaches the other machines

The epoch is the part worth understanding. Each install keeps a cookie jar so it can read amazon.com over plain HTTP in about a second with no browser (see The direct path). That jar is a copy, and a copy outlives the login it came from. So the service counts sign-ins and sign-outs per account, each jar is stamped with the count it was warmed under, and a jar stamped with any other value is deleted unread on that machine's next command. Nothing has to reach the other laptop; it finds out by asking. Signing in moves the epoch too, which is the case that matters if you sign in as a different Amazon account.

If okati cannot tell which epoch is current — the service is unreachable — the direct path stands down rather than using the last jar it saved. Not knowing which login is current is not permission to use the previous one.

Where the vendor went

okati's browser leg runs on Browserbase. That is deliberate and it has not changed. What changed on 2026-09-16 is who holds the credential.

Until then every install held BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID and built its own CDP url with the key pasted into the query string. That is a development setup with an install script on it. An end user has no reason to open a vendor account, and shipping the key inside the package only means anyone who installs it can read it back out.

So the key lives in the service and never moves. What the client gets instead is the connect url the vendor mints per session:

wss://connect.<region>.browserbase.com/?signingKey=<JWE>

Measured against the live API rather than read off a docs page, that signing key is a capability for one already-created browser and nothing else. Used as an account credential it answers 401 on create-session, list-sessions and create-context, and the session it names is COMPLETED the moment it is released. It cannot open a second browser, enumerate the account, or spend money.

That asymmetry is why there is no websocket relay here. Relaying CDP would put a 2.5MB Amazon page through a Worker for no security gained.

Two powers moved to the service along with the key. Reclaiming the plan's only session slot used to happen in the client, which meant every laptop needed permission to list and kill sessions across the whole account; nothing on a laptop can do that now. And browser contexts are created and mapped server-side, which is what makes the second machine work.

service/ is the whole thing: one Worker, one KV namespace, and three secrets set with wrangler secret put. npm run deploy bundles the client into it and ships it. The build refuses to produce a bundle containing anything that matches a credential shape or any value from the building machine's .env.

How discovery works

Unknown domain means one page load in a real browser, about 20 seconds, and we keep only what the page volunteers about itself:

  • its forms, which declare their own method, action and parameters
  • the JSON endpoints it calls on its own
  • its navigation, which is where cart, orders and login actually live

No crawling, no clicking, no classifying the site. Verbs are recognised by shape, not by name: a type=search input or a lone text box is a search box on every site in the world, whether the field is called k, _nkw or search_query.

Nothing deeper is discovered until someone asks for it by name. Discovery is per verb, just in time, and the result is cached so the next agent skips it.

Commands carry their own next moves

Every response ends with the two or three things you can do next, built from what was actually on that page, so an agent never has to wonder whether there was a button it could not see. Results are numbered so follow-ups stay cheap.

Filters are flags on the command you already ran. They stay out of the default response and appear only when asked:

$ okati target.com search --help
filters on this page:
  --facetedValue=vdy4o   Women
  --facetedValue=q7xor   Men
  --facetedValue=5zlb1   Boy

Facet links carry the machine value in the query string and the human meaning in their own text, so we pair the two. The agent copies a flag without needing to understand the site.

What it looks like

$ okati ebay.com
ebay.com  Electronics, Cars, Fashion, Collectibles & More | eBay

verbs:
  orders           your past orders
  wishlist         your saved items
  account          your account settings
  deals            current deals
  search <query>   find things

$ okati ebay.com search "fender stratocaster"
{"command":"search","count":15,"results":[
  {"id":"377490623786","title":"Fender Stratocaster 1974 Red Solid Body...","price":"$2,000.00","url":"..."},
  ...]}

Verified working, cold, on amazon.com, ebay.com and etsy.com. Three different stacks, zero per-site code.

Result rows are cleaned hard on the way out: redirect wrappers unwrapped, tracking parameters stripped, ad and chrome rows dropped, real product ids pulled from the path. A raw Amazon result URL is about 300 tokens on its own, which would defeat the entire point.

Finding a control

Two rungs, both free:

  1. A selector okati already learned for this site.
  2. What the web standardises. autocomplete="username", autocomplete="current-password", autocomplete="one-time-code", input types, form-control shape. Sites maintain these so password managers work, which makes them the most portable signal available.

There used to be a third rung that asked a model, through the vendor SDK plus an OPENAI_API_KEY, to find an element and return a selector. Both of those are credentials an end user does not have and should not need, so the rung could only ever fire for a developer. It was removed on 2026-09-16, and it took a whole dependency out of every install with it.

Judgment on the browser path

The rung above is not coming back, and this is not it. That rung asked a model to FIND an element and hand back a selector, which meant the model drove the page. This one cannot: TypeSafe's Jev returns only a yes/no probability, a choice from a set the caller defines, or a position on a caller-defined rubric. It returns no strings at all. Code still gathers every candidate, code still owns the loop, and Jev only ranks rows code already has.

It fires only on the browser fallback, at three places where okati's querySelector heuristics are a genuine guess rather than a fact:

  1. Which control is the real add-to-cart. The ranker scores controls by how much their name looks like the real thing, which is exactly the signal that cannot separate submit.add-to-cart from nav-assist-add-to-cart. When the top two tie, the shortlist goes over as a choice. A clear winner is never asked about.
  2. Whether the add actually happened. cart add used to call itself confirmed when the ASIN appeared in the cart or when the cart merely had any rows at all — which is true of every cart that was not empty beforehand. The ASIN match is still proof and still costs nothing; only the guess is judged.
  3. Whether a scraped row is coherent. Selectors slip silently and the row still comes back, just wrong — on 2026-09-17 okati returned a brand name where the product title belongs. Each row gets a score, all in one request. A flagged row is named in lowConfidence, never dropped.

Three properties hold, and the tests grade all three:

  • Optional. The key is read from a local vault at call time. An install with no vault entry resolves nothing, asks nothing, and behaves exactly as it does today. No end user needs a TypeSafe account, the same way none needs a Browserbase one.
  • Fail-open. No key, a timeout, a 429, a malformed answer and a low-confidence answer all mean the heuristic wins. A judgment layer that can break a browser command is worse than none.
  • Visible. Every judgment is reported in judged, next to fellBackTo, even when it agreed with the code — otherwise a layer that quietly died would look identical to one that was working.

Measured cost and latency, and what the API does and does not tell you about rate limits, are in docs/jev-envelope.md.

One live browser, no open or close

A CLI exits after every command, so the browser cannot live in the okati process. It lives on the service's side instead. The first command asks the service for a browser, which creates a keepAlive session or hands back the account's existing one; every later command reconnects to that same browser; the backend's timeout reaps it when nobody comes back. The agent never opens or closes anything.

That is what makes a code prompt survive between two commands:

okati amazon.com login <email>             -> needs_code, the page stays open
okati amazon.com login code 481920         -> lands on that same page
okati amazon.com search "guitar capo"       -> same browser, already signed in

Scope right now is deliberately one user, one agent, one site at a time.

A keepAlive session outliving its process is the point, and also the hazard: on a plan that allows one concurrent session, a command that crashes leaves the slot held for the full TTL and every later command fails with a 429. The service handles that: when the backend refuses a new session it releases a stranded one and retries. It will not release a session with a sign-in parked on it, because a code prompt cannot be recreated and the code Amazon already sent would land nowhere.

okati amazon.com session           which browser, and whether its replay is ready
okati amazon.com session release   give the slot back now

session only reads. Asking "what browser am I on" must not open one.

Every session is recorded. The receipt is read from the backend's own replay artifact, never from the flag okati set when it asked for recording -- a flag is a request, a replay with pages in it is a receipt. It reports three states, not two: available, pending while the browser is still open, and none. The endpoint most examples use, GET /sessions/:id/recording, now answers 404 on every session with "the rrweb-based Session Replay API is being deprecated", and reading that 404 as an absence is a confident, wrong answer about a session that is being recorded the whole time.

amazon.com is the one site okati is for

Everything above this reads a page the way any site would be read. amazon.com gets its own reader, because the whole point of a shopping CLI is the fields a shopper actually decides on, and Amazon publishes every one of them in data-asin and data-component-type attributes that no site-agnostic heuristic would ever look at.

okati amazon.com search <query> [--sort=..] [--prime] [--min=..] [--max=..] [--page=..] [--limit=..]
okati amazon.com product <ASIN>
okati amazon.com offers <ASIN>
okati amazon.com reviews <ASIN> [--stars=1..5|positive|critical] [--sort=recent]
okati amazon.com orders [--period=last30|months-3|year-2026|..]
okati amazon.com cart
okati amazon.com cart add <n|ASIN>
okati amazon.com cart remove <n|ASIN>

--help on any of them lists its flags. --generic forces the site-agnostic path back on, which is the honest way to compare the two.

$ okati amazon.com search "usb c hub" --limit=2
{"command":"search","query":"usb c hub","count":2,"sponsoredSkipped":4,
 "url":"https://www.amazon.com/dp/{asin}",
 "results":[
  {"n":1,"asin":"B0BQLLB61B","title":"Anker USB C Hub, 5-in-1 USBC to HDMI Splitter with 4K Display",
   "price":19.99,"was":24.99,"rating":4.4,"reviews":18800,"boughtPastMonth":10000,
   "primeDelivery":"Tomorrow 4 AM - 8 AM","delivery":"Mon, Sep 21","badge":"Overall Pick"},
  ...]}

Four things in that response are the whole argument for a per-site reader:

Sponsored rows are ads, not results. The generic extractor returned them mixed in, including one whose id was the literal string clk, from sponsored-ads.amazon.com/clk. They are now dropped and counted, so an agent can see how much of the page was paid for without reading any of it.

An ASIN is an address. Every row carries one, and the page for it is https://www.amazon.com/dp/<asin>. That is why no row carries a URL: a raw Amazon result URL is about 300 tokens, the template is said once, and cart add <ASIN> works on an item that was never in a search.

The delivery date is two dates. Amazon writes one cell that reads "Join Prime to get FREE delivery Tomorrow 4 AM - 8 AM ... Or Non-members get FREE delivery Mon, Sep 21". Those are different promises to different people, and collapsing them into one is how a tool promises next-day delivery that is not on offer. delivery is what this account gets; primeDelivery is what Prime would get, and it is absent when Amazon did not offer one.

Numbers are numbers. 19.99, not "$19.99". 18800, not "(18.8K)".

offers is worth a line of its own: it reads All Offers Display, so it finds the used and resale sellers the buybox hides. On the hub above the buybox says $19.99 and offers finds the same item at $13.98.

The parsers are tested, and the browser is not involved

page.evaluate returns raw strings exactly as Amazon wrote them, and every piece of interpretation happens back in node as a pure function. That is what makes npm test possible at all: a parser that needs a browser to run is a parser nobody can test. Every fixture in tests/parse.test.mjs was copied off a live page through .probe/, never invented.

npm test

.probe/ is the harness that found every selector in here. It attaches to the session okati already owns rather than starting its own, because the plan allows exactly one.

Amazon sign-in

amazon.com has explicit steps instead of the generic heuristics, and every screen Amazon can show maps to one status:

| status | meaning | |---|---| | ok | signed in, verified by visiting a page that needs a login | | needs_code | Amazon wants a code from SMS, email or an authenticator. Run login code <code> | | code_rejected | Amazon refused the code. Send the newest one | | needs_approval | Amazon sent an approval prompt to a device. Approve, then login status | | failed + no_account | no Amazon account for that email | | failed + wrong_password | wrong password | | captcha | a captcha Browserbase did not clear. Run login again | | not_waiting | login code was run but the browser is not on a code prompt |

And signing out, where partial is the interesting one:

| status | meaning | |---|---| | ok | Amazon was told, the context is destroyed, the epoch moved | | partial | okati destroyed what it holds, but could not tell Amazon. Amazon's own session token stays alive until it expires. Exits non-zero |

partial exists because a logout that quietly half-worked is the failure this whole section is about. The fields say exactly which half: siteSignedOut, contextDestroyed, contextOrphaned, browserReleased, cachedCookiesDropped.

Login and cart

There are two ways in, one for a person and one for an agent, and neither one puts a credential in argv:

okati amazon.com login [email protected]                      # prompts, echo off
<password source> | okati amazon.com login [email protected] --password-stdin
<authenticator>   | okati amazon.com login code --code-stdin

ps shows every argument of every process to every user on the machine for as long as the command runs, and shell history keeps it afterwards. So the prompt writes to stderr with the echo off, and an agent pipes from a password manager. A one-time code is a credential too, which is why it has the same door. Passing a password as an argument still works so that an existing script does not break, and it now prints a warning saying why not to.

It fills whatever field is in front of it, submits, and looks again, so one-step and multi-step forms (email, then password on the next page) work the same way. Credentials are never stored, on this machine or on the service. The signed-in session is kept in a browser context the service owns, so later commands are already signed in.

A one-time code is a command, never a browser. okati never opens a browser for you, and never asks an agent for a code it cannot get. When a site asks for a TOTP after the password, okati parks the live session and tells the agent exactly what to run next:

$ okati amazon.com login [email protected] --password-stdin < /dev/stdin
{"command":"login","status":"needs_code","method":"authenticator app",
 "next":["okati amazon.com login code <code>"]}

$ okati amazon.com login code 481920
{"command":"login","status":"ok","next":["okati amazon.com cart"]}

Sites that do not ask for a code simply return status: ok from the first command.

How okati knows it is signed in: it does not read the login page. Reading a login page means guessing from wording, which breaks on the first site that is not in English. Instead okati navigates to a page that requires a login, one of the orders, account or wishlist commands it already discovered, and sees whether the site serves it or bounces to a sign-in. That is the whole test, and it works on any site in any language.

$ okati amazon.com login status
{"command":"login status","signedIn":false,
 "landedOn":"https://www.amazon.com/ap/signin?...",
 "next":["okati amazon.com login <email>"]}

Structure on the login page is demoted to a hint about why we are not in yet:

| status | what it means | |---|---| | ok | verified by visiting a page that needs a login | | needs_code | a code field is waiting (autocomplete="one-time-code", or one-character boxes) | | needs_approval | parked on an auth page with nothing to type: push approval, magic link or a security key. Approve it, then run login status | | failed | the site rejected the credentials | | unknown | okati could not finish and will not pretend to know why. Returns where it stopped |

unknown is deliberate. Inventing a confident answer is worse than admitting the tool got lost.

The parked session stays alive between the two commands, so the code lands on the same page that asked for it. It expires after 15 minutes. Whoever runs okati supplies the code from wherever it lives, email, SMS or an authenticator; that is the harness's job, not okati's.

Captchas are Browserbase's job and sessions run with captcha solving on.

okati amazon.com search "guitar capo"
okati amazon.com cart add 1        # 1 is the numbered result
okati amazon.com cart

cart add verifies by outcome, not by click. It clicks, waits for the redirect to settle, then reads the cart and only reports ok if the item is really there. Otherwise it says unconfirmed and tells you what it landed on. Anything that reports success without checking will eventually lie, and it lied to us three times while this was being built.

The direct path

Once you are signed in, okati stops using the browser for most work. The traffic was measured: every read is one HTML document GET, and Amazon authenticates it with cookies alone -- there is no JWT. So okati replays those requests straight from the CLI process and keeps Browserbase for creating the session and for anything the direct path cannot do.

Measured, one process per command, direct versus OKATI_DIRECT=0:

| command | direct | browser | | --- | --- | --- | | search | 1.9s | 8.1s | | cart | 1.3s | 3.1s | | cart add | 6.1s | 10.2s | | cart remove | 2.8s | 5.3s | | lucky | 7.0s | 17.4s |

It falls back on its own -- a non-200, a robot check, a dead session, a success predicate that does not match, or a cart that does not confirm a mutation -- and says so in fellBackTo so a slow run is never silent. OKATI_DIRECT=0 turns it off entirely.

The request contracts live in schemas/amazon.com/v1/, one file per subcommand, carrying the method, path, parameter classification, required headers, CSRF dependency, success predicate and fallback triggers. Cookie names only, never values. docs/amazon-protocol.md is why.

Status

Verified live against amazon.com, signed in to a real account:

  • cold discovery, the command listing, and the generic path on any site
  • login and login code, end to end through Amazon's two-step verification. This used to say login had never run against a real account. It has now.
  • search with --sort, --prime, --min/--max, --limit
  • product, offers, reviews
  • cart, cart add, cart remove, as a full round trip that left the cart exactly as it found it
  • lucky, end to end, against the real cart
  • the direct path for all nine operations, re-verified after a deliberate sign-out and a fresh sign-in, with the cart restored to its starting state
  • the whole product path from a clean machine: curl | sh into an empty home with BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID absent from the process, okati login, a real Amazon sign-in including the authenticator step, then search, cart add and cart remove, ending with the cart identical
  • a second and a third clean install on the same account, each already signed in with nothing copied to them
  • session recording, read as a real replay on a session the service created

Not verified: orders against an account that has orders. The empty case is verified, including reading the period list off Amazon's own filter rather than hardcoding one that goes stale every January, but the account used for this work has never placed an order, so the code that parses an order card has never seen one.

Not built: checkout, repair when a stored command stops working. Checkout is deliberately absent.

Design notes

Failure is the discovery trigger. Nothing is crawled in advance and there are no background jobs. A verb only gets learned when someone asks for it, and only gets re-learned when it breaks. Maintaining a map of the internet is a losing game; repairing one route on demand is not.

Never classify a site. amazon.com has no obligation to resemble target.com. Everything here reads what a page says about itself.

okati is for acting, not reading. Exa, Parallel, Firecrawl and friends have the read side covered and do it better than we could. The bet is the act side.