mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
196 lines
5.5 KiB
Go
196 lines
5.5 KiB
Go
// jean — single-binary LLM server manager + web UI for llama.cpp deployments.
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const Version = "0.1.0"
|
|
|
|
func main() {
|
|
args := os.Args[1:]
|
|
cmd := "help"
|
|
if len(args) > 0 {
|
|
cmd = args[0]
|
|
args = args[1:]
|
|
}
|
|
switch cmd {
|
|
case "start", "stop", "restart", "status", "enable", "disable":
|
|
mustExit(serviceAction(cmd))
|
|
case "logs":
|
|
mustExit(serviceLogs())
|
|
case "edit":
|
|
mustExit(editConfig())
|
|
case "set-api-key":
|
|
mustExit(cmdSetAPIKey(args))
|
|
case "vram":
|
|
mustExit(showVram())
|
|
case "switch":
|
|
mustExit(cmdSwitch(args))
|
|
case "chat":
|
|
mustExit(cmdChat(args))
|
|
case "web":
|
|
mustExit(cmdWeb(args))
|
|
case "skills":
|
|
mustExit(cmdSkills(args))
|
|
case "tools":
|
|
mustExit(cmdTools(args))
|
|
case "serve":
|
|
mustExit(cmdServe(args))
|
|
case "test":
|
|
mustExit(cmdTest(args))
|
|
case "bench":
|
|
mustExit(cmdBench(args))
|
|
case "llamacpp", "llama":
|
|
mustExit(cmdLlamacpp(args))
|
|
case "install":
|
|
mustExit(cmdInstall(args))
|
|
case "uninstall":
|
|
mustExit(cmdUninstall(args))
|
|
case "version", "-v", "--version":
|
|
fmt.Println("jean", Version)
|
|
case "help", "-h", "--help", "":
|
|
printHelp()
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "commande inconnue: %s\n\n", cmd)
|
|
printHelp()
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
func printHelp() {
|
|
fmt.Printf(`jean %s — manager llama.cpp + UI web (single binary)
|
|
|
|
Usage: jean <commande> [args]
|
|
|
|
Service:
|
|
start | stop | restart gérer le service systemd
|
|
status | logs état / logs en direct
|
|
enable | disable auto-démarrage au boot
|
|
edit éditer $JEAN_HOME/config.env
|
|
set-api-key [clé] protéger l'API (clé Bearer); vide = générer, "" = retirer
|
|
vram utilisation GPU/VRAM (nvidia-smi)
|
|
test vérifie que l'IA répond (health + completion)
|
|
bench [N] mesure prefill + decode tok/s (prompt 2000 tok, N=200 par défaut)
|
|
|
|
Presets:
|
|
switch [N] choisir un preset dans configs/ (interactif ou par numéro)
|
|
|
|
Interaction:
|
|
chat [system-prompt] chat terminal streamé
|
|
web [PORT] UI web (défaut :8090) — chat + presets + skills + tools
|
|
|
|
LLM-side outils:
|
|
skills [on|off|list] active la lecture de SKILLS/<nom>/SKILL.md par l'IA
|
|
tools [on|off|status] active run_shell (l'IA exécute des commandes bash)
|
|
|
|
Backend llama.cpp :
|
|
llamacpp install clone + compile llama.cpp (détecte CUDA/ROCm/Metal/CPU), pointe BIN dessus
|
|
llamacpp update git pull + recompile le backend existant (arrête/redémarre le service)
|
|
llamacpp status commit courant, backend détecté, retard sur origin
|
|
|
|
Entrypoint (utilisé par jean.service) :
|
|
serve lit config.env et exec le binaire llama-server
|
|
|
|
Installation:
|
|
install installer (systemd unit, sudoers, dossiers)
|
|
uninstall désinstaller
|
|
|
|
Env:
|
|
JEAN_HOME racine (défaut: $HOME/JEAN, ou /home/<USER>/JEAN si root)
|
|
EDITOR éditeur pour 'jean edit' (défaut: nano)
|
|
|
|
Config: $JEAN_HOME/config.env
|
|
`, Version)
|
|
}
|
|
|
|
func mustExit(err error) {
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "[err]", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// DefaultJeanHome is where everything lives unless overridden.
|
|
const DefaultJeanHome = "/etc/jean"
|
|
|
|
// JeanHome resolves the JEAN data directory.
|
|
// Precedence: $JEAN_HOME → /etc/default/jean → DefaultJeanHome.
|
|
func JeanHome() string {
|
|
if h := os.Getenv("JEAN_HOME"); h != "" {
|
|
return h
|
|
}
|
|
if h := readEtcDefault(); h != "" {
|
|
return h
|
|
}
|
|
return DefaultJeanHome
|
|
}
|
|
|
|
// readEtcDefault parses /etc/default/jean for JEAN_HOME=<path>. Quiet on errors.
|
|
func readEtcDefault() string {
|
|
b, err := os.ReadFile("/etc/default/jean")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, line := range strings.Split(string(b), "\n") {
|
|
s := strings.TrimSpace(line)
|
|
if s == "" || strings.HasPrefix(s, "#") {
|
|
continue
|
|
}
|
|
s = strings.TrimPrefix(s, "export ")
|
|
if eq := strings.IndexByte(s, '='); eq > 0 {
|
|
k := strings.TrimSpace(s[:eq])
|
|
v := strings.Trim(strings.TrimSpace(s[eq+1:]), "\"'")
|
|
if k == "JEAN_HOME" {
|
|
return v
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func confPath() string { return filepath.Join(JeanHome(), "config.env") }
|
|
func presetsDir() string { return filepath.Join(JeanHome(), "configs") }
|
|
func skillsDir() string { return filepath.Join(JeanHome(), "SKILLS") }
|
|
func skillsFlag() string { return filepath.Join(skillsDir(), ".enabled") }
|
|
func toolsFlag() string { return filepath.Join(JeanHome(), ".tools_enabled") }
|
|
func apiKeyPath() string { return filepath.Join(JeanHome(), ".api_key") }
|
|
func serviceName() string {
|
|
if n := os.Getenv("JEAN_SERVICE"); n != "" {
|
|
return n
|
|
}
|
|
return "jean"
|
|
}
|
|
|
|
// Color helpers (ANSI). Disabled when stdout is not a TTY.
|
|
var colorOn = isTerminal()
|
|
|
|
func col(code, s string) string {
|
|
if !colorOn {
|
|
return s
|
|
}
|
|
return "\033[" + code + "m" + s + "\033[0m"
|
|
}
|
|
func bold(s string) string { return col("1", s) }
|
|
func cyan(s string) string { return col("1;36", s) }
|
|
func green(s string) string { return col("32", s) }
|
|
func red(s string) string { return col("31", s) }
|
|
func dim(s string) string { return col("2", s) }
|
|
func yellow(s string) string { return col("33", s) }
|
|
func magenta(s string) string { return col("35", s) }
|
|
|
|
// trimSplit splits and drops empty tokens.
|
|
func trimSplit(s, sep string) []string {
|
|
out := []string{}
|
|
for _, p := range strings.Split(s, sep) {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|