├── src └── neural │ ├── __init__.py │ └── provider │ ├── __init__.py │ └── openai.py ├── autoload ├── neural │ ├── pre_process │ │ ├── css.vim │ │ ├── sql.vim │ │ ├── lua.vim │ │ ├── cpp.vim │ │ ├── python.vim │ │ ├── vim.vim │ │ ├── cmake.vim │ │ ├── json.vim │ │ ├── markdown.vim │ │ ├── go.vim │ │ └── sh.vim │ ├── provider │ │ └── openai.vim │ ├── preview.vim │ ├── visual.vim │ ├── redact.vim │ ├── explain.vim │ ├── buffer.vim │ ├── config.vim │ └── job.vim └── neural.vim ├── ftplugin └── neuralbuf.vim ├── LICENSE.md ├── lua ├── neural.lua └── neural │ └── ui.lua ├── plugin └── neural.vim ├── pyproject.toml ├── doc ├── neural-development.txt └── neural.txt └── uv.lock /src/neural/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/neural/provider/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /autoload/neural/pre_process/css.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Pre-processing rules for CSS files. 3 | 4 | function! neural#pre_process#css#Process(buffer, input) abort 5 | let a:input.prompt = 'Write CSS code. ' . a:input.prompt 6 | endfunction 7 | -------------------------------------------------------------------------------- /autoload/neural/pre_process/sql.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Pre-processing rules for SQL files. 3 | 4 | function! neural#pre_process#sql#Process(buffer, input) abort 5 | let a:input.prompt = 'Write SQL code. ' . a:input.prompt 6 | endfunction 7 | -------------------------------------------------------------------------------- /autoload/neural/pre_process/lua.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Pre-processing rules for Lua files. 3 | 4 | function! neural#pre_process#lua#Process(buffer, input) abort 5 | let a:input.prompt = 'Write Lua code. ' 6 | \ . a:input.prompt 7 | endfunction 8 | -------------------------------------------------------------------------------- /autoload/neural/pre_process/cpp.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Pre-processing rules for C++ files. 3 | 4 | function! neural#pre_process#cpp#Process(buffer, input) abort 5 | let a:input.prompt = 'Write C++ syntax. ' 6 | \ . a:input.prompt 7 | endfunction 8 | -------------------------------------------------------------------------------- /autoload/neural/pre_process/python.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Pre-processing rules for Python 3 | 4 | function! neural#pre_process#python#Process(buffer, input) abort 5 | let a:input.prompt = 'Write Python syntax. ' 6 | \ . a:input.prompt 7 | endfunction 8 | -------------------------------------------------------------------------------- /autoload/neural/pre_process/vim.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Pre-processing rules for Vim script 3 | 4 | function! neural#pre_process#vim#Process(buffer, input) abort 5 | let a:input.prompt = 'Write Vim script syntax. ' 6 | \ . a:input.prompt 7 | endfunction 8 | -------------------------------------------------------------------------------- /autoload/neural/pre_process/cmake.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Pre-processing rules for CMake files. 3 | 4 | function! neural#pre_process#cmake#Process(buffer, input) abort 5 | let a:input.prompt = 'Write CMakeLists.txt syntax. ' 6 | \ . a:input.prompt 7 | endfunction 8 | -------------------------------------------------------------------------------- /autoload/neural/pre_process/json.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Pre-processing rules for JSON files. 3 | 4 | function! neural#pre_process#json#Process(buffer, input) abort 5 | let a:input.prompt = 'Write only JSON syntax, no explanation. ' 6 | \ . a:input.prompt 7 | endfunction 8 | -------------------------------------------------------------------------------- /autoload/neural/pre_process/markdown.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Pre-processing rules for markdown files. 3 | 4 | function! neural#pre_process#markdown#Process(buffer, input) abort 5 | let a:input.prompt = 'Write text in a markdown file. ' 6 | \ . a:input.prompt 7 | endfunction 8 | -------------------------------------------------------------------------------- /autoload/neural/provider/openai.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: A script describing how to use OpenAI compatible APIs with Neural 3 | 4 | function! neural#provider#openai#Get() abort 5 | return { 6 | \ 'name': 'openai', 7 | \ 'script_language': 'python', 8 | \ 'script': neural#GetScriptDir() . '/openai.py', 9 | \} 10 | endfunction 11 | -------------------------------------------------------------------------------- /autoload/neural/pre_process/go.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Pre-processing rules for Go files. 3 | 4 | function! neural#pre_process#go#Process(buffer, input) abort 5 | let l:has_package = search('^package ', 'wnc') != 0 6 | 7 | let a:input.prompt = 'Write golang code. ' 8 | \ . (l:has_package ? 'Do not write package main or main func. ' : '') 9 | \ . a:input.prompt 10 | endfunction 11 | -------------------------------------------------------------------------------- /ftplugin/neuralbuf.vim: -------------------------------------------------------------------------------- 1 | " Author: Anexon 2 | " Description: Neural Buffer for interacting with neural providers directly. 3 | 4 | call neural#config#Load() 5 | 6 | command! -buffer -nargs=0 NeuralCompletion :call neural#buffer#RunBuffer() 7 | 8 | nnoremap (neural_completion) :NeuralCompletion 9 | 10 | " Keybindings of Neural Buffer 11 | execute 'nnoremap ' . g:neural.buffer.completion_key . ' (neural_completion)' 12 | execute 'inoremap ' . g:neural.buffer.completion_key . ' (neural_completion)' 13 | -------------------------------------------------------------------------------- /autoload/neural/preview.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Preview windows for showing whatever information in. 3 | 4 | " Open a preview window and show some lines in it. 5 | " The second argument allows options to be passed in. 6 | " 7 | " filetype - The filetype to use, defaulting to 'neural-preview' 8 | " stay_here - If 1, stay in the window you came from. 9 | function! neural#preview#Show(lines, options) abort 10 | silent pedit NeuralPreviewWindow 11 | wincmd P 12 | 13 | setlocal modifiable 14 | setlocal noreadonly 15 | setlocal nobuflisted 16 | setlocal buftype=nofile 17 | setlocal bufhidden=wipe 18 | :%d 19 | call setline(1, a:lines) 20 | setlocal nomodifiable 21 | setlocal readonly 22 | let &l:filetype = get(a:options, 'filetype', 'neural-preview') 23 | 24 | if get(a:options, 'stay_here') 25 | wincmd p 26 | endif 27 | endfunction 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 Dense Analysis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /autoload/neural/visual.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Neural functions for working with Vim's visual mode. 3 | 4 | " Get all information for a visual range command we might want. 5 | " 6 | " Credit to xolox for most of this. 7 | function! neural#visual#GetRange() abort 8 | let [l:lnum, l:col] = getpos("'<")[1:2] 9 | let [l:end_lnum, l:end_col] = getpos("'>")[1:2] 10 | let l:end_offset = &selection is# 'inclusive' ? 1 : 2 11 | 12 | " Get the line range and slice the text we selected. 13 | let l:selection = getline(l:lnum, l:end_lnum) 14 | 15 | if !empty(l:selection) 16 | let l:selection[0] = l:selection[0][l:col - 1:] 17 | let l:selection[-1] = l:selection[-1][: l:end_col - l:end_offset] 18 | " Get the actual end column from the text length, as Vim can give us 19 | " the maximum int for visual line mode. 20 | let l:end_col = len(l:selection[-1]) 21 | endif 22 | 23 | return { 24 | \ 'lnum': l:lnum, 25 | \ 'col': l:col, 26 | \ 'end_lnum': l:end_lnum, 27 | \ 'end_col': l:end_col, 28 | \ 'selection': l:selection, 29 | \} 30 | endfunction 31 | -------------------------------------------------------------------------------- /autoload/neural/redact.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Redact passwords and secrets from code. 3 | 4 | " This will never be a perfect implementation, but effort can be made. 5 | " 6 | " The regex to use for keys in objects. 7 | let s:key_regex = 'password|secret[_ ]?key' 8 | " This substitution will be applied for each of the s:key_value_groups. 9 | let s:key_value_sub = '\1\3*\4' 10 | " Regular expressions to replace with s:key_value_sub 11 | let s:key_value_groups = map( 12 | \ [ 13 | \ '("(KEYS)")( *[=:]+ *r?")[^"]+(")', 14 | \ '("(KEYS)")( *[=:]+ *r?'')[^'']+('')', 15 | \ '(''(KEYS)'')( *[=:]+ *r?'')[^'']+('')', 16 | \ '(''(KEYS)'')( *[=:]+ *r?")[^"]+(")', 17 | \ '((KEYS))( *[=:]+ *r?")[^"]+(")', 18 | \ '((KEYS))( *[=:]+ *r?'')[^'']+('')', 19 | \ '((KEYS))( *[=:]+ *r?`)[^`]+(`)', 20 | \ ], 21 | \ {_, template -> '\v\c' . substitute(template, 'KEYS', s:key_regex, '')} 22 | \) 23 | 24 | function! neural#redact#PasswordsAndSecrets(unredacted) abort 25 | let l:line_list = [] 26 | 27 | for l:line in a:unredacted 28 | for l:regex in s:key_value_groups 29 | let l:line = substitute(l:line, l:regex, s:key_value_sub, 'g') 30 | endfor 31 | 32 | call add(l:line_list, l:line) 33 | endfor 34 | 35 | return l:line_list 36 | endfunction 37 | -------------------------------------------------------------------------------- /autoload/neural/pre_process/sh.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Pre-processing rules for shell scripts 3 | 4 | function! neural#pre_process#sh#GetScriptType(buffer) abort 5 | let l:current_syntax = getbufvar(a:buffer, 'current_syntax', '') 6 | 7 | if getbufvar(a:buffer, 'is_bash', 0) 8 | return 'bash' 9 | endif 10 | 11 | if l:current_syntax is# 'zsh' 12 | return 'zsh' 13 | endif 14 | 15 | if getbufvar(a:buffer, 'is_kornshell', 0) 16 | " https://www.youtube.com/watch?v=jRGrNDV2mKc 17 | return 'ksh' 18 | endif 19 | 20 | return 'sh' 21 | endfunction 22 | 23 | function! neural#pre_process#sh#GetScriptTypePrefix(script_type) abort 24 | if a:script_type is# 'bash' 25 | return 'Write Bash syntax. ' 26 | endif 27 | 28 | if a:script_type is# 'zsh' 29 | return 'Write zsh syntax. ' 30 | endif 31 | 32 | if a:script_type is# 'ksh' 33 | return 'Write Kornshell syntax. ' 34 | endif 35 | 36 | return 'Write shell script syntax. ' 37 | endfunction 38 | 39 | function! neural#pre_process#sh#Process(buffer, input) abort 40 | let l:script_type = neural#pre_process#sh#GetScriptType(a:buffer) 41 | let l:prefix = neural#pre_process#sh#GetScriptTypePrefix(l:script_type) 42 | 43 | let a:input.prompt = l:prefix . a:input.prompt 44 | endfunction 45 | -------------------------------------------------------------------------------- /lua/neural.lua: -------------------------------------------------------------------------------- 1 | -- External dependencies 2 | local UI = {} 3 | local AnimatedSign = {} 4 | local has_nui, _ = pcall(require, 'nui.input') 5 | local has_significant, _ = pcall(require, 'significant') 6 | 7 | if has_nui then 8 | UI = require('neural.ui') 9 | end 10 | 11 | if has_significant then 12 | AnimatedSign = require('significant') 13 | end 14 | 15 | local Neural = {} 16 | 17 | function Neural.setup(settings) 18 | -- Call the Vim script function to set up the shared configuration. 19 | vim.fn['neural#config#Set'](settings) 20 | end 21 | 22 | function Neural.prompt() 23 | local prompt_enabled = vim.g.neural.ui.prompt_enabled 24 | 25 | if has_nui and (prompt_enabled and prompt_enabled ~= 0) then 26 | UI.prompt( 27 | ' Neural ', 28 | function(value) 29 | if value == nil or value == '' then 30 | vim.fn['neural#ComplainNoPromptText']() 31 | else 32 | vim.fn['neural#Prompt'](value) 33 | end 34 | end 35 | ) 36 | else 37 | vim.fn.feedkeys(':Neural ') 38 | end 39 | end 40 | 41 | function Neural.start_animated_sign(line) 42 | local sign_enabled = vim.g.neural and vim.g.neural.ui.animated_sign_enabled 43 | 44 | if has_significant and (sign_enabled and sign_enabled ~= 0) and line > 0 then 45 | AnimatedSign.start_animated_sign(line, 'dots', 100) 46 | end 47 | end 48 | 49 | function Neural.stop_animated_sign(line) 50 | local sign_enabled = vim.g.neural and vim.g.neural.ui.animated_sign_enabled 51 | 52 | if has_significant and (sign_enabled and sign_enabled ~= 0) and line > 0 then 53 | AnimatedSign.stop_animated_sign(line, {unplace_sign=true}) 54 | end 55 | end 56 | 57 | return Neural 58 | -------------------------------------------------------------------------------- /lua/neural/ui.lua: -------------------------------------------------------------------------------- 1 | -- Author: Anexon 2 | -- Description: UI module for creating the prompt with nui.nvim input popup. 3 | 4 | local Input = require('nui.input') 5 | local Event = require('nui.utils.autocmd').event 6 | 7 | local UI = {} 8 | 9 | -- TODO: Expose option configuration. 10 | local opts = { 11 | default_value = '', 12 | winblend = 0, 13 | style = 'rounded' 14 | } 15 | 16 | -- Prompts the user for input. 17 | --- @param title string The title of the prompt. 18 | --- @param on_submit function The function to call when the user submits the prompt. 19 | function UI.prompt(title, on_submit) 20 | local input = Input({ 21 | position = {row = '85.2%', col = '50%'}, 22 | size = { width = '51.8%', height = '20%'}, 23 | relative = 'editor', 24 | border = { 25 | highlight = 'NeuralPromptBorder', 26 | style = opts.style, 27 | text = { 28 | top = title, 29 | top_align = 'center', 30 | }, 31 | }, 32 | win_options = { 33 | winblend = opts.winblend, 34 | }, 35 | }, { 36 | prompt = vim.g.neural.ui.prompt_icon .. ' ', 37 | default_value = opts.default_value, 38 | on_close = function() end, 39 | on_submit = function(value) 40 | on_submit(value) 41 | end, 42 | }) 43 | input:mount() 44 | 45 | -- Handle unmounting 46 | input:on(Event.BufLeave, function() 47 | input:unmount() 48 | end) 49 | 50 | local exit_keys = { 51 | {'n', 'q'}, 52 | {'n', ''}, 53 | {'i', ''}, 54 | {'n', ''}, 55 | {'i', ''}, 56 | } 57 | for _, key in ipairs(exit_keys) do 58 | input:map(key[1], key[2], function() 59 | input:unmount() 60 | end, { noremap = true }) 61 | end 62 | end 63 | 64 | return UI 65 | -------------------------------------------------------------------------------- /plugin/neural.vim: -------------------------------------------------------------------------------- 1 | " Author: Anexon , w0rp 2 | " Description: Main entry point for the Neural Vim plugin 3 | 4 | if exists('g:loaded_neural') 5 | finish 6 | endif 7 | 8 | let g:loaded_neural = 1 9 | 10 | " A flag for detecting if the required features are set. 11 | if has('nvim') 12 | let s:has_features = has('timers') && has('nvim-0.2.0') 13 | else 14 | " Check if Job and Channel functions are available, instead of the 15 | " features. This works better on old MacVim versions. 16 | let s:has_features = has('timers') && exists('*job_start') && exists('*ch_close_in') 17 | endif 18 | 19 | if !s:has_features 20 | " Only output a warning if editing some special files. 21 | if index(['', 'gitcommit'], &filetype) == -1 22 | " no-custom-checks 23 | echoerr 'Neural requires NeoVim >= 0.2.0 or Vim 8 with +timers +job +channel' 24 | " no-custom-checks 25 | echoerr 'Please update your editor appropriately.' 26 | endif 27 | 28 | " Stop here, as it won't work. 29 | finish 30 | endif 31 | 32 | " Have Neural write to the buffer given a prompt. 33 | command! -nargs=? Neural :call neural#Prompt() 34 | " Stop Neural doing anything. 35 | command! -nargs=0 NeuralStop :call neural#Stop() 36 | " Create a completion buffer. 37 | command! -nargs=? NeuralBuffer :call neural#buffer#CreateBuffer() 38 | " Have Neural explain the visually selected lines. 39 | command! -range NeuralExplain :call neural#explain#SelectedLines() 40 | " Have Neural print the prompt that will be sent. 41 | command! -nargs=? NeuralViewPrompt :call neural#ViewPrompt() 42 | 43 | " mappings for commands 44 | nnoremap (neural_prompt) :call neural#OpenPrompt() 45 | nnoremap (neural_stop) :call neural#Stop() 46 | nnoremap (neural_buffer) :call neural#buffer#CreateBuffer({}) 47 | vnoremap (neural_explain) :NeuralExplain 48 | nnoremap (neural_view_prompt) :call neural#ViewPrompt() 49 | 50 | " Set default keybinds for Neural unless we're told not to. We should almost 51 | " never define keybinds by default in a plugin, but we can add only a few to 52 | " make things convenient for users. 53 | if has_key(g:, 'neural') && get(g:neural, 'set_default_keybinds') 54 | if empty(maparg("\", 'n')) 55 | nnoremap (neural_stop) 56 | endif 57 | endif 58 | -------------------------------------------------------------------------------- /autoload/neural/explain.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: Explain a visual selection with Neural. 3 | 4 | let s:current_job = get(s:, 'current_explain_job', 0) 5 | 6 | function! s:AddOutputLine(buffer, job_data, line) abort 7 | call add(a:job_data.output_lines, a:line) 8 | endfunction 9 | 10 | function! s:AddErrorLine(buffer, job_data, line) abort 11 | call add(a:job_data.error_lines, a:line) 12 | endfunction 13 | 14 | function! s:HandleOutputEnd(buffer, job_data, exit_code) abort 15 | " Output an error message from the program if something goes wrong. 16 | if a:exit_code != 0 17 | " Complain when something goes wrong. 18 | call neural#OutputErrorMessage(join(a:job_data.error_lines, "\n")) 19 | else 20 | let l:i = 0 21 | 22 | while l:i < len(a:job_data.output_lines) 23 | if !empty(a:job_data.output_lines[l:i]) 24 | break 25 | endif 26 | 27 | let l:i += 1 28 | endwhile 29 | 30 | call neural#preview#Show( 31 | \ a:job_data.output_lines[l:i :], 32 | \ {'stay_here': 1}, 33 | \) 34 | endif 35 | 36 | let s:current_job = 0 37 | endfunction 38 | 39 | function! neural#explain#Cleanup() abort 40 | if s:current_job 41 | call neural#job#Stop(s:current_job) 42 | let s:current_job = 0 43 | endif 44 | endfunction 45 | 46 | function! neural#explain#SelectedLines() abort 47 | " Reload the Neural config if needed. 48 | call neural#config#Load() 49 | " Stop Neural doing anything else if explaining code. 50 | call neural#Cleanup() 51 | 52 | let l:range = neural#visual#GetRange() 53 | let l:buffer = bufnr('') 54 | 55 | let [l:provider, l:command] = neural#GetCommand(l:buffer) 56 | 57 | let l:job_data = { 58 | \ 'output_lines': [], 59 | \ 'error_lines': [], 60 | \} 61 | let l:job_id = neural#job#Start(l:command, { 62 | \ 'mode': 'nl', 63 | \ 'out_cb': {job_id, line -> s:AddOutputLine(l:buffer, l:job_data, line)}, 64 | \ 'err_cb': {job_id, line -> s:AddErrorLine(l:buffer, l:job_data, line)}, 65 | \ 'exit_cb': {job_id, exit_code -> s:HandleOutputEnd(l:buffer, l:job_data, exit_code)}, 66 | \}) 67 | 68 | if l:job_id > 0 69 | let l:lines = neural#redact#PasswordsAndSecrets(l:range.selection) 70 | 71 | let l:input = { 72 | \ 'config': l:provider.config, 73 | \ 'prompt': "Explain these lines:\n\n" . join(l:lines, "\n"), 74 | \} 75 | call neural#job#SendRaw(l:job_id, json_encode(l:input) . "\n") 76 | else 77 | call neural#OutputErrorMessage('Failed to run ' . l:provider.name) 78 | 79 | return 80 | endif 81 | 82 | let s:current_job = l:job_id 83 | endfunction 84 | -------------------------------------------------------------------------------- /autoload/neural/buffer.vim: -------------------------------------------------------------------------------- 1 | " Author: Anexon 2 | " Description: A Neural Scratch Buffer acts as a playground for interacting with 3 | " Neural providers directly, sending all content of the buffer to the provider. 4 | 5 | scriptencoding utf-8 6 | 7 | call neural#config#Load() 8 | 9 | function! s:GetOptions(options_dict_string) abort 10 | call neural#config#Load() 11 | 12 | " TODO: Set buffer name based on provider. 13 | let l:options = { 14 | \ 'name': 'Neural Buffer', 15 | \ 'create_mode': g:neural.buffer.create_mode, 16 | \ 'wrap': g:neural.buffer.wrap, 17 | \} 18 | 19 | " Override default options for the buffer instance. 20 | if !empty(a:options_dict_string) 21 | let l:options_dict = eval(a:options_dict_string) 22 | 23 | if has_key(l:options_dict, 'name') 24 | let l:options.name = l:options_dict.name 25 | endif 26 | 27 | if has_key(l:options_dict, 'create_mode') 28 | let l:options.create_mode = l:options_dict.create_mode 29 | endif 30 | 31 | if has_key(l:options_dict, 'wrap') 32 | let l:options.wrap = l:options_dict.wrap 33 | endif 34 | endif 35 | 36 | return l:options 37 | endfunction 38 | 39 | function! neural#buffer#CreateBuffer(options) abort 40 | let l:buffer_options = s:GetOptions(a:options) 41 | 42 | " TODO: Add auto incrementing buffer names instead of switching. 43 | if bufexists(l:buffer_options.name) 44 | execute 'buffer' bufnr(l:buffer_options.name) 45 | setlocal filetype=neuralbuf 46 | setlocal buftype=nofile 47 | setlocal bufhidden=hide 48 | setlocal noswapfile 49 | else 50 | if l:buffer_options.create_mode is# 'vertical' 51 | vertical new 52 | elseif l:buffer_options.create_mode is# 'horizontal' 53 | new 54 | else 55 | call neural#OutputErrorMessage('Invalid create mode for Neural Buffer. Must be horizontal or vertical.') 56 | endif 57 | 58 | if l:buffer_options.wrap 59 | setlocal wrap linebreak 60 | else 61 | setlocal nowrap nolinebreak 62 | endif 63 | 64 | execute 'file ' . escape(l:buffer_options.name, ' ') 65 | setlocal filetype=neuralbuf 66 | setlocal buftype=nofile 67 | setlocal bufhidden=hide 68 | setlocal noswapfile 69 | endif 70 | 71 | " Switch into insert mode when entering the buffer 72 | startinsert 73 | endfunction 74 | 75 | function! neural#buffer#RunBuffer() abort 76 | let l:buffer_contents = join(getline(1, '$'), "\n") 77 | let l:options = { 78 | \ 'line': line('$'), 79 | \ 'echo': 0, 80 | \} 81 | 82 | if &filetype is# 'neuralbuf' 83 | call neural#Run(l:buffer_contents, l:options) 84 | endif 85 | endfunction 86 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # This enables: `pip install -e .` 2 | [tool.setuptools] 3 | # map the “root” package directory to src/ 4 | package-dir = { "" = "src" } 5 | # turn on automatic package discovery 6 | packages = { find = { where = ["src"], include = ["*"] } } 7 | 8 | [build-system] 9 | # NOTE: We should *not* add "wheel" here! 10 | # See: https://www.reddit.com/r/Python/comments/1jwbymm/psa_you_should_remove_wheel_from_your/ 11 | requires = ["setuptools ~=77.0"] 12 | build-backend = "setuptools.build_meta" 13 | 14 | [project] 15 | name = "neural" 16 | version = "0.2.0" 17 | description = "A coding agent" 18 | authors = [ 19 | {name = "Georgi Angelchev", email = "george@denseanalysis.org"}, 20 | {name = "Andrew Wray", email = "andrew@denseanalysis.org"}, 21 | ] 22 | readme = "README.md" 23 | requires-python = ">=3.10" 24 | 25 | # Core dependencies -> NONE 26 | dependencies = [ 27 | ] 28 | 29 | [dependency-groups] 30 | dev = [ 31 | "setuptools", 32 | "pyright==1.1.402", 33 | "pytest>=8.4.1", 34 | "ruff==0.10.0", 35 | "vim-vint===0.3.21", 36 | ] 37 | 38 | [tool.pyright] 39 | venvPath = "./" 40 | venv = ".venv" 41 | pythonVersion = "3.10" 42 | extraPaths = ["src"] 43 | reportMissingImports = true 44 | reportMissingModuleSource = "error" 45 | typeCheckingMode = "strict" 46 | 47 | # Settings for ruff 48 | [tool.ruff] 49 | line-length = 79 50 | target-version = "py310" 51 | 52 | # Include all default rules from the ruff "all" baseline, but we'll fine-tune. 53 | lint.select = [ 54 | "E", # PEP8 errors (confirms with PEP8) 55 | "W", # PEP8 warnings (confirms with PEP8) 56 | "I", # isort (sorts imports) 57 | "B", # flake8-bugbear rules (catches mulable by mistake and more) 58 | "A", # flake8-builtins (stop accidental shadowing) 59 | "COM", # flake8-commas (no accidental tuples and more) 60 | "ASYNC", # flake8-async rules (fixes async issues) 61 | "FBT", # flake8-boolean-trap rules (stops the Boolean Trap) 62 | "F", # Pyflakes (fixes common mistakes) 63 | "FAST", # FastAPI rules (fixes FastAPI misusage) 64 | "RUF", # ruff-specific rules (fixes common bugs) 65 | "UP", # pyupgrade (modernises code) 66 | "C90", # mccabe (no overly-complex code) 67 | ] 68 | 69 | [tool.ruff.lint.mccabe] 70 | # Flag errors (`C901`) whenever the complexity level exceeds 20 71 | # This is 20 possible branches through code in a single function. 72 | # See also: https://ntrs.nasa.gov/api/citations/20205011566/downloads/20205011566.pdf 73 | max-complexity = 20 74 | 75 | # ruff isort settings 76 | [tool.ruff.lint.isort] 77 | # Black-compatible isort settings for ruff 78 | combine-as-imports = true 79 | force-wrap-aliases = true 80 | split-on-trailing-comma = true 81 | 82 | section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] 83 | # You can keep third party libraries grouped together by adding them here. 84 | # known-third-party = [] 85 | 86 | # pytest configuration 87 | [tool.pytest.ini_options] 88 | minversion = "7.0" 89 | addopts = "--strict-markers --tb=short" 90 | testpaths = ["test/python"] 91 | python_files = ["test_*.py"] 92 | markers = [ 93 | "slow: mark slow tests", 94 | "integration: mark integration tests", 95 | "allow_network: allow network connections for this test", 96 | "unit: mark unit tests", 97 | ] 98 | -------------------------------------------------------------------------------- /autoload/neural/config.vim: -------------------------------------------------------------------------------- 1 | scriptencoding utf-8 2 | " Author: w0rp 3 | " Description: Configuration of Neural with a default config. 4 | 5 | " Track modifications to g:neural, in case we set it again. 6 | let s:last_dictionary = get(s:, 'last_dictionary', {}) 7 | 8 | " Defaults for different providers. 9 | let s:provider_defaults = { 10 | \ 'openai': { 11 | \ 'type': 'openai', 12 | \ 'url': 'https://api.openai.com', 13 | \ 'api_key': '', 14 | \ 'frequency_penalty': 0.1, 15 | \ 'max_tokens': 1024, 16 | \ 'model': 'gpt-3.5-turbo-instruct', 17 | \ 'use_chat_api': v:false, 18 | \ 'presence_penalty': 0.1, 19 | \ 'temperature': 0.2, 20 | \ 'top_p': 1, 21 | \ }, 22 | \} 23 | 24 | " Defaults for general settings. 25 | let s:defaults = { 26 | \ 'pre_process': { 27 | \ 'enabled': v:true, 28 | \ }, 29 | \ 'set_default_keybinds': v:true, 30 | \ 'ui': { 31 | \ 'prompt_enabled': v:true, 32 | \ 'prompt_icon': '🗲', 33 | \ 'animated_sign_enabled': v:true, 34 | \ 'echo_enabled': v:true, 35 | \ }, 36 | \ 'buffer': { 37 | \ 'completion_key': '', 38 | \ 'create_mode': 'vertical', 39 | \ 'wrap': v:true, 40 | \ }, 41 | \ 'providers': [], 42 | \} 43 | 44 | function! neural#config#DeepMerge(into, from) abort 45 | for [l:key, l:value] in items(a:from) 46 | if type(l:value) is v:t_dict && type(get(a:into, l:key, 0)) is v:t_dict 47 | let a:into[key] = neural#config#DeepMerge(a:into[l:key], l:value) 48 | else 49 | let a:into[key] = l:value 50 | endif 51 | endfor 52 | 53 | return a:into 54 | endfunction 55 | 56 | " Set the shared configuration for Neural. 57 | function! neural#config#Set(settings) abort 58 | let g:neural = a:settings 59 | call neural#config#Load() 60 | endfunction 61 | 62 | function! neural#config#ConvertLegacySettings(dictionary) abort 63 | " Use the old selection to know which source to copy to providers. 64 | let l:selected = get(a:dictionary, 'selected', 'openai') 65 | 66 | " Replace 'source' with newer 'providers' 67 | if has_key(a:dictionary, 'source') && !has_key(a:dictionary, 'providers') 68 | let l:source = remove(a:dictionary, 'source') 69 | let a:dictionary.providers = [] 70 | 71 | if type(l:source) is v:t_dict 72 | " Default the configuration to the openai config for chatgpt. 73 | let l:settings = get(l:source, l:selected, get(l:source, 'openai')) 74 | " Keep old behavior to default the chatgpt key to the openai key. 75 | let l:default_api_key = get(get(l:source, 'openai', {}), 'api_key', '') 76 | 77 | if type(l:settings) is v:t_dict 78 | let l:settings = copy(l:settings) 79 | let l:settings.type = l:selected 80 | 81 | if l:selected is# 'chatgpt' 82 | let l:settings.use_chat_api = v:true 83 | endif 84 | 85 | if empty(get(l:settings, 'api_key')) 86 | let l:settings.api_key = l:default_api_key 87 | endif 88 | 89 | call add(a:dictionary.providers, l:settings) 90 | endif 91 | endif 92 | endif 93 | 94 | " Remove the 'selected' key if set. 95 | if has_key(a:dictionary, 'selected') 96 | call remove(a:dictionary, 'selected') 97 | endif 98 | endfunction 99 | 100 | " Merge defaults for different provider types in. 101 | function! neural#config#MergeProviderDefaults(providers) abort 102 | let l:merged_providers = [] 103 | 104 | if type(a:providers) is v:t_list && !empty(a:providers) 105 | for l:source in a:providers 106 | let l:type = get(l:source, 'type', v:null) 107 | 108 | call add(l:merged_providers, neural#config#DeepMerge( 109 | \ deepcopy(get(s:provider_defaults, l:type, {})), 110 | \ l:source, 111 | \)) 112 | endfor 113 | else 114 | " Default providers to just openai with no API key. 115 | let l:merged_providers = [deepcopy(s:provider_defaults.openai)] 116 | endif 117 | 118 | return l:merged_providers 119 | endfunction 120 | 121 | function! neural#config#Load() abort 122 | let l:dictionary = get(g:, 'neural', {}) 123 | 124 | " Merge the Dictionary with defaults again if g:neural changed. 125 | if l:dictionary isnot# s:last_dictionary 126 | " Create a shallow copy to modify 127 | let l:dictionary = copy(l:dictionary) 128 | call neural#config#ConvertLegacySettings(l:dictionary) 129 | let l:dictionary.providers = neural#config#MergeProviderDefaults( 130 | \ get(l:dictionary, 'providers', v:null) 131 | \) 132 | 133 | let s:last_dictionary = neural#config#DeepMerge( 134 | \ deepcopy(s:defaults), 135 | \ l:dictionary, 136 | \) 137 | let g:neural = s:last_dictionary 138 | endif 139 | endfunction 140 | -------------------------------------------------------------------------------- /src/neural/provider/openai.py: -------------------------------------------------------------------------------- 1 | """ 2 | A Neural provider for GPT conversations. 3 | """ 4 | import json 5 | import platform 6 | import ssl 7 | import sys 8 | import urllib.error 9 | import urllib.request 10 | from typing import Any 11 | 12 | OPENAI_DATA_HEADER = 'data: ' 13 | OPENAI_DONE = '[DONE]' 14 | 15 | 16 | class Config: 17 | """ 18 | The sanitised configuration. 19 | """ 20 | def __init__( 21 | self, 22 | *, 23 | url: str, 24 | api_key: str, 25 | model: str, 26 | use_chat_api: bool, 27 | temperature: float, 28 | top_p: float, 29 | max_tokens: int, 30 | presence_penalty: float, 31 | frequency_penalty: float, 32 | ): 33 | self.url = url 34 | self.api_key = api_key 35 | self.model = model 36 | self.use_chat_api = use_chat_api 37 | self.temperature = temperature 38 | self.top_p = top_p 39 | self.max_tokens = max_tokens 40 | self.presence_penalty = presence_penalty 41 | self.frequency_penalty = frequency_penalty 42 | 43 | 44 | def get_openai_completion( 45 | config: Config, 46 | prompt: str | list[dict[str, str]], 47 | ) -> None: 48 | headers = { 49 | "Content-Type": "application/json", 50 | } 51 | 52 | if config.api_key: 53 | headers["Authorization"] = f"Bearer {config.api_key}" 54 | 55 | data: dict[str, Any] = { 56 | "model": config.model, 57 | "temperature": config.temperature, 58 | "max_tokens": config.max_tokens, 59 | "top_p": 1, 60 | "presence_penalty": config.presence_penalty, 61 | "frequency_penalty": config.frequency_penalty, 62 | "stream": True, 63 | } 64 | 65 | if config.use_chat_api: 66 | data["messages"] = ( 67 | [{"role": "user", "content": prompt}] 68 | if isinstance(prompt, str) else 69 | prompt 70 | ) 71 | else: 72 | data["prompt"] = prompt 73 | 74 | req = urllib.request.Request( 75 | ( 76 | f'{config.url}/v1/chat/completions' 77 | if config.use_chat_api else 78 | f'{config.url}/v1/completions' 79 | ), 80 | data=json.dumps(data).encode("utf-8"), 81 | headers=headers, 82 | method="POST", 83 | ) 84 | role: str | None = None 85 | 86 | # Disable SSL certificate verification on macOS. 87 | # This is bad for security, and we need to deal with SSL errors better. 88 | # 89 | # This is the error: 90 | # urllib.error.URLError: # noqa 91 | context = ( 92 | ssl._create_unverified_context() # type: ignore 93 | if platform.system() == "Darwin" else 94 | None 95 | ) 96 | 97 | with urllib.request.urlopen(req, context=context) as response: 98 | while True: 99 | line_bytes = response.readline() 100 | 101 | if not line_bytes: 102 | break 103 | 104 | line = line_bytes.decode("utf-8", errors="replace") 105 | line_data = ( 106 | line[len(OPENAI_DATA_HEADER):-1] 107 | if line.startswith(OPENAI_DATA_HEADER) else 108 | None 109 | ) 110 | 111 | if line_data and line_data != OPENAI_DONE: 112 | openai_obj = json.loads(line_data) 113 | 114 | if config.use_chat_api: 115 | delta = openai_obj["choices"][0]["delta"] 116 | # The role is typically in the first delta only. 117 | role = delta.get("role", role) 118 | 119 | if role == "assistant" and "content" in delta: 120 | print(delta["content"], end="", flush=True) 121 | else: 122 | print(openai_obj["choices"][0]["text"], end="", flush=True) 123 | 124 | print() 125 | 126 | 127 | def load_config(raw_config: dict[str, Any]) -> Config: 128 | # TODO: Add range validation for request parameters. 129 | if not isinstance(raw_config, dict): # type: ignore 130 | raise ValueError("openai config is not a dictionary") 131 | 132 | url = raw_config.get('url') 133 | 134 | if url is None: 135 | url = 'https://api.openai.com' 136 | elif not isinstance(url, str): 137 | raise ValueError("url must be a string") 138 | elif not url.startswith("http://") and not url.startswith("https://"): 139 | raise ValueError("url must start with http(s)://") 140 | 141 | api_key = raw_config.get('api_key') 142 | 143 | if not isinstance(api_key, str | None): # type: ignore 144 | raise ValueError(f"api_key is an invalid type: {type(api_key)}") 145 | 146 | model = raw_config.get('model') 147 | 148 | if not isinstance(model, str) or not model: 149 | raise ValueError("model is not defined") 150 | 151 | use_chat_api = raw_config.get('use_chat_api') 152 | 153 | if use_chat_api is None: 154 | # Default to the older completions API if using certain older models. 155 | use_chat_api = model not in ( 156 | 'ada', 157 | 'babbage', 158 | 'curie', 159 | 'davinci', 160 | 'gpt-3.5-turbo-instruct', 161 | 'text-ada-001', 162 | 'text-babbage-001', 163 | 'text-curie-001', 164 | 'text-davinci-002', 165 | 'text-davinci-003', 166 | ) 167 | elif not isinstance(use_chat_api, bool): 168 | raise ValueError("use_chat_api must be true or false") 169 | 170 | temperature = raw_config.get('temperature', 0.2) 171 | 172 | if not isinstance(temperature, int | float): 173 | raise ValueError("temperature is invalid") 174 | 175 | top_p = raw_config.get('top_p', 1) 176 | 177 | if not isinstance(top_p, int | float): 178 | raise ValueError("top_p is invalid") 179 | 180 | max_tokens = raw_config.get('max_tokens', 1024) 181 | 182 | if not isinstance(max_tokens, (int)): 183 | raise ValueError("max_tokens is invalid") 184 | 185 | presence_penalty = raw_config.get('presence_penalty', 0) 186 | 187 | if not isinstance(presence_penalty, int | float): 188 | raise ValueError("presence_penalty is invalid") 189 | 190 | frequency_penalty = raw_config.get('frequency_penalty', 0) 191 | 192 | if not isinstance(frequency_penalty, int | float): 193 | raise ValueError("frequency_penalty is invalid") 194 | 195 | return Config( 196 | url=url, 197 | api_key=api_key or '', 198 | model=model, 199 | use_chat_api=use_chat_api, 200 | temperature=temperature, 201 | top_p=top_p, 202 | max_tokens=max_tokens, 203 | presence_penalty=presence_penalty, 204 | frequency_penalty=presence_penalty, 205 | ) 206 | 207 | 208 | def get_error_message(error: urllib.error.HTTPError) -> str: 209 | message = error.read().decode('utf-8', errors='ignore') 210 | 211 | try: 212 | # JSON data might look like this: 213 | # { 214 | # "error": { 215 | # "message": "...", 216 | # "type": "...", 217 | # "param": null, 218 | # "code": null 219 | # } 220 | # } 221 | message = json.loads(message)['error']['message'] 222 | 223 | if "This model's maximum context length is" in message: 224 | message = 'Too much text for a request!' 225 | except Exception: 226 | # If we can't get a better message use the JSON payload at least. 227 | pass 228 | 229 | return message 230 | 231 | 232 | def main() -> None: 233 | input_data = json.loads(sys.stdin.readline()) 234 | 235 | try: 236 | config = load_config(input_data["config"]) 237 | except ValueError as err: 238 | sys.exit(str(err)) 239 | 240 | try: 241 | get_openai_completion(config, input_data["prompt"]) 242 | except urllib.error.HTTPError as error: 243 | if error.code == 400 or error.code == 401: 244 | message = get_error_message(error) 245 | sys.exit('Neural error: OpenAI request failure: ' + message) 246 | if error.code == 404: 247 | message = get_error_message(error) 248 | sys.exit('Neural error: OpenAI request failure: ' + message) 249 | elif error.code == 429: 250 | message = get_error_message(error) 251 | sys.exit("Neural error: OpenAI request limit reached: " + message) 252 | else: 253 | raise 254 | 255 | 256 | if __name__ == "__main__": # pragma: no cover 257 | main() # pragma: no cover 258 | -------------------------------------------------------------------------------- /doc/neural-development.txt: -------------------------------------------------------------------------------- 1 | *neural-development.txt* For Vim and Neovim. 2 | *neural-dev* 3 | *neural-development* 4 | 5 | Neural Development Documentation 6 | 7 | =============================================================================== 8 | CONTENTS *neural-development-contents* 9 | 10 | 1. Introduction.........................|neural-development-introduction| 11 | 2. Design Goals.........................|neural-design-goals| 12 | 3. Coding Standards.....................|neural-coding-standards| 13 | 4. Development Environment..............|neural-development-environment| 14 | 5. Testing Neural.......................|neural-development-tests| 15 | 6. Contributing.........................|neural-development-contributing| 16 | 6.1. Preparing a Release..............|neural-development-release| 17 | 18 | 19 | =============================================================================== 20 | 1. Introduction *neural-development-introduction* 21 | 22 | This document contains helpful information for Neural developers, including 23 | design goals, information on how to run the tests, coding standards, and so 24 | on. You should read this document if you want to get involved with Neural 25 | development. 26 | 27 | 28 | =============================================================================== 29 | 2. Design Goals *neural-design-goals* 30 | 31 | This section lists design goals for Neural, in no particular order. They are as 32 | follows. 33 | 34 | Neural code should be mostly VimL to maintain compatibility with Vim, 35 | in some part Lua to permit better functionality for Neovim, and Python using 36 | absolutely zero dependencies. Using Python without anything but first party 37 | library functions allows Neural to run if it can find any system-installed 38 | Python of a reasonable version, with no further installation steps required. 39 | 40 | Neural should run without needing any other Vim plugins to be installed, to 41 | keep installation simple. Neural can integrate with other plugins for more 42 | advanced functionality, non-essential functionality, or improving on basic 43 | first party functionality. 44 | 45 | Neural should be free of breaking changes to the public API, which is 46 | comprised of documented functions and options, until a major version is 47 | planned. Breaking changes should be preceded by a deprecation phase complete 48 | with warnings. Changes required for security may be an exception. 49 | 50 | Neural supports Vim 8 and above, and Neovim 0.8.0 or newer. Vim 8 is the 51 | earliest version of Vim which supports |job|, |timer|, |closure|, and |lambda| 52 | features. Neovim 0.8.0 is the earliest version to support moderately good UI 53 | features users now expect for a plugin like Neural. 54 | 55 | Just about everything should be documented and covered with tests. 56 | 57 | By and large, people shouldn't pay for the functionality they don't use. Care 58 | should be taken when adding new features, so supporting new features doesn't 59 | degrade the general performance of anything Neural does. 60 | 61 | 62 | =============================================================================== 63 | 3. Coding Standards *neural-coding-standards* 64 | 65 | The following general coding standards should be adhered to for Vim code. 66 | 67 | * Check your Vim code with `Vint` and do everything it says. ALE will check 68 | your Vim code with Vint automatically. See: https://github.com/Kuniwak/vint 69 | * Try to write descriptive and concise names for variables and functions. 70 | Names shouldn't be too short or too long. Think about others reading your 71 | code later on. 72 | * Use `snake_case` names for variables and arguments, and `PascalCase` names 73 | for functions. Prefix every variable name with its scope. (`l:`, `g:`, etc.) 74 | * Try to keep lines no longer than 80 characters, but this isn't an absolute 75 | requirement. 76 | * Use 4 spaces for every level of indentation in Vim code. 77 | * Add a blank line before every `function`, `if`, `for`, `while`, or `return`, 78 | which doesn't start a new level of indentation. This makes the logic in 79 | your code easier to follow. 80 | * End every file with a trailing newline character, but not with extra blank 81 | lines. Remove trailing whitespace from the ends of lines. 82 | * Write the full names of commands instead of abbreviations. For example, write 83 | `function` instead of `func`, and `endif` instead of `end`. 84 | * Write functions with `!`, so files can be reloaded. Use the |abort| keyword 85 | for all functions, so functions exit on the first error. 86 | * Make sure to credit yourself in files you have authored with `Author:` 87 | and `Description:` comments. 88 | 89 | The following general coding standards should be adhered to for Python code. 90 | 91 | * Make sure to run `flake8` on code, and adhere to typical PEP8 coding 92 | standards. 93 | * Make sure to run `pyright` on code, and ensure your code passes type 94 | checking rules. 95 | * Make sure to run `isort` on code to keep imports sorted. 96 | * Maintain near 100% test coverage of Python code with `unittest` tests. 97 | * You may only use first party Python libraries. 98 | * Try to make sure code will run with whatever version of Python Debian stable 99 | runs. 100 | 101 | NOTE: We do not currently offer guidelines for Lua code. 102 | 103 | Apply the following guidelines when writing Vader test files. 104 | 105 | * Use 2 spaces for Vader test files, instead of the 4 spaces for Vim files. 106 | * If you write `Before` and `After` blocks, you should typically write them at 107 | the top of the file, so they run for all tests. There may be some tests 108 | where it make sense to modify the `Before` and `After` code part of the way 109 | through the file. 110 | * If you modify any settings or global variables, reset them in `After` 111 | blocks. The Vader `Save` and `Restore` commands can be useful for this 112 | purpose. 113 | * Just write `Execute` blocks for Vader tests, and don't bother writing `Then` 114 | blocks. `Then` blocks execute after `After` blocks in older versions, and 115 | that can be confusing. 116 | 117 | Apply the following rules when writing Bash scripts. 118 | 119 | * Run `shellcheck`, (ALE will run it) and do everything it says. 120 | See: https://github.com/koalaman/shellcheck 121 | * Try to write scripts so they will run on Linux, BSD, or Mac OSX. 122 | 123 | 124 | =============================================================================== 125 | 4. Development Environment *neural-development-environment* 126 | 127 | Run the following: > 128 | pyenv install 129 | pip install uv 130 | uv sync 131 | < 132 | This should install all Python dependencies for development including 133 | `pyright`, `ruff`, etc. You may wish to configure ALE to run only the linters 134 | and fixers for the project. You can configure ALE like so: > 135 | 136 | if expand('%:p') =~# 'neural' 137 | let b:ale_linters = ['ruff', 'pyright'] 138 | let b:ale_fixers = ['ruff'] 139 | endif 140 | < 141 | 142 | =============================================================================== 143 | 5. Testing Neural *neural-development-tests* *neural-tests* 144 | 145 | Neural is tested with a suite of tests executed via GitHub Actions. 146 | Neural runs tests with the following versions of Vim. 147 | 148 | 1. Vim 8.0.0027 on Linux. 149 | 2. Vim 9.0.0297 on Linux. 150 | 3. NeoVim 0.8.0 on Linux. 151 | 152 | If you are developing Neural code on Linux, Mac OSX, or BSD, you can run 153 | Neural's tests by installing Docker and running the `run-tests` script. Follow 154 | the instructions on the Docker site for installing Docker. 155 | See: https://docs.docker.com/install/ 156 | 157 | NOTE: Don't forget to add your user to the `docker` group on Linux, or Docker 158 | just won't work. See: https://docs.docker.com/install/linux/linux-postinstall/ 159 | 160 | If you run simply `./run-tests` from the Neural repository root directory, the 161 | latest Docker image for tests will be downloaded if needed, and the script 162 | will run all of the tests in Vader, Vint checks, `tox`, and several Bash 163 | scripts for finding extra issues. Run `./run-tests --help` to see all of the 164 | options the script supports. The script supports selecting particular test 165 | files. 166 | 167 | Once you get used to dealing with Vim and Neovim compatibility issues, you 168 | probably want to use `./run-tests --fast -q` for running tests with only the 169 | fastest available Vim version, and with success messages from tests 170 | suppressed. 171 | 172 | Generally write tests for any changes you make. 173 | 174 | Look at existing tests in the codebase for examples of how to write tests. 175 | Refer to the Vader documentation for general information on how to write Vader 176 | tests: https://github.com/junegunn/vader.vim 177 | 178 | 179 | =============================================================================== 180 | 6. Contributing *neural-development-contributing* 181 | 182 | All integration of new code into Neural is done through GitHub pull requests. 183 | Using that tool streamlines the process and minimizes the time and effort 184 | required to e.g. ensure test suites are run for every change. 185 | 186 | As for any project hosted by GitHub, the choice of platform demands every 187 | contributor to take care to setup an account and configure it accordingly. 188 | 189 | Due to details of our process, a difference to many other GitHub hosted 190 | projects is that contributors who wish to keep the author fields for their 191 | commits unaltered need to configure a public email address in their account 192 | and profile settings. See: https://docs.github.com/en/account-and-profile/ 193 | 194 | Unless configuring GitHub to expose contact details, commits will be rewritten 195 | to appear by `USERNAME ` . 196 | 197 | 198 | ------------------------------------------------------------------------------- 199 | 6.1 Preparing a Release *neural-development-release* 200 | 201 | Neural offers release packages through GitHub, for two reasons: 202 | 203 | 1. Some users like to target specific release versions rather than simply 204 | installing the plugin from `master`. This includes users who create Linux 205 | distribution specific packages from GitHub releases. 206 | 2. The releases provide a nice way to get an overview of what has changed in 207 | Neural over time. 208 | 209 | Neural has no fixed release schedule. Release versions are created whenever 210 | the Neural developers feel the need to create one. Neural release versions 211 | follow the typical Semantic Versioning scheme. See: https://semver.org/ 212 | 213 | If there are ever to be any breaking changes made for Neural, there should 214 | first come a minor version release for Neural documenting all of the coming 215 | breaking changes to Neural. It should be described how users can prepare for a 216 | breaking change that is coming before it is done. At the time of writing, 217 | Neural lives as version 0.x.x, and as such breaking changes may be more 218 | common. 219 | 220 | To create a release for Neural, you will need sufficient permissions in GitHub. 221 | Once you do, follow these steps. 222 | 223 | 1. Create a new release draft, or edit an existing one. It helps to craft 224 | drafts ahead of time and write the last commit ID checked for release notes 225 | on the last update to a draft. 226 | See the releases page: https://github.com/dense-analysis/neural/releases 227 | 2. Examine `git log` and read changes made between the last ID checked, or the 228 | git tag of the previous release, and the current commit in `master`. 229 | 3. Write updates in separate sections (except where empty) for: 230 | 3.a. New Features 231 | 3.b. Bugs Fixed 232 | 4. Commit the changes after `./run-tests --fast -q` passes. 233 | 5. Tag the release with `git tag vA.B.C`, replacing `A`, `B`, and `C` with the 234 | version numbers. See `git tag --list` for examples. 235 | 6. Run `git push` and `git push --tags` to push the commit and the tag. 236 | 7. Edit the release draft in GitHub, select the tag you just pushed, and 237 | publish the draft. 238 | 8. If you're creating a new major or minor version: `git checkout -b vA.B.x`, 239 | replacing `A` and `B` with the major and minor versions. `git push` the new 240 | branch, and the GitHub branch protection settings should automatically 241 | apply to the new release branch. 242 | 9. You have already completed the last step. 243 | 244 | Have fun creating Neural releases. Use your head, or Neural in place of it. 245 | 246 | =============================================================================== 247 | vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: 248 | -------------------------------------------------------------------------------- /autoload/neural.vim: -------------------------------------------------------------------------------- 1 | " Author: Anexon , w0rp 2 | " Description: The main autoload file for the Neural Vim plugin 3 | 4 | " The location of Neural provider scripts 5 | let s:neural_script_dir = expand(':p:h:h') . '/src/neural/provider' 6 | " Keep track of the current job. 7 | let s:current_job = get(s:, 'current_job', 0) 8 | " Keep track of the line the last request happened on. 9 | let s:request_line = get(s:, 'request_line', 0) 10 | let s:initial_timer = get(s:, 'initial_timer', -1) 11 | let s:busy_timer = get(s:, 'busy_timer', -1) 12 | 13 | " A function purely for tests to be able to reset state 14 | function! neural#ResetState() abort 15 | let s:current_job = 0 16 | let s:request_line = 0 17 | let s:initial_timer = -1 18 | let s:busy_timer = -1 19 | endfunction 20 | 21 | " Get the Neural scripts directory in a way that makes it hard to modify. 22 | function! neural#GetScriptDir() abort 23 | return s:neural_script_dir 24 | endfunction 25 | 26 | " Output an error message. The message should be a string. 27 | " The output error lines will be split in a platform-independent way. 28 | function! neural#OutputErrorMessage(message) abort 29 | let l:lines = split(a:message, '\v\r\n|\n|\r') 30 | 31 | if len(l:lines) > 0 32 | if len(l:lines) > 1 33 | " Add a message to say to look at a long error if there's more 34 | " than 1 error line. 35 | call add(l:lines, 'Neural hit a snag! Type :mes to see why') 36 | endif 37 | 38 | " no-custom-checks 39 | echohl error 40 | 41 | try 42 | for l:line in l:lines 43 | " no-custom-checks 44 | echomsg l:line 45 | endfor 46 | finally 47 | " no-custom-checks 48 | echohl None 49 | endtry 50 | endif 51 | endfunction 52 | 53 | function! s:AddLineToBuffer(buffer, job_data, line) abort 54 | " Add lines either if we can add them to the buffer which is no longer the 55 | " current one, or otherwise only if we're still in the same buffer. 56 | if bufnr('') isnot a:buffer && !exists('*appendbufline') 57 | return 58 | endif 59 | 60 | let l:moving_line = a:job_data.moving_line 61 | let l:started = a:job_data.content_started 62 | 63 | " Skip introductory empty lines. 64 | if !l:started && len(a:line) == 0 65 | return 66 | endif 67 | 68 | " Check if we need to re-position the cursor to stop it appearing to move 69 | " down as lines are added. 70 | let l:pos = getpos('.') 71 | let l:last_line = len(getbufline(a:buffer, 1, '$')) 72 | let l:move_up = 0 73 | 74 | if l:pos[1] == l:last_line 75 | let l:move_up = 1 76 | endif 77 | 78 | " appendbufline isn't available in old Vim versions. 79 | if bufnr('') is a:buffer 80 | call append(l:moving_line, a:line) 81 | else 82 | call appendbufline(a:buffer, l:moving_line, a:line) 83 | endif 84 | 85 | " Move the cursor back up again to make content appear below. 86 | if l:move_up 87 | call setpos('.', l:pos) 88 | endif 89 | 90 | let a:job_data.moving_line = l:moving_line + 1 91 | let a:job_data.content_started = 1 92 | endfunction 93 | 94 | function! s:AddErrorLine(buffer, job_data, line) abort 95 | call add(a:job_data.error_lines, a:line) 96 | endfunction 97 | 98 | function! s:HandleOutputEnd(buffer, job_data, exit_code) abort 99 | if has('nvim') 100 | execute 'lua require(''neural'').stop_animated_sign(' . s:request_line . ')' 101 | endif 102 | 103 | " Output an error message from the program if something goes wrong. 104 | if a:exit_code != 0 105 | call neural#OutputErrorMessage(join(a:job_data.error_lines, "\n")) 106 | else 107 | " Signal Neural is done for plugin integration. 108 | silent doautocmd User NeuralWritePost 109 | 110 | if g:neural.ui.echo_enabled 111 | " no-custom-checks 112 | echomsg 'Neural is done!' 113 | endif 114 | endif 115 | 116 | let s:current_job = 0 117 | endfunction 118 | 119 | " Get the path to the executable for a script language. 120 | function! s:GetScriptExecutable(provider) abort 121 | if a:provider.script_language is# 'python' 122 | let l:executable = '' 123 | 124 | if has('win32') 125 | " Try to automatically find Python on Windows, even if not in PATH. 126 | let l:executable = expand('~/AppData/Local/Programs/Python/Python3*/python.exe') 127 | endif 128 | 129 | if empty(l:executable) 130 | let l:executable = 'python3' 131 | endif 132 | 133 | return l:executable 134 | endif 135 | 136 | throw 'Unknown script language: ' . a:provider.script_language 137 | endfunction 138 | 139 | " Escape a string suitably for each platform. 140 | " shellescape does not work on Windows. 141 | function! neural#Escape(str) abort 142 | if fnamemodify(&shell, ':t') is? 'cmd.exe' 143 | " If the string contains spaces, it will be surrounded by quotes. 144 | " Otherwise, special characters will be escaped with carets (^). 145 | return substitute( 146 | \ a:str =~# ' ' 147 | \ ? '"' . substitute(a:str, '"', '""', 'g') . '"' 148 | \ : substitute(a:str, '\v([&|<>^])', '^\1', 'g'), 149 | \ '%', 150 | \ '%%', 151 | \ 'g', 152 | \) 153 | endif 154 | 155 | return shellescape (a:str) 156 | endfunction 157 | 158 | " Complain about no prompt text being provided. 159 | " This function is also called from Lua code. 160 | function! neural#ComplainNoPromptText() abort 161 | call neural#OutputErrorMessage('No prompt text!') 162 | endfunction 163 | 164 | function! neural#OpenPrompt() abort 165 | if has('nvim') 166 | " Reload the Neural config on a prompt request if needed. 167 | call neural#config#Load() 168 | " In Neovim, try to use the fancy prompt UI, if we can. 169 | lua require('neural').prompt() 170 | else 171 | call feedkeys(':Neural ') 172 | endif 173 | endfunction 174 | 175 | function! s:InitiallyInformUser(job_id) abort 176 | if neural#job#IsRunning(a:job_id) 177 | " no-custom-checks 178 | echomsg 'Neural is working...' 179 | endif 180 | endfunction 181 | 182 | function! s:InformUserIfStillBusy(job_id) abort 183 | if neural#job#IsRunning(a:job_id) 184 | " no-custom-checks 185 | echomsg 'Neural is still working...' 186 | endif 187 | endfunction 188 | 189 | function! neural#Cleanup() abort 190 | " Stop :NeuralExplain if it might be running. 191 | if exists('*neural#explain#Cleanup') 192 | call neural#explain#Cleanup() 193 | endif 194 | 195 | if s:current_job 196 | " Stop any currently running jobs. 197 | call neural#job#Stop(s:current_job) 198 | let s:current_job = 0 199 | endif 200 | 201 | " Stop timers for informing the user. 202 | call timer_stop(s:initial_timer) 203 | call timer_stop(s:busy_timer) 204 | 205 | " Remove any current signs. 206 | if has('nvim') 207 | execute 'lua require(''neural'').stop_animated_sign(' . s:request_line . ')' 208 | endif 209 | endfunction 210 | 211 | function! neural#Stop() abort 212 | let l:was_running = neural#job#IsRunning(s:current_job) 213 | call neural#Cleanup() 214 | 215 | if l:was_running && g:neural.ui.echo_enabled 216 | " no-custom-checks 217 | echomsg 'Neural stopped.' 218 | endif 219 | endfunction 220 | 221 | " Pre-process input for LLMs based on custom code in Neural. 222 | function! neural#PreProcess(buffer, input) abort 223 | " Skip pre-processing if disabled. 224 | if !g:neural.pre_process.enabled 225 | return 226 | endif 227 | 228 | for l:split_type in split(&filetype, '\.') 229 | try 230 | let l:func_name = 'neural#pre_process#' . l:split_type . '#Process' 231 | call function(l:func_name)(a:buffer, a:input) 232 | catch /E117/ 233 | endtry 234 | endfor 235 | endfunction 236 | 237 | function! s:LoadProvider() abort 238 | " TODO: Change message if nothing is selected. 239 | let l:config = get(g:neural.providers, 0, {}) 240 | let l:type = get(l:config, 'type', '') 241 | 242 | try 243 | let l:provider = function('neural#provider#' . l:type . '#Get')() 244 | catch /E117/ 245 | call neural#OutputErrorMessage('Invalid provider: ' . l:type) 246 | 247 | return 248 | endtry 249 | 250 | let l:provider.config = config 251 | 252 | return l:provider 253 | endfunction 254 | 255 | function! s:GetProviderInput(buffer, provider, prompt) abort 256 | let l:input = {'config': a:provider.config, 'prompt': a:prompt} 257 | 258 | " Pre-process input, such as modifying a prompt. 259 | call neural#PreProcess(a:buffer, l:input) 260 | 261 | return l:input 262 | endfunction 263 | 264 | function! neural#GetCommand(buffer) abort 265 | let l:provider = s:LoadProvider() 266 | let l:script_exe = s:GetScriptExecutable(l:provider) 267 | let l:command = neural#Escape(l:script_exe) 268 | \ . ' ' . neural#Escape(l:provider.script) 269 | let l:command = neural#job#PrepareCommand(a:buffer, l:command) 270 | 271 | return [l:provider, l:command] 272 | endfunction 273 | 274 | function! neural#Prompt(prompt) abort 275 | " Reload the Neural config on a prompt request if needed. 276 | call neural#config#Load() 277 | call neural#Cleanup() 278 | 279 | if empty(a:prompt) 280 | if has('nvim') && g:neural.ui.prompt_enabled 281 | call neural#OpenPrompt() 282 | else 283 | call neural#ComplainNoPromptText() 284 | endif 285 | 286 | return 287 | endif 288 | 289 | call neural#Run(a:prompt, {}) 290 | endfunction 291 | 292 | " Print the prompt that Neural will use in full. 293 | function! neural#ViewPrompt(...) abort 294 | " Reload the Neural config on a prompt request if needed. 295 | call neural#config#Load() 296 | 297 | " Take the first argument or nothing. 298 | let l:prompt = get(a:000, 0, '') 299 | let l:buffer = bufnr('') 300 | let l:provider = s:LoadProvider() 301 | let l:input = s:GetProviderInput(l:buffer, l:provider, l:prompt) 302 | 303 | " no-custom-checks 304 | echohl Question 305 | " no-custom-checks 306 | echo 'The following prompt will be sent.' 307 | " no-custom-checks 308 | echohl None 309 | " no-custom-checks 310 | echo "\n" 311 | " no-custom-checks 312 | echo l:input.prompt 313 | endfunction 314 | 315 | function! neural#Run(prompt, options) abort 316 | let l:buffer = bufnr('') 317 | 318 | if has_key(a:options, 'line') 319 | let l:moving_line = a:options.line 320 | else 321 | let l:moving_line = getpos('.')[1] 322 | endif 323 | 324 | let s:request_line = l:moving_line 325 | 326 | if len(getline(l:moving_line)) == 0 327 | let l:moving_line -= 1 328 | endif 329 | 330 | let [l:provider, l:command] = neural#GetCommand(l:buffer) 331 | let l:job_data = { 332 | \ 'moving_line': l:moving_line, 333 | \ 'error_lines': [], 334 | \ 'content_started': 0, 335 | \} 336 | let l:job_id = neural#job#Start(l:command, { 337 | \ 'mode': 'nl', 338 | \ 'out_cb': {job_id, line -> s:AddLineToBuffer(l:buffer, l:job_data, line)}, 339 | \ 'err_cb': {job_id, line -> s:AddErrorLine(l:buffer, l:job_data, line)}, 340 | \ 'exit_cb': {job_id, exit_code -> s:HandleOutputEnd(l:buffer, l:job_data, exit_code)}, 341 | \}) 342 | 343 | if l:job_id > 0 344 | let l:input = s:GetProviderInput(l:buffer, l:provider, a:prompt) 345 | call neural#job#SendRaw(l:job_id, json_encode(l:input) . "\n") 346 | else 347 | call neural#OutputErrorMessage('Failed to run ' . l:provider.name) 348 | 349 | return 350 | endif 351 | 352 | let s:current_job = l:job_id 353 | 354 | " Tell the user something is happening, if enabled. 355 | if g:neural.ui.echo_enabled && get(a:options, 'echo', 1) 356 | " Echo with a 0 millisecond timer to avoid 'Press Enter to Continue' 357 | let s:initial_timer = timer_start(0, {-> s:InitiallyInformUser(l:job_id)}) 358 | 359 | " If returning an answer takes a while, tell them again. 360 | let s:busy_timer = timer_start(5000, {-> s:InformUserIfStillBusy(l:job_id)}) 361 | endif 362 | 363 | if has('nvim') 364 | execute 'lua require(''neural'').start_animated_sign(' . s:request_line . ')' 365 | endif 366 | endfunction 367 | 368 | " Stop Neural doing things when you kill buffers, quit, or suspend. 369 | augroup NeuralCleanupGroup 370 | autocmd! 371 | autocmd BufDelete * call neural#Cleanup() 372 | autocmd QuitPre * call neural#Cleanup() 373 | 374 | if exists('##VimSuspend') 375 | autocmd VimSuspend call neural#Cleanup() 376 | endif 377 | augroup END 378 | 379 | highlight NeuralPromptBorder ctermfg=172 guifg=#ff9d0a 380 | -------------------------------------------------------------------------------- /autoload/neural/job.vim: -------------------------------------------------------------------------------- 1 | " Author: w0rp 2 | " Description: APIs for working with Asynchronous jobs, with an API normalised 3 | " between Vim 8 and NeoVim. 4 | " 5 | " Important functions are described below. They are: 6 | " 7 | " neural#job#Start(command, options) -> job_id 8 | " neural#job#IsRunning(job_id) -> 1 if running, 0 otherwise. 9 | " neural#job#Stop(job_id) 10 | 11 | if !has_key(s:, 'job_map') 12 | let s:job_map = {} 13 | endif 14 | 15 | " A map from timer IDs to jobs, for tracking jobs that need to be killed 16 | " with SIGKILL if they don't terminate right away. 17 | if !has_key(s:, 'job_kill_timers') 18 | let s:job_kill_timers = {} 19 | endif 20 | 21 | function! s:GetFunction(string_or_ref) abort 22 | if type(a:string_or_ref) is v:t_string 23 | return function(a:string_or_ref) 24 | endif 25 | 26 | return a:string_or_ref 27 | endfunction 28 | 29 | function! s:JoinNeovimOutput(job, last_line, data, mode, callback) abort 30 | if a:mode is# 'raw' 31 | call a:callback(a:job, join(a:data, "\n")) 32 | 33 | return '' 34 | endif 35 | 36 | let l:lines = a:data[:-2] 37 | 38 | if len(a:data) > 1 39 | let l:lines[0] = a:last_line . l:lines[0] 40 | let l:new_last_line = a:data[-1] 41 | else 42 | let l:new_last_line = a:last_line . get(a:data, 0, '') 43 | endif 44 | 45 | for l:line in l:lines 46 | call a:callback(a:job, l:line) 47 | endfor 48 | 49 | return l:new_last_line 50 | endfunction 51 | 52 | function! s:KillHandler(timer) abort 53 | let l:job = remove(s:job_kill_timers, a:timer) 54 | call job_stop(l:job, 'kill') 55 | endfunction 56 | 57 | function! s:NeoVimCallback(job, data, event) abort 58 | let l:info = s:job_map[a:job] 59 | 60 | if a:event is# 'stdout' 61 | let l:info.out_cb_line = s:JoinNeovimOutput( 62 | \ a:job, 63 | \ l:info.out_cb_line, 64 | \ a:data, 65 | \ l:info.mode, 66 | \ s:GetFunction(l:info.out_cb), 67 | \) 68 | elseif a:event is# 'stderr' 69 | let l:info.err_cb_line = s:JoinNeovimOutput( 70 | \ a:job, 71 | \ l:info.err_cb_line, 72 | \ a:data, 73 | \ l:info.mode, 74 | \ s:GetFunction(l:info.err_cb), 75 | \) 76 | else 77 | if has_key(l:info, 'out_cb') && !empty(l:info.out_cb_line) 78 | call s:GetFunction(l:info.out_cb)(a:job, l:info.out_cb_line) 79 | endif 80 | 81 | if has_key(l:info, 'err_cb') && !empty(l:info.err_cb_line) 82 | call s:GetFunction(l:info.err_cb)(a:job, l:info.err_cb_line) 83 | endif 84 | 85 | try 86 | call s:GetFunction(l:info.exit_cb)(a:job, a:data) 87 | finally 88 | " Automatically forget about the job after it's done. 89 | if has_key(s:job_map, a:job) 90 | call remove(s:job_map, a:job) 91 | endif 92 | endtry 93 | endif 94 | endfunction 95 | 96 | function! s:VimOutputCallback(channel, data) abort 97 | let l:job = ch_getjob(a:channel) 98 | let l:job_id = neural#job#ParseVim8ProcessID(string(l:job)) 99 | 100 | " Only call the callbacks for jobs which are valid. 101 | if l:job_id > 0 && has_key(s:job_map, l:job_id) 102 | call s:GetFunction(s:job_map[l:job_id].out_cb)(l:job_id, a:data) 103 | endif 104 | endfunction 105 | 106 | function! s:VimErrorCallback(channel, data) abort 107 | let l:job = ch_getjob(a:channel) 108 | let l:job_id = neural#job#ParseVim8ProcessID(string(l:job)) 109 | 110 | " Only call the callbacks for jobs which are valid. 111 | if l:job_id > 0 && has_key(s:job_map, l:job_id) 112 | call s:GetFunction(s:job_map[l:job_id].err_cb)(l:job_id, a:data) 113 | endif 114 | endfunction 115 | 116 | function! s:VimCloseCallback(channel) abort 117 | let l:job = ch_getjob(a:channel) 118 | let l:job_id = neural#job#ParseVim8ProcessID(string(l:job)) 119 | let l:info = get(s:job_map, l:job_id, {}) 120 | 121 | if empty(l:info) 122 | return 123 | endif 124 | 125 | " job_status() can trigger the exit handler. 126 | " The channel can close before the job has exited. 127 | if job_status(l:job) is# 'dead' 128 | try 129 | if !empty(l:info) && has_key(l:info, 'exit_cb') 130 | " We have to remove the callback, so we don't call it twice. 131 | call s:GetFunction(remove(l:info, 'exit_cb'))(l:job_id, get(l:info, 'exit_code', 1)) 132 | endif 133 | finally 134 | " Automatically forget about the job after it's done. 135 | if has_key(s:job_map, l:job_id) 136 | call remove(s:job_map, l:job_id) 137 | endif 138 | endtry 139 | endif 140 | endfunction 141 | 142 | function! s:VimExitCallback(job, exit_code) abort 143 | let l:job_id = neural#job#ParseVim8ProcessID(string(a:job)) 144 | let l:info = get(s:job_map, l:job_id, {}) 145 | 146 | if empty(l:info) 147 | return 148 | endif 149 | 150 | let l:info.exit_code = a:exit_code 151 | 152 | " The program can exit before the data has finished being read. 153 | if ch_status(job_getchannel(a:job)) is# 'closed' 154 | try 155 | if !empty(l:info) && has_key(l:info, 'exit_cb') 156 | " We have to remove the callback, so we don't call it twice. 157 | call s:GetFunction(remove(l:info, 'exit_cb'))(l:job_id, a:exit_code) 158 | endif 159 | finally 160 | " Automatically forget about the job after it's done. 161 | if has_key(s:job_map, l:job_id) 162 | call remove(s:job_map, l:job_id) 163 | endif 164 | endtry 165 | endif 166 | endfunction 167 | 168 | function! neural#job#ParseVim8ProcessID(job_string) abort 169 | return matchstr(a:job_string, '\d\+') + 0 170 | endfunction 171 | 172 | function! neural#job#ValidateArguments(command, options) abort 173 | if a:options.mode isnot# 'nl' && a:options.mode isnot# 'raw' 174 | throw 'Invalid mode: ' . a:options.mode 175 | endif 176 | endfunction 177 | 178 | function! neural#job#PrepareCommand(buffer, command) abort 179 | " The command will be executed in a subshell. This fixes a number of 180 | " issues, including reading the PATH variables correctly, %PATHEXT% 181 | " expansion on Windows, etc. 182 | " 183 | " NeoVim handles this issue automatically if the command is a String, 184 | " but we'll do this explicitly, so we use the same exact command for both 185 | " versions. 186 | if has('win32') 187 | return 'cmd /s/c "' . a:command . '"' 188 | endif 189 | 190 | return ['/bin/sh', '-c', a:command] 191 | endfunction 192 | 193 | " Start a job with options which are agnostic to Vim and NeoVim. 194 | " 195 | " The following options are accepted: 196 | " 197 | " out_cb - A callback for receiving stdin. Arguments: (job_id, data) 198 | " err_cb - A callback for receiving stderr. Arguments: (job_id, data) 199 | " exit_cb - A callback for program exit. Arguments: (job_id, status_code) 200 | " mode - A mode for I/O. Can be 'nl' for split lines or 'raw'. 201 | function! neural#job#Start(command, options) abort 202 | call neural#job#ValidateArguments(a:command, a:options) 203 | 204 | let l:job_info = copy(a:options) 205 | let l:job_options = {} 206 | 207 | if has('nvim') 208 | if has_key(a:options, 'out_cb') 209 | let l:job_options.on_stdout = function('s:NeoVimCallback') 210 | let l:job_info.out_cb_line = '' 211 | endif 212 | 213 | if has_key(a:options, 'err_cb') 214 | let l:job_options.on_stderr = function('s:NeoVimCallback') 215 | let l:job_info.err_cb_line = '' 216 | endif 217 | 218 | if has_key(a:options, 'exit_cb') 219 | let l:job_options.on_exit = function('s:NeoVimCallback') 220 | endif 221 | 222 | let l:job_info.job = jobstart(a:command, l:job_options) 223 | let l:job_id = l:job_info.job 224 | else 225 | let l:job_options = { 226 | \ 'in_mode': l:job_info.mode, 227 | \ 'out_mode': l:job_info.mode, 228 | \ 'err_mode': l:job_info.mode, 229 | \} 230 | 231 | if has_key(a:options, 'out_cb') 232 | let l:job_options.out_cb = function('s:VimOutputCallback') 233 | else 234 | " prevent buffering of output and excessive polling in case close_cb is set 235 | let l:job_options.out_cb = {->0} 236 | endif 237 | 238 | if has_key(a:options, 'err_cb') 239 | let l:job_options.err_cb = function('s:VimErrorCallback') 240 | else 241 | " prevent buffering of output and excessive polling in case close_cb is set 242 | let l:job_options.err_cb = {->0} 243 | endif 244 | 245 | if has_key(a:options, 'exit_cb') 246 | " Set a close callback to which simply calls job_status() 247 | " when the channel is closed, which can trigger the exit callback 248 | " earlier on. 249 | let l:job_options.close_cb = function('s:VimCloseCallback') 250 | let l:job_options.exit_cb = function('s:VimExitCallback') 251 | endif 252 | 253 | " Use non-blocking writes for Vim versions that support the option. 254 | if has('patch-8.1.350') 255 | let l:job_options.noblock = 1 256 | endif 257 | 258 | " Vim 8 will read the stdin from the file's buffer. 259 | let l:job_info.job = job_start(a:command, l:job_options) 260 | let l:job_id = neural#job#ParseVim8ProcessID(string(l:job_info.job)) 261 | endif 262 | 263 | if l:job_id > 0 264 | " Store the job in the map for later only if we can get the ID. 265 | let s:job_map[l:job_id] = l:job_info 266 | endif 267 | 268 | return l:job_id 269 | endfunction 270 | 271 | " Force running commands in a Windows CMD command line. 272 | " This means the same command syntax works everywhere. 273 | function! neural#job#StartWithCmd(command, options) abort 274 | let l:shell = &l:shell 275 | let l:shellcmdflag = &l:shellcmdflag 276 | let &l:shell = 'cmd' 277 | let &l:shellcmdflag = '/c' 278 | 279 | try 280 | let l:job_id = neural#job#Start(a:command, a:options) 281 | finally 282 | let &l:shell = l:shell 283 | let &l:shellcmdflag = l:shellcmdflag 284 | endtry 285 | 286 | return l:job_id 287 | endfunction 288 | 289 | " Send raw data to the job. 290 | function! neural#job#SendRaw(job_id, string) abort 291 | if has('nvim') 292 | call jobsend(a:job_id, a:string) 293 | else 294 | let l:job = s:job_map[a:job_id].job 295 | 296 | if ch_status(l:job) is# 'open' 297 | call ch_sendraw(job_getchannel(l:job), a:string) 298 | endif 299 | endif 300 | endfunction 301 | 302 | " Given a job ID, return 1 if the job is currently running. 303 | " Invalid job IDs will be ignored. 304 | function! neural#job#IsRunning(job_id) abort 305 | if has('nvim') 306 | try 307 | " In NeoVim, if the job isn't running, jobpid() will throw. 308 | call jobpid(a:job_id) 309 | 310 | return 1 311 | catch 312 | endtry 313 | elseif has_key(s:job_map, a:job_id) 314 | let l:job = s:job_map[a:job_id].job 315 | 316 | return job_status(l:job) is# 'run' 317 | endif 318 | 319 | return 0 320 | endfunction 321 | 322 | function! neural#job#HasOpenChannel(job_id) abort 323 | if neural#job#IsRunning(a:job_id) 324 | if has('nvim') 325 | " TODO: Implement a check for NeoVim. 326 | return 1 327 | endif 328 | 329 | " Check if the Job's channel can be written to. 330 | return ch_status(s:job_map[a:job_id].job) is# 'open' 331 | endif 332 | 333 | return 0 334 | endfunction 335 | 336 | " Given a Job ID, stop that job. 337 | " Invalid job IDs will be ignored. 338 | function! neural#job#Stop(job_id) abort 339 | if !has_key(s:job_map, a:job_id) 340 | return 341 | endif 342 | 343 | if has('nvim') 344 | " FIXME: NeoVim kills jobs on a timer, but will not kill any processes 345 | " which are child processes on Unix. Some work needs to be done to 346 | " kill child processes to stop long-running processes like pylint. 347 | silent! call jobstop(a:job_id) 348 | else 349 | let l:job = s:job_map[a:job_id].job 350 | 351 | " We must close the channel for reading the buffer if it is open 352 | " when stopping a job. Otherwise, we will get errors in the status line. 353 | if ch_status(job_getchannel(l:job)) is# 'open' 354 | call ch_close_in(job_getchannel(l:job)) 355 | endif 356 | 357 | " Ask nicely for the job to stop. 358 | call job_stop(l:job) 359 | 360 | if neural#job#IsRunning(l:job) 361 | " Set a 100ms delay for killing the job with SIGKILL. 362 | let s:job_kill_timers[timer_start(100, function('s:KillHandler'))] = l:job 363 | endif 364 | endif 365 | endfunction 366 | -------------------------------------------------------------------------------- /doc/neural.txt: -------------------------------------------------------------------------------- 1 | *neural.txt* Plugin to generate text and code with machine learning. 2 | *neural* 3 | 4 | =============================================================================== 5 | CONTENTS *neural-contents* 6 | 7 | 1. Introduction ............................... |neural-introduction| 8 | 2. Supported Tools ............................ |neural-support| 9 | 3. Commands/Keybinds .......................... |neural-commands| 10 | 4. Options .................................... |neural-options| 11 | 4.1 UI ........................................ |neural-ui| 12 | 4.2 Neural Buffer ............................. |neural-buffer| 13 | 4.3 OpenAI .................................... |neural-openai| 14 | 4.4 Highlights ................................ |neural-highlights| 15 | 5. API ........................................ |neural-api| 16 | 6. Environment Variables ...................... |neural-env| 17 | 6.1 Linux + KDE ............................. |neural-env-kde| 18 | 7. Contact .................................... |neural-contact| 19 | 20 | =============================================================================== 21 | 1. Introduction *neural-introduction* 22 | 23 | Neural is a plugin for Vim and Neovim that provides blazingly fast AI code 24 | generation, editing, and completion. 25 | 26 | It uses machine learning tools under the hood, such as OpenAI's GPT-3 API, to 27 | generate text, code, and much more. 28 | 29 | =============================================================================== 30 | 2. Supported Languages & Tools *neural-support* 31 | 32 | Neural supports the following tools. 33 | 34 | 1. OpenAI - https://platform.openai.com/signup 35 | 2. Any model that uses the OpenAI API. See |neural-provider-openai.url| 36 | 37 | To select the tool that Neural will use, set |g:neural.providers| to the 38 | appropriate value. OpenAI is the default data provider. 39 | 40 | =============================================================================== 41 | 3. Commands/Keybinds *neural-commands* 42 | 43 | 44 | `:Neural` *Neural* 45 | 46 | Prompt Neural for a response. e.g. `:Neural say hello` 47 | 48 | If the command is run with no text input and `nui.vim` is installed, a fancy 49 | UI for entering the prompt will be shown. 50 | 51 | See https://github.com/MunifTanjim/nui.nvim for installation instructions. 52 | 53 | A plug mapping `(neural_prompt)` is defined for this command. 54 | 55 | A |NeuralWritePost| event will be fired whenever Neural successfully 56 | completes writing text to a buffer. 57 | 58 | 59 | `:NeuralBuffer` *NeuralBuffer* 60 | 61 | Create a buffer with a `neuralbuf` filetype for interacting with neural 62 | providers directly. This can be a useful scratch buffer and playground for 63 | code generation and completion. 64 | 65 | - See |neural-buffer| for configuration options. 66 | - See |NeuralCompletion| for running neural completions in the buffer. 67 | 68 | A plug mapping `(neural_buffer)` is defined for this. 69 | 70 | 71 | `:NeuralCompletion` *NeuralCompletion* 72 | 73 | A command for a |NeuralBuffer| (`neuralbuf` filetype) that sends all buffer 74 | contents to the current neural provider for completion and appends the 75 | response to the buffer. 76 | 77 | A `neuralbuf` plug mapping `(neural_completion)` is defined for this 78 | with the default: `` 79 | 80 | 81 | `:NeuralExplain` *NeuralExplain* 82 | 83 | A |visual-mode| command for explaining the highlighted lines. The visual 84 | selection will be sent to the currently selected provider and the response 85 | will be displayed in a preview window. 86 | 87 | Neural will make basic attempts to redact lines that appear to contain 88 | passwords or secrets. 89 | 90 | A plug mapping `(neural_explain)` is defined for this. 91 | 92 | 93 | `:NeuralStop` *NeuralStop* 94 | 95 | Stop any currently running Neural tasks, and immediately stop printing text 96 | to a Vim buffer at the first available opportunity. 97 | 98 | A plug mapping `(neural_stop)` is defined for this. 99 | 100 | Neural will by default bind (CTRL-C) to stopping neural if no mapping 101 | is already defined for that key. This behavior can be disabled by setting 102 | |g:neural.set_default_keybinds| to any falsy value. 103 | 104 | 105 | `:NeuralViewPrompt` *NeuralViewPrompt* 106 | 107 | View the complete prompt that will be sent. e.g. `:NeuralViewPrompt say hello` 108 | 109 | Neural will automatically alter prompts sent to virtual assistants before 110 | they are sent depending on the filetype of the current file and the 111 | surrounding context. This command allows you to see what that prompt will be 112 | before it is sent. 113 | 114 | A plug mapping `(neural_view_prompt)` is defined for this. 115 | 116 | 117 | =============================================================================== 118 | 4. Options *neural-options* 119 | *g:neural* 120 | 121 | All of Neural's options are controlled with a single dictionary that can be 122 | configured either in Vim or in Lua. 123 | 124 | In Vim just set `g:neural`: > 125 | 126 | let g:neural = { 127 | \ 'providers': [ 128 | \ { 129 | \ 'openai': { 130 | \ 'api_key': $OPENAI_API_KEY, 131 | \ }, 132 | \ }, 133 | \ ], 134 | \} 135 | < 136 | In a Neovim `init.lua` call `require('neural').setup`: > 137 | 138 | require('neural').setup({ 139 | ui = { 140 | animated_sign_enabled = false, 141 | }, 142 | providers = { 143 | { 144 | openai = { 145 | api_key = vim.env.OPENAI_API_KEY, 146 | }, 147 | } 148 | }, 149 | }) 150 | < 151 | 152 | You can modify settings at any time, before or after Neural is loaded, and 153 | Neural will react to the change in settings. A complete list of supported 154 | options listed below. 155 | 156 | ------------------------------------------------------------------------------- 157 | 158 | 159 | g:neural.set_default_keybinds *g:neural.set_default_keybinds* 160 | *vim.g.neural.set_default_keybinds* 161 | Type: |Boolean| 162 | Default: `v:true` 163 | 164 | Sets default keybinds for Neural, assuming the keys are not already bound 165 | to something else. The default keybinds are as follows: > 166 | 167 | nnoremap (neural_stop) 168 | < 169 | 170 | g:neural.pre_process.enabled *g:neural.pre_process.enabled* 171 | *vim.g.neural.pre_process.enabled* 172 | Type: |Boolean| 173 | Default: `v:true` 174 | 175 | If `v:true`, pre-process prompts before sending them to language models. 176 | 177 | Neural edits the text you send automatically by default to improve the 178 | quality of prompts to produce better results for each filetype. 179 | 180 | g:neural.providers *g:neural.providres* 181 | *vim.g.neural.providers* 182 | Type: |List| 183 | Default: `[]` 184 | 185 | The List of providers to configure which providers Neural will use. 186 | 187 | NOTE: At the moment Neural will only ever use the first provider, and 188 | ignore the rest. If unspecified, OpenAI will be used by default. 189 | 190 | 191 | ------------------------------------------------------------------------------- 192 | 4.1 UI *neural-ui* 193 | 194 | Options for configuring various UI configurations are listed below. 195 | 196 | g:neural.ui.echo_enabled *g:neural.ui.echo_enabled* 197 | *vim.g.neural.ui.echo_enabled* 198 | Type: |Boolean| 199 | Default: `v:true` 200 | 201 | If `v:true`, echo messages about things that are happening. 202 | 203 | You might want to disable this option if you are asked to press Enter a lot. 204 | 205 | 206 | g:neural.ui.prompt_enabled *g:neural.ui.prompt_enabled* 207 | *vim.g.neural.ui.prompt_enabled* 208 | Type: |Boolean| 209 | Default: `v:true` 210 | 211 | If `v:true`, show a fancy prompt. 212 | 213 | Available in Neovim only with `nui.nvim` installed. 214 | 215 | The icon can be changed with |g:neural.ui.prompt_icon|. 216 | 217 | 218 | g:neural.ui.prompt_icon *g:neural.ui.prompt_icon* 219 | *vim.g.neural.ui.prompt_icon* 220 | Type: |String| 221 | Default: `'🗲'` 222 | 223 | Set the icon Neural uses in the Neovim animated prompt. 224 | 225 | See also: |g:neural.ui.prompt_enabled|. 226 | 227 | 228 | g:neural.ui.animated_sign_enabled *g:neural.ui.animated_sign_enabled* 229 | *vim.g.neural.ui.animated_sign_enabled* 230 | Type: |Boolean| 231 | Default: `v:true` 232 | 233 | If `v:true`, show animated signs when Neural is working. 234 | 235 | Available in Neovim only with `significant.nvim` installed. 236 | 237 | 238 | ------------------------------------------------------------------------------- 239 | 4.2 Neural Buffer *neural-buffer* 240 | 241 | Options for configuring the |NeuralBuffer| are listed below. 242 | 243 | 244 | g:neural.buffer.completion_key *g:neural.buffer.completion_key* 245 | *vim.g.neural.buffer.completion_key* 246 | Type: |String| 247 | Default: `''` 248 | 249 | The default key mapping for creating a Neural Buffer. 250 | 251 | 252 | 253 | g:neural.buffer.create_mode *g:neural.buffer.create_mode* 254 | *vim.g.neural.buffer.create_mode* 255 | Type: |String| 256 | Default: `'vertical'` 257 | 258 | Options between `'vertical'` or `'horizontal'`. 259 | The split mode when creating a new Neural Buffer. 260 | 261 | 262 | g:neural.buffer.wrap *g:neural.buffer.wrap* 263 | *vim.g.neural.buffer.wrap* 264 | Type: |Boolean| 265 | Default: `v:true` 266 | 267 | Option to wrap the contents of the Neural Buffer. 268 | 269 | 270 | ------------------------------------------------------------------------------- 271 | 4.3 OpenAI *neural-openai* 272 | 273 | Options for configuring OpenAI are listed below. This settings should be set 274 | as an Dictionary/table in the `providers` List with `type` set to `'openai'`. 275 | 276 | 277 | api_key *neural-provider-openai.api_key* 278 | 279 | Type: |String| 280 | Default: `''` 281 | 282 | The OpenAI API key. See: https://beta.openai.com/signup/ 283 | 284 | 285 | frequency_penalty 286 | *neural-provider-openai.frequency_penalty* 287 | Type: |Number| or |Float| 288 | Default: `0.1` 289 | 290 | Number between `-2.0` and `2.0`. 291 | Positive values penalize new tokens based on their existing frequency in the 292 | output so far, decreasing the likelihood to repeat the same line verbatim. 293 | 294 | See: https://platform.openai.com/docs/api-reference/parameter-details 295 | 296 | 297 | max_tokens *neural-provider-openai.max_tokens* 298 | 299 | Type: |Number| 300 | Default: `1024` 301 | 302 | The maximum number of tokens to generate in the output. 303 | One token generally corresponds to `~4` characters for common English text. 304 | 305 | This translates to roughly `¾` of a word (e.g. `100 tokens ~= 75 words`). 306 | 307 | 308 | model *neural-provider-openai.model* 309 | 310 | Type: |String| 311 | Default: `'gpt-3.5-turbo-instruct'` 312 | 313 | The model to use for OpenAI. Please consult OpenAI's documentation for more 314 | information on models: https://platform.openai.com/docs/models/overview 315 | 316 | See |neural-provider-openai.use_chat_api| if changing models, as you may 317 | need to use the chat API only for newer models. 318 | 319 | 320 | presence_penalty 321 | *neural-provider-openai.presence_penalty* 322 | Type: |Number| or |Float| 323 | Default: `0.1` 324 | 325 | Number between `-2.0` and `2.0`. 326 | 327 | Positive values penalize new tokens based on whether they appear in the text 328 | so far, increasing the model's likelihood to talk about new topics. 329 | 330 | See: https://platform.openai.com/docs/api-reference/parameter-details 331 | 332 | 333 | temperature *neural-provider-openai.temperature* 334 | 335 | Type: |Number| or |Float| 336 | Default: `0.2` 337 | 338 | The OpenAI sampling temperature between `0` and `2`. 339 | 340 | Higher values like `0.8` will make the output more random, while lower values 341 | like `0.2` will make it more focused and deterministic. 342 | 343 | 344 | top_p *neural-provider-openai.top_p* 345 | 346 | Type: |Number| or |Float| 347 | Default: `1` 348 | 349 | The OpenAI nucleus sampling between `0` and `1`. 350 | 351 | An alternative to sampling with temperature, called nucleus sampling, where 352 | the model considers the results of the tokens with top_p probability mass. 353 | 354 | For example `0.1` means only tokens comprising the top `10%` probability mass 355 | are considered. 356 | 357 | OpenAI recommends altering this or temperature but not both. 358 | 359 | 360 | use_chat_api *neural-provider-openai.use_chat_api* 361 | Type: |Boolean| 362 | Default: `true` 363 | 364 | For configuring if Neural should use `/v1/chat/completions` if `true`, or 365 | the `/v1/completions` API if `false`. Older models such as 366 | `'gpt-3.5-turbo-instruct'` will only run with the completions API, and 367 | newer models may only run with the chat API. 368 | 369 | 370 | url *neural-provider-openai.url* 371 | Type: |String| 372 | Default: `'https://api.openai.com'` 373 | 374 | For configuring the API URL to send LLM requests to. This URL can be 375 | configured to connect Neural to other models with OpenAI compatible APIs 376 | such as DeepSeek, Qwen, etc. 377 | 378 | 379 | ------------------------------------------------------------------------------- 380 | 4.4 Highlights *neural-highlights* 381 | 382 | The following highlights can be configured to change |neural|'s colors. 383 | 384 | 385 | `NeuralPromptBorder` *NeuralPromptBorder* 386 | 387 | Default: `ctermfg=172 guifg=#ff9d0a` 388 | 389 | Color for the |Neural| prompt border. 390 | 391 | =============================================================================== 392 | 5. API *neural-api* 393 | 394 | NeuralWritePost *NeuralWritePost-autocmd* 395 | *NeuralWritePost* 396 | 397 | An event that fires whenever Neural successfully completes writing text to a 398 | buffer. This event can be used to trigger other commands on files, such as 399 | automatically fixing generated code with ALE. > 400 | 401 | augroup NeuralEvents 402 | autocmd! 403 | autocmd User NeuralWritePost ALEFix! 404 | augroup END 405 | < 406 | 407 | =============================================================================== 408 | 6. Environment Variables *neural-env* 409 | 410 | Configuring environment variables across different operating systems is almost 411 | a research topic of its own. This section of documentation provides some 412 | helpful pointers for where you might like to stuff environment variables on 413 | different systems. Pick the strategy you like most, based on a balance of your 414 | own level of security concerns and need for convenience. 415 | 416 | 417 | ------------------------------------------------------------------------------- 418 | 6.1 Linux + KDE *neural-env-kde* 419 | 420 | KDE makes it easy to set environment variables such that they can be read by 421 | all GUI applications. One strategy you can try is to make environment 422 | variables available to GUI apps, and then also expose them in your shell. KDE 423 | will read any script stored in `~/.config/plasma-workspace/env`. Create a file 424 | named `~/.config/plasma-workspace/env/keys.sh` with the following contents, 425 | and make sure to set `chmod ug+x` on the file. > 426 | 427 | #!/bin/bash 428 | 429 | # Put whatever variables you want here. 430 | export OPENAI_API_KEY='sh...' 431 | < 432 | Once in place, `source` the same script in your shell startup script, such as 433 | `~/.bashrc`, like so: > 434 | 435 | # shellcheck source=.config/plasma-workspace/env/keys.sh 436 | source ~/.config/plasma-workspace/env/keys.sh 437 | < 438 | The `shellcheck` comment will tell `shellcheck` where to read the script from, 439 | which will help it to understand which variables are valid, etc. After logging 440 | out and in again, you will be able to access your environment variables in any 441 | Vim or GUI Vim startup script you like uniformly, like so: > 442 | 443 | let g:neural = { 444 | \ 'providers': [ 445 | \ { 446 | \ 'openai': { 447 | \ 'api_key': $OPENAI_API_KEY, 448 | \ }, 449 | \ }, 450 | \ ], 451 | \} 452 | < 453 | All applications on your system will also be able to read the same environment 454 | variable, so consider that in terms of security. 455 | 456 | 457 | =============================================================================== 458 | 7. Contact *neural-contact* 459 | 460 | If you like this plugin, and wish to get in touch, check out the GitHub 461 | page for issues and more at https://github.com/dense-analysis/neural 462 | 463 | Machines should work; people should think. 464 | 465 | =============================================================================== 466 | vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: 467 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.10" 4 | 5 | [[package]] 6 | name = "ansicolor" 7 | version = "0.3.2" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/79/74/630817c7eb1289a1412fcc4faeca74a69760d9c9b0db94fc09c91978a6ac/ansicolor-0.3.2.tar.gz", hash = "sha256:3b840a6b1184b5f1568635b1adab28147947522707d41ceba02d5ed0a0877279", size = 9725, upload-time = "2021-05-05T07:04:18.51Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/30/c7/acf0f7f9ac5e2da2989e570ddf5e4688101e91a90ae5ba3651955fa29fb8/ansicolor-0.3.2-py2.py3-none-any.whl", hash = "sha256:91e9fccea5cf596c39bc015d423ed2dd74c0104fd520a42d7acccb66cbfc39e9", size = 9793, upload-time = "2021-05-05T07:04:16.908Z" }, 12 | ] 13 | 14 | [[package]] 15 | name = "chardet" 16 | version = "5.2.0" 17 | source = { registry = "https://pypi.org/simple" } 18 | sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } 19 | wheels = [ 20 | { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, 21 | ] 22 | 23 | [[package]] 24 | name = "colorama" 25 | version = "0.4.6" 26 | source = { registry = "https://pypi.org/simple" } 27 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 28 | wheels = [ 29 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 30 | ] 31 | 32 | [[package]] 33 | name = "exceptiongroup" 34 | version = "1.3.0" 35 | source = { registry = "https://pypi.org/simple" } 36 | dependencies = [ 37 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 38 | ] 39 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } 40 | wheels = [ 41 | { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, 42 | ] 43 | 44 | [[package]] 45 | name = "iniconfig" 46 | version = "2.1.0" 47 | source = { registry = "https://pypi.org/simple" } 48 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 49 | wheels = [ 50 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 51 | ] 52 | 53 | [[package]] 54 | name = "neural" 55 | version = "0.2.0" 56 | source = { editable = "." } 57 | 58 | [package.dev-dependencies] 59 | dev = [ 60 | { name = "pyright" }, 61 | { name = "pytest" }, 62 | { name = "ruff" }, 63 | { name = "setuptools" }, 64 | { name = "vim-vint" }, 65 | ] 66 | 67 | [package.metadata] 68 | 69 | [package.metadata.requires-dev] 70 | dev = [ 71 | { name = "pyright", specifier = "==1.1.402" }, 72 | { name = "pytest", specifier = ">=8.4.1" }, 73 | { name = "ruff", specifier = "==0.10.0" }, 74 | { name = "setuptools" }, 75 | { name = "vim-vint", specifier = "===0.3.21" }, 76 | ] 77 | 78 | [[package]] 79 | name = "nodeenv" 80 | version = "1.9.1" 81 | source = { registry = "https://pypi.org/simple" } 82 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } 83 | wheels = [ 84 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, 85 | ] 86 | 87 | [[package]] 88 | name = "packaging" 89 | version = "25.0" 90 | source = { registry = "https://pypi.org/simple" } 91 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 92 | wheels = [ 93 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 94 | ] 95 | 96 | [[package]] 97 | name = "pluggy" 98 | version = "1.6.0" 99 | source = { registry = "https://pypi.org/simple" } 100 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 101 | wheels = [ 102 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 103 | ] 104 | 105 | [[package]] 106 | name = "pygments" 107 | version = "2.19.2" 108 | source = { registry = "https://pypi.org/simple" } 109 | sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } 110 | wheels = [ 111 | { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, 112 | ] 113 | 114 | [[package]] 115 | name = "pyright" 116 | version = "1.1.402" 117 | source = { registry = "https://pypi.org/simple" } 118 | dependencies = [ 119 | { name = "nodeenv" }, 120 | { name = "typing-extensions" }, 121 | ] 122 | sdist = { url = "https://files.pythonhosted.org/packages/aa/04/ce0c132d00e20f2d2fb3b3e7c125264ca8b909e693841210534b1ea1752f/pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683", size = 3888207, upload-time = "2025-06-11T08:48:35.759Z" } 123 | wheels = [ 124 | { url = "https://files.pythonhosted.org/packages/fe/37/1a1c62d955e82adae588be8e374c7f77b165b6cb4203f7d581269959abbc/pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982", size = 5624004, upload-time = "2025-06-11T08:48:33.998Z" }, 125 | ] 126 | 127 | [[package]] 128 | name = "pytest" 129 | version = "8.4.1" 130 | source = { registry = "https://pypi.org/simple" } 131 | dependencies = [ 132 | { name = "colorama", marker = "sys_platform == 'win32'" }, 133 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 134 | { name = "iniconfig" }, 135 | { name = "packaging" }, 136 | { name = "pluggy" }, 137 | { name = "pygments" }, 138 | { name = "tomli", marker = "python_full_version < '3.11'" }, 139 | ] 140 | sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } 141 | wheels = [ 142 | { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, 143 | ] 144 | 145 | [[package]] 146 | name = "pyyaml" 147 | version = "6.0.2" 148 | source = { registry = "https://pypi.org/simple" } 149 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } 150 | wheels = [ 151 | { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, 152 | { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, 153 | { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, 154 | { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, 155 | { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, 156 | { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, 157 | { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, 158 | { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, 159 | { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, 160 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, 161 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, 162 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, 163 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, 164 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, 165 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, 166 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, 167 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, 168 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, 169 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, 170 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, 171 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, 172 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, 173 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, 174 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, 175 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, 176 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, 177 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, 178 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, 179 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, 180 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, 181 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, 182 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, 183 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, 184 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, 185 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, 186 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, 187 | ] 188 | 189 | [[package]] 190 | name = "ruff" 191 | version = "0.10.0" 192 | source = { registry = "https://pypi.org/simple" } 193 | sdist = { url = "https://files.pythonhosted.org/packages/4c/ec/9c59d2956566517c98ac8267554f4eaceafb2a19710a429368518b7fab43/ruff-0.10.0.tar.gz", hash = "sha256:fa1554e18deaf8aa097dbcfeafaf38b17a2a1e98fdc18f50e62e8a836abee392", size = 3789921, upload-time = "2025-03-13T18:38:05.228Z" } 194 | wheels = [ 195 | { url = "https://files.pythonhosted.org/packages/bf/3f/742afe91b43def2a75990b293c676355576c0ff9cdbcf4249f78fa592544/ruff-0.10.0-py3-none-linux_armv6l.whl", hash = "sha256:46a2aa0eaae5048e5f804f0be9489d8a661633e23277b7293089e70d5c1a35c4", size = 10078369, upload-time = "2025-03-13T18:37:20.499Z" }, 196 | { url = "https://files.pythonhosted.org/packages/8d/a0/8696fb4862e82f7b40bbbc2917137594b22826cc62d77278a91391507514/ruff-0.10.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:775a6bc61af9dd0a2e1763406522a137e62aabb743d8b43ed95f019cdd1526c7", size = 10876912, upload-time = "2025-03-13T18:37:24.184Z" }, 197 | { url = "https://files.pythonhosted.org/packages/40/aa/0d48b7b7d7a1f168bb8fd893ed559d633c7d68c4a8ef9b996f0c2bd07aca/ruff-0.10.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8b03e6fcd39d20f0004f9956f0ed5eadc404d3a299f9d9286323884e3b663730", size = 10229962, upload-time = "2025-03-13T18:37:28.211Z" }, 198 | { url = "https://files.pythonhosted.org/packages/21/de/861ced2f75b045d8cfc038d68961d8ac117344df1f43a11abdd05bf7991b/ruff-0.10.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:621101d1af80248827f2409a78c8177c8319986a57b4663613b9c72f8617bfcd", size = 10404627, upload-time = "2025-03-13T18:37:30.626Z" }, 199 | { url = "https://files.pythonhosted.org/packages/21/69/666e0b840191c3ce433962c0d05fc0f6800afe259ea5d230cc731655d8e2/ruff-0.10.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2dfe85cb6bfbd4259801e7d4982f2a72bdbd5749dc73a09d68a6dbf77f2209a", size = 9939383, upload-time = "2025-03-13T18:37:33.186Z" }, 200 | { url = "https://files.pythonhosted.org/packages/76/bf/34a2adc58092c99cdfa9f1303acd82d840d56412022e477e2ab20c261d2d/ruff-0.10.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43ac3879a20c22fdc57e559f0bb27f0c71828656841d0b42d3505b1e5b3a83c8", size = 11492269, upload-time = "2025-03-13T18:37:35.377Z" }, 201 | { url = "https://files.pythonhosted.org/packages/31/3d/f7ccfcf69f15948623b190feea9d411d5029ae39725fcc078f8d43bd07a6/ruff-0.10.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ef5e3aac421bbc62f8a7aab21edd49a359ed42205f7a5091a74386bca1efa293", size = 12186939, upload-time = "2025-03-13T18:37:38.381Z" }, 202 | { url = "https://files.pythonhosted.org/packages/6e/3e/c557c0abfdea85c7d238a3cb238c73e7b6d17c30a584234c4fd8fe2cafb6/ruff-0.10.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f4f62d7fac8b748fce67ad308116b4d4cc1a9f964b4804fc5408fbd06e13ba9", size = 11655896, upload-time = "2025-03-13T18:37:40.753Z" }, 203 | { url = "https://files.pythonhosted.org/packages/3b/8e/3bfa110f37e5192eb3943f14943d05fbb9a76fea380aa87655e6f6276a54/ruff-0.10.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9f6205c5b0d626f98da01a0e75b724a64c21c554bba24b12522c9e9ba6a04", size = 13885502, upload-time = "2025-03-13T18:37:43.226Z" }, 204 | { url = "https://files.pythonhosted.org/packages/51/4a/22cdab59b5563dd7f4c504d0f1e6bb25fc800a5a057395bc24f8ff3a85b2/ruff-0.10.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46a97f3d55f68464c48d1e929a8582c7e5bb80ac73336bbc7b0da894d8e6cd9e", size = 11344767, upload-time = "2025-03-13T18:37:46.656Z" }, 205 | { url = "https://files.pythonhosted.org/packages/3d/0f/8f85de2ac565f82f47c6d8fb7ae04383e6300560f2d1b91c1268ff91e507/ruff-0.10.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0b811197d0dc96c13d610f8cfdc56030b405bcff5c2f10eab187b329da0ca4a", size = 10300331, upload-time = "2025-03-13T18:37:48.682Z" }, 206 | { url = "https://files.pythonhosted.org/packages/90/4a/b337df327832cb30bd8607e8d1fdf1b6b5ca228307d5008dd49028fb66ae/ruff-0.10.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a13a3fda0870c1c964b47ff5d73805ae80d2a9de93ee2d185d453b8fddf85a84", size = 9926551, upload-time = "2025-03-13T18:37:50.698Z" }, 207 | { url = "https://files.pythonhosted.org/packages/d7/e9/141233730b85675ac806c4b62f70516bd9c0aae8a55823f3a6589ed411be/ruff-0.10.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6ceb8d9f062e90ddcbad929f6136edf764bbf6411420a07e8357602ea28cd99f", size = 10925061, upload-time = "2025-03-13T18:37:53.28Z" }, 208 | { url = "https://files.pythonhosted.org/packages/24/09/02987935b55c2d353a226ac1b4f9718830e2e195834929f46c07eeede746/ruff-0.10.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c41d07d573617ed2f287ea892af2446fd8a8d877481e8e1ba6928e020665d240", size = 11394949, upload-time = "2025-03-13T18:37:55.375Z" }, 209 | { url = "https://files.pythonhosted.org/packages/d6/ec/054f9879fb6f4122d43ffe5c9f88c8c323a9cd14220d5c813aea5805e02c/ruff-0.10.0-py3-none-win32.whl", hash = "sha256:76e2de0cbdd587e373cd3b4050d2c45babdd7014c1888a6f121c29525c748a15", size = 10272077, upload-time = "2025-03-13T18:37:57.913Z" }, 210 | { url = "https://files.pythonhosted.org/packages/6e/49/915d8682f24645b904fe6a1aac36101464fc814923fdf293c1388dc5533c/ruff-0.10.0-py3-none-win_amd64.whl", hash = "sha256:f943acdecdcc6786a8d1dad455dd9f94e6d57ccc115be4993f9b52ef8316027a", size = 11393300, upload-time = "2025-03-13T18:38:00.414Z" }, 211 | { url = "https://files.pythonhosted.org/packages/82/ed/5c59941634c9026ceeccc7c119f23f4356f09aafd28c15c1bc734ac66b01/ruff-0.10.0-py3-none-win_arm64.whl", hash = "sha256:935a943bdbd9ff0685acd80d484ea91088e27617537b5f7ef8907187d19d28d0", size = 10510133, upload-time = "2025-03-13T18:38:02.91Z" }, 212 | ] 213 | 214 | [[package]] 215 | name = "setuptools" 216 | version = "80.9.0" 217 | source = { registry = "https://pypi.org/simple" } 218 | sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } 219 | wheels = [ 220 | { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, 221 | ] 222 | 223 | [[package]] 224 | name = "tomli" 225 | version = "2.2.1" 226 | source = { registry = "https://pypi.org/simple" } 227 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } 228 | wheels = [ 229 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, 230 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, 231 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, 232 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, 233 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, 234 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, 235 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, 236 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, 237 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, 238 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, 239 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, 240 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, 241 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, 242 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, 243 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, 244 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, 245 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, 246 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, 247 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, 248 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, 249 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, 250 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, 251 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, 252 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, 253 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, 254 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, 255 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, 256 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, 257 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, 258 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, 259 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, 260 | ] 261 | 262 | [[package]] 263 | name = "typing-extensions" 264 | version = "4.14.1" 265 | source = { registry = "https://pypi.org/simple" } 266 | sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } 267 | wheels = [ 268 | { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, 269 | ] 270 | 271 | [[package]] 272 | name = "vim-vint" 273 | version = "0.3.21" 274 | source = { registry = "https://pypi.org/simple" } 275 | dependencies = [ 276 | { name = "ansicolor" }, 277 | { name = "chardet" }, 278 | { name = "pyyaml" }, 279 | ] 280 | sdist = { url = "https://files.pythonhosted.org/packages/9c/c7/d5fbe5f778edee83cba3aea8cc3308db327e4c161e0656e861b9cc2cb859/vim-vint-0.3.21.tar.gz", hash = "sha256:5dc59b2e5c2a746c88f5f51f3fafea3d639c6b0fdbb116bb74af27bf1c820d97", size = 65889, upload-time = "2019-05-13T23:05:01.995Z" } 281 | wheels = [ 282 | { url = "https://files.pythonhosted.org/packages/c1/57/b174313406c08fb5a4327e9f288ead04503c00d23c4f1765d88ca451977a/vim_vint-0.3.21-py2.py3-none-any.whl", hash = "sha256:61357ebbb41e909e194a4187419ad72bde9e67cc12eb13b31c578866b99d45c0", size = 89165, upload-time = "2019-05-13T23:05:00.001Z" }, 283 | ] 284 | --------------------------------------------------------------------------------