- Rust 73.5%
- Vue 15.3%
- Lua 5%
- JavaScript 3.3%
- TypeScript 2.5%
- Other 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| docs/screenshots | ||
| migrations | ||
| nvim | ||
| src | ||
| tests | ||
| vscode | ||
| web | ||
| .gitignore | ||
| .luarc.json | ||
| .tool-versions | ||
| build.rs | ||
| build.sh | ||
| Cargo.lock | ||
| Cargo.toml | ||
| dev.md | ||
| justfile | ||
| plan-lsp.md | ||
| plan-mcp.md | ||
| plan.md | ||
| README.md | ||
| research.md | ||
ai-cmp-lsp
AI code completions as ghost text in Neovim and VS Code, AI command-line
completion for bash/zsh/fish, a web dashboard, and an MCP server — one
local daemon (ai-cmp-daemon) behind OpenAI-compatible APIs serves them
all. Development instructions: dev.md.
Build
./build.sh # web dashboard + daemon + VS Code .vsix (debug)
./build.sh --release # optimized
Configuration — ~/.ai-cmp/config.toml
On first start the daemon creates this file with the defaults below (mode
0600) when it doesn't exist — nothing to copy by hand.
# Top-level, before the sections: paths the daemon must not index.
# Globs — `*`/`?` stop at "/", `**` crosses directories.
# ignore = ["**/node_modules/api/**", "/home/marc/old-projects/*"]
# Any string value can come from a file: `api_key = "{file:~/.secrets/key}"`
# (absolute, `~`- or config-relative; a missing file fails startup).
[server]
# port = 8890 # optional fixed port; unset = free port, the plugins read
# # the actual one from the runtime dir's daemon.json
[embeddings]
api_url = "http://localhost:8888/v1/embeddings"
api_key = ""
model = "unsloth/bge-small-en-v1.5"
[completion]
provider = "openai"
model = "unsloth/Qwen3.5-4B-GGUF"
api_key = ""
max_tokens = 96
[completion.providers.openai]
api_url = "http://localhost:8888/v1"
format = "openai" # "openai" | "anthropic" | "fim"
auth = "bearer"
# Extra request fields, deep-merged into the LLM call (vLLM/llama.cpp
# flags; public APIs reject unknown top-level fields).
[completion.providers.openai.extra]
chat_template_kwargs = { enable_thinking = false }
# Rules generation (dashboard → Rules). Optional; defaults shown.
# Rules calls run with thinking enabled, and reasoning counts against
# these caps — keep them big enough to think and answer in. Reasoning is
# stripped before anything is persisted.
[rules]
batch_chars = 8000 # source characters per LLM call
map_max_tokens = 10240 # generated tokens per batch extraction
reduce_max_tokens = 15360 # generated tokens for merge/final calls
concurrency = 2 # parallel LLM calls
max_chunks = 0 # uniformly sample at most N chunks; 0 = all
# Shell autocomplete. Optional; defaults shown.
[shell]
mode = "cycle" # cycle | single | ghost (zsh only; bash/fish → single)
keybind = "<C-Space>" # nvim notation, translated per shell
accept_key = "<S-Right>" # ghost mode: accepts the suggestion
history_count = 20 # last N history entries sent to the LLM
timeout_ms = 2000 # LLM budget; past it the shell shows an error
candidates = 3 # command lines per call (cycle depth)
The dashboard's Config page edits the same file: file pickers for
{file:…} and path fields, comments preserved on save, daemon restarts
to apply.
Neovim
-- lazy.nvim
{
dir = "~/Dev/ai-cmp-lsp/nvim",
name = "ai-cmp",
config = function()
require("ai-cmp").setup({
trigger = { mode = "debounced", debounce_ms = 100, keymap = "<C-Space>" },
display = { max_visible_lines = 12, hl_group = "Comment" },
keymaps = { accept = "<Tab>", dismiss = "<Esc>" },
})
end,
}
Attaches to the shared daemon or spawns a detached one (a lock file keeps
exactly one per user), syncs buffers, and streams completions as ghost
text — <Tab> accepts, <Esc> dismisses. Every workspace is a session
with its own embedding worker; idle sessions expire after 5 minutes
(AI_CMP_SESSION_TTL_SECS, 0 = never).
Commands: :AICmpTrigger manual completion · :AICmpStatus daemon
health · :AICmpLog log tail · :AICmpDashboard open the dashboard ·
:AICmpStart start or re-attach on demand · :AICmpShutdown stop the
daemon (it outlives the editor otherwise).
Shell autocomplete
Press a key and the chat model completes the whole command line from your
history — atuin's read-only history.db when installed, else the shell's
history file ($HISTFILE / fish's), else just the cwd:
# ~/.bashrc # ~/.zshrc # ~/.config/fish/config.fish
eval "$(ai-cmp-daemon shell init bash)" # or: … init zsh # ai-cmp-daemon shell init fish | source
Default key Ctrl-Space; [shell] selects the mode:
- cycle — insert the best candidate, press again to walk the rest (wraps; refetches whenever the line changes).
- single — top candidate, a fresh call per press.
- ghost (zsh) — dim suggestion after the cursor;
accept_key(defaultShift-Right) accepts it,Enterruns the line without it. bash/fish have no ghost-text API and fall back to single-insert.
Errors surface instead of failing silently (budget is timeout_ms);
history is redacted (token/password/key-shaped values masked) before it
reaches the LLM; the provider must be chat-format (format = "fim" is
rejected). Re-run the eval … line after changing [shell] — keybind
and mode are baked into the generated script. Needs bash ≥ 4.4
(mapfile), zsh with bindkey, or fish ≥ 3.1 (string split0).
Web dashboard
The daemon serves it on the same loopback port (127.0.0.1 only).
Browse and delete projects/files/chunks/embeddings, see embedding
staleness and live counters, run natural-language Search over a
project's chunks, group duplicated code on the Shared page (exact by
content hash, near-identical by cosine), and generate a project ruleset
on the Rules page (chat-format provider required; live progress,
pause, cancel, continue for jobs that stopped without a ruleset,
rendered result). The sidebar shows daemon liveness; a dead
daemon reads as a plain error, not a crash.
cd web && bun install && bun run build # once → web/dist
Until web/dist exists the daemon serves a notice page with these
instructions; files are read per request, so a rebuild only needs a
reload.
Screenshots
![]() |
![]() |
| Overview | Semantic search |
![]() |
![]() |
| Files | Chunks |
![]() |
![]() |
| Embeddings | Duplicate detection |
![]() |
![]() |
| Rules | Log |
![]() |
|
| Config |
MCP server
POST /mcp on the daemon's port — point a local MCP host at it:
{ "mcpServers": { "ai-cmp": { "type": "http", "url": "http://127.0.0.1:8890/mcp" } } }
Port: fixed by the editor plugin (serverPort), otherwise read port
from $XDG_RUNTIME_DIR/ai-cmp/daemon.json. Scope: a read-only view plus
indexing — rules jobs can be listed and inspected but never started, and
nothing is ever deleted.
| tool | what it answers |
|---|---|
overview |
server/db snapshot: counts, embedding progress, live counters |
list_projects |
project hashes and row counts |
index_project |
start indexing a root no editor has opened yet |
search_code |
natural-language semantic search (one embedding round-trip) |
shared_code |
duplicated / near-duplicated code across projects |
chunk_at |
chunk covering a file line range + its same-kind neighbours |
list_rules |
rules-job history (read-only) |
get_rule_job |
one job's progress and finished ruleset (read-only) |
VS Code
npx @vscode/vsce package # in vscode/ → ai-cmp-0.1.0.vsix
code --install-extension ai-cmp-0.1.0.vsix
Same shared daemon as Neovim (settings ai-cmp.serverCommand,
ai-cmp.serverConfig, ai-cmp.serverPort); completions ride the native
inline suggestions (Tab accepts, Esc dismisses). The status bar shows
embedding progress and opens the dashboard on click. Commands: AI
Complete, Show Daemon Status, Show Daemon Log, Open Web
Dashboard, Start Shared Daemon, Stop Shared Daemon. Debugging:
Output panel → ai-cmp. Note: the stable inline-completion API
resolves the provider once per request, so ghost text appears when the
done event lands rather than token-by-token.








