Add Hanzo AI integration

- Add Hanzo AI source (Claude, GPT-4, Gemini, Ollama support)
- Add WebSocket bridge for MCP/ZAP AI agent control
- Add REPL integration via Jupyter kernels
- Add Neovim Lua module for enhanced UI
- Add comprehensive commands (Complete, Explain, Refactor, Fix, Tests, Docs, Review)
- Update README with Hanzo documentation
This commit is contained in:
Zach Kelling
2026-01-22 18:29:14 -08:00
parent 0a8f737f80
commit b843f3ef57
7 changed files with 1544 additions and 6 deletions
+70 -6
View File
@@ -1,18 +1,25 @@
# ⚡ Neural
# hanzo.vim
[![Vim](https://img.shields.io/badge/VIM-%2311AB00.svg?style=for-the-badge&logo=vim&logoColor=white)](https://www.vim.org/) [![Neovim](https://img.shields.io/badge/NeoVim-%2357A143.svg?&style=for-the-badge&logo=neovim&logoColor=white)](https://neovim.io/) [![CI](https://img.shields.io/github/actions/workflow/status/dense-analysis/neural/main.yml?branch=main&label=CI&logo=github&style=for-the-badge)](https://github.com/dense-analysis/neural/actions?query=event%3Apush+workflow%3ACI+branch%3Amain++) [![Join the Dense Analysis Discord server](https://img.shields.io/badge/chat-Discord-5865F2?style=for-the-badge&logo=appveyor)](https://discord.gg/5zFD6pQxDk)
[![Vim](https://img.shields.io/badge/VIM-%2311AB00.svg?style=for-the-badge&logo=vim&logoColor=white)](https://www.vim.org/) [![Neovim](https://img.shields.io/badge/NeoVim-%2357A143.svg?&style=for-the-badge&logo=neovim&logoColor=white)](https://neovim.io/)
A ChatGPT Vim plugin, an OpenAI Neovim plugin, and so much more! Neural integrates various machine learning tools so you can let AI write code for you in Vim/Neovim, among other helpful things.
Fork of [dense-analysis/neural](https://github.com/dense-analysis/neural) with comprehensive Hanzo AI integration.
A multi-provider AI plugin for Vim/Neovim supporting Claude, GPT-4, Gemini, Ollama, and more. Includes MCP/ZAP bridge for AI agent control and REPL integration.
## 🌟 Features
### Neural Base
* Generate text easily `:Neural write a story`
* Support for multiple machine learning models
* Focused on privacy and avoiding leaking data to third parties
* Easily ask AI to explain code or paragraphs `:NeuralExplain`
* Compatible with Vim 8.0+ & Neovim 0.8+
* Supported on Linux, Mac OSX, and Windows
* Only dependency is Python 3.7+
### Hanzo Extensions
* **Multi-Provider**: Claude, GPT-4, Gemini, Ollama, any OpenAI-compatible API
* **LLM Gateway**: Unified proxy for 100+ providers via `http://localhost:4000`
* **MCP/ZAP Bridge**: WebSocket bridge for AI agent control (hanzo-mcp compatible)
* **REPL Integration**: Jupyter kernel support for interactive code evaluation
* **Extended Commands**: Complete, Explain, Refactor, Fix, Tests, Docs, Review
Experience lightning-fast code generation and completion with asynchronous
streaming.
@@ -109,6 +116,32 @@ Try typing `:Neural say hello`, and if all goes well the machine learning
tool will say "hello" to you in the current buffer. Type `:help neural` to
see the full documentation.
### Hanzo Configuration
```vim
" Model and provider
let g:hanzo_model = 'claude-sonnet-4-20250514'
let g:hanzo_provider = 'anthropic' " anthropic, openai, google, ollama
" Mode selection
let g:hanzo_mode = 'api' " api, mcp, or ollama
" LLM Gateway (optional)
let g:hanzo_llm_gateway = 'http://localhost:4000'
" Enable default keybinds
let g:hanzo_set_default_keybinds = 1
```
```lua
-- Neovim Lua config
require('hanzo').setup({
model = 'claude-sonnet-4-20250514',
provider = 'anthropic',
mode = 'api',
})
```
## 🛠️ Commands
### `:NeuralExplain`
@@ -129,6 +162,37 @@ the stop command by default when you enter that key combination. The default
keybind can be disabled by setting `g:neural.set_default_keybinds` to any falsy
value. You can set a keybind to stop Neural by mapping to `<Plug>(neural_stop)`.
### Hanzo Commands
| Command | Description |
|---------|-------------|
| `:Hanzo <prompt>` | Send prompt to AI |
| `:H <prompt>` | Short alias for `:Hanzo` |
| `:HanzoComplete` | Complete code at cursor |
| `:HanzoExplain` | Explain selected code |
| `:HanzoRefactor <instruction>` | Refactor with instructions |
| `:HanzoFix` | Fix issues in selection |
| `:HanzoTests` | Generate tests |
| `:HanzoDocs` | Generate documentation |
| `:HanzoReview` | Code review |
| `:HanzoStart` | Start MCP/ZAP bridge |
| `:HanzoStop` | Stop bridge |
| `:HanzoModel <model>` | Set active model |
| `:HanzoMode <mode>` | Set mode (api/mcp/ollama) |
### Hanzo Keybindings
| Mapping | Mode | Action |
|---------|------|--------|
| `<Leader>h` | Normal | Open prompt |
| `<Leader>hc` | Normal | Complete |
| `<Leader>he` | Visual | Explain |
| `<Leader>hr` | Visual | Refactor |
| `<Leader>hf` | Visual | Fix |
| `<Leader>ht` | Visual | Tests |
| `<Leader>hd` | Visual | Docs |
| `<Leader>hv` | Visual | Review |
## 📜 Acknowledgements
Neural was created by [Anexon](https://github.com/Angelchev), and is maintained
+530
View File
@@ -0,0 +1,530 @@
" Hanzo AI integration for Vim/Neovim
" Provides: MCP/ZAP bridge, REPL, IDE control, Browser integration
"
" This file extends Neural with Hanzo-specific functionality:
" - WebSocket bridge for AI agent control
" - REPL integration via Jupyter kernels
" - Additional AI-powered commands
let s:script_dir = expand('<sfile>:p:h:h')
let s:bridge_job = v:null
let s:bridge_channel = v:null
let s:pending = {}
" ============================================================================
" Configuration
" ============================================================================
function! hanzo#GetConfig() abort
return {
\ 'port': get(g:, 'hanzo_port', 9228),
\ 'auto_start': get(g:, 'hanzo_auto_start', 0),
\ 'debug': get(g:, 'hanzo_debug', 0),
\ 'model': get(g:, 'hanzo_model', 'claude-sonnet-4-20250514'),
\ 'provider': get(g:, 'hanzo_provider', 'anthropic'),
\ 'mode': get(g:, 'hanzo_mode', 'api'),
\ 'llm_gateway': get(g:, 'hanzo_llm_gateway', 'http://localhost:4000'),
\}
endfunction
" ============================================================================
" Bridge Management (for AI Agent Control)
" ============================================================================
function! hanzo#StartBridge() abort
if s:bridge_job != v:null
echo "Hanzo bridge already running"
return
endif
let l:config = hanzo#GetConfig()
let l:bridge_script = s:script_dir . '/python3/bridge.py'
if !filereadable(l:bridge_script)
echoerr "Hanzo bridge script not found: " . l:bridge_script
return
endif
let s:bridge_job = job_start(['python3', l:bridge_script, string(l:config.port)], {
\ 'out_cb': function('s:OnBridgeOutput'),
\ 'err_cb': function('s:OnBridgeError'),
\ 'exit_cb': function('s:OnBridgeExit'),
\})
if job_status(s:bridge_job) == 'run'
echo "Hanzo bridge started on port " . l:config.port
" Connect after brief delay
call timer_start(500, {-> s:ConnectBridge()})
else
echoerr "Failed to start Hanzo bridge"
let s:bridge_job = v:null
endif
endfunction
function! hanzo#StopBridge() abort
if s:bridge_channel != v:null
call ch_close(s:bridge_channel)
let s:bridge_channel = v:null
endif
if s:bridge_job != v:null
call job_stop(s:bridge_job)
let s:bridge_job = v:null
echo "Hanzo bridge stopped"
endif
endfunction
function! hanzo#BridgeStatus() abort
let l:config = hanzo#GetConfig()
if s:bridge_job == v:null
echo "Hanzo bridge: not running"
elseif job_status(s:bridge_job) == 'run'
echo "Hanzo bridge: running on port " . l:config.port
if s:bridge_channel != v:null && ch_status(s:bridge_channel) == 'open'
echo " Channel: connected"
else
echo " Channel: disconnected"
endif
else
echo "Hanzo bridge: " . job_status(s:bridge_job)
endif
endfunction
function! s:ConnectBridge() abort
let l:config = hanzo#GetConfig()
try
let s:bridge_channel = ch_open('localhost:' . l:config.port, {
\ 'mode': 'json',
\ 'callback': function('s:OnBridgeMessage'),
\})
if ch_status(s:bridge_channel) == 'open'
if l:config.debug
echo "Hanzo bridge connected"
endif
endif
catch
" Retry connection
call timer_start(1000, {-> s:ConnectBridge()})
endtry
endfunction
function! s:OnBridgeMessage(channel, msg) abort
let l:config = hanzo#GetConfig()
if l:config.debug
echom "Hanzo recv: " . string(a:msg)
endif
let l:id = get(a:msg, 'id', '')
let l:action = get(a:msg, 'action', '')
" Handle pending responses
if l:id != '' && has_key(s:pending, l:id)
let l:Callback = s:pending[l:id]
unlet s:pending[l:id]
call l:Callback(a:msg)
return
endif
" Handle incoming requests from AI agent
call hanzo#HandleRequest(a:msg)
endfunction
function! s:OnBridgeOutput(channel, msg) abort
if hanzo#GetConfig().debug
echom "Hanzo: " . a:msg
endif
endfunction
function! s:OnBridgeError(channel, msg) abort
echoerr "Hanzo error: " . a:msg
endfunction
function! s:OnBridgeExit(job, status) abort
let s:bridge_job = v:null
let s:bridge_channel = v:null
if hanzo#GetConfig().debug
echom "Hanzo bridge exited with status " . a:status
endif
endfunction
" ============================================================================
" Bridge Communication
" ============================================================================
function! hanzo#Send(msg, ...) abort
if s:bridge_channel == v:null || ch_status(s:bridge_channel) != 'open'
echoerr "Hanzo bridge not connected"
return
endif
let l:msg_id = ''
if a:0 > 0
" Generate unique ID for callback
let l:msg_id = 'vim_' . localtime() . '_' . rand()
let a:msg.id = l:msg_id
let s:pending[l:msg_id] = a:1
endif
call ch_sendexpr(s:bridge_channel, a:msg)
return l:msg_id
endfunction
function! hanzo#HandleRequest(msg) abort
let l:action = get(a:msg, 'action', '')
let l:id = get(a:msg, 'id', '')
" File operations
if l:action == 'file.info'
call s:Reply(l:id, hanzo#GetFileInfo())
elseif l:action == 'file.open'
call hanzo#OpenFile(a:msg.path, get(a:msg, 'line', 1))
call s:Reply(l:id, {'success': 1})
elseif l:action == 'file.save'
write
call s:Reply(l:id, {'success': 1})
elseif l:action == 'file.close'
bdelete
call s:Reply(l:id, {'success': 1})
" Editor operations
elseif l:action == 'editor.selection'
call s:Reply(l:id, {'text': hanzo#GetSelection()})
elseif l:action == 'editor.insert'
call hanzo#Insert(a:msg.text, a:msg.line, get(a:msg, 'column', 1))
call s:Reply(l:id, {'success': 1})
elseif l:action == 'editor.replace'
call hanzo#Replace(a:msg.text, a:msg.line, a:msg.column, a:msg.endLine, a:msg.endColumn)
call s:Reply(l:id, {'success': 1})
elseif l:action == 'editor.goto'
call hanzo#GoTo(a:msg.line, get(a:msg, 'column', 1))
call s:Reply(l:id, {'success': 1})
elseif l:action == 'editor.text'
let l:lines = getline(get(a:msg, 'line', 1), get(a:msg, 'endLine', '$'))
call s:Reply(l:id, {'text': join(l:lines, "\n")})
" Vim commands
elseif l:action == 'command'
execute a:msg.command
call s:Reply(l:id, {'success': 1})
" Diagnostics (via ALE or built-in)
elseif l:action == 'diagnostics'
call s:Reply(l:id, hanzo#GetDiagnostics())
" Unknown action
else
if hanzo#GetConfig().debug
echom "Unknown action: " . l:action
endif
endif
endfunction
function! s:Reply(id, data) abort
if a:id != ''
let l:response = extend({'id': a:id}, a:data)
call hanzo#Send(l:response)
endif
endfunction
" ============================================================================
" Editor Operations
" ============================================================================
function! hanzo#GetFileInfo() abort
return {
\ 'path': expand('%:p'),
\ 'name': expand('%:t'),
\ 'line': line('.'),
\ 'column': col('.'),
\ 'modified': &modified,
\ 'filetype': &filetype,
\ 'bufnr': bufnr('%'),
\ 'winnr': winnr(),
\}
endfunction
function! hanzo#GetSelection() abort
let [l:lnum1, l:col1] = getpos("'<")[1:2]
let [l:lnum2, l:col2] = getpos("'>")[1:2]
let l:lines = getline(l:lnum1, l:lnum2)
if len(l:lines) == 0
return ''
endif
let l:lines[-1] = l:lines[-1][:l:col2 - 1]
let l:lines[0] = l:lines[0][l:col1 - 1:]
return join(l:lines, "\n")
endfunction
function! hanzo#Insert(text, line, col) abort
call cursor(a:line, a:col)
execute "normal! i" . a:text
endfunction
function! hanzo#Replace(text, line1, col1, line2, col2) abort
call cursor(a:line1, a:col1)
execute "normal! v"
call cursor(a:line2, a:col2)
execute "normal! c" . a:text
endfunction
function! hanzo#GoTo(line, col) abort
call cursor(a:line, a:col)
normal! zz
endfunction
function! hanzo#OpenFile(path, ...) abort
execute 'edit ' . fnameescape(a:path)
if a:0 > 0
call cursor(a:1, 1)
normal! zz
endif
endfunction
function! hanzo#GetDiagnostics() abort
let l:diagnostics = []
" Try ALE first
if exists('*ale#engine#GetLoclist')
let l:ale_items = ale#engine#GetLoclist(bufnr('%'))
for l:item in l:ale_items
call add(l:diagnostics, {
\ 'line': l:item.lnum,
\ 'column': l:item.col,
\ 'message': l:item.text,
\ 'severity': l:item.type == 'E' ? 'error' : 'warning',
\ 'source': get(l:item, 'linter_name', 'ale'),
\})
endfor
endif
" Try built-in diagnostics (Vim 9+ / Neovim)
if has('nvim')
lua << EOF
local diags = vim.diagnostic.get(0)
for _, d in ipairs(diags) do
local severity = "info"
if d.severity == vim.diagnostic.severity.ERROR then
severity = "error"
elseif d.severity == vim.diagnostic.severity.WARN then
severity = "warning"
end
vim.fn.add(vim.g.hanzo_diagnostics or {}, {
line = d.lnum + 1,
column = d.col + 1,
message = d.message,
severity = severity,
source = d.source or "nvim",
})
end
EOF
if exists('g:hanzo_diagnostics')
let l:diagnostics = extend(l:diagnostics, g:hanzo_diagnostics)
unlet g:hanzo_diagnostics
endif
endif
return {'diagnostics': l:diagnostics}
endfunction
" ============================================================================
" AI Commands (extend Neural)
" ============================================================================
function! hanzo#Chat(prompt) abort
" Configure Neural to use Hanzo provider
let l:config = hanzo#GetConfig()
" Set up Neural config
if !exists('g:neural')
let g:neural = {}
endif
let g:neural.providers = [{
\ 'type': 'hanzo',
\ 'model': l:config.model,
\ 'mode': l:config.mode,
\ 'url': l:config.llm_gateway,
\}]
" Forward to Neural
call neural#Prompt(a:prompt)
endfunction
function! hanzo#Complete() abort
" Get context around cursor
let l:line = line('.')
let l:col = col('.')
let l:context_start = max([1, l:line - 20])
let l:context_end = min([line('$'), l:line + 5])
let l:context = join(getline(l:context_start, l:context_end), "\n")
let l:prompt = printf("Complete the code at line %d, column %d. Context:\n\n```%s\n%s\n```\n\nProvide only the completion, no explanation.",
\ l:line, l:col, &filetype, l:context)
call hanzo#Chat(l:prompt)
endfunction
function! hanzo#Explain() abort
let l:selection = hanzo#GetSelection()
if empty(l:selection)
echoerr "No selection"
return
endif
let l:prompt = printf("Explain this %s code:\n\n```%s\n%s\n```",
\ &filetype, &filetype, l:selection)
call hanzo#Chat(l:prompt)
endfunction
function! hanzo#Refactor(instruction) abort
let l:selection = hanzo#GetSelection()
if empty(l:selection)
echoerr "No selection"
return
endif
let l:prompt = printf("Refactor this %s code according to: %s\n\n```%s\n%s\n```\n\nProvide only the refactored code.",
\ &filetype, a:instruction, &filetype, l:selection)
call hanzo#Chat(l:prompt)
endfunction
function! hanzo#Fix() abort
let l:selection = hanzo#GetSelection()
if empty(l:selection)
" Use current line
let l:selection = getline('.')
endif
let l:prompt = printf("Fix any bugs or issues in this %s code:\n\n```%s\n%s\n```\n\nProvide only the fixed code.",
\ &filetype, &filetype, l:selection)
call hanzo#Chat(l:prompt)
endfunction
function! hanzo#Tests() abort
let l:selection = hanzo#GetSelection()
if empty(l:selection)
echoerr "No selection"
return
endif
let l:prompt = printf("Write comprehensive tests for this %s code:\n\n```%s\n%s\n```",
\ &filetype, &filetype, l:selection)
call hanzo#Chat(l:prompt)
endfunction
function! hanzo#Docs() abort
let l:selection = hanzo#GetSelection()
if empty(l:selection)
echoerr "No selection"
return
endif
let l:prompt = printf("Add documentation/comments to this %s code:\n\n```%s\n%s\n```\n\nProvide the code with documentation added.",
\ &filetype, &filetype, l:selection)
call hanzo#Chat(l:prompt)
endfunction
function! hanzo#Review() abort
let l:selection = hanzo#GetSelection()
if empty(l:selection)
" Review entire buffer
let l:selection = join(getline(1, '$'), "\n")
endif
let l:prompt = printf("Review this %s code for bugs, performance issues, and improvements:\n\n```%s\n%s\n```",
\ &filetype, &filetype, l:selection)
call hanzo#Chat(l:prompt)
endfunction
" ============================================================================
" REPL Integration
" ============================================================================
function! hanzo#Repl(lang) abort
call hanzo#Send({
\ 'action': 'repl.start',
\ 'language': a:lang,
\})
endfunction
function! hanzo#Eval(code) abort
call hanzo#Send({
\ 'action': 'repl.eval',
\ 'code': a:code,
\})
endfunction
function! hanzo#EvalSelection() abort
let l:code = hanzo#GetSelection()
if !empty(l:code)
call hanzo#Eval(l:code)
endif
endfunction
function! hanzo#EvalLine() abort
let l:code = getline('.')
if !empty(l:code)
call hanzo#Eval(l:code)
endif
endfunction
" ============================================================================
" Model Selection
" ============================================================================
function! hanzo#SetModel(model) abort
let g:hanzo_model = a:model
echo "Hanzo model set to: " . a:model
endfunction
function! hanzo#SetMode(mode) abort
if a:mode =~ '^\(api\|mcp\|ollama\)$'
let g:hanzo_mode = a:mode
echo "Hanzo mode set to: " . a:mode
else
echoerr "Invalid mode. Use: api, mcp, or ollama"
endif
endfunction
function! hanzo#Models() abort
echo "Available models:"
echo ""
echo " Anthropic:"
echo " claude-sonnet-4-20250514 (default)"
echo " claude-opus-4-20250514"
echo " claude-3-5-sonnet-20241022"
echo ""
echo " OpenAI:"
echo " gpt-4-turbo"
echo " gpt-4o"
echo " o1-preview"
echo ""
echo " Google:"
echo " gemini-1.5-pro"
echo " gemini-1.5-flash"
echo ""
echo " Ollama (local):"
echo " ollama:llama3.2"
echo " ollama:codellama"
echo " ollama:deepseek-coder"
echo ""
echo "Current: " . get(g:, 'hanzo_model', 'claude-sonnet-4-20250514')
echo "Mode: " . get(g:, 'hanzo_mode', 'api')
endfunction
function! hanzo#Version() abort
echo "hanzo.vim v0.1.0"
echo " Neural integration: " . (exists('g:loaded_neural') ? 'yes' : 'no')
echo " Bridge: " . (s:bridge_job != v:null ? 'running' : 'stopped')
echo " Model: " . get(g:, 'hanzo_model', 'claude-sonnet-4-20250514')
echo " Mode: " . get(g:, 'hanzo_mode', 'api')
endfunction
+12
View File
@@ -0,0 +1,12 @@
" Hanzo AI source for Neural
" Supports: Direct API, MCP/ZAP bridge, Ollama
let s:script = expand('<sfile>:p:h:h:h:h') . '/neural_sources/hanzo.py'
function! neural#source#hanzo#Get() abort
return {
\ 'name': 'Hanzo',
\ 'script_language': 'python',
\ 'script': s:script,
\}
endfunction
+177
View File
@@ -0,0 +1,177 @@
-- Hanzo AI integration for Neovim
-- Provides enhanced UI, floating windows, and native Lua APIs
local M = {}
-- Configuration
M.config = {
-- UI settings
ui = {
prompt_enabled = true,
border = "rounded",
width = 0.8,
height = 0.4,
},
-- Model settings
model = "claude-sonnet-4-20250514",
mode = "api",
-- Keybinds
set_keymaps = false,
}
-- Setup function
function M.setup(opts)
M.config = vim.tbl_deep_extend("force", M.config, opts or {})
-- Sync with Vim globals
vim.g.hanzo_model = M.config.model
vim.g.hanzo_mode = M.config.mode
if M.config.set_keymaps then
M.setup_keymaps()
end
end
-- Setup default keymaps
function M.setup_keymaps()
local keymap = vim.keymap.set
local opts = { silent = true, noremap = true }
-- Normal mode
keymap("n", "<leader>hh", M.prompt, vim.tbl_extend("force", opts, { desc = "Hanzo prompt" }))
keymap("n", "<leader>hc", "<cmd>HanzoComplete<cr>", vim.tbl_extend("force", opts, { desc = "Hanzo complete" }))
keymap("n", "<leader>hm", M.model_picker, vim.tbl_extend("force", opts, { desc = "Hanzo model picker" }))
-- Visual mode
keymap("v", "<leader>he", "<cmd>HanzoExplain<cr>", vim.tbl_extend("force", opts, { desc = "Hanzo explain" }))
keymap("v", "<leader>hf", "<cmd>HanzoFix<cr>", vim.tbl_extend("force", opts, { desc = "Hanzo fix" }))
keymap("v", "<leader>hr", function() M.refactor_prompt() end, vim.tbl_extend("force", opts, { desc = "Hanzo refactor" }))
keymap("v", "<leader>ht", "<cmd>HanzoTests<cr>", vim.tbl_extend("force", opts, { desc = "Hanzo tests" }))
keymap("v", "<leader>hd", "<cmd>HanzoDocs<cr>", vim.tbl_extend("force", opts, { desc = "Hanzo docs" }))
keymap("v", "<leader>hv", "<cmd>HanzoReview<cr>", vim.tbl_extend("force", opts, { desc = "Hanzo review" }))
end
-- Floating window prompt
function M.prompt()
local width = math.floor(vim.o.columns * M.config.ui.width)
local height = 1
local row = math.floor((vim.o.lines - height) / 2)
local col = math.floor((vim.o.columns - width) / 2)
local buf = vim.api.nvim_create_buf(false, true)
local win = vim.api.nvim_open_win(buf, true, {
relative = "editor",
width = width,
height = height,
row = row,
col = col,
style = "minimal",
border = M.config.ui.border,
title = " Hanzo AI ",
title_pos = "center",
})
vim.bo[buf].buftype = "prompt"
vim.fn.prompt_setprompt(buf, "Hanzo> ")
vim.fn.prompt_setcallback(buf, function(text)
vim.api.nvim_win_close(win, true)
if text and text ~= "" then
vim.fn["hanzo#Chat"](text)
end
end)
vim.cmd("startinsert")
-- Close on escape
vim.keymap.set("n", "<Esc>", function()
vim.api.nvim_win_close(win, true)
end, { buffer = buf })
end
-- Refactor with prompt
function M.refactor_prompt()
vim.ui.input({ prompt = "Refactor instruction: " }, function(input)
if input and input ~= "" then
vim.fn["hanzo#Refactor"](input)
end
end)
end
-- Model picker using vim.ui.select
function M.model_picker()
local models = {
{ name = "claude-sonnet-4-20250514", desc = "Claude Sonnet 4 (Latest)" },
{ name = "claude-opus-4-20250514", desc = "Claude Opus 4" },
{ name = "claude-3-5-sonnet-20241022", desc = "Claude 3.5 Sonnet" },
{ name = "gpt-4-turbo", desc = "GPT-4 Turbo" },
{ name = "gpt-4o", desc = "GPT-4 Omni" },
{ name = "gemini-1.5-pro", desc = "Gemini 1.5 Pro" },
{ name = "ollama:llama3.2", desc = "Llama 3.2 (Local)" },
{ name = "ollama:codellama", desc = "Code Llama (Local)" },
}
vim.ui.select(models, {
prompt = "Select model:",
format_item = function(item)
return item.desc
end,
}, function(choice)
if choice then
vim.fn["hanzo#SetModel"](choice.name)
end
end)
end
-- Mode picker
function M.mode_picker()
vim.ui.select({ "api", "mcp", "ollama" }, {
prompt = "Select mode:",
}, function(choice)
if choice then
vim.fn["hanzo#SetMode"](choice)
end
end)
end
-- Get current status
function M.status()
return {
model = vim.g.hanzo_model or M.config.model,
mode = vim.g.hanzo_mode or M.config.mode,
bridge = vim.fn.exists("*hanzo#BridgeStatus") == 1 and vim.fn["hanzo#BridgeStatus"]() or "unknown",
}
end
-- Animated sign for processing (like Neural)
local sign_timer = nil
local sign_line = 0
local sign_frames = { "", "", "", "", "", "", "", "", "", "" }
local sign_frame = 1
function M.start_animated_sign(line)
sign_line = line
sign_frame = 1
-- Define sign
vim.fn.sign_define("HanzoWorking", { text = sign_frames[1], texthl = "Question" })
vim.fn.sign_place(1, "hanzo", "HanzoWorking", vim.fn.bufnr("%"), { lnum = line })
-- Start animation
sign_timer = vim.loop.new_timer()
sign_timer:start(100, 100, vim.schedule_wrap(function()
sign_frame = (sign_frame % #sign_frames) + 1
vim.fn.sign_define("HanzoWorking", { text = sign_frames[sign_frame], texthl = "Question" })
end))
end
function M.stop_animated_sign(line)
if sign_timer then
sign_timer:stop()
sign_timer:close()
sign_timer = nil
end
vim.fn.sign_unplace("hanzo", { buffer = vim.fn.bufnr("%") })
end
return M
+371
View File
@@ -0,0 +1,371 @@
#!/usr/bin/env python3
"""
Hanzo AI provider for Neural/hanzo.vim
Supports:
1. Direct LLM API calls (OpenAI-compatible via LLM Gateway)
2. MCP/ZAP protocol via WebSocket bridge
3. Local Ollama models
4. Claude, GPT-4, Gemini, and other providers via hanzo-llm
"""
import asyncio
import json
import os
import platform
import ssl
import sys
from typing import Any, Optional
# Try to import websockets for MCP/ZAP bridge
try:
import websockets
HAS_WEBSOCKETS = True
except ImportError:
HAS_WEBSOCKETS = False
# Constants
HANZO_LLM_GATEWAY = "http://localhost:4000" # Default LLM Gateway
HANZO_MCP_BRIDGE = "ws://localhost:9228" # Vim bridge port
DATA_HEADER = "data: "
DONE_MARKER = "[DONE]"
class HanzoConfig:
"""Configuration for Hanzo provider."""
def __init__(
self,
*,
# Connection settings
mode: str = "api", # "api", "mcp", "ollama"
url: str = "",
api_key: str = "",
# Model settings
model: str = "claude-sonnet-4-20250514",
provider: str = "anthropic", # anthropic, openai, google, ollama
# Generation settings
temperature: float = 0.2,
top_p: float = 1.0,
max_tokens: int = 4096,
# MCP settings
mcp_bridge_port: int = 9228,
# System prompt
system_prompt: str = "",
):
self.mode = mode
self.url = url
self.api_key = api_key
self.model = model
self.provider = provider
self.temperature = temperature
self.top_p = top_p
self.max_tokens = max_tokens
self.mcp_bridge_port = mcp_bridge_port
self.system_prompt = system_prompt
def load_config(raw_config: dict[str, Any]) -> HanzoConfig:
"""Load and validate configuration."""
if not isinstance(raw_config, dict):
raise ValueError("hanzo config is not a dictionary")
# Determine mode
mode = raw_config.get("mode", "api")
if mode not in ("api", "mcp", "ollama"):
mode = "api"
# URL defaults
url = raw_config.get("url", "")
if not url:
if mode == "ollama":
url = "http://localhost:11434"
elif mode == "mcp":
url = "" # Uses WebSocket
else:
# Check for LLM Gateway first, then direct API
url = os.environ.get("HANZO_LLM_GATEWAY", HANZO_LLM_GATEWAY)
# API key from config or environment
api_key = raw_config.get("api_key", "")
if not api_key:
# Try multiple environment variables
for env_var in [
"HANZO_API_KEY",
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"GOOGLE_API_KEY",
]:
api_key = os.environ.get(env_var, "")
if api_key:
break
# Model selection
model = raw_config.get("model", "claude-sonnet-4-20250514")
# Provider inference from model name
provider = raw_config.get("provider", "")
if not provider:
if "claude" in model.lower():
provider = "anthropic"
elif "gpt" in model.lower() or "o1" in model.lower():
provider = "openai"
elif "gemini" in model.lower():
provider = "google"
elif mode == "ollama":
provider = "ollama"
else:
provider = "anthropic"
# Generation settings
temperature = float(raw_config.get("temperature", 0.2))
top_p = float(raw_config.get("top_p", 1.0))
max_tokens = int(raw_config.get("max_tokens", 4096))
# MCP settings
mcp_bridge_port = int(raw_config.get("mcp_bridge_port", 9228))
# System prompt
system_prompt = raw_config.get("system_prompt", "")
if not system_prompt:
system_prompt = """You are an expert programmer assistant integrated into Vim/Neovim.
Provide concise, accurate code and explanations.
When writing code, match the existing style in the file.
Focus on the specific task requested."""
return HanzoConfig(
mode=mode,
url=url,
api_key=api_key,
model=model,
provider=provider,
temperature=temperature,
top_p=top_p,
max_tokens=max_tokens,
mcp_bridge_port=mcp_bridge_port,
system_prompt=system_prompt,
)
async def call_via_mcp(config: HanzoConfig, prompt: str) -> None:
"""Call LLM via MCP/ZAP WebSocket bridge."""
if not HAS_WEBSOCKETS:
raise RuntimeError("websockets not installed. Run: pip install websockets")
uri = f"ws://localhost:{config.mcp_bridge_port + 1}"
try:
async with websockets.connect(uri) as ws:
# Send LLM request via MCP bridge
request = {
"action": "llm",
"params": {
"prompt": prompt,
"model": config.model,
"provider": config.provider,
"temperature": config.temperature,
"max_tokens": config.max_tokens,
"stream": True,
"system_prompt": config.system_prompt,
}
}
await ws.send(json.dumps(request))
# Process streaming response
async for message in ws:
data = json.loads(message)
if "error" in data:
raise RuntimeError(data["error"])
elif "chunk" in data:
print(data["chunk"], end="", flush=True)
elif "done" in data:
break
print()
except ConnectionRefusedError:
raise RuntimeError(f"Cannot connect to MCP bridge on port {config.mcp_bridge_port + 1}. Start Vim with :HanzoStart")
def call_openai_compatible(config: HanzoConfig, prompt: str) -> None:
"""Call OpenAI-compatible API (works with LLM Gateway, OpenAI, Anthropic via proxy)."""
import urllib.request
import urllib.error
headers = {
"Content-Type": "application/json",
}
# Set authorization header
if config.api_key:
if config.provider == "anthropic":
headers["x-api-key"] = config.api_key
headers["anthropic-version"] = "2023-06-01"
else:
headers["Authorization"] = f"Bearer {config.api_key}"
# Build messages
messages = []
if config.system_prompt:
messages.append({"role": "system", "content": config.system_prompt})
messages.append({"role": "user", "content": prompt})
data: dict[str, Any] = {
"model": config.model,
"messages": messages,
"temperature": config.temperature,
"max_tokens": config.max_tokens,
"top_p": config.top_p,
"stream": True,
}
# Determine endpoint
if config.provider == "anthropic" and "api.anthropic.com" in config.url:
endpoint = f"{config.url}/v1/messages"
else:
endpoint = f"{config.url}/v1/chat/completions"
req = urllib.request.Request(
endpoint,
data=json.dumps(data).encode("utf-8"),
headers=headers,
method="POST",
)
# Handle SSL for macOS
context = (
ssl._create_unverified_context()
if platform.system() == "Darwin"
else None
)
try:
with urllib.request.urlopen(req, context=context) as response:
while True:
line_bytes = response.readline()
if not line_bytes:
break
line = line_bytes.decode("utf-8", errors="replace").strip()
if not line:
continue
if line.startswith(DATA_HEADER):
line_data = line[len(DATA_HEADER):]
if line_data == DONE_MARKER:
break
try:
obj = json.loads(line_data)
# OpenAI format
if "choices" in obj:
delta = obj["choices"][0].get("delta", {})
content = delta.get("content", "")
if content:
print(content, end="", flush=True)
# Anthropic format
elif "delta" in obj:
content = obj["delta"].get("text", "")
if content:
print(content, end="", flush=True)
elif "content_block" in obj:
content = obj["content_block"].get("text", "")
if content:
print(content, end="", flush=True)
except json.JSONDecodeError:
continue
print()
except urllib.error.HTTPError as error:
message = error.read().decode("utf-8", errors="ignore")
try:
err_data = json.loads(message)
if "error" in err_data:
message = err_data["error"].get("message", message)
except:
pass
raise RuntimeError(f"API error ({error.code}): {message}")
def call_ollama(config: HanzoConfig, prompt: str) -> None:
"""Call local Ollama instance."""
import urllib.request
import urllib.error
messages = []
if config.system_prompt:
messages.append({"role": "system", "content": config.system_prompt})
messages.append({"role": "user", "content": prompt})
data = {
"model": config.model,
"messages": messages,
"stream": True,
"options": {
"temperature": config.temperature,
"num_predict": config.max_tokens,
}
}
req = urllib.request.Request(
f"{config.url}/api/chat",
data=json.dumps(data).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req) as response:
while True:
line_bytes = response.readline()
if not line_bytes:
break
try:
obj = json.loads(line_bytes.decode("utf-8"))
if "message" in obj:
content = obj["message"].get("content", "")
if content:
print(content, end="", flush=True)
if obj.get("done"):
break
except json.JSONDecodeError:
continue
print()
except urllib.error.HTTPError as error:
raise RuntimeError(f"Ollama error ({error.code}): {error.read().decode()}")
def main() -> None:
"""Main entry point."""
input_data = json.loads(sys.stdin.readline())
try:
config = load_config(input_data.get("config", {}))
except ValueError as err:
sys.exit(f"Configuration error: {err}")
prompt = input_data.get("prompt", "")
if not prompt:
sys.exit("No prompt provided")
try:
if config.mode == "mcp":
asyncio.run(call_via_mcp(config, prompt))
elif config.mode == "ollama":
call_ollama(config, prompt)
else:
call_openai_compatible(config, prompt)
except Exception as err:
sys.exit(f"Hanzo error: {err}")
if __name__ == "__main__":
main()
+158
View File
@@ -0,0 +1,158 @@
" Hanzo AI integration for Vim/Neovim
" Author: Hanzo AI Inc
" License: MIT
"
" This plugin extends dense-analysis/neural with:
" - Hanzo AI provider (Claude, GPT-4, Gemini, Ollama, LLM Gateway)
" - MCP/ZAP bridge for AI agent control
" - REPL integration via Jupyter kernels
" - Additional AI-powered commands
if exists('g:loaded_hanzo')
finish
endif
let g:loaded_hanzo = 1
" Check for required features
if has('nvim')
let s:has_features = has('timers') && has('nvim-0.5.0')
else
let s:has_features = has('timers') && exists('*job_start') && exists('*ch_open')
endif
if !s:has_features
if index(['', 'gitcommit'], &filetype) == -1
echoerr 'Hanzo requires NeoVim >= 0.5.0 or Vim 8+ with +timers +job +channel'
endif
finish
endif
" ============================================================================
" Configuration Defaults
" ============================================================================
" Bridge settings
let g:hanzo_port = get(g:, 'hanzo_port', 9228)
let g:hanzo_auto_start = get(g:, 'hanzo_auto_start', 0)
let g:hanzo_debug = get(g:, 'hanzo_debug', 0)
" Model settings
let g:hanzo_model = get(g:, 'hanzo_model', 'claude-sonnet-4-20250514')
let g:hanzo_provider = get(g:, 'hanzo_provider', 'anthropic')
let g:hanzo_mode = get(g:, 'hanzo_mode', 'api')
let g:hanzo_llm_gateway = get(g:, 'hanzo_llm_gateway', 'http://localhost:4000')
" Keybind settings
let g:hanzo_set_default_keybinds = get(g:, 'hanzo_set_default_keybinds', 0)
" ============================================================================
" Bridge Commands
" ============================================================================
command! HanzoStart call hanzo#StartBridge()
command! HanzoStop call hanzo#StopBridge()
command! HanzoStatus call hanzo#BridgeStatus()
" ============================================================================
" AI Commands
" ============================================================================
" Main chat command
command! -nargs=? Hanzo call hanzo#Chat(<q-args>)
command! -nargs=? H call hanzo#Chat(<q-args>)
" Code operations
command! HanzoComplete call hanzo#Complete()
command! -range HanzoExplain call hanzo#Explain()
command! -nargs=1 -range HanzoRefactor call hanzo#Refactor(<q-args>)
command! -range HanzoFix call hanzo#Fix()
command! -range HanzoTests call hanzo#Tests()
command! -range HanzoDocs call hanzo#Docs()
command! -range HanzoReview call hanzo#Review()
" ============================================================================
" REPL Commands
" ============================================================================
command! -nargs=1 HanzoRepl call hanzo#Repl(<q-args>)
command! -nargs=1 HanzoEval call hanzo#Eval(<q-args>)
command! -range HanzoEvalSelection call hanzo#EvalSelection()
command! HanzoEvalLine call hanzo#EvalLine()
" ============================================================================
" Configuration Commands
" ============================================================================
command! -nargs=1 HanzoModel call hanzo#SetModel(<q-args>)
command! -nargs=1 HanzoMode call hanzo#SetMode(<q-args>)
command! HanzoModels call hanzo#Models()
command! HanzoVersion call hanzo#Version()
" ============================================================================
" Mappings
" ============================================================================
" <Plug> mappings
nnoremap <silent> <Plug>(hanzo_prompt) :call hanzo#Chat(input('Hanzo> '))<CR>
nnoremap <silent> <Plug>(hanzo_complete) :HanzoComplete<CR>
vnoremap <silent> <Plug>(hanzo_explain) :HanzoExplain<CR>
vnoremap <silent> <Plug>(hanzo_refactor) :call hanzo#Refactor(input('Refactor: '))<CR>
vnoremap <silent> <Plug>(hanzo_fix) :HanzoFix<CR>
vnoremap <silent> <Plug>(hanzo_tests) :HanzoTests<CR>
vnoremap <silent> <Plug>(hanzo_docs) :HanzoDocs<CR>
vnoremap <silent> <Plug>(hanzo_review) :HanzoReview<CR>
vnoremap <silent> <Plug>(hanzo_eval) :HanzoEvalSelection<CR>
nnoremap <silent> <Plug>(hanzo_eval_line) :HanzoEvalLine<CR>
" Default keybinds (if enabled)
if g:hanzo_set_default_keybinds
" Leader + h prefix for Hanzo commands
if !hasmapto('<Plug>(hanzo_prompt)', 'n')
nmap <Leader>h <Plug>(hanzo_prompt)
endif
if !hasmapto('<Plug>(hanzo_complete)', 'n')
nmap <Leader>hc <Plug>(hanzo_complete)
endif
if !hasmapto('<Plug>(hanzo_explain)', 'v')
vmap <Leader>he <Plug>(hanzo_explain)
endif
if !hasmapto('<Plug>(hanzo_refactor)', 'v')
vmap <Leader>hr <Plug>(hanzo_refactor)
endif
if !hasmapto('<Plug>(hanzo_fix)', 'v')
vmap <Leader>hf <Plug>(hanzo_fix)
endif
if !hasmapto('<Plug>(hanzo_tests)', 'v')
vmap <Leader>ht <Plug>(hanzo_tests)
endif
if !hasmapto('<Plug>(hanzo_docs)', 'v')
vmap <Leader>hd <Plug>(hanzo_docs)
endif
if !hasmapto('<Plug>(hanzo_review)', 'v')
vmap <Leader>hv <Plug>(hanzo_review)
endif
if !hasmapto('<Plug>(hanzo_eval)', 'v')
vmap <Leader>hx <Plug>(hanzo_eval)
endif
if !hasmapto('<Plug>(hanzo_eval_line)', 'n')
nmap <Leader>hx <Plug>(hanzo_eval_line)
endif
endif
" ============================================================================
" Auto-commands
" ============================================================================
" Auto-start bridge if configured
if g:hanzo_auto_start
augroup HanzoAutoStart
autocmd!
autocmd VimEnter * call hanzo#StartBridge()
augroup END
endif
" Clean up on exit
augroup HanzoCleanup
autocmd!
autocmd VimLeavePre * call hanzo#StopBridge()
augroup END
+226
View File
@@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""
Hanzo-Vim Bridge Server
Bridges between:
- Vim (via channel on localhost:PORT, JSON mode)
- AI agents (via WebSocket on localhost:PORT+1)
This allows hanzo-tools-ide to control Vim just like VS Code.
Architecture:
┌─────────────┐ Channel ┌──────────────┐ WebSocket ┌─────────────┐
│ Vim │ ◄───────────────► │ Python Bridge │ ◄───────────────► │ AI Agent │
│ │ localhost:9228 │ │ localhost:9229 │ (hanzo-mcp) │
└─────────────┘ └──────────────┘ └─────────────┘
"""
import asyncio
import json
import sys
import uuid
from typing import Any
# Try websockets, fall back to simple socket server
try:
import websockets
HAS_WEBSOCKETS = True
except ImportError:
HAS_WEBSOCKETS = False
class VimBridge:
"""Bridge between Vim channel and WebSocket clients."""
def __init__(self, port: int = 9228):
self.port = port
self.vim_port = port # Vim connects here
self.ws_port = port + 1 # WebSocket on next port
self.vim_reader: asyncio.StreamReader | None = None
self.vim_writer: asyncio.StreamWriter | None = None
self.pending: dict[str, asyncio.Future] = {}
self.ws_clients: set = set()
async def start(self):
"""Start the bridge server."""
# Start TCP server for Vim channel
vim_server = await asyncio.start_server(
self.handle_vim_connection,
'localhost',
self.port,
)
print(f"Vim bridge listening on localhost:{self.port}")
if HAS_WEBSOCKETS:
# Start WebSocket server on port+1 for AI agents
ws_server = await websockets.serve(
self.handle_ws_connection,
'localhost',
self.ws_port,
)
print(f"WebSocket server on localhost:{self.ws_port}")
await asyncio.gather(
vim_server.serve_forever(),
ws_server.wait_closed(),
)
else:
print("websockets not installed, WebSocket disabled")
print("Install with: pip install websockets")
await vim_server.serve_forever()
async def handle_vim_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
"""Handle Vim channel connection."""
print("Vim connected")
self.vim_reader = reader
self.vim_writer = writer
try:
buffer = b""
while True:
data = await reader.read(4096)
if not data:
break
buffer += data
# Parse JSON messages (Vim sends [id, msg] format)
while b'\n' in buffer:
line, buffer = buffer.split(b'\n', 1)
if not line.strip():
continue
try:
msg = json.loads(line.decode('utf-8'))
await self.on_vim_message(msg)
except json.JSONDecodeError as e:
print(f"JSON error: {e}")
except Exception as e:
print(f"Vim connection error: {e}")
finally:
print("Vim disconnected")
self.vim_reader = None
self.vim_writer = None
async def handle_ws_connection(self, websocket):
"""Handle WebSocket connection from AI agent."""
print("AI agent connected")
self.ws_clients.add(websocket)
try:
async for message in websocket:
try:
msg = json.loads(message)
response = await self.handle_agent_request(msg)
await websocket.send(json.dumps(response))
except json.JSONDecodeError as e:
await websocket.send(json.dumps({"error": str(e)}))
except Exception as e:
print(f"WebSocket error: {e}")
finally:
self.ws_clients.discard(websocket)
print("AI agent disconnected")
async def on_vim_message(self, msg: Any):
"""Handle message from Vim."""
# Vim sends [id, data] for responses
if isinstance(msg, list) and len(msg) >= 2:
msg_id, data = msg[0], msg[1]
if msg_id in self.pending:
self.pending[msg_id].set_result(data)
return
# Broadcast events to WebSocket clients
for client in self.ws_clients:
try:
await client.send(json.dumps({"event": "vim", "data": msg}))
except Exception:
pass
async def send_to_vim(self, action: str, **params) -> dict:
"""Send request to Vim and wait for response."""
if not self.vim_writer:
return {"error": "Vim not connected"}
msg_id = str(uuid.uuid4())[:8]
request = {"id": msg_id, "action": action, **params}
# Create future for response
loop = asyncio.get_event_loop()
future: asyncio.Future = loop.create_future()
self.pending[msg_id] = future
try:
# Vim channel expects [id, msg] format
data = json.dumps([msg_id, request]) + "\n"
self.vim_writer.write(data.encode('utf-8'))
await self.vim_writer.drain()
# Wait for response
result = await asyncio.wait_for(future, timeout=30.0)
return result
except asyncio.TimeoutError:
return {"error": "Timeout"}
finally:
self.pending.pop(msg_id, None)
async def handle_agent_request(self, msg: dict) -> dict:
"""Handle request from AI agent."""
action = msg.get("action", "")
params = msg.get("params", {})
# Map IDE tool actions to Vim actions
action_map = {
# File operations
"file.open": "file.open",
"file.save": "file.save",
"file.close": "file.close",
"file.info": "file.info",
# Editor operations
"editor.selection": "editor.selection",
"editor.select": "editor.goto",
"editor.insert": "editor.insert",
"editor.replace": "editor.replace",
"editor.text": "editor.text",
"editor.goto": "editor.goto",
# Navigation
"nav.goto": "editor.goto",
# Commands
"command": "command",
# Diagnostics
"diagnostics": "diagnostics",
# REPL
"repl.start": "repl.start",
"repl.eval": "repl.eval",
"repl.stop": "repl.stop",
# IDE-specific aliases
"open": "file.open",
"save": "file.save",
"close": "file.close",
"status": "file.info",
"insert": "editor.insert",
"replace": "editor.replace",
"goto": "editor.goto",
}
vim_action = action_map.get(action, action)
return await self.send_to_vim(vim_action, **params)
async def main():
port = int(sys.argv[1]) if len(sys.argv) > 1 else 9228
bridge = VimBridge(port)
await bridge.start()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass