Files
community-plugins/llamanager/registry.luau
T
Marc Tristan VictoriaandGitHub f228731141 Add llamanager plugin (#171)
* feat: add Llamanager plugin

* fix image paths in README

* docs: fix validation issues

* fix: handle missing trailing separator in modelfile directory

* fix: escape user input in shell commands

* fix: disable modelfile editor when path is unset or misconfigured

* fix: handle invalid Modelfile directory

* docs: update README

* fix: reload launcher model from config

* docs: update demo assets
2026-08-01 08:58:54 -04:00

353 lines
10 KiB
Luau

local modelfilePath = noctalia.expandPath(noctalia.getConfig("modelfile_path"))
if modelfilePath:sub(-1) ~= "/" then
modelfilePath = modelfilePath .. "/"
end
local function joinModelNameToPath(modelName)
return modelfilePath .. modelName .. ".modelfile"
end
local function isNotEmpty(value)
if not value or value:match("^%s*$") then
noctalia.notify("LLamanager", "Field cannot be empty.")
return false
end
return true
end
local function shellEscape(str)
return "'" .. tostring(str):gsub("'", "'\\''") .. "'"
end
-- return ollama table
local function loadOllama()
local isInstalled = function() return noctalia.commandExists("ollama") end
local ollama = {
server = {
executable = "ollama",
installed = isInstalled(),
endpoint = "localhost:11434",
api_connected = false,
version = nil,
},
models = {}, -- models installed
runtime = {}, -- runtime models and other info
}
return ollama
end
-- models dashboard
local function getModels(onDone)
local models = {}
noctalia.runAsync("ollama list | awk 'NR>1 {print $1}'", function(result)
if result.exitCode ~= 0 then
onDone({})
return
end
for line in string.gmatch(result.stdout, "[^\r\n]+") do
local model_name = line:gsub("%s+$", "")
if model_name ~= "" then
table.insert(models,
{
name = model_name,
executable = "ollama run " .. model_name .. " --think=false",
})
end
end
onDone(models)
end)
end
-- runtime dashboard
local function getOllamaVersion(onVersion)
noctalia.runAsync("ollama --version | awk '{print $4}'", function(result)
if result.exitCode ~= 0 then
noctalia.notify("Failed to get Ollama version")
return
end
local version = result.stdout
onVersion(version)
end)
end
local function launch(ollama)
local selectedModel = tonumber(noctalia.state.get("llamanager.selectedModel"))
local models = ollama.models
if selectedModel ~= 0 then
local model = models[selectedModel]
if not model then
noctalia.notify("Selected model not found")
return
end
noctalia.notify(model.name)
noctalia.runInTerminal(model.executable)
else
noctalia.notify("Ollama")
noctalia.runInTerminal(ollama.server.executable)
end
end
local function getRuntimeInfo(onLoad)
local request = {
url = "http://localhost:11434/api/ps",
method = "GET",
headers = { "Accept: application/json" },
follow_redirects = false,
}
noctalia.http(request, function(response)
if not (response.ok and response.status == 200) then
noctalia.notify("Request failed")
return
end
local api_status = true
-- parse
local data = noctalia.json.decode(response.body)
if not data.models or #data.models == 0 then
onLoad({}, api_status)
return
end
-- update models
local info = {}
for _, model in ipairs(data.models) do
table.insert(info, {
name = model.name,
parameterSize = model.details.parameter_size,
quantization = model.details.quantization_level,
context = model.context_length,
expires = model.expires_at,
})
end
onLoad(info, api_status)
end)
end
local function downloadModel(modelName, onProgress, onFinish)
local request = {
url = "http://localhost:11434/api/pull",
method = "POST",
headers = {
"Content-Type: application/json",
"Accept: application/json",
},
body = noctalia.json.encode({
model = modelName,
stream = true,
}),
}
noctalia.httpStream( request,
-- progress
function(line)
local ok, data = pcall(noctalia.json.decode, line)
if not ok then
return
end
if onProgress then
onProgress(data)
end
end,
-- finished
function(result)
if onFinish then
onFinish(result)
end
end
)
end
local function edit()
local listDir, err = noctalia.listDir(modelfilePath)
if not listDir then
noctalia.notify("Llamanager", err)
return
end
modelfileDirs = {}
for _, dir in ipairs(listDir) do
table.insert(modelfileDirs, dir)
end
noctalia.state.set("llamanager.modelfilePaths", modelfileDirs)
end
local function loadToOllama(modelName, modelfilePath)
local cmd = string.format(
'ollama create %s -f %s',
shellEscape(modelName),
shellEscape(modelfilePath)
)
noctalia.runAsync(cmd, function(result)
if result.exitCode == 0 then
noctalia.notify("Model created!")
else
local err = result.stderr ~= "" and result.stderr or result.stdout
local message = err:match("Error:.-[\r\n]") or err:match("Error:.*") or err
message = message:gsub("[\r\n]", "")
noctalia.notify("Llamanager", message)
end
end)
end
local function deleteModelOllama(modelName)
local cmd = string.format('ollama rm %s', shellEscape(modelName))
noctalia.runAsync(cmd, function(result)
if result.exitCode == 0 then
noctalia.notify("Ollama: Removed ".. modelName)
-- refresh model list
noctalia.state.set("llamanager.nextCommand", "refresh")
else
noctalia.notify("LLamanager", "Modelfile successfully deleted, but no matching ollama model was found.")
noctalia.state.set("llamanager.nextCommand", "refresh")
end
end)
end
noctalia.state.watch("llamanager.nextCommand", function(command)
if command == "refresh" then -- load ollama and its models
local ollama = loadOllama()
getModels(function(models)
ollama.models = models
noctalia.state.set("llamanager.ollama", ollama)
noctalia.state.set("llamanager.view", "modelsView")
end)
-- reset field value
noctalia.state.set("llamanager.modelfileNameField", "")
elseif command == "launch" then
local ollama = noctalia.state.get("llamanager.ollama")
launch(ollama)
elseif command == "runtime" then
local ollama = loadOllama()
getModels(function(models)
ollama.models = models
getRuntimeInfo(function(runtime, status)
ollama.runtime = runtime
ollama.server.api_connected = status
getOllamaVersion(function(version)
ollama.server.version = version
noctalia.state.set(
"llamanager.ollama",
ollama
)
noctalia.state.set(
"llamanager.view",
"runtimeView"
)
end)
end)
end)
elseif command == "download" then
local downloadData = noctalia.state.get("llamanager.downloadModelField")
noctalia.notify("Download Started...")
local lastNotified = -30
local hadError = false
downloadModel(
downloadData.modelName,
function(progress)
if progress.error then
hadError = true
noctalia.notify(progress.error)
return
end
if progress.completed and progress.total then
local percent = math.floor(progress.completed / progress.total * 100)
local milestone = math.floor(percent / 30) * 30
if milestone >= 30 and milestone > lastNotified then
lastNotified = milestone
noctalia.notify("Download Progress: " .. tostring(milestone) .. "%")
end
end
end,
function(result)
if hadError then
return
end
if result.ok then
noctalia.notify("Download complete!")
noctalia.state.set("llamanager.nextCommand", "refresh")
else
noctalia.notify(
"Download failed (" ..
tostring(result.status) ..
")"
)
end
end
)
-- editor
elseif command == "editor" then
edit()
noctalia.state.set("llamanager.view", "editorView")
elseif command == "create" then
local modelName = noctalia.state.get("llamanager.modelfileNameField")
if isNotEmpty(modelName) then
local path = joinModelNameToPath(modelName)
noctalia.writeFile(path, "")
noctalia.state.set("llamanager.view", "modelfileEditorView")
end
elseif command == "edit" then
local modelName = noctalia.state.get("llamanager.modelfileNameField")
if isNotEmpty(modelName) then
local path = joinModelNameToPath(modelName)
local content = noctalia.readFile(path)
noctalia.state.set("llamanager.modelfileContent", content)
noctalia.state.set("llamanager.view", "modelfileEditorView")
end
elseif command == "delete" then
-- note: Include the model tag (e.g. :latest, :v2) on deleting a model.
local modelName = noctalia.state.get("llamanager.modelfileNameField")
if isNotEmpty(modelName) then
local base = modelName:gsub(":.*$", "")
local path = modelfilePath .. base .. ".modelfile"
local ok, err = noctalia.removeFile(path)
if not ok then
noctalia.notify("LLamanager", tostring(err))
else
-- delete in ollama
deleteModelOllama(modelName)
end
end
elseif command == "loadToOllama" then
local modelName = noctalia.state.get("llamanager.modelfileNameField")
if isNotEmpty(modelName) then
local path = joinModelNameToPath(modelName)
loadToOllama(modelName, path)
end
end
end)