IF YOU ARE HERE FROM THE YT VIDEO
a few things changed. completion is a bit different for skills. i now require # to begin with
... ill try to update as it happens ...
It will happen that apis will disapear or be changed. Sorry, this is an ALPHA product.
The AI client that Neovim deserves, built by those that still enjoy to code.
- visual implementation
- search
- debug
- Extensions.Worker.work
This repo is meant to be my exploration grounds for using AI mixed with tradcoding.
I believe that hand coding is still very important and the best products i know of today still do that (see opencode vs claude code)
- Prompts are temporary right now. they could be massively improved
- Officially in beta, but api can still change. unlikely at this point
you must have a supported AI CLI installed (opencode, claude, or cursor-agent — see Providers below) Opencode is preferred, but dax sucks. so... dont forget that. I also hear that Dax is the best DevRel / CEO ever
Add the following configuration to your neovim config
I make the assumption you are using Lazy
{
"ThePrimeagen/99",
config = function()
local _99 = require("99")
-- For logging that is to a file if you wish to trace through requests
-- for reporting bugs, i would not rely on this, but instead the provided
-- logging mechanisms within 99. This is for more debugging purposes
local cwd = vim.uv.cwd()
local basename = vim.fs.basename(cwd)
_99.setup({
-- provider = _99.Providers.ClaudeCodeProvider, -- default: OpenCodeProvider
logger = {
level = _99.DEBUG,
path = "/tmp/" .. basename .. ".99.debug",
print_on_error = true,
},
-- When setting this to something that is not inside the CWD tools
-- such as claude code or opencode will have permission issues
-- and generation will fail refer to tool documentation to resolve
-- https://opencode.ai/docs/permissions/#external-directories
-- https://code.claude.com/docs/en/permissions#read-and-edit
tmp_dir = "./tmp",
--- Completions: #rules and @files in the prompt buffer
completion = {
-- I am going to disable these until i understand the
-- problem better. Inside of cursor rules there is also
-- application rules, which means i need to apply these
-- differently
-- cursor_rules = "<custom path to cursor rules>"
--- A list of folders where you have your own SKILL.md
--- Expected format:
--- /path/to/dir/<skill_name>/SKILL.md
---
--- Example:
--- Input Path:
--- "scratch/custom_rules/"
---
--- Output Rules:
--- {path = "scratch/custom_rules/vim/SKILL.md", name = "vim"},
--- ... the other rules in that dir ...
---
custom_rules = {
"scratch/custom_rules/",
},
--- Configure @file completion (all fields optional, sensible defaults)
files = {
-- enabled = true,
-- max_file_size = 102400, -- bytes, skip files larger than this
-- max_files = 5000, -- cap on total discovered files
-- exclude = { ".env", ".env.*", "node_modules", ".git", ... },
},
--- What autocomplete do you use. We currently only
--- support cmp right now
source = "cmp" | "blink",
},
--- WARNING: if you change cwd then this is likely broken
--- ill likely fix this in a later change
---
--- md_files is a list of files to look for and auto add based on the location
--- of the originating request. That means if you are at /foo/bar/baz.lua
--- the system will automagically look for:
--- /foo/bar/AGENT.md
--- /foo/AGENT.md
--- assuming that /foo is project root (based on cwd)
md_files = {
"AGENT.md",
},
})
-- take extra note that i have visual selection only in v mode
-- technically whatever your last visual selection is, will be used
-- so i have this set to visual mode so i dont screw up and use an
-- old visual selection
--
-- likely ill add a mode check and assert on required visual mode
-- so just prepare for it now
vim.keymap.set("v", "<leader>9v", function()
_99.visual()
end)
--- if you have a request you dont want to make any changes, just cancel it
vim.keymap.set("n", "<leader>9x", function()
_99.stop_all_requests()
end)
vim.keymap.set("n", "<leader>9s", function()
_99.search()
end)
end,
},When prompting, you can reference rules and files to add context to your request.
#references rules — type#in the prompt to autocomplete rule files from your configured rule directories@references files — type@to fuzzy-search project files
Referenced content is automatically resolved and injected into the AI context. Requires cmp (source = "cmp" in your completion config).
99 supports multiple AI CLI backends. Set provider in your setup to switch. If you don't set model, the provider's default is used.
| Provider | CLI tool | Default model |
|---|---|---|
OpenCodeProvider (default) |
opencode |
opencode/claude-sonnet-4-5 |
ClaudeCodeProvider |
claude |
claude-sonnet-4-5 |
CursorAgentProvider |
cursor-agent |
sonnet-4.5 |
_99.setup({
provider = _99.ClaudeCodeProvider,
-- model is optional, overrides the provider's default
model = "claude-sonnet-4-5",
})To report a bug, please provide the full running debug logs. This may require a bit of back and forth.
Please do not request features. We will hold a public discussion on Twitch about features, which will be a much better jumping point then a bunch of requests that i have to close down. If you do make a feature request ill just shut it down instantly.
To get the last run's logs execute :lua require("99").view_logs(). If this happens to not be the log, you can navigate the logs with:
function _99.prev_request_logs() ... end
function _99.next_request_logs() ... endIf there are secrets or other information in the logs you want to be removed make sure that you delete the query printing. This will likely contain information you may not want to share.
- long function definition issues.
function display_text(
game_state: GameState,
text: string,
x: number,
y: number,
): void {
const ctx = game_state.canvas.getContext("2d");
assert(ctx, "cannot get game context");
ctx.fillStyle = "white";
ctx.fillText(text, x, y);
}Then the virtual text will be displayed one line below "function" instead of first line in body
-
in lua and likely jsdoc, the replacing function will duplicate comment definitions
- this wont happen in languages with types in the syntax
-
visual selection sends the whole file. there is likely a better way to use treesitter to make the selection of the content being sent more sensible.
- every now and then the replacement seems to get jacked up and it screws up
what i am currently editing.. I think it may have something to do with auto-complete
- definitely not suure on this one
- export function ... sometimes gets export as well. I think the prompt could help prevent this