Counts real git clones of this repo — every first-time install.sh run does one internally, plus some people who clone without installing, so treat it as a close proxy, not a byte-exact install count.
```sh
curl -fsSL https://guesswork.findmalek.com/install.sh | bash
```
- **AI-ranked, not prefix-matched** — understands abbreviations and fuzzy
matches, not just commands that start with what you typed
- **Fish-style inline suggestions** — grey text after your cursor, accept
with →, works in emacs and vi keymaps
- **Four providers** — TypeSafe or Cloudflare Workers AI run the real Jev
model; Anthropic or Groq work as fallbacks if you'd rather not sign up for
either
- **Runs anywhere zsh does** — hooks `zle` directly, no raw escape codes, no
terminal-specific code path
- **Cheap** — ~$0.000081 per request on Jev; a few hundred requests a day for
a year lands around $10 total
- **No build step** — plain TypeScript, run directly by Node
**Jump to:** [Install](#install) · [Choose a provider](#choose-a-provider) ·
[Configuration](#configuration) · [How it works](#how-it-works) ·
[What it costs](#what-it-costs) · [CLI](#cli) · [Contributing](#contributing)
Prefix-matching history search only works if you remember how a command
*started*. You know you ran something with `--dir blog` in it last week, but
not whether it began with `amp`, `npm`, or `git`. guesswork closes that gap:
as you type, it asks an AI model which recent command you're most likely
retyping — abbreviations (`gst` → `git status`) and fuzzy matches, not just
literal prefixes — and shows the best guess inline, fish-style, grey text
after your cursor. Press → to accept it.
```
% gst▌ ⇢ git status [0.990] fuzzy mode: abbreviation match
% last 5 commits▌ ⇢ git log --oneline -5 [0.990] fuzzy mode: no entry starts with the input
```
The GIF above is that same moment recorded for real, not a mockup — the
transcript just adds the mode annotations. Regenerate either with
`demo/make-demo.sh` (needs `vhs`, `ffmpeg`, and a provider key; runs against a
fabricated history, never your real one).
There's also `demo/make-launch-demo.sh` / `demo/launch.tape`, a longer,
slower-paced cut of the same fabricated environment meant for a first-impression
launch video rather than a quick inline GIF — it covers the same prefix/fuzzy
suggestion beats plus a clean closing shot, and deliberately stops short of
the install one-liner and setup wizard (those need a real network fetch and
real provider credentials, so they're recorded by hand separately).
## Why not just fzf / prefix search?
Those are still the right tool for *searching* history on demand. guesswork
is for the moment you don't even think to search — a suggestion just
appears, the way fish's or zsh's `autosuggestions` plugin already does,
except the ranking understands intent instead of only comparing characters.
## Install
The one-liner above fetches [`install.sh`](./install.sh) straight from this
repo (the URL is just a friendlier alias — no separate service, nothing else
to trust), clones the repo to `~/.zsh/guesswork`, installs dependencies, and
launches an arrow-key setup wizard: pick a provider, paste credentials, it
makes one live request to confirm they work, then writes everything into
`~/.zshrc` for you. See [Choose a provider](#choose-a-provider) for what
"provider" means and what each one costs.
Both the installer and the wizard are idempotent — safe to re-run, and both
only touch the block between `# >>> guesswork >>>` / `# <<< guesswork <<<`
markers in your `.zshrc`. Re-running with a different provider switches
cleanly.
**Requirements:** zsh 5.9+, Node 22+ (runs the plugin's TypeScript directly,
no build step).
If you'd rather clone by hand first and see what you're running before it
runs:
```sh
git clone https://github.com/findmalek/guesswork.git ~/.zsh/guesswork
~/.zsh/guesswork/install.sh
```
### Non-interactive install
For scripting, CI, or an agent setting this up on your behalf, skip the
prompts by passing everything after `--`:
```sh
./install.sh -- --provider typesafe --api-key sk-...
./install.sh -- --provider cloudflare --account-id --api-token
./install.sh -- --provider anthropic --api-key sk-ant-...
./install.sh -- --provider groq --api-key gsk_...
```
Run `./install.sh -- --help` for the full flag list. If an AI coding agent is
doing this for you, point it at [`CLAUDE.md`](./CLAUDE.md) — it documents this
path in more detail so it can be done in one shot.
### Manual install
```sh
git clone https://github.com/findmalek/guesswork.git ~/.zsh/guesswork
cd ~/.zsh/guesswork && npm install
```
In `~/.zshrc` (TypeSafe shown — see [Choose a provider](#choose-a-provider)
for the others' env vars):
```zsh
export TYPESAFE_API_KEY=...
source ~/.zsh/guesswork/zsh/guesswork.plugin.zsh
```
Suggestions are accepted by `forward-char`, `vi-forward-char`, `end-of-line`
and `vi-end-of-line` when the cursor is at the end of the line, so →
and ^E work in both emacs and vi keymaps (in vi mode, bind `^E` to
`end-of-line` if you want it there). There's also a standalone widget,
`guesswork-accept-suggestion`, for a custom binding.
### Terminal support
guesswork works in **any terminal emulator that runs zsh** — Ghostty,
iTerm2, Terminal.app, Alacritty, kitty, WezTerm, tmux, all of it. The plugin
hooks zsh's line editor (`zle`) directly and draws the suggestion through
`POSTDISPLAY` + `region_highlight`, the same terminal-agnostic mechanism
`zsh-autosuggestions` uses — nothing terminal-specific, no raw escape codes.
Verified hands-on in Ghostty (`TERM=xterm-ghostty`) and Terminal.app
(`TERM=xterm-256color`); both register the plugin's widgets identically.
One real caveat, not specific to guesswork: SSHing *from* a terminal with its
own newer `TERM` value (Ghostty's `xterm-ghostty` is a known case) *into* a
host whose terminfo database doesn't have that entry yet can misrender any
terminal program until you copy the entry over — see
[Ghostty's terminfo docs](https://ghostty.org/docs/help/terminfo).
## Choose a provider
guesswork talks to TypeSafe's Jev model either directly or through Cloudflare
Workers AI, which hosts the same model behind its own account-scoped API —
same request/response shape, same code path either way. Anthropic and Groq
are fallbacks for when you'd rather not sign up for either: each substitutes
a real general-purpose chat model for Jev, asked to do the same structured
evaluation task (see [#1](https://github.com/findmalek/guesswork/issues/1)
for why a real self-hosted Jev isn't possible today) — not equivalent
models, escape hatches. Groq is the speed-optimized fallback: its
structured-output "strict" mode guarantees schema-valid JSON by constrained
decoding on custom LPU hardware, a stronger guarantee than Anthropic's
schema-constrained (but not decoding-guaranteed) output — whether that
closes the latency gap with Jev hasn't been benchmarked yet
([#11](https://github.com/findmalek/guesswork/issues/11)).
| | TypeSafe (direct) | Cloudflare Workers AI | Anthropic (fallback) | Groq (fallback) |
| --- | --- | --- | --- | --- |
| Runs | Jev | Jev | a real Claude model | a real open-weight model |
| Input price | $42 / billion tokens | $42 / billion tokens | Haiku 4.5: $1 / million tokens | GPT-OSS 20B: ~$0.075 / million tokens¹ |
| Context length | not published | 32,000 tokens | 200,000 tokens | 128,000 tokens |
| Credentials | [API key](https://typesafe.ai) | [account ID + token](#finding-your-cloudflare-account-id-and-api-token) | [API key](https://console.anthropic.com/settings/keys) | [API key](https://console.groq.com/keys) |
| Billed through | TypeSafe | Cloudflare | Anthropic | Groq |
| Env vars | `TYPESAFE_API_KEY` | `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_API_TOKEN` | `ANTHROPIC_API_KEY` | `GROQ_API_KEY` |
¹ Groq's own pricing page is client-rendered and wasn't directly confirmable
at the time this was written — sourced from third-party trackers instead, so
treat it as approximate and check [console.groq.com](https://console.groq.com)
before relying on it. Every other price above is that provider's own
published rate (see [What it costs](#what-it-costs) for the per-request math).
If you already have a Cloudflare account, that's usually the path of least
friction; TypeSafe direct is one less account if you don't. Reach for
Anthropic or Groq only if you want neither — both are general-purpose models
standing in for a purpose-built one, so scores may not calibrate the same way
against the default thresholds (`GUESSWORK_THRESHOLD` / `GUESSWORK_MIN_SCORE`
/ `GUESSWORK_STRONG_SCORE`).
### Finding your Cloudflare Account ID and API Token
- **Account ID** — open [dash.cloudflare.com](https://dash.cloudflare.com)
and pick an account. It's the 32-character hex string right after
`dash.cloudflare.com/` in the URL bar — copy that segment. Also shown under
**Workers & Pages → Overview**, top right, with a copy icon.
- **API Token** — [dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens)
→ **Create Token** → **Custom Token** → add **Workers AI** (Read is enough)
scoped to your account → **Continue** → **Create Token**. Copy it
immediately; Cloudflare only shows it once.
## Configuration
Set these before sourcing the plugin (above the `source` line in `.zshrc`):
| Variable | Default | Meaning |
| --- | --- | --- |
| `GUESSWORK_HISTORY_LIMIT` | `100` | Recent distinct commands to consider |
| `GUESSWORK_MIN_CHARS` | `2` | Do nothing until this many characters are typed |
| `GUESSWORK_THRESHOLD` | `0.5` | Fuzzy mode: min probability that *some* entry completes the input |
| `GUESSWORK_MIN_SCORE` | `0.3` | Fuzzy mode: min probability of the top candidate |
| `GUESSWORK_STRONG_SCORE` | `0.9` | Fuzzy mode: a top score this high overrides `GUESSWORK_THRESHOLD` |
| `GUESSWORK_HIGHLIGHT` | `fg=8` | zle highlight spec for the suggestion |
| `GUESSWORK_SHOW_SCORE` | `1` | Append the score, e.g. `[0.87]` |
| `GUESSWORK_NODE` | `node` | Node binary |
| `GUESSWORK_MODEL` | *(varies)* | Model override — TypeSafe: Jev version; Anthropic: Claude model (default `claude-haiku-4-5`); Groq: chat model (default `openai/gpt-oss-20b`); ignored on Cloudflare |
| `GUESSWORK_PROJECT_SCRIPTS` | `1` | Set to `0` to stop adding `package.json`/`Makefile` commands as candidates (see [How it works](#how-it-works)) |
| `GUESSWORK_DEBUG_LOG` | *(unset)* | When set, every request/response is appended to this file |
Which provider runs is decided by which credentials are set — TypeSafe takes
priority, then Cloudflare, then Anthropic, then Groq, if more than one
happens to be set.
## How it works
`zsh/guesswork.plugin.zsh` hooks `line-pre-redraw`. On every buffer change it
kills any in-flight request and starts `src/cli.ts` in the background (`zle -F`
on a process-substitution fd, so the prompt never blocks). The result is
applied only if the buffer is still what was typed when the request started —
a stale response from a buffer you've since changed is silently discarded.
`src/cli.ts` does one request per keystroke to whichever provider is
configured:
1. **Candidates.** The last `--limit` distinct commands come from history
(extended format, multi-line entries supported). `src/project-scripts.ts`
adds commands you haven't necessarily *run* before: `package.json`
`scripts` (as `npm run ` / `pnpm ` / `yarn ` / `bun run
`, with the package manager read from the `packageManager` field or
the nearest lockfile) and `Makefile` targets (`make `), from the
nearest ancestor directory that has either file — a script already in
history is left out so it isn't offered twice. Set
`GUESSWORK_PROJECT_SCRIPTS=0` to turn this off. If any candidate literally
starts with the typed text, only those are sent (*prefix mode*);
otherwise all of them are (*fuzzy mode*), and a single prefix match
answers instantly with no request at all.
2. **One request, two questions.** A `Choice` over the candidate IDs asks
which one the user is completing (score = its probability); a `Noul` asks
whether *any* candidate completes the input at all — without that second
gate, the closest irrelevant command would always "win" a Choice, since
its probabilities always sum to 1.
3. **Gate.** Prefix mode always suggests the top candidate. Fuzzy mode
suggests only when the top score clears `GUESSWORK_MIN_SCORE` and either
the Noul clears `GUESSWORK_THRESHOLD` or the top score clears
`GUESSWORK_STRONG_SCORE` on its own — the Noul under-fires on tiny
histories where the Choice is already decisive, and on nonsense the
Choice spreads thin while the Noul correctly goes near zero, so both are
checked.
Exact rules — prefix matching, dedup, thresholds — live in code; the model
only judges *which* candidate fits. Latency is roughly 0.7–0.9s per request,
almost all of it API time. A suggestion sourced from `package.json`/`Makefile`
gets tagged `script` rather than `prefix`/`replace` in the header line handed
to the zsh plugin, since it's something you've never actually run — the zsh
side doesn't style it differently yet, but the distinction exists for a
future pass to.
## What it costs
Every request is small: your typed prefix plus ~100 short command strings
in, a probability distribution over ~100 IDs out. TypeSafe publishes
**$0.000081 per request** as representative for this kind of task — about
245x cheaper than routing the same request through a general-purpose chat
model, which is exactly what the Anthropic and Groq fallbacks do instead (see
the [provider table](#choose-a-provider) for their per-token rates). Even
triggering a few hundred requests *every single day* for a full year on Jev
lands around $10 total — not a subscription, not a line item you'll notice.
The fallbacks cost more per token but are still cheap in absolute terms for a
request this size; just not purpose-built-model cheap.
Run any request with `--json` to see the exact `usage` your own history
produces:
```sh
node src/cli.ts --buffer "git ch" --json | jq .usage
```
## CLI
```sh
node src/cli.ts --buffer 'git ch' --list # show the ranked candidates
node src/cli.ts --buffer 'git ch' --json # full result incl. usage
node src/cli.ts --buffer 'git ch' --list-candidates # show what would be sent, no request, no credentials needed
node src/cli.ts --buffer 'git ch' --jev-only # skip the prefix filter
node src/cli.ts --help
```
## Tests
```sh
npm test # unit tests (history parsing, request shape, gating)
npm run typecheck
npm run test:e2e # drives a real zsh in a pty; needs a provider's credentials
```
The e2e test types into an interactive `zsh -f` with a throwaway history file
and checks that suggestions appear, that typing along a suggestion does not
re-request, that → and ^E accept and run the command (in
emacs and vi keymaps), that nonsense yields nothing, and that a stale response
is discarded when the buffer changes mid-request.
## Website
**[guesswork.findmalek.com](https://guesswork.findmalek.com)** — a Next.js
landing page with an interactive terminal playground (a hand-built terminal
look-alike, not a real shell) that ranks suggestions with the real Jev model
through a rate-limited server proxy, falling back to a local deterministic
stand-in when that's unavailable — see [`site/`](./site) for details.
## Contributing
Every PR needs a linked issue first — open one with the
[detailed-task template](https://github.com/findmalek/guesswork/issues/new/choose)
(context, current state, a concrete acceptance checklist) before writing
code; if the branch name follows `type/NNN-slug` or `NNN-slug`, a bot links
the PR to it automatically, otherwise add `Closes #N` to the PR body
yourself. Work is tracked on the
[Guesswork project board](https://github.com/users/findmalek/projects/14).
The [PR template](.github/pull_request_template.md) covers the rest.
`npm run sync-repo` fetches origin, pulls `main`, and cleans up local
branches once a PR merges — handy given the branch-per-issue workflow above.
## Uninstall
```sh
sed -i.bak '/# >>> guesswork >>>/,/# <<< guesswork <<