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:
@@ -1,18 +1,25 @@
|
||||
# ⚡ Neural
|
||||
# hanzo.vim
|
||||
|
||||
[](https://www.vim.org/) [](https://neovim.io/) [](https://github.com/dense-analysis/neural/actions?query=event%3Apush+workflow%3ACI+branch%3Amain++) [](https://discord.gg/5zFD6pQxDk)
|
||||
[](https://www.vim.org/) [](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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user