Repository: kiddos/gemini.nvim
Branch: master
Commit: 6e8474b0a099
Files: 16
Total size: 44.2 KB
Directory structure:
gitextract_p6arm2ii/
├── .gitignore
├── .luacheckrs
├── LICENSE
├── Makefile
├── README.md
├── doc/
│ └── gemini.nvim.txt
├── lua/
│ ├── gemini/
│ │ ├── api.lua
│ │ ├── chat.lua
│ │ ├── completion.lua
│ │ ├── config.lua
│ │ ├── instruction.lua
│ │ ├── task.lua
│ │ └── util.lua
│ └── gemini.lua
└── tests/
├── gemini/
│ └── api_spec.lua
└── init.lua
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
__pycache__
*.db
lua/gemini/*.so
.cache
build
doc/tags
================================================
FILE: .luacheckrs
================================================
read_globals = { "vim" }
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 Joseph
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
TESTS_INIT=tests/init.lua
TESTS_DIR=tests/
all:
cmake -B build
make -C build -j
clean:
rm -rf build
rm -rf lua/gemini/*.so
test:
@nvim \
--headless \
--noplugin \
-u ${TESTS_INIT} \
-c "PlenaryBustedDirectory ${TESTS_DIR} { minimal_init = '${TESTS_INIT}' }"
.PHONY: all clean test
================================================
FILE: README.md
================================================
# gemini.nvim
This plugin try to interface Google's Gemini API into neovim.
## Features
- Code Complete
- Code Explain
- Unit Test Generation
- Code Review
- Hints
- Chat
### Code Complete
https://github.com/user-attachments/assets/11ae6719-4f3f-41db-8ded-56db20e6e9f4
https://github.com/user-attachments/assets/34c38078-a028-47d2-acb1-49e03d0b4330
### Do some changes
https://github.com/user-attachments/assets/c0a001a2-a5fe-469d-ae01-3468d05b041c
### Code Explain
https://github.com/user-attachments/assets/6b2492ee-7c70-4bbc-937b-27bfa50f8944
### Unit Test generation
https://github.com/user-attachments/assets/0620a8a4-5ea6-431d-ba17-41c7d553f742
### Code Review
https://github.com/user-attachments/assets/9100ab70-f107-40de-96e2-fb4ea749c014
### Hints
https://github.com/user-attachments/assets/a36804e9-073f-4e3e-9178-56b139fd0c62
### Chat
https://github.com/user-attachments/assets/d3918d2a-4cf7-4639-bc21-689d4225ba6d
## Installation
- install `curl`
```
sudo apt install curl
```
```shell
export GEMINI_API_KEY="<your API key here>"
```
* [lazy.nvim](https://github.com/folke/lazy.nvim)
```lua
{
'kiddos/gemini.nvim',
opts = {}
}
```
* [packer.nvim](https://github.com/wbthomason/packer.nvim)
```lua
use { 'kiddos/gemini.nvim', opts = {} }
```
## Settings
default setting
```lua
{
model_config = {
model_id = 'gemini-2.5-flash',
temperature = 0.10,
top_k = 128,
response_mime_type = 'text/plain',
},
chat_config = {
enabled = true,
},
hints = {
enabled = true,
hints_delay = 2000,
insert_result_key = '<S-Tab>',
get_prompt = function(node, bufnr)
local code_block = vim.treesitter.get_node_text(node, bufnr)
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = [[
In struction: Use 1 or 2 sentences to describe what the following {filetype} function does:
```{filetype}
{code_block}
``]] .. '`'
prompt = prompt:gsub('{filetype}', filetype)
prompt = prompt:gsub('{code_block}', code_block)
return prompt
end
},
completion = {
enabled = true,
blacklist_filetypes = { 'help', 'qf', 'json', 'yaml', 'toml', 'xml' },
blacklist_filenames = { '.env' },
completion_delay = 800,
insert_result_key = '<S-Tab>',
move_cursor_end = true,
can_complete = function()
return vim.fn.pumvisible() ~= 1
end,
get_system_text = function()
return "You are a coding AI assistant that autocomplete user's code."
.. "\n* Your task is to provide code suggestion at the cursor location marked by <cursor></cursor>."
.. '\n* Your response does not need to contain explaination.'
end,
get_prompt = function(bufnr, pos)
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = 'Below is the content of a %s file `%s`:\n'
.. '```%s\n%s\n```\n\n'
.. 'Suggest the most likely code at <cursor></cursor>.\n'
.. 'Wrap your response in ``` ```\n'
.. 'eg.\n```\n```\n\n'
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local line = pos[1]
local col = pos[2]
local target_line = lines[line]
if target_line then
lines[line] = target_line:sub(1, col) .. '<cursor></cursor>' .. target_line:sub(col + 1)
else
return nil
end
local code = vim.fn.join(lines, '\n')
local abs_path = vim.api.nvim_buf_get_name(bufnr)
local filename = vim.fn.fnamemodify(abs_path, ':.')
prompt = string.format(prompt, filetype, filename, filetype, code)
return prompt
end
},
instruction = {
enabled = true,
menu_key = '<Leader><Leader><Leader>g',
prompts = {
{
name = 'Unit Test',
command_name = 'GeminiUnitTest',
menu = 'Unit Test 🚀',
get_prompt = function(lines, bufnr)
local code = vim.fn.join(lines, '\n')
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = 'Context:\n\n```%s\n%s\n```\n\n'
.. 'Objective: Write unit test for the above snippet of code\n'
return string.format(prompt, filetype, code)
end,
},
{
name = 'Code Review',
command_name = 'GeminiCodeReview',
menu = 'Code Review 📜',
get_prompt = function(lines, bufnr)
local code = vim.fn.join(lines, '\n')
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = 'Context:\n\n```%s\n%s\n```\n\n'
.. 'Objective: Do a thorough code review for the following code.\n'
.. 'Provide detail explaination and sincere comments.\n'
return string.format(prompt, filetype, code)
end,
},
{
name = 'Code Explain',
command_name = 'GeminiCodeExplain',
menu = 'Code Explain',
get_prompt = function(lines, bufnr)
local code = vim.fn.join(lines, '\n')
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = 'Context:\n\n```%s\n%s\n```\n\n'
.. 'Objective: Explain the following code.\n'
.. 'Provide detail explaination and sincere comments.\n'
return string.format(prompt, filetype, code)
end,
},
}
},
task = {
enabled = true,
get_system_text = function()
return 'You are an AI assistant that helps user write code.'
.. '\n* You should output the new content for the Current Opened File'
end,
get_prompt = function(bufnr, user_prompt)
local buffers = vim.api.nvim_list_bufs()
local file_contents = {}
for _, b in ipairs(buffers) do
if vim.api.nvim_buf_is_loaded(b) then -- Only get content from loaded buffers
local lines = vim.api.nvim_buf_get_lines(b, 0, -1, false)
local abs_path = vim.api.nvim_buf_get_name(b)
local filename = vim.fn.fnamemodify(abs_path, ':.')
local filetype = vim.api.nvim_get_option_value('filetype', { buf = b })
local file_content = table.concat(lines, "\n")
file_content = string.format("`%s`:\n\n```%s\n%s\n```\n\n", filename, filetype, file_content)
table.insert(file_contents, file_content)
end
end
local current_filepath = vim.api.nvim_buf_get_name(bufnr)
current_filepath = vim.fn.fnamemodify(current_filepath, ":.")
local context = table.concat(file_contents, "\n\n")
return string.format('%s\n\nCurrent Opened File: %s\n\nTask: %s',
context, current_filepath, user_prompt)
end
},
}
```
================================================
FILE: doc/gemini.nvim.txt
================================================
*gemini.nvim* Google's Gemini neovim binding
INTRODUCTION *gemini.nvim-intro*
This plugin try to interface Google's Gemini API into neovim.
INSTALLATION *gemini.nvim-install*
install curl
sudo apt install curl
setup API key
export GEMINI_API_KEY="<your API key here>"
lazy.nvim:
{
'kiddos/gemini.nvim',
opts = {}
}
CONFIGURATION *gemini.nvim-config*
{
model_config = {
completion_delay = 1000,
model_id = api.MODELS.GEMINI_2_0_FLASH,
temperature = 0.2,
top_k = 20,
max_output_tokens = 8196,
response_mime_type = 'text/plain',
},
chat_config = {
enabled = true,
},
hints = {
enabled = true,
hints_delay = 2000,
insert_result_key = '<S-Tab>',
get_prompt = function(node, bufnr)
local code_block = vim.treesitter.get_node_text(node, bufnr)
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = "
Instruction: Use 1 or 2 sentences to describe what the following {filetype} function does:
```{filetype}
{code_block}
```",
prompt = prompt:gsub('{filetype}', filetype)
prompt = prompt:gsub('{code_block}', code_block)
return prompt
end
}
completion = {
enabled = true,
blacklist_filetypes = { 'help', 'qf', 'json', 'yaml', 'toml' },
blacklist_filenames = { '.env' },
completion_delay = 600,
move_cursor_end = false,
insert_result_key = '<S-Tab>',
can_complete = function()
return vim.fn.pumvisible() ~= 1
end,
get_system_text = function()
return "You are a coding AI assistant that autocomplete user's code."
.. "\n* Your task is to provide code suggestion at the cursor location marked by <cursor></cursor>."
.. '\n* Do not wrap your code response in ```'
end,
get_prompt = function(bufnr, pos)
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = 'Below is the content of a %s file `%s`:\n'
.. '```%s\n%s\n```\n\n'
.. 'Suggest the most likely code at <cursor></cursor>.\n'
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local line = pos[1]
local col = pos[2]
local target_line = lines[line]
if target_line then
lines[line] = target_line:sub(1, col) .. '<cursor></cursor>' .. target_line:sub(col + 1)
else
return nil
end
local code = vim.fn.join(lines, '\n')
local filename = vim.api.nvim_buf_get_name(bufnr)
prompt = string.format(prompt, filetype, filename, filetype, code)
return prompt
end
},
instruction = {
enabled = true,
menu_key = '<C-o>',
prompts = {
{
name = 'Unit Test',
command_name = 'GeminiUnitTest',
menu = 'Unit Test 🚀',
get_prompt = function(lines, bufnr)
local code = vim.fn.join(lines, '\n')
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = 'Context:\n\n```%s\n%s\n```\n\n'
.. 'Objective: Write unit test for the above snippet of code\n'
return string.format(prompt, filetype, code)
end,
},
{
name = 'Code Review',
command_name = 'GeminiCodeReview',
menu = 'Code Review 📜',
get_prompt = function(lines, bufnr)
local code = vim.fn.join(lines, '\n')
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = 'Context:\n\n```%s\n%s\n```\n\n'
.. 'Objective: Do a thorough code review for the following code.\n'
.. 'Provide detail explaination and sincere comments.\n'
return string.format(prompt, filetype, code)
end,
},
{
name = 'Code Explain',
command_name = 'GeminiCodeExplain',
menu = 'Code Explain',
get_prompt = function(lines, bufnr)
local code = vim.fn.join(lines, '\n')
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = 'Context:\n\n```%s\n%s\n```\n\n'
.. 'Objective: Explain the following code.\n'
.. 'Provide detail explaination and sincere comments.\n'
return string.format(prompt, filetype, code)
end,
},
},
},
task = {
enabled = true,
get_system_text = function()
return 'You are an AI assistant that helps user write code.\n'
.. 'Your output should be a code diff for git.'
end,
get_prompt = function(bufnr, user_prompt)
local buffers = vim.api.nvim_list_bufs()
local file_contents = {}
for _, b in ipairs(buffers) do
if vim.api.nvim_buf_is_loaded(b) then -- Only get content from loaded buffers
local lines = vim.api.nvim_buf_get_lines(b, 0, -1, false)
local filename = vim.api.nvim_buf_get_name(b)
filename = vim.fn.fnamemodify(filename, ":.")
local filetype = vim.api.nvim_get_option_value('filetype', { buf = b })
local file_content = table.concat(lines, "\n")
file_content = string.format("`%s`:\n\n```%s\n%s\n```\n\n", filename, filetype, file_content)
table.insert(file_contents, file_content)
end
end
local current_filepath = vim.api.nvim_buf_get_name(bufnr)
current_filepath = vim.fn.fnamemodify(current_filepath, ":.")
local context = table.concat(file_contents, "\n\n")
return string.format('%s\n\nCurrent Opened File: %s\n\nTask: %s',
context, current_filepath, user_prompt)
end
},
}
COMMANDS *gemini.nvim-commands*
:GeminiTask ask Gemini to complete some task. this would use current opened buffer as context. this would also open up a diff pane in the current window
:GeminiApply this would apply the diff result from :GeminiTask
:GeminiChat ask Gemini to do something. this doesn't use any context
:GeminiCodeExplain select some code and ask Gemini what does it do
:GeminiCodeReview do some code review
:GeminiUnitTest let Gemini write your unit test
:GeminiFunctionHint give some function documentation
MAPPINGS *gemini.nvim-mappings*
<leader><leader><leader>g open a popup menu to select some task to do
<S-Tab> confirm gemini's autocomplete
================================================
FILE: lua/gemini/api.lua
================================================
local uv = vim.loop or vim.uv
local M = {}
local API = "https://generativelanguage.googleapis.com/v1beta/models/";
M.MODELS = {
GEMINI_3_FLASH_PREVIEW = 'gemini-3-flash-preview',
GEMINI_FLASH_LATEST = 'gemini-flash-latest',
GEMINI_3_1_PRO_PREVIEW = 'gemini-3.1-pro-preview',
GEMINI_3_1_FLASH_LITE_PREVIEW = 'gemini-3.1-flash-lite-preview',
GEMINI_FLASH_LITE_LATEST = 'gemini-flash-lite-latest',
GEMINI_2_5_PRO = 'gemini-2.5-pro',
GEMINI_2_5_FLASH = 'gemini-2.5-flash',
GEMINI_2_5_FLASH_LITE = 'gemini-2.5-flash-lite',
GEMINI_2_0_FLASH = 'gemini-2.0-flash',
GEMINI_2_0_FLASH_LITE = 'gemini-2.0-flash-lite',
}
M.gemini_generate_content = function(user_text, system_text, model_name, generation_config, callback)
local api_key = os.getenv("GEMINI_API_KEY")
if not api_key then
return ''
end
local api = API .. model_name .. ':generateContent?key=' .. api_key
local contents = {
{
parts = {
{
text = user_text
}
}
}
}
local data = {
contents = contents,
generationConfig = generation_config,
}
if system_text then
data.systemInstruction = {
parts = {
{
text = system_text,
}
}
}
end
local json_text = vim.json.encode(data)
local cmd = { 'curl', '--no-progress-meter', '-X', 'POST', api, '-H', 'Content-Type: application/json', '--data-binary', '@-' }
local opts = { stdin = json_text }
if callback then
return vim.system(cmd, opts, callback)
else
return vim.system(cmd, opts)
end
end
M.gemini_generate_content_stream = function(user_text, model_name, generation_config, callback)
local api_key = os.getenv("GEMINI_API_KEY")
if not api_key then
return
end
if not callback then
return
end
local api = API .. model_name .. ':streamGenerateContent?alt=sse&key=' .. api_key
local data = {
contents = {
{
parts = {
{
text = user_text
}
}
}
},
generationConfig = generation_config,
}
local json_text = vim.json.encode(data)
local stdin = uv.new_pipe()
local stdout = uv.new_pipe()
local stderr = uv.new_pipe()
local options = {
stdio = { stdin, stdout, stderr },
args = { api, '-X', 'POST', '-s', '-H', 'Content-Type: application/json', '-d', json_text }
}
uv.spawn('curl', options, function(code, _)
print("gemini chat finished exit code", code)
end)
local streamed_data = ''
uv.read_start(stdout, function(err, data)
if not err and data then
streamed_data = streamed_data .. data
local start_index = string.find(streamed_data, 'data:')
local end_index = string.find(streamed_data, '\r')
local json_text = ''
while start_index and end_index do
if end_index >= start_index then
json_text = string.sub(streamed_data, start_index + 5, end_index - 1)
callback(json_text)
end
streamed_data = string.sub(streamed_data, end_index + 1)
start_index = string.find(streamed_data, 'data:')
end_index = string.find(streamed_data, '\r')
end
end
end)
end
return M
================================================
FILE: lua/gemini/chat.lua
================================================
local config = require('gemini.config')
local util = require('gemini.util')
local api = require('gemini.api')
local M = {}
M.setup = function()
local model = config.get_config({ 'chat', 'model' })
if not model or not model.model_id then
return
end
vim.api.nvim_create_user_command('GeminiChat', M.start_chat, {
force = true,
desc = 'Google Gemini',
nargs = 1,
})
end
local context = {
chat_winnr = nil,
chat_number = 0,
}
local function get_bufnr(user_text)
local conf = config.get_config({ 'chat' })
if not conf then
vim.api.nvim_command('tabnew')
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_set_option_value('ft', 'markdown', { buf = bufnr })
return bufnr
end
local bufnr = nil
if not context.chat_winnr or not vim.api.nvim_win_is_valid(context.chat_winnr) or conf.window.position == 'new_tab' then
if conf.window.position == 'tab' or conf.window.position == 'new_tab' then
vim.api.nvim_command('tabnew')
elseif conf.window.position == 'left' then
vim.api.nvim_command('vertical topleft split new')
vim.api.nvim_win_set_width(0, conf.window.width or 80)
elseif conf.window.position == 'right' then
vim.api.nvim_command('rightbelow vnew')
vim.api.nvim_win_set_width(0, conf.window.width or 80)
end
context.chat_winnr = vim.api.nvim_tabpage_get_win(0)
bufnr = vim.api.nvim_win_get_buf(0)
end
vim.api.nvim_set_current_win(context.chat_winnr)
bufnr = bufnr or vim.api.nvim_win_get_buf(0)
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr })
vim.api.nvim_set_option_value('ft', 'markdown', { buf = bufnr })
vim.api.nvim_buf_set_name(bufnr, 'Chat' .. context.chat_number .. ': ' .. user_text)
return vim.api.nvim_win_get_buf(0)
end
M.start_chat = function(cxt)
local user_text = cxt.args
context.chat_number = context.chat_number + 1
local bufnr = get_bufnr(user_text)
local lines = { 'Generating response...' }
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
local generation_config = config.get_gemini_generation_config('chat')
local text = ''
local model_id = config.get_config({ 'chat', 'model', 'model_id' })
api.gemini_generate_content_stream(user_text, model_id, generation_config, function(json_text)
local model_response = vim.json.decode(json_text)
model_response = util.table_get(model_response, { 'candidates', 1, 'content', 'parts', 1, 'text' })
if not model_response then
return
end
text = text .. model_response
vim.schedule(function()
lines = vim.split(text, '\n')
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
end)
end)
end
return M
================================================
FILE: lua/gemini/completion.lua
================================================
local config = require('gemini.config')
local util = require('gemini.util')
local api = require('gemini.api')
local M = {}
local context = {
namespace_id = vim.api.nvim_create_namespace('gemini_completion'),
completion = nil,
}
M.setup = function()
local model = config.get_config({ 'completion', 'model' })
if not model or not model.model_id then
return
end
vim.api.nvim_create_autocmd('CursorMovedI', {
callback = function()
M.gemini_complete()
end,
})
local trigger_completion_key = config.get_config({ 'completion', 'trigger_key' })
if trigger_completion_key then
vim.api.nvim_set_keymap('i', trigger_completion_key, '', {
callback = function()
M.gemini_complete()
end,
})
end
local insert_key = config.get_config({ 'completion', 'insert_result_key' }) or '<S-Tab>'
vim.api.nvim_set_keymap('i', insert_key, '', {
callback = function()
M.insert_completion_result()
end,
})
end
local get_prompt_text = function(bufnr, pos)
local get_prompt = config.get_config({ 'completion', 'get_prompt' })
if not get_prompt then
vim.notify('prompt function is not found', vim.log.levels.WARN)
return nil
end
return get_prompt(bufnr, pos)
end
local function handle_result(win, pos, json_text)
local model_response = vim.json.decode(json_text)
local text = util.table_get(model_response, { 'candidates', 1, 'content', 'parts', 1, 'text' })
if model_response ~= nil and #model_response > 0 then
vim.schedule(function()
if model_response then
local code_blocks = util.strip_code(text)
local response = vim.fn.join(code_blocks, '\n\n')
M.show_completion_result(response, win, pos)
end
end)
end
local error_msg = util.table_get(model_response, { 'error', 'message' })
if error_msg then
vim.schedule(function()
vim.api.nvim_echo({{ error_msg, 'WarningMsg' }}, false, {})
end)
end
end
M._gemini_complete = function()
local bufnr = vim.api.nvim_get_current_buf()
local win = vim.api.nvim_get_current_win()
local pos = vim.api.nvim_win_get_cursor(win)
local user_text = get_prompt_text(bufnr, pos)
if not user_text then
return
end
local system_text = nil
local get_system_text = config.get_config({ 'completion', 'get_system_text' })
if get_system_text then
system_text = get_system_text()
end
local generation_config = config.get_gemini_generation_config('completion')
local model_id = config.get_config({ 'completion', 'model', 'model_id' })
api.gemini_generate_content(user_text, system_text, model_id, generation_config, function(result)
local json_text = result.stdout
if json_text and #json_text > 0 then
handle_result(win, pos, json_text)
end
end)
end
M.gemini_complete = util.debounce(function()
local blacklist_filetypes = config.get_config({ 'completion', 'blacklist_filetypes' }) or {}
local blacklist_filenames = config.get_config({ 'completion', 'blacklist_filenames' }) or {}
local buf = vim.api.nvim_get_current_buf()
local filetype = vim.api.nvim_get_option_value('filetype', { buf = buf })
local filename = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ":t")
if util.is_blacklisted(blacklist_filetypes, filetype) or util.is_blacklisted(blacklist_filenames, filename) then
return
end
if vim.fn.mode() ~= 'i' then
return
end
local can_complete = config.get_config({'completion', 'can_complete'})
if not can_complete or not can_complete() then
return
end
print('-- gemini complete --')
M._gemini_complete()
end, config.get_config({ 'completion', 'completion_delay' }) or 1000)
M.show_completion_result = function(result, win_id, pos)
local win = vim.api.nvim_get_current_win()
if win ~= win_id then
return
end
local current_pos = vim.api.nvim_win_get_cursor(win)
if current_pos[1] ~= pos[1] or current_pos[2] ~= pos[2] then
return
end
if vim.fn.mode() ~= 'i' then
return
end
local can_complete = config.get_config({'completion', 'can_complete'})
if not can_complete or not can_complete() then
return
end
local bufnr = vim.api.nvim_get_current_buf()
local options = {
id = 1,
virt_text = {},
virt_lines = {},
hl_mode = 'combine',
virt_text_pos = 'inline',
}
local content = result:match("^%s*(.-)%s*$")
for i, l in pairs(vim.split(content, '\n')) do
if i == 1 then
options.virt_text[1] = { l, 'Comment' }
else
options.virt_lines[i - 1] = { { l, 'Comment' } }
end
end
local row = pos[1]
local col = pos[2]
local id = vim.api.nvim_buf_set_extmark(bufnr, context.namespace_id, row - 1, col, options)
context.completion = {
content = content,
row = row,
col = col,
bufnr = bufnr,
}
vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI', 'InsertLeavePre' }, {
buffer = bufnr,
callback = function()
context.completion = nil
vim.api.nvim_buf_del_extmark(bufnr, context.namespace_id, id)
vim.api.nvim_command('redraw')
end,
once = true,
})
end
M.insert_completion_result = function()
if not context.completion then
return
end
local bufnr = vim.api.nvim_get_current_buf()
if not context.completion.bufnr == bufnr then
return
end
local row = context.completion.row - 1
local col = context.completion.col
local first_line = vim.api.nvim_buf_get_lines(0, row, row + 1, false)[1]
local lines = vim.split(context.completion.content, '\n')
lines[1] = string.sub(first_line, 1, col) .. lines[1] .. string.sub(first_line, col + 1)
vim.api.nvim_buf_set_lines(bufnr, row, row + 1, false, lines)
if config.get_config({ 'completion', 'move_cursor_end' }) == true then
local new_row = row + #lines
local new_col = #vim.api.nvim_buf_get_lines(0, new_row - 1, new_row, false)[1]
vim.api.nvim_win_set_cursor(0, { new_row, new_col })
end
context.completion = nil
end
return M
================================================
FILE: lua/gemini/config.lua
================================================
local api = require('gemini.api')
local util = require('gemini.util')
local M = {}
local default_temperature = 0.06
local default_top_k = 64
local default_chat_config = {
model = {
model_id = api.MODELS.GEMINI_2_5_FLASH,
temperature = default_temperature,
top_k = default_top_k,
},
window = {
position = "new_tab", -- left, right, new_tab, tab
width = 80, -- number of columns of the left/right window
}
}
local default_instruction_config = {
model = {
model_id = api.MODELS.GEMINI_2_5_FLASH,
temperature = default_temperature,
top_k = default_top_k,
},
menu_key = '<Leader><Leader><Leader>g',
prompts = {
{
name = 'Unit Test',
command_name = 'GeminiUnitTest',
menu = 'Unit Test 🚀',
get_prompt = function(lines, bufnr)
local code = vim.fn.join(lines, '\n')
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = 'Context:\n\n```%s\n%s\n```\n\n'
.. 'Objective: Write unit test for the above snippet of code\n'
return string.format(prompt, filetype, code)
end,
},
{
name = 'Code Review',
command_name = 'GeminiCodeReview',
menu = 'Code Review 📜',
get_prompt = function(lines, bufnr)
local code = vim.fn.join(lines, '\n')
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = 'Context:\n\n```%s\n%s\n```\n\n'
.. 'Objective: Do a thorough code review for the following code.\n'
.. 'Provide detail explaination and sincere comments.\n'
return string.format(prompt, filetype, code)
end,
},
{
name = 'Code Explain',
command_name = 'GeminiCodeExplain',
menu = 'Code Explain',
get_prompt = function(lines, bufnr)
local code = vim.fn.join(lines, '\n')
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local prompt = 'Context:\n\n```%s\n%s\n```\n\n'
.. 'Objective: Explain the following code.\n'
.. 'Provide detail explaination and sincere comments.\n'
return string.format(prompt, filetype, code)
end,
},
}
}
local default_completion_config = {
model = {
model_id = api.MODELS.GEMINI_2_5_FLASH,
temperature = default_temperature,
top_k = default_top_k,
},
blacklist_filetypes = { 'help', 'qf', 'json', 'yaml', 'toml', 'xml', 'ini' },
blacklist_filenames = { '.env' },
completion_delay = 800,
insert_result_key = '<S-Tab>',
move_cursor_end = true,
can_complete = function()
return vim.fn.pumvisible() ~= 1
end,
get_system_text = function()
return "I need you to act as a pure code-completion tool."
end,
get_prompt = function(bufnr, pos)
local abs_path = vim.api.nvim_buf_get_name(bufnr)
local filename = vim.fn.fnamemodify(abs_path, ':.')
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local radius = 10
local row = pos[1]
local col = pos[2]
local start_line = math.max(1, row - radius)
local line_count = vim.api.nvim_buf_line_count(bufnr)
local end_line = math.min(line_count, row + radius)
local lines = vim.api.nvim_buf_get_lines(bufnr, start_line, end_line, false)
local prompt = 'I have a %s file %s. Below is a snippet around the missing code block.\n'
prompt = string.format(prompt, filetype, filename)
prompt = prompt .. '[CODE BEFORE GAP]\n'
if start_line > 1 then
prompt = prompt .. "// ... (earlier code omitted)\n"
end
for i, line in ipairs(lines) do
local current_buffer_row = start_line + i - 1
if current_buffer_row == row then
local line_prefix = line:sub(1, col)
local line_suffix = line:sub(col + 1)
prompt = prompt .. line_prefix .. '\n\n[CODE AFTER GAP]\n' .. line_suffix .. '\n'
else
prompt = prompt .. line .. '\n'
end
end
if end_line < line_count then
prompt = prompt .. "// ... (later code omitted)\n"
end
prompt = prompt .. '\n[INSTRUCTION]\n'
prompt = prompt .. 'Generate EXACTLY the code that fills the gap between the two blocks. '
prompt = prompt .. 'Return ONLY the code. No explanation. Stop immediately when the logic is complete.\n'
return prompt
end
}
local default_task_config = {
model = {
model_id = api.MODELS.GEMINI_2_5_FLASH,
temperature = default_temperature,
top_k = default_top_k,
},
get_system_text = function()
return 'You are an AI assistant that helps user write code.'
.. '\n* You should output the new content for the Current Opened File'
end,
get_prompt = function(bufnr, user_prompt)
local buffers = vim.api.nvim_list_bufs()
local file_contents = {}
for _, b in ipairs(buffers) do
if vim.api.nvim_buf_is_loaded(b) then -- Only get content from loaded buffers
local lines = vim.api.nvim_buf_get_lines(b, 0, -1, false)
local abs_path = vim.api.nvim_buf_get_name(b)
local filename = vim.fn.fnamemodify(abs_path, ':.')
local filetype = vim.api.nvim_get_option_value('filetype', { buf = b })
local file_content = table.concat(lines, "\n")
file_content = string.format("`%s`:\n\n```%s\n%s\n```\n\n", filename, filetype, file_content)
table.insert(file_contents, file_content)
end
end
local current_filepath = vim.api.nvim_buf_get_name(bufnr)
current_filepath = vim.fn.fnamemodify(current_filepath, ":.")
local context = table.concat(file_contents, "\n\n")
return string.format('%s\n\nCurrent Opened File: %s\n\nTask: %s',
context, current_filepath, user_prompt)
end
}
M.set_config = function(opts)
opts = opts or {}
M.config = {
chat = vim.tbl_deep_extend('force', {}, default_chat_config, opts.chat_config or {}),
completion = vim.tbl_deep_extend('force', {}, default_completion_config, opts.completion or {}),
instruction = vim.tbl_deep_extend('force', {}, default_instruction_config, opts.instruction or {}),
task = vim.tbl_deep_extend('force', {}, default_task_config, opts.task or {})
}
end
M.get_config = function(keys)
return util.table_get(M.config, keys)
end
M.get_gemini_generation_config = function(space)
return {
temperature = M.get_config({ space, 'model', 'temperature' }) or default_temperature,
topK = M.get_config({ space, 'model', 'top_k' }) or default_top_k,
response_mime_type = 'text/plain',
thinkingConfig = {
thinkingBudget = 0
}
}
end
return M
================================================
FILE: lua/gemini/instruction.lua
================================================
local config = require('gemini.config')
local util = require('gemini.util')
local api = require('gemini.api')
local M = {}
M.setup = function()
local model = config.get_config({ 'instruction', 'model' })
if not model or not model.model_id then
return
end
local register_menu = function(prompt_item)
local get_prompt = prompt_item.get_prompt
if not get_prompt then
return
end
local command_name = prompt_item.command_name
if not command_name then
return
end
local gemini_generate = function(context)
local bufnr = vim.api.nvim_get_current_buf()
local lines
if not context.line1 or not context.line2 or context.line1 == context.line2 then
lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
else
lines = vim.api.nvim_buf_get_lines(bufnr, context.line1 - 1, context.line2 - 1, false)
end
local user_text = get_prompt(lines, bufnr)
local generation_config = config.get_gemini_generation_config('instruction')
vim.api.nvim_command('tabnew')
local new_buf = vim.api.nvim_get_current_buf()
vim.api.nvim_set_option_value('filetype', 'markdown', { buf = new_buf })
local model_id = config.get_config({ 'instruction', 'model', 'model_id' })
local text = ''
api.gemini_generate_content_stream(user_text, model_id, generation_config, function(json_text)
local model_response = vim.json.decode(json_text)
model_response = util.table_get(model_response, { 'candidates', 1, 'content', 'parts', 1, 'text' })
text = text .. model_response
vim.schedule(function()
lines = vim.split(text, '\n')
vim.api.nvim_buf_set_lines(new_buf, 0, -1, false, lines)
end)
end)
end
vim.api.nvim_create_user_command(command_name, gemini_generate, {
range = true,
})
local menu = prompt_item.menu
if menu then
menu = menu:gsub(' ', '\\ ')
vim.api.nvim_command('nnoremenu Gemini.' .. menu .. ' :' .. command_name .. '<CR>')
end
end
for _, item in pairs(config.get_config({ 'instruction', 'prompts' }) or {}) do
register_menu(item)
end
local register_keymap = function(mode, keymap)
vim.api.nvim_set_keymap(mode, keymap, '', {
expr = true,
noremap = true,
silent = true,
callback = function()
if vim.fn.pumvisible() == 0 then
vim.api.nvim_command('popup Gemini')
end
end
})
end
local modes = { 'n' }
for _, mode in pairs(modes) do
register_keymap(mode, config.get_config({ 'instruction', 'menu_key' }) or '<Leader><Leader><Leader>g')
end
end
return M
================================================
FILE: lua/gemini/task.lua
================================================
local config = require('gemini.config')
local api = require('gemini.api')
local util = require('gemini.util')
local M = {}
local context = {
bufnr = nil,
model_response = nil,
tmpfile = nil,
}
M.setup = function()
local model = config.get_config({ 'task', 'model' })
if not model or not model.model_id then
return
end
vim.api.nvim_create_user_command('GeminiTask', M.run_task, {
force = true,
desc = 'Google Gemini',
nargs = 1,
})
vim.api.nvim_create_user_command('GeminiApply', M.apply_patch, {
force = true,
desc = 'Apply patch',
})
end
local get_prompt_text = function(bufnr, user_prompt)
local get_prompt = config.get_config({ 'task', 'get_prompt' })
if not get_prompt then
vim.notify('prompt function is not found', vim.log.levels.WARN)
return nil
end
return get_prompt(bufnr, user_prompt)
end
local function open_file_in_split(filepath, ft)
local bufnr = vim.fn.bufnr(filepath, true)
if bufnr == 0 then
print("Error: Could not find or create buffer for file: " .. filepath)
return
end
vim.api.nvim_set_option_value('filetype', ft, {buf = bufnr})
local win_id = vim.api.nvim_open_win(bufnr, false, {
split = 'right',
win = 0,
})
vim.api.nvim_set_current_win(win_id)
vim.api.nvim_set_option_value('diff', true, { win = win_id })
vim.api.nvim_set_option_value('scrollbind', true, { win = win_id })
vim.api.nvim_set_option_value('cursorbind', true, { win = win_id })
end
local function diff_with_current_file(bufnr, new_content)
local tmpfile = vim.fn.tempname()
-- Write to the temp file
local f = io.open(tmpfile, "w")
if f then
f:write(new_content)
f:close()
end
local ft = vim.api.nvim_get_option_value('filetype', {buf = bufnr})
open_file_in_split(vim.fn.fnameescape(tmpfile), ft)
return tmpfile
end
M.run_task = function(ctx)
local bufnr = vim.api.nvim_get_current_buf()
local user_prompt = ctx.args
local prompt = get_prompt_text(bufnr, user_prompt)
local system_text = nil
local get_system_text = config.get_config({ 'task', 'get_system_text' })
if get_system_text then
system_text = get_system_text()
end
print('-- running Gemini Task...')
local generation_config = config.get_gemini_generation_config('task')
local model_id = config.get_config({ 'task', 'model', 'model_id' })
api.gemini_generate_content(prompt, system_text, model_id, generation_config, function(result)
local json_text = result.stdout
if json_text and #json_text > 0 then
local model_response = vim.json.decode(json_text)
model_response = util.table_get(model_response, { 'candidates', 1, 'content', 'parts', 1, 'text' })
if model_response ~= nil and #model_response > 0 then
model_response = util.strip_code(model_response)
vim.schedule(function()
model_response = vim.fn.join(model_response, '\n')
if #model_response then
context.bufnr = bufnr
context.model_response = model_response
context.tmpfile = diff_with_current_file(bufnr, model_response)
end
end)
end
end
end)
end
local function close_split_by_filename(tmpfile)
-- Get the buffer number for the temp file
local bufnr = vim.fn.bufnr(tmpfile)
if bufnr == -1 then
print("No buffer found for file: " .. tmpfile)
return
end
-- Find the window displaying this buffer
for _, win in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_get_buf(win) == bufnr then
vim.api.nvim_win_close(win, true) -- force close the window
vim.api.nvim_buf_delete(bufnr, { force = true, unload = true })
return
end
end
print("No window found showing the buffer for file: " .. tmpfile)
end
M.apply_patch = function()
if not context.bufnr or not context.tmpfile then
vim.notify('No Gemini task to apply.', vim.log.levels.WARN)
return
end
print('-- apply changes from Gemini')
local tmp_bufnr = vim.fn.bufnr(vim.fn.fnamemodify(context.tmpfile, ':p'))
if tmp_bufnr == -1 then
vim.notify('Could not find the temporary buffer for edited changes.', vim.log.levels.ERROR)
return
end
local edited_lines = vim.api.nvim_buf_get_lines(tmp_bufnr, 0, -1, false)
vim.api.nvim_buf_set_lines(context.bufnr, 0, -1, false, edited_lines)
if context.tmpfile then
close_split_by_filename(context.tmpfile)
end
context.bufnr = nil
context.model_response = nil
context.tmpfile = nil
end
return M
================================================
FILE: lua/gemini/util.lua
================================================
local M = {}
M.borderchars = { '─', '│', '─', '│', '╭', '╮', '╯', '╰' }
M.open_window = function(content, options)
local popup = require('plenary.popup')
options.borderchars = M.borderchars
local win_id, result = popup.create(content, options)
local bufnr = vim.api.nvim_win_get_buf(win_id)
local border = result.border
vim.api.nvim_set_option_value('ft', 'markdown', { buf = bufnr })
vim.api.nvim_set_option_value('wrap', true, { win = win_id })
local close_popup = function()
vim.api.nvim_win_close(win_id, true)
end
local keys = { '<C-q>', 'q' }
for _, key in pairs(keys) do
vim.api.nvim_buf_set_keymap(bufnr, 'n', key, '', {
silent = true,
callback = close_popup,
})
end
return win_id, bufnr, border
end
M.treesitter_has_lang = function(bufnr)
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local lang = vim.treesitter.language.get_lang(filetype)
return lang ~= nil
end
M.find_node_by_type = function(node_type)
local node = vim.treesitter.get_node()
while node do
local type = node:type()
if string.find(type, node_type) then
return node
end
local parent = node:parent()
if parent == node then
break
end
node = parent
end
return nil
end
M.debounce = function(callback, timeout)
local timer = nil
local f = function(...)
local t = { ... }
local handler = function()
callback(unpack(t))
end
if timer ~= nil then
timer:stop()
end
timer = vim.defer_fn(handler, timeout)
end
return f
end
M.table_get = function(t, id)
if type(id) ~= 'table' then return M.table_get(t, { id }) end
local success, res = true, t
for _, i in ipairs(id) do
success, res = pcall(function() return res[i] end)
if not success or res == nil then return end
end
return res
end
M.is_blacklisted = function(blacklist, filetype)
for _, ft in ipairs(blacklist) do
if string.find(filetype, ft, 1, true) ~= nil then
return true
end
end
return false
end
M.strip_code = function(text)
local code_blocks = {}
if not text then
return code_blocks
end
local pattern = "```(%w+)%s*(.-)%s*```"
for _, code_block in text:gmatch(pattern) do
table.insert(code_blocks, code_block)
end
if #code_blocks == 0 then
return { text }
end
return code_blocks
end
return M
================================================
FILE: lua/gemini.lua
================================================
local config = require('gemini.config')
local M = {}
local function is_nvim_version_ge(major, minor, patch)
local v = vim.version()
if v.major > major then
return true
elseif v.major == major then
if v.minor > minor then
return true
elseif v.minor == minor and v.patch >= patch then
return true
end
end
return false
end
M.setup = function(opts)
if not vim.fn.executable('curl') then
vim.notify('curl is not found', vim.log.levels.WARN)
return
end
if not is_nvim_version_ge(0, 10, 0) then
vim.notify('neovim version too old', vim.log.levels.WARN)
return
end
config.set_config(opts)
require('gemini.chat').setup()
require('gemini.instruction').setup()
require('gemini.completion').setup()
require('gemini.task').setup()
end
return M
================================================
FILE: tests/gemini/api_spec.lua
================================================
local api = require('gemini.api')
local util = require('gemini.util')
describe('api', function()
it('should send message', function()
local generation_config = {
temperature = 0.9,
top_k = 1.0,
max_output_tokens = 2048,
response_mime_type = 'text/plain',
}
local future = api.gemini_generate_content('hello there', nil, api.MODELS.GEMINI_2_0_FLASH, generation_config, nil)
local result = future:wait()
local stdout = result.stdout
print(stdout)
assert(#stdout > 0)
local result = vim.json.decode(stdout)
local model_response = util.table_get(result, { 'candidates', 1, 'content',
'parts', 1, 'text' })
assert(#model_response > 0)
end)
it('should send long message', function()
local generation_config = {
temperature = 0.9,
top_k = 1.0,
max_output_tokens = 2048,
response_mime_type = 'text/plain',
}
local long_message = string.rep('this is a very very long message ', 3000)
local future = api.gemini_generate_content(long_message, nil, api.MODELS.GEMINI_2_0_FLASH, generation_config, nil)
local result = future:wait()
local stdout = result.stdout
print(stdout)
assert(#stdout > 0)
local result = vim.json.decode(stdout)
local model_response = util.table_get(result, { 'candidates', 1, 'content',
'parts', 1, 'text' })
assert(#model_response > 0)
end)
end)
================================================
FILE: tests/init.lua
================================================
local plenary_dir = os.getenv("PLENARY_DIR") or "/tmp/plenary.nvim"
local is_not_a_directory = vim.fn.isdirectory(plenary_dir) == 0
if is_not_a_directory then
vim.fn.system({"git", "clone", "https://github.com/nvim-lua/plenary.nvim", plenary_dir})
end
vim.opt.rtp:append(".")
vim.opt.rtp:append(plenary_dir)
vim.cmd("runtime plugin/plenary.vim")
require("plenary.busted")
gitextract_p6arm2ii/
├── .gitignore
├── .luacheckrs
├── LICENSE
├── Makefile
├── README.md
├── doc/
│ └── gemini.nvim.txt
├── lua/
│ ├── gemini/
│ │ ├── api.lua
│ │ ├── chat.lua
│ │ ├── completion.lua
│ │ ├── config.lua
│ │ ├── instruction.lua
│ │ ├── task.lua
│ │ └── util.lua
│ └── gemini.lua
└── tests/
├── gemini/
│ └── api_spec.lua
└── init.lua
Condensed preview — 16 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (48K chars).
[
{
"path": ".gitignore",
"chars": 57,
"preview": "__pycache__\n*.db\n\nlua/gemini/*.so\n.cache\nbuild\n\ndoc/tags\n"
},
{
"path": ".luacheckrs",
"chars": 25,
"preview": "read_globals = { \"vim\" }\n"
},
{
"path": "LICENSE",
"chars": 1063,
"preview": "MIT License\n\nCopyright (c) 2025 Joseph\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
},
{
"path": "Makefile",
"chars": 300,
"preview": "TESTS_INIT=tests/init.lua\nTESTS_DIR=tests/\n\nall:\n\tcmake -B build\n\tmake -C build -j\n\nclean:\n\trm -rf build\n\trm -rf lua/gem"
},
{
"path": "README.md",
"chars": 6704,
"preview": "# gemini.nvim\n\nThis plugin try to interface Google's Gemini API into neovim.\n\n\n## Features\n\n- Code Complete\n- Code Expla"
},
{
"path": "doc/gemini.nvim.txt",
"chars": 6563,
"preview": "*gemini.nvim* Google's Gemini neovim binding\n\nINTRODUCTION *gemini.nvim-intro*\n\nTh"
},
{
"path": "lua/gemini/api.lua",
"chars": 3152,
"preview": "local uv = vim.loop or vim.uv\n\nlocal M = {}\n\nlocal API = \"https://generativelanguage.googleapis.com/v1beta/models/\";\n\nM."
},
{
"path": "lua/gemini/chat.lua",
"chars": 2692,
"preview": "local config = require('gemini.config')\nlocal util = require('gemini.util')\nlocal api = require('gemini.api')\n\nlocal M ="
},
{
"path": "lua/gemini/completion.lua",
"chars": 5955,
"preview": "local config = require('gemini.config')\nlocal util = require('gemini.util')\nlocal api = require('gemini.api')\n\nlocal M ="
},
{
"path": "lua/gemini/config.lua",
"chars": 6593,
"preview": "local api = require('gemini.api')\nlocal util = require('gemini.util')\n\nlocal M = {}\n\nlocal default_temperature = 0.06\nlo"
},
{
"path": "lua/gemini/instruction.lua",
"chars": 2671,
"preview": "local config = require('gemini.config')\nlocal util = require('gemini.util')\nlocal api = require('gemini.api')\n\nlocal M ="
},
{
"path": "lua/gemini/task.lua",
"chars": 4489,
"preview": "local config = require('gemini.config')\nlocal api = require('gemini.api')\nlocal util = require('gemini.util')\n\nlocal M ="
},
{
"path": "lua/gemini/util.lua",
"chars": 2377,
"preview": "local M = {}\n\nM.borderchars = { '─', '│', '─', '│', '╭', '╮', '╯', '╰' }\n\nM.open_window = function(content, options)\n l"
},
{
"path": "lua/gemini.lua",
"chars": 810,
"preview": "local config = require('gemini.config')\n\nlocal M = {}\n\nlocal function is_nvim_version_ge(major, minor, patch)\n local v "
},
{
"path": "tests/gemini/api_spec.lua",
"chars": 1413,
"preview": "local api = require('gemini.api')\nlocal util = require('gemini.util')\n\ndescribe('api', function()\n it('should send mess"
},
{
"path": "tests/init.lua",
"chars": 376,
"preview": "local plenary_dir = os.getenv(\"PLENARY_DIR\") or \"/tmp/plenary.nvim\"\nlocal is_not_a_directory = vim.fn.isdirectory(plenar"
}
]
About this extraction
This page contains the full source code of the kiddos/gemini.nvim GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 16 files (44.2 KB), approximately 12.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.