No API key. No model to choose. No account to create. If you're already signed
into Cursor, there is nothing else to set up — neocursor drives Cursor's own
StreamCpp backend with your existing login, so you get the same predictions,
at the same latency, as Cursor itself.
[!WARNING] Beta. The goal is 1:1 parity with Cursor's Tab, and most of it is there. See Cursor Tab parity for the honest scoreboard.
uv on your PATH — the Python sidecar
runs through it and fetches its own deps. Nothing to pip install.Works on macOS, Linux and Windows; the sidecar finds Cursor's session wherever your platform puts it. Portable install, Flatpak/Snap, or WSL? See Cursor can't be found.
{
"teocns/neocursor.nvim",
event = "InsertEnter",
-- pre-warm the sidecar (double quotes so cmd.exe and sh both parse it)
build = 'uv run --with "httpx[http2]" python -c "import httpx"',
opts = {},
}
That's the entire setup. No tokens, no required setup() arguments — open a
file, start typing, pause → ghost text → <Tab>.
Built into Neovim ≥ 0.12:
vim.pack.add { "https://github.com/teocns/neocursor.nvim" }
require("neocursor").setup {}
More setups → docs/installation.md: full vim.pack parity with the lazy.nvim spec, coexisting with nvim-cmp / blink.cmp, and pinning a version.
Type, pause, and a suggestion appears. Then:
| Key / Command | Does |
|---|---|
<Tab> |
Accept · or jump to the predicted next edit · or chain to the next one |
<M-Right> |
Accept the suggestion word-by-word |
<C-]> |
Dismiss |
:NeocursorSuggest |
Force a request right now |
:NeocursorLog |
Toggle the live state dashboard |
:NeocursorDebug |
Print diagnostics |
One key does three jobs, in Cursor's exact rhythm — accept, then jump, then accept again:
flowchart LR
T["you type, then pause"] --> G["ghost text / diff appears"]
G -->|Tab| A["edit applied"]
A --> Q{"Cursor predicts an<br/>edit elsewhere?"}
Q -->|"yes"| P["jump pill appears<br/>⟪Tab → L42⟫"]
P -->|Tab| J["cursor jumps there"]
J -->|Tab| A
Q -->|"no"| T
The loop back through jump → accept is what makes it feel like Cursor rather than a completion engine: you keep pressing the same key and the edits come to you.
Every option is optional. This is the complete set, at its defaults:
require("neocursor").setup({
debounce = 250,
map_tab = true,
map_partial = "<M-Right>",
filetypes = nil,
show_hints = true,
sidecar_cmd = { "uv", "run", "--with", "httpx[http2]" },
})
| Option | Type | Description |
|---|---|---|
debounce |
number |
Idle milliseconds before a request. Overridden by Cursor's CppConfig at startup. |
map_tab |
boolean |
false leaves <Tab> unmapped so nvim-cmp / blink.cmp can own it. |
map_partial |
string | false |
Key for word-by-word accept. false disables it. |
filetypes |
string[] | nil |
Allow-list, e.g. { "python", "lua" }. nil means every normal buffer. |
show_hints |
boolean | table |
Hint chrome. false hides it; a table hides one surface. |
sidecar_cmd |
string[] |
How the Python sidecar launches. Override only for unusual setups. |
Full reference → docs/configuration.md: what each
option does, why debounce is usually overridden, and which hint surface you
actually want to keep.
The goal is a 1:1 port of Cursor's Tab — here's what actually made it across, and what's still in flight:
| Cursor Tab capability | neocursor |
|---|---|
| Inline multi-line completions (ghost text) | ✅ |
| Diff-style rewrites of existing lines | ✅ |
| Next-edit prediction + cursor jump (the tab-tab-tab flow) | ✅ |
Multi-edit chains — one <Tab> per edit, no extra round-trips |
✅ |
| Recent-edit / diff-history context | ✅ |
| Nearby-file + linter-error context | ✅ |
| Request gating — stays quiet while you read/navigate | ✅ |
| Partial accept (word-by-word) | ✅ |
Config pulled live from Cursor (CppConfig: debounce, heuristics) |
✅ |
| macOS / Linux / Windows auth paths | ✅ |
| Character-level diffs for single-character edits | 🚧 |
| Cross-file apply on jump targets | 🚧 partial — jump lands, chain is partial |
✅ ported · 🚧 in progress
The core loop — predict, ghost, <Tab>, jump, chain — is complete and running on
Cursor's actual backend. The 🚧 rows are edges, not the main path.
neocursor doesn't reimplement or retrain a model — it is Cursor's Tab, reached through a tiny stdio bridge:
flowchart LR
NV["<b>Neovim</b><br/><code>lua/neocursor</code><br/>ghost text · diffs · owns Tab"]
SC["<b>sidecar.py</b><br/>reads your Cursor session<br/>forges the checksum"]
CU["<b>Cursor StreamCpp</b><br/><code>api2.cursor.sh</code>"]
NV -->|"buffer, cursor, edit history<br/>(JSON over stdio)"| SC
SC -->|"Connect / protobuf over h2"| CU
CU -.->|"streamed edits<br/>+ next-jump target"| SC
SC -.->|"JSON"| NV
The sidecar reads your local Cursor session, speaks the exact StreamCpp call
Cursor's own client makes, and streams back the edit sequence plus the next
cursor-jump target. Your token never leaves the machine, and the Lua side never
touches the network — that boundary is deliberate.
See NOTICE for rendering-technique attribution.
Start with :NeocursorDebug (resolved config, sidecar state, last error) and
:NeocursorLog (live event stream). Between them, most problems name themselves.
Full guide → docs/troubleshooting.md: Cursor
can't be found, the sidecar won't start, no suggestions appear, <Tab> does
nothing, and what to include in a bug report.
Issues and PRs welcome — see CONTRIBUTING.md for the project layout, how to run the specs, and what makes a bug report actionable.
Reporting something? Include :NeocursorDebug output, plus
uv run cursor_paths.py for anything path- or auth-related.
Independent, personal-use interoperability project — not affiliated with Anysphere / Cursor. It uses your own account; the token never leaves your machine. It talks to Cursor's private API, which is undocumented and may change, and using it may not fit Cursor's ToS — that's between you and Cursor. No Cursor source is redistributed. Use at your own risk.