mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Rebaptise AJEAN en Loki (fork, lignée conservée)
- module github.com/R0m1k3/Loki, cmd/loki, internal/loki (package loki) - LOKI_HOME, LOKI_MODEL_DIRS, LOKI_SERVICE, LOKI_DL_CONNS ; /etc/loki ; units loki-engine / loki-ui ; binaire et aide CLI - updateRepo pointe sur R0m1k3/Loki (l'auto-update ne tirera plus les binaires AJEAN amont) Conservé à l'identique : le domaine ajean.link (service de tunnel amont), les littéraux de migration 0.7.x (migrate_07.go), RELEASE_NOTES.md et LICENSE (historique et licence de l'amont). go build/vet/test : verts.
This commit is contained in:
157 files changed
+928
-928
No files matched your search
@@ -1,6 +1,6 @@
|
||||
// backend_build.go — machinerie de compilation de llama.cpp : détection du
|
||||
// plan de build (CUDA/ROCm/Metal/Vulkan/CPU), cmake, suivi de progression, logs.
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -73,7 +73,7 @@ func detectBuildPlan() buildPlan {
|
||||
// L'UI web embarquée de llama-server exige npm (ou un téléchargement
|
||||
// d'assets pré-compilés depuis HuggingFace) pour générer un service-worker
|
||||
// PWA — une dépendance lourde qui casse le build sur une machine sans node.
|
||||
// ajean fournit sa propre UI, donc on la désactive : build plus rapide et
|
||||
// loki fournit sa propre UI, donc on la désactive : build plus rapide et
|
||||
// sans dépendance réseau/npm. BUILD_UI=OFF coupe npm ; USE_PREBUILT_UI=OFF
|
||||
// coupe le téléchargement d'assets pré-compilés depuis HuggingFace (qui
|
||||
// échoue sur un réseau restreint et fait planter l'embed). Sur un checkout
|
||||
@@ -237,7 +237,7 @@ func hintMissingBuildDep(p buildPlan, cfgLog string) {
|
||||
fmt.Printf(" Relance l'installeur du CUDA Toolkit (installation personnalisée) en cochant « CUDA → Visual Studio Integration »\n")
|
||||
fmt.Printf(" (Visual Studio avec le workload C++ doit déjà être installé), ou copie les fichiers de\n")
|
||||
fmt.Printf(" <toolkit>\\extras\\visual_studio_integration\\MSBuildExtensions vers\n")
|
||||
fmt.Printf(" <VS>\\MSBuild\\Microsoft\\VC\\<version>\\BuildCustomizations, puis relance %s.\n", bold("ajean llamacpp install"))
|
||||
fmt.Printf(" <VS>\\MSBuild\\Microsoft\\VC\\<version>\\BuildCustomizations, puis relance %s.\n", bold("loki llamacpp install"))
|
||||
}
|
||||
// Linux/CUDA : nvcc trouvé mais en-têtes/cudart introuvables = le toolkit
|
||||
// complet n'est pas installé (seul le paquet nvcc l'est). On passe déjà
|
||||
@@ -245,15 +245,15 @@ func hintMissingBuildDep(p buildPlan, cfgLog string) {
|
||||
// c'est qu'il manque pour de bon.
|
||||
if p.backend == "cuda" && strings.Contains(log, "CUDA Toolkit not found") {
|
||||
fmt.Printf("\n%s nvcc est présent mais le CUDA Toolkit complet (en-têtes + cudart) est introuvable.\n", yellow("[dépendance]"))
|
||||
fmt.Printf(" Installe le toolkit NVIDIA officiel (il se pose dans /usr/local/cuda), puis relance %s.\n", bold("ajean llamacpp install"))
|
||||
fmt.Printf(" Installe le toolkit NVIDIA officiel (il se pose dans /usr/local/cuda), puis relance %s.\n", bold("loki llamacpp install"))
|
||||
}
|
||||
if p.backend == "vulkan" && strings.Contains(log, "SPIRV-Headers") {
|
||||
fmt.Printf("\n%s dépendance manquante pour le backend %s : les en-têtes SPIR-V (paquet « SPIRV-Headers ») sont introuvables.\n",
|
||||
yellow("[dépendance]"), green("Vulkan"))
|
||||
if cmd := pkgInstallHint("spirv-headers"); cmd != "" {
|
||||
fmt.Printf(" installe-les puis relance %s : %s\n", bold("ajean llamacpp install"), bold(cmd))
|
||||
fmt.Printf(" installe-les puis relance %s : %s\n", bold("loki llamacpp install"), bold(cmd))
|
||||
} else {
|
||||
fmt.Printf(" installe le paquet de développement « SPIRV-Headers » de ta distribution, puis relance %s.\n", bold("ajean llamacpp install"))
|
||||
fmt.Printf(" installe le paquet de développement « SPIRV-Headers » de ta distribution, puis relance %s.\n", bold("loki llamacpp install"))
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"errors"
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -12,7 +12,7 @@ func TestCompiledFile(t *testing.T) {
|
||||
`[123/456] Building CUDA object ggml/src/ggml-cuda/CMakeFiles/ggml-cuda.dir/acc.cu.o`: "acc.cu",
|
||||
// La ligne de commande nvcc géante ne doit PAS être prise pour un fichier.
|
||||
` C:\...\nvcc.exe -x cu ... -o ggml-cuda.dir\Release\acc.obj "C:\...\acc.cu"`: "",
|
||||
`Building Custom Rule C:/ProgramData/ajean/...`: "",
|
||||
`Building Custom Rule C:/ProgramData/loki/...`: "",
|
||||
`-- UI: running npm install`: "",
|
||||
}
|
||||
for in, want := range cases {
|
||||
@@ -1,11 +1,11 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// backend_catalog.go — récupère la liste de modèles curatée servie par ajean.link et la
|
||||
// combine avec les infos matérielles locales, pour que l'écran d'accueil (à
|
||||
// venir) propose « en un clic » un modèle adapté à la machine.
|
||||
//
|
||||
// La liste vit sur https://ajean.link/models.json (éditée par l'opérateur, voir
|
||||
// ajean-relay/models_catalog.go). Si le réseau échoue, on retombe sur un
|
||||
// loki-relay/models_catalog.go). Si le réseau échoue, on retombe sur un
|
||||
// catalogue minimal embarqué (fallbackCatalogJSON) pour ne jamais bloquer.
|
||||
|
||||
import (
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
func readAPIKey() string { return getStr(bkState, "api_key") }
|
||||
|
||||
// authHeader sets the Authorization: Bearer header on req when an API key is
|
||||
// configured, so AJEAN's own internal calls (chat/web/bench/test) authenticate
|
||||
// configured, so Loki's own internal calls (chat/web/bench/test) authenticate
|
||||
// against a protected llama-server. No-op when no key is set.
|
||||
func authHeader(req *http.Request) {
|
||||
if k := readAPIKey(); k != "" {
|
||||
@@ -30,7 +30,7 @@ func authHeader(req *http.Request) {
|
||||
func genAPIKey() string {
|
||||
buf := make([]byte, 24)
|
||||
_, _ = rand.Read(buf)
|
||||
return "sk-ajean-" + hex.EncodeToString(buf)
|
||||
return "sk-loki-" + hex.EncodeToString(buf)
|
||||
}
|
||||
|
||||
// writeAPIKey enregistre (key != "") ou supprime (key == "") la clé Bearer.
|
||||
@@ -41,7 +41,7 @@ func writeAPIKey(key string) error {
|
||||
return putStr(bkState, "api_key", key)
|
||||
}
|
||||
|
||||
// maskAPIKey renders a key for display: keep the "sk-ajean-" prefix and last 4
|
||||
// maskAPIKey renders a key for display: keep the "sk-loki-" prefix and last 4
|
||||
// chars, elide the middle. Empty in → empty out.
|
||||
func maskAPIKey(k string) string {
|
||||
if k == "" {
|
||||
@@ -57,9 +57,9 @@ func maskAPIKey(k string) string {
|
||||
// internet, le serveur exige alors « Authorization: Bearer <clé> » à chaque
|
||||
// appel.
|
||||
//
|
||||
// ajean set-api-key <clé> définit la clé
|
||||
// ajean set-api-key génère une clé aléatoire
|
||||
// ajean set-api-key "" supprime la protection
|
||||
// loki set-api-key <clé> définit la clé
|
||||
// loki set-api-key génère une clé aléatoire
|
||||
// loki set-api-key "" supprime la protection
|
||||
func cmdSetAPIKey(args []string) error {
|
||||
var key string
|
||||
switch {
|
||||
@@ -83,7 +83,7 @@ func cmdSetAPIKey(args []string) error {
|
||||
fmt.Print(dim("[info] redémarrer le service pour appliquer ? [Y/n] "))
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
if sc.Scan() && strings.HasPrefix(strings.ToLower(strings.TrimSpace(sc.Text())), "n") {
|
||||
fmt.Println(dim("[info] pense à lancer 'ajean restart'"))
|
||||
fmt.Println(dim("[info] pense à lancer 'loki restart'"))
|
||||
return nil
|
||||
}
|
||||
return serviceAction("restart")
|
||||
@@ -105,7 +105,7 @@ func WriteConfig(m map[string]string) error { return replaceKV(bkConfig, m) }
|
||||
// eux : une paire ouvrante/fermante du même caractère.
|
||||
//
|
||||
// Un simple Trim(v, `"`) mangeait le guillemet d'un argument interne — par
|
||||
// exemple EXTRA_ARGS=--chat-template-file "/etc/ajean/tpl.jinja" perdait son
|
||||
// exemple EXTRA_ARGS=--chat-template-file "/etc/loki/tpl.jinja" perdait son
|
||||
// guillemet final et repartait déséquilibré dans le preset (issue #17 : contenu
|
||||
// tronqué / guillemets non appariés à la création d'un preset).
|
||||
func unquoteValue(v string) string {
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// dans le preset (issue #17).
|
||||
func TestParseEnvKeepsInnerQuotes(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
`EXTRA_ARGS=--jinja --chat-template-file "/etc/ajean/tpl.jinja"`: `--jinja --chat-template-file "/etc/ajean/tpl.jinja"`,
|
||||
`EXTRA_ARGS=--jinja --chat-template-file "/etc/loki/tpl.jinja"`: `--jinja --chat-template-file "/etc/loki/tpl.jinja"`,
|
||||
`EXTRA_ARGS="--jinja --flash-attn"`: `--jinja --flash-attn`,
|
||||
`MODEL='/mnt/d/x.gguf'`: `/mnt/d/x.gguf`,
|
||||
`MODEL=/mnt/d/x.gguf`: `/mnt/d/x.gguf`,
|
||||
@@ -31,7 +31,7 @@ func TestParseEnvKeepsInnerQuotes(t *testing.T) {
|
||||
func TestFormatEnvRoundTrip(t *testing.T) {
|
||||
in := map[string]string{
|
||||
"MODEL": "/mnt/mes modèles/x.gguf",
|
||||
"EXTRA_ARGS": `--jinja --chat-template-file "/etc/ajean/tpl.jinja"`,
|
||||
"EXTRA_ARGS": `--jinja --chat-template-file "/etc/loki/tpl.jinja"`,
|
||||
"CTX": "32768",
|
||||
}
|
||||
out := parseEnv(formatEnv(in))
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
|
||||
// backend_gpu.go — sélection du/des GPU utilisés par llama-server.
|
||||
//
|
||||
// ajean gpu liste les GPU et montre la sélection courante
|
||||
// ajean gpu 1 n'utilise que le GPU d'index 1
|
||||
// ajean gpu 0 1 utilise les GPU 0 et 1
|
||||
// ajean gpu all réinitialise (tous les GPU visibles)
|
||||
// loki gpu liste les GPU et montre la sélection courante
|
||||
// loki gpu 1 n'utilise que le GPU d'index 1
|
||||
// loki gpu 0 1 utilise les GPU 0 et 1
|
||||
// loki gpu all réinitialise (tous les GPU visibles)
|
||||
//
|
||||
// La sélection est stockée dans config.env sous CUDA_VISIBLE_DEVICES ; backend_serve.go
|
||||
// l'exporte (avec CUDA_DEVICE_ORDER=PCI_BUS_ID pour que les index correspondent
|
||||
@@ -53,7 +53,7 @@ func cmdGPU(args []string) error {
|
||||
return fmt.Errorf("index GPU invalide: %q (attendu un nombre)", tok)
|
||||
}
|
||||
if n < 0 || n >= len(gpus) {
|
||||
return fmt.Errorf("index GPU %d hors limites (0..%d) — voir « ajean gpu »", n, len(gpus)-1)
|
||||
return fmt.Errorf("index GPU %d hors limites (0..%d) — voir « loki gpu »", n, len(gpus)-1)
|
||||
}
|
||||
idx = append(idx, strconv.Itoa(n))
|
||||
}
|
||||
@@ -102,7 +102,7 @@ func gpuList() error {
|
||||
} else {
|
||||
fmt.Printf(" Sélection : %s (CUDA_VISIBLE_DEVICES=%s)\n", bold(sel), sel)
|
||||
}
|
||||
fmt.Printf(" %s ajean gpu <index…> pour choisir, ajean gpu all pour réinitialiser\n", dim("→"))
|
||||
fmt.Printf(" %s loki gpu <index…> pour choisir, loki gpu all pour réinitialiser\n", dim("→"))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func gpuSet(value string) error {
|
||||
fmt.Print(dim("[info] redémarrer le service pour appliquer ? [Y/n] "))
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
if sc.Scan() && strings.HasPrefix(strings.ToLower(strings.TrimSpace(sc.Text())), "n") {
|
||||
fmt.Println(dim("[info] pense à lancer 'ajean restart'"))
|
||||
fmt.Println(dim("[info] pense à lancer 'loki restart'"))
|
||||
return nil
|
||||
}
|
||||
return serviceAction("restart")
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
|
||||
// backend_llamacpp.go — gestion du backend llama.cpp (clone, build, mise à jour).
|
||||
//
|
||||
// `ajean llamacpp install` installe un build neuf, détecte automatiquement
|
||||
// `loki llamacpp install` installe un build neuf, détecte automatiquement
|
||||
// l'accélérateur (CUDA / ROCm / Metal / CPU) et la
|
||||
// compute capability du GPU, puis pointe BIN dessus.
|
||||
// `ajean llamacpp update` met à jour le dépôt existant (git pull) et recompile
|
||||
// `loki llamacpp update` met à jour le dépôt existant (git pull) et recompile
|
||||
// avec la bonne config, sans intervention.
|
||||
// `ajean llamacpp status` montre le commit courant, le backend détecté et le
|
||||
// `loki llamacpp status` montre le commit courant, le backend détecté et le
|
||||
// retard éventuel sur origin.
|
||||
|
||||
const llamacppRepoURL = "https://github.com/ggml-org/llama.cpp.git"
|
||||
@@ -55,7 +55,7 @@ func cmdLlamacpp(args []string) error {
|
||||
if err := SetConfigKey("BIN", bin); err != nil {
|
||||
return fmt.Errorf("binaires installés mais échec d'écriture de BIN : %w", err)
|
||||
}
|
||||
fmt.Printf("%s BIN mis à jour — %s pour appliquer\n", green("✓"), bold("ajean restart"))
|
||||
fmt.Printf("%s BIN mis à jour — %s pour appliquer\n", green("✓"), bold("loki restart"))
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("sous-commande inconnue: %s (install | update | prebuilt | status)", sub)
|
||||
@@ -68,7 +68,7 @@ func cmdLlamacpp(args []string) error {
|
||||
|
||||
// llamacppRepoDir resolves the llama.cpp checkout: derived from config BIN when
|
||||
// possible (so `update` targets whatever build the service actually runs),
|
||||
// otherwise the default under $AJEAN_HOME/backends/llama.cpp.
|
||||
// otherwise the default under $LOKI_HOME/backends/llama.cpp.
|
||||
func llamacppRepoDir() string {
|
||||
if bin := ReadConfig()["BIN"]; bin != "" {
|
||||
if real, err := filepath.EvalSymlinks(bin); err == nil {
|
||||
@@ -161,7 +161,7 @@ func llamacppInstall(args []string) error {
|
||||
if isDir(filepath.Join(repo, ".git")) {
|
||||
if !force {
|
||||
fmt.Printf("%s dépôt déjà présent dans %s\n", yellow("[info]"), repo)
|
||||
fmt.Printf(" → %s pour le mettre à jour, ou --force pour repartir de zéro\n", bold("ajean llamacpp update"))
|
||||
fmt.Printf(" → %s pour le mettre à jour, ou --force pour repartir de zéro\n", bold("loki llamacpp update"))
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("%s --force : suppression de %s\n", yellow("[info]"), repo)
|
||||
@@ -208,7 +208,7 @@ func llamacppInstall(args []string) error {
|
||||
}
|
||||
fmt.Printf("%s BIN mis à jour\n", green("✓"))
|
||||
fmt.Printf("\nProchaines étapes :\n 1. renseigne MODEL : %s\n 2. démarre : %s\n",
|
||||
bold("ajean edit"), bold("ajean restart"))
|
||||
bold("loki edit"), bold("loki restart"))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ func llamacppUpdate(args []string) error {
|
||||
|
||||
repo := llamacppRepoDir()
|
||||
if !isDir(filepath.Join(repo, ".git")) {
|
||||
return fmt.Errorf("aucun dépôt llama.cpp trouvé (%s).\n → lance d'abord %s", repo, bold("ajean llamacpp install"))
|
||||
return fmt.Errorf("aucun dépôt llama.cpp trouvé (%s).\n → lance d'abord %s", repo, bold("loki llamacpp install"))
|
||||
}
|
||||
fmt.Printf("%s dépôt : %s\n", cyan("▶"), repo)
|
||||
|
||||
@@ -317,14 +317,14 @@ func llamacppUpdate(args []string) error {
|
||||
fmt.Printf("\n%s mis à jour : %s → %s\n", green("✓"), oldCommit, newCommit)
|
||||
|
||||
if noRestart {
|
||||
fmt.Printf("%s --no-restart : pense à lancer %s\n", dim("[info]"), bold("ajean restart"))
|
||||
fmt.Printf("%s --no-restart : pense à lancer %s\n", dim("[info]"), bold("loki restart"))
|
||||
return nil
|
||||
}
|
||||
if svcWasUp {
|
||||
fmt.Printf("%s redémarrage du service…\n", cyan("▶"))
|
||||
return serviceAction("start")
|
||||
}
|
||||
fmt.Printf("%s service non démarré auparavant — lance %s quand tu veux\n", dim("[info]"), bold("ajean start"))
|
||||
fmt.Printf("%s service non démarré auparavant — lance %s quand tu veux\n", dim("[info]"), bold("loki start"))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ func llamacppStatus(args []string) error {
|
||||
fmt.Printf("%s\n", bold("llama.cpp"))
|
||||
fmt.Printf(" dépôt : %s\n", repo)
|
||||
if !isDir(filepath.Join(repo, ".git")) {
|
||||
fmt.Printf(" %s pas encore installé — %s\n", yellow("état"), bold("ajean llamacpp install"))
|
||||
fmt.Printf(" %s pas encore installé — %s\n", yellow("état"), bold("loki llamacpp install"))
|
||||
return nil
|
||||
}
|
||||
commit := gitOutput(repo, "log", "-1", "--format=%h %ci %s")
|
||||
@@ -354,7 +354,7 @@ func llamacppStatus(args []string) error {
|
||||
// Retard sur origin (best-effort, sans fetch réseau).
|
||||
if branch != "" && branch != "HEAD" {
|
||||
if behind := gitOutput(repo, "rev-list", "--count", "HEAD..origin/"+branch); behind != "" && behind != "0" {
|
||||
fmt.Printf(" maj : %s commit(s) de retard sur origin/%s — %s\n", yellow(behind), branch, bold("ajean llamacpp update"))
|
||||
fmt.Printf(" maj : %s commit(s) de retard sur origin/%s — %s\n", yellow(behind), branch, bold("loki llamacpp update"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -10,13 +10,13 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Les .gguf vivent dans $AJEAN_HOME/models, mais un modèle de 40 Go tient
|
||||
// Les .gguf vivent dans $LOKI_HOME/models, mais un modèle de 40 Go tient
|
||||
// rarement sur le disque système : on autorise des dossiers supplémentaires
|
||||
// (disque dur externe, second SSD, montage réseau…). La liste est persistée en
|
||||
// base — PAS dans la configuration, que le changement de preset remplace — et
|
||||
// peut aussi venir de $AJEAN_MODEL_DIRS.
|
||||
// peut aussi venir de $LOKI_MODEL_DIRS.
|
||||
|
||||
// modelDirsListSep sépare les dossiers dans $AJEAN_MODEL_DIRS (':' sous Unix,
|
||||
// modelDirsListSep sépare les dossiers dans $LOKI_MODEL_DIRS (':' sous Unix,
|
||||
// ';' sous Windows, comme le PATH).
|
||||
func modelDirsListSep() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
@@ -30,7 +30,7 @@ func modelDirsListSep() string {
|
||||
func extraModelDirs() []string {
|
||||
var raw []string
|
||||
getJSON(bkState, "model_dirs", &raw)
|
||||
if v := os.Getenv("AJEAN_MODEL_DIRS"); v != "" {
|
||||
if v := os.Getenv("LOKI_MODEL_DIRS"); v != "" {
|
||||
raw = append(raw, strings.Split(v, modelDirsListSep())...)
|
||||
}
|
||||
return dedupDirs(raw, modelsDir())
|
||||
@@ -92,7 +92,7 @@ func pathWithin(dir, p string) bool {
|
||||
|
||||
// baseName renvoie le nom de fichier d'un chemin en acceptant les DEUX
|
||||
// séparateurs : un config.env écrit sous Windows (C:\models\x.gguf) peut être
|
||||
// relu par un ajean Linux, où filepath.Base ne coupe pas sur '\'.
|
||||
// relu par un loki Linux, où filepath.Base ne coupe pas sur '\'.
|
||||
func baseName(p string) string {
|
||||
p = strings.TrimRight(strings.TrimSpace(p), `/\`)
|
||||
if i := strings.LastIndexAny(p, `/\`); i >= 0 {
|
||||
@@ -128,7 +128,7 @@ func resolveServeModelPath(name string) (string, error) {
|
||||
// absolu) en chemin absolu vers un .gguf. Un chemin absolu n'est accepté que
|
||||
// s'il est dans un des dossiers déclarés — sinon l'UI web deviendrait un moyen
|
||||
// de lire/supprimer n'importe quel fichier de la machine. Un simple nom de
|
||||
// fichier est cherché dans chaque dossier, AJEAN_HOME en premier.
|
||||
// fichier est cherché dans chaque dossier, LOKI_HOME en premier.
|
||||
func resolveModelPath(name string) (string, error) {
|
||||
s := strings.TrimSpace(strings.Trim(strings.TrimSpace(name), `"`))
|
||||
if s == "" {
|
||||
@@ -218,7 +218,7 @@ func resolveDownloadDir(dir string) (string, error) {
|
||||
return "", fmt.Errorf("dossier non autorisé : %s — ajoute-le dans « Dossiers de modèles »", abs)
|
||||
}
|
||||
|
||||
// handleModelDirs : GET liste les dossiers de modèles (AJEAN_HOME + ajoutés),
|
||||
// handleModelDirs : GET liste les dossiers de modèles (LOKI_HOME + ajoutés),
|
||||
// POST {path, action:"add"|"remove"} en ajoute ou en retire un.
|
||||
func handleModelDirs(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
@@ -269,5 +269,5 @@ func handleModelDirs(w http.ResponseWriter, r *http.Request) {
|
||||
n := modelDirCount(d)
|
||||
out = append(out, map[string]any{"path": d, "home": false, "count": n, "exists": n >= 0, "free": diskFree(d)})
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "dirs": out, "env": os.Getenv("AJEAN_MODEL_DIRS") != ""})
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "dirs": out, "env": os.Getenv("LOKI_MODEL_DIRS") != ""})
|
||||
}
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
func TestResolveModelPathExtraDir(t *testing.T) {
|
||||
testHome(t)
|
||||
ext := t.TempDir()
|
||||
t.Setenv("AJEAN_MODEL_DIRS", "")
|
||||
t.Setenv("LOKI_MODEL_DIRS", "")
|
||||
|
||||
extModel := filepath.Join(ext, "gros.gguf")
|
||||
if err := os.WriteFile(extModel, []byte("x"), 0o644); err != nil {
|
||||
@@ -42,7 +42,7 @@ func TestResolveModelPathExtraDir(t *testing.T) {
|
||||
func TestResolveModelPathHomeFirst(t *testing.T) {
|
||||
testHome(t)
|
||||
ext := t.TempDir()
|
||||
t.Setenv("AJEAN_MODEL_DIRS", ext)
|
||||
t.Setenv("LOKI_MODEL_DIRS", ext)
|
||||
if err := os.MkdirAll(modelsDir(), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -87,7 +87,7 @@ func detectQuant(content string) string {
|
||||
}
|
||||
|
||||
// downloadDestPath resolves the destination of a downloaded model : le dossier
|
||||
// demandé (vide = AJEAN_HOME) parmi les dossiers de modèles déclarés, en
|
||||
// demandé (vide = LOKI_HOME) parmi les dossiers de modèles déclarés, en
|
||||
// refusant tout ce qui en sortirait (path traversal).
|
||||
func downloadDestPath(name, dir string) (string, error) {
|
||||
base := filepath.Base(strings.TrimSpace(name))
|
||||
@@ -146,7 +146,7 @@ func deleteModelFile(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleModelDelete deletes a single .gguf from AJEAN_HOME.
|
||||
// handleModelDelete deletes a single .gguf from LOKI_HOME.
|
||||
func handleModelDelete(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
@@ -208,10 +208,10 @@ var dlClient = &http.Client{
|
||||
}
|
||||
|
||||
// dlConns is the number of parallel range requests used per download.
|
||||
// Overridable with AJEAN_DL_CONNS (1 disables parallelism).
|
||||
// Overridable with LOKI_DL_CONNS (1 disables parallelism).
|
||||
func dlConns() int {
|
||||
n := 8
|
||||
if v := os.Getenv("AJEAN_DL_CONNS"); v != "" {
|
||||
if v := os.Getenv("LOKI_DL_CONNS"); v != "" {
|
||||
if p, err := strconv.Atoi(v); err == nil && p > 0 {
|
||||
n = p
|
||||
}
|
||||
@@ -256,7 +256,7 @@ func normalizeHFURL(raw string) (string, string, error) {
|
||||
}
|
||||
|
||||
// handleModelDownload kicks off a background download of a .gguf from a URL
|
||||
// (typically Hugging Face) into AJEAN_HOME. Progress is polled via
|
||||
// (typically Hugging Face) into LOKI_HOME. Progress is polled via
|
||||
// /api/models/download/status.
|
||||
func handleModelDownload(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
@@ -412,7 +412,7 @@ func dlRequest(ctx context.Context, dlURL, rng string) (*http.Request, error) {
|
||||
if k := os.Getenv("HF_TOKEN"); k != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+k)
|
||||
}
|
||||
req.Header.Set("User-Agent", "ajean/"+Version)
|
||||
req.Header.Set("User-Agent", "loki/"+Version)
|
||||
req.Header.Set("Accept-Encoding", "identity") // never gzip a .gguf: it breaks ranges
|
||||
if rng != "" {
|
||||
req.Header.Set("Range", rng)
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// Destination des téléchargements : le dossier doit être choisi parmi les
|
||||
// dossiers de modèles déclarés (sinon l'UI web écrirait n'importe où), et
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
func TestDownloadDestPathDir(t *testing.T) {
|
||||
testHome(t)
|
||||
extra := t.TempDir()
|
||||
t.Setenv("AJEAN_MODEL_DIRS", extra)
|
||||
t.Setenv("LOKI_MODEL_DIRS", extra)
|
||||
|
||||
// Dossier vide → models/.
|
||||
got, err := downloadDestPath("m.gguf", "")
|
||||
@@ -77,7 +77,7 @@ func TestDiskFreeAndCheck(t *testing.T) {
|
||||
func TestCleanStalePartFilesExtraDirs(t *testing.T) {
|
||||
testHome(t)
|
||||
extra := t.TempDir()
|
||||
t.Setenv("AJEAN_MODEL_DIRS", extra)
|
||||
t.Setenv("LOKI_MODEL_DIRS", extra)
|
||||
stale := filepath.Join(extra, "coupe.gguf.part")
|
||||
if err := os.WriteFile(stale, []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -27,7 +27,7 @@ func serveBlob(data []byte, ranges bool) *httptest.Server {
|
||||
|
||||
// Un téléchargement annulé ne doit laisser NI le .gguf final NI le .part.
|
||||
func TestRunDownloadCancelLeavesNothing(t *testing.T) {
|
||||
t.Setenv("AJEAN_DL_CONNS", "4")
|
||||
t.Setenv("LOKI_DL_CONNS", "4")
|
||||
release := make(chan struct{})
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Range") == "bytes=0-0" { // sonde : réponse immédiate
|
||||
@@ -99,7 +99,7 @@ func TestCleanStalePartFiles(t *testing.T) {
|
||||
func TestRunDownloadParallelAndFallback(t *testing.T) {
|
||||
data := make([]byte, 48<<20) // > 3×dlMinChunk so the split actually kicks in
|
||||
rand.New(rand.NewSource(1)).Read(data)
|
||||
t.Setenv("AJEAN_DL_CONNS", "4")
|
||||
t.Setenv("LOKI_DL_CONNS", "4")
|
||||
|
||||
for _, ranges := range []bool{true, false} {
|
||||
srv := serveBlob(data, ranges)
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// llama-server n'a besoin QUE de la première : il ouvre les suivantes tout seul,
|
||||
// à condition qu'elles soient dans le même dossier.
|
||||
//
|
||||
// Tout le reste d'AJEAN traitait chaque tranche comme un modèle indépendant :
|
||||
// Tout le reste d'Loki traitait chaque tranche comme un modèle indépendant :
|
||||
// le lien collé ne rapatriait qu'un fichier sur trois (le moteur mourait sur
|
||||
// « tensor not found »), la liste du sélecteur montrait trois entrées dont deux
|
||||
// ne démarrent pas, et supprimer « le modèle » laissait les deux autres tranches
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -8,7 +8,7 @@
|
||||
// CUDA officiel pour Linux (on retombe sur Vulkan — la compilation locale
|
||||
// reste la voie CUDA sous Linux). La compilation locale reste indispensable
|
||||
// pour les forks (ex. PrismML).
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
@@ -375,7 +375,7 @@ func prebuiltInstall(logf, phasef func(string)) (string, error) {
|
||||
logf(fmt.Sprintf("release %s — variant retenu : %s", tag, label))
|
||||
|
||||
// Réinstaller à l'identique est inutile SAUF si l'extraction date d'une
|
||||
// version de AJEAN qui ignorait les liens des archives (backend installé mais
|
||||
// version de Loki qui ignorait les liens des archives (backend installé mais
|
||||
// bibliothèques introuvables au lancement) : le marqueur de format force alors
|
||||
// une ré-extraction propre au lieu d'un « déjà à jour » trompeur.
|
||||
if cur := prebuiltServerBin(); curTag == tag && prebuiltVersionFormat() == prebuiltFormat && cur != "" {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -193,7 +193,7 @@ var preservedKeys = []string{"MEM_MODE", "CRAWL4AI_URL", "WEB_ENGINE", "CUDA_VIS
|
||||
|
||||
// softPreservedKeys : préservées SEULEMENT si le preset d'arrivée ne les définit
|
||||
// pas lui-même. CUDA_VISIBLE_DEVICES est dans ce cas : c'est d'ordinaire un
|
||||
// choix de machine (`ajean gpu`) qui doit survivre aux bascules, MAIS un preset
|
||||
// choix de machine (`loki gpu`) qui doit survivre aux bascules, MAIS un preset
|
||||
// a le droit de l'imposer — « QWEN 3.6 27B FABLE 2 GPU » réclame `1,0` pour que
|
||||
// son `--tensor-split 0.965,0.035` ait bien deux cartes en face. Le préserver
|
||||
// inconditionnellement écrasait cette valeur par l'ancienne (mono-GPU) : le
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -36,9 +36,9 @@ func TestPresetFingerprintIgnoresCosmetics(t *testing.T) {
|
||||
|
||||
// TestSwitchToPresetGardeLesReglagesMachine : basculer de preset ne doit pas
|
||||
// effacer les réglages qui décrivent la MACHINE et non le modèle. Le cas vécu :
|
||||
// `ajean gpu 1` écrit CUDA_VISIBLE_DEVICES dans config.env, la bascule de preset
|
||||
// `loki gpu 1` écrit CUDA_VISIBLE_DEVICES dans config.env, la bascule de preset
|
||||
// suivante l'écrasait, llama.cpp revoyait les deux cartes et réétalait le modèle
|
||||
// sur la petite — pendant que `ajean gpu` réaffichait « auto ».
|
||||
// sur la petite — pendant que `loki gpu` réaffichait « auto ».
|
||||
func TestSwitchToPresetGardeLesReglagesMachine(t *testing.T) {
|
||||
home := testHome(t)
|
||||
write := func(p, body string) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -41,15 +41,15 @@ func cmdServe(args []string) error {
|
||||
cfg := ReadConfig()
|
||||
bin := cfg["BIN"]
|
||||
if bin == "" {
|
||||
return fmt.Errorf("BIN non défini — lance « ajean edit »")
|
||||
return fmt.Errorf("BIN non défini — lance « loki edit »")
|
||||
}
|
||||
model := cfg["MODEL"]
|
||||
if model == "" {
|
||||
return fmt.Errorf("MODEL non défini — lance « ajean edit »")
|
||||
return fmt.Errorf("MODEL non défini — lance « loki edit »")
|
||||
}
|
||||
// MODEL vaut soit un simple nom de fichier (le .gguf vit dans AJEAN_HOME ou
|
||||
// MODEL vaut soit un simple nom de fichier (le .gguf vit dans LOKI_HOME ou
|
||||
// dans un dossier déclaré — disque externe…), soit un chemin absolu. Sous
|
||||
// systemd/launchd le WorkingDirectory vaut AJEAN_HOME, donc le relatif tombait
|
||||
// systemd/launchd le WorkingDirectory vaut LOKI_HOME, donc le relatif tombait
|
||||
// juste ; lancé depuis une app de bureau, le répertoire courant est « / » et
|
||||
// llama-server ne trouvait rien. On résout donc explicitement, quel que soit
|
||||
// le contexte de lancement.
|
||||
@@ -69,7 +69,7 @@ func cmdServe(args []string) error {
|
||||
strings.Join(missing, ", "), filepath.Dir(model), len(shardFamily(filepath.Base(model))))
|
||||
}
|
||||
if !filepath.IsAbs(bin) {
|
||||
bin = filepath.Join(AjeanHome(), bin)
|
||||
bin = filepath.Join(LokiHome(), bin)
|
||||
}
|
||||
// Le moteur précompilé s'installe dans un dossier versionné : un preset écrit
|
||||
// avant une mise à jour pointe sur une release qui n'existe plus. On le fait
|
||||
@@ -81,7 +81,7 @@ func cmdServe(args []string) error {
|
||||
// Linux, PATH on Windows — handled inside execServer.
|
||||
setLibraryPath(filepath.Dir(bin))
|
||||
|
||||
// Sélection GPU (ajean gpu) : on filtre les devices visibles par llama-server.
|
||||
// Sélection GPU (loki gpu) : on filtre les devices visibles par llama-server.
|
||||
// CUDA_DEVICE_ORDER=PCI_BUS_ID garantit que les index correspondent à ceux
|
||||
// affichés par nvidia-smi (sinon CUDA réordonne par "device le plus rapide").
|
||||
if v := cfg["CUDA_VISIBLE_DEVICES"]; v != "" {
|
||||
@@ -160,12 +160,12 @@ func cmdServe(args []string) error {
|
||||
// Vieux moteur (ou fork) qui ne connaît pas le drapeau : le lui passer
|
||||
// le ferait sortir en erreur au démarrage, donc boucler. On le dit et on
|
||||
// continue sans — mieux vaut un modèle qui réfléchit qu'un moteur mort.
|
||||
fmt.Fprintf(os.Stderr, "[ajean serve] ce moteur ne connaît pas --reasoning : impossible de désactiver le raisonnement\n")
|
||||
fmt.Fprintf(os.Stderr, "[loki serve] ce moteur ne connaît pas --reasoning : impossible de désactiver le raisonnement\n")
|
||||
}
|
||||
}
|
||||
// API_KEY protège le serveur quand il est exposé sur internet : llama-server
|
||||
// exige alors l'en-tête "Authorization: Bearer <clé>". La clé est lue depuis
|
||||
// $AJEAN_HOME/.api_key en priorité (elle survit ainsi aux changements de preset
|
||||
// $LOKI_HOME/.api_key en priorité (elle survit ainsi aux changements de preset
|
||||
// qui réécrivent config.env), avec config.env comme repli rétro-compatible.
|
||||
if k := readAPIKey(); k != "" {
|
||||
llmArgs = append(llmArgs, "--api-key", k)
|
||||
@@ -176,11 +176,11 @@ func cmdServe(args []string) error {
|
||||
// together so a path with spaces stays one argument.
|
||||
llmArgs = append(llmArgs, splitArgs(cfg["EXTRA_ARGS"])...)
|
||||
|
||||
// Working dir = AJEAN_HOME so relative paths in EXTRA_ARGS (e.g. --mmproj
|
||||
// Working dir = LOKI_HOME so relative paths in EXTRA_ARGS (e.g. --mmproj
|
||||
// mmproj-F16.gguf) still resolve.
|
||||
_ = os.Chdir(AjeanHome())
|
||||
_ = os.Chdir(LokiHome())
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[ajean serve] %s model=%s port=%s\n",
|
||||
fmt.Fprintf(os.Stderr, "[loki serve] %s model=%s port=%s\n",
|
||||
bin, filepath.Base(model), get("PORT", "8080"))
|
||||
|
||||
// Hand off to the llama-server process. On Unix this replaces the current
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import "fmt"
|
||||
|
||||
@@ -43,7 +43,7 @@ func cmdAgent(args []string) error {
|
||||
fmt.Printf(" %s %s\n", bold(p.Name), p.Title)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("usage: ajean agent [on|off|status]")
|
||||
return fmt.Errorf("usage: loki agent [on|off|status]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -12,14 +12,14 @@ import (
|
||||
// First positional arg (if any) becomes the system prompt.
|
||||
func cmdChat(args []string) error {
|
||||
if !healthCheck() {
|
||||
return fmt.Errorf("serveur injoignable sur :%d — ajean start d'abord", LLMPort())
|
||||
return fmt.Errorf("serveur injoignable sur :%d — loki start d'abord", LLMPort())
|
||||
}
|
||||
sysPrompt := strings.Join(args, " ")
|
||||
msgs := []Message{}
|
||||
if sysPrompt != "" {
|
||||
msgs = append(msgs, Message{Role: "system", Content: sysPrompt})
|
||||
}
|
||||
fmt.Printf("\n%s — /reset pour vider, /sys <prompt> pour changer le system, /quit ou Ctrl-D pour sortir\n", cyan("ajean chat"))
|
||||
fmt.Printf("\n%s — /reset pour vider, /sys <prompt> pour changer le system, /quit ou Ctrl-D pour sortir\n", cyan("loki chat"))
|
||||
if sysPrompt != "" {
|
||||
fmt.Println(dim("system: " + sysPrompt))
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func cmdChat(args []string) error {
|
||||
inReason := false
|
||||
var stats *StatsEvent
|
||||
// Print the assistant prefix once; reasoning is shown inline with a tag.
|
||||
fmt.Print(cyan("ajean") + " > ")
|
||||
fmt.Print(cyan("loki") + " > ")
|
||||
caps := globalCaps()
|
||||
// Compaction proactive (façon Hermes) : on résume les vieux tours quand
|
||||
// l'historique dépasse le seuil, au lieu d'imposer un /reset.
|
||||
@@ -120,7 +120,7 @@ func cmdChat(args []string) error {
|
||||
fmt.Print("\n")
|
||||
inReason = false
|
||||
}
|
||||
fmt.Printf("\n%s %s : %s\n%s ", dim(icon+" "+verb), "", magenta(ev.ToolUsed.Label), cyan("ajean")+" >")
|
||||
fmt.Printf("\n%s %s : %s\n%s ", dim(icon+" "+verb), "", magenta(ev.ToolUsed.Label), cyan("loki")+" >")
|
||||
case ev.Reasoning != "":
|
||||
if !inReason {
|
||||
fmt.Print(magenta("[reasoning] ") + dim(""))
|
||||
@@ -129,7 +129,7 @@ func cmdChat(args []string) error {
|
||||
fmt.Print(dim(ev.Reasoning))
|
||||
case ev.Content != "":
|
||||
if inReason {
|
||||
fmt.Print("\n" + cyan("ajean") + " > ")
|
||||
fmt.Print("\n" + cyan("loki") + " > ")
|
||||
inReason = false
|
||||
}
|
||||
full.WriteString(ev.Content)
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -159,7 +159,7 @@ func compactWouldTrigger(msgs []Message, knownTokens int) bool {
|
||||
}
|
||||
|
||||
// logCompact trace UNE ligne par décision de compaction sur la sortie d'erreur
|
||||
// (donc dans `journalctl -u ajean-ui`). Sans ça, une compaction qui ne se
|
||||
// (donc dans `journalctl -u loki-ui`). Sans ça, une compaction qui ne se
|
||||
// déclenche pas — ou qui se déclenche et n'enlève rien — est invisible : côté
|
||||
// UI on ne voit qu'une jauge qui reste haute, sans savoir si le seuil n'a pas
|
||||
// été atteint ou si la réduction a été refusée.
|
||||
@@ -318,7 +318,7 @@ func compactMessages(ctx context.Context, msgs []Message, caps Caps) ([]Message,
|
||||
|
||||
// Garantie de réduction : on n'accepte la compaction que si elle enlève au
|
||||
// moins ~20% du contexte estimé. Sinon (torse déjà maigre, résumé peu rentable)
|
||||
// on la refuse — sans ça, ajean « compactait » à presque chaque message sans
|
||||
// on la refuse — sans ça, loki « compactait » à presque chaque message sans
|
||||
// vraiment réduire, puis re-déclenchait aussitôt.
|
||||
before, after := estimateTokens(msgs), estimateTokens(out)
|
||||
if after > before*4/5 {
|
||||
@@ -387,7 +387,7 @@ Strict rules: no preamble or conclusion, no verbatim or long quotes, no throwawa
|
||||
Write the summary in the SAME language as the conversation.`
|
||||
|
||||
payload := map[string]any{
|
||||
"model": "ajean",
|
||||
"model": "loki",
|
||||
"messages": []Message{
|
||||
{Role: "system", Content: sys},
|
||||
{Role: "user", Content: transcript},
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// chat_diff.go — petit diff ligne à ligne pour l'interface : quand l'IA modifie
|
||||
// un fichier (outil `edit`) ou une page de mémoire (`mem_edit`, `mem_add`), on
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
// chat_export.go — sortir la conversation de la base.
|
||||
//
|
||||
// Jusqu'à la 0.7, l'historique vivait dans un `conversation.json` qu'on pouvait
|
||||
// ouvrir, lire et copier pour archive. La 0.8 l'a rangé dans ajean.db (bbolt) :
|
||||
// ouvrir, lire et copier pour archive. La 0.8 l'a rangé dans loki.db (bbolt) :
|
||||
// un fichier binaire, et qui porte de surcroît un verrou EXCLUSIF tant que le
|
||||
// service tourne — donc impossible ne serait-ce que de le copier sans arrêter
|
||||
// AJEAN. Le fil de discussion et les raisonnements sont toujours là, mais plus
|
||||
// Loki. Le fil de discussion et les raisonnements sont toujours là, mais plus
|
||||
// personne ne peut les récupérer. C'est une régression pour qui relisait ses
|
||||
// échanges, et ce fichier la répare.
|
||||
//
|
||||
@@ -80,7 +80,7 @@ func exportOptsFromQuery(q map[string][]string) exportOpts {
|
||||
// envoyée au modèle, `log` le journal d'affichage (celui qui porte les
|
||||
// raisonnements, les outils et les vitesses).
|
||||
type exportPayload struct {
|
||||
Version string `json:"ajean_version"`
|
||||
Version string `json:"loki_version"`
|
||||
ExportedAt string `json:"exported_at"`
|
||||
CtxUsed int `json:"ctx_used"`
|
||||
Messages []Message `json:"messages"`
|
||||
@@ -199,7 +199,7 @@ func (c *Conversation) ExportMarkdown(o exportOpts) string {
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "# Conversation AJEAN\n\nExportée le %s par AJEAN %s.\n",
|
||||
fmt.Fprintf(&b, "# Conversation Loki\n\nExportée le %s par Loki %s.\n",
|
||||
time.Now().Format("02/01/2006 à 15:04"), Version)
|
||||
// Un export tronqué ou allégé doit le DIRE : relu six mois plus tard, un fil
|
||||
// sans ses raisonnements ne doit pas passer pour le fil complet.
|
||||
@@ -208,12 +208,12 @@ func (c *Conversation) ExportMarkdown(o exportOpts) string {
|
||||
}
|
||||
|
||||
// openBubble : une bulle assistant est ouverte (au moins un bout de réponse a
|
||||
// été écrit). Sert à ne poser l'en-tête « AJEAN » qu'une fois par tour, même
|
||||
// été écrit). Sert à ne poser l'en-tête « Loki » qu'une fois par tour, même
|
||||
// quand la réponse est entrecoupée d'appels d'outils.
|
||||
openBubble := false
|
||||
head := func() {
|
||||
if !openBubble {
|
||||
b.WriteString("\n## AJEAN\n")
|
||||
b.WriteString("\n## Loki\n")
|
||||
openBubble = true
|
||||
}
|
||||
}
|
||||
@@ -303,7 +303,7 @@ func mdInline(v any) string {
|
||||
// exportFilename : nom proposé au téléchargement, horodaté pour que deux exports
|
||||
// ne s'écrasent pas dans le dossier de téléchargements.
|
||||
func exportFilename(ext string) string {
|
||||
return "ajean-conversation-" + time.Now().Format("2006-01-02-1504") + "." + ext
|
||||
return "loki-conversation-" + time.Now().Format("2006-01-02-1504") + "." + ext
|
||||
}
|
||||
|
||||
// exportBody rend la conversation selon les options. Renvoie le corps, son type
|
||||
@@ -318,10 +318,10 @@ func exportBody(o exportOpts) ([]byte, string, string, error) {
|
||||
|
||||
// cmdExport écrit la conversation dans un fichier (ou sur la sortie standard).
|
||||
//
|
||||
// ajean export → ajean-conversation-<date>.md dans le dossier courant
|
||||
// ajean export --json → idem en JSON
|
||||
// ajean export mon-fil.md → nom de fichier imposé (l'extension choisit le format)
|
||||
// ajean export - → sur la sortie standard, pour enchaîner un tube
|
||||
// loki export → loki-conversation-<date>.md dans le dossier courant
|
||||
// loki export --json → idem en JSON
|
||||
// loki export mon-fil.md → nom de fichier imposé (l'extension choisit le format)
|
||||
// loki export - → sur la sortie standard, pour enchaîner un tube
|
||||
//
|
||||
// Options de contenu, les mêmes que la fenêtre d'export de l'interface :
|
||||
//
|
||||
@@ -357,12 +357,12 @@ func cmdExport(args []string) error {
|
||||
o.Turns = n
|
||||
default:
|
||||
if strings.HasPrefix(a, "-") && a != "-" {
|
||||
return fmt.Errorf("option inconnue : %s (voir « ajean help »)", a)
|
||||
return fmt.Errorf("option inconnue : %s (voir « loki help »)", a)
|
||||
}
|
||||
out = a
|
||||
}
|
||||
}
|
||||
// Une extension explicite l'emporte sur le drapeau : `ajean export fil.json`
|
||||
// Une extension explicite l'emporte sur le drapeau : `loki export fil.json`
|
||||
// qui produirait du Markdown serait un piège.
|
||||
if strings.HasSuffix(strings.ToLower(out), ".json") {
|
||||
o.Format = "json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -37,7 +37,7 @@ func TestExportMarkdownPorteToutLeFil(t *testing.T) {
|
||||
md := conv.ExportMarkdown(defaultExportOpts())
|
||||
for _, want := range []string{
|
||||
"## Vous", "combien font 2+2 ?",
|
||||
"## AJEAN",
|
||||
"## Loki",
|
||||
"<summary>Raisonnement</summary>", "addition simple",
|
||||
"bash", "echo 4",
|
||||
} {
|
||||
@@ -45,9 +45,9 @@ func TestExportMarkdownPorteToutLeFil(t *testing.T) {
|
||||
t.Errorf("Markdown exporté sans %q :\n%s", want, md)
|
||||
}
|
||||
}
|
||||
// L'en-tête AJEAN ne doit apparaître qu'UNE fois pour un tour, même entrecoupé
|
||||
// L'en-tête Loki ne doit apparaître qu'UNE fois pour un tour, même entrecoupé
|
||||
// d'un appel d'outil.
|
||||
if n := strings.Count(md, "\n## AJEAN\n"); n != 1 {
|
||||
if n := strings.Count(md, "\n## Loki\n"); n != 1 {
|
||||
t.Errorf("en-tête assistant répété %d fois", n)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
// Accès internet de l'IA — port Go de l'extension pi ~/.pi/agent/extensions/web.ts.
|
||||
//
|
||||
// ajean parle à un serveur Crawl4AI (Chrome headless, endpoint /crawl) dont l'URL
|
||||
// loki parle à un serveur Crawl4AI (Chrome headless, endpoint /crawl) dont l'URL
|
||||
// est configurée dans config.env (CRAWL4AI_URL). Quand le mode agent ET l'accès
|
||||
// internet sont actifs ET que le serveur répond, l'IA dispose de 4 outils :
|
||||
//
|
||||
@@ -1,6 +1,6 @@
|
||||
// chat_internet_tools.go — les 4 outils web exposés au modèle (web_search,
|
||||
// web_open, web_read, web_grep) + la sous-commande `ajean internet`.
|
||||
package ajean
|
||||
// web_open, web_read, web_grep) + la sous-commande `loki internet`.
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -281,7 +281,7 @@ func toolWebGrep(args map[string]any) string {
|
||||
entry.url, len(matchIdx), pattern, capped, strings.Join(blocks, "\n\n---\n\n"))
|
||||
}
|
||||
|
||||
// ─── CLI : ajean internet [on|off|status|engine|url|key] ────────────────────
|
||||
// ─── CLI : loki internet [on|off|status|engine|url|key] ────────────────────
|
||||
|
||||
func cmdInternet(args []string) error {
|
||||
sub := ""
|
||||
@@ -292,7 +292,7 @@ func cmdInternet(args []string) error {
|
||||
case "on":
|
||||
// Le moteur intégré ne demande aucun réglage ; Crawl4AI exige un serveur.
|
||||
if webEngine() == engineCrawl && crawl4aiURL() == "" {
|
||||
return fmt.Errorf("configure d'abord l'URL : ajean internet url <url> (ou bascule sur le moteur intégré : ajean internet engine go)")
|
||||
return fmt.Errorf("configure d'abord l'URL : loki internet url <url> (ou bascule sur le moteur intégré : loki internet engine go)")
|
||||
}
|
||||
if err := setInternetEnabled(true); err != nil {
|
||||
return err
|
||||
@@ -305,7 +305,7 @@ func cmdInternet(args []string) error {
|
||||
fmt.Println(green("[ok]") + " accès internet désactivé")
|
||||
case "url":
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: ajean internet url <url> (ex: http://localhost:11235)")
|
||||
return fmt.Errorf("usage: loki internet url <url> (ex: http://localhost:11235)")
|
||||
}
|
||||
u := strings.TrimRight(strings.TrimSpace(args[1]), "/")
|
||||
if err := SetConfigKey("CRAWL4AI_URL", u); err != nil {
|
||||
@@ -317,7 +317,7 @@ func cmdInternet(args []string) error {
|
||||
fmt.Printf("%s serveur Crawl4AI : %s\n", green("[ok]"), bold(u))
|
||||
case "key":
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: ajean internet key <clé> (vide pour l'enlever : ajean internet key \"\")")
|
||||
return fmt.Errorf("usage: loki internet key <clé> (vide pour l'enlever : loki internet key \"\")")
|
||||
}
|
||||
k := strings.TrimSpace(args[1])
|
||||
if err := writeCrawlKey(k); err != nil {
|
||||
@@ -333,7 +333,7 @@ func cmdInternet(args []string) error {
|
||||
}
|
||||
case "engine":
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: ajean internet engine <go|crawl4ai>")
|
||||
return fmt.Errorf("usage: loki internet engine <go|crawl4ai>")
|
||||
}
|
||||
e := strings.ToLower(strings.TrimSpace(args[1]))
|
||||
if err := setWebEngine(e); err != nil {
|
||||
@@ -342,7 +342,7 @@ func cmdInternet(args []string) error {
|
||||
if e == engineGo {
|
||||
fmt.Println(green("[ok]") + " moteur intégré — aucune installation requise (pas de rendu JavaScript)")
|
||||
} else {
|
||||
fmt.Println(green("[ok]") + " moteur Crawl4AI — configure le serveur : ajean internet url <url>")
|
||||
fmt.Println(green("[ok]") + " moteur Crawl4AI — configure le serveur : loki internet url <url>")
|
||||
}
|
||||
case "", "status", "list":
|
||||
state := dim("off")
|
||||
@@ -358,7 +358,7 @@ func cmdInternet(args []string) error {
|
||||
fmt.Printf(" moteur : %s\n", bold("crawl4ai"))
|
||||
u := crawl4aiURL()
|
||||
if u == "" {
|
||||
fmt.Printf(" serveur : %s — configure : ajean internet url <url>\n", dim("(non configuré)"))
|
||||
fmt.Printf(" serveur : %s — configure : loki internet url <url>\n", dim("(non configuré)"))
|
||||
return nil
|
||||
}
|
||||
reach := red("injoignable")
|
||||
@@ -368,7 +368,7 @@ func cmdInternet(args []string) error {
|
||||
fmt.Printf(" serveur : %s (%s)\n", bold(u), reach)
|
||||
fmt.Printf(" outils : web_search, web_open, web_read, web_grep\n")
|
||||
default:
|
||||
return fmt.Errorf("usage: ajean internet [on|off|status|engine <go|crawl4ai>|url <url>|key <clé>]")
|
||||
return fmt.Errorf("usage: loki internet [on|off|status|engine <go|crawl4ai>|url <url>|key <clé>]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// La mémoire de ajean = des fichiers Markdown plats sous memory/<nom>.md.
|
||||
// La mémoire de loki = des fichiers Markdown plats sous memory/<nom>.md.
|
||||
// L'IA y range ce qu'elle veut retenir entre les sessions (préférences,
|
||||
// décisions, procédures, infos projet). Quatre outils : mem_search, mem_read,
|
||||
// mem_add, mem_edit.
|
||||
@@ -331,7 +331,7 @@ func setMemMode(m MemMode) error {
|
||||
return SetConfigKey("MEM_MODE", string(m))
|
||||
}
|
||||
|
||||
// cmdMemory : ajean memory [off|ondemand|always|status]
|
||||
// cmdMemory : loki memory [off|ondemand|always|status]
|
||||
func cmdMemory(args []string) error {
|
||||
sub := ""
|
||||
if len(args) > 0 {
|
||||
@@ -362,7 +362,7 @@ func cmdMemory(args []string) error {
|
||||
fmt.Printf(" %d page(s) sous %s\n", len(pages), memoryDir())
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("usage: ajean memory [off|ondemand|always|status]")
|
||||
return fmt.Errorf("usage: loki memory [off|ondemand|always|status]")
|
||||
}
|
||||
m := memMode()
|
||||
fmt.Printf("%s mémoire : %s — %s\n", green("[ok]"), bold(string(m)), label[m])
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -41,9 +41,9 @@ func baseSystemPrompt(caps Caps) string {
|
||||
// sur-raisonner les modèles à reasoning (Qwen3) : ils émettent leur <think>
|
||||
// puis le token de fin SANS appeler d'outil (~25-45 % de tours « morts »
|
||||
// mesurés). Une version courte et directe ramène ça à 0 %. NE PAS regonfler.
|
||||
// « AJEAN » avec une majuscule : c'est un nom propre, et le modèle recopie
|
||||
// littéralement la casse d'ici quand il se présente (« je suis ajean »).
|
||||
b.WriteString("You are AJEAN, an expert assistant operating directly on this machine with real tools.")
|
||||
// « Loki » avec une majuscule : c'est un nom propre, et le modèle recopie
|
||||
// littéralement la casse d'ici quand il se présente (« je suis loki »).
|
||||
b.WriteString("You are Loki, an expert assistant operating directly on this machine with real tools.")
|
||||
if caps.Mem == MemAlways {
|
||||
b.WriteString(" You evolve with every conversation: you actively maintain a persistent memory so nothing useful is lost between sessions.")
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func machineSystemPrompt(caps Caps) string {
|
||||
}
|
||||
b.WriteString(".")
|
||||
if cwd != "" {
|
||||
b.WriteString(" Relative paths in write/edit/bash resolve inside this working folder — put scratch files there. Write outside it ONLY with an absolute path the user explicitly asked for; never scatter files into the folder ajean was launched from.")
|
||||
b.WriteString(" Relative paths in write/edit/bash resolve inside this working folder — put scratch files there. Write outside it ONLY with an absolute path the user explicitly asked for; never scatter files into the folder loki was launched from.")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func runShell(parent context.Context, command string, timeoutSec int) string {
|
||||
ctx, cancel := context.WithTimeout(parent, time.Duration(timeoutSec)*time.Second)
|
||||
defer cancel()
|
||||
cmd := newShellCmd(ctx, command)
|
||||
// Le shell démarre dans le workspace, pas dans le dossier d'où ajean a été
|
||||
// Le shell démarre dans le workspace, pas dans le dossier d'où loki a été
|
||||
// lancé : un `> notes.txt` du modèle ne doit pas atterrir sur le Bureau.
|
||||
//
|
||||
// Le dossier est résolu UNE fois par process (agentWorkspace), donc s'il
|
||||
@@ -208,7 +208,7 @@ func runShell(parent context.Context, command string, timeoutSec int) string {
|
||||
// commande du genre « ./serveur & » rend la main tout de suite mais laisse
|
||||
// un process en arrière-plan accroché aux tubes : runShell ne revenait alors
|
||||
// JAMAIS, ni au délai, ni au stop. Le tour restait bloqué à vie, et la seule
|
||||
// issue connue était de redémarrer ajean-ui.
|
||||
// issue connue était de redémarrer loki-ui.
|
||||
cmd.WaitDelay = 2 * time.Second
|
||||
err := cmd.Run()
|
||||
switch {
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -11,14 +11,14 @@ import (
|
||||
//
|
||||
// Sans ça, les outils write/edit/bash héritent du répertoire courant du PROCESSUS :
|
||||
// le modèle écrit "meteo.json", et le fichier atterrit là d'où l'utilisateur a
|
||||
// lancé ajean — le Bureau quand on double-clique l'installateur, C:\ProgramData\ajean\bin
|
||||
// lancé loki — le Bureau quand on double-clique l'installateur, C:\ProgramData\loki\bin
|
||||
// quand on lance le binaire installé. Personne ne s'attend à voir un chat déposer
|
||||
// des fichiers sur son Bureau. On donne donc à l'agent un dossier à lui : les
|
||||
// chemins relatifs y sont résolus, et le shell y démarre. Les chemins ABSOLUS
|
||||
// restent honorés tels quels — quand l'utilisateur demande d'écrire dans un
|
||||
// dossier précis, ça doit marcher.
|
||||
|
||||
const workspaceEnv = "AJEAN_WORKSPACE"
|
||||
const workspaceEnv = "LOKI_WORKSPACE"
|
||||
|
||||
var (
|
||||
workspaceOnce sync.Once
|
||||
@@ -26,7 +26,7 @@ var (
|
||||
)
|
||||
|
||||
// agentWorkspace renvoie le dossier de travail de l'agent, créé au besoin. Il
|
||||
// essaie plusieurs emplacements car AjeanHome() vaut %ProgramData%\ajean sous
|
||||
// essaie plusieurs emplacements car LokiHome() vaut %ProgramData%\loki sous
|
||||
// Windows, qui n'est pas inscriptible par un utilisateur non administrateur.
|
||||
func agentWorkspace() string {
|
||||
workspaceOnce.Do(func() {
|
||||
@@ -62,9 +62,9 @@ func workspaceCandidates() []string {
|
||||
}
|
||||
c = append(c, workspaceDir())
|
||||
if home, err := os.UserHomeDir(); err == nil && home != "" {
|
||||
c = append(c, filepath.Join(home, "ajean", "workspace"))
|
||||
c = append(c, filepath.Join(home, "loki", "workspace"))
|
||||
}
|
||||
c = append(c, filepath.Join(os.TempDir(), "ajean-workspace"))
|
||||
c = append(c, filepath.Join(os.TempDir(), "loki-workspace"))
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// Un chemin relatif du modèle doit atterrir dans le workspace, jamais dans le
|
||||
// répertoire courant du processus (Bureau, C:\ProgramData\ajean\bin…).
|
||||
// répertoire courant du processus (Bureau, C:\ProgramData\loki\bin…).
|
||||
func TestResolveAgentPathRelative(t *testing.T) {
|
||||
got := resolveAgentPath("meteo.json")
|
||||
want := filepath.Join(agentWorkspace(), "meteo.json")
|
||||
@@ -1,12 +1,12 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// cli_app.go — expérience « application » de AJEAN.
|
||||
// cli_app.go — expérience « application » de Loki.
|
||||
//
|
||||
// Quand on double-clique sur le binaire (aucun argument, console fraîche), au
|
||||
// lieu d'afficher l'aide dans une console qui se ferme, AJEAN démarre son UI web,
|
||||
// lieu d'afficher l'aide dans une console qui se ferme, Loki démarre son UI web,
|
||||
// l'ouvre dans le navigateur ET pose une icône dans la zone de notification
|
||||
// Windows (voir sys_tray_windows.go) : on voit que AJEAN tourne et on le pilote
|
||||
// (« Ouvrir AJEAN » / « Quitter »).
|
||||
// Windows (voir sys_tray_windows.go) : on voit que Loki tourne et on le pilote
|
||||
// (« Ouvrir Loki » / « Quitter »).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -34,13 +34,13 @@ func cmdApp(args []string) error {
|
||||
addr := fmt.Sprintf("0.0.0.0:%d", appPort)
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
// AJEAN tourne déjà sur ce port : on ouvre juste l'UI sur l'instance
|
||||
// Loki tourne déjà sur ce port : on ouvre juste l'UI sur l'instance
|
||||
// existante plutôt que d'échouer.
|
||||
fmt.Printf("AJEAN est déjà lancé — ouverture de %s\n", url)
|
||||
fmt.Printf("Loki est déjà lancé — ouverture de %s\n", url)
|
||||
return openBrowser(url)
|
||||
}
|
||||
|
||||
// UN SEUL process propriétaire de la conversation, comme le service ajean-ui
|
||||
// UN SEUL process propriétaire de la conversation, comme le service loki-ui
|
||||
// sous Linux : l'app sert l'UI locale ET le tunnel avec le MÊME mux. Deux
|
||||
// process qui servent la conversation (objet en mémoire persisté dans
|
||||
// conversation.json) donnent deux fils divergents entre le local et
|
||||
@@ -60,7 +60,7 @@ func cmdApp(args []string) error {
|
||||
startAppLink(appWebMux)
|
||||
}()
|
||||
|
||||
sp := showSplash("Lancement d'AJEAN en cours…")
|
||||
sp := showSplash("Lancement d'Loki en cours…")
|
||||
waitServerReady(url)
|
||||
_ = openBrowser(url)
|
||||
time.Sleep(900 * time.Millisecond) // laisse le navigateur s'afficher par-dessus le splash
|
||||
@@ -1,8 +1,8 @@
|
||||
// AJEAN — cœur du binaire (package ajean, appelé par cmd/ajean). Go n'autorise
|
||||
// Loki — cœur du binaire (package loki, appelé par cmd/loki). Go n'autorise
|
||||
// pas de sous-dossiers dans un même package : les fichiers sont donc préfixés
|
||||
// par domaine.
|
||||
//
|
||||
// run.go point d'entrée : dispatch des sous-commandes, AjeanHome() et
|
||||
// run.go point d'entrée : dispatch des sous-commandes, LokiHome() et
|
||||
// l'arborescence de données
|
||||
// store.go LA base (bbolt) : configuration, préférences, conversation,
|
||||
// clés, jetons, interrupteurs — tout l'état non éditable à la main
|
||||
@@ -28,11 +28,11 @@
|
||||
// supprimer un symbole jugé inutilisé, vérifier `grep` sur les fichiers _darwin.
|
||||
//
|
||||
// DEUX SERVICES à l'exécution, un seul binaire :
|
||||
// - ajean-engine (« ajean serve ») exec llama-server ;
|
||||
// - ajean-ui (« ajean web ») sert l'UI locale, le tunnel du relais et
|
||||
// - loki-engine (« loki serve ») exec llama-server ;
|
||||
// - loki-ui (« loki web ») sert l'UI locale, le tunnel du relais et
|
||||
// l'endpoint OpenAI depuis un SEUL process — condition d'une conversation
|
||||
// unique, celle-ci vivant en mémoire (voir chat_conversation.go).
|
||||
//
|
||||
// ui/index.html est GÉNÉRÉ depuis ui/src/ : éditer les sources puis
|
||||
// `go generate ./internal/ajean` (voir tools/assemble-ui).
|
||||
package ajean
|
||||
// `go generate ./internal/loki` (voir tools/assemble-ui).
|
||||
package loki
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -70,7 +70,7 @@ func runBench(nPrompt, nPredict int) (*benchResult, error) {
|
||||
// Use the same endpoint your real chat hits, so the comparison is honest
|
||||
// (chat template, reasoning, OpenAI-compat layer all included).
|
||||
payload := map[string]any{
|
||||
"model": "ajean",
|
||||
"model": "loki",
|
||||
"messages": []Message{{Role: "user", Content: prompt + "\n\nContinue this passage with another 1000+ words of original varied prose, mixing French and English narrative paragraphs on different topics."}},
|
||||
"max_tokens": nPredict,
|
||||
"stream": false,
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -575,7 +575,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
// le contexte — c'est un choix assumé.
|
||||
for iter := 0; ; iter++ {
|
||||
payload := map[string]any{
|
||||
"model": "ajean",
|
||||
"model": "loki",
|
||||
"messages": messages,
|
||||
"stream": true,
|
||||
"temperature": temperature,
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -83,12 +83,12 @@ func setOAIPublic(on bool) error { return putBool(bkState, "oai_public", on) }
|
||||
// oaiTLSConfig renvoie une config TLS qui, à la demande, obtient/renouvelle via
|
||||
// Let's Encrypt (TLS-ALPN-01) le certificat de tout nom en *.oai.ajean.link, et
|
||||
// répond elle-même aux challenges ACME. La clé privée est stockée dans
|
||||
// $AJEAN_HOME/certs et ne quitte jamais la machine. Toujours construite ; c'est le
|
||||
// $LOKI_HOME/certs et ne quitte jamais la machine. Toujours construite ; c'est le
|
||||
// démux (oaiPublicEnabled, lu en direct) qui décide de router ou non le trafic.
|
||||
func oaiTLSConfig() *tls.Config {
|
||||
certmagic.Default.Storage = &certmagic.FileStorage{Path: filepath.Join(AjeanHome(), "certs")}
|
||||
certmagic.Default.Storage = &certmagic.FileStorage{Path: filepath.Join(LokiHome(), "certs")}
|
||||
certmagic.DefaultACME.Agreed = true
|
||||
certmagic.DefaultACME.Email = os.Getenv("AJEAN_ACME_EMAIL")
|
||||
certmagic.DefaultACME.Email = os.Getenv("LOKI_ACME_EMAIL")
|
||||
certmagic.DefaultACME.DisableHTTPChallenge = true // pas de :80 accessible (CGNAT) → TLS-ALPN uniquement
|
||||
magic := certmagic.NewDefault()
|
||||
magic.OnDemand = &certmagic.OnDemandConfig{
|
||||
@@ -206,7 +206,7 @@ func selfSignedTLSConfig(host string) (*tls.Config, error) {
|
||||
|
||||
// cmdOAI pilote l'accès OpenAI public côté agent.
|
||||
//
|
||||
// ajean oai serve [port] [host] (test local) termine le TLS sur :port avec un
|
||||
// loki oai serve [port] [host] (test local) termine le TLS sur :port avec un
|
||||
// cert auto-signé et proxifie vers llama /v1.
|
||||
func cmdOAI(args []string) error {
|
||||
sub := ""
|
||||
@@ -236,12 +236,12 @@ func cmdOAI(args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("[ajean oai] front TLS (test, auto-signé) https://%s:%d/v1 → llama :%d\n", host, port, LLMPort())
|
||||
fmt.Printf("[loki oai] front TLS (test, auto-signé) https://%s:%d/v1 → llama :%d\n", host, port, LLMPort())
|
||||
return runOAIFront(ln, tlsCfg)
|
||||
default:
|
||||
fmt.Println("usage: ajean oai serve [port] [host] (front TLS de test → llama /v1)")
|
||||
fmt.Println(" en prod, le front TLS est servi automatiquement dans le tunnel (ajean link)")
|
||||
fmt.Println(" quand AJEAN_LINK_ALLOW_OAI=1 ; cert Let's Encrypt via TLS-ALPN-01.")
|
||||
fmt.Println("usage: loki oai serve [port] [host] (front TLS de test → llama /v1)")
|
||||
fmt.Println(" en prod, le front TLS est servi automatiquement dans le tunnel (loki link)")
|
||||
fmt.Println(" quand LOKI_LINK_ALLOW_OAI=1 ; cert Let's Encrypt via TLS-ALPN-01.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -14,7 +14,7 @@ func cmdTest(args []string) error {
|
||||
fmt.Printf("→ GET http://localhost:%d/health … ", port)
|
||||
if !healthCheck() {
|
||||
fmt.Println(red("ko"))
|
||||
return fmt.Errorf("/health ne répond pas — ajean start d'abord")
|
||||
return fmt.Errorf("/health ne répond pas — loki start d'abord")
|
||||
}
|
||||
fmt.Println(green("ok"))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -127,7 +127,7 @@ func (h headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
|
||||
// connect établit une session pour la config donnée.
|
||||
func mcpConnect(ctx context.Context, name string, cfg MCPServerConfig) (*mcpsdk.ClientSession, error) {
|
||||
client := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "ajean", Version: Version}, nil)
|
||||
client := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "loki", Version: Version}, nil)
|
||||
|
||||
var transport mcpsdk.Transport
|
||||
switch cfg.Transport() {
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
@@ -17,7 +17,7 @@ func TestMCPEnsureDeduplicatesConcurrentConnects(t *testing.T) {
|
||||
|
||||
// Commande inexistante : la connexion échoue, mais l'entrée du pool est bien
|
||||
// partagée — c'est ce qu'on teste.
|
||||
cfg := MCPServerConfig{Command: "ajean-binaire-inexistant-pour-test", Enabled: true}
|
||||
cfg := MCPServerConfig{Command: "loki-binaire-inexistant-pour-test", Enabled: true}
|
||||
|
||||
const callers = 8
|
||||
var wg sync.WaitGroup
|
||||
@@ -83,8 +83,8 @@ func TestMCPEndToEnd(t *testing.T) {
|
||||
t.Fatalf("outil %s introuvable dans %d outils", echoName, len(tools))
|
||||
}
|
||||
|
||||
out := mcpCall(echoName, map[string]any{"message": "bonjour-ajean"})
|
||||
if !strings.Contains(out, "bonjour-ajean") {
|
||||
out := mcpCall(echoName, map[string]any{"message": "bonjour-loki"})
|
||||
if !strings.Contains(out, "bonjour-loki") {
|
||||
t.Fatalf("echo n'a pas renvoyé le message ; got: %q", out)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -10,14 +10,14 @@ import (
|
||||
|
||||
// Configuration des serveurs MCP (Model Context Protocol).
|
||||
//
|
||||
// ajean peut se connecter à des serveurs MCP tiers pour enrichir la palette
|
||||
// loki peut se connecter à des serveurs MCP tiers pour enrichir la palette
|
||||
// d'outils de l'IA (accès fichiers rapide type desktop-commander, bases de
|
||||
// données, APIs métier…). Deux transports :
|
||||
//
|
||||
// - stdio : ajean lance un process local (command + args) et parle en
|
||||
// - stdio : loki lance un process local (command + args) et parle en
|
||||
// JSON-RPC sur stdin/stdout. C'est le cas de desktop-commander, du serveur
|
||||
// filesystem officiel, etc. — lancés via npx/uvx/un binaire.
|
||||
// - http : ajean parle à un serveur MCP distant en Streamable HTTP (url +
|
||||
// - http : loki parle à un serveur MCP distant en Streamable HTTP (url +
|
||||
// éventuels en-têtes d'auth).
|
||||
//
|
||||
// Le format du fichier mcp.json reprend celui de Claude Desktop (clé
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
// leurs configs existantes. On ajoute un champ "enabled" par serveur.
|
||||
//
|
||||
// IMPORTANT (sécurité) : un serveur MCP stdio exécute un process arbitraire sur
|
||||
// la machine où tourne ajean — même niveau de confiance que l'outil `bash` du
|
||||
// la machine où tourne loki — même niveau de confiance que l'outil `bash` du
|
||||
// mode agent. La configuration MCP est donc réservée au propriétaire local de la
|
||||
// machine et ne doit JAMAIS être pilotable depuis le relais/accès distant.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// migrate_07.go — CODE TEMPORAIRE, à supprimer d'ici une version ou deux.
|
||||
//
|
||||
// Reprend une installation 0.7.x au moment de `ajean install`. Tout est ici, et
|
||||
// Reprend une installation 0.7.x au moment de `loki install`. Tout est ici, et
|
||||
// rien d'autre n'en dépend : le jour où le parc aura basculé, supprimer ce
|
||||
// fichier et les deux appels à migrateFrom07() (sys_install_unix.go et
|
||||
// sys_install_windows.go) suffit. C'est la seule raison d'être de ce fichier —
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -38,7 +38,7 @@ func fake07Home(t *testing.T) string {
|
||||
write(".agent_enabled", "")
|
||||
write("webprefs.json", `{"theme":"dark"}`)
|
||||
write("mcp.json", `{"mcpServers":{"fs":{"command":"npx","enabled":true}}}`)
|
||||
write("sysprompt.txt", "Tu es AJEAN.\n")
|
||||
write("sysprompt.txt", "Tu es Loki.\n")
|
||||
write("conversation.json", `{"seq":42,"messages":[{"role":"user","content":"salut"}]}`)
|
||||
write("SKILLS/vieux-skill/SKILL.md", "# un skill d'avant\n")
|
||||
write(".authorized_users", "AABBCCDDEEFF00112233445566778899AABBCCDDEEFF001122334455667788AA\n")
|
||||
@@ -90,7 +90,7 @@ func TestMigration07(t *testing.T) {
|
||||
if !agentEnabled() {
|
||||
t.Error("mode agent perdu")
|
||||
}
|
||||
if readSysPrompt() != "Tu es AJEAN." {
|
||||
if readSysPrompt() != "Tu es Loki." {
|
||||
t.Errorf("prompt système = %q", readSysPrompt())
|
||||
}
|
||||
if loadWebPrefs()["theme"] != "dark" {
|
||||
@@ -6,7 +6,7 @@
|
||||
// - ENRÔLEMENT (public) : le poste échange un code d'appairage à usage unique
|
||||
// contre sa clé d'appareil. Authentifié par le code, pas par la clé de
|
||||
// pilotage — que le poste ne possède pas.
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
@@ -1,16 +1,16 @@
|
||||
// node_proto.go — côté serveur, alias vers le protocole partagé (internal/nodewire)
|
||||
// + les helpers de nommage d'outils propres à ajean.
|
||||
// + les helpers de nommage d'outils propres à loki.
|
||||
//
|
||||
// Le format du fil (Msg, capacités, confinement de chemin) vit dans nodewire,
|
||||
// paquet SANS dépendance, pour que le client léger ajean-node le partage sans
|
||||
// tirer tout ajean. Ici on ne fait qu'exposer ces symboles sous les noms
|
||||
// paquet SANS dépendance, pour que le client léger loki-node le partage sans
|
||||
// tirer tout loki. Ici on ne fait qu'exposer ces symboles sous les noms
|
||||
// historiques du paquet, pour ne pas toucher le reste du code serveur.
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/nathaninline/ajean/internal/nodewire"
|
||||
"github.com/R0m1k3/Loki/internal/nodewire"
|
||||
)
|
||||
|
||||
// Capacités (alias des constantes partagées).
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
@@ -2,7 +2,7 @@
|
||||
// WebSocket, registre des postes connectés, et exposition de leurs outils à
|
||||
// l'agent. Le serveur n'exécute JAMAIS rien lui-même ici : il route une demande
|
||||
// d'outil vers le poste concerné et attend sa réponse.
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
"github.com/nathaninline/ajean/internal/nodewire"
|
||||
"github.com/R0m1k3/Loki/internal/nodewire"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
"github.com/nathaninline/ajean/internal/nodewire"
|
||||
"github.com/R0m1k3/Loki/internal/nodewire"
|
||||
)
|
||||
|
||||
// TestNodeE2E exerce tout le chemin : enrôlement SCELLÉ (le sceau du client est
|
||||
@@ -1,11 +1,11 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// relay_e2e.go — chiffrement bout-en-bout du chat entre le NAVIGATEUR et CET agent, de
|
||||
// sorte que le relais (ajean.link) ne voie que de l'opaque : « boîte noire ».
|
||||
//
|
||||
// Modèle : cet agent a une paire X25519 long-terme (clé privée locale, jamais
|
||||
// transmise). Sa clé publique est publiée au relais et son EMPREINTE est affichée
|
||||
// au `ajean link` — l'utilisateur la confirme dans le portail, ce qui défait tout
|
||||
// au `loki link` — l'utilisateur la confirme dans le portail, ce qui défait tout
|
||||
// MITM du relais. Le navigateur dérive une racine R de son mot de passe (exportKey
|
||||
// OPAQUE), la SCELLE vers la clé publique de l'agent (le relais ne peut pas
|
||||
// l'ouvrir), puis chiffre/déchiffre le chat avec une clé dérivée de R.
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
func e2eKeyPath() string { return filepath.Join(AjeanHome(), ".e2e_key") }
|
||||
func e2eKeyPath() string { return filepath.Join(LokiHome(), ".e2e_key") }
|
||||
|
||||
var (
|
||||
e2eOnce sync.Once
|
||||
@@ -55,7 +55,7 @@ func e2ePrivateKey() (*ecdh.PrivateKey, error) {
|
||||
e2eErr = err
|
||||
return
|
||||
}
|
||||
_ = os.MkdirAll(AjeanHome(), 0o755)
|
||||
_ = os.MkdirAll(LokiHome(), 0o755)
|
||||
if err := os.WriteFile(e2eKeyPath(), []byte(hex.EncodeToString(k.Bytes())+"\n"), 0o600); err != nil {
|
||||
e2eErr = err
|
||||
return
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// relay_e2eauth.go — authentification MUTUELLE + anti-rejeu du canal chiffré.
|
||||
//
|
||||
@@ -9,7 +9,7 @@ package ajean
|
||||
// Modèle : l'utilisateur a une identité X25519 dérivée de façon déterministe de son
|
||||
// mot de passe (exportKey OPAQUE → racine R → uPriv) — le relais ne connaît pas R,
|
||||
// donc ne peut PAS reproduire cette identité. La clé publique uPub est APPAIRÉE une
|
||||
// fois à l'agent via un code affiché par « ajean link » (canal hors-bande : le log du
|
||||
// fois à l'agent via un code affiché par « loki link » (canal hors-bande : le log du
|
||||
// serveur, que le relais ne voit pas ; le code voyage scellé vers l'agent). Ensuite,
|
||||
// chaque requête est chiffrée avec K = SHA256(ECDH(uPriv, agentPriv) || "authchan"),
|
||||
// liée à uPub+horodatage (AAD) et protégée contre le rejeu. Seul le vrai utilisateur
|
||||
@@ -92,7 +92,7 @@ func authorizeUser(uPubHex string) error {
|
||||
|
||||
// ---- Codes d'appairage : à la demande, usage unique, TTL 10 min --------------
|
||||
//
|
||||
// Générés par « ajean link code » (ou affichés par « ajean link »), ils sont
|
||||
// Générés par « loki link code » (ou affichés par « loki link »), ils sont
|
||||
// partagés avec le service d'interface (autre process) via la base — sinon les
|
||||
// deux ne s'accorderaient pas. Stockés HACHÉS (SHA-256) : la base ne révèle
|
||||
// aucun code. Chaque code expire au bout de 10 min et est consommé (retiré) au
|
||||
@@ -232,7 +232,7 @@ func replayCheck(uPub string, ts int64, iv string) bool {
|
||||
// ---- Dérivation de clé + ouverture authentifiée ----------------------------
|
||||
|
||||
// e2eAuthKey dérive la clé du canal authentifié pour l'utilisateur uPub :
|
||||
// SHA256( ECDH(agentPriv, uPub) || "ajean-authchan-v1" ). Symétrique de la version
|
||||
// SHA256( ECDH(agentPriv, uPub) || "loki-authchan-v1" ). Symétrique de la version
|
||||
// navigateur (WASM) qui calcule ECDH(uPriv, agentPub) = même secret partagé.
|
||||
func e2eAuthKey(uPubHex string) ([]byte, error) {
|
||||
priv, err := e2ePrivateKey()
|
||||
@@ -253,7 +253,7 @@ func e2eAuthKey(uPubHex string) ([]byte, error) {
|
||||
}
|
||||
h := sha256.New()
|
||||
h.Write(ss)
|
||||
h.Write([]byte("ajean-authchan-v1"))
|
||||
h.Write([]byte("loki-authchan-v1"))
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -1,16 +1,16 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// relay_link.go — `ajean link <token>` : connecte ce serveur AJEAN au relais public
|
||||
// relay_link.go — `loki link <token>` : connecte ce serveur Loki au relais public
|
||||
// (ajean.link) par une connexion SORTANTE persistante, pour qu'un utilisateur
|
||||
// y accède depuis n'importe où sans ouvrir de port (CGNAT, box, etc.).
|
||||
//
|
||||
// Principe : l'agent ouvre un WebSocket vers le relais, l'authentifie avec le
|
||||
// token d'abonnement, puis multiplexe (yamux) ce lien unique en un stream par
|
||||
// requête navigateur. Chaque stream est reverse-proxyfié vers le `ajean web`
|
||||
// requête navigateur. Chaque stream est reverse-proxyfié vers le `loki web`
|
||||
// local. Keepalive + reconnexion automatique avec backoff.
|
||||
//
|
||||
// Le token est fourni par la boutique à l'achat. Il est mémorisé dans
|
||||
// $AJEAN_HOME/.link_token pour que `ajean link` (sans argument) reprenne la
|
||||
// $LOKI_HOME/.link_token pour que `loki link` (sans argument) reprenne la
|
||||
// connexion.
|
||||
|
||||
import (
|
||||
@@ -39,7 +39,7 @@ import (
|
||||
)
|
||||
|
||||
// defaultRelayURL est l'endpoint WebSocket du relais. Surchageable via
|
||||
// $AJEAN_LINK_URL (utile pour tester contre un relais local).
|
||||
// $LOKI_LINK_URL (utile pour tester contre un relais local).
|
||||
const defaultRelayURL = "wss://ajean.link/agent"
|
||||
|
||||
// machineID renvoie un identifiant stable de la machine, créé au premier appel.
|
||||
@@ -65,20 +65,20 @@ func removeLinkToken() error { return putStr(bkState, "link_token", "") }
|
||||
|
||||
// relayURL resolves the relay WebSocket endpoint (env override → default).
|
||||
func relayURL() string {
|
||||
if u := os.Getenv("AJEAN_LINK_URL"); u != "" {
|
||||
if u := os.Getenv("LOKI_LINK_URL"); u != "" {
|
||||
return u
|
||||
}
|
||||
return defaultRelayURL
|
||||
}
|
||||
|
||||
// uiUnitName est l'unité qui exécute « ajean web » : l'UI locale, le tunnel du
|
||||
// uiUnitName est l'unité qui exécute « loki web » : l'UI locale, le tunnel du
|
||||
// relais et l'endpoint OpenAI, dans un seul et même process.
|
||||
const uiUnitName = "ajean-ui"
|
||||
const uiUnitName = "loki-ui"
|
||||
|
||||
func uiServiceName() string { return uiUnitName }
|
||||
|
||||
// cmdLink ne gère QUE le compte d'accès distant : le jeton, l'appairage, l'état.
|
||||
// Le tunnel n'est plus un service à part — il est ouvert par « ajean web » dès
|
||||
// Le tunnel n'est plus un service à part — il est ouvert par « loki web » dès
|
||||
// qu'un jeton est enregistré. Piloter le tunnel, c'est donc piloter le service
|
||||
// d'interface (voir cmdUI).
|
||||
func cmdLink(args []string) error {
|
||||
@@ -88,33 +88,33 @@ func cmdLink(args []string) error {
|
||||
}
|
||||
switch sub {
|
||||
case "":
|
||||
// `ajean link` seul : afficher l'aide (NE démarre rien — éviter de prendre
|
||||
// `loki link` seul : afficher l'aide (NE démarre rien — éviter de prendre
|
||||
// un mot pour un jeton et d'écraser le vrai).
|
||||
printLinkHelp()
|
||||
return nil
|
||||
case "status":
|
||||
tok := readLinkToken()
|
||||
if tok == "" {
|
||||
fmt.Println(yellow("[info]") + " aucun jeton enregistré — lance: ajean link <token>")
|
||||
fmt.Println(yellow("[info]") + " aucun jeton enregistré — lance: loki link <token>")
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("%s jeton enregistré (%s…), relais: %s\n", green("[ok]"), tok[:min(8, len(tok))], relayURL())
|
||||
if uiServiceActive() {
|
||||
fmt.Printf("%s service %s actif — le tunnel est ouvert\n", green("[ok]"), uiServiceName())
|
||||
} else {
|
||||
fmt.Printf("%s service %s arrêté — pas de tunnel (ajean ui start)\n", yellow("[info]"), uiServiceName())
|
||||
fmt.Printf("%s service %s arrêté — pas de tunnel (loki ui start)\n", yellow("[info]"), uiServiceName())
|
||||
}
|
||||
return nil
|
||||
case "logout":
|
||||
if err := removeLinkToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(green("[ok]") + " jeton supprimé — « ajean ui restart » pour fermer le tunnel")
|
||||
fmt.Println(green("[ok]") + " jeton supprimé — « loki ui restart » pour fermer le tunnel")
|
||||
return nil
|
||||
case "code":
|
||||
code, err := newPairCode()
|
||||
if err != nil {
|
||||
return fmt.Errorf("génération du code (droits sur %s ?): %w", AjeanHome(), err)
|
||||
return fmt.Errorf("génération du code (droits sur %s ?): %w", LokiHome(), err)
|
||||
}
|
||||
fmt.Printf("%s code d'appairage (valable 10 min, à usage unique) :\n %s\n", green("[link]"), bold(code))
|
||||
return nil
|
||||
@@ -138,7 +138,7 @@ func cmdLink(args []string) error {
|
||||
return linkPrintIdentity()
|
||||
}
|
||||
|
||||
// cmdUI pilote le service d'interface — « ajean web » en arrière-plan : l'UI
|
||||
// cmdUI pilote le service d'interface — « loki web » en arrière-plan : l'UI
|
||||
// locale, le tunnel du relais et l'endpoint OpenAI, servis par le MÊME process.
|
||||
// C'est ce qui garantit une conversation unique, identique en local et à distance.
|
||||
func cmdUI(args []string) error {
|
||||
@@ -160,22 +160,22 @@ func cmdUI(args []string) error {
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("usage: ajean ui [start|stop|restart|status]")
|
||||
return fmt.Errorf("usage: loki ui [start|stop|restart|status]")
|
||||
}
|
||||
}
|
||||
|
||||
// printLinkHelp liste les sous-commandes de `ajean link`.
|
||||
// printLinkHelp liste les sous-commandes de `loki link`.
|
||||
func printLinkHelp() {
|
||||
fmt.Print(`ajean link — accès distant via le relais ajean.link
|
||||
fmt.Print(`loki link — accès distant via le relais ajean.link
|
||||
|
||||
Usage :
|
||||
ajean link <token> enregistre le jeton (1re fois / pour le changer) et ouvre le tunnel
|
||||
ajean link status état du jeton et du tunnel
|
||||
ajean link code génère un code d'appairage (valable 10 min, à usage unique)
|
||||
ajean link logout oublie le jeton enregistré
|
||||
loki link <token> enregistre le jeton (1re fois / pour le changer) et ouvre le tunnel
|
||||
loki link status état du jeton et du tunnel
|
||||
loki link code génère un code d'appairage (valable 10 min, à usage unique)
|
||||
loki link logout oublie le jeton enregistré
|
||||
|
||||
Le jeton est fourni sur ajean.link. Le tunnel est ouvert par le service
|
||||
d'interface (« ajean ui ») dès qu'un jeton est enregistré : il n'y a pas de
|
||||
d'interface (« loki ui ») dès qu'un jeton est enregistré : il n'y a pas de
|
||||
service séparé pour l'accès distant.
|
||||
`)
|
||||
}
|
||||
@@ -189,8 +189,8 @@ func linkPrintIdentity() error {
|
||||
}
|
||||
code, err := newPairCode()
|
||||
if err != nil {
|
||||
fmt.Printf("%s code d'appairage indisponible (droits sur %s ?): %v\n", yellow("[link]"), AjeanHome(), err)
|
||||
fmt.Printf(" Réessaie : sudo ajean link code\n")
|
||||
fmt.Printf("%s code d'appairage indisponible (droits sur %s ?): %v\n", yellow("[link]"), LokiHome(), err)
|
||||
fmt.Printf(" Réessaie : sudo loki link code\n")
|
||||
return nil
|
||||
}
|
||||
fmt.Printf(" Code d'appairage (valable 10 min, usage unique) : %s\n\n", bold(code))
|
||||
@@ -208,7 +208,7 @@ func linkPrintIdentity() error {
|
||||
//
|
||||
// Sur un poste de bureau, l'app EST ce process propriétaire : elle sert déjà
|
||||
// :8090, donc elle fait aussi tourner le tunnel elle-même au lieu de déléguer à
|
||||
// un worker détaché. L'accès distant vit donc aussi longtemps que AJEAN.app est
|
||||
// un worker détaché. L'accès distant vit donc aussi longtemps que Loki.app est
|
||||
// ouverte — sur un portable qui s'endort, c'est de toute façon la réalité.
|
||||
|
||||
var appLink struct {
|
||||
@@ -310,14 +310,14 @@ func killForeignUIWorker() {
|
||||
}
|
||||
|
||||
// uiPIDPath / uiLogPath : suivi du worker de lien hors systemd (macOS,
|
||||
// Windows), où AJEAN est une app de bureau lancée sans droits root.
|
||||
func uiPIDPath() string { return filepath.Join(AjeanHome(), uiUnitName+".pid") }
|
||||
func uiLogPath() string { return filepath.Join(AjeanHome(), uiUnitName+".log") }
|
||||
// Windows), où Loki est une app de bureau lancée sans droits root.
|
||||
func uiPIDPath() string { return filepath.Join(LokiHome(), uiUnitName+".pid") }
|
||||
func uiLogPath() string { return filepath.Join(LokiHome(), uiUnitName+".log") }
|
||||
|
||||
// uiServiceCtl pilote le worker de lien (start/stop/restart). Sous Linux c'est
|
||||
// l'unité systemd ajean-ui (avec sudo non interactif si on n'est pas root) ;
|
||||
// l'unité systemd loki-ui (avec sudo non interactif si on n'est pas root) ;
|
||||
// ailleurs — macOS et Windows, où il n'y a ni systemd ni droits root — on lance
|
||||
// « ajean link serve » en processus détaché suivi par un fichier PID, comme le
|
||||
// « loki link serve » en processus détaché suivi par un fichier PID, comme le
|
||||
// fait déjà le service principal. Sans ça, l'accès distant restait définitivement
|
||||
// « arrêté » sur un Mac ou un PC : le token était enregistré mais rien ne
|
||||
// composait jamais le tunnel.
|
||||
@@ -422,7 +422,7 @@ func uiUserSvcCtl(action string) error {
|
||||
if p, err := filepath.EvalSymlinks(self); err == nil {
|
||||
self = p
|
||||
}
|
||||
if err := os.MkdirAll(AjeanHome(), 0o755); err != nil {
|
||||
if err := os.MkdirAll(LokiHome(), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
logf, err := os.OpenFile(uiLogPath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
@@ -434,7 +434,7 @@ func uiUserSvcCtl(action string) error {
|
||||
cmd := spawnDetached(self, "web")
|
||||
cmd.Stdout, cmd.Stderr = logf, logf
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("démarrage de « ajean web »: %w", err)
|
||||
return fmt.Errorf("démarrage de « loki web »: %w", err)
|
||||
}
|
||||
pid := cmd.Process.Pid
|
||||
// PID + binaire d'origine : la 2e ligne dit QUELLE copie de l'app a lancé ce
|
||||
@@ -460,7 +460,7 @@ func uiUserSvcCtl(action string) error {
|
||||
// - /v1/*, /health, /props, /metrics, /slots → llama-server local (endpoint
|
||||
// compatible OpenAI, avec injection de la clé API locale) → permet de
|
||||
// brancher OpenCode, Hermes, etc. sur ajean.link/oai/<machine>/v1
|
||||
// - tout le reste → l'UI web de AJEAN (avec injection de la clé de pilotage)
|
||||
// - tout le reste → l'UI web de Loki (avec injection de la clé de pilotage)
|
||||
func newLinkHandler(mux *http.ServeMux) http.Handler {
|
||||
web := withLocalAuth(mux)
|
||||
llama := &url.URL{Scheme: "http", Host: fmt.Sprintf("127.0.0.1:%d", LLMPort())}
|
||||
@@ -484,7 +484,7 @@ func newLinkHandler(mux *http.ServeMux) http.Handler {
|
||||
// - /api/e2e/chat : chat chiffré (streaming SSE chiffré).
|
||||
// - /api/e2e/req : proxy de contrôle chiffré (presets, VRAM, skills, service…).
|
||||
// Tout autre /api/* en clair est REFUSÉ via le tunnel.
|
||||
oaiAllowed := os.Getenv("AJEAN_LINK_ALLOW_OAI") == "1"
|
||||
oaiAllowed := os.Getenv("LOKI_LINK_ALLOW_OAI") == "1"
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := r.URL.Path
|
||||
if strings.HasPrefix(p, "/api/e2e/") {
|
||||
@@ -514,7 +514,7 @@ func newLinkHandler(mux *http.ServeMux) http.Handler {
|
||||
// L'endpoint OpenAI (OpenCode/Hermes) ne peut pas être chiffré navigateur :
|
||||
// il transiterait en clair par le relais. Désactivé par défaut.
|
||||
if !oaiAllowed {
|
||||
http.Error(w, "endpoint OpenAI désactivé via le relais (transiterait en clair) ; AJEAN_LINK_ALLOW_OAI=1 pour l'autoriser", http.StatusForbidden)
|
||||
http.Error(w, "endpoint OpenAI désactivé via le relais (transiterait en clair) ; LOKI_LINK_ALLOW_OAI=1 pour l'autoriser", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
lp.ServeHTTP(w, r)
|
||||
@@ -531,7 +531,7 @@ func newLinkHandler(mux *http.ServeMux) http.Handler {
|
||||
//
|
||||
// ⚠️ La clé est relue À CHAQUE REQUÊTE, et surtout pas capturée une fois pour
|
||||
// toutes à l'ouverture du tunnel. Avec une capture, changer la clé (depuis
|
||||
// l'interface ou `ajean set-web-key`) faisait injecter l'ANCIENNE jusqu'au
|
||||
// l'interface ou `loki set-web-key`) faisait injecter l'ANCIENNE jusqu'au
|
||||
// prochain redémarrage du service : tout l'accès distant tombait en 401, et
|
||||
// comme le chat n'affiche pas le code HTTP d'un flux qui n'arrive jamais, le
|
||||
// symptôme était un « chargement de la conversation » infini, sans la moindre
|
||||
@@ -599,7 +599,7 @@ func runLinkSession(ctx context.Context, token string, handler http.Handler, oai
|
||||
fmt.Printf("%s lien établi ✓\n", green("[link]"))
|
||||
|
||||
// Chaque Accept() = un stream. Deux natures possibles :
|
||||
// - requête HTTP normale (UI AJEAN / E2E) → servie par `handler` ;
|
||||
// - requête HTTP normale (UI Loki / E2E) → servie par `handler` ;
|
||||
// - session TLS brute (accès OpenAI public) → terminée par le front TLS.
|
||||
// On les distingue au 1er octet (0x16 = handshake TLS). Sans OAI activé, tout
|
||||
// va au HTTP (comportement historique inchangé).
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
// Changer la clé de pilotage ne doit PAS couper l'accès distant jusqu'au
|
||||
// prochain redémarrage. La clé était capturée à l'ouverture du tunnel : après un
|
||||
// `ajean set-web-key`, le tunnel injectait l'ancienne, tout passait en 401, et
|
||||
// `loki set-web-key`, le tunnel injectait l'ancienne, tout passait en 401, et
|
||||
// le portail restait bloqué sur « chargement de la conversation » sans jamais
|
||||
// dire pourquoi.
|
||||
func TestWithLocalAuthSuitLaCleCourante(t *testing.T) {
|
||||
@@ -1,17 +1,17 @@
|
||||
// Sous-commande « ajean remote » — client « poste distant » intégré au binaire
|
||||
// principal. On l'installe sur un PC pour que l'IA d'un serveur AJEAN puisse agir
|
||||
// Sous-commande « loki remote » — client « poste distant » intégré au binaire
|
||||
// principal. On l'installe sur un PC pour que l'IA d'un serveur Loki puisse agir
|
||||
// dessus (shell/fichiers), chiffré de bout en bout via ajean.link (relais aveugle)
|
||||
// ou en direct sur le réseau local.
|
||||
//
|
||||
// ajean remote install <url-serveur> --machine ID --key CLÉ --code CODE [--allow shell,read,write,list] [--root DIR]
|
||||
// loki remote install <url-serveur> --machine ID --key CLÉ --code CODE [--allow shell,read,write,list] [--root DIR]
|
||||
// appaire ce PC et l'installe en service (démarrage auto)
|
||||
// ajean remote connect <url> ... idem mais en avant-plan (test)
|
||||
// ajean remote run boucle client (utilisé par le service)
|
||||
// ajean remote status | uninstall | logout
|
||||
// loki remote connect <url> ... idem mais en avant-plan (test)
|
||||
// loki remote run boucle client (utilisé par le service)
|
||||
// loki remote status | uninstall | logout
|
||||
//
|
||||
// Historiquement un binaire séparé « ajean-remote » ; fusionné dans ce binaire
|
||||
// Historiquement un binaire séparé « loki-remote » ; fusionné dans ce binaire
|
||||
// pour n'avoir qu'une seule application.
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/nathaninline/ajean/internal/nodeclient"
|
||||
"github.com/R0m1k3/Loki/internal/nodeclient"
|
||||
)
|
||||
|
||||
func cmdRemote(args []string) error {
|
||||
@@ -59,18 +59,18 @@ func cmdRemote(args []string) error {
|
||||
}
|
||||
|
||||
func remoteUsage() {
|
||||
fmt.Print(`ajean remote — piloter CE PC depuis l'IA d'un serveur AJEAN
|
||||
fmt.Print(`loki remote — piloter CE PC depuis l'IA d'un serveur Loki
|
||||
|
||||
ajean remote install <url-serveur> --code CODE [options] appaire + installe en service (démarrage auto, invisible)
|
||||
ajean remote connect <url-serveur> --code CODE [options] idem en avant-plan (pour tester)
|
||||
ajean remote status état de la configuration
|
||||
ajean remote uninstall retire le service (garde la config)
|
||||
ajean remote logout retire le service ET oublie la clé
|
||||
loki remote install <url-serveur> --code CODE [options] appaire + installe en service (démarrage auto, invisible)
|
||||
loki remote connect <url-serveur> --code CODE [options] idem en avant-plan (pour tester)
|
||||
loki remote status état de la configuration
|
||||
loki remote uninstall retire le service (garde la config)
|
||||
loki remote logout retire le service ET oublie la clé
|
||||
|
||||
Options :
|
||||
--machine ID machine à joindre via ajean.link (fournie par l'UI → Postes distants)
|
||||
--key CLÉ clé publique de l'agent (le poste scelle vers elle)
|
||||
--code CODE code d'appairage (10 min, généré dans l'UI AJEAN → Postes distants)
|
||||
--code CODE code d'appairage (10 min, généré dans l'UI Loki → Postes distants)
|
||||
--allow LISTE capacités acceptées : shell,read,write,list (défaut : ce que le serveur autorise)
|
||||
--root DIR dossier autorisé (confine lecture/écriture)
|
||||
--yes auto-approuve les actions (obligatoire pour le service, sans terminal)
|
||||
@@ -84,8 +84,8 @@ func remoteConnect(args []string, install bool) error {
|
||||
cfg, lerr := nodeclient.LoadConfig()
|
||||
// Un --code est un jeton d'appairage à usage unique : le fournir veut TOUJOURS
|
||||
// dire « (ré)appaire-moi maintenant ». Sans ce « || f.code != "" », une config
|
||||
// résiduelle (test précédent, ancien binaire ajean-remote au même emplacement
|
||||
// %ProgramData%\ajean-remote\config.json) faisait sauter l'appairage en silence :
|
||||
// résiduelle (test précédent, ancien binaire loki-remote au même emplacement
|
||||
// %ProgramData%\loki-remote\config.json) faisait sauter l'appairage en silence :
|
||||
// le service démarrait avec de vieilles clés que le serveur ignore et le poste
|
||||
// ne remontait jamais, alors que tout affichait « [ok] ».
|
||||
needEnroll := lerr != nil || cfg.Priv == "" || f.code != ""
|
||||
@@ -148,7 +148,7 @@ func remoteConnect(args []string, install bool) error {
|
||||
func remoteRun() error {
|
||||
cfg, err := nodeclient.LoadConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("aucune configuration — lance d'abord « ajean remote install … »")
|
||||
return fmt.Errorf("aucune configuration — lance d'abord « loki remote install … »")
|
||||
}
|
||||
return nodeclient.RunService(cfg)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// ajean — single-binary LLM server manager + web UI for llama.cpp deployments.
|
||||
// Point d'entrée réel : Main(), appelé par cmd/ajean (les métadonnées Windows
|
||||
// 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 ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
const Version = "0.9.4"
|
||||
|
||||
// Main est le vrai main() du binaire (cmd/ajean ne fait que l'appeler).
|
||||
// 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) →
|
||||
@@ -34,7 +34,7 @@ func Main() {
|
||||
// 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,
|
||||
// `ajean` sans argument garde son comportement d'aide.
|
||||
// `loki` sans argument garde son comportement d'aide.
|
||||
if noArgs && !haveConsole {
|
||||
mustExit(cmdApp(args))
|
||||
return
|
||||
@@ -107,7 +107,7 @@ func Main() {
|
||||
case "uninstall":
|
||||
mustExit(cmdUninstall(args))
|
||||
case "version", "-v", "--version":
|
||||
fmt.Println("ajean", Version)
|
||||
fmt.Println("loki", Version)
|
||||
case "help", "-h", "--help", "":
|
||||
printHelp()
|
||||
default:
|
||||
@@ -118,14 +118,14 @@ func Main() {
|
||||
}
|
||||
|
||||
func printHelp() {
|
||||
fmt.Printf(`ajean %s — manager llama.cpp + UI web (single binary)
|
||||
fmt.Printf(`loki %s — manager llama.cpp + UI web (single binary)
|
||||
|
||||
Usage: ajean <commande> [args]
|
||||
Usage: loki <commande> [args]
|
||||
|
||||
AJEAN tourne en deux services : ajean-engine (le modèle) et ajean-ui
|
||||
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 (ajean-engine) :
|
||||
Moteur (loki-engine) :
|
||||
start | stop | restart gérer le service
|
||||
status | logs état / logs en direct
|
||||
enable | disable auto-démarrage au boot
|
||||
@@ -138,7 +138,7 @@ Moteur (ajean-engine) :
|
||||
network [on|off|status] rendre l'endpoint OpenAI joignable depuis le réseau local
|
||||
(HOST + règle de pare-feu sous Windows)
|
||||
|
||||
Interface (ajean-ui) :
|
||||
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é
|
||||
@@ -160,7 +160,7 @@ Accès distant (ajean.link) :
|
||||
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 AJEAN) :
|
||||
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)
|
||||
@@ -179,8 +179,8 @@ Installation:
|
||||
serve entrypoint du service : exec llama-server (usage interne)
|
||||
|
||||
Env:
|
||||
AJEAN_HOME racine des données (défaut : /etc/ajean, %%ProgramData%%\ajean sous Windows)
|
||||
EDITOR éditeur pour 'ajean edit' (défaut : nano, notepad sous Windows)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -191,21 +191,21 @@ func mustExit(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// AjeanHome resolves the AJEAN data directory.
|
||||
// Précédence : $AJEAN_HOME → /etc/default/ajean (unix) → défaut plateforme.
|
||||
func AjeanHome() string {
|
||||
if h := os.Getenv("AJEAN_HOME"); h != "" {
|
||||
// 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 defaultAjeanHome()
|
||||
return defaultLokiHome()
|
||||
}
|
||||
|
||||
// readEtcDefault parses /etc/default/ajean for AJEAN_HOME=<path>. Quiet on errors.
|
||||
// readEtcDefault parses /etc/default/loki for LOKI_HOME=<path>. Quiet on errors.
|
||||
func readEtcDefault() string {
|
||||
b, err := os.ReadFile("/etc/default/ajean")
|
||||
b, err := os.ReadFile("/etc/default/loki")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -215,30 +215,30 @@ func readEtcDefault() string {
|
||||
continue
|
||||
}
|
||||
s = strings.TrimPrefix(s, "export ")
|
||||
if k, v, ok := strings.Cut(s, "="); ok && strings.TrimSpace(k) == "AJEAN_HOME" {
|
||||
if k, v, ok := strings.Cut(s, "="); ok && strings.TrimSpace(k) == "LOKI_HOME" {
|
||||
return strings.Trim(strings.TrimSpace(v), "\"'")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Arborescence de $AJEAN_HOME. Elle tient en six dossiers, et rien d'autre :
|
||||
// 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 ajean.db — voir store.go.
|
||||
func backendsDir() string { return filepath.Join(AjeanHome(), "backends") }
|
||||
func binDir() string { return filepath.Join(AjeanHome(), "bin") }
|
||||
func presetsDir() string { return filepath.Join(AjeanHome(), "presets") }
|
||||
func memoryDir() string { return filepath.Join(AjeanHome(), "memory") }
|
||||
func modelsDir() string { return filepath.Join(AjeanHome(), "models") }
|
||||
func workspaceDir() string { return filepath.Join(AjeanHome(), "workspace") }
|
||||
// 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") }
|
||||
func memoryDir() string { return filepath.Join(LokiHome(), "memory") }
|
||||
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 (« ajean-ui »), qui sert l'interface et le tunnel.
|
||||
// uiUnitName (« loki-ui »), qui sert l'interface et le tunnel.
|
||||
func serviceName() string {
|
||||
if n := os.Getenv("AJEAN_SERVICE"); n != "" {
|
||||
if n := os.Getenv("LOKI_SERVICE"); n != "" {
|
||||
return n
|
||||
}
|
||||
return "ajean-engine"
|
||||
return "loki-engine"
|
||||
}
|
||||
|
||||
// Color helpers (ANSI). Disabled when stdout is not a TTY.
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
bolterrors "go.etcd.io/bbolt/errors"
|
||||
)
|
||||
|
||||
// store.go — l'unique endroit où AJEAN écrit son état.
|
||||
// store.go — l'unique endroit où Loki écrit son état.
|
||||
//
|
||||
// Avant, chaque réglage avait son fichier : config.env, webprefs.json,
|
||||
// conversation.json, .api_key, .link_token, .agent_enabled, model_dirs.json…
|
||||
@@ -40,7 +40,7 @@ const (
|
||||
// bbolt pose un verrou EXCLUSIF sur son fichier tant qu'il est ouvert. Or une
|
||||
// machine installée fait tourner en permanence le service de lien, qui sert le
|
||||
// tunnel et l'UI : s'il gardait la base ouverte, plus une seule commande ne
|
||||
// fonctionnerait à côté — « ajean status », « ajean switch », « ajean edit »
|
||||
// fonctionnerait à côté — « loki status », « loki switch », « loki edit »
|
||||
// échoueraient toutes sur un délai d'attente, sur la machine même où tout est
|
||||
// censé marcher. On ouvre donc pour la durée d'une opération, puis on referme.
|
||||
//
|
||||
@@ -49,7 +49,7 @@ const (
|
||||
// contention entre process ; dbMu la sérialise à l'intérieur du process.
|
||||
var dbMu sync.Mutex
|
||||
|
||||
func dbPath() string { return filepath.Join(AjeanHome(), "ajean.db") }
|
||||
func dbPath() string { return filepath.Join(LokiHome(), "loki.db") }
|
||||
|
||||
// withDB ouvre la base, exécute fn, puis referme — toujours, même en erreur.
|
||||
func withDB(fn func(*bolt.DB) error) error {
|
||||
@@ -1,9 +1,9 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import "testing"
|
||||
|
||||
// Le cache de lecture ne doit jamais servir une valeur périmée après une
|
||||
// écriture de ce process : un « ajean edit » suivi d'un « ajean restart » doit
|
||||
// écriture de ce process : un « loki edit » suivi d'un « loki restart » doit
|
||||
// démarrer sur la NOUVELLE configuration.
|
||||
func TestCacheConfigInvalideParEcriture(t *testing.T) {
|
||||
testHome(t)
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"sync"
|
||||
@@ -1,14 +1,14 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import "testing"
|
||||
|
||||
// testHome donne au test un $AJEAN_HOME à lui, et neutralise le pare-feu : un
|
||||
// testHome donne au test un $LOKI_HOME à lui, et neutralise le pare-feu : un
|
||||
// test ne doit jamais poser (ni retirer) une règle entrante sur la machine qui
|
||||
// le fait tourner.
|
||||
func testHome(t *testing.T) string {
|
||||
t.Helper()
|
||||
home := t.TempDir()
|
||||
t.Setenv("AJEAN_HOME", home)
|
||||
t.Setenv("LOKI_HOME", home)
|
||||
firewallInert = true
|
||||
t.Cleanup(func() { firewallInert = false })
|
||||
return home
|
||||
@@ -1,6 +1,6 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// sys_brand_icon.go — l'icône de la marque AJEAN, rendue à la volée.
|
||||
// sys_brand_icon.go — l'icône de la marque Loki, rendue à la volée.
|
||||
//
|
||||
// RÉPLIQUE EXACTE du favicon de l'UI web : carré à coins arrondis NOIR + « j »
|
||||
// blanc (rects (6,3) (6,5) (4,7) sur une grille 12x12). Une seule source pour
|
||||
@@ -9,7 +9,7 @@ package ajean
|
||||
// au noir en laissant les icônes système en bleu.
|
||||
//
|
||||
// Aucun asset binaire à committer : tout est dessiné ici, et l'icône du .exe est
|
||||
// produite par `go generate ./cmd/ajean` (voir tools/gen-icon).
|
||||
// produite par `go generate ./cmd/loki` (voir tools/gen-icon).
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,16 +1,16 @@
|
||||
//go:build darwin
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// sys_console_darwin.go — détection du lancement « par clic » sous macOS.
|
||||
//
|
||||
// Sur macOS il n'y a pas d'équivalent d'AttachConsole : un binaire lancé depuis
|
||||
// le Finder hérite quand même de descripteurs stdout/stderr (vers les logs
|
||||
// système). Le signal fiable, c'est l'emplacement de l'exécutable : la release
|
||||
// publie un bundle AJEAN.app dont le binaire vit dans
|
||||
// AJEAN.app/Contents/MacOS/ajean. Si on tourne depuis là, c'est un double-clic
|
||||
// publie un bundle Loki.app dont le binaire vit dans
|
||||
// Loki.app/Contents/MacOS/loki. Si on tourne depuis là, c'est un double-clic
|
||||
// dans le Finder → expérience « application » (UI web + navigateur), exactement
|
||||
// comme le double-clic sur ajean.exe sous Windows.
|
||||
// comme le double-clic sur loki.exe sous Windows.
|
||||
//
|
||||
// Le même binaire copié dans /usr/local/bin garde évidemment le comportement CLI.
|
||||
|
||||
@@ -28,7 +28,7 @@ func setupConsole() bool {
|
||||
if p, err := filepath.EvalSymlinks(exe); err == nil {
|
||||
exe = p
|
||||
}
|
||||
// .../AJEAN.app/Contents/MacOS/ajean
|
||||
// .../Loki.app/Contents/MacOS/loki
|
||||
if strings.HasSuffix(filepath.Dir(exe), "/Contents/MacOS") {
|
||||
fixFinderPath()
|
||||
return false
|
||||
@@ -36,7 +36,7 @@ func setupConsole() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// appWarning signale une situation de lancement qui casse AJEAN de façon peu
|
||||
// appWarning signale une situation de lancement qui casse Loki de façon peu
|
||||
// évidente. Aujourd'hui : l'App Translocation de Gatekeeper. Une app non signée
|
||||
// ouverte depuis Téléchargements est exécutée depuis une copie en LECTURE SEULE
|
||||
// sous /private/var/folders/…/AppTranslocation/<uuid>/, à un chemin différent à
|
||||
@@ -44,7 +44,7 @@ func setupConsole() bool {
|
||||
// dessus (la seconde trouve le port 8090 occupé et se contente d'ouvrir l'UI de
|
||||
// la première, souvent une version périmée), des process fantômes issus de
|
||||
// copies précédentes, et une mise à jour en place impossible.
|
||||
// Le remède est côté utilisateur : déplacer AJEAN.app dans /Applications.
|
||||
// Le remède est côté utilisateur : déplacer Loki.app dans /Applications.
|
||||
func appWarning() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
@@ -53,14 +53,14 @@ func appWarning() string {
|
||||
if !strings.Contains(exe, "/AppTranslocation/") {
|
||||
return ""
|
||||
}
|
||||
return "AJEAN tourne depuis une copie temporaire en lecture seule (App Translocation de macOS). " +
|
||||
"Fermez l'app, déplacez AJEAN.app dans le dossier Applications, puis rouvrez-la — " +
|
||||
return "Loki tourne depuis une copie temporaire en lecture seule (App Translocation de macOS). " +
|
||||
"Fermez l'app, déplacez Loki.app dans le dossier Applications, puis rouvrez-la — " +
|
||||
"sinon chaque lancement crée une instance séparée et les mises à jour du moteur échouent."
|
||||
}
|
||||
|
||||
// fixFinderPath complète le PATH famélique hérité du Finder. Une app lancée par
|
||||
// LaunchServices reçoit /usr/bin:/bin:/usr/sbin:/sbin — pas les dossiers de
|
||||
// Homebrew : sans ça, AJEAN ne voit ni `brew`, ni `cmake`, ni `git`, et la
|
||||
// Homebrew : sans ça, Loki ne voit ni `brew`, ni `cmake`, ni `git`, et la
|
||||
// compilation d'un backend échoue alors que les outils sont bien installés.
|
||||
func fixFinderPath() {
|
||||
extra := []string{
|
||||
@@ -1,12 +1,12 @@
|
||||
//go:build !windows && !darwin
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// setupConsole : sous Linux, le process a toujours une vraie console/tty
|
||||
// standard (stdout hérité). Rien à faire → on signale simplement « on a une
|
||||
// console ». Les cas « lancé par un clic » sont traités par
|
||||
// sys_console_windows.go (double-clic sur ajean.exe) et sys_console_darwin.go
|
||||
// (ouverture de AJEAN.app depuis le Finder).
|
||||
// sys_console_windows.go (double-clic sur loki.exe) et sys_console_darwin.go
|
||||
// (ouverture de Loki.app depuis le Finder).
|
||||
func setupConsole() bool { return true }
|
||||
|
||||
// appWarning : rien à signaler hors macOS (l'App Translocation est spécifique à
|
||||
@@ -1,19 +1,19 @@
|
||||
//go:build windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// sys_console_windows.go — gestion de la console sous Windows.
|
||||
//
|
||||
// Le binaire est compilé en sous-système CONSOLE. C'est ce qui fait que `cmd`
|
||||
// ATTEND la fin du programme : avec un binaire graphique, il rendait la main
|
||||
// aussitôt et réaffichait son invite AVANT la sortie d'AJEAN, ce qui donnait
|
||||
// aussitôt et réaffichait son invite AVANT la sortie d'Loki, ce qui donnait
|
||||
//
|
||||
// C:\Users\Nathan>ajean
|
||||
// C:\Users\Nathan>loki
|
||||
//
|
||||
// C:\Users\Nathan>ajean 0.8.0 — manager llama.cpp + UI web
|
||||
// C:\Users\Nathan>loki 0.8.0 — manager llama.cpp + UI web
|
||||
//
|
||||
// La même cause cassait la redirection et les tubes, et rendait `ajean chat`
|
||||
// inutilisable depuis un terminal (le shell et AJEAN se disputaient l'entrée).
|
||||
// La même cause cassait la redirection et les tubes, et rendait `loki chat`
|
||||
// inutilisable depuis un terminal (le shell et Loki se disputaient l'entrée).
|
||||
//
|
||||
// EN CONTREPARTIE, Windows alloue une console à tout lancement sans terminal —
|
||||
// un double-clic. On la referme alors immédiatement : voir setupConsole. Les
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -11,15 +11,15 @@ import (
|
||||
// modèle de config.env : quatre copies qui divergeaient à la première
|
||||
// modification. Il n'y en a plus qu'une.
|
||||
|
||||
// dataDirs est l'arborescence complète de $AJEAN_HOME. Rien d'autre n'y est
|
||||
// créé : tout le reste vit dans ajean.db.
|
||||
// dataDirs est l'arborescence complète de $LOKI_HOME. Rien d'autre n'y est
|
||||
// créé : tout le reste vit dans loki.db.
|
||||
func dataDirs() []string {
|
||||
return []string{AjeanHome(), backendsDir(), binDir(), presetsDir(), memoryDir(), modelsDir(), workspaceDir()}
|
||||
return []string{LokiHome(), backendsDir(), binDir(), presetsDir(), memoryDir(), modelsDir(), workspaceDir()}
|
||||
}
|
||||
|
||||
// defaultConfig est la configuration de départ d'une installation neuve. Les
|
||||
// valeurs sont volontairement incomplètes (BIN et MODEL sont vides) : c'est
|
||||
// l'écran d'accueil, ou « ajean llamacpp install », qui les renseigne.
|
||||
// l'écran d'accueil, ou « loki llamacpp install », qui les renseigne.
|
||||
func defaultConfig() map[string]string {
|
||||
host := "0.0.0.0"
|
||||
if runtime.GOOS == "windows" {
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build !windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// sys_disk_other.go — espace libre d'un système de fichiers via statfs(2).
|
||||
// Bavail (et pas Bfree) : les blocs réservés à root ne sont pas utilisables pour
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// sys_disk_windows.go — espace libre d'un volume via GetDiskFreeSpaceExW. On
|
||||
// veut l'espace réellement disponible pour l'utilisateur courant (quotas
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build !windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// Linux et macOS n'ont pas de pare-feu entrant activé par défaut, et quand il y
|
||||
// en a un (ufw, firewalld, pf) c'est une affaire d'administration système qu'un
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
// Sur une installation Windows fraîche, même avec HOST=0.0.0.0, le pare-feu
|
||||
// bloque tout ce qui arrive du réseau : l'utilisateur voyait « ça écoute
|
||||
// partout » et se prenait quand même un délai d'attente depuis son autre
|
||||
// machine. AJEAN pose donc la règle lui-même, quand il en a le droit.
|
||||
// machine. Loki pose donc la règle lui-même, quand il en a le droit.
|
||||
//
|
||||
// `ajean install` ne réclame PAS les droits administrateur (voir
|
||||
// `loki install` ne réclame PAS les droits administrateur (voir
|
||||
// sys_install_windows.go) : netsh échouera donc souvent. On ne fait pas semblant
|
||||
// que ça a marché — firewallState relit la règle, et firewallManualHint donne la
|
||||
// commande exacte à coller dans un terminal administrateur.
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
// firewallRuleName identifie nos règles. Une par port : changer PORT dans la
|
||||
// configuration ne doit pas laisser une règle ouverte sur l'ancien.
|
||||
func firewallRuleName(port int) string {
|
||||
return "AJEAN moteur (port " + strconv.Itoa(port) + ")"
|
||||
return "Loki moteur (port " + strconv.Itoa(port) + ")"
|
||||
}
|
||||
|
||||
// netsh exécute netsh sans faire clignoter de console (hideCmd) et renvoie sa
|
||||
@@ -71,7 +71,7 @@ func firewallState(port int) string {
|
||||
}
|
||||
|
||||
// firewallManualHint : la commande à coller dans un terminal ADMINISTRATEUR
|
||||
// quand AJEAN n'a pas pu poser la règle lui-même.
|
||||
// quand Loki n'a pas pu poser la règle lui-même.
|
||||
func firewallManualHint(port int) string {
|
||||
return fmt.Sprintf("le pare-feu Windows bloque encore le port %d. Ouvre un terminal ADMINISTRATEUR et lance :\n"+
|
||||
` netsh advfirewall firewall add rule name="%s" dir=in action=allow protocol=TCP localport=%d profile=private,domain`,
|
||||
@@ -1,8 +1,8 @@
|
||||
//go:build !windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// appFirstRun : hors Windows, `ajean app` est lancé depuis un terminal par
|
||||
// appFirstRun : hors Windows, `loki app` est lancé depuis un terminal par
|
||||
// quelqu'un qui a déjà suivi l'installation — pas de double-clic à désambiguïser,
|
||||
// donc pas de boîte de dialogue. On se contente de provisionner les données au
|
||||
// premier lancement. Ne relance jamais (renvoie toujours false).
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
@@ -22,8 +22,8 @@ import (
|
||||
|
||||
// Premier lancement sous Windows.
|
||||
//
|
||||
// Avant : double-cliquer ajean-windows-amd64.exe déclenchait une installation
|
||||
// SILENCIEUSE (copie du binaire dans %ProgramData%\ajean\bin, ajout au PATH) dont
|
||||
// Avant : double-cliquer loki-windows-amd64.exe déclenchait une installation
|
||||
// SILENCIEUSE (copie du binaire dans %ProgramData%\loki\bin, ajout au PATH) dont
|
||||
// l'utilisateur ne voyait rien — cmdApp appelait cmdInstall sans console pour en
|
||||
// afficher la sortie. D'où la confusion légitime : est-ce que ça installe ou est-ce
|
||||
// que ça lance ? Et surtout, deux copies du binaire coexistaient (celle du Bureau,
|
||||
@@ -32,13 +32,13 @@ import (
|
||||
//
|
||||
// Maintenant, deux cas seulement :
|
||||
//
|
||||
// 1. AJEAN n'est pas installé. On installe, sans demander : rester à
|
||||
// 1. Loki n'est pas installé. On installe, sans demander : rester à
|
||||
// l'emplacement du fichier téléchargé ne donne pas une installation
|
||||
// utilisable, la question n'avait donc qu'une réponse. Copie + PATH +
|
||||
// raccourcis, un message qui dit ce qui a été fait, et on démarre depuis la
|
||||
// copie installée.
|
||||
//
|
||||
// 2. AJEAN est déjà installé. Le fichier téléchargé se comporte en installeur de
|
||||
// 2. Loki est déjà installé. Le fichier téléchargé se comporte en installeur de
|
||||
// mise à jour, avec trois situations bien distinctes (voir runAsInstaller) :
|
||||
// plus récent et application arrêtée, on remplace et on démarre, sans un mot ;
|
||||
// plus récent mais application EN COURS, on demande s'il faut la fermer pour
|
||||
@@ -93,7 +93,7 @@ func appFirstRun() bool {
|
||||
if canonPath(exe) == canonPath(target) {
|
||||
// On EST l'application installée. On en profite pour garantir que les
|
||||
// raccourcis existent : sans ça, quelqu'un qui perd son raccourci ne
|
||||
// retrouve plus AJEAN, et n'a aucune raison de relancer le fichier
|
||||
// retrouve plus Loki, et n'a aucune raison de relancer le fichier
|
||||
// téléchargé (qu'il a souvent supprimé) pour le récupérer. En tâche de
|
||||
// fond, l'appel PowerShell ne doit pas retarder le démarrage.
|
||||
go ensureShortcuts(target)
|
||||
@@ -114,18 +114,18 @@ func appFirstRun() bool {
|
||||
// un fichier, autant dire où le programme s'est installé et qu'il peut
|
||||
// supprimer ce qu'il a téléchargé.
|
||||
if _, err := installSelf(filepath.Dir(target)); err != nil {
|
||||
// Échec le plus courant : pas les droits sur %ProgramData%. AJEAN reste
|
||||
// Échec le plus courant : pas les droits sur %ProgramData%. Loki reste
|
||||
// parfaitement utilisable depuis son emplacement actuel, le dossier de
|
||||
// données étant ailleurs — on démarre donc au lieu d'abandonner.
|
||||
messageBox("Installation impossible :\n\n"+err.Error()+"\n\nAJEAN va démarrer depuis l'emplacement actuel.", "AJEAN", mbIconInfo)
|
||||
messageBox("Installation impossible :\n\n"+err.Error()+"\n\nLoki va démarrer depuis l'emplacement actuel.", "Loki", mbIconInfo)
|
||||
return false
|
||||
}
|
||||
_, _ = addToUserPath(filepath.Dir(target))
|
||||
shortcuts := ensureShortcuts(target)
|
||||
|
||||
messageBox("AJEAN est installé.\n\n"+target+"\n\n"+shortcuts+
|
||||
messageBox("Loki est installé.\n\n"+target+"\n\n"+shortcuts+
|
||||
"\n\nL'application va démarrer. Vous pouvez supprimer le fichier téléchargé.",
|
||||
"AJEAN", mbIconInfo)
|
||||
"Loki", mbIconInfo)
|
||||
|
||||
return launch(target)
|
||||
}
|
||||
@@ -193,7 +193,7 @@ func launch(target string) bool {
|
||||
return cmd.Start() == nil
|
||||
}
|
||||
|
||||
// runAsInstaller : AJEAN est déjà installé et on exécute une AUTRE copie (le
|
||||
// runAsInstaller : Loki est déjà installé et on exécute une AUTRE copie (le
|
||||
// fichier fraîchement téléchargé). Le fichier joue alors le rôle d'installeur de
|
||||
// mise à jour. Trois situations, trois comportements distincts, parce qu'elles
|
||||
// n'appellent pas la même décision de l'utilisateur.
|
||||
@@ -224,37 +224,37 @@ func runAsInstaller(target string) bool {
|
||||
// serait une régression silencieuse, et démarrer sans rien dire laisserait
|
||||
// croire qu'on utilise la version qu'on vient de télécharger.
|
||||
if messageBox(
|
||||
"Une version plus récente d'AJEAN est déjà installée sur cet ordinateur.\n\n"+
|
||||
"Une version plus récente d'Loki est déjà installée sur cet ordinateur.\n\n"+
|
||||
" installée : "+installed+"\n"+
|
||||
" ce fichier : "+Version+"\n\n"+
|
||||
"Rien ne sera remplacé.\n\n"+
|
||||
"Voulez-vous démarrer AJEAN (version "+installed+") ?\n"+
|
||||
"Voulez-vous démarrer Loki (version "+installed+") ?\n"+
|
||||
"Répondre Non ferme simplement cette fenêtre.",
|
||||
"AJEAN", mbYesNo|mbIconQuestion) != idYes {
|
||||
"Loki", mbYesNo|mbIconQuestion) != idYes {
|
||||
return true // rien à faire, on quitte sans démarrer
|
||||
}
|
||||
ensureShortcuts(target)
|
||||
return launch(target)
|
||||
|
||||
case cmp > 0 && len(running) > 0:
|
||||
// Mise à jour disponible, mais AJEAN tourne. Écrire le nouveau binaire
|
||||
// Mise à jour disponible, mais Loki tourne. Écrire le nouveau binaire
|
||||
// sans redémarrer ne changerait RIEN à ce que l'utilisateur a sous les
|
||||
// yeux : l'onglet qui s'ouvrirait serait servi par l'ancienne version. On
|
||||
// pose donc la seule question qui compte.
|
||||
if messageBox(
|
||||
"AJEAN est déjà en cours d'exécution.\n\n"+
|
||||
"Loki est déjà en cours d'exécution.\n\n"+
|
||||
" version en cours : "+verLabel(installed)+"\n"+
|
||||
" ce fichier : "+Version+" (plus récente)\n\n"+
|
||||
"Fermer AJEAN et le redémarrer pour appliquer la mise à jour ?\n\n"+
|
||||
"Répondre Non ouvre AJEAN dans sa version actuelle, sans rien mettre à jour.",
|
||||
"Mise à jour d'AJEAN", mbYesNo|mbIconQuestion) != idYes {
|
||||
"Fermer Loki et le redémarrer pour appliquer la mise à jour ?\n\n"+
|
||||
"Répondre Non ouvre Loki dans sa version actuelle, sans rien mettre à jour.",
|
||||
"Mise à jour d'Loki", mbYesNo|mbIconQuestion) != idYes {
|
||||
ensureShortcuts(target)
|
||||
return launch(target) // l'instance en cours reprend la main (port occupé)
|
||||
}
|
||||
stopProcesses(running)
|
||||
if err := replaceInstalled(target); err != nil {
|
||||
messageBox("La mise à jour a échoué :\n\n"+err.Error()+"\n\nAJEAN va redémarrer dans sa version actuelle.",
|
||||
"AJEAN", mbIconInfo)
|
||||
messageBox("La mise à jour a échoué :\n\n"+err.Error()+"\n\nLoki va redémarrer dans sa version actuelle.",
|
||||
"Loki", mbIconInfo)
|
||||
}
|
||||
ensureShortcuts(target)
|
||||
return launch(target)
|
||||
@@ -282,7 +282,7 @@ func verLabel(v string) string {
|
||||
|
||||
// replaceInstalled écrase le binaire installé par celui qu'on exécute. Le
|
||||
// renommage préalable en .old permet de remplacer un exécutable encore ouvert
|
||||
// (même mécanique que replaceBinary pour `ajean update`).
|
||||
// (même mécanique que replaceBinary pour `loki update`).
|
||||
func replaceInstalled(target string) error {
|
||||
removeOldBinaries(target)
|
||||
old, err := renameAside(target) // nom unique, cf. renameAside
|
||||
@@ -298,7 +298,7 @@ func replaceInstalled(target string) error {
|
||||
}
|
||||
|
||||
// runningPIDs liste les processus qui exécutent exactement ce fichier. On compare
|
||||
// le CHEMIN, pas le nom : tuer par nom d'image (« ajean.exe ») emporterait aussi
|
||||
// le CHEMIN, pas le nom : tuer par nom d'image (« loki.exe ») emporterait aussi
|
||||
// le processus courant et toute autre copie sans rapport.
|
||||
//
|
||||
// La comparaison se fait ICI, sur des chemins canonisés, et non dans le script
|
||||
@@ -375,10 +375,10 @@ func portBusy(port int) bool {
|
||||
}
|
||||
|
||||
// binaryVersion lit la version dans les ressources du fichier (VS_VERSIONINFO,
|
||||
// posées par goversioninfo, cf cmd/ajean/versioninfo.json), comme le fait
|
||||
// posées par goversioninfo, cf cmd/loki/versioninfo.json), comme le fait
|
||||
// l'onglet « Détails » des propriétés Windows.
|
||||
//
|
||||
// ⚠️ NE PAS remplacer par un `ajean version` exécuté : le binaire est compilé en
|
||||
// ⚠️ NE PAS remplacer par un `loki version` exécuté : le binaire est compilé en
|
||||
// sous-système GUI, il s'attache à la console du parent et n'écrit RIEN dans un
|
||||
// tuyau. La sortie capturée est vide, donc la comparaison de versions échouerait
|
||||
// toujours en silence et la mise à jour ne se ferait jamais (vérifié).
|
||||
@@ -429,7 +429,7 @@ type vsFixedFileInfo struct {
|
||||
FileDateLS uint32
|
||||
}
|
||||
|
||||
// ensureShortcuts garantit qu'un raccourci « AJEAN » existe dans le menu
|
||||
// ensureShortcuts garantit qu'un raccourci « Loki » existe dans le menu
|
||||
// Démarrer et sur le Bureau, et qu'il pointe sur le bon exécutable. Appelée à
|
||||
// CHAQUE lancement du fichier téléchargé, pas seulement à l'installation :
|
||||
// un raccourci perdu (nettoyage, migration de profil, dossier Démarrer
|
||||
@@ -448,9 +448,9 @@ $progs=Join-Path ([Environment]::GetFolderPath('StartMenu')) 'Programs'
|
||||
foreach ($d in @($progs, [Environment]::GetFolderPath('Desktop'))) {
|
||||
if (-not $d) { continue }
|
||||
if (-not (Test-Path $d)) { continue }
|
||||
$lnk=Join-Path $d 'AJEAN.lnk'
|
||||
$lnk=Join-Path $d 'Loki.lnk'
|
||||
# Un raccourci existant est REPOINTÉ s'il vise autre chose que le binaire
|
||||
# canonique — typiquement l'ancien « ajean.exe », qui n'est plus qu'un alias.
|
||||
# canonique — typiquement l'ancien « loki.exe », qui n'est plus qu'un alias.
|
||||
# Le laisser en l'état condamnait l'utilisateur à relancer indéfiniment une
|
||||
# version périmée par son propre raccourci.
|
||||
if (Test-Path $lnk) {
|
||||
@@ -476,9 +476,9 @@ foreach ($d in @($progs, [Environment]::GetFolderPath('Desktop'))) {
|
||||
$s=$w.CreateShortcut($lnk)
|
||||
$s.TargetPath=$t
|
||||
$s.WorkingDirectory=(Split-Path $t)
|
||||
$s.Description='AJEAN, votre IA locale'
|
||||
$s.Description='Loki, votre IA locale'
|
||||
# Minimisé (7) : le binaire étant en sous-système console, Windows lui alloue
|
||||
# une console au lancement. AJEAN la referme aussitôt, mais demander un
|
||||
# une console au lancement. Loki la referme aussitôt, mais demander un
|
||||
# démarrage minimisé garantit qu'elle n'est jamais peinte à l'écran.
|
||||
$s.WindowStyle=7
|
||||
$s.IconLocation="$t,0"
|
||||
@@ -490,9 +490,9 @@ Write-Output ($done -join ';')`, psQuote(target))
|
||||
cmd := hideCmd(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil || strings.TrimSpace(string(out)) == "" {
|
||||
return "(raccourcis non créés — lancez AJEAN depuis " + target + ")"
|
||||
return "(raccourcis non créés — lancez Loki depuis " + target + ")"
|
||||
}
|
||||
return "Raccourci « AJEAN » ajouté au menu Démarrer et au Bureau."
|
||||
return "Raccourci « Loki » ajouté au menu Démarrer et au Bureau."
|
||||
}
|
||||
|
||||
// removeShortcuts efface les raccourcis posés par ensureShortcuts. Renvoie true
|
||||
@@ -503,7 +503,7 @@ func removeShortcuts() bool {
|
||||
$sm=[Environment]::GetFolderPath('StartMenu')
|
||||
foreach ($d in @($sm, (Join-Path $sm 'Programs'), [Environment]::GetFolderPath('Desktop'))) {
|
||||
if (-not $d) { continue }
|
||||
$p=Join-Path $d 'AJEAN.lnk'
|
||||
$p=Join-Path $d 'Loki.lnk'
|
||||
if (Test-Path $p) { try { Remove-Item $p -Force; $n++ } catch {} }
|
||||
}
|
||||
Write-Output $n`
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
//go:build windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// binaryVersion pilote la mise à jour automatique du binaire installé quand on
|
||||
// lance le fichier téléchargé : si elle renvoie du vide, plus aucune mise à jour
|
||||
// n'a lieu, et en silence. C'est exactement ce qui arrivait avec la première
|
||||
// implémentation (exécuter `ajean version`, dont la sortie est vide pour un
|
||||
// implémentation (exécuter `loki version`, dont la sortie est vide pour un
|
||||
// binaire en sous-système GUI). D'où ces tests sur la plomberie syscall.
|
||||
func TestBinaryVersionReadsResource(t *testing.T) {
|
||||
// kernel32.dll porte toujours une ressource de version.
|
||||
@@ -28,10 +28,10 @@ func TestBinaryVersionReadsResource(t *testing.T) {
|
||||
|
||||
// Sur une machine vierge, le dossier bin n'existe pas encore : installSelf doit
|
||||
// le créer. Sans ça, le premier lancement échouait sur « open
|
||||
// C:\ProgramData\ajean\bin\ajean.exe: The system cannot find the path specified »
|
||||
// et AJEAN ne s'installait jamais.
|
||||
// C:\ProgramData\loki\bin\loki.exe: The system cannot find the path specified »
|
||||
// et Loki ne s'installait jamais.
|
||||
func TestInstallSelfCreatesBinDir(t *testing.T) {
|
||||
binDir := filepath.Join(t.TempDir(), "ajean", "bin") // deux niveaux absents
|
||||
binDir := filepath.Join(t.TempDir(), "loki", "bin") // deux niveaux absents
|
||||
dst, err := installSelf(binDir)
|
||||
if err != nil {
|
||||
t.Fatalf("installSelf sur un dossier absent : %v", err)
|
||||
@@ -45,7 +45,7 @@ func TestInstallSelfCreatesBinDir(t *testing.T) {
|
||||
// cible existe déjà, et laisser l'ancien de côté sans le confondre avec la cible.
|
||||
func TestReplaceInstalled(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
target := filepath.Join(dir, "ajean.exe")
|
||||
target := filepath.Join(dir, "loki.exe")
|
||||
if err := os.WriteFile(target, []byte("ancienne version"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func TestBinaryVersionWithoutResource(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduit le cas signale en usage : l'alias « ajean.exe » est EN COURS
|
||||
// Reproduit le cas signale en usage : l'alias « loki.exe » est EN COURS
|
||||
// d'execution au moment de la mise a jour. copyExe seul echouait, l'alias
|
||||
// restait fige sur une version perimee, et tout raccourci le visant relancait
|
||||
// indefiniment l'ancienne version — qui affichait « une version plus recente
|
||||
@@ -89,7 +89,7 @@ func TestBinaryVersionWithoutResource(t *testing.T) {
|
||||
func TestReplaceExeSurchargeUnFichierVerrouille(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := filepath.Join(dir, "neuf.exe")
|
||||
dst := filepath.Join(dir, "ajean.exe")
|
||||
dst := filepath.Join(dir, "loki.exe")
|
||||
if err := os.WriteFile(src, []byte("VERSION-NEUVE"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func TestReplaceExeSurchargeUnFichierVerrouille(t *testing.T) {
|
||||
// echoue apres le renommage, l'ancien fichier revient a sa place.
|
||||
func TestReplaceExeRestaureSiLaCopieEchoue(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dst := filepath.Join(dir, "ajean.exe")
|
||||
dst := filepath.Join(dir, "loki.exe")
|
||||
if err := os.WriteFile(dst, []byte("a-preserver"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build darwin
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// Implémentation prudente, à valider sur une machine Apple.
|
||||
|
||||
// launchdPlistTemplate — champs : Label, binaire, argument, UserName,
|
||||
// WorkingDirectory, AJEAN_HOME, StandardOutPath, StandardErrorPath.
|
||||
// WorkingDirectory, LOKI_HOME, StandardOutPath, StandardErrorPath.
|
||||
// KeepAlive/SuccessfulExit=false ≈ Restart=on-failure ; RunAtLoad relance au
|
||||
// boot une fois chargé avec `-w`.
|
||||
const launchdPlistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -33,7 +33,7 @@ const launchdPlistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<key>UserName</key><string>%s</string>
|
||||
<key>WorkingDirectory</key><string>%s</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict><key>AJEAN_HOME</key><string>%s</string></dict>
|
||||
<dict><key>LOKI_HOME</key><string>%s</string></dict>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key>
|
||||
<dict><key>SuccessfulExit</key><false/></dict>
|
||||
@@ -48,12 +48,12 @@ func unitArgs() map[string]string {
|
||||
return map[string]string{serviceName(): "serve", uiUnitName: "web"}
|
||||
}
|
||||
|
||||
func installServices(targetUser, ajeanHome string) error {
|
||||
func installServices(targetUser, lokiHome string) error {
|
||||
exe := installedExePath()
|
||||
for name, arg := range unitArgs() {
|
||||
logPath := filepath.Join(ajeanHome, name+".log")
|
||||
logPath := filepath.Join(lokiHome, name+".log")
|
||||
plist := fmt.Sprintf(launchdPlistTemplate,
|
||||
launchdLabel(name), exe, arg, targetUser, ajeanHome, ajeanHome, logPath, logPath)
|
||||
launchdLabel(name), exe, arg, targetUser, lokiHome, lokiHome, logPath, logPath)
|
||||
path := launchdPlistPath(name)
|
||||
if err := os.WriteFile(path, []byte(plist), 0o644); err != nil {
|
||||
return err
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build linux
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -14,14 +14,14 @@ import (
|
||||
// sudoersTemplate autorise l'utilisateur à piloter UNE unité sans mot de passe.
|
||||
// Posé pour les deux : l'interface, qui tourne sans privilèges, doit pouvoir
|
||||
// redémarrer le moteur (bascule de preset) comme elle-même (nouveau jeton).
|
||||
const sudoersTemplate = `# Permet à %[1]s de piloter l'unité %[2]s sans mot de passe (posé par ajean install).
|
||||
const sudoersTemplate = `# Permet à %[1]s de piloter l'unité %[2]s sans mot de passe (posé par loki install).
|
||||
%[1]s ALL=(root) NOPASSWD: /bin/systemctl start %[2]s, /bin/systemctl stop %[2]s, /bin/systemctl restart %[2]s, /bin/systemctl enable %[2]s, /bin/systemctl disable %[2]s
|
||||
`
|
||||
|
||||
// engineUnitTemplate — le moteur : exec llama-server, supervisé directement par
|
||||
// systemd. Champs : User, WorkingDirectory, ExecStart.
|
||||
const engineUnitTemplate = `[Unit]
|
||||
Description=AJEAN — moteur llama.cpp
|
||||
Description=Loki — moteur llama.cpp
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
@@ -47,7 +47,7 @@ WantedBy=multi-user.target
|
||||
// OpenAI, servis par un SEUL process (donc une seule conversation).
|
||||
// Champs : unité du moteur (dépendance), User, WorkingDirectory, ExecStart.
|
||||
const uiUnitTemplate = `[Unit]
|
||||
Description=AJEAN — interface web + accès distant
|
||||
Description=Loki — interface web + accès distant
|
||||
After=network-online.target %s.service
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -63,11 +63,11 @@ RestartSec=5
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
func installServices(targetUser, ajeanHome string) error {
|
||||
func installServices(targetUser, lokiHome string) error {
|
||||
svc, exe := serviceName(), installedExePath()
|
||||
units := map[string]string{
|
||||
svc: fmt.Sprintf(engineUnitTemplate, targetUser, ajeanHome, exe+" serve"),
|
||||
uiUnitName: fmt.Sprintf(uiUnitTemplate, svc, targetUser, ajeanHome, exe+" web"),
|
||||
svc: fmt.Sprintf(engineUnitTemplate, targetUser, lokiHome, exe+" serve"),
|
||||
uiUnitName: fmt.Sprintf(uiUnitTemplate, svc, targetUser, lokiHome, exe+" web"),
|
||||
}
|
||||
for name, body := range units {
|
||||
path := "/etc/systemd/system/" + name + ".service"
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build !windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
|
||||
func cmdInstall(args []string) error {
|
||||
if os.Geteuid() != 0 {
|
||||
return fmt.Errorf("ajean install doit être exécuté en root (sudo ajean install)")
|
||||
return fmt.Errorf("loki install doit être exécuté en root (sudo loki install)")
|
||||
}
|
||||
targetUser := os.Getenv("SUDO_USER")
|
||||
if targetUser == "" {
|
||||
@@ -38,19 +38,19 @@ func cmdInstall(args []string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("utilisateur '%s' introuvable: %w", targetUser, err)
|
||||
}
|
||||
ajeanHome := defaultAjeanHome()
|
||||
if v := os.Getenv("AJEAN_HOME"); v != "" {
|
||||
ajeanHome = v
|
||||
lokiHome := defaultLokiHome()
|
||||
if v := os.Getenv("LOKI_HOME"); v != "" {
|
||||
lokiHome = v
|
||||
}
|
||||
|
||||
fmt.Printf("Installation pour utilisateur %s\n", cyan(targetUser))
|
||||
fmt.Printf(" AJEAN_HOME = %s\n", ajeanHome)
|
||||
fmt.Printf(" LOKI_HOME = %s\n", lokiHome)
|
||||
fmt.Printf(" services = %s + %s\n", serviceName(), uiUnitName)
|
||||
|
||||
// 0. Reprise d'une installation 0.7, si c'en est une. AVANT provisionDataDir :
|
||||
// elle déplace des dossiers entiers, donc la destination ne doit pas
|
||||
// encore exister. Code temporaire, voir migrate_07.go.
|
||||
if err := migrateFrom07(ajeanHome); err != nil {
|
||||
if err := migrateFrom07(lokiHome); err != nil {
|
||||
return fmt.Errorf("reprise de l'installation 0.7 : %w", err)
|
||||
}
|
||||
|
||||
@@ -60,45 +60,45 @@ func cmdInstall(args []string) error {
|
||||
}
|
||||
fmt.Printf(" %s arborescence prête\n", green("✓"))
|
||||
|
||||
// 2. Lien vers /usr/local/bin/ajean.
|
||||
// 2. Lien vers /usr/local/bin/loki.
|
||||
if err := linkInstalledExe(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. /etc/default/ajean, pour que les invocations root résolvent AJEAN_HOME.
|
||||
// 3. /etc/default/loki, pour que les invocations root résolvent LOKI_HOME.
|
||||
if err := os.MkdirAll("/etc/default", 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
defaults := fmt.Sprintf("# Généré par ajean install — racine des données\nAJEAN_HOME=%s\n", ajeanHome)
|
||||
if err := os.WriteFile("/etc/default/ajean", []byte(defaults), 0o644); err != nil {
|
||||
defaults := fmt.Sprintf("# Généré par loki install — racine des données\nLOKI_HOME=%s\n", lokiHome)
|
||||
if err := os.WriteFile("/etc/default/loki", []byte(defaults), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" %s /etc/default/ajean\n", green("✓"))
|
||||
fmt.Printf(" %s /etc/default/loki\n", green("✓"))
|
||||
|
||||
// 4. Déclaration des services (systemd ou launchd selon la plateforme).
|
||||
if err := installServices(targetUser, ajeanHome); err != nil {
|
||||
if err := installServices(targetUser, lokiHome); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. AJEAN_HOME appartient à l'utilisateur cible : les deux services et les
|
||||
// 5. LOKI_HOME appartient à l'utilisateur cible : les deux services et les
|
||||
// commandes qu'il tape doivent pouvoir écrire dans la base.
|
||||
chown(ajeanHome, u)
|
||||
chown(lokiHome, u)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%s installation terminée.\n", green("[ok]"))
|
||||
fmt.Printf("\nProchaines étapes :\n")
|
||||
fmt.Printf(" 1. installe le moteur : %s (compile llama.cpp, renseigne BIN)\n", bold("sudo -u "+targetUser+" ajean llamacpp install"))
|
||||
fmt.Printf(" 2. choisis le modèle : %s (renseigne MODEL, un fichier .gguf)\n", bold("sudo -u "+targetUser+" ajean edit"))
|
||||
fmt.Printf(" 3. démarre le moteur : %s\n", bold("sudo ajean start"))
|
||||
fmt.Printf(" 4. vérifie : %s\n", bold("ajean test"))
|
||||
fmt.Printf(" 5. démarre l'interface : %s (http://localhost:8090)\n", bold("sudo ajean ui start"))
|
||||
fmt.Printf(" 1. installe le moteur : %s (compile llama.cpp, renseigne BIN)\n", bold("sudo -u "+targetUser+" loki llamacpp install"))
|
||||
fmt.Printf(" 2. choisis le modèle : %s (renseigne MODEL, un fichier .gguf)\n", bold("sudo -u "+targetUser+" loki edit"))
|
||||
fmt.Printf(" 3. démarre le moteur : %s\n", bold("sudo loki start"))
|
||||
fmt.Printf(" 4. vérifie : %s\n", bold("loki test"))
|
||||
fmt.Printf(" 5. démarre l'interface : %s (http://localhost:8090)\n", bold("sudo loki ui start"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// linkInstalledExe pose /usr/local/bin/ajean sur le binaire courant.
|
||||
// linkInstalledExe pose /usr/local/bin/loki sur le binaire courant.
|
||||
//
|
||||
// Garde-fou (issue #5) : si on tourne DÉJÀ depuis la cible — l'utilisateur a
|
||||
// posé le binaire là puis lancé `sudo ajean install` — il ne faut surtout pas
|
||||
// posé le binaire là puis lancé `sudo loki install` — il ne faut surtout pas
|
||||
// Remove+Symlink sur soi-même : ça effacerait le vrai binaire et créerait un
|
||||
// lien vers lui-même (« Too many levels of symbolic links »). On compare les
|
||||
// chemins réels, résolus.
|
||||
@@ -135,15 +135,15 @@ func linkInstalledExe() error {
|
||||
|
||||
func cmdUninstall(args []string) error {
|
||||
if os.Geteuid() != 0 {
|
||||
return fmt.Errorf("ajean uninstall doit être exécuté en root")
|
||||
return fmt.Errorf("loki uninstall doit être exécuté en root")
|
||||
}
|
||||
uninstallServices()
|
||||
for _, p := range []string{"/etc/default/ajean", installedExePath()} {
|
||||
for _, p := range []string{"/etc/default/loki", installedExePath()} {
|
||||
if err := os.Remove(p); err == nil {
|
||||
fmt.Printf(" %s %s\n", green("✓"), p)
|
||||
}
|
||||
}
|
||||
fmt.Println(dim("(données utilisateur conservées — supprime $AJEAN_HOME manuellement si tu veux purger)"))
|
||||
fmt.Println(dim("(données utilisateur conservées — supprime $LOKI_HOME manuellement si tu veux purger)"))
|
||||
fmt.Println(green("[ok]") + " désinstallé")
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,21 +12,21 @@ import (
|
||||
)
|
||||
|
||||
// On Windows there's no systemd unit, sudoers, or /usr/local/bin to populate.
|
||||
// `ajean install` simply provisions the data directory and a starter config; the
|
||||
// `loki install` simply provisions the data directory and a starter config; the
|
||||
// service itself is managed by the PID-file supervisor in sys_service_windows.go
|
||||
// (ajean start / stop / status), which needs no admin rights.
|
||||
// (loki start / stop / status), which needs no admin rights.
|
||||
|
||||
func cmdInstall(args []string) error {
|
||||
ajeanHome := AjeanHome()
|
||||
lokiHome := LokiHome()
|
||||
|
||||
fmt.Printf("Installation (Windows)\n")
|
||||
fmt.Printf(" AJEAN_HOME = %s\n", ajeanHome)
|
||||
fmt.Printf(" LOKI_HOME = %s\n", lokiHome)
|
||||
fmt.Printf(" service = %s\n", serviceName())
|
||||
|
||||
// 0. Reprise d'une installation 0.7, si c'en est une. AVANT provisionDataDir :
|
||||
// elle déplace des dossiers entiers, donc la destination ne doit pas
|
||||
// encore exister. Code temporaire, voir migrate_07.go.
|
||||
if err := migrateFrom07(ajeanHome); err != nil {
|
||||
if err := migrateFrom07(lokiHome); err != nil {
|
||||
return fmt.Errorf("reprise de l'installation 0.7 : %w", err)
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ func cmdInstall(args []string) error {
|
||||
}
|
||||
fmt.Printf(" %s arborescence prête\n", green("✓"))
|
||||
|
||||
// 2. Le binaire va dans AJEAN_HOME\bin, ajouté au PATH utilisateur, pour que
|
||||
// `ajean` soit appelable depuis n'importe quel shell (pendant Windows du
|
||||
// 2. Le binaire va dans LOKI_HOME\bin, ajouté au PATH utilisateur, pour que
|
||||
// `loki` soit appelable depuis n'importe quel shell (pendant Windows du
|
||||
// lien /usr/local/bin créé par l'installateur Unix).
|
||||
dir := binDir()
|
||||
onPath := false
|
||||
@@ -62,40 +62,40 @@ func cmdInstall(args []string) error {
|
||||
fmt.Println()
|
||||
fmt.Printf("%s installation terminée.\n", green("[ok]"))
|
||||
fmt.Printf("\nProchaines étapes :\n")
|
||||
fmt.Printf(" 1. édite la config : %s (renseigne BIN, MODEL)\n", bold("ajean edit"))
|
||||
fmt.Printf(" 2. démarre le service: %s\n", bold("ajean start"))
|
||||
fmt.Printf(" 3. UI web : %s\n", bold("ajean web"))
|
||||
fmt.Printf(" 1. édite la config : %s (renseigne BIN, MODEL)\n", bold("loki edit"))
|
||||
fmt.Printf(" 2. démarre le service: %s\n", bold("loki start"))
|
||||
fmt.Printf(" 3. UI web : %s\n", bold("loki web"))
|
||||
if onPath {
|
||||
fmt.Printf("\n%s ouvre un NOUVEAU terminal pour que 'ajean' soit reconnu (le PATH n'est lu qu'au démarrage du shell).\n", dim("[info]"))
|
||||
fmt.Printf("\n%s ouvre un NOUVEAU terminal pour que 'loki' soit reconnu (le PATH n'est lu qu'au démarrage du shell).\n", dim("[info]"))
|
||||
} else {
|
||||
fmt.Printf("\n%s pour exécuter 'ajean' depuis n'importe où, ajoute son dossier au PATH.\n", dim("[info]"))
|
||||
fmt.Printf("\n%s pour exécuter 'loki' depuis n'importe où, ajoute son dossier au PATH.\n", dim("[info]"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// installSelf copie l'exécutable en cours dans binDir sous le nom ajean.exe et
|
||||
// installSelf copie l'exécutable en cours dans binDir sous le nom loki.exe et
|
||||
// renvoie le chemin de destination. Sans effet si l'exe tourne déjà depuis là
|
||||
// (ré-installation).
|
||||
//
|
||||
// binDir est créé ici, et pas seulement par l'appelant : sur une machine vierge,
|
||||
// le premier lancement passait par appFirstRun, qui ne créait que le dossier de
|
||||
// données et pas son sous-dossier bin. L'utilisateur recevait alors « open
|
||||
// C:\ProgramData\ajean\bin\ajean.exe: The system cannot find the path specified »
|
||||
// et AJEAN démarrait depuis le fichier téléchargé, sans jamais s'installer.
|
||||
// C:\ProgramData\loki\bin\loki.exe: The system cannot find the path specified »
|
||||
// et Loki démarrait depuis le fichier téléchargé, sans jamais s'installer.
|
||||
func installSelf(binDir string) (string, error) {
|
||||
src, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
src, _ = filepath.EvalSymlinks(src)
|
||||
dst := filepath.Join(binDir, "ajean.exe")
|
||||
dst := filepath.Join(binDir, "loki.exe")
|
||||
if strings.EqualFold(src, dst) {
|
||||
return dst, nil
|
||||
}
|
||||
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// replaceExe et non copyExe : ajean.exe peut etre en cours d'execution
|
||||
// replaceExe et non copyExe : loki.exe peut etre en cours d'execution
|
||||
// (service en tache de fond), auquel cas Windows refuse de l'ecraser.
|
||||
if err := replaceExe(src, dst); err != nil {
|
||||
return "", err
|
||||
@@ -107,7 +107,7 @@ func installSelf(binDir string) (string, error) {
|
||||
//
|
||||
// Windows refuse d'écraser un .exe en cours d'exécution, mais accepte de le
|
||||
// RENOMMER : on décale l'ancien puis on écrit le nouveau à sa place. Sans ça,
|
||||
// l'alias « ajean.exe » restait figé sur une version périmée dès qu'il tournait
|
||||
// l'alias « loki.exe » restait figé sur une version périmée dès qu'il tournait
|
||||
// au moment de la mise à jour — et comme les raccourcis existants le visent
|
||||
// encore, chaque lancement relançait l'ancienne version, qui constatait qu'une
|
||||
// plus récente était installée et le disait. À chaque fois. Constaté en usage.
|
||||
@@ -209,7 +209,7 @@ func cmdUninstall(args []string) error {
|
||||
// Stop the background server if it's running.
|
||||
_ = svcStop(false)
|
||||
|
||||
// Pull AJEAN_HOME\bin off the user PATH (best-effort).
|
||||
// Pull LOKI_HOME\bin off the user PATH (best-effort).
|
||||
dir := binDir()
|
||||
if removed, err := removeFromUserPath(dir); err == nil && removed {
|
||||
fmt.Printf(" %s %s retiré du PATH utilisateur\n", green("✓"), dir)
|
||||
@@ -217,15 +217,15 @@ func cmdUninstall(args []string) error {
|
||||
|
||||
// Raccourcis posés à l'installation (menu Démarrer + Bureau).
|
||||
if removeShortcuts() {
|
||||
fmt.Printf(" %s raccourcis « AJEAN » supprimés\n", green("✓"))
|
||||
fmt.Printf(" %s raccourcis « Loki » supprimés\n", green("✓"))
|
||||
}
|
||||
|
||||
if !keepData {
|
||||
ajeanHome := AjeanHome()
|
||||
if err := os.RemoveAll(ajeanHome); err != nil {
|
||||
return fmt.Errorf("suppression de %s: %w", ajeanHome, err)
|
||||
lokiHome := LokiHome()
|
||||
if err := os.RemoveAll(lokiHome); err != nil {
|
||||
return fmt.Errorf("suppression de %s: %w", lokiHome, err)
|
||||
}
|
||||
fmt.Printf(" %s %s supprimé\n", green("✓"), ajeanHome)
|
||||
fmt.Printf(" %s %s supprimé\n", green("✓"), lokiHome)
|
||||
} else {
|
||||
fmt.Println(dim("(données utilisateur conservées — relance avec --purge pour tout supprimer)"))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -11,19 +11,19 @@ import (
|
||||
//
|
||||
// Le moteur écoute à l'adresse HOST. Sous Windows, l'installation pose
|
||||
// HOST=127.0.0.1 : le chat du navigateur marche (il passe par le serveur web
|
||||
// d'AJEAN, sur la même machine), mais l'endpoint OpenAI :8080/v1 est INVISIBLE
|
||||
// d'Loki, sur la même machine), mais l'endpoint OpenAI :8080/v1 est INVISIBLE
|
||||
// depuis le reste du réseau. Or c'est très exactement ce que les gens viennent
|
||||
// chercher : brancher un logiciel tiers sur le modèle qui tourne dans le salon.
|
||||
//
|
||||
// Deux choses manquaient, et pas une seule :
|
||||
// 1. HOST n'était réglable NULLE PART dans l'interface — il fallait connaître
|
||||
// « ajean edit » et savoir quoi y écrire.
|
||||
// « loki edit » et savoir quoi y écrire.
|
||||
// 2. Même à 0.0.0.0, le pare-feu Windows bloque les connexions entrantes tant
|
||||
// qu'aucune règle n'autorise le port. AJEAN n'en posait aucune.
|
||||
// qu'aucune règle n'autorise le port. Loki n'en posait aucune.
|
||||
//
|
||||
// D'où un seul interrupteur qui fait les deux, et qui DIT ce qu'il n'a pas pu
|
||||
// faire (poser une règle de pare-feu exige les droits administrateur, que
|
||||
// l'installation d'AJEAN ne réclame pas).
|
||||
// l'installation d'Loki ne réclame pas).
|
||||
|
||||
// firewallInert coupe tout pilotage du pare-feu. Positionné par les tests : une
|
||||
// suite qui laisse une règle entrante derrière elle est un effet de bord
|
||||
@@ -55,7 +55,7 @@ func lanExposed() bool {
|
||||
return h != hostLocalOnly && h != "::1" && !strings.EqualFold(h, "localhost")
|
||||
}
|
||||
|
||||
// netStatus est ce que l'interface affiche, et ce que renvoie `ajean network`.
|
||||
// netStatus est ce que l'interface affiche, et ce que renvoie `loki network`.
|
||||
type netStatus struct {
|
||||
Exposed bool `json:"exposed"` // le moteur écoute-t-il sur le réseau
|
||||
Host string `json:"host"` // adresse d'écoute effective
|
||||
@@ -103,7 +103,7 @@ func setLANExposure(on bool) (netStatus, error) {
|
||||
return networkStatus(), nil
|
||||
}
|
||||
|
||||
// cmdNetwork : `ajean network [on|off|status]`.
|
||||
// cmdNetwork : `loki network [on|off|status]`.
|
||||
func cmdNetwork(args []string) error {
|
||||
sub := "status"
|
||||
if len(args) > 0 {
|
||||
@@ -121,7 +121,7 @@ func cmdNetwork(args []string) error {
|
||||
fmt.Printf("%s le moteur n'écoutera plus que sur cette machine\n", green("[ok]"))
|
||||
}
|
||||
printNetHint(st)
|
||||
fmt.Printf("%s redémarre le moteur pour appliquer : %s\n", dim("[info]"), bold("ajean restart"))
|
||||
fmt.Printf("%s redémarre le moteur pour appliquer : %s\n", dim("[info]"), bold("loki restart"))
|
||||
return nil
|
||||
case "status", "":
|
||||
st := networkStatus()
|
||||
@@ -136,7 +136,7 @@ func cmdNetwork(args []string) error {
|
||||
case "ferme":
|
||||
fmt.Printf(" pare-feu : %s\n", yellow("aucune règle pour ce port"))
|
||||
default:
|
||||
fmt.Printf(" pare-feu : %s\n", dim("non géré par AJEAN sur cette plateforme"))
|
||||
fmt.Printf(" pare-feu : %s\n", dim("non géré par Loki sur cette plateforme"))
|
||||
}
|
||||
if st.Exposed {
|
||||
fmt.Printf(" endpoint OpenAI : %s\n", bold(st.URL))
|
||||
@@ -144,7 +144,7 @@ func cmdNetwork(args []string) error {
|
||||
printNetHint(st)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("usage: ajean network [on|off|status]")
|
||||
return fmt.Errorf("usage: loki network [on|off|status]")
|
||||
}
|
||||
|
||||
func printNetHint(st netStatus) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
// emplacements invisibles : on ne sait plus si le .exe qu'on a double-cliqué EST
|
||||
// l'application ou seulement un installateur, ni où atterrissent la config, les
|
||||
// modèles et les fichiers que l'agent écrit. On centralise donc la réponse ici,
|
||||
// et on l'expose à la fois en ligne de commande (`ajean where`) et dans l'UI.
|
||||
// et on l'expose à la fois en ligne de commande (`loki where`) et dans l'UI.
|
||||
|
||||
type ajeanPaths struct {
|
||||
Home string `json:"home"` // AJEAN_HOME : racine des données
|
||||
Database string `json:"database"` // ajean.db : config, préférences, conversation, clés
|
||||
type lokiPaths struct {
|
||||
Home string `json:"home"` // LOKI_HOME : racine des données
|
||||
Database string `json:"database"` // loki.db : config, préférences, conversation, clés
|
||||
Exe string `json:"exe"` // binaire en cours d'exécution
|
||||
Installed string `json:"installed"` // binaire installé (peut différer de Exe)
|
||||
Workspace string `json:"workspace"` // dossier de travail du mode agent
|
||||
@@ -28,23 +28,23 @@ type ajeanPaths struct {
|
||||
}
|
||||
|
||||
// installedExePath est l'emplacement canonique du binaire après installation.
|
||||
// Il diffère par plateforme : sous Unix les installateurs posent /usr/local/bin/ajean
|
||||
// Il diffère par plateforme : sous Unix les installateurs posent /usr/local/bin/loki
|
||||
// (c'est ce que référencent les unités systemd et le plist launchd) ; sous Windows,
|
||||
// faute d'équivalent, on utilise AJEAN_HOME\bin ajouté au PATH utilisateur.
|
||||
// faute d'équivalent, on utilise LOKI_HOME\bin ajouté au PATH utilisateur.
|
||||
func installedExePath() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(binDir(), "ajean.exe")
|
||||
return filepath.Join(binDir(), "loki.exe")
|
||||
}
|
||||
return "/usr/local/bin/ajean"
|
||||
return "/usr/local/bin/loki"
|
||||
}
|
||||
|
||||
func currentPaths() ajeanPaths {
|
||||
func currentPaths() lokiPaths {
|
||||
exe, _ := os.Executable()
|
||||
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
|
||||
exe = resolved
|
||||
}
|
||||
return ajeanPaths{
|
||||
Home: AjeanHome(),
|
||||
return lokiPaths{
|
||||
Home: LokiHome(),
|
||||
Database: dbPath(),
|
||||
Exe: exe,
|
||||
Installed: installedExePath(),
|
||||
@@ -58,9 +58,9 @@ func currentPaths() ajeanPaths {
|
||||
|
||||
func cmdWhere(args []string) error {
|
||||
p := currentPaths()
|
||||
fmt.Printf("Emplacements AJEAN\n\n")
|
||||
fmt.Printf("Emplacements Loki\n\n")
|
||||
for _, row := range [][2]string{
|
||||
{"données (AJEAN_HOME)", p.Home},
|
||||
{"données (LOKI_HOME)", p.Home},
|
||||
{"base", p.Database},
|
||||
{"binaire en cours", p.Exe},
|
||||
{"binaire installé", p.Installed},
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build unix
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -16,22 +16,22 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// defaultAjeanHome est la racine des données quand ni $AJEAN_HOME ni
|
||||
// /etc/default/ajean n'en imposent une.
|
||||
// defaultLokiHome est la racine des données quand ni $LOKI_HOME ni
|
||||
// /etc/default/loki n'en imposent une.
|
||||
//
|
||||
// Linux : /etc/ajean (machine de prod, service systemd lancé par root).
|
||||
// macOS : /etc/ajean n'est PAS écrivable par une app lancée depuis le Finder
|
||||
// (« mkdir /etc/ajean: permission denied »), et un Mac est une machine de bureau,
|
||||
// Linux : /etc/loki (machine de prod, service systemd lancé par root).
|
||||
// macOS : /etc/loki n'est PAS écrivable par une app lancée depuis le Finder
|
||||
// (« mkdir /etc/loki: permission denied »), et un Mac est une machine de bureau,
|
||||
// pas un serveur — on retombe donc sur le dossier utilisateur standard, sauf si
|
||||
// un `sudo ajean install` a déjà créé /etc/ajean à notre nom.
|
||||
func defaultAjeanHome() string { return unixHome("ajean") }
|
||||
// un `sudo loki install` a déjà créé /etc/loki à notre nom.
|
||||
func defaultLokiHome() string { return unixHome("loki") }
|
||||
|
||||
func unixHome(name string) string {
|
||||
if runtime.GOOS != "darwin" || os.Geteuid() == 0 {
|
||||
return "/etc/" + name
|
||||
}
|
||||
if isWritableDir("/etc/" + name) {
|
||||
return "/etc/" + name // installé par `sudo ajean install` puis chown à l'utilisateur
|
||||
return "/etc/" + name // installé par `sudo loki install` puis chown à l'utilisateur
|
||||
}
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
return filepath.Join(home, "Library", "Application Support", name)
|
||||
@@ -46,7 +46,7 @@ func isWritableDir(dir string) bool {
|
||||
if err != nil || !fi.IsDir() {
|
||||
return false
|
||||
}
|
||||
probe := filepath.Join(dir, ".ajean-write-test")
|
||||
probe := filepath.Join(dir, ".loki-write-test")
|
||||
f, err := os.OpenFile(probe, os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -56,7 +56,7 @@ func isWritableDir(dir string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// defaultEditor is used by `ajean edit` when $EDITOR is unset.
|
||||
// defaultEditor is used by `loki edit` when $EDITOR is unset.
|
||||
func defaultEditor() string { return "nano" }
|
||||
|
||||
// hideCmd : no-op sur Unix (pas de fenêtre de console à masquer).
|
||||
@@ -332,7 +332,7 @@ func ramUsageMB() (used, total int) {
|
||||
// --- Supervision de processus détachés (worker de lien, service en mode
|
||||
// utilisateur). Pendant Unix de sys_platform_windows.go.
|
||||
|
||||
// spawnDetached prépare une commande qui survivra à la mort de AJEAN : Setsid la
|
||||
// spawnDetached prépare une commande qui survivra à la mort de Loki : Setsid la
|
||||
// place dans une nouvelle session, donc elle n'est pas tuée avec notre groupe.
|
||||
func spawnDetached(name string, args ...string) *exec.Cmd {
|
||||
cmd := exec.Command(name, args...)
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
// init makes the Windows console behave like a modern terminal: UTF-8 so the
|
||||
// Unicode glyphs ajean prints (✓ ▶ …) and child-process output don't turn into
|
||||
// Unicode glyphs loki prints (✓ ▶ …) and child-process output don't turn into
|
||||
// mojibake (the default OEM codepage, e.g. cp850, renders "✓" as "├ö"), and VT
|
||||
// processing so the ANSI colour/cursor escapes (including the build progress
|
||||
// line) are interpreted instead of printed literally. Best-effort: a redirected
|
||||
@@ -43,7 +43,7 @@ func init() {
|
||||
// homeRoot is the parent directory of the data root: %ProgramData% (machine-wide,
|
||||
// the closest analogue to /etc), falling back to %LOCALAPPDATA% for unprivileged
|
||||
// setups. Ancien et nouveau dossier partagent ce parent, ce qui garantit que la
|
||||
// migration ajean → ajean est un rename intra-volume (voir sys_migrate.go).
|
||||
// migration loki → loki est un rename intra-volume (voir sys_migrate.go).
|
||||
func homeRoot() string {
|
||||
if pd := os.Getenv("ProgramData"); pd != "" {
|
||||
return pd
|
||||
@@ -54,10 +54,10 @@ func homeRoot() string {
|
||||
return os.TempDir()
|
||||
}
|
||||
|
||||
// defaultAjeanHome est la racine des données quand $AJEAN_HOME est absent.
|
||||
func defaultAjeanHome() string { return filepath.Join(homeRoot(), "ajean") }
|
||||
// defaultLokiHome est la racine des données quand $LOKI_HOME est absent.
|
||||
func defaultLokiHome() string { return filepath.Join(homeRoot(), "loki") }
|
||||
|
||||
// defaultEditor is used by `ajean edit` when $EDITOR is unset.
|
||||
// defaultEditor is used by `loki edit` when $EDITOR is unset.
|
||||
func defaultEditor() string { return "notepad" }
|
||||
|
||||
// openBrowser ouvre l'URL dans le navigateur par défaut. rundll32 évite les
|
||||
@@ -67,7 +67,7 @@ func openBrowser(url string) error {
|
||||
}
|
||||
|
||||
// hideCmd empêche une commande externe d'ouvrir une fenêtre de console
|
||||
// (CREATE_NO_WINDOW). Indispensable quand AJEAN tourne sans console (mode app) :
|
||||
// (CREATE_NO_WINDOW). Indispensable quand Loki tourne sans console (mode app) :
|
||||
// sinon chaque `nvidia-smi`/`git`/… ferait clignoter une fenêtre noire. Fusionne
|
||||
// avec les flags déjà présents pour ne pas écraser un éventuel détachement.
|
||||
func hideCmd(cmd *exec.Cmd) *exec.Cmd {
|
||||
@@ -144,7 +144,7 @@ func libraryPathEnv(dir string) []string {
|
||||
}
|
||||
|
||||
// execServer runs llama-server as a child process and waits for it. Windows has
|
||||
// no exec() that replaces the current image, so `ajean serve` stays alive as the
|
||||
// no exec() that replaces the current image, so `loki serve` stays alive as the
|
||||
// parent (this is the detached process the service supervisor tracks).
|
||||
// args[0] is the binary path; the rest are its arguments.
|
||||
func execServer(bin string, args []string) error {
|
||||
@@ -246,7 +246,7 @@ func msvcGenerator() string {
|
||||
|
||||
// ensureCompiler makes sure an MSVC C++ toolchain is available, installing the
|
||||
// Visual Studio 2022 Build Tools (VCTools workload) via winget if not. This is a
|
||||
// large download but keeps `ajean llamacpp install` fully unattended on a bare
|
||||
// large download but keeps `loki llamacpp install` fully unattended on a bare
|
||||
// Windows box.
|
||||
func ensureCompiler() error {
|
||||
if msvcInstallVersion() != "" {
|
||||
@@ -368,7 +368,7 @@ Sans elle, CMake échoue sur « No CUDA toolset found ». Pour corriger, au choi
|
||||
1. relance l'installeur du CUDA Toolkit (installation personnalisée) et coche « CUDA → Visual Studio Integration » — Visual Studio doit déjà être installé à ce moment-là ;
|
||||
2. ou copie (en admin) les fichiers de
|
||||
%s
|
||||
vers le dossier BuildCustomizations ci-dessus, puis relance ajean llamacpp install`, installPath, src)
|
||||
vers le dossier BuildCustomizations ci-dessus, puis relance loki llamacpp install`, installPath, src)
|
||||
}
|
||||
|
||||
// cudaPathEnv returns the CUDA toolkit env vars the MSBuild CUDA integration
|
||||
@@ -386,7 +386,7 @@ func cudaPathEnv(toolkitDir string) []string {
|
||||
}
|
||||
|
||||
// newShellCmd builds the command used by the run_shell tool. hideCmd évite un
|
||||
// flash de console à chaque commande d'agent quand AJEAN tourne en mode app.
|
||||
// flash de console à chaque commande d'agent quand Loki tourne en mode app.
|
||||
func newShellCmd(ctx context.Context, command string) *exec.Cmd {
|
||||
return hideCmd(exec.CommandContext(ctx, "cmd", "/C", command))
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build !windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// Pendant non-Windows de sys_restart_windows.go : sous Linux le redemarrage
|
||||
// passe par systemd (restartAfterUpdate), qui sait deja relancer proprement un
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// lancée avec une sous-commande interne — qui attend notre disparition, profite
|
||||
// de cette fenêtre pour terminer la migration, puis relance l'application.
|
||||
//
|
||||
// C'est cette fenêtre qui manquait. Une mise à jour en place laisse AJEAN
|
||||
// C'est cette fenêtre qui manquait. Une mise à jour en place laisse Loki
|
||||
// tourner, et donc le dossier tenu : le renommage échouait avec « Accès refusé »,
|
||||
// qui sous Windows ne distingue pas un verrou d'un manque de droits. D'où des
|
||||
// postes qui restaient indéfiniment sur l'ancien nom.
|
||||
@@ -44,7 +44,7 @@ func scheduleAppRestart() (bool, string) {
|
||||
time.Sleep(1500 * time.Millisecond) // laisser la réponse atteindre le navigateur
|
||||
os.Exit(0)
|
||||
}()
|
||||
return true, "AJEAN redémarre — la page se reconnectera toute seule dans quelques secondes."
|
||||
return true, "Loki redémarre — la page se reconnectera toute seule dans quelques secondes."
|
||||
}
|
||||
|
||||
// cmdRestartAfterUpdate est exécuté par l'accompagnateur détaché.
|
||||
@@ -1,4 +1,4 @@
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -19,33 +19,33 @@ import (
|
||||
|
||||
// preflightEngine vérifie ce sans quoi le moteur ne PEUT pas démarrer, avant de
|
||||
// lancer le service. Sinon llama-server sortait en erreur, systemd le relançait
|
||||
// toutes les 3 s, et `ajean start` affichait un « activating » rassurant pendant
|
||||
// que `ajean test` répondait « /health ne répond pas ». Le diagnostic n'était
|
||||
// toutes les 3 s, et `loki start` affichait un « activating » rassurant pendant
|
||||
// que `loki test` répondait « /health ne répond pas ». Le diagnostic n'était
|
||||
// visible que dans le journal.
|
||||
func preflightEngine() error {
|
||||
cfg := ReadConfig()
|
||||
bin := strings.TrimSpace(cfg["BIN"])
|
||||
if bin == "" {
|
||||
return fmt.Errorf("BIN non défini — installe un moteur : %s (ou renseigne BIN avec %s)",
|
||||
bold("ajean llamacpp install"), bold("ajean edit"))
|
||||
bold("loki llamacpp install"), bold("loki edit"))
|
||||
}
|
||||
if !filepath.IsAbs(bin) {
|
||||
bin = filepath.Join(AjeanHome(), bin)
|
||||
bin = filepath.Join(LokiHome(), bin)
|
||||
}
|
||||
if _, err := os.Stat(prebuiltResolveBin(bin)); err != nil {
|
||||
return fmt.Errorf("moteur introuvable : %s — relance %s", bin, bold("ajean llamacpp install"))
|
||||
return fmt.Errorf("moteur introuvable : %s — relance %s", bin, bold("loki llamacpp install"))
|
||||
}
|
||||
model := strings.TrimSpace(cfg["MODEL"])
|
||||
if model == "" {
|
||||
return fmt.Errorf("MODEL non défini — indique un .gguf avec %s (dossier des modèles : %s)",
|
||||
bold("ajean edit"), modelsDir())
|
||||
bold("loki edit"), modelsDir())
|
||||
}
|
||||
p, err := resolveServeModelPath(model)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MODEL=%s : %w", model, err)
|
||||
}
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
return fmt.Errorf("modèle introuvable : %s — corrige MODEL avec %s", p, bold("ajean edit"))
|
||||
return fmt.Errorf("modèle introuvable : %s — corrige MODEL avec %s", p, bold("loki edit"))
|
||||
}
|
||||
// Modèle en plusieurs fichiers : une tranche manquante ne se voit qu'au moment
|
||||
// où llama-server réclame un tenseur absent, dans le journal du service.
|
||||
@@ -56,13 +56,13 @@ func preflightEngine() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// configTemplate est le squelette commenté proposé par `ajean edit` : la
|
||||
// configTemplate est le squelette commenté proposé par `loki edit` : la
|
||||
// configuration vit en base, donc sur une installation neuve le fichier
|
||||
// temporaire était QUASI VIDE — impossible de deviner quoi écrire. On y déroule
|
||||
// donc les clés utiles avec leur rôle, les valeurs déjà définies telles quelles,
|
||||
// les autres commentées.
|
||||
var configTemplate = []struct{ key, help string }{
|
||||
{"BIN", "chemin de llama-server (posé par « ajean llamacpp install »)"},
|
||||
{"BIN", "chemin de llama-server (posé par « loki llamacpp install »)"},
|
||||
{"MODEL", "nom de fichier .gguf ou chemin complet"},
|
||||
{"HOST", "adresse d'écoute du moteur (défaut 0.0.0.0)"},
|
||||
{"PORT", "port du moteur (défaut 8080)"},
|
||||
@@ -83,9 +83,9 @@ var configTemplate = []struct{ key, help string }{
|
||||
// configEditorText rend la configuration au format présenté dans $EDITOR.
|
||||
func configEditorText(cfg map[string]string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("# Configuration du moteur AJEAN (ajean-engine).\n")
|
||||
b.WriteString("# Configuration du moteur Loki (loki-engine).\n")
|
||||
b.WriteString("# Une clé par ligne : CLE=valeur. Les lignes commentées (#) sont ignorées :\n")
|
||||
b.WriteString("# décommente celles dont tu as besoin. « ajean restart » applique.\n\n")
|
||||
b.WriteString("# décommente celles dont tu as besoin. « loki restart » applique.\n\n")
|
||||
seen := map[string]bool{}
|
||||
for _, f := range configTemplate {
|
||||
seen[f.key] = true
|
||||
@@ -120,7 +120,7 @@ func editConfig() error {
|
||||
if editor == "" {
|
||||
editor = defaultEditor()
|
||||
}
|
||||
tmp, err := os.CreateTemp("", "ajean-config-*.env")
|
||||
tmp, err := os.CreateTemp("", "loki-config-*.env")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func editConfig() error {
|
||||
if err := WriteConfig(parseEnv(string(b))); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(dim("[info] ajean restart pour appliquer"))
|
||||
fmt.Println(dim("[info] loki restart pour appliquer"))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build darwin
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -18,16 +18,16 @@ import (
|
||||
// le support macOS était absent (le code systemd/Linux était utilisé par erreur,
|
||||
// cf. issue #4). Implémentation prudente basée sur launchctl load/unload/list.
|
||||
|
||||
// launchdLabel dérive le label launchd du service (ex. "com.ajean.ajean"), en
|
||||
// conservant le préfixe hérité « com.ajean. » tant que c'est LUI qui est
|
||||
// launchdLabel dérive le label launchd du service (ex. "com.loki.loki"), en
|
||||
// conservant le préfixe hérité « com.loki. » tant que c'est LUI qui est
|
||||
// réellement installé : le plist d'un daemon déjà chargé ne se renomme pas tout
|
||||
// seul, et viser le mauvais label reviendrait à ne plus voir le service.
|
||||
func launchdLabel(svc string) string {
|
||||
modern := "com.ajean." + svc
|
||||
modern := "com.loki." + svc
|
||||
if _, err := os.Stat("/Library/LaunchDaemons/" + modern + ".plist"); err == nil {
|
||||
return modern
|
||||
}
|
||||
if legacy := "com.ajean." + svc; fileExists("/Library/LaunchDaemons/" + legacy + ".plist") {
|
||||
if legacy := "com.loki." + svc; fileExists("/Library/LaunchDaemons/" + legacy + ".plist") {
|
||||
return legacy
|
||||
}
|
||||
return modern
|
||||
@@ -44,18 +44,18 @@ func launchdPlistPath(svc string) string {
|
||||
return "/Library/LaunchDaemons/" + launchdLabel(svc) + ".plist"
|
||||
}
|
||||
|
||||
// launchdLogPath : sortie standard/erreur du service, sous AJEAN_HOME (accessible
|
||||
// launchdLogPath : sortie standard/erreur du service, sous LOKI_HOME (accessible
|
||||
// en écriture par l'utilisateur du service après le chown de l'installation).
|
||||
func launchdLogPath() string { return filepath.Join(AjeanHome(), serviceName()+".log") }
|
||||
func launchdLogPath() string { return filepath.Join(LokiHome(), serviceName()+".log") }
|
||||
|
||||
// serviceAction mappe start/stop/restart/enable/disable sur launchctl. `load -w`
|
||||
// (re)active le service ET le rend persistant au boot ; `unload -w` le désactive.
|
||||
func serviceAction(action string) error {
|
||||
svc := serviceName()
|
||||
plist := launchdPlistPath(svc)
|
||||
// Pas de LaunchDaemon installé — cas normal d'un Mac de bureau, où AJEAN tourne
|
||||
// via AJEAN.app sans jamais passer par `sudo ajean install` : on gère alors le
|
||||
// service comme sous Windows, avec un `ajean serve` détaché suivi par un fichier
|
||||
// Pas de LaunchDaemon installé — cas normal d'un Mac de bureau, où Loki tourne
|
||||
// via Loki.app sans jamais passer par `sudo loki install` : on gère alors le
|
||||
// service comme sous Windows, avec un `loki serve` détaché suivi par un fichier
|
||||
// PID. Aucun droit root requis. Sans ça, `start` échouait sur un plist absent
|
||||
// et l'UI affichait éternellement « service arrêté ».
|
||||
if _, err := os.Stat(plist); err != nil {
|
||||
@@ -116,7 +116,7 @@ func checkStarted(svc string) error {
|
||||
fmt.Println(strings.Join(lines, "\n"))
|
||||
}
|
||||
fmt.Println("------------------------------------------------")
|
||||
fmt.Printf("→ ajean logs pour plus de détails\n→ ajean edit pour corriger config.env\n")
|
||||
fmt.Printf("→ loki logs pour plus de détails\n→ loki edit pour corriger config.env\n")
|
||||
return fmt.Errorf("service %s non démarré", svc)
|
||||
}
|
||||
|
||||
@@ -143,14 +143,14 @@ func serviceIsActive() bool {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mode utilisateur (sans launchd) — même approche que Windows : `ajean serve` est
|
||||
// lancé détaché, son PID va dans AJEAN_HOME/<svc>.pid et sa sortie dans
|
||||
// AJEAN_HOME/<svc>.log. Setsid le détache de notre session pour qu'il survive à
|
||||
// la fermeture de AJEAN.app, et permet de tuer tout le groupe (llama-server
|
||||
// Mode utilisateur (sans launchd) — même approche que Windows : `loki serve` est
|
||||
// lancé détaché, son PID va dans LOKI_HOME/<svc>.pid et sa sortie dans
|
||||
// LOKI_HOME/<svc>.log. Setsid le détache de notre session pour qu'il survive à
|
||||
// la fermeture de Loki.app, et permet de tuer tout le groupe (llama-server
|
||||
// enfant compris) d'un seul signal.
|
||||
|
||||
func pidFilePath() string { return filepath.Join(AjeanHome(), serviceName()+".pid") }
|
||||
func logFilePath() string { return filepath.Join(AjeanHome(), serviceName()+".log") }
|
||||
func pidFilePath() string { return filepath.Join(LokiHome(), serviceName()+".pid") }
|
||||
func logFilePath() string { return filepath.Join(LokiHome(), serviceName()+".log") }
|
||||
|
||||
func userSvcAction(action string) error {
|
||||
switch action {
|
||||
@@ -173,7 +173,7 @@ func userSvcAction(action string) error {
|
||||
return nil
|
||||
case "enable", "disable":
|
||||
fmt.Printf("%s '%s' réclame un service système : lance %s pour installer le LaunchDaemon.\n",
|
||||
yellow("[info]"), action, bold("sudo ajean install"))
|
||||
yellow("[info]"), action, bold("sudo loki install"))
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("action inconnue: %s", action)
|
||||
@@ -188,7 +188,7 @@ func userSvcStart() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(AjeanHome(), 0o755); err != nil {
|
||||
if err := os.MkdirAll(LokiHome(), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
logf, err := os.OpenFile(logFilePath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
@@ -200,12 +200,12 @@ func userSvcStart() error {
|
||||
cmd := exec.Command(self, "serve")
|
||||
cmd.Stdout, cmd.Stderr = logf, logf
|
||||
// Même répertoire de travail que sous systemd/launchd : les chemins relatifs
|
||||
// de config.env (MODEL=…gguf) se résolvent depuis AJEAN_HOME, pas depuis « / »
|
||||
// de config.env (MODEL=…gguf) se résolvent depuis LOKI_HOME, pas depuis « / »
|
||||
// que nous hérite le Finder.
|
||||
cmd.Dir = AjeanHome()
|
||||
cmd.Dir = LokiHome()
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("démarrage de 'ajean serve': %w", err)
|
||||
return fmt.Errorf("démarrage de 'loki serve': %w", err)
|
||||
}
|
||||
pid := cmd.Process.Pid
|
||||
if err := os.WriteFile(pidFilePath(), []byte(strconv.Itoa(pid)), 0o644); err != nil {
|
||||
@@ -219,14 +219,14 @@ func userCheckStarted(pid int) error {
|
||||
time.Sleep(2 * time.Second)
|
||||
if processAlive(pid) {
|
||||
fmt.Printf("%s %s: démarré (PID %d)\n", green("[ok]"), serviceName(), pid)
|
||||
fmt.Printf(" logs: %s (ajean logs pour suivre)\n", dim(logFilePath()))
|
||||
fmt.Printf(" logs: %s (loki logs pour suivre)\n", dim(logFilePath()))
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("%s %s: le processus s'est arrêté — derniers logs :\n", red("[ERREUR]"), serviceName())
|
||||
fmt.Println("------------------------------------------------")
|
||||
fmt.Print(tailFile(logFilePath(), 20))
|
||||
fmt.Println("------------------------------------------------")
|
||||
fmt.Printf("→ ajean logs pour plus de détails\n→ ajean edit pour corriger config.env\n")
|
||||
fmt.Printf("→ loki logs pour plus de détails\n→ loki edit pour corriger config.env\n")
|
||||
_ = os.Remove(pidFilePath())
|
||||
return fmt.Errorf("service %s non démarré", serviceName())
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build linux
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -65,9 +65,9 @@ func unitState(svc string) (active, sub string) {
|
||||
//
|
||||
// ⚠️ « activating » n'est PAS un succès : avec Restart=on-failure, un moteur qui
|
||||
// meurt au démarrage repasse en boucle par activating (SubState auto-restart).
|
||||
// L'ancien test l'acceptait, et `ajean start` répondait « [ok] activating » sur
|
||||
// un service qui ne démarrerait jamais — d'où des rapports « ajean start dit ok
|
||||
// mais ajean test ne répond pas ». On distingue donc le SubState, et on laisse
|
||||
// L'ancien test l'acceptait, et `loki start` répondait « [ok] activating » sur
|
||||
// un service qui ne démarrerait jamais — d'où des rapports « loki start dit ok
|
||||
// mais loki test ne répond pas ». On distingue donc le SubState, et on laisse
|
||||
// au moteur le temps de charger un gros modèle (il reste en activating, mais
|
||||
// PAS en auto-restart).
|
||||
func checkStarted(svc string) error {
|
||||
@@ -86,7 +86,7 @@ func checkStarted(svc string) error {
|
||||
if state == "activating" && sub != "auto-restart" {
|
||||
// Chargement en cours (un gros .gguf prend des minutes) : légitime.
|
||||
fmt.Printf("%s %s: démarrage en cours (chargement du modèle) — %s pour suivre\n",
|
||||
green("[ok]"), svc, bold("ajean logs"))
|
||||
green("[ok]"), svc, bold("loki logs"))
|
||||
return nil
|
||||
}
|
||||
what := state
|
||||
@@ -98,7 +98,7 @@ func checkStarted(svc string) error {
|
||||
logs, _ := exec.Command("journalctl", "-u", svc, "-n", "20", "--no-pager").Output()
|
||||
fmt.Print(string(logs))
|
||||
fmt.Println("------------------------------------------------")
|
||||
fmt.Printf("→ ajean logs pour plus de détails\n→ ajean edit pour corriger la configuration (BIN, MODEL…)\n")
|
||||
fmt.Printf("→ loki logs pour plus de détails\n→ loki edit pour corriger la configuration (BIN, MODEL…)\n")
|
||||
return fmt.Errorf("service %s non démarré", svc)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
)
|
||||
|
||||
// On Windows there is no systemd, so "the service" is a background copy of
|
||||
// `ajean serve` that this process launches detached. We track it with a PID file
|
||||
// and stream its output to a log file under AJEAN_HOME. This needs no admin
|
||||
// `loki serve` that this process launches detached. We track it with a PID file
|
||||
// and stream its output to a log file under LOKI_HOME. This needs no admin
|
||||
// rights and no external tools beyond the always-present tasklist/taskkill.
|
||||
|
||||
const (
|
||||
@@ -26,8 +26,8 @@ const (
|
||||
createNoWindow = 0x08000000 // CREATE_NO_WINDOW (aucune console pour l'enfant)
|
||||
)
|
||||
|
||||
func pidFilePath() string { return filepath.Join(AjeanHome(), serviceName()+".pid") }
|
||||
func logFilePath() string { return filepath.Join(AjeanHome(), serviceName()+".log") }
|
||||
func pidFilePath() string { return filepath.Join(LokiHome(), serviceName()+".pid") }
|
||||
func logFilePath() string { return filepath.Join(LokiHome(), serviceName()+".log") }
|
||||
|
||||
func serviceAction(action string) error {
|
||||
switch action {
|
||||
@@ -59,7 +59,7 @@ func svcStart() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(AjeanHome(), 0o755); err != nil {
|
||||
if err := os.MkdirAll(LokiHome(), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
logf, err := os.OpenFile(logFilePath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
@@ -71,15 +71,15 @@ func svcStart() error {
|
||||
cmd := exec.Command(self, "serve")
|
||||
cmd.Stdout = logf
|
||||
cmd.Stderr = logf
|
||||
cmd.Dir = AjeanHome() // les chemins relatifs de config.env se résolvent depuis AJEAN_HOME
|
||||
// createNoWindow + HideWindow : le service enfant (`ajean serve`) ne doit JAMAIS
|
||||
// faire clignoter de console noire quand AJEAN est lancé en mode app (double-clic).
|
||||
cmd.Dir = LokiHome() // les chemins relatifs de config.env se résolvent depuis LOKI_HOME
|
||||
// createNoWindow + HideWindow : le service enfant (`loki serve`) ne doit JAMAIS
|
||||
// faire clignoter de console noire quand Loki est lancé en mode app (double-clic).
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
CreationFlags: createNewProcessGroup | detachedProcess | createNoWindow,
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("démarrage de 'ajean serve': %w", err)
|
||||
return fmt.Errorf("démarrage de 'loki serve': %w", err)
|
||||
}
|
||||
pid := cmd.Process.Pid
|
||||
if err := os.WriteFile(pidFilePath(), []byte(strconv.Itoa(pid)), 0o644); err != nil {
|
||||
@@ -94,14 +94,14 @@ func checkStarted(pid int) error {
|
||||
time.Sleep(2 * time.Second)
|
||||
if processAlive(pid) {
|
||||
fmt.Printf("%s %s: démarré (PID %d)\n", green("[ok]"), serviceName(), pid)
|
||||
fmt.Printf(" logs: %s (ajean logs pour suivre)\n", dim(logFilePath()))
|
||||
fmt.Printf(" logs: %s (loki logs pour suivre)\n", dim(logFilePath()))
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("%s %s: le processus s'est arrêté — derniers logs :\n", red("[ERREUR]"), serviceName())
|
||||
fmt.Println("------------------------------------------------")
|
||||
fmt.Print(tailFile(logFilePath(), 20))
|
||||
fmt.Println("------------------------------------------------")
|
||||
fmt.Printf("→ ajean logs pour plus de détails\n→ ajean edit pour corriger config.env\n")
|
||||
fmt.Printf("→ loki logs pour plus de détails\n→ loki edit pour corriger config.env\n")
|
||||
_ = os.Remove(pidFilePath())
|
||||
return fmt.Errorf("service %s non démarré", serviceName())
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build !windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// sys_splash_other.go — pas de fenêtre de démarrage hors Windows (no-op).
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//go:build windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// sys_splash_windows.go — écran de démarrage « Lancement de AJEAN… ».
|
||||
// sys_splash_windows.go — écran de démarrage « Lancement de Loki… ».
|
||||
//
|
||||
// Petite fenêtre sans bordure, à coins arrondis, aux couleurs de la marque
|
||||
// (fond bleu #1f6feb, logo « j » blanc, texte blanc), affichée le temps que le
|
||||
@@ -127,7 +127,7 @@ func (s *splash) close() {
|
||||
}
|
||||
|
||||
func createSplashWindow() uintptr {
|
||||
className, _ := syscall.UTF16PtrFromString("AJEANSplash")
|
||||
className, _ := syscall.UTF16PtrFromString("LokiSplash")
|
||||
hInst, _, _ := pGetModuleHandleW.Call(0)
|
||||
|
||||
splashClassOnce.Do(func() {
|
||||
@@ -154,7 +154,7 @@ func createSplashWindow() uintptr {
|
||||
cy, _, _ := pGetSystemMetrics.Call(smCY)
|
||||
x := (int(cx) - splashW) / 2
|
||||
y := (int(cy) - splashH) / 2
|
||||
title, _ := syscall.UTF16PtrFromString("AJEAN")
|
||||
title, _ := syscall.UTF16PtrFromString("Loki")
|
||||
|
||||
hwnd, _, _ := pCreateWindowExW.Call(
|
||||
uintptr(exTopmost|exToolwindow),
|
||||
@@ -243,7 +243,7 @@ func paintSplash(hdc uintptr) {
|
||||
pSetTextColor.Call(hdc, colWhite)
|
||||
|
||||
textX := ox + logo + 26
|
||||
drawText(hdc, "AJEAN", -30, 700, rect{textX, 40, splashW - 20, 82})
|
||||
drawText(hdc, "Loki", -30, 700, rect{textX, 40, splashW - 20, 82})
|
||||
drawText(hdc, "Lancement en cours…", -17, 400, rect{textX, 84, splashW - 20, 118})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//go:build darwin
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// sys_tray_darwin.go — icône de AJEAN dans la barre de menus macOS (en haut à
|
||||
// sys_tray_darwin.go — icône de Loki dans la barre de menus macOS (en haut à
|
||||
// droite, à côté de l'horloge), pendant exact du system tray Windows : quand on
|
||||
// ouvre AJEAN.app, l'UI web s'ouvre dans le navigateur ET l'icône apparaît, avec
|
||||
// le menu « Ouvrir AJEAN » / « Quitter ».
|
||||
// ouvre Loki.app, l'UI web s'ouvre dans le navigateur ET l'icône apparaît, avec
|
||||
// le menu « Ouvrir Loki » / « Quitter ».
|
||||
//
|
||||
// Contrairement à Windows, systray passe ici par Cocoa → CGO obligatoire : les
|
||||
// binaires macOS sont donc compilés sur un runner macOS (voir release.yml), et
|
||||
@@ -26,10 +26,10 @@ func runTray(url string) {
|
||||
// colore selon le thème. Indispensable depuis que la marque est noire —
|
||||
// une icône noire « en dur » est illisible sur une barre de menus sombre.
|
||||
systray.SetTemplateIcon(brandTemplatePNG(trayIconSize), BrandIconPNG(trayIconSize))
|
||||
systray.SetTooltip("AJEAN — votre IA locale")
|
||||
mOpen := systray.AddMenuItem("Ouvrir AJEAN", "Ouvrir l'interface")
|
||||
systray.SetTooltip("Loki — votre IA locale")
|
||||
mOpen := systray.AddMenuItem("Ouvrir Loki", "Ouvrir l'interface")
|
||||
systray.AddSeparator()
|
||||
mQuit := systray.AddMenuItem("Quitter", "Arrêter AJEAN")
|
||||
mQuit := systray.AddMenuItem("Quitter", "Arrêter Loki")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build !windows && !darwin
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// sys_tray_other.go — pas d'icône de zone de notification sous Linux (serveur
|
||||
// sans session graphique la plupart du temps). On se contente d'ouvrir l'UI dans
|
||||
@@ -1,11 +1,11 @@
|
||||
//go:build windows
|
||||
|
||||
package ajean
|
||||
package loki
|
||||
|
||||
// sys_tray_windows.go — icône de AJEAN dans la zone de notification Windows (system
|
||||
// tray). Quand on lance l'app (double-clic ou `ajean app`), l'UI web s'ouvre dans
|
||||
// sys_tray_windows.go — icône de Loki dans la zone de notification Windows (system
|
||||
// tray). Quand on lance l'app (double-clic ou `loki app`), l'UI web s'ouvre dans
|
||||
// le navigateur ET une petite icône apparaît près de l'horloge : elle montre que
|
||||
// AJEAN tourne et offre un menu « Ouvrir AJEAN » / « Quitter ».
|
||||
// Loki tourne et offre un menu « Ouvrir Loki » / « Quitter ».
|
||||
//
|
||||
// getlantern/systray est pur Go sur Windows (Win32 via x/sys, aucun CGO) — le
|
||||
// binaire reste statique. L'équivalent macOS vit dans sys_tray_darwin.go (Cocoa,
|
||||
@@ -22,11 +22,11 @@ import (
|
||||
func runTray(url string) {
|
||||
systray.Run(func() {
|
||||
systray.SetIcon(trayIcon())
|
||||
systray.SetTitle("AJEAN")
|
||||
systray.SetTooltip("AJEAN — votre IA locale")
|
||||
mOpen := systray.AddMenuItem("Ouvrir AJEAN", "Ouvrir l'interface")
|
||||
systray.SetTitle("Loki")
|
||||
systray.SetTooltip("Loki — votre IA locale")
|
||||
mOpen := systray.AddMenuItem("Ouvrir Loki", "Ouvrir l'interface")
|
||||
systray.AddSeparator()
|
||||
mQuit := systray.AddMenuItem("Quitter", "Arrêter AJEAN et décharger le modèle")
|
||||
mQuit := systray.AddMenuItem("Quitter", "Arrêter Loki et décharger le modèle")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
Loaded 100 of 157 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user