feat: adding lua linter
This commit is contained in:
@@ -11,112 +11,112 @@ local utils = require("codetyper.support.utils")
|
||||
--- Get the credentials file path
|
||||
---@return string Path to credentials file
|
||||
local function get_credentials_path()
|
||||
local data_dir = vim.fn.stdpath("data")
|
||||
return data_dir .. "/codetyper/configuration.json"
|
||||
local data_dir = vim.fn.stdpath("data")
|
||||
return data_dir .. "/codetyper/configuration.json"
|
||||
end
|
||||
|
||||
--- Ensure the credentials directory exists
|
||||
---@return boolean Success
|
||||
local function ensure_dir()
|
||||
local data_dir = vim.fn.stdpath("data")
|
||||
local codetyper_dir = data_dir .. "/codetyper"
|
||||
return utils.ensure_dir(codetyper_dir)
|
||||
local data_dir = vim.fn.stdpath("data")
|
||||
local codetyper_dir = data_dir .. "/codetyper"
|
||||
return utils.ensure_dir(codetyper_dir)
|
||||
end
|
||||
|
||||
--- Load credentials from file
|
||||
---@return table Credentials data
|
||||
function M.load()
|
||||
local path = get_credentials_path()
|
||||
local content = utils.read_file(path)
|
||||
local path = get_credentials_path()
|
||||
local content = utils.read_file(path)
|
||||
|
||||
if not content or content == "" then
|
||||
return {
|
||||
version = 1,
|
||||
providers = {},
|
||||
}
|
||||
end
|
||||
if not content or content == "" then
|
||||
return {
|
||||
version = 1,
|
||||
providers = {},
|
||||
}
|
||||
end
|
||||
|
||||
local ok, data = pcall(vim.json.decode, content)
|
||||
if not ok or not data then
|
||||
return {
|
||||
version = 1,
|
||||
providers = {},
|
||||
}
|
||||
end
|
||||
local ok, data = pcall(vim.json.decode, content)
|
||||
if not ok or not data then
|
||||
return {
|
||||
version = 1,
|
||||
providers = {},
|
||||
}
|
||||
end
|
||||
|
||||
return data
|
||||
return data
|
||||
end
|
||||
|
||||
--- Save credentials to file
|
||||
---@param data table Credentials data
|
||||
---@return boolean Success
|
||||
function M.save(data)
|
||||
if not ensure_dir() then
|
||||
return false
|
||||
end
|
||||
if not ensure_dir() then
|
||||
return false
|
||||
end
|
||||
|
||||
local path = get_credentials_path()
|
||||
local ok, json = pcall(vim.json.encode, data)
|
||||
if not ok then
|
||||
return false
|
||||
end
|
||||
local path = get_credentials_path()
|
||||
local ok, json = pcall(vim.json.encode, data)
|
||||
if not ok then
|
||||
return false
|
||||
end
|
||||
|
||||
return utils.write_file(path, json)
|
||||
return utils.write_file(path, json)
|
||||
end
|
||||
|
||||
--- Get API key for a provider
|
||||
---@param provider string Provider name (copilot, ollama)
|
||||
---@return string|nil API key or nil if not found
|
||||
function M.get_api_key(provider)
|
||||
local data = M.load()
|
||||
local provider_data = data.providers and data.providers[provider]
|
||||
local data = M.load()
|
||||
local provider_data = data.providers and data.providers[provider]
|
||||
|
||||
if provider_data and provider_data.api_key then
|
||||
return provider_data.api_key
|
||||
end
|
||||
if provider_data and provider_data.api_key then
|
||||
return provider_data.api_key
|
||||
end
|
||||
|
||||
return nil
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Get model for a provider
|
||||
---@param provider string Provider name
|
||||
---@return string|nil Model name or nil if not found
|
||||
function M.get_model(provider)
|
||||
local data = M.load()
|
||||
local provider_data = data.providers and data.providers[provider]
|
||||
local data = M.load()
|
||||
local provider_data = data.providers and data.providers[provider]
|
||||
|
||||
if provider_data and provider_data.model then
|
||||
return provider_data.model
|
||||
end
|
||||
if provider_data and provider_data.model then
|
||||
return provider_data.model
|
||||
end
|
||||
|
||||
return nil
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Get endpoint for a provider (for custom OpenAI-compatible endpoints)
|
||||
---@param provider string Provider name
|
||||
---@return string|nil Endpoint URL or nil if not found
|
||||
function M.get_endpoint(provider)
|
||||
local data = M.load()
|
||||
local provider_data = data.providers and data.providers[provider]
|
||||
local data = M.load()
|
||||
local provider_data = data.providers and data.providers[provider]
|
||||
|
||||
if provider_data and provider_data.endpoint then
|
||||
return provider_data.endpoint
|
||||
end
|
||||
if provider_data and provider_data.endpoint then
|
||||
return provider_data.endpoint
|
||||
end
|
||||
|
||||
return nil
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Get host for Ollama
|
||||
---@return string|nil Host URL or nil if not found
|
||||
function M.get_ollama_host()
|
||||
local data = M.load()
|
||||
local provider_data = data.providers and data.providers.ollama
|
||||
local data = M.load()
|
||||
local provider_data = data.providers and data.providers.ollama
|
||||
|
||||
if provider_data and provider_data.host then
|
||||
return provider_data.host
|
||||
end
|
||||
if provider_data and provider_data.host then
|
||||
return provider_data.host
|
||||
end
|
||||
|
||||
return nil
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Set credentials for a provider
|
||||
@@ -124,452 +124,452 @@ end
|
||||
---@param credentials table Credentials (api_key, model, endpoint, host)
|
||||
---@return boolean Success
|
||||
function M.set_credentials(provider, credentials)
|
||||
local data = M.load()
|
||||
local data = M.load()
|
||||
|
||||
if not data.providers then
|
||||
data.providers = {}
|
||||
end
|
||||
if not data.providers then
|
||||
data.providers = {}
|
||||
end
|
||||
|
||||
if not data.providers[provider] then
|
||||
data.providers[provider] = {}
|
||||
end
|
||||
if not data.providers[provider] then
|
||||
data.providers[provider] = {}
|
||||
end
|
||||
|
||||
-- Merge credentials
|
||||
for key, value in pairs(credentials) do
|
||||
if value and value ~= "" then
|
||||
data.providers[provider][key] = value
|
||||
end
|
||||
end
|
||||
-- Merge credentials
|
||||
for key, value in pairs(credentials) do
|
||||
if value and value ~= "" then
|
||||
data.providers[provider][key] = value
|
||||
end
|
||||
end
|
||||
|
||||
data.updated = os.time()
|
||||
data.updated = os.time()
|
||||
|
||||
return M.save(data)
|
||||
return M.save(data)
|
||||
end
|
||||
|
||||
--- Remove credentials for a provider
|
||||
---@param provider string Provider name
|
||||
---@return boolean Success
|
||||
function M.remove_credentials(provider)
|
||||
local data = M.load()
|
||||
local data = M.load()
|
||||
|
||||
if data.providers and data.providers[provider] then
|
||||
data.providers[provider] = nil
|
||||
data.updated = os.time()
|
||||
return M.save(data)
|
||||
end
|
||||
if data.providers and data.providers[provider] then
|
||||
data.providers[provider] = nil
|
||||
data.updated = os.time()
|
||||
return M.save(data)
|
||||
end
|
||||
|
||||
return true
|
||||
return true
|
||||
end
|
||||
|
||||
--- List all configured providers (checks both stored credentials AND config)
|
||||
---@return table List of provider names with their config status
|
||||
function M.list_providers()
|
||||
local data = M.load()
|
||||
local result = {}
|
||||
local data = M.load()
|
||||
local result = {}
|
||||
|
||||
local all_providers = { "copilot", "ollama" }
|
||||
local all_providers = { "copilot", "ollama" }
|
||||
|
||||
for _, provider in ipairs(all_providers) do
|
||||
local provider_data = data.providers and data.providers[provider]
|
||||
local has_stored_key = provider_data and provider_data.api_key and provider_data.api_key ~= ""
|
||||
local has_model = provider_data and provider_data.model and provider_data.model ~= ""
|
||||
for _, provider in ipairs(all_providers) do
|
||||
local provider_data = data.providers and data.providers[provider]
|
||||
local has_stored_key = provider_data and provider_data.api_key and provider_data.api_key ~= ""
|
||||
local has_model = provider_data and provider_data.model and provider_data.model ~= ""
|
||||
|
||||
local configured_from_config = false
|
||||
local config_model = nil
|
||||
local ok, codetyper = pcall(require, "codetyper")
|
||||
if ok then
|
||||
local config = codetyper.get_config()
|
||||
if config and config.llm and config.llm[provider] then
|
||||
local pc = config.llm[provider]
|
||||
config_model = pc.model
|
||||
local configured_from_config = false
|
||||
local config_model = nil
|
||||
local ok, codetyper = pcall(require, "codetyper")
|
||||
if ok then
|
||||
local config = codetyper.get_config()
|
||||
if config and config.llm and config.llm[provider] then
|
||||
local pc = config.llm[provider]
|
||||
config_model = pc.model
|
||||
|
||||
if provider == "copilot" then
|
||||
configured_from_config = true
|
||||
elseif provider == "ollama" then
|
||||
configured_from_config = pc.host ~= nil
|
||||
end
|
||||
end
|
||||
end
|
||||
if provider == "copilot" then
|
||||
configured_from_config = true
|
||||
elseif provider == "ollama" then
|
||||
configured_from_config = pc.host ~= nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local is_configured = has_stored_key
|
||||
or (provider == "ollama" and provider_data ~= nil)
|
||||
or (provider == "copilot" and (provider_data ~= nil or configured_from_config))
|
||||
or configured_from_config
|
||||
local is_configured = has_stored_key
|
||||
or (provider == "ollama" and provider_data ~= nil)
|
||||
or (provider == "copilot" and (provider_data ~= nil or configured_from_config))
|
||||
or configured_from_config
|
||||
|
||||
table.insert(result, {
|
||||
name = provider,
|
||||
configured = is_configured,
|
||||
has_api_key = has_stored_key,
|
||||
has_model = has_model or config_model ~= nil,
|
||||
model = (provider_data and provider_data.model) or config_model,
|
||||
source = has_stored_key and "stored" or (configured_from_config and "config" or nil),
|
||||
})
|
||||
end
|
||||
table.insert(result, {
|
||||
name = provider,
|
||||
configured = is_configured,
|
||||
has_api_key = has_stored_key,
|
||||
has_model = has_model or config_model ~= nil,
|
||||
model = (provider_data and provider_data.model) or config_model,
|
||||
source = has_stored_key and "stored" or (configured_from_config and "config" or nil),
|
||||
})
|
||||
end
|
||||
|
||||
return result
|
||||
return result
|
||||
end
|
||||
|
||||
--- Default models for each provider
|
||||
M.default_models = {
|
||||
copilot = "claude-sonnet-4",
|
||||
ollama = "deepseek-coder:6.7b",
|
||||
copilot = "claude-sonnet-4",
|
||||
ollama = "deepseek-coder:6.7b",
|
||||
}
|
||||
|
||||
--- Available models for Copilot (GitHub Copilot Chat API)
|
||||
--- Models with cost multipliers: 0x = free, 0.33x = discount, 1x = standard, 3x = premium
|
||||
M.copilot_models = {
|
||||
-- Free tier (0x)
|
||||
{ name = "gpt-4.1", cost = "0x" },
|
||||
{ name = "gpt-4o", cost = "0x" },
|
||||
{ name = "gpt-5-mini", cost = "0x" },
|
||||
{ name = "grok-code-fast-1", cost = "0x" },
|
||||
{ name = "raptor-mini", cost = "0x" },
|
||||
-- Discount tier (0.33x)
|
||||
{ name = "claude-haiku-4.5", cost = "0.33x" },
|
||||
{ name = "gemini-3-flash", cost = "0.33x" },
|
||||
{ name = "gpt-5.1-codex-mini", cost = "0.33x" },
|
||||
-- Standard tier (1x)
|
||||
{ name = "claude-sonnet-4", cost = "1x" },
|
||||
{ name = "claude-sonnet-4.5", cost = "1x" },
|
||||
{ name = "gemini-2.5-pro", cost = "1x" },
|
||||
{ name = "gemini-3-pro", cost = "1x" },
|
||||
{ name = "gpt-5", cost = "1x" },
|
||||
{ name = "gpt-5-codex", cost = "1x" },
|
||||
{ name = "gpt-5.1", cost = "1x" },
|
||||
{ name = "gpt-5.1-codex", cost = "1x" },
|
||||
{ name = "gpt-5.1-codex-max", cost = "1x" },
|
||||
{ name = "gpt-5.2", cost = "1x" },
|
||||
{ name = "gpt-5.2-codex", cost = "1x" },
|
||||
-- Premium tier (3x)
|
||||
{ name = "claude-opus-4.5", cost = "3x" },
|
||||
-- Free tier (0x)
|
||||
{ name = "gpt-4.1", cost = "0x" },
|
||||
{ name = "gpt-4o", cost = "0x" },
|
||||
{ name = "gpt-5-mini", cost = "0x" },
|
||||
{ name = "grok-code-fast-1", cost = "0x" },
|
||||
{ name = "raptor-mini", cost = "0x" },
|
||||
-- Discount tier (0.33x)
|
||||
{ name = "claude-haiku-4.5", cost = "0.33x" },
|
||||
{ name = "gemini-3-flash", cost = "0.33x" },
|
||||
{ name = "gpt-5.1-codex-mini", cost = "0.33x" },
|
||||
-- Standard tier (1x)
|
||||
{ name = "claude-sonnet-4", cost = "1x" },
|
||||
{ name = "claude-sonnet-4.5", cost = "1x" },
|
||||
{ name = "gemini-2.5-pro", cost = "1x" },
|
||||
{ name = "gemini-3-pro", cost = "1x" },
|
||||
{ name = "gpt-5", cost = "1x" },
|
||||
{ name = "gpt-5-codex", cost = "1x" },
|
||||
{ name = "gpt-5.1", cost = "1x" },
|
||||
{ name = "gpt-5.1-codex", cost = "1x" },
|
||||
{ name = "gpt-5.1-codex-max", cost = "1x" },
|
||||
{ name = "gpt-5.2", cost = "1x" },
|
||||
{ name = "gpt-5.2-codex", cost = "1x" },
|
||||
-- Premium tier (3x)
|
||||
{ name = "claude-opus-4.5", cost = "3x" },
|
||||
}
|
||||
|
||||
--- Get list of copilot model names (for completion)
|
||||
---@return string[]
|
||||
function M.get_copilot_model_names()
|
||||
local names = {}
|
||||
for _, model in ipairs(M.copilot_models) do
|
||||
table.insert(names, model.name)
|
||||
end
|
||||
return names
|
||||
local names = {}
|
||||
for _, model in ipairs(M.copilot_models) do
|
||||
table.insert(names, model.name)
|
||||
end
|
||||
return names
|
||||
end
|
||||
|
||||
--- Get cost for a copilot model
|
||||
---@param model_name string
|
||||
---@return string|nil
|
||||
function M.get_copilot_model_cost(model_name)
|
||||
for _, model in ipairs(M.copilot_models) do
|
||||
if model.name == model_name then
|
||||
return model.cost
|
||||
end
|
||||
end
|
||||
return nil
|
||||
for _, model in ipairs(M.copilot_models) do
|
||||
if model.name == model_name then
|
||||
return model.cost
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Interactive command to add/update configuration
|
||||
function M.interactive_add()
|
||||
local providers = { "copilot", "ollama" }
|
||||
local providers = { "copilot", "ollama" }
|
||||
|
||||
vim.ui.select(providers, {
|
||||
prompt = "Select LLM provider:",
|
||||
format_item = function(item)
|
||||
local display = item:sub(1, 1):upper() .. item:sub(2)
|
||||
local creds = M.load()
|
||||
local configured = creds.providers and creds.providers[item]
|
||||
if configured and (configured.configured or item == "ollama") then
|
||||
return display .. " [configured]"
|
||||
end
|
||||
return display
|
||||
end,
|
||||
}, function(provider)
|
||||
if not provider then
|
||||
return
|
||||
end
|
||||
vim.ui.select(providers, {
|
||||
prompt = "Select LLM provider:",
|
||||
format_item = function(item)
|
||||
local display = item:sub(1, 1):upper() .. item:sub(2)
|
||||
local creds = M.load()
|
||||
local configured = creds.providers and creds.providers[item]
|
||||
if configured and (configured.configured or item == "ollama") then
|
||||
return display .. " [configured]"
|
||||
end
|
||||
return display
|
||||
end,
|
||||
}, function(provider)
|
||||
if not provider then
|
||||
return
|
||||
end
|
||||
|
||||
if provider == "ollama" then
|
||||
M.interactive_ollama_config()
|
||||
elseif provider == "copilot" then
|
||||
M.interactive_copilot_config()
|
||||
end
|
||||
end)
|
||||
if provider == "ollama" then
|
||||
M.interactive_ollama_config()
|
||||
elseif provider == "copilot" then
|
||||
M.interactive_copilot_config()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Interactive Copilot configuration (no API key, uses OAuth)
|
||||
---@param silent? boolean If true, don't show the OAuth info message
|
||||
function M.interactive_copilot_config(silent)
|
||||
if not silent then
|
||||
utils.notify("Copilot uses OAuth from copilot.lua/copilot.vim - no API key needed", vim.log.levels.INFO)
|
||||
end
|
||||
if not silent then
|
||||
utils.notify("Copilot uses OAuth from copilot.lua/copilot.vim - no API key needed", vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
-- Get current model if configured
|
||||
local current_model = M.get_model("copilot") or M.default_models.copilot
|
||||
local current_cost = M.get_copilot_model_cost(current_model) or "?"
|
||||
-- Get current model if configured
|
||||
local current_model = M.get_model("copilot") or M.default_models.copilot
|
||||
local current_cost = M.get_copilot_model_cost(current_model) or "?"
|
||||
|
||||
-- Build model options with "Custom..." option
|
||||
local model_options = vim.deepcopy(M.copilot_models)
|
||||
table.insert(model_options, { name = "Custom...", cost = "" })
|
||||
-- Build model options with "Custom..." option
|
||||
local model_options = vim.deepcopy(M.copilot_models)
|
||||
table.insert(model_options, { name = "Custom...", cost = "" })
|
||||
|
||||
vim.ui.select(model_options, {
|
||||
prompt = "Select Copilot model (current: " .. current_model .. " — " .. current_cost .. "):",
|
||||
format_item = function(item)
|
||||
local display = item.name
|
||||
if item.cost and item.cost ~= "" then
|
||||
display = display .. " — " .. item.cost
|
||||
end
|
||||
if item.name == current_model then
|
||||
display = display .. " [current]"
|
||||
end
|
||||
return display
|
||||
end,
|
||||
}, function(choice)
|
||||
if choice == nil then
|
||||
return -- Cancelled
|
||||
end
|
||||
vim.ui.select(model_options, {
|
||||
prompt = "Select Copilot model (current: " .. current_model .. " — " .. current_cost .. "):",
|
||||
format_item = function(item)
|
||||
local display = item.name
|
||||
if item.cost and item.cost ~= "" then
|
||||
display = display .. " — " .. item.cost
|
||||
end
|
||||
if item.name == current_model then
|
||||
display = display .. " [current]"
|
||||
end
|
||||
return display
|
||||
end,
|
||||
}, function(choice)
|
||||
if choice == nil then
|
||||
return -- Cancelled
|
||||
end
|
||||
|
||||
if choice.name == "Custom..." then
|
||||
-- Allow custom model input
|
||||
vim.ui.input({
|
||||
prompt = "Enter custom model name: ",
|
||||
default = current_model,
|
||||
}, function(custom_model)
|
||||
if custom_model and custom_model ~= "" then
|
||||
M.save_and_notify("copilot", {
|
||||
model = custom_model,
|
||||
configured = true,
|
||||
})
|
||||
end
|
||||
end)
|
||||
else
|
||||
M.save_and_notify("copilot", {
|
||||
model = choice.name,
|
||||
configured = true,
|
||||
})
|
||||
end
|
||||
end)
|
||||
if choice.name == "Custom..." then
|
||||
-- Allow custom model input
|
||||
vim.ui.input({
|
||||
prompt = "Enter custom model name: ",
|
||||
default = current_model,
|
||||
}, function(custom_model)
|
||||
if custom_model and custom_model ~= "" then
|
||||
M.save_and_notify("copilot", {
|
||||
model = custom_model,
|
||||
configured = true,
|
||||
})
|
||||
end
|
||||
end)
|
||||
else
|
||||
M.save_and_notify("copilot", {
|
||||
model = choice.name,
|
||||
configured = true,
|
||||
})
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Interactive Ollama configuration
|
||||
function M.interactive_ollama_config()
|
||||
vim.ui.input({
|
||||
prompt = "Ollama host (default: http://localhost:11434): ",
|
||||
default = "http://localhost:11434",
|
||||
}, function(host)
|
||||
if host == nil then
|
||||
return -- Cancelled
|
||||
end
|
||||
vim.ui.input({
|
||||
prompt = "Ollama host (default: http://localhost:11434): ",
|
||||
default = "http://localhost:11434",
|
||||
}, function(host)
|
||||
if host == nil then
|
||||
return -- Cancelled
|
||||
end
|
||||
|
||||
if host == "" then
|
||||
host = "http://localhost:11434"
|
||||
end
|
||||
if host == "" then
|
||||
host = "http://localhost:11434"
|
||||
end
|
||||
|
||||
-- Get model
|
||||
local default_model = M.default_models.ollama
|
||||
vim.ui.input({
|
||||
prompt = string.format("Ollama model (default: %s): ", default_model),
|
||||
default = default_model,
|
||||
}, function(model)
|
||||
if model == nil then
|
||||
return -- Cancelled
|
||||
end
|
||||
-- Get model
|
||||
local default_model = M.default_models.ollama
|
||||
vim.ui.input({
|
||||
prompt = string.format("Ollama model (default: %s): ", default_model),
|
||||
default = default_model,
|
||||
}, function(model)
|
||||
if model == nil then
|
||||
return -- Cancelled
|
||||
end
|
||||
|
||||
if model == "" then
|
||||
model = default_model
|
||||
end
|
||||
if model == "" then
|
||||
model = default_model
|
||||
end
|
||||
|
||||
M.save_and_notify("ollama", {
|
||||
host = host,
|
||||
model = model,
|
||||
})
|
||||
end)
|
||||
end)
|
||||
M.save_and_notify("ollama", {
|
||||
host = host,
|
||||
model = model,
|
||||
})
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Save credentials and notify user
|
||||
---@param provider string Provider name
|
||||
---@param credentials table Credentials to save
|
||||
function M.save_and_notify(provider, credentials)
|
||||
if M.set_credentials(provider, credentials) then
|
||||
local msg = string.format("Saved %s configuration", provider:upper())
|
||||
if credentials.model then
|
||||
msg = msg .. " (model: " .. credentials.model .. ")"
|
||||
end
|
||||
utils.notify(msg, vim.log.levels.INFO)
|
||||
else
|
||||
utils.notify("Failed to save credentials", vim.log.levels.ERROR)
|
||||
end
|
||||
if M.set_credentials(provider, credentials) then
|
||||
local msg = string.format("Saved %s configuration", provider:upper())
|
||||
if credentials.model then
|
||||
msg = msg .. " (model: " .. credentials.model .. ")"
|
||||
end
|
||||
utils.notify(msg, vim.log.levels.INFO)
|
||||
else
|
||||
utils.notify("Failed to save credentials", vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
--- Show current credentials status
|
||||
function M.show_status()
|
||||
local providers = M.list_providers()
|
||||
local providers = M.list_providers()
|
||||
|
||||
-- Get current active provider
|
||||
local codetyper = require("codetyper")
|
||||
local current = codetyper.get_config().llm.provider
|
||||
-- Get current active provider
|
||||
local codetyper = require("codetyper")
|
||||
local current = codetyper.get_config().llm.provider
|
||||
|
||||
local lines = {
|
||||
"Codetyper Credentials Status",
|
||||
"============================",
|
||||
"",
|
||||
"Storage: " .. get_credentials_path(),
|
||||
"Active: " .. current:upper(),
|
||||
"",
|
||||
}
|
||||
local lines = {
|
||||
"Codetyper Credentials Status",
|
||||
"============================",
|
||||
"",
|
||||
"Storage: " .. get_credentials_path(),
|
||||
"Active: " .. current:upper(),
|
||||
"",
|
||||
}
|
||||
|
||||
for _, p in ipairs(providers) do
|
||||
local status_icon = p.configured and "✓" or "✗"
|
||||
local active_marker = p.name == current and " [ACTIVE]" or ""
|
||||
local source_info = ""
|
||||
if p.configured then
|
||||
source_info = p.source == "stored" and " (stored)" or " (config)"
|
||||
end
|
||||
local model_info = p.model and (" - " .. p.model) or ""
|
||||
for _, p in ipairs(providers) do
|
||||
local status_icon = p.configured and "✓" or "✗"
|
||||
local active_marker = p.name == current and " [ACTIVE]" or ""
|
||||
local source_info = ""
|
||||
if p.configured then
|
||||
source_info = p.source == "stored" and " (stored)" or " (config)"
|
||||
end
|
||||
local model_info = p.model and (" - " .. p.model) or ""
|
||||
|
||||
table.insert(
|
||||
lines,
|
||||
string.format(" %s %s%s%s%s", status_icon, p.name:upper(), active_marker, source_info, model_info)
|
||||
)
|
||||
end
|
||||
table.insert(
|
||||
lines,
|
||||
string.format(" %s %s%s%s%s", status_icon, p.name:upper(), active_marker, source_info, model_info)
|
||||
)
|
||||
end
|
||||
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "Commands:")
|
||||
table.insert(lines, " :CoderAddApiKey - Add/update credentials")
|
||||
table.insert(lines, " :CoderSwitchProvider - Switch active provider")
|
||||
table.insert(lines, " :CoderRemoveApiKey - Remove stored credentials")
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "Commands:")
|
||||
table.insert(lines, " :CoderAddApiKey - Add/update credentials")
|
||||
table.insert(lines, " :CoderSwitchProvider - Switch active provider")
|
||||
table.insert(lines, " :CoderRemoveApiKey - Remove stored credentials")
|
||||
|
||||
utils.notify(table.concat(lines, "\n"))
|
||||
utils.notify(table.concat(lines, "\n"))
|
||||
end
|
||||
|
||||
--- Interactive remove credentials
|
||||
function M.interactive_remove()
|
||||
local data = M.load()
|
||||
local configured = {}
|
||||
local data = M.load()
|
||||
local configured = {}
|
||||
|
||||
for provider, _ in pairs(data.providers or {}) do
|
||||
table.insert(configured, provider)
|
||||
end
|
||||
for provider, _ in pairs(data.providers or {}) do
|
||||
table.insert(configured, provider)
|
||||
end
|
||||
|
||||
if #configured == 0 then
|
||||
utils.notify("No credentials configured", vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
if #configured == 0 then
|
||||
utils.notify("No credentials configured", vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
vim.ui.select(configured, {
|
||||
prompt = "Select provider to remove:",
|
||||
}, function(provider)
|
||||
if not provider then
|
||||
return
|
||||
end
|
||||
vim.ui.select(configured, {
|
||||
prompt = "Select provider to remove:",
|
||||
}, function(provider)
|
||||
if not provider then
|
||||
return
|
||||
end
|
||||
|
||||
vim.ui.select({ "Yes", "No" }, {
|
||||
prompt = "Remove " .. provider:upper() .. " credentials?",
|
||||
}, function(choice)
|
||||
if choice == "Yes" then
|
||||
if M.remove_credentials(provider) then
|
||||
utils.notify("Removed " .. provider:upper() .. " credentials", vim.log.levels.INFO)
|
||||
else
|
||||
utils.notify("Failed to remove credentials", vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
vim.ui.select({ "Yes", "No" }, {
|
||||
prompt = "Remove " .. provider:upper() .. " credentials?",
|
||||
}, function(choice)
|
||||
if choice == "Yes" then
|
||||
if M.remove_credentials(provider) then
|
||||
utils.notify("Removed " .. provider:upper() .. " credentials", vim.log.levels.INFO)
|
||||
else
|
||||
utils.notify("Failed to remove credentials", vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Set the active provider
|
||||
---@param provider string Provider name
|
||||
function M.set_active_provider(provider)
|
||||
local data = M.load()
|
||||
data.active_provider = provider
|
||||
data.updated = os.time()
|
||||
M.save(data)
|
||||
local data = M.load()
|
||||
data.active_provider = provider
|
||||
data.updated = os.time()
|
||||
M.save(data)
|
||||
|
||||
-- Also update the runtime config
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
config.llm.provider = provider
|
||||
-- Also update the runtime config
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
config.llm.provider = provider
|
||||
|
||||
utils.notify("Active provider set to: " .. provider:upper(), vim.log.levels.INFO)
|
||||
utils.notify("Active provider set to: " .. provider:upper(), vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
--- Get the active provider from stored config
|
||||
---@return string|nil Active provider
|
||||
function M.get_active_provider()
|
||||
local data = M.load()
|
||||
return data.active_provider
|
||||
local data = M.load()
|
||||
return data.active_provider
|
||||
end
|
||||
|
||||
--- Check if a provider is configured (from stored credentials OR config)
|
||||
---@param provider string Provider name
|
||||
---@return boolean configured, string|nil source
|
||||
local function is_provider_configured(provider)
|
||||
local data = M.load()
|
||||
local stored = data.providers and data.providers[provider]
|
||||
if stored then
|
||||
if stored.configured or provider == "ollama" or provider == "copilot" then
|
||||
return true, "stored"
|
||||
end
|
||||
end
|
||||
local data = M.load()
|
||||
local stored = data.providers and data.providers[provider]
|
||||
if stored then
|
||||
if stored.configured or provider == "ollama" or provider == "copilot" then
|
||||
return true, "stored"
|
||||
end
|
||||
end
|
||||
|
||||
local ok, codetyper = pcall(require, "codetyper")
|
||||
if not ok then
|
||||
return false, nil
|
||||
end
|
||||
local ok, codetyper = pcall(require, "codetyper")
|
||||
if not ok then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
local config = codetyper.get_config()
|
||||
if not config or not config.llm then
|
||||
return false, nil
|
||||
end
|
||||
local config = codetyper.get_config()
|
||||
if not config or not config.llm then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
local provider_config = config.llm[provider]
|
||||
if not provider_config then
|
||||
return false, nil
|
||||
end
|
||||
local provider_config = config.llm[provider]
|
||||
if not provider_config then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
if provider == "copilot" then
|
||||
return true, "config"
|
||||
elseif provider == "ollama" then
|
||||
if provider_config.host then
|
||||
return true, "config"
|
||||
end
|
||||
end
|
||||
if provider == "copilot" then
|
||||
return true, "config"
|
||||
elseif provider == "ollama" then
|
||||
if provider_config.host then
|
||||
return true, "config"
|
||||
end
|
||||
end
|
||||
|
||||
return false, nil
|
||||
return false, nil
|
||||
end
|
||||
|
||||
--- Interactive switch provider
|
||||
function M.interactive_switch_provider()
|
||||
local all_providers = { "copilot", "ollama" }
|
||||
local available = {}
|
||||
local sources = {}
|
||||
local all_providers = { "copilot", "ollama" }
|
||||
local available = {}
|
||||
local sources = {}
|
||||
|
||||
for _, provider in ipairs(all_providers) do
|
||||
local configured, source = is_provider_configured(provider)
|
||||
if configured then
|
||||
table.insert(available, provider)
|
||||
sources[provider] = source
|
||||
end
|
||||
end
|
||||
for _, provider in ipairs(all_providers) do
|
||||
local configured, source = is_provider_configured(provider)
|
||||
if configured then
|
||||
table.insert(available, provider)
|
||||
sources[provider] = source
|
||||
end
|
||||
end
|
||||
|
||||
if #available == 0 then
|
||||
utils.notify("No providers configured. Use :CoderAddApiKey or add to your config.", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
if #available == 0 then
|
||||
utils.notify("No providers configured. Use :CoderAddApiKey or add to your config.", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local codetyper = require("codetyper")
|
||||
local current = codetyper.get_config().llm.provider
|
||||
local codetyper = require("codetyper")
|
||||
local current = codetyper.get_config().llm.provider
|
||||
|
||||
vim.ui.select(available, {
|
||||
prompt = "Select provider (current: " .. current .. "):",
|
||||
format_item = function(item)
|
||||
local marker = item == current and " [active]" or ""
|
||||
local source_marker = sources[item] == "stored" and " (stored)" or " (config)"
|
||||
return item:upper() .. marker .. source_marker
|
||||
end,
|
||||
}, function(provider)
|
||||
if provider then
|
||||
M.set_active_provider(provider)
|
||||
end
|
||||
end)
|
||||
vim.ui.select(available, {
|
||||
prompt = "Select provider (current: " .. current .. "):",
|
||||
format_item = function(item)
|
||||
local marker = item == current and " [active]" or ""
|
||||
local source_marker = sources[item] == "stored" and " (stored)" or " (config)"
|
||||
return item:upper() .. marker .. source_marker
|
||||
end,
|
||||
}, function(provider)
|
||||
if provider then
|
||||
M.set_active_provider(provider)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -4,48 +4,48 @@ local M = {}
|
||||
|
||||
---@type CoderConfig
|
||||
local defaults = {
|
||||
llm = {
|
||||
provider = "ollama", -- Options: "ollama", "copilot"
|
||||
ollama = {
|
||||
host = "http://localhost:11434",
|
||||
model = "deepseek-coder:6.7b",
|
||||
},
|
||||
copilot = {
|
||||
model = "claude-sonnet-4", -- Uses GitHub Copilot authentication
|
||||
},
|
||||
},
|
||||
auto_gitignore = true,
|
||||
auto_index = false, -- Auto-create coder companion files on file open
|
||||
indexer = {
|
||||
enabled = true, -- Enable project indexing
|
||||
auto_index = true, -- Index files on save
|
||||
index_on_open = false, -- Index project when opening
|
||||
max_file_size = 100000, -- Skip files larger than 100KB
|
||||
excluded_dirs = { "node_modules", "dist", "build", ".git", ".codetyper", "__pycache__", "vendor", "target" },
|
||||
index_extensions = { "lua", "ts", "tsx", "js", "jsx", "py", "go", "rs", "rb", "java", "c", "cpp", "h", "hpp" },
|
||||
memory = {
|
||||
enabled = true, -- Enable memory persistence
|
||||
max_memories = 1000, -- Maximum stored memories
|
||||
prune_threshold = 0.1, -- Remove low-weight memories
|
||||
},
|
||||
},
|
||||
brain = {
|
||||
enabled = true, -- Enable brain learning system
|
||||
auto_learn = true, -- Auto-learn from events
|
||||
auto_commit = true, -- Auto-commit after threshold
|
||||
commit_threshold = 10, -- Changes before auto-commit
|
||||
max_nodes = 5000, -- Maximum nodes before pruning
|
||||
max_deltas = 500, -- Maximum delta history
|
||||
prune = {
|
||||
enabled = true, -- Enable auto-pruning
|
||||
threshold = 0.1, -- Remove nodes below this weight
|
||||
unused_days = 90, -- Remove unused nodes after N days
|
||||
},
|
||||
output = {
|
||||
max_tokens = 4000, -- Token budget for LLM context
|
||||
format = "compact", -- "compact"|"json"|"natural"
|
||||
},
|
||||
},
|
||||
llm = {
|
||||
provider = "ollama", -- Options: "ollama", "copilot"
|
||||
ollama = {
|
||||
host = "http://localhost:11434",
|
||||
model = "deepseek-coder:6.7b",
|
||||
},
|
||||
copilot = {
|
||||
model = "claude-sonnet-4", -- Uses GitHub Copilot authentication
|
||||
},
|
||||
},
|
||||
auto_gitignore = true,
|
||||
auto_index = false, -- Auto-create coder companion files on file open
|
||||
indexer = {
|
||||
enabled = true, -- Enable project indexing
|
||||
auto_index = true, -- Index files on save
|
||||
index_on_open = false, -- Index project when opening
|
||||
max_file_size = 100000, -- Skip files larger than 100KB
|
||||
excluded_dirs = { "node_modules", "dist", "build", ".git", ".codetyper", "__pycache__", "vendor", "target" },
|
||||
index_extensions = { "lua", "ts", "tsx", "js", "jsx", "py", "go", "rs", "rb", "java", "c", "cpp", "h", "hpp" },
|
||||
memory = {
|
||||
enabled = true, -- Enable memory persistence
|
||||
max_memories = 1000, -- Maximum stored memories
|
||||
prune_threshold = 0.1, -- Remove low-weight memories
|
||||
},
|
||||
},
|
||||
brain = {
|
||||
enabled = true, -- Enable brain learning system
|
||||
auto_learn = true, -- Auto-learn from events
|
||||
auto_commit = true, -- Auto-commit after threshold
|
||||
commit_threshold = 10, -- Changes before auto-commit
|
||||
max_nodes = 5000, -- Maximum nodes before pruning
|
||||
max_deltas = 500, -- Maximum delta history
|
||||
prune = {
|
||||
enabled = true, -- Enable auto-pruning
|
||||
threshold = 0.1, -- Remove nodes below this weight
|
||||
unused_days = 90, -- Remove unused nodes after N days
|
||||
},
|
||||
output = {
|
||||
max_tokens = 4000, -- Token budget for LLM context
|
||||
format = "compact", -- "compact"|"json"|"natural"
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
--- Deep merge two tables
|
||||
@@ -53,53 +53,53 @@ local defaults = {
|
||||
---@param t2 table Table to merge into base
|
||||
---@return table Merged table
|
||||
local function deep_merge(t1, t2)
|
||||
local result = vim.deepcopy(t1)
|
||||
for k, v in pairs(t2) do
|
||||
if type(v) == "table" and type(result[k]) == "table" then
|
||||
result[k] = deep_merge(result[k], v)
|
||||
else
|
||||
result[k] = v
|
||||
end
|
||||
end
|
||||
return result
|
||||
local result = vim.deepcopy(t1)
|
||||
for k, v in pairs(t2) do
|
||||
if type(v) == "table" and type(result[k]) == "table" then
|
||||
result[k] = deep_merge(result[k], v)
|
||||
else
|
||||
result[k] = v
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
--- Setup configuration with user options
|
||||
---@param opts? CoderConfig User configuration options
|
||||
---@return CoderConfig Final configuration
|
||||
function M.setup(opts)
|
||||
opts = opts or {}
|
||||
return deep_merge(defaults, opts)
|
||||
opts = opts or {}
|
||||
return deep_merge(defaults, opts)
|
||||
end
|
||||
|
||||
--- Get default configuration
|
||||
---@return CoderConfig Default configuration
|
||||
function M.get_defaults()
|
||||
return vim.deepcopy(defaults)
|
||||
return vim.deepcopy(defaults)
|
||||
end
|
||||
|
||||
--- Validate configuration
|
||||
---@param config CoderConfig Configuration to validate
|
||||
---@return boolean, string? Valid status and optional error message
|
||||
function M.validate(config)
|
||||
if not config.llm then
|
||||
return false, "Missing LLM configuration"
|
||||
end
|
||||
if not config.llm then
|
||||
return false, "Missing LLM configuration"
|
||||
end
|
||||
|
||||
local valid_providers = { "ollama", "copilot" }
|
||||
local is_valid_provider = false
|
||||
for _, p in ipairs(valid_providers) do
|
||||
if config.llm.provider == p then
|
||||
is_valid_provider = true
|
||||
break
|
||||
end
|
||||
end
|
||||
local valid_providers = { "ollama", "copilot" }
|
||||
local is_valid_provider = false
|
||||
for _, p in ipairs(valid_providers) do
|
||||
if config.llm.provider == p then
|
||||
is_valid_provider = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not is_valid_provider then
|
||||
return false, "Invalid LLM provider. Must be one of: " .. table.concat(valid_providers, ", ")
|
||||
end
|
||||
if not is_valid_provider then
|
||||
return false, "Invalid LLM provider. Must be one of: " .. table.concat(valid_providers, ", ")
|
||||
end
|
||||
|
||||
return true
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -12,8 +12,8 @@ local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Default preferences
|
||||
local defaults = {
|
||||
auto_process = nil, -- nil means "not yet decided"
|
||||
asked_auto_process = false,
|
||||
auto_process = nil, -- nil means "not yet decided"
|
||||
asked_auto_process = false,
|
||||
}
|
||||
|
||||
--- Cached preferences per project
|
||||
@@ -23,113 +23,113 @@ local cache = {}
|
||||
--- Get the preferences file path for current project
|
||||
---@return string
|
||||
local function get_preferences_path()
|
||||
local cwd = vim.fn.getcwd()
|
||||
return cwd .. "/.codetyper/preferences.json"
|
||||
local cwd = vim.fn.getcwd()
|
||||
return cwd .. "/.codetyper/preferences.json"
|
||||
end
|
||||
|
||||
--- Ensure .codetyper directory exists
|
||||
local function ensure_coder_dir()
|
||||
local cwd = vim.fn.getcwd()
|
||||
local coder_dir = cwd .. "/.codetyper"
|
||||
if vim.fn.isdirectory(coder_dir) == 0 then
|
||||
vim.fn.mkdir(coder_dir, "p")
|
||||
end
|
||||
local cwd = vim.fn.getcwd()
|
||||
local coder_dir = cwd .. "/.codetyper"
|
||||
if vim.fn.isdirectory(coder_dir) == 0 then
|
||||
vim.fn.mkdir(coder_dir, "p")
|
||||
end
|
||||
end
|
||||
|
||||
--- Load preferences from file
|
||||
---@return CoderPreferences
|
||||
function M.load()
|
||||
local cwd = vim.fn.getcwd()
|
||||
local cwd = vim.fn.getcwd()
|
||||
|
||||
-- Check cache first
|
||||
if cache[cwd] then
|
||||
return cache[cwd]
|
||||
end
|
||||
-- Check cache first
|
||||
if cache[cwd] then
|
||||
return cache[cwd]
|
||||
end
|
||||
|
||||
local path = get_preferences_path()
|
||||
local prefs = vim.deepcopy(defaults)
|
||||
local path = get_preferences_path()
|
||||
local prefs = vim.deepcopy(defaults)
|
||||
|
||||
if utils.file_exists(path) then
|
||||
local content = utils.read_file(path)
|
||||
if content then
|
||||
local ok, decoded = pcall(vim.json.decode, content)
|
||||
if ok and decoded then
|
||||
-- Merge with defaults
|
||||
for k, v in pairs(decoded) do
|
||||
prefs[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if utils.file_exists(path) then
|
||||
local content = utils.read_file(path)
|
||||
if content then
|
||||
local ok, decoded = pcall(vim.json.decode, content)
|
||||
if ok and decoded then
|
||||
-- Merge with defaults
|
||||
for k, v in pairs(decoded) do
|
||||
prefs[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Cache it
|
||||
cache[cwd] = prefs
|
||||
return prefs
|
||||
-- Cache it
|
||||
cache[cwd] = prefs
|
||||
return prefs
|
||||
end
|
||||
|
||||
--- Save preferences to file
|
||||
---@param prefs CoderPreferences
|
||||
function M.save(prefs)
|
||||
local cwd = vim.fn.getcwd()
|
||||
ensure_coder_dir()
|
||||
local cwd = vim.fn.getcwd()
|
||||
ensure_coder_dir()
|
||||
|
||||
local path = get_preferences_path()
|
||||
local ok, encoded = pcall(vim.json.encode, prefs)
|
||||
if ok then
|
||||
utils.write_file(path, encoded)
|
||||
-- Update cache
|
||||
cache[cwd] = prefs
|
||||
end
|
||||
local path = get_preferences_path()
|
||||
local ok, encoded = pcall(vim.json.encode, prefs)
|
||||
if ok then
|
||||
utils.write_file(path, encoded)
|
||||
-- Update cache
|
||||
cache[cwd] = prefs
|
||||
end
|
||||
end
|
||||
|
||||
--- Get a specific preference
|
||||
---@param key string
|
||||
---@return any
|
||||
function M.get(key)
|
||||
local prefs = M.load()
|
||||
return prefs[key]
|
||||
local prefs = M.load()
|
||||
return prefs[key]
|
||||
end
|
||||
|
||||
--- Set a specific preference
|
||||
---@param key string
|
||||
---@param value any
|
||||
function M.set(key, value)
|
||||
local prefs = M.load()
|
||||
prefs[key] = value
|
||||
M.save(prefs)
|
||||
local prefs = M.load()
|
||||
prefs[key] = value
|
||||
M.save(prefs)
|
||||
end
|
||||
|
||||
--- Check if auto-process is enabled
|
||||
---@return boolean|nil Returns true/false if set, nil if not yet decided
|
||||
function M.is_auto_process_enabled()
|
||||
return M.get("auto_process")
|
||||
return M.get("auto_process")
|
||||
end
|
||||
|
||||
--- Set auto-process preference
|
||||
---@param enabled boolean
|
||||
function M.set_auto_process(enabled)
|
||||
M.set("auto_process", enabled)
|
||||
M.set("asked_auto_process", true)
|
||||
M.set("auto_process", enabled)
|
||||
M.set("asked_auto_process", true)
|
||||
end
|
||||
|
||||
--- Check if we've already asked the user about auto-process
|
||||
---@return boolean
|
||||
function M.has_asked_auto_process()
|
||||
return M.get("asked_auto_process") == true
|
||||
return M.get("asked_auto_process") == true
|
||||
end
|
||||
|
||||
--- Clear cached preferences (useful when changing projects)
|
||||
function M.clear_cache()
|
||||
cache = {}
|
||||
cache = {}
|
||||
end
|
||||
|
||||
--- Toggle auto-process mode
|
||||
function M.toggle_auto_process()
|
||||
local current = M.is_auto_process_enabled()
|
||||
local new_value = not current
|
||||
M.set_auto_process(new_value)
|
||||
local mode = new_value and "automatic" or "manual"
|
||||
vim.notify("Codetyper: Switched to " .. mode .. " mode", vim.log.levels.INFO)
|
||||
local current = M.is_auto_process_enabled()
|
||||
local new_value = not current
|
||||
M.set_auto_process(new_value)
|
||||
local mode = new_value and "automatic" or "manual"
|
||||
vim.notify("Codetyper: Switched to " .. mode .. " mode", vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user