mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Trois apports repris d'AJEAN 0.12.9 → 0.13.5, adaptés au fork. MÉMOIRE LONGUE DE LA CONVERSATION (chat_recall.go) Le compactage résumait, donc perdait. Chaque gros bloc du torse est désormais ARCHIVÉ verbatim sous un identifiant court (r7…) dans bbolt AVANT d'être résumé ; le résumé cite l'id, et le modèle le rappelle avec recall(id) ou le retrouve par mots-clés avec recall_search. Le contexte reste plat, l'archive grossit sur disque. En mode code, un fichier lu ou un diff produit tôt dans la session n'est plus perdu au compactage suivant. Au passage : garde-fou anti-résumé-dégénéré, budget de résumé indexé sur la fenêtre, queue ramenée à 20 % (compacter plus large ne coûte plus de perte), et fin de l'épinglage du 1er message user — le modèle répondait à l'ancienne demande au lieu de continuer la tâche en cours. PROJETS (projects.go) Un projet cloisonne une mémoire, ses discussions et ses trackers. La couture est memoryDir(), qui pointe sur le projet actif : tout le code mémoire en hérite sans le savoir. Migration automatique au premier démarrage (memory/*.md → memory/generale/, discussions et tâches orphelines rattachées). Une tâche planifiée vise un projet et l'exécution le force, pour qu'une veille n'écrive pas dans la mémoire du chantier affiché à l'écran. TRACKERS (tracker.go) 3e type de mémoire : les données datées qui s'accumulent. On ne les lit jamais en entier — consultation par niveaux (vue d'ensemble → année → mois → événements), et la dernière valeur de chaque tracker est donnée d'emblée au modèle, qui répond sans appeler l'outil. Aussi : index MEMORY.md tenu par le CODE et injecté en tête de conversation avec la description du projet (le modèle ne peut plus le désynchroniser) ; prompt système rattaché au PRESET et non plus global — stocké en base, pas dans le .env, qui ne saurait pas porter un texte multiligne. Deux correctifs du lot précédent voyagent ici, faute de pouvoir séparer les fichiers : msgText signale la présence d'une image au compactage, et le garde-fou « pensé sans agir » passe à deux relances (nudgeCount/maxNudges). Le paquet UI est regénéré dans le commit suivant, qui touche les mêmes sources. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
268 lines
9.6 KiB
Go
268 lines
9.6 KiB
Go
// loki — single-binary LLM server manager + web UI for llama.cpp deployments.
|
|
// Point d'entrée réel : Main(), appelé par cmd/loki (les métadonnées Windows
|
|
// .syso/go:generate vivent là-bas, dans le dossier du package main).
|
|
package loki
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const Version = "0.12.3"
|
|
|
|
// Main est le vrai main() du binaire (cmd/loki ne fait que l'appeler).
|
|
func Main() {
|
|
// Rattache la console du terminal parent si on est lancé depuis un shell
|
|
// (Windows : binaire GUI). Retourne false au double-clic (aucune console) →
|
|
// on bascule alors sur l'expérience « application ». Hors Windows : toujours
|
|
// true. À faire AVANT toute écriture.
|
|
haveConsole := setupConsole()
|
|
|
|
// Nettoie un éventuel binaire .old laissé par une mise à jour Windows.
|
|
cleanupOldBinary()
|
|
|
|
args := os.Args[1:]
|
|
noArgs := len(args) == 0
|
|
cmd := "help"
|
|
if len(args) > 0 {
|
|
cmd = args[0]
|
|
args = args[1:]
|
|
}
|
|
// Double-clic sur le binaire (aucun argument, aucune console rattachée) → on
|
|
// lance l'expérience « application » (UI web + navigateur + icône tray) plutôt
|
|
// que d'afficher l'aide. Le binaire étant compilé en sous-système GUI, il n'y a
|
|
// AUCUNE console à ce stade (donc plus de fenêtre noire). Lancé depuis un shell,
|
|
// `loki` sans argument garde son comportement d'aide.
|
|
if noArgs && !haveConsole {
|
|
mustExit(cmdApp(args))
|
|
return
|
|
}
|
|
switch cmd {
|
|
case "app":
|
|
mustExit(cmdApp(args))
|
|
case "start", "restart":
|
|
// Un moteur sans BIN ni MODEL démarre, meurt, et systemd le relance en
|
|
// boucle : l'utilisateur ne voyait qu'un « activating » rassurant et un
|
|
// /health muet. On le dit AVANT de lancer le service.
|
|
mustExit(preflightEngine())
|
|
mustExit(serviceAction(cmd))
|
|
case "stop", "status", "enable", "disable":
|
|
mustExit(serviceAction(cmd))
|
|
case "logs":
|
|
mustExit(serviceLogs())
|
|
case "edit":
|
|
mustExit(editConfig())
|
|
case "config":
|
|
mustExit(cmdConfigKV(args))
|
|
case "set-api-key":
|
|
mustExit(cmdSetAPIKey(args))
|
|
case "set-web-key":
|
|
mustExit(cmdSetWebKey(args))
|
|
case "vram":
|
|
mustExit(showVram())
|
|
case "gpu":
|
|
mustExit(cmdGPU(args))
|
|
case "network":
|
|
mustExit(cmdNetwork(args))
|
|
case "switch":
|
|
mustExit(cmdSwitch(args))
|
|
case "chat":
|
|
mustExit(cmdChat(args))
|
|
case "export":
|
|
mustExit(cmdExport(args))
|
|
case "web":
|
|
mustExit(cmdWeb(args))
|
|
case "link":
|
|
mustExit(cmdLink(args))
|
|
case "remote":
|
|
mustExit(cmdRemote(args))
|
|
case "ui":
|
|
mustExit(cmdUI(args))
|
|
case "oai":
|
|
mustExit(cmdOAI(args))
|
|
case "agent":
|
|
mustExit(cmdAgent(args))
|
|
case "internet":
|
|
mustExit(cmdInternet(args))
|
|
case "memory":
|
|
mustExit(cmdMemory(args))
|
|
case "serve":
|
|
mustExit(cmdServe(args))
|
|
case "test":
|
|
mustExit(cmdTest(args))
|
|
case "bench":
|
|
mustExit(cmdBench(args))
|
|
case "llamacpp":
|
|
mustExit(cmdLlamacpp(args))
|
|
case "update":
|
|
mustExit(cmdUpdate(args))
|
|
case "where":
|
|
mustExit(cmdWhere(args))
|
|
case restartArg:
|
|
// Sous-commande interne, absente de l'aide : l'accompagnateur détaché qui
|
|
// attend la fermeture de l'app puis la relance.
|
|
mustExit(cmdRestartAfterUpdate(args))
|
|
case "install":
|
|
mustExit(cmdInstall(args))
|
|
case "uninstall":
|
|
mustExit(cmdUninstall(args))
|
|
case "version", "-v", "--version":
|
|
fmt.Println("loki", 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(`loki %s — manager llama.cpp + UI web (single binary)
|
|
|
|
Usage: loki <commande> [args]
|
|
|
|
Loki tourne en deux services : loki-engine (le modèle) et loki-ui
|
|
(l'interface web, le tunnel d'accès distant et l'endpoint OpenAI).
|
|
|
|
Moteur (loki-engine) :
|
|
start | stop | restart gérer le service
|
|
status | logs état / logs en direct
|
|
enable | disable auto-démarrage au boot
|
|
edit éditer la configuration dans $EDITOR
|
|
switch [N] activer un preset de presets/ (interactif ou par numéro)
|
|
test | bench [N] vérifier que l'IA répond / mesurer prefill + decode tok/s
|
|
vram utilisation GPU/VRAM (nvidia-smi)
|
|
gpu [index…] liste les GPU / choisit le(s)quel(s) utiliser (gpu all = tous)
|
|
set-api-key [clé] protéger llama-server (clé Bearer); vide = générer, "" = retirer
|
|
network [on|off|status] rendre l'endpoint OpenAI joignable depuis le réseau local
|
|
(HOST + règle de pare-feu sous Windows)
|
|
|
|
Interface (loki-ui) :
|
|
ui [start|stop|restart|status] pilote le service d'interface
|
|
web [PORT] sert l'interface au premier plan (défaut :8090) —
|
|
ouvre aussi le tunnel si un jeton est enregistré
|
|
set-web-key [clé] protéger l'API de pilotage; vide = générer, "" = retirer
|
|
|
|
Interaction:
|
|
chat [system-prompt] chat terminal streamé
|
|
export [options] [fichier] exporte la conversation de l'interface web
|
|
(Markdown par défaut, « - » = sortie standard)
|
|
--json --last N --no-reasoning
|
|
--no-tools --no-results
|
|
agent [on|off|status] donne à l'IA ses outils (shell, fichiers, mémoire)
|
|
memory [off|ondemand|always|status] mode mémoire de l'IA
|
|
internet [on|off|status|engine <go|crawl4ai>|url <url>|key <clé>]
|
|
accès web de l'IA (moteur intégré ou serveur Crawl4AI)
|
|
|
|
Accès distant (ajean.link) :
|
|
link <token> enregistre le jeton et ouvre le tunnel
|
|
link code code d'appairage (10 min, usage unique) pour le portail
|
|
link status | logout état du jeton / l'oublier
|
|
|
|
Poste distant (piloter CE PC depuis l'IA d'un serveur Loki) :
|
|
remote install <url> --machine ID --key CLÉ --code CODE [--allow shell,read,write,list]
|
|
appaire ce PC + installe le service (démarrage auto)
|
|
remote connect <url> … idem en avant-plan (pour tester)
|
|
remote status | uninstall | logout
|
|
(commande d'appairage prête à copier : UI web → Postes distants)
|
|
|
|
Backend llama.cpp :
|
|
llamacpp install clone + compile llama.cpp (CUDA/ROCm/Metal/CPU), pointe BIN dessus
|
|
llamacpp update git pull + recompile le backend existant
|
|
llamacpp status commit courant, backend détecté, retard sur origin
|
|
|
|
Installation:
|
|
where affiche l'emplacement du binaire, de la base et des dossiers
|
|
install | uninstall installer / désinstaller
|
|
update [--check] mettre à jour depuis les releases GitHub
|
|
serve entrypoint du service : exec llama-server (usage interne)
|
|
|
|
Env:
|
|
LOKI_HOME racine des données (défaut : /etc/loki, %%ProgramData%%\loki sous Windows)
|
|
EDITOR éditeur pour 'loki edit' (défaut : nano, notepad sous Windows)
|
|
`, Version)
|
|
}
|
|
|
|
func mustExit(err error) {
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "[err]", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// LokiHome resolves the Loki data directory.
|
|
// Précédence : $LOKI_HOME → /etc/default/loki (unix) → défaut plateforme.
|
|
func LokiHome() string {
|
|
if h := os.Getenv("LOKI_HOME"); h != "" {
|
|
return h
|
|
}
|
|
if h := readEtcDefault(); h != "" {
|
|
return h
|
|
}
|
|
return defaultLokiHome()
|
|
}
|
|
|
|
// readEtcDefault parses /etc/default/loki for LOKI_HOME=<path>. Quiet on errors.
|
|
func readEtcDefault() string {
|
|
b, err := os.ReadFile("/etc/default/loki")
|
|
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 k, v, ok := strings.Cut(s, "="); ok && strings.TrimSpace(k) == "LOKI_HOME" {
|
|
return strings.Trim(strings.TrimSpace(v), "\"'")
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Arborescence de $LOKI_HOME. Elle tient en six dossiers, et rien d'autre :
|
|
// tout le reste (config, préférences, conversation, clés, drapeaux) vit dans la
|
|
// base loki.db — voir store.go.
|
|
func backendsDir() string { return filepath.Join(LokiHome(), "backends") }
|
|
func binDir() string { return filepath.Join(LokiHome(), "bin") }
|
|
func presetsDir() string { return filepath.Join(LokiHome(), "presets") }
|
|
|
|
// memoryDir est le dossier mémoire du projet ACTIF. C'est LA couture des projets
|
|
// (projects.go) : tout le code mémoire passe par ici, donc il hérite du
|
|
// cloisonnement sans avoir à connaître la notion de projet. Avant les projets,
|
|
// memory/ était plat ; il est désormais la racine des dossiers de projet, et
|
|
// ensureDefaultProject a migré les pages historiques vers memory/generale/.
|
|
func memoryDir() string { return projectMemoryDir(activeProjectSlug()) }
|
|
func modelsDir() string { return filepath.Join(LokiHome(), "models") }
|
|
func workspaceDir() string { return filepath.Join(LokiHome(), "workspace") }
|
|
|
|
// serviceName est le nom de l'unité qui exécute llama-server. Son pendant est
|
|
// uiUnitName (« loki-ui »), qui sert l'interface et le tunnel.
|
|
func serviceName() string {
|
|
if n := os.Getenv("LOKI_SERVICE"); n != "" {
|
|
return n
|
|
}
|
|
return "loki-engine"
|
|
}
|
|
|
|
// 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) }
|