diff --git a/.gitignore b/.gitignore index e033bc6..1e72bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ lazy-lock.json + +# Secrets - decrypted files +lua/secrets/decrypted.lua diff --git a/init.lua b/init.lua index 00866bb..5cf7f97 100644 --- a/init.lua +++ b/init.lua @@ -274,6 +274,13 @@ if not (vim.uv or vim.loop).fs_stat(lazypath) then end ---@diagnostic disable-next-line: undefined-field vim.opt.rtp:prepend(lazypath) +-- [[ Load Encrypted Secrets ]] +-- Auto-load secrets if password is set and encrypted file exists +-- Use :SecretManager set to set password, then :SecretManager load +pcall(function() + require('secrets.manager').auto_load() +end) + -- [[ Configure and Load Plugins with lazy.nvim ]] -- -- All actual plugin specs are imported from lua/plugins/ diff --git a/lua/model_fetcher.lua b/lua/model_fetcher.lua new file mode 100644 index 0000000..694e71f --- /dev/null +++ b/lua/model_fetcher.lua @@ -0,0 +1,238 @@ +--- Model fetcher utility for fetching available models from various AI providers +--- @class ModelFetcherConfig +--- @field endpoint string API endpoint to fetch models from +--- @field api_key_env string Environment variable name containing the API key +--- @field cache_file string Path to cache file (optional, will be generated if not provided) +--- @field cache_ttl number Cache time-to-live in seconds (default: 24 hours) +--- @field transform_response fun(body: table): string[]? Function to transform API response to model list +--- @field headers table? Additional headers to include in request +--- @field default_models string[]? Models to return if API call fails or no API key + +local M = {} + +--- Load models from cache synchronously +--- @param provider_name string Name of the provider +--- @param cache_ttl number? Cache TTL in seconds (default: 24 hours) +--- @return string[]? models Cached models or nil if not cached/expired +function M.load_cached_models(provider_name, cache_ttl) + local json = vim.json + local cache_file = vim.fn.stdpath 'cache' .. '/' .. provider_name .. '_models.json' + cache_ttl = cache_ttl or (24 * 60 * 60) + + local f = io.open(cache_file, 'r') + if not f then + return nil + end + local content = f:read '*a' + f:close() + local ok, data = pcall(json.decode, content) + if not ok or not data then + return nil + end + + -- Check if cache is expired + if os.time() - (data.timestamp or 0) > cache_ttl then + return nil + end + return data.models +end + +--- @param provider_name string Name of the provider (used for logging and cache file naming) +--- @param config ModelFetcherConfig Configuration for the model fetcher +--- @param cb fun(models: string[]?) Callback function to receive the models +function M.fetch_models(provider_name, config, cb) + local curl = require 'plenary.curl' + local log = require('plenary.log').new { plugin = provider_name } + local json = vim.json + + -- Set defaults + local cache_ttl = config.cache_ttl or (24 * 60 * 60) -- 24 hours + local cache_file = config.cache_file or (vim.fn.stdpath 'cache' .. '/' .. provider_name .. '_models.json') + + --- Load models from cache + --- @return string[]? + local function load_cache() + local f = io.open(cache_file, 'r') + if not f then + return nil + end + local content = f:read '*a' + f:close() + local ok, data = pcall(json.decode, content) + if not ok or not data then + return nil + end + + -- Check if cache is expired + if os.time() - (data.timestamp or 0) > cache_ttl then + log.debug('Cache expired for ' .. provider_name) + return nil + end + return data.models + end + + --- Save models to cache + --- @param models string[] + local function save_cache(models) + local f = io.open(cache_file, 'w') + if not f then + log.warn('Failed to open cache file for writing: ' .. cache_file) + return + end + local ok, encoded = pcall(json.encode, { + timestamp = os.time(), + models = models, + }) + if not ok then + log.warn 'Failed to encode cache data' + f:close() + return + end + f:write(encoded) + f:close() + log.debug('Cached models for ' .. provider_name) + end + + -- Try loading from cache first + local cached = load_cache() + if cached then + log.debug('Loading ' .. provider_name .. ' models from cache') + cb(cached) + return + end + + -- Check if API key is set + local api_key = os.getenv(config.api_key_env) + if not api_key or api_key == '' then + log.debug(config.api_key_env .. ' not set for ' .. provider_name) + -- Return default models if provided + cb(config.default_models) + return + end + + -- Build headers + local headers = { + ['Authorization'] = 'Bearer ' .. api_key, + ['Content-Type'] = 'application/json', + } + + -- Merge additional headers if provided + if config.headers then + for k, v in pairs(config.headers) do + headers[k] = v + end + end + + -- Fetch models from API + log.debug('Fetching models for ' .. provider_name .. ' from ' .. config.endpoint) + curl.get(config.endpoint, { + headers = headers, + callback = function(res) + if res.status ~= 200 then + vim.schedule(function() + local msg = string.format('Failed to fetch %s models: HTTP %d', provider_name, res.status) + log.warn(msg) + vim.notify(msg, vim.log.levels.WARN) + end) + -- Fallback to default models + vim.schedule(function() + cb(config.default_models) + end) + return + end + + local ok, body = pcall(json.decode, res.body) + if not ok or not body then + vim.schedule(function() + local msg = string.format('Error decoding %s response', provider_name) + log.error(msg) + vim.notify(msg, vim.log.levels.ERROR) + end) + vim.schedule(function() + cb(config.default_models) + end) + return + end + + -- Transform the response to get model list + local models = config.transform_response(body) + + if not models or #models == 0 then + vim.schedule(function() + local msg = string.format('No models found in %s response', provider_name) + log.warn(msg) + cb(config.default_models) + end) + return + end + + log.debug(string.format('Loaded %d models from %s', #models, provider_name)) + save_cache(models) + + vim.schedule(function() + cb(models) + end) + end, + }) +end + +--- Convenience function for OpenRouter +--- @param cb fun(models: string[]?) +function M.fetch_openrouter_models(cb) + M.fetch_models('openrouter', { + endpoint = 'https://openrouter.ai/api/v1/models', + api_key_env = 'OPENROUTER_API_KEY', + default_models = { + 'anthropic/claude-sonnet-4-20250514', + 'deepseek/deepseek-chat-v3.1', + 'x-ai/grok-code-fast-1', + 'qwen/qwen3-coder', + 'openai/gpt-4o', + 'google/gemini-2.5-flash-exp', + }, + transform_response = function(body) + if not body.data then + return nil + end + local models = {} + for _, m in ipairs(body.data) do + table.insert(models, m.id) + end + return models + end, + }, cb) +end + +--- Convenience function for Cerebro +--- @param cb fun(models: string[]?) +function M.fetch_cerebro_models(cb) + M.fetch_models('cerebro', { + endpoint = 'https://chat.cerebroai.de/api/v1/models', + api_key_env = 'AVANTE_CEREBRO_API_KEY', + default_models = { + 'gemini-3-pro-preview', + 'gemini-2.5-flash', + 'gpt-5-chat-latest', + 'gpt-4o', + }, + transform_response = function(body) + if body.data then + local models = {} + for _, m in ipairs(body.data) do + -- Handle both {id: "..."} and direct string formats + local model_id = type(m) == 'string' and m or m.id + if model_id then + table.insert(models, model_id) + end + end + return models + elseif body.models then + -- Alternative response format + return body.models + end + return nil + end, + }, cb) +end + +return M diff --git a/lua/plugins/avante.lua b/lua/plugins/avante.lua new file mode 100644 index 0000000..a600794 --- /dev/null +++ b/lua/plugins/avante.lua @@ -0,0 +1,307 @@ +-- Import the model fetcher utility +local model_fetcher = require 'model_fetcher' + +--- @type LazySpec +return { + 'yetone/avante.nvim', + event = 'VeryLazy', + version = false, -- Never set to "*" + -- Build function + build = vim.fn.has 'win32' == 1 and 'powershell -ExecutionPolicy Bypass -File Build.ps1 -BuildFromSource false' or 'make', + config = function() + ---@module 'avante' + ---@type avante.Config + local opt = { + -- Default provider + provider = 'opencode', + auto_suggestions_provider = 'copilot', + -- ACP Provider Configuration - Integration with Claude Code + acp_providers = { + ['claude-code'] = { + command = 'npx', + args = { '@zed-industries/claude-code-acp' }, + env = { + NODE_NO_WARNINGS = '1', + ANTHROPIC_API_KEY = os.getenv 'ANTHROPIC_API_KEY', + }, + }, + ['opencode'] = { + command = 'opencode', + args = { 'acp' }, + }, + }, + + -- Provider configurations + providers = { + -- OpenCode ACP Provider (also in acp_providers) + -- This dummy entry prevents errors when ACP is the active provider + opencode = { + __inherited_from = 'openai', -- Required by avante, but not actually used for ACP + model = 'default', + hide_in_model_selector = true, -- Hide from model selector since it's ACP + }, + + -- Copilot for auto-suggestions + copilot = { + model = 'gpt-4o-2024-11-20', + }, + + -- OpenRouter with dynamic model loading + openrouter = { + __inherited_from = 'openai', + endpoint = 'https://openrouter.ai/api/v1', + api_key_name = 'AVANTE_OPENROUTER_API_KEY', + model = 'anthropic/claude-sonnet-4-20250514', + model_names = { + 'anthropic/claude-sonnet-4-20250514', + 'deepseek/deepseek-chat-v3.1', + 'x-ai/grok-code-fast-1', + 'qwen/qwen3-coder', + 'openai/gpt-4o', + 'google/gemini-2.5-flash-exp', + }, + }, + + -- Cerebro (if you still use it) + cerebro = { + __inherited_from = 'openai', + endpoint = 'https://chat.cerebroai.de/api/v1', + api_key_name = 'AVANTE_CEREBRO_API_KEY', + model = 'gemini-3-pro-preview', + model_names = { + 'gemini-3-pro-preview', + 'gemini-2.5-flash', + 'gpt-5-chat-latest', + 'gpt-4o', + }, + }, + }, + + -- Behavior settings + behaviour = { + auto_suggestions = false, -- Enable/disable inline suggestions + auto_set_keymaps = true, + auto_apply_diff_after_generation = false, + support_paste_from_clipboard = false, + }, + + -- Prompt logger for debugging + prompt_logger = { + enabled = true, + log_dir = vim.fn.stdpath 'cache' .. '/avante_prompts', + next_prompt = { + normal = '', + insert = '', + }, + prev_prompt = { + normal = '', + insert = '', + }, + }, + + -- History configuration + history = { + max_tokens = 327680, + }, + + -- Keymaps + mappings = { + --- @class AvanteConflictMappings + diff = { + ours = 'co', + theirs = 'ct', + all_theirs = 'ca', + both = 'cb', + cursor = 'cc', + next = ']x', + prev = '[x', + }, + suggestion = { + accept = '', + next = '', + prev = '', + dismiss = '', + }, + jump = { + next = ']]', + prev = '[[', + }, + submit = { + normal = '', + insert = '', + }, + cancel = { + normal = { '', '', 'q' }, + insert = { '' }, + }, + sidebar = { + apply_all = 'A', + apply_cursor = 'a', + retry_user_request = 'r', + edit_user_request = 'e', + switch_windows = '', + reverse_switch_windows = '', + remove_file = 'd', + add_file = '@', + close = { '', 'q' }, + close_from_input = { '' }, + }, + }, + + --- @class AvanteHintsConfig + hints = { enabled = true }, + + -- Window configuration + windows = { + ---@type "right" | "left" | "top" | "bottom" + position = 'right', + wrap = true, + width = 35, -- % of available width + sidebar_header = { + enabled = true, + align = 'center', + rounded = true, + }, + }, + + -- Suggestion settings + suggestion = { + debounce = 500, + throttle = 800, + }, + } + + -- Load cached models synchronously if available + local openrouter_cached = model_fetcher.load_cached_models 'openrouter' + if openrouter_cached and #openrouter_cached > 0 then + opt.providers.openrouter.model_names = openrouter_cached + end + + local cerebro_cached = model_fetcher.load_cached_models 'cerebro' + if cerebro_cached and #cerebro_cached > 0 then + opt.providers.cerebro.model_names = cerebro_cached + end + + -- Setup Avante with loaded configuration (using cached models if available) + require('avante').setup(opt) + + -- Fetch models in the background to update cache for next session + -- This happens asynchronously and won't block usage + model_fetcher.fetch_openrouter_models(function(models) + -- Models will be cached and available on next restart + end) + + model_fetcher.fetch_cerebro_models(function(models) + -- Models will be cached and available on next restart + end) + + -- Additional keymaps for Avante + vim.keymap.set({ 'n', 'v' }, 'aa', function() + require('avante.api').ask() + end, { desc = '[A]vante [A]sk' }) + + vim.keymap.set('n', 'ac', function() + vim.cmd 'AvanteChat' + end, { desc = '[A]vante [C]hat' }) + + vim.keymap.set('n', 'ar', function() + require('avante.api').refresh() + end, { desc = '[A]vante [R]efresh' }) + + vim.keymap.set('v', 'ae', function() + require('avante.api').edit() + end, { desc = '[A]vante [E]dit' }) + + -- Provider switcher that works with both ACP and regular providers + vim.keymap.set('n', 'ap', function() + local Config = require 'avante.config' + local providers = {} + + -- Add ACP providers + for name, _ in pairs(Config.acp_providers or {}) do + table.insert(providers, { name = name, type = 'acp' }) + end + + -- Add regular providers + for name, _ in pairs(Config.providers or {}) do + if name ~= 'opencode' and name ~= 'copilot' then -- Exclude ACP entries and copilot + table.insert(providers, { name = name, type = 'regular' }) + end + end + + -- Use vim.ui.select to show provider picker + vim.ui.select(providers, { + prompt = 'Select Provider:', + format_item = function(item) + return item.name .. ' (' .. item.type .. ')' + end, + }, function(choice) + if choice then + require('avante.providers').refresh(choice.name) + vim.notify('Switched to provider: ' .. choice.name, vim.log.levels.INFO) + end + end) + end, { desc = '[A]vante [P]rovider switch' }) + + -- Zen mode alias for terminal + vim.api.nvim_create_user_command('AvanteZen', function() + require('avante.api').zen_mode() + end, { desc = 'Start Avante in Zen mode' }) + end, + + dependencies = { + -- Required dependencies + 'nvim-lua/plenary.nvim', + 'MunifTanjim/nui.nvim', + + -- File selector (pick one or multiple) + 'nvim-telescope/telescope.nvim', + -- 'ibhagwan/fzf-lua', + -- 'echasnovski/mini.pick', + + -- Input provider (recommended) + { + 'folke/snacks.nvim', + opts = { + input = {}, + picker = {}, + terminal = {}, + }, + }, + -- 'stevearc/dressing.nvim', + + -- Icons + 'nvim-tree/nvim-web-devicons', + + -- Completion + 'hrsh7th/nvim-cmp', + + -- Copilot integration + 'zbirenbaum/copilot.lua', + + -- Image pasting support + { + 'HakonHarnes/img-clip.nvim', + event = 'VeryLazy', + opts = { + default = { + embed_image_as_base64 = false, + prompt_for_file_name = false, + drag_and_drop = { + insert_mode = true, + }, + use_absolute_path = true, + }, + }, + }, + + -- Markdown rendering for Avante + { + 'MeanderingProgrammer/render-markdown.nvim', + opts = { + file_types = { 'markdown', 'Avante' }, + }, + ft = { 'markdown', 'Avante' }, + }, + }, +} diff --git a/lua/plugins/telescope.lua b/lua/plugins/telescope.lua index 86561d1..750add3 100644 --- a/lua/plugins/telescope.lua +++ b/lua/plugins/telescope.lua @@ -46,7 +46,7 @@ end return { -- Fuzzy Finder (files, lsp, etc) 'nvim-telescope/telescope.nvim', event = 'VimEnter', - branch = '0.1.x', + branch = 'master', dependencies = { { 'nvim-treesitter/nvim-treesitter' }, 'nvim-lua/plenary.nvim', @@ -62,13 +62,6 @@ return { -- Fuzzy Finder (files, lsp, etc) -- See `:help telescope` and `:help telescope.setup()` require('telescope').setup { -- You can put your default mappings / updates / etc. in here - -- All the info you're looking for is in `:help telescope.setup()` - -- - -- defaults = { - -- mappings = { - -- i = { [''] = 'to_fuzzy_refine' }, - -- }, - -- }, -- pickers = {} extensions = { ['ui-select'] = { diff --git a/lua/secrets/decrypted.example.lua b/lua/secrets/decrypted.example.lua new file mode 100644 index 0000000..2ed4dcc --- /dev/null +++ b/lua/secrets/decrypted.example.lua @@ -0,0 +1,11 @@ +-- Secrets file template +-- This file contains your age password and environment variables +-- rename it to decrypted.lua +-- Encrypt it with: :SecretManager encrypt + +return { + password = 'your-age-password-here', + env = { + SECRET_ENV_VAR_1 = 'your-secret-value-1', + }, +} diff --git a/lua/secrets/manager.lua b/lua/secrets/manager.lua new file mode 100644 index 0000000..bafe90e --- /dev/null +++ b/lua/secrets/manager.lua @@ -0,0 +1,271 @@ +-- Secrets Manager with age encryption +-- Stores password and env vars in encrypted file +-- Auto-loads from decrypted plaintext on startup + +local M = {} + +-- Path to files +local ENCRYPTED_FILE = vim.fn.stdpath 'config' .. '/lua/secrets/secrets.enc' +local DECRYPTED_FILE = vim.fn.stdpath 'config' .. '/lua/secrets/decrypted.lua' + +-- Encryption algorithm (chacha20 is modern and fast) +local CIPHER = 'chacha20' + +---Load decrypted secrets file and return the table +---@return table|nil secrets {password = "...", env = {...}} +---@return string|nil error +local function load_decrypted_file() + if vim.fn.filereadable(DECRYPTED_FILE) ~= 1 then + return nil, 'Decrypted file not found' + end + + local chunk, err = loadfile(DECRYPTED_FILE) + if not chunk then + return nil, 'Failed to load file: ' .. (err or 'unknown error') + end + + local ok, result = pcall(chunk) + if not ok then + return nil, 'Failed to execute file: ' .. result + end + + if type(result) ~= 'table' then + return nil, 'File must return a table' + end + + return result, nil +end + +---Decrypt encrypted file with password +---@param password string +---@return boolean success +---@return string|nil error +function M.decrypt(password) + if vim.fn.executable('openssl') ~= 1 then + return false, 'openssl not found. Install with: sudo pacman -S openssl' + end + + if vim.fn.filereadable(ENCRYPTED_FILE) ~= 1 then + return false, 'Encrypted file not found: ' .. ENCRYPTED_FILE + end + + if not password or password == '' then + return false, 'Password required' + end + + -- Use openssl for decryption + local cmd = string.format( + 'openssl enc -%s -d -pbkdf2 -in %s -out %s -pass pass:%s 2>&1', + CIPHER, + vim.fn.shellescape(ENCRYPTED_FILE), + vim.fn.shellescape(DECRYPTED_FILE), + vim.fn.shellescape(password) + ) + + local result = vim.fn.system(cmd) + local success = vim.v.shell_error == 0 + + if success then + vim.notify('Decrypted to: ' .. DECRYPTED_FILE, vim.log.levels.INFO) + return true, nil + else + return false, 'Decryption failed. Check your password: ' .. result + end +end + +---Encrypt decrypted file +---@param new_password string|nil Optional new password (uses stored password if not provided) +---@return boolean success +---@return string|nil error +function M.encrypt(new_password) + if vim.fn.executable('openssl') ~= 1 then + return false, 'openssl not found. Install with: sudo pacman -S openssl' + end + + local secrets, err = load_decrypted_file() + if not secrets then + return false, 'Cannot load decrypted file: ' .. (err or 'unknown error') + end + + -- Use new password or stored password + local password = new_password or secrets.password + if not password or password == '' then + return false, 'No password found. Use: SecretManager encrypt ' + end + + -- Update password in file if new password provided + if new_password and new_password ~= secrets.password then + secrets.password = new_password + + -- Read current file content + local file = io.open(DECRYPTED_FILE, 'r') + if not file then + return false, 'Failed to read file' + end + local content = file:read('*a') + file:close() + + -- Replace password line + content = content:gsub('password%s*=%s*[^\n]+', 'password = ' .. string.format('%q', new_password)) + + -- Write back + file = io.open(DECRYPTED_FILE, 'w') + if not file then + return false, 'Failed to write file' + end + file:write(content) + file:close() + end + + -- Use openssl for symmetric encryption + local cmd = string.format( + 'openssl enc -%s -salt -pbkdf2 -in %s -out %s -pass pass:%s 2>&1', + CIPHER, + vim.fn.shellescape(DECRYPTED_FILE), + vim.fn.shellescape(ENCRYPTED_FILE), + vim.fn.shellescape(password) + ) + + local result = vim.fn.system(cmd) + local success = vim.v.shell_error == 0 + + if success then + vim.notify('Encrypted to: ' .. ENCRYPTED_FILE, vim.log.levels.INFO) + if new_password then + vim.notify('Password updated', vim.log.levels.INFO) + end + return true, nil + else + return false, 'Encryption failed: ' .. result + end +end + +---Load environment variables from decrypted file +---@param silent boolean|nil Don't show notifications if true +---@return boolean success +---@return string|nil error +function M.load(silent) + local secrets, err = load_decrypted_file() + + if not secrets then + if not silent then + vim.notify('No decrypted secrets file found', vim.log.levels.WARN) + vim.notify('Run: SecretManager decrypt ', vim.log.levels.INFO) + end + return false, err + end + + if not secrets.env or type(secrets.env) ~= 'table' then + if not silent then + vim.notify('No env variables found in secrets file', vim.log.levels.ERROR) + end + return false, 'Invalid secrets format' + end + + -- Load into environment + local count = 0 + for key, value in pairs(secrets.env) do + vim.env[key] = value + count = count + 1 + end + + if not silent then + vim.notify(string.format('Loaded %d secret(s) into environment', count), vim.log.levels.INFO) + end + + return true, nil +end + +---Auto-load on startup (shows hint if no decrypted file exists) +function M.auto_load() + -- No secret files to load + if vim.fn.filereadable(DECRYPTED_FILE) ~= 1 and vim.fn.filereadable(ENCRYPTED_FILE) ~= 1 then + return + end + + -- Hint to decrypt if no decrypted file + if vim.fn.filereadable(DECRYPTED_FILE) ~= 1 then + vim.defer_fn(function() + vim.notify('Run: SecretManager decrypt ', vim.log.levels.WARN) + end, 500) + return + end + + -- Warn if decrypted file exists without encrypted file + if vim.fn.filereadable(ENCRYPTED_FILE) ~= 1 and vim.fn.filereadable(DECRYPTED_FILE) == 1 then + vim.defer_fn(function() + vim.notify('Decrypted secrets file found without corresponding encrypted file. Run :SecretManager encrypt ', vim.log.levels.WARN) + end, 500) + end + + -- Load silently if file exists + return M.load(true) +end + +-- Create user command +vim.api.nvim_create_user_command('SecretManager', function(opts) + local args = vim.split(opts.args, '%s+') + local cmd = args[1] + + if cmd == 'decrypt' then + local password = args[2] + + -- Try to get password from existing decrypted file if not provided + if not password then + local secrets = load_decrypted_file() + if secrets and secrets.password then + password = secrets.password + else + vim.notify('Usage: SecretManager decrypt ', vim.log.levels.ERROR) + return + end + end + + local ok, err = M.decrypt(password) + if not ok then + vim.notify('Decrypt failed: ' .. (err or 'unknown error'), vim.log.levels.ERROR) + end + + elseif cmd == 'encrypt' then + local new_password = args[2] + local ok, err = M.encrypt(new_password) + if not ok then + vim.notify('Encrypt failed: ' .. (err or 'unknown error'), vim.log.levels.ERROR) + end + + elseif cmd == 'load' or cmd == 'refresh' then + M.load() + elseif cmd == 'status' then + local has_encrypted = vim.fn.filereadable(ENCRYPTED_FILE) == 1 + local has_decrypted = vim.fn.filereadable(DECRYPTED_FILE) == 1 + + local status = 'Secrets Manager Status:\n' + status = status .. ' Encrypted file: ' .. (has_encrypted and 'yes' or 'no') .. '\n' + status = status .. ' Decrypted file: ' .. (has_decrypted and 'yes' or 'no') + + if has_decrypted then + local secrets = load_decrypted_file() + if secrets and secrets.env then + local count = 0 + for _ in pairs(secrets.env) do + count = count + 1 + end + status = status .. '\n Env variables: ' .. count + end + end + + vim.notify(status, vim.log.levels.INFO) + end +end, { + nargs = '*', + complete = function(_, line) + local cmds = { 'decrypt', 'encrypt', 'load', 'refresh', 'status' } + local args = vim.split(line, '%s+') + if #args <= 2 then + return cmds + end + return {} + end, +}) + +return M diff --git a/lua/secrets/secrets.enc b/lua/secrets/secrets.enc new file mode 100644 index 0000000..9037bbb --- /dev/null +++ b/lua/secrets/secrets.enc @@ -0,0 +1,2 @@ +Salted__o�)��3g�7�)�;�"ه��{����8DJ ��S�� +�/2{��ڋ�,b6��:Ү^��못Z�,� `�o�.!�$9d}O���}U'[����f�7�{/�������(�@�Z� d%7������B�\$��b7[\"�3$��E�s�|�%��\��g;��\�����ծs \ No newline at end of file