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:
Loki committed 2026-08-14 21:33:15 +00:00
1 parent 2164b46319
commit de6551a153
157 files changed
+928 -928

No files matched your search

+3 -3
View File
@@ -28,7 +28,7 @@ jobs:
# par Cocoa (CGO) → les cibles macOS se compilent sur un runner Apple # par Cocoa (CGO) → les cibles macOS se compilent sur un runner Apple
# (job « macos » ci-dessous, comme dans release.yml). # (job « macos » ci-dessous, comme dans release.yml).
for t in linux/amd64 linux/arm64 windows/amd64 windows/arm64; do for t in linux/amd64 linux/arm64 windows/amd64 windows/arm64; do
GOOS=${t%/*} GOARCH=${t#*/} CGO_ENABLED=0 go build -o /dev/null ./cmd/ajean GOOS=${t%/*} GOARCH=${t#*/} CGO_ENABLED=0 go build -o /dev/null ./cmd/loki
done done
macos: macos:
@@ -45,7 +45,7 @@ jobs:
- run: go test ./... - run: go test ./...
- name: build macOS (CGO, arm64 + amd64) - name: build macOS (CGO, arm64 + amd64)
run: | run: |
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o /dev/null ./cmd/ajean CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o /dev/null ./cmd/loki
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 \ CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 \
CGO_CFLAGS="-arch x86_64" CGO_LDFLAGS="-arch x86_64" \ CGO_CFLAGS="-arch x86_64" CGO_LDFLAGS="-arch x86_64" \
go build -o /dev/null ./cmd/ajean go build -o /dev/null ./cmd/loki
+19 -19
View File
@@ -1,4 +1,4 @@
# Build macOS à la demande, SANS rien publier : produit AJEAN.app en artifact # Build macOS à la demande, SANS rien publier : produit Loki.app en artifact
# téléchargeable depuis l'onglet Actions (visible seulement par le dépôt). # téléchargeable depuis l'onglet Actions (visible seulement par le dépôt).
# Sert à tester le bundle avant de décider d'une vraie release (release.yml). # Sert à tester le bundle avant de décider d'une vraie release (release.yml).
name: mac-build name: mac-build
@@ -13,40 +13,40 @@ jobs:
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version-file: go.mod go-version-file: go.mod
- name: build + bundle AJEAN.app (arm64) - name: build + bundle Loki.app (arm64)
run: | run: |
mkdir -p dist mkdir -p dist
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 \ CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 \
go build -trimpath -ldflags="-s -w" -o dist/ajean-macos-arm ./cmd/ajean go build -trimpath -ldflags="-s -w" -o dist/loki-macos-arm ./cmd/loki
mkdir -p /tmp/ajean.iconset mkdir -p /tmp/loki.iconset
sips -s format png 'cmd/ajean/icon.ico' --out /tmp/icon.png >/dev/null 2>&1 || true sips -s format png 'cmd/loki/icon.ico' --out /tmp/icon.png >/dev/null 2>&1 || true
for s in 16 32 128 256 512; do for s in 16 32 128 256 512; do
sips -z $s $s /tmp/icon.png --out "/tmp/ajean.iconset/icon_${s}x${s}.png" >/dev/null 2>&1 || true sips -z $s $s /tmp/icon.png --out "/tmp/loki.iconset/icon_${s}x${s}.png" >/dev/null 2>&1 || true
done done
iconutil -c icns /tmp/ajean.iconset -o /tmp/ajean.icns >/dev/null 2>&1 || true iconutil -c icns /tmp/loki.iconset -o /tmp/loki.icns >/dev/null 2>&1 || true
app="dist/AJEAN.app" app="dist/Loki.app"
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources"
cp dist/ajean-macos-arm "$app/Contents/MacOS/ajean" cp dist/loki-macos-arm "$app/Contents/MacOS/loki"
chmod +x "$app/Contents/MacOS/ajean" chmod +x "$app/Contents/MacOS/loki"
cp /tmp/ajean.icns "$app/Contents/Resources/ajean.icns" 2>/dev/null || true cp /tmp/loki.icns "$app/Contents/Resources/loki.icns" 2>/dev/null || true
printf '%s\n' \ printf '%s\n' \
'<?xml version="1.0" encoding="UTF-8"?>' \ '<?xml version="1.0" encoding="UTF-8"?>' \
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
'<plist version="1.0"><dict>' \ '<plist version="1.0"><dict>' \
'<key>CFBundleName</key><string>AJEAN</string>' \ '<key>CFBundleName</key><string>Loki</string>' \
'<key>CFBundleDisplayName</key><string>AJEAN</string>' \ '<key>CFBundleDisplayName</key><string>Loki</string>' \
'<key>CFBundleExecutable</key><string>ajean</string>' \ '<key>CFBundleExecutable</key><string>loki</string>' \
'<key>CFBundleIdentifier</key><string>link.ajean.app</string>' \ '<key>CFBundleIdentifier</key><string>link.loki.app</string>' \
'<key>CFBundleIconFile</key><string>ajean</string>' \ '<key>CFBundleIconFile</key><string>loki</string>' \
'<key>CFBundlePackageType</key><string>APPL</string>' \ '<key>CFBundlePackageType</key><string>APPL</string>' \
'<key>LSMinimumSystemVersion</key><string>11.0</string>' \ '<key>LSMinimumSystemVersion</key><string>11.0</string>' \
'<key>LSUIElement</key><true/>' \ '<key>LSUIElement</key><true/>' \
'<key>NSHighResolutionCapable</key><true/>' \ '<key>NSHighResolutionCapable</key><true/>' \
'</dict></plist>' > "$app/Contents/Info.plist" '</dict></plist>' > "$app/Contents/Info.plist"
codesign --force --deep -s - "$app" || true codesign --force --deep -s - "$app" || true
(cd dist && zip -qry ajean-macos-arm.zip AJEAN.app && rm -rf AJEAN.app) (cd dist && zip -qry loki-macos-arm.zip Loki.app && rm -rf Loki.app)
ls -l dist ls -l dist
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: ajean-macos-arm name: loki-macos-arm
path: dist/ajean-macos-arm.zip path: dist/loki-macos-arm.zip
+34 -34
View File
@@ -1,11 +1,11 @@
# Release : sur tag vX.Y.Z, compile les 6 cibles et publie la release GitHub # Release : sur tag vX.Y.Z, compile les 6 cibles et publie la release GitHub
# avec les binaires (ajean-linux, ajean-macos, ajean-windows.exe et leurs # avec les binaires (loki-linux, loki-macos, loki-windows.exe et leurs
# variantes -arm) ET le fichier SHA256SUMS que `ajean update` vérifie avant # variantes -arm) ET le fichier SHA256SUMS que `loki update` vérifie avant
# d'installer. Ces noms doivent rester en phase avec assetNameFor (sys_update.go), # d'installer. Ces noms doivent rester en phase avec assetNameFor (sys_update.go),
# sinon plus aucune machine ne se met à jour. # sinon plus aucune machine ne se met à jour.
# ⚠️ NE PAS uploader de binaires à la main en plus : ce workflow écrase les # ⚠️ NE PAS uploader de binaires à la main en plus : ce workflow écrase les
# assets du même nom (softprops remplace), et des sommes qui ne correspondent # assets du même nom (softprops remplace), et des sommes qui ne correspondent
# plus aux binaires font échouer `ajean update` (vécu sur v0.4.7). # plus aux binaires font échouer `loki update` (vécu sur v0.4.7).
# #
# Deux jobs de build : macOS a besoin d'un vrai runner Apple (l'icône de barre de # Deux jobs de build : macOS a besoin d'un vrai runner Apple (l'icône de barre de
# menus passe par Cocoa → CGO, pas cross-compilable depuis Linux) ; Linux et # menus passe par Cocoa → CGO, pas cross-compilable depuis Linux) ; Linux et
@@ -31,43 +31,43 @@ jobs:
mkdir -p dist mkdir -p dist
# arm64 = natif ; amd64 = cross avec le SDK universel (-arch x86_64). # arm64 = natif ; amd64 = cross avec le SDK universel (-arch x86_64).
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 \ CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 \
go build -trimpath -ldflags="-s -w" -o dist/ajean-macos-arm ./cmd/ajean go build -trimpath -ldflags="-s -w" -o dist/loki-macos-arm ./cmd/loki
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 \ CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 \
CGO_CFLAGS="-arch x86_64" CGO_LDFLAGS="-arch x86_64" \ CGO_CFLAGS="-arch x86_64" CGO_LDFLAGS="-arch x86_64" \
go build -trimpath -ldflags="-s -w" -o dist/ajean-macos ./cmd/ajean go build -trimpath -ldflags="-s -w" -o dist/loki-macos ./cmd/loki
file dist/ajean-macos* file dist/loki-macos*
- name: bundles AJEAN.app - name: bundles Loki.app
run: | run: |
# Un binaire nu double-cliqué ouvrirait le Terminal. On publie donc # Un binaire nu double-cliqué ouvrirait le Terminal. On publie donc
# aussi un vrai bundle AJEAN.app (zippé pour garder le bit exécutable) : # aussi un vrai bundle Loki.app (zippé pour garder le bit exécutable) :
# au clic, le Finder lance Contents/MacOS/ajean, qui détecte le bundle # au clic, le Finder lance Contents/MacOS/loki, qui détecte le bundle
# et bascule en mode « application » (UI web + navigateur + icône dans # et bascule en mode « application » (UI web + navigateur + icône dans
# la barre de menus), comme le double-clic sur ajean.exe sous Windows. # la barre de menus), comme le double-clic sur loki.exe sous Windows.
# LSUIElement : app de barre de menus, aucune icône dans le Dock. # LSUIElement : app de barre de menus, aucune icône dans le Dock.
# L'icône part du PNG 1024 généré avec le reste de la marque # L'icône part du PNG 1024 généré avec le reste de la marque
# (cmd/ajean/icon.png, voir tools/gen-icon) : plus de conversion depuis # (cmd/loki/icon.png, voir tools/gen-icon) : plus de conversion depuis
# le .ico, dont l'échec silencieux donnait une app sans icône. # le .ico, dont l'échec silencieux donnait une app sans icône.
mkdir -p /tmp/ajean.iconset mkdir -p /tmp/loki.iconset
for s in 16 32 128 256 512; do for s in 16 32 128 256 512; do
sips -z $s $s 'cmd/ajean/icon.png' --out "/tmp/ajean.iconset/icon_${s}x${s}.png" >/dev/null sips -z $s $s 'cmd/loki/icon.png' --out "/tmp/loki.iconset/icon_${s}x${s}.png" >/dev/null
done done
iconutil -c icns /tmp/ajean.iconset -o /tmp/ajean.icns iconutil -c icns /tmp/loki.iconset -o /tmp/loki.icns
v="${GITHUB_REF_NAME#v}" v="${GITHUB_REF_NAME#v}"
for bin in ajean-macos ajean-macos-arm; do for bin in loki-macos loki-macos-arm; do
app="dist/bundle-$bin/AJEAN.app" app="dist/bundle-$bin/Loki.app"
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources"
cp "dist/$bin" "$app/Contents/MacOS/ajean" cp "dist/$bin" "$app/Contents/MacOS/loki"
chmod +x "$app/Contents/MacOS/ajean" chmod +x "$app/Contents/MacOS/loki"
cp /tmp/ajean.icns "$app/Contents/Resources/ajean.icns" 2>/dev/null || true cp /tmp/loki.icns "$app/Contents/Resources/loki.icns" 2>/dev/null || true
printf '%s\n' \ printf '%s\n' \
'<?xml version="1.0" encoding="UTF-8"?>' \ '<?xml version="1.0" encoding="UTF-8"?>' \
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
'<plist version="1.0"><dict>' \ '<plist version="1.0"><dict>' \
'<key>CFBundleName</key><string>AJEAN</string>' \ '<key>CFBundleName</key><string>Loki</string>' \
'<key>CFBundleDisplayName</key><string>AJEAN</string>' \ '<key>CFBundleDisplayName</key><string>Loki</string>' \
'<key>CFBundleExecutable</key><string>ajean</string>' \ '<key>CFBundleExecutable</key><string>loki</string>' \
'<key>CFBundleIdentifier</key><string>link.ajean.app</string>' \ '<key>CFBundleIdentifier</key><string>link.loki.app</string>' \
'<key>CFBundleIconFile</key><string>ajean</string>' \ '<key>CFBundleIconFile</key><string>loki</string>' \
'<key>CFBundlePackageType</key><string>APPL</string>' \ '<key>CFBundlePackageType</key><string>APPL</string>' \
"<key>CFBundleShortVersionString</key><string>$v</string>" \ "<key>CFBundleShortVersionString</key><string>$v</string>" \
"<key>CFBundleVersion</key><string>$v</string>" \ "<key>CFBundleVersion</key><string>$v</string>" \
@@ -78,7 +78,7 @@ jobs:
# Signature ad-hoc : sans elle, macOS refuse purement et simplement # Signature ad-hoc : sans elle, macOS refuse purement et simplement
# de lancer un binaire arm64 non signé (pas juste un avertissement). # de lancer un binaire arm64 non signé (pas juste un avertissement).
codesign --force --deep -s - "$app" || true codesign --force --deep -s - "$app" || true
(cd "dist/bundle-$bin" && zip -qry "../$bin.zip" AJEAN.app) (cd "dist/bundle-$bin" && zip -qry "../$bin.zip" Loki.app)
rm -rf "dist/bundle-$bin" rm -rf "dist/bundle-$bin"
done done
ls -l dist ls -l dist
@@ -98,13 +98,13 @@ jobs:
- name: build Linux + Windows - name: build Linux + Windows
run: | run: |
mkdir -p dist mkdir -p dist
# Noms d'asset : ajean-<os>[-arm][.exe]. Volontairement lisibles, et # Noms d'asset : loki-<os>[-arm][.exe]. Volontairement lisibles, et
# volontairement DIFFÉRENTS du schéma ajean-<GOOS>-<GOARCH> des 0.7 : # volontairement DIFFÉRENTS du schéma loki-<GOOS>-<GOARCH> des 0.7 :
# leur mise à jour automatique ne trouve alors aucun asset et échoue # leur mise à jour automatique ne trouve alors aucun asset et échoue
# sans rien remplacer, au lieu d'installer un binaire 0.8 sur une # sans rien remplacer, au lieu d'installer un binaire 0.8 sur une
# machine encore agencée en 0.7 (voir updateAssetName, sys_update.go). # machine encore agencée en 0.7 (voir updateAssetName, sys_update.go).
for t in linux/amd64:ajean-linux linux/arm64:ajean-linux-arm \ for t in linux/amd64:loki-linux linux/arm64:loki-linux-arm \
windows/amd64:ajean-windows.exe windows/arm64:ajean-windows-arm.exe; do windows/amd64:loki-windows.exe windows/arm64:loki-windows-arm.exe; do
target=${t%%:*}; name=${t#*:} target=${t%%:*}; name=${t#*:}
os=${target%/*}; arch=${target#*/}; ldflags="-s -w" os=${target%/*}; arch=${target#*/}; ldflags="-s -w"
# Windows : sous-système CONSOLE, pour que `cmd` attende la fin du # Windows : sous-système CONSOLE, pour que `cmd` attende la fin du
@@ -112,7 +112,7 @@ jobs:
# fonctionnent. La console créée au double-clic est refermée par le # fonctionnent. La console créée au double-clic est refermée par le
# programme lui-même (voir setupConsole, sys_console_windows.go). # programme lui-même (voir setupConsole, sys_console_windows.go).
GOOS=$os GOARCH=$arch CGO_ENABLED=0 \ GOOS=$os GOARCH=$arch CGO_ENABLED=0 \
go build -trimpath -ldflags="$ldflags" -o "dist/$name" ./cmd/ajean go build -trimpath -ldflags="$ldflags" -o "dist/$name" ./cmd/loki
done done
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
@@ -120,11 +120,11 @@ jobs:
path: dist path: dist
- name: SHA256SUMS - name: SHA256SUMS
run: | run: |
chmod +x dist/ajean-macos dist/ajean-macos-arm chmod +x dist/loki-macos dist/loki-macos-arm
# Tous les assets commencent par « ajean- », zips compris : un seul # Tous les assets commencent par « loki- », zips compris : un seul
# motif suffit. Ajouter « *.zip » les compterait DEUX fois, et un # motif suffit. Ajouter « *.zip » les compterait DEUX fois, et un
# SHA256SUMS à doublons est exactement ce que `ajean update` vérifie. # SHA256SUMS à doublons est exactement ce que `loki update` vérifie.
(cd dist && sha256sum ajean-* > SHA256SUMS) (cd dist && sha256sum loki-* > SHA256SUMS)
ls -l dist && cat dist/SHA256SUMS ls -l dist && cat dist/SHA256SUMS
- name: publier la release - name: publier la release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
+2 -2
View File
@@ -1,6 +1,6 @@
# Binaires compilés # Binaires compilés
/ajean /loki
/ajean.exe /loki.exe
/dist/ /dist/
# Artefacts de build Go # Artefacts de build Go
File renamed without changes.
File renamed without changes.
+4 -4
View File
@@ -1,4 +1,4 @@
// Point d'entrée du binaire ajean. Tout le code vit dans internal/ajean ; ce // Point d'entrée du binaire loki. Tout le code vit dans internal/loki ; ce
// dossier ne porte que le main() et les ressources Windows (.syso, icône, // dossier ne porte que le main() et les ressources Windows (.syso, icône,
// versioninfo) qui doivent résider dans le dossier du package main. // versioninfo) qui doivent résider dans le dossier du package main.
// //
@@ -7,11 +7,11 @@
// les .syso après avoir bumpé la version : `go generate ./...` // les .syso après avoir bumpé la version : `go generate ./...`
// (nécessite : go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest) // (nécessite : go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest)
// //
//go:generate go run github.com/nathaninline/ajean/tools/gen-icon icon.ico //go:generate go run github.com/R0m1k3/Loki/tools/gen-icon icon.ico
//go:generate goversioninfo -64 -icon=icon.ico -o resource_windows_amd64.syso versioninfo.json //go:generate goversioninfo -64 -icon=icon.ico -o resource_windows_amd64.syso versioninfo.json
//go:generate goversioninfo -64 -arm -icon=icon.ico -o resource_windows_arm64.syso versioninfo.json //go:generate goversioninfo -64 -arm -icon=icon.ico -o resource_windows_arm64.syso versioninfo.json
package main package main
import "github.com/nathaninline/ajean/internal/ajean" import "github.com/R0m1k3/Loki/internal/loki"
func main() { ajean.Main() } func main() { loki.Main() }
File renamed without changes.
File renamed without changes.
@@ -19,14 +19,14 @@
"FileSubType": "00" "FileSubType": "00"
}, },
"StringFileInfo": { "StringFileInfo": {
"CompanyName": "AJEAN contributors", "CompanyName": "Loki contributors",
"FileDescription": "AJEAN — gestionnaire mono-binaire de serveurs llama.cpp auto-hébergés", "FileDescription": "Loki — gestionnaire mono-binaire de serveurs llama.cpp auto-hébergés",
"InternalName": "ajean", "InternalName": "loki",
"LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.", "LegalCopyright": "Copyright (c) 2026 Loki contributors. MIT License.",
"OriginalFilename": "ajean.exe", "OriginalFilename": "loki.exe",
"ProductName": "AJEAN", "ProductName": "Loki",
"ProductVersion": "0.9.4", "ProductVersion": "0.9.4",
"Comments": "https://github.com/nathaninline/ajean — projet open source (MIT)" "Comments": "https://github.com/R0m1k3/Loki — projet open source (MIT)"
}, },
"VarFileInfo": { "VarFileInfo": {
"Translation": { "Translation": {
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/nathaninline/ajean module github.com/R0m1k3/Loki
go 1.25.0 go 1.25.0
@@ -1,6 +1,6 @@
// backend_build.go — machinerie de compilation de llama.cpp : détection du // 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. // plan de build (CUDA/ROCm/Metal/Vulkan/CPU), cmake, suivi de progression, logs.
package ajean package loki
import ( import (
"bufio" "bufio"
@@ -73,7 +73,7 @@ func detectBuildPlan() buildPlan {
// L'UI web embarquée de llama-server exige npm (ou un téléchargement // 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 // 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. // 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 // 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 // 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 // é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(" 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(" (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(" <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 // 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à // 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. // c'est qu'il manque pour de bon.
if p.backend == "cuda" && strings.Contains(log, "CUDA Toolkit not found") { 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("\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") { 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", 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")) yellow("[dépendance]"), green("Vulkan"))
if cmd := pkgInstallHint("spirv-headers"); cmd != "" { 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 { } 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,4 +1,4 @@
package ajean package loki
import ( import (
"errors" "errors"
@@ -1,4 +1,4 @@
package ajean package loki
import "testing" 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", `[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. // 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"`: "", ` 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`: "", `-- UI: running npm install`: "",
} }
for in, want := range cases { 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 // 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 (à // combine avec les infos matérielles locales, pour que l'écran d'accueil (à
// venir) propose « en un clic » un modèle adapté à la machine. // 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 // 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. // catalogue minimal embarqué (fallbackCatalogJSON) pour ne jamais bloquer.
import ( import (
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"bufio" "bufio"
@@ -18,7 +18,7 @@ import (
func readAPIKey() string { return getStr(bkState, "api_key") } func readAPIKey() string { return getStr(bkState, "api_key") }
// authHeader sets the Authorization: Bearer header on req when an API key is // 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. // against a protected llama-server. No-op when no key is set.
func authHeader(req *http.Request) { func authHeader(req *http.Request) {
if k := readAPIKey(); k != "" { if k := readAPIKey(); k != "" {
@@ -30,7 +30,7 @@ func authHeader(req *http.Request) {
func genAPIKey() string { func genAPIKey() string {
buf := make([]byte, 24) buf := make([]byte, 24)
_, _ = rand.Read(buf) _, _ = rand.Read(buf)
return "sk-ajean-" + hex.EncodeToString(buf) return "sk-loki-" + hex.EncodeToString(buf)
} }
// writeAPIKey enregistre (key != "") ou supprime (key == "") la clé Bearer. // writeAPIKey enregistre (key != "") ou supprime (key == "") la clé Bearer.
@@ -41,7 +41,7 @@ func writeAPIKey(key string) error {
return putStr(bkState, "api_key", key) 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. // chars, elide the middle. Empty in → empty out.
func maskAPIKey(k string) string { func maskAPIKey(k string) string {
if k == "" { if k == "" {
@@ -57,9 +57,9 @@ func maskAPIKey(k string) string {
// internet, le serveur exige alors « Authorization: Bearer <clé> » à chaque // internet, le serveur exige alors « Authorization: Bearer <clé> » à chaque
// appel. // appel.
// //
// ajean set-api-key <clé> définit la clé // loki set-api-key <clé> définit la clé
// ajean set-api-key génère une clé aléatoire // loki set-api-key génère une clé aléatoire
// ajean set-api-key "" supprime la protection // loki set-api-key "" supprime la protection
func cmdSetAPIKey(args []string) error { func cmdSetAPIKey(args []string) error {
var key string var key string
switch { switch {
@@ -83,7 +83,7 @@ func cmdSetAPIKey(args []string) error {
fmt.Print(dim("[info] redémarrer le service pour appliquer ? [Y/n] ")) fmt.Print(dim("[info] redémarrer le service pour appliquer ? [Y/n] "))
sc := bufio.NewScanner(os.Stdin) sc := bufio.NewScanner(os.Stdin)
if sc.Scan() && strings.HasPrefix(strings.ToLower(strings.TrimSpace(sc.Text())), "n") { 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 nil
} }
return serviceAction("restart") 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. // eux : une paire ouvrante/fermante du même caractère.
// //
// Un simple Trim(v, `"`) mangeait le guillemet d'un argument interne — par // 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 // guillemet final et repartait déséquilibré dans le preset (issue #17 : contenu
// tronqué / guillemets non appariés à la création d'un preset). // tronqué / guillemets non appariés à la création d'un preset).
func unquoteValue(v string) string { func unquoteValue(v string) string {
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"reflect" "reflect"
@@ -10,7 +10,7 @@ import (
// dans le preset (issue #17). // dans le preset (issue #17).
func TestParseEnvKeepsInnerQuotes(t *testing.T) { func TestParseEnvKeepsInnerQuotes(t *testing.T) {
cases := map[string]string{ 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`, `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`,
`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) { func TestFormatEnvRoundTrip(t *testing.T) {
in := map[string]string{ in := map[string]string{
"MODEL": "/mnt/mes modèles/x.gguf", "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", "CTX": "32768",
} }
out := parseEnv(formatEnv(in)) out := parseEnv(formatEnv(in))
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"bufio" "bufio"
@@ -11,10 +11,10 @@ import (
// backend_gpu.go — sélection du/des GPU utilisés par llama-server. // backend_gpu.go — sélection du/des GPU utilisés par llama-server.
// //
// ajean gpu liste les GPU et montre la sélection courante // loki gpu liste les GPU et montre la sélection courante
// ajean gpu 1 n'utilise que le GPU d'index 1 // loki gpu 1 n'utilise que le GPU d'index 1
// ajean gpu 0 1 utilise les GPU 0 et 1 // loki gpu 0 1 utilise les GPU 0 et 1
// ajean gpu all réinitialise (tous les GPU visibles) // 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 // 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 // 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) return fmt.Errorf("index GPU invalide: %q (attendu un nombre)", tok)
} }
if n < 0 || n >= len(gpus) { 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)) idx = append(idx, strconv.Itoa(n))
} }
@@ -102,7 +102,7 @@ func gpuList() error {
} else { } else {
fmt.Printf(" Sélection : %s (CUDA_VISIBLE_DEVICES=%s)\n", bold(sel), sel) 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 return nil
} }
@@ -120,7 +120,7 @@ func gpuSet(value string) error {
fmt.Print(dim("[info] redémarrer le service pour appliquer ? [Y/n] ")) fmt.Print(dim("[info] redémarrer le service pour appliquer ? [Y/n] "))
sc := bufio.NewScanner(os.Stdin) sc := bufio.NewScanner(os.Stdin)
if sc.Scan() && strings.HasPrefix(strings.ToLower(strings.TrimSpace(sc.Text())), "n") { 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 nil
} }
return serviceAction("restart") return serviceAction("restart")
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"fmt" "fmt"
@@ -9,12 +9,12 @@ import (
// backend_llamacpp.go — gestion du backend llama.cpp (clone, build, mise à jour). // 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 // l'accélérateur (CUDA / ROCm / Metal / CPU) et la
// compute capability du GPU, puis pointe BIN dessus. // 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. // 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. // retard éventuel sur origin.
const llamacppRepoURL = "https://github.com/ggml-org/llama.cpp.git" 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 { if err := SetConfigKey("BIN", bin); err != nil {
return fmt.Errorf("binaires installés mais échec d'écriture de BIN : %w", err) 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 return nil
default: default:
return fmt.Errorf("sous-commande inconnue: %s (install | update | prebuilt | status)", sub) 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 // llamacppRepoDir resolves the llama.cpp checkout: derived from config BIN when
// possible (so `update` targets whatever build the service actually runs), // 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 { func llamacppRepoDir() string {
if bin := ReadConfig()["BIN"]; bin != "" { if bin := ReadConfig()["BIN"]; bin != "" {
if real, err := filepath.EvalSymlinks(bin); err == nil { if real, err := filepath.EvalSymlinks(bin); err == nil {
@@ -161,7 +161,7 @@ func llamacppInstall(args []string) error {
if isDir(filepath.Join(repo, ".git")) { if isDir(filepath.Join(repo, ".git")) {
if !force { if !force {
fmt.Printf("%s dépôt déjà présent dans %s\n", yellow("[info]"), repo) 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 return nil
} }
fmt.Printf("%s --force : suppression de %s\n", yellow("[info]"), repo) 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("%s BIN mis à jour\n", green("✓"))
fmt.Printf("\nProchaines étapes :\n 1. renseigne MODEL : %s\n 2. démarre : %s\n", 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 return nil
} }
@@ -246,7 +246,7 @@ func llamacppUpdate(args []string) error {
repo := llamacppRepoDir() repo := llamacppRepoDir()
if !isDir(filepath.Join(repo, ".git")) { 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) 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) fmt.Printf("\n%s mis à jour : %s → %s\n", green("✓"), oldCommit, newCommit)
if noRestart { 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 return nil
} }
if svcWasUp { if svcWasUp {
fmt.Printf("%s redémarrage du service…\n", cyan("▶")) fmt.Printf("%s redémarrage du service…\n", cyan("▶"))
return serviceAction("start") 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 return nil
} }
@@ -337,7 +337,7 @@ func llamacppStatus(args []string) error {
fmt.Printf("%s\n", bold("llama.cpp")) fmt.Printf("%s\n", bold("llama.cpp"))
fmt.Printf(" dépôt : %s\n", repo) fmt.Printf(" dépôt : %s\n", repo)
if !isDir(filepath.Join(repo, ".git")) { 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 return nil
} }
commit := gitOutput(repo, "log", "-1", "--format=%h %ci %s") 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). // Retard sur origin (best-effort, sans fetch réseau).
if branch != "" && branch != "HEAD" { if branch != "" && branch != "HEAD" {
if behind := gitOutput(repo, "rev-list", "--count", "HEAD..origin/"+branch); behind != "" && behind != "0" { 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 ( import (
"encoding/json" "encoding/json"
@@ -10,13 +10,13 @@ import (
"strings" "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 // 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 // (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 // 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). // ';' sous Windows, comme le PATH).
func modelDirsListSep() string { func modelDirsListSep() string {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@@ -30,7 +30,7 @@ func modelDirsListSep() string {
func extraModelDirs() []string { func extraModelDirs() []string {
var raw []string var raw []string
getJSON(bkState, "model_dirs", &raw) 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())...) raw = append(raw, strings.Split(v, modelDirsListSep())...)
} }
return dedupDirs(raw, modelsDir()) 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 // 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 // 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 { func baseName(p string) string {
p = strings.TrimRight(strings.TrimSpace(p), `/\`) p = strings.TrimRight(strings.TrimSpace(p), `/\`)
if i := strings.LastIndexAny(p, `/\`); i >= 0 { 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 // 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 // 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 // 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) { func resolveModelPath(name string) (string, error) {
s := strings.TrimSpace(strings.Trim(strings.TrimSpace(name), `"`)) s := strings.TrimSpace(strings.Trim(strings.TrimSpace(name), `"`))
if s == "" { 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) 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. // POST {path, action:"add"|"remove"} en ajoute ou en retire un.
func handleModelDirs(w http.ResponseWriter, r *http.Request) { func handleModelDirs(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" { if r.Method == "POST" {
@@ -269,5 +269,5 @@ func handleModelDirs(w http.ResponseWriter, r *http.Request) {
n := modelDirCount(d) n := modelDirCount(d)
out = append(out, map[string]any{"path": d, "home": false, "count": n, "exists": n >= 0, "free": diskFree(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") != ""})
} }
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"os" "os"
@@ -11,7 +11,7 @@ import (
func TestResolveModelPathExtraDir(t *testing.T) { func TestResolveModelPathExtraDir(t *testing.T) {
testHome(t) testHome(t)
ext := t.TempDir() ext := t.TempDir()
t.Setenv("AJEAN_MODEL_DIRS", "") t.Setenv("LOKI_MODEL_DIRS", "")
extModel := filepath.Join(ext, "gros.gguf") extModel := filepath.Join(ext, "gros.gguf")
if err := os.WriteFile(extModel, []byte("x"), 0o644); err != nil { if err := os.WriteFile(extModel, []byte("x"), 0o644); err != nil {
@@ -42,7 +42,7 @@ func TestResolveModelPathExtraDir(t *testing.T) {
func TestResolveModelPathHomeFirst(t *testing.T) { func TestResolveModelPathHomeFirst(t *testing.T) {
testHome(t) testHome(t)
ext := t.TempDir() ext := t.TempDir()
t.Setenv("AJEAN_MODEL_DIRS", ext) t.Setenv("LOKI_MODEL_DIRS", ext)
if err := os.MkdirAll(modelsDir(), 0o755); err != nil { if err := os.MkdirAll(modelsDir(), 0o755); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"context" "context"
@@ -87,7 +87,7 @@ func detectQuant(content string) string {
} }
// downloadDestPath resolves the destination of a downloaded model : le dossier // 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). // refusant tout ce qui en sortirait (path traversal).
func downloadDestPath(name, dir string) (string, error) { func downloadDestPath(name, dir string) (string, error) {
base := filepath.Base(strings.TrimSpace(name)) base := filepath.Base(strings.TrimSpace(name))
@@ -146,7 +146,7 @@ func deleteModelFile(name string) error {
return nil 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) { func handleModelDelete(w http.ResponseWriter, r *http.Request) {
var req struct { var req struct {
Name string `json:"name"` Name string `json:"name"`
@@ -208,10 +208,10 @@ var dlClient = &http.Client{
} }
// dlConns is the number of parallel range requests used per download. // 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 { func dlConns() int {
n := 8 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 { if p, err := strconv.Atoi(v); err == nil && p > 0 {
n = p 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 // 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. // /api/models/download/status.
func handleModelDownload(w http.ResponseWriter, r *http.Request) { func handleModelDownload(w http.ResponseWriter, r *http.Request) {
var req struct { 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 != "" { if k := os.Getenv("HF_TOKEN"); k != "" {
req.Header.Set("Authorization", "Bearer "+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 req.Header.Set("Accept-Encoding", "identity") // never gzip a .gguf: it breaks ranges
if rng != "" { if rng != "" {
req.Header.Set("Range", rng) req.Header.Set("Range", rng)
@@ -1,4 +1,4 @@
package ajean package loki
// Destination des téléchargements : le dossier doit être choisi parmi les // 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 // 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) { func TestDownloadDestPathDir(t *testing.T) {
testHome(t) testHome(t)
extra := t.TempDir() extra := t.TempDir()
t.Setenv("AJEAN_MODEL_DIRS", extra) t.Setenv("LOKI_MODEL_DIRS", extra)
// Dossier vide → models/. // Dossier vide → models/.
got, err := downloadDestPath("m.gguf", "") got, err := downloadDestPath("m.gguf", "")
@@ -77,7 +77,7 @@ func TestDiskFreeAndCheck(t *testing.T) {
func TestCleanStalePartFilesExtraDirs(t *testing.T) { func TestCleanStalePartFilesExtraDirs(t *testing.T) {
testHome(t) testHome(t)
extra := t.TempDir() extra := t.TempDir()
t.Setenv("AJEAN_MODEL_DIRS", extra) t.Setenv("LOKI_MODEL_DIRS", extra)
stale := filepath.Join(extra, "coupe.gguf.part") stale := filepath.Join(extra, "coupe.gguf.part")
if err := os.WriteFile(stale, []byte("x"), 0o644); err != nil { if err := os.WriteFile(stale, []byte("x"), 0o644); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"bytes" "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. // Un téléchargement annulé ne doit laisser NI le .gguf final NI le .part.
func TestRunDownloadCancelLeavesNothing(t *testing.T) { func TestRunDownloadCancelLeavesNothing(t *testing.T) {
t.Setenv("AJEAN_DL_CONNS", "4") t.Setenv("LOKI_DL_CONNS", "4")
release := make(chan struct{}) release := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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 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) { func TestRunDownloadParallelAndFallback(t *testing.T) {
data := make([]byte, 48<<20) // > 3×dlMinChunk so the split actually kicks in data := make([]byte, 48<<20) // > 3×dlMinChunk so the split actually kicks in
rand.New(rand.NewSource(1)).Read(data) 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} { for _, ranges := range []bool{true, false} {
srv := serveBlob(data, ranges) srv := serveBlob(data, ranges)
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"fmt" "fmt"
@@ -15,7 +15,7 @@ import (
// llama-server n'a besoin QUE de la première : il ouvre les suivantes tout seul, // 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. // à 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 // 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 // « 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 // ne démarrent pas, et supprimer « le modèle » laissait les deux autres tranches
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"os" "os"
@@ -8,7 +8,7 @@
// CUDA officiel pour Linux (on retombe sur Vulkan — la compilation locale // CUDA officiel pour Linux (on retombe sur Vulkan — la compilation locale
// reste la voie CUDA sous Linux). La compilation locale reste indispensable // reste la voie CUDA sous Linux). La compilation locale reste indispensable
// pour les forks (ex. PrismML). // pour les forks (ex. PrismML).
package ajean package loki
import ( import (
"archive/tar" "archive/tar"
@@ -375,7 +375,7 @@ func prebuiltInstall(logf, phasef func(string)) (string, error) {
logf(fmt.Sprintf("release %s — variant retenu : %s", tag, label)) logf(fmt.Sprintf("release %s — variant retenu : %s", tag, label))
// Réinstaller à l'identique est inutile SAUF si l'extraction date d'une // 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 // bibliothèques introuvables au lancement) : le marqueur de format force alors
// une ré-extraction propre au lieu d'un « déjà à jour » trompeur. // une ré-extraction propre au lieu d'un « déjà à jour » trompeur.
if cur := prebuiltServerBin(); curTag == tag && prebuiltVersionFormat() == prebuiltFormat && cur != "" { if cur := prebuiltServerBin(); curTag == tag && prebuiltVersionFormat() == prebuiltFormat && cur != "" {
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"runtime" "runtime"
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"archive/tar" "archive/tar"
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"bufio" "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 // 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 // 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 // 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 // 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 // inconditionnellement écrasait cette valeur par l'ancienne (mono-GPU) : le
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"os" "os"
@@ -36,9 +36,9 @@ func TestPresetFingerprintIgnoresCosmetics(t *testing.T) {
// TestSwitchToPresetGardeLesReglagesMachine : basculer de preset ne doit pas // 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 : // 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 // 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) { func TestSwitchToPresetGardeLesReglagesMachine(t *testing.T) {
home := testHome(t) home := testHome(t)
write := func(p, body string) { write := func(p, body string) {
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"bytes" "bytes"
@@ -41,15 +41,15 @@ func cmdServe(args []string) error {
cfg := ReadConfig() cfg := ReadConfig()
bin := cfg["BIN"] bin := cfg["BIN"]
if 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"] model := cfg["MODEL"]
if 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 // 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 // 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 // llama-server ne trouvait rien. On résout donc explicitement, quel que soit
// le contexte de lancement. // le contexte de lancement.
@@ -69,7 +69,7 @@ func cmdServe(args []string) error {
strings.Join(missing, ", "), filepath.Dir(model), len(shardFamily(filepath.Base(model)))) strings.Join(missing, ", "), filepath.Dir(model), len(shardFamily(filepath.Base(model))))
} }
if !filepath.IsAbs(bin) { 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 // 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 // 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. // Linux, PATH on Windows — handled inside execServer.
setLibraryPath(filepath.Dir(bin)) 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 // 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"). // affichés par nvidia-smi (sinon CUDA réordonne par "device le plus rapide").
if v := cfg["CUDA_VISIBLE_DEVICES"]; v != "" { 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 // 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 // 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. // 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 // 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 // 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. // qui réécrivent config.env), avec config.env comme repli rétro-compatible.
if k := readAPIKey(); k != "" { if k := readAPIKey(); k != "" {
llmArgs = append(llmArgs, "--api-key", 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. // together so a path with spaces stays one argument.
llmArgs = append(llmArgs, splitArgs(cfg["EXTRA_ARGS"])...) 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. // 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")) bin, filepath.Base(model), get("PORT", "8080"))
// Hand off to the llama-server process. On Unix this replaces the current // Hand off to the llama-server process. On Unix this replaces the current
@@ -1,4 +1,4 @@
package ajean package loki
import "fmt" import "fmt"
@@ -43,7 +43,7 @@ func cmdAgent(args []string) error {
fmt.Printf(" %s %s\n", bold(p.Name), p.Title) fmt.Printf(" %s %s\n", bold(p.Name), p.Title)
} }
default: default:
return fmt.Errorf("usage: ajean agent [on|off|status]") return fmt.Errorf("usage: loki agent [on|off|status]")
} }
return nil return nil
} }
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"bufio" "bufio"
@@ -12,14 +12,14 @@ import (
// First positional arg (if any) becomes the system prompt. // First positional arg (if any) becomes the system prompt.
func cmdChat(args []string) error { func cmdChat(args []string) error {
if !healthCheck() { 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, " ") sysPrompt := strings.Join(args, " ")
msgs := []Message{} msgs := []Message{}
if sysPrompt != "" { if sysPrompt != "" {
msgs = append(msgs, Message{Role: "system", Content: 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 != "" { if sysPrompt != "" {
fmt.Println(dim("system: " + sysPrompt)) fmt.Println(dim("system: " + sysPrompt))
} }
@@ -65,7 +65,7 @@ func cmdChat(args []string) error {
inReason := false inReason := false
var stats *StatsEvent var stats *StatsEvent
// Print the assistant prefix once; reasoning is shown inline with a tag. // Print the assistant prefix once; reasoning is shown inline with a tag.
fmt.Print(cyan("ajean") + " > ") fmt.Print(cyan("loki") + " > ")
caps := globalCaps() caps := globalCaps()
// Compaction proactive (façon Hermes) : on résume les vieux tours quand // Compaction proactive (façon Hermes) : on résume les vieux tours quand
// l'historique dépasse le seuil, au lieu d'imposer un /reset. // l'historique dépasse le seuil, au lieu d'imposer un /reset.
@@ -120,7 +120,7 @@ func cmdChat(args []string) error {
fmt.Print("\n") fmt.Print("\n")
inReason = false 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 != "": case ev.Reasoning != "":
if !inReason { if !inReason {
fmt.Print(magenta("[reasoning] ") + dim("")) fmt.Print(magenta("[reasoning] ") + dim(""))
@@ -129,7 +129,7 @@ func cmdChat(args []string) error {
fmt.Print(dim(ev.Reasoning)) fmt.Print(dim(ev.Reasoning))
case ev.Content != "": case ev.Content != "":
if inReason { if inReason {
fmt.Print("\n" + cyan("ajean") + " > ") fmt.Print("\n" + cyan("loki") + " > ")
inReason = false inReason = false
} }
full.WriteString(ev.Content) full.WriteString(ev.Content)
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"bytes" "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 // 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é // 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 // 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. // é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 // 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) // 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. // vraiment réduire, puis re-déclenchait aussitôt.
before, after := estimateTokens(msgs), estimateTokens(out) before, after := estimateTokens(msgs), estimateTokens(out)
if after > before*4/5 { 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.` Write the summary in the SAME language as the conversation.`
payload := map[string]any{ payload := map[string]any{
"model": "ajean", "model": "loki",
"messages": []Message{ "messages": []Message{
{Role: "system", Content: sys}, {Role: "system", Content: sys},
{Role: "user", Content: transcript}, {Role: "user", Content: transcript},
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"strings" "strings"
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package ajean package loki
// chat_diff.go — petit diff ligne à ligne pour l'interface : quand l'IA modifie // 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 // un fichier (outil `edit`) ou une page de mémoire (`mem_edit`, `mem_add`), on
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"encoding/json" "encoding/json"
@@ -13,10 +13,10 @@ import (
// chat_export.go — sortir la conversation de la base. // chat_export.go — sortir la conversation de la base.
// //
// Jusqu'à la 0.7, l'historique vivait dans un `conversation.json` qu'on pouvait // 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 // 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 // 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 // personne ne peut les récupérer. C'est une régression pour qui relisait ses
// échanges, et ce fichier la répare. // é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 // envoyée au modèle, `log` le journal d'affichage (celui qui porte les
// raisonnements, les outils et les vitesses). // raisonnements, les outils et les vitesses).
type exportPayload struct { type exportPayload struct {
Version string `json:"ajean_version"` Version string `json:"loki_version"`
ExportedAt string `json:"exported_at"` ExportedAt string `json:"exported_at"`
CtxUsed int `json:"ctx_used"` CtxUsed int `json:"ctx_used"`
Messages []Message `json:"messages"` Messages []Message `json:"messages"`
@@ -199,7 +199,7 @@ func (c *Conversation) ExportMarkdown(o exportOpts) string {
} }
var b strings.Builder 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) 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 // 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. // 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 // 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. // quand la réponse est entrecoupée d'appels d'outils.
openBubble := false openBubble := false
head := func() { head := func() {
if !openBubble { if !openBubble {
b.WriteString("\n## AJEAN\n") b.WriteString("\n## Loki\n")
openBubble = true openBubble = true
} }
} }
@@ -303,7 +303,7 @@ func mdInline(v any) string {
// exportFilename : nom proposé au téléchargement, horodaté pour que deux exports // exportFilename : nom proposé au téléchargement, horodaté pour que deux exports
// ne s'écrasent pas dans le dossier de téléchargements. // ne s'écrasent pas dans le dossier de téléchargements.
func exportFilename(ext string) string { 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 // 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). // cmdExport écrit la conversation dans un fichier (ou sur la sortie standard).
// //
// ajean export → ajean-conversation-<date>.md dans le dossier courant // loki export → loki-conversation-<date>.md dans le dossier courant
// ajean export --json → idem en JSON // loki export --json → idem en JSON
// ajean export mon-fil.md → nom de fichier imposé (l'extension choisit le format) // loki 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 - → sur la sortie standard, pour enchaîner un tube
// //
// Options de contenu, les mêmes que la fenêtre d'export de l'interface : // 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 o.Turns = n
default: default:
if strings.HasPrefix(a, "-") && a != "-" { 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 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. // qui produirait du Markdown serait un piège.
if strings.HasSuffix(strings.ToLower(out), ".json") { if strings.HasSuffix(strings.ToLower(out), ".json") {
o.Format = "json" o.Format = "json"
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"encoding/json" "encoding/json"
@@ -37,7 +37,7 @@ func TestExportMarkdownPorteToutLeFil(t *testing.T) {
md := conv.ExportMarkdown(defaultExportOpts()) md := conv.ExportMarkdown(defaultExportOpts())
for _, want := range []string{ for _, want := range []string{
"## Vous", "combien font 2+2 ?", "## Vous", "combien font 2+2 ?",
"## AJEAN", "## Loki",
"<summary>Raisonnement</summary>", "addition simple", "<summary>Raisonnement</summary>", "addition simple",
"bash", "echo 4", "bash", "echo 4",
} { } {
@@ -45,9 +45,9 @@ func TestExportMarkdownPorteToutLeFil(t *testing.T) {
t.Errorf("Markdown exporté sans %q :\n%s", want, md) 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. // 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) t.Errorf("en-tête assistant répété %d fois", n)
} }
} }
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"bytes" "bytes"
@@ -18,7 +18,7 @@ import (
// Accès internet de l'IA — port Go de l'extension pi ~/.pi/agent/extensions/web.ts. // 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 // 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 : // 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, // 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`. // web_open, web_read, web_grep) + la sous-commande `loki internet`.
package ajean package loki
import ( import (
"fmt" "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")) 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 { func cmdInternet(args []string) error {
sub := "" sub := ""
@@ -292,7 +292,7 @@ func cmdInternet(args []string) error {
case "on": case "on":
// Le moteur intégré ne demande aucun réglage ; Crawl4AI exige un serveur. // Le moteur intégré ne demande aucun réglage ; Crawl4AI exige un serveur.
if webEngine() == engineCrawl && crawl4aiURL() == "" { 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 { if err := setInternetEnabled(true); err != nil {
return err return err
@@ -305,7 +305,7 @@ func cmdInternet(args []string) error {
fmt.Println(green("[ok]") + " accès internet désactivé") fmt.Println(green("[ok]") + " accès internet désactivé")
case "url": case "url":
if len(args) < 2 { 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]), "/") u := strings.TrimRight(strings.TrimSpace(args[1]), "/")
if err := SetConfigKey("CRAWL4AI_URL", u); err != nil { 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)) fmt.Printf("%s serveur Crawl4AI : %s\n", green("[ok]"), bold(u))
case "key": case "key":
if len(args) < 2 { 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]) k := strings.TrimSpace(args[1])
if err := writeCrawlKey(k); err != nil { if err := writeCrawlKey(k); err != nil {
@@ -333,7 +333,7 @@ func cmdInternet(args []string) error {
} }
case "engine": case "engine":
if len(args) < 2 { 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])) e := strings.ToLower(strings.TrimSpace(args[1]))
if err := setWebEngine(e); err != nil { if err := setWebEngine(e); err != nil {
@@ -342,7 +342,7 @@ func cmdInternet(args []string) error {
if e == engineGo { if e == engineGo {
fmt.Println(green("[ok]") + " moteur intégré — aucune installation requise (pas de rendu JavaScript)") fmt.Println(green("[ok]") + " moteur intégré — aucune installation requise (pas de rendu JavaScript)")
} else { } 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": case "", "status", "list":
state := dim("off") state := dim("off")
@@ -358,7 +358,7 @@ func cmdInternet(args []string) error {
fmt.Printf(" moteur : %s\n", bold("crawl4ai")) fmt.Printf(" moteur : %s\n", bold("crawl4ai"))
u := crawl4aiURL() u := crawl4aiURL()
if u == "" { 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 return nil
} }
reach := red("injoignable") reach := red("injoignable")
@@ -368,7 +368,7 @@ func cmdInternet(args []string) error {
fmt.Printf(" serveur : %s (%s)\n", bold(u), reach) fmt.Printf(" serveur : %s (%s)\n", bold(u), reach)
fmt.Printf(" outils : web_search, web_open, web_read, web_grep\n") fmt.Printf(" outils : web_search, web_open, web_read, web_grep\n")
default: 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 return nil
} }
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"errors" "errors"
@@ -10,7 +10,7 @@ import (
"strings" "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, // 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, // décisions, procédures, infos projet). Quatre outils : mem_search, mem_read,
// mem_add, mem_edit. // mem_add, mem_edit.
@@ -331,7 +331,7 @@ func setMemMode(m MemMode) error {
return SetConfigKey("MEM_MODE", string(m)) 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 { func cmdMemory(args []string) error {
sub := "" sub := ""
if len(args) > 0 { if len(args) > 0 {
@@ -362,7 +362,7 @@ func cmdMemory(args []string) error {
fmt.Printf(" %d page(s) sous %s\n", len(pages), memoryDir()) fmt.Printf(" %d page(s) sous %s\n", len(pages), memoryDir())
return nil return nil
default: default:
return fmt.Errorf("usage: ajean memory [off|ondemand|always|status]") return fmt.Errorf("usage: loki memory [off|ondemand|always|status]")
} }
m := memMode() m := memMode()
fmt.Printf("%s mémoire : %s — %s\n", green("[ok]"), bold(string(m)), label[m]) fmt.Printf("%s mémoire : %s — %s\n", green("[ok]"), bold(string(m)), label[m])
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"encoding/json" "encoding/json"
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"context" "context"
@@ -41,9 +41,9 @@ func baseSystemPrompt(caps Caps) string {
// sur-raisonner les modèles à reasoning (Qwen3) : ils émettent leur <think> // 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 » // 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. // 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 // « 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 ajean »). // littéralement la casse d'ici quand il se présente (« je suis loki »).
b.WriteString("You are AJEAN, an expert assistant operating directly on this machine with real tools.") b.WriteString("You are Loki, an expert assistant operating directly on this machine with real tools.")
if caps.Mem == MemAlways { if caps.Mem == MemAlways {
b.WriteString(" You evolve with every conversation: you actively maintain a persistent memory so nothing useful is lost between sessions.") 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(".") b.WriteString(".")
if cwd != "" { 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() 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) ctx, cancel := context.WithTimeout(parent, time.Duration(timeoutSec)*time.Second)
defer cancel() defer cancel()
cmd := newShellCmd(ctx, command) 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. // 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 // 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 // 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 // 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 // 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 cmd.WaitDelay = 2 * time.Second
err := cmd.Run() err := cmd.Run()
switch { switch {
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"os" "os"
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"os" "os"
@@ -11,14 +11,14 @@ import (
// //
// Sans ça, les outils write/edit/bash héritent du répertoire courant du PROCESSUS : // 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 // 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 // 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 // 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 // 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 // restent honorés tels quels — quand l'utilisateur demande d'écrire dans un
// dossier précis, ça doit marcher. // dossier précis, ça doit marcher.
const workspaceEnv = "AJEAN_WORKSPACE" const workspaceEnv = "LOKI_WORKSPACE"
var ( var (
workspaceOnce sync.Once workspaceOnce sync.Once
@@ -26,7 +26,7 @@ var (
) )
// agentWorkspace renvoie le dossier de travail de l'agent, créé au besoin. Il // 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. // Windows, qui n'est pas inscriptible par un utilisateur non administrateur.
func agentWorkspace() string { func agentWorkspace() string {
workspaceOnce.Do(func() { workspaceOnce.Do(func() {
@@ -62,9 +62,9 @@ func workspaceCandidates() []string {
} }
c = append(c, workspaceDir()) c = append(c, workspaceDir())
if home, err := os.UserHomeDir(); err == nil && home != "" { 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 return c
} }
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"path/filepath" "path/filepath"
@@ -7,7 +7,7 @@ import (
) )
// Un chemin relatif du modèle doit atterrir dans le workspace, jamais dans le // 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) { func TestResolveAgentPathRelative(t *testing.T) {
got := resolveAgentPath("meteo.json") got := resolveAgentPath("meteo.json")
want := filepath.Join(agentWorkspace(), "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 // 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 // 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 // Windows (voir sys_tray_windows.go) : on voit que Loki tourne et on le pilote
// (« Ouvrir AJEAN » / « Quitter »). // (« Ouvrir Loki » / « Quitter »).
import ( import (
"fmt" "fmt"
@@ -34,13 +34,13 @@ func cmdApp(args []string) error {
addr := fmt.Sprintf("0.0.0.0:%d", appPort) addr := fmt.Sprintf("0.0.0.0:%d", appPort)
ln, err := net.Listen("tcp", addr) ln, err := net.Listen("tcp", addr)
if err != nil { 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. // 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) 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 // 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 // process qui servent la conversation (objet en mémoire persisté dans
// conversation.json) donnent deux fils divergents entre le local et // conversation.json) donnent deux fils divergents entre le local et
@@ -60,7 +60,7 @@ func cmdApp(args []string) error {
startAppLink(appWebMux) startAppLink(appWebMux)
}() }()
sp := showSplash("Lancement d'AJEAN en cours…") sp := showSplash("Lancement d'Loki en cours…")
waitServerReady(url) waitServerReady(url)
_ = openBrowser(url) _ = openBrowser(url)
time.Sleep(900 * time.Millisecond) // laisse le navigateur s'afficher par-dessus le splash 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 // pas de sous-dossiers dans un même package : les fichiers sont donc préfixés
// par domaine. // 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 // l'arborescence de données
// store.go LA base (bbolt) : configuration, préférences, conversation, // store.go LA base (bbolt) : configuration, préférences, conversation,
// clés, jetons, interrupteurs — tout l'état non éditable à la main // 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. // supprimer un symbole jugé inutilisé, vérifier `grep` sur les fichiers _darwin.
// //
// DEUX SERVICES à l'exécution, un seul binaire : // DEUX SERVICES à l'exécution, un seul binaire :
// - ajean-engine (« ajean serve ») exec llama-server ; // - loki-engine (« loki serve ») exec llama-server ;
// - ajean-ui (« ajean web ») sert l'UI locale, le tunnel du relais et // - loki-ui (« loki web ») sert l'UI locale, le tunnel du relais et
// l'endpoint OpenAI depuis un SEUL process — condition d'une conversation // l'endpoint OpenAI depuis un SEUL process — condition d'une conversation
// unique, celle-ci vivant en mémoire (voir chat_conversation.go). // 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 // ui/index.html est GÉNÉRÉ depuis ui/src/ : éditer les sources puis
// `go generate ./internal/ajean` (voir tools/assemble-ui). // `go generate ./internal/loki` (voir tools/assemble-ui).
package ajean package loki
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"bytes" "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 // Use the same endpoint your real chat hits, so the comparison is honest
// (chat template, reasoning, OpenAI-compat layer all included). // (chat template, reasoning, OpenAI-compat layer all included).
payload := map[string]any{ 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."}}, "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, "max_tokens": nPredict,
"stream": false, "stream": false,
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"bufio" "bufio"
@@ -575,7 +575,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
// le contexte — c'est un choix assumé. // le contexte — c'est un choix assumé.
for iter := 0; ; iter++ { for iter := 0; ; iter++ {
payload := map[string]any{ payload := map[string]any{
"model": "ajean", "model": "loki",
"messages": messages, "messages": messages,
"stream": true, "stream": true,
"temperature": temperature, "temperature": temperature,
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"bufio" "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 // 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 // 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 // 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. // démux (oaiPublicEnabled, lu en direct) qui décide de router ou non le trafic.
func oaiTLSConfig() *tls.Config { 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.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 certmagic.DefaultACME.DisableHTTPChallenge = true // pas de :80 accessible (CGNAT) → TLS-ALPN uniquement
magic := certmagic.NewDefault() magic := certmagic.NewDefault()
magic.OnDemand = &certmagic.OnDemandConfig{ 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. // 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. // cert auto-signé et proxifie vers llama /v1.
func cmdOAI(args []string) error { func cmdOAI(args []string) error {
sub := "" sub := ""
@@ -236,12 +236,12 @@ func cmdOAI(args []string) error {
if err != nil { if err != nil {
return err 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) return runOAIFront(ln, tlsCfg)
default: default:
fmt.Println("usage: ajean oai serve [port] [host] (front TLS de test → llama /v1)") 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 (ajean link)") fmt.Println(" en prod, le front TLS est servi automatiquement dans le tunnel (loki link)")
fmt.Println(" quand AJEAN_LINK_ALLOW_OAI=1 ; cert Let's Encrypt via TLS-ALPN-01.") fmt.Println(" quand LOKI_LINK_ALLOW_OAI=1 ; cert Let's Encrypt via TLS-ALPN-01.")
return nil return nil
} }
} }
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"strings" "strings"
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"context" "context"
@@ -14,7 +14,7 @@ func cmdTest(args []string) error {
fmt.Printf("→ GET http://localhost:%d/health … ", port) fmt.Printf("→ GET http://localhost:%d/health … ", port)
if !healthCheck() { if !healthCheck() {
fmt.Println(red("ko")) 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")) fmt.Println(green("ok"))
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"context" "context"
@@ -127,7 +127,7 @@ func (h headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
// connect établit une session pour la config donnée. // connect établit une session pour la config donnée.
func mcpConnect(ctx context.Context, name string, cfg MCPServerConfig) (*mcpsdk.ClientSession, error) { 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 var transport mcpsdk.Transport
switch cfg.Transport() { switch cfg.Transport() {
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"os/exec" "os/exec"
@@ -17,7 +17,7 @@ func TestMCPEnsureDeduplicatesConcurrentConnects(t *testing.T) {
// Commande inexistante : la connexion échoue, mais l'entrée du pool est bien // Commande inexistante : la connexion échoue, mais l'entrée du pool est bien
// partagée — c'est ce qu'on teste. // 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 const callers = 8
var wg sync.WaitGroup var wg sync.WaitGroup
@@ -83,8 +83,8 @@ func TestMCPEndToEnd(t *testing.T) {
t.Fatalf("outil %s introuvable dans %d outils", echoName, len(tools)) t.Fatalf("outil %s introuvable dans %d outils", echoName, len(tools))
} }
out := mcpCall(echoName, map[string]any{"message": "bonjour-ajean"}) out := mcpCall(echoName, map[string]any{"message": "bonjour-loki"})
if !strings.Contains(out, "bonjour-ajean") { if !strings.Contains(out, "bonjour-loki") {
t.Fatalf("echo n'a pas renvoyé le message ; got: %q", out) t.Fatalf("echo n'a pas renvoyé le message ; got: %q", out)
} }
} }
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"encoding/json" "encoding/json"
@@ -10,14 +10,14 @@ import (
// Configuration des serveurs MCP (Model Context Protocol). // 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 // d'outils de l'IA (accès fichiers rapide type desktop-commander, bases de
// données, APIs métier…). Deux transports : // 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 // JSON-RPC sur stdin/stdout. C'est le cas de desktop-commander, du serveur
// filesystem officiel, etc. — lancés via npx/uvx/un binaire. // 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). // éventuels en-têtes d'auth).
// //
// Le format du fichier mcp.json reprend celui de Claude Desktop (clé // 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. // leurs configs existantes. On ajoute un champ "enabled" par serveur.
// //
// IMPORTANT (sécurité) : un serveur MCP stdio exécute un process arbitraire sur // 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 // 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. // 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. // 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 // 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 // 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 — // sys_install_windows.go) suffit. C'est la seule raison d'être de ce fichier —
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"os" "os"
@@ -38,7 +38,7 @@ func fake07Home(t *testing.T) string {
write(".agent_enabled", "") write(".agent_enabled", "")
write("webprefs.json", `{"theme":"dark"}`) write("webprefs.json", `{"theme":"dark"}`)
write("mcp.json", `{"mcpServers":{"fs":{"command":"npx","enabled":true}}}`) 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("conversation.json", `{"seq":42,"messages":[{"role":"user","content":"salut"}]}`)
write("SKILLS/vieux-skill/SKILL.md", "# un skill d'avant\n") write("SKILLS/vieux-skill/SKILL.md", "# un skill d'avant\n")
write(".authorized_users", "AABBCCDDEEFF00112233445566778899AABBCCDDEEFF001122334455667788AA\n") write(".authorized_users", "AABBCCDDEEFF00112233445566778899AABBCCDDEEFF001122334455667788AA\n")
@@ -90,7 +90,7 @@ func TestMigration07(t *testing.T) {
if !agentEnabled() { if !agentEnabled() {
t.Error("mode agent perdu") t.Error("mode agent perdu")
} }
if readSysPrompt() != "Tu es AJEAN." { if readSysPrompt() != "Tu es Loki." {
t.Errorf("prompt système = %q", readSysPrompt()) t.Errorf("prompt système = %q", readSysPrompt())
} }
if loadWebPrefs()["theme"] != "dark" { if loadWebPrefs()["theme"] != "dark" {
@@ -6,7 +6,7 @@
// - ENRÔLEMENT (public) : le poste échange un code d'appairage à usage unique // - 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 // contre sa clé d'appareil. Authentifié par le code, pas par la clé de
// pilotage — que le poste ne possède pas. // pilotage — que le poste ne possède pas.
package ajean package loki
import ( import (
"encoding/base64" "encoding/base64"
@@ -1,16 +1,16 @@
// node_proto.go — côté serveur, alias vers le protocole partagé (internal/nodewire) // 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, // 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 // paquet SANS dépendance, pour que le client léger loki-node le partage sans
// tirer tout ajean. Ici on ne fait qu'exposer ces symboles sous les noms // 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. // historiques du paquet, pour ne pas toucher le reste du code serveur.
package ajean package loki
import ( import (
"strings" "strings"
"github.com/nathaninline/ajean/internal/nodewire" "github.com/R0m1k3/Loki/internal/nodewire"
) )
// Capacités (alias des constantes partagées). // Capacités (alias des constantes partagées).
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"path/filepath" "path/filepath"
@@ -2,7 +2,7 @@
// WebSocket, registre des postes connectés, et exposition de leurs outils à // 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 // 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. // d'outil vers le poste concerné et attend sa réponse.
package ajean package loki
import ( import (
"context" "context"
@@ -18,7 +18,7 @@ import (
"github.com/coder/websocket" "github.com/coder/websocket"
"github.com/coder/websocket/wsjson" "github.com/coder/websocket/wsjson"
"github.com/nathaninline/ajean/internal/nodewire" "github.com/R0m1k3/Loki/internal/nodewire"
) )
const ( const (
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"context" "context"
@@ -12,7 +12,7 @@ import (
"github.com/coder/websocket" "github.com/coder/websocket"
"github.com/coder/websocket/wsjson" "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 // 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 // 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 ». // 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 // 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 // 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 // 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 // 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. // l'ouvrir), puis chiffre/déchiffre le chat avec une clé dérivée de R.
@@ -32,7 +32,7 @@ import (
"sync" "sync"
) )
func e2eKeyPath() string { return filepath.Join(AjeanHome(), ".e2e_key") } func e2eKeyPath() string { return filepath.Join(LokiHome(), ".e2e_key") }
var ( var (
e2eOnce sync.Once e2eOnce sync.Once
@@ -55,7 +55,7 @@ func e2ePrivateKey() (*ecdh.PrivateKey, error) {
e2eErr = err e2eErr = err
return return
} }
_ = os.MkdirAll(AjeanHome(), 0o755) _ = os.MkdirAll(LokiHome(), 0o755)
if err := os.WriteFile(e2eKeyPath(), []byte(hex.EncodeToString(k.Bytes())+"\n"), 0o600); err != nil { if err := os.WriteFile(e2eKeyPath(), []byte(hex.EncodeToString(k.Bytes())+"\n"), 0o600); err != nil {
e2eErr = err e2eErr = err
return return
@@ -1,4 +1,4 @@
package ajean package loki
// relay_e2eauth.go — authentification MUTUELLE + anti-rejeu du canal chiffré. // 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 // 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, // 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 // 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, // 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"), // 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 // 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 -------------- // ---- 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 // 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 // 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 // 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 ---------------------------- // ---- Dérivation de clé + ouverture authentifiée ----------------------------
// e2eAuthKey dérive la clé du canal authentifié pour l'utilisateur uPub : // 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é. // navigateur (WASM) qui calcule ECDH(uPriv, agentPub) = même secret partagé.
func e2eAuthKey(uPubHex string) ([]byte, error) { func e2eAuthKey(uPubHex string) ([]byte, error) {
priv, err := e2ePrivateKey() priv, err := e2ePrivateKey()
@@ -253,7 +253,7 @@ func e2eAuthKey(uPubHex string) ([]byte, error) {
} }
h := sha256.New() h := sha256.New()
h.Write(ss) h.Write(ss)
h.Write([]byte("ajean-authchan-v1")) h.Write([]byte("loki-authchan-v1"))
return h.Sum(nil), nil return h.Sum(nil), nil
} }
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"strings" "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 // (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.). // 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 // 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 // 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. // local. Keepalive + reconnexion automatique avec backoff.
// //
// Le token est fourni par la boutique à l'achat. Il est mémorisé dans // 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. // connexion.
import ( import (
@@ -39,7 +39,7 @@ import (
) )
// defaultRelayURL est l'endpoint WebSocket du relais. Surchageable via // 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" const defaultRelayURL = "wss://ajean.link/agent"
// machineID renvoie un identifiant stable de la machine, créé au premier appel. // 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). // relayURL resolves the relay WebSocket endpoint (env override → default).
func relayURL() string { func relayURL() string {
if u := os.Getenv("AJEAN_LINK_URL"); u != "" { if u := os.Getenv("LOKI_LINK_URL"); u != "" {
return u return u
} }
return defaultRelayURL 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. // 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 } func uiServiceName() string { return uiUnitName }
// cmdLink ne gère QUE le compte d'accès distant : le jeton, l'appairage, l'état. // 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 // qu'un jeton est enregistré. Piloter le tunnel, c'est donc piloter le service
// d'interface (voir cmdUI). // d'interface (voir cmdUI).
func cmdLink(args []string) error { func cmdLink(args []string) error {
@@ -88,33 +88,33 @@ func cmdLink(args []string) error {
} }
switch sub { switch sub {
case "": 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). // un mot pour un jeton et d'écraser le vrai).
printLinkHelp() printLinkHelp()
return nil return nil
case "status": case "status":
tok := readLinkToken() tok := readLinkToken()
if tok == "" { 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 return nil
} }
fmt.Printf("%s jeton enregistré (%s…), relais: %s\n", green("[ok]"), tok[:min(8, len(tok))], relayURL()) fmt.Printf("%s jeton enregistré (%s…), relais: %s\n", green("[ok]"), tok[:min(8, len(tok))], relayURL())
if uiServiceActive() { if uiServiceActive() {
fmt.Printf("%s service %s actif — le tunnel est ouvert\n", green("[ok]"), uiServiceName()) fmt.Printf("%s service %s actif — le tunnel est ouvert\n", green("[ok]"), uiServiceName())
} else { } 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 return nil
case "logout": case "logout":
if err := removeLinkToken(); err != nil { if err := removeLinkToken(); err != nil {
return err 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 return nil
case "code": case "code":
code, err := newPairCode() code, err := newPairCode()
if err != nil { 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)) fmt.Printf("%s code d'appairage (valable 10 min, à usage unique) :\n %s\n", green("[link]"), bold(code))
return nil return nil
@@ -138,7 +138,7 @@ func cmdLink(args []string) error {
return linkPrintIdentity() 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. // 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. // C'est ce qui garantit une conversation unique, identique en local et à distance.
func cmdUI(args []string) error { func cmdUI(args []string) error {
@@ -160,22 +160,22 @@ func cmdUI(args []string) error {
} }
return nil return nil
default: 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() { 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 : Usage :
ajean link <token> enregistre le jeton (1re fois / pour le changer) et ouvre le tunnel loki link <token> enregistre le jeton (1re fois / pour le changer) et ouvre le tunnel
ajean link status état du jeton et du tunnel loki link status état du jeton et du tunnel
ajean link code génère un code d'appairage (valable 10 min, à usage unique) loki link code génère un code d'appairage (valable 10 min, à usage unique)
ajean link logout oublie le jeton enregistré loki link logout oublie le jeton enregistré
Le jeton est fourni sur ajean.link. Le tunnel est ouvert par le service 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. service séparé pour l'accès distant.
`) `)
} }
@@ -189,8 +189,8 @@ func linkPrintIdentity() error {
} }
code, err := newPairCode() code, err := newPairCode()
if err != nil { if err != nil {
fmt.Printf("%s code d'appairage indisponible (droits sur %s ?): %v\n", yellow("[link]"), AjeanHome(), err) fmt.Printf("%s code d'appairage indisponible (droits sur %s ?): %v\n", yellow("[link]"), LokiHome(), err)
fmt.Printf(" Réessaie : sudo ajean link code\n") fmt.Printf(" Réessaie : sudo loki link code\n")
return nil return nil
} }
fmt.Printf(" Code d'appairage (valable 10 min, usage unique) : %s\n\n", bold(code)) 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à // 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 à // :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é. // ouverte — sur un portable qui s'endort, c'est de toute façon la réalité.
var appLink struct { var appLink struct {
@@ -310,14 +310,14 @@ func killForeignUIWorker() {
} }
// uiPIDPath / uiLogPath : suivi du worker de lien hors systemd (macOS, // uiPIDPath / uiLogPath : suivi du worker de lien hors systemd (macOS,
// Windows), où AJEAN est une app de bureau lancée sans droits root. // Windows), où Loki est une app de bureau lancée sans droits root.
func uiPIDPath() string { return filepath.Join(AjeanHome(), uiUnitName+".pid") } func uiPIDPath() string { return filepath.Join(LokiHome(), uiUnitName+".pid") }
func uiLogPath() string { return filepath.Join(AjeanHome(), uiUnitName+".log") } func uiLogPath() string { return filepath.Join(LokiHome(), uiUnitName+".log") }
// uiServiceCtl pilote le worker de lien (start/stop/restart). Sous Linux c'est // 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 // 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 // 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 // « arrêté » sur un Mac ou un PC : le token était enregistré mais rien ne
// composait jamais le tunnel. // composait jamais le tunnel.
@@ -422,7 +422,7 @@ func uiUserSvcCtl(action string) error {
if p, err := filepath.EvalSymlinks(self); err == nil { if p, err := filepath.EvalSymlinks(self); err == nil {
self = p self = p
} }
if err := os.MkdirAll(AjeanHome(), 0o755); err != nil { if err := os.MkdirAll(LokiHome(), 0o755); err != nil {
return err return err
} }
logf, err := os.OpenFile(uiLogPath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) 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 := spawnDetached(self, "web")
cmd.Stdout, cmd.Stderr = logf, logf cmd.Stdout, cmd.Stderr = logf, logf
if err := cmd.Start(); err != nil { 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 := cmd.Process.Pid
// PID + binaire d'origine : la 2e ligne dit QUELLE copie de l'app a lancé ce // 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 // - /v1/*, /health, /props, /metrics, /slots → llama-server local (endpoint
// compatible OpenAI, avec injection de la clé API locale) → permet de // compatible OpenAI, avec injection de la clé API locale) → permet de
// brancher OpenCode, Hermes, etc. sur ajean.link/oai/<machine>/v1 // 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 { func newLinkHandler(mux *http.ServeMux) http.Handler {
web := withLocalAuth(mux) web := withLocalAuth(mux)
llama := &url.URL{Scheme: "http", Host: fmt.Sprintf("127.0.0.1:%d", LLMPort())} 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/chat : chat chiffré (streaming SSE chiffré).
// - /api/e2e/req : proxy de contrôle chiffré (presets, VRAM, skills, service…). // - /api/e2e/req : proxy de contrôle chiffré (presets, VRAM, skills, service…).
// Tout autre /api/* en clair est REFUSÉ via le tunnel. // 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path p := r.URL.Path
if strings.HasPrefix(p, "/api/e2e/") { 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 : // L'endpoint OpenAI (OpenCode/Hermes) ne peut pas être chiffré navigateur :
// il transiterait en clair par le relais. Désactivé par défaut. // il transiterait en clair par le relais. Désactivé par défaut.
if !oaiAllowed { 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 return
} }
lp.ServeHTTP(w, r) 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 // ⚠️ 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 // 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 // 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 // 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 // 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]")) fmt.Printf("%s lien établi ✓\n", green("[link]"))
// Chaque Accept() = un stream. Deux natures possibles : // 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. // - 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 // On les distingue au 1er octet (0x16 = handshake TLS). Sans OAI activé, tout
// va au HTTP (comportement historique inchangé). // va au HTTP (comportement historique inchangé).
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"net/http" "net/http"
@@ -8,7 +8,7 @@ import (
// Changer la clé de pilotage ne doit PAS couper l'accès distant jusqu'au // 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 // 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 // le portail restait bloqué sur « chargement de la conversation » sans jamais
// dire pourquoi. // dire pourquoi.
func TestWithLocalAuthSuitLaCleCourante(t *testing.T) { func TestWithLocalAuthSuitLaCleCourante(t *testing.T) {
@@ -1,17 +1,17 @@
// Sous-commande « ajean remote » — client « poste distant » intégré au binaire // 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 AJEAN puisse agir // 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) // dessus (shell/fichiers), chiffré de bout en bout via ajean.link (relais aveugle)
// ou en direct sur le réseau local. // 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) // appaire ce PC et l'installe en service (démarrage auto)
// ajean remote connect <url> ... idem mais en avant-plan (test) // loki remote connect <url> ... idem mais en avant-plan (test)
// ajean remote run boucle client (utilisé par le service) // loki remote run boucle client (utilisé par le service)
// ajean remote status | uninstall | logout // 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. // pour n'avoir qu'une seule application.
package ajean package loki
import ( import (
"context" "context"
@@ -20,7 +20,7 @@ import (
"runtime" "runtime"
"strings" "strings"
"github.com/nathaninline/ajean/internal/nodeclient" "github.com/R0m1k3/Loki/internal/nodeclient"
) )
func cmdRemote(args []string) error { func cmdRemote(args []string) error {
@@ -59,18 +59,18 @@ func cmdRemote(args []string) error {
} }
func remoteUsage() { 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) loki 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) loki remote connect <url-serveur> --code CODE [options] idem en avant-plan (pour tester)
ajean remote status état de la configuration loki remote status état de la configuration
ajean remote uninstall retire le service (garde la config) loki remote uninstall retire le service (garde la config)
ajean remote logout retire le service ET oublie la clé loki remote logout retire le service ET oublie la clé
Options : Options :
--machine ID machine à joindre via ajean.link (fournie par l'UI → Postes distants) --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) --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) --allow LISTE capacités acceptées : shell,read,write,list (défaut : ce que le serveur autorise)
--root DIR dossier autorisé (confine lecture/écriture) --root DIR dossier autorisé (confine lecture/écriture)
--yes auto-approuve les actions (obligatoire pour le service, sans terminal) --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() cfg, lerr := nodeclient.LoadConfig()
// Un --code est un jeton d'appairage à usage unique : le fournir veut TOUJOURS // Un --code est un jeton d'appairage à usage unique : le fournir veut TOUJOURS
// dire « (ré)appaire-moi maintenant ». Sans ce « || f.code != "" », une config // 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 // résiduelle (test précédent, ancien binaire loki-remote au même emplacement
// %ProgramData%\ajean-remote\config.json) faisait sauter l'appairage en silence : // %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 // le service démarrait avec de vieilles clés que le serveur ignore et le poste
// ne remontait jamais, alors que tout affichait « [ok] ». // ne remontait jamais, alors que tout affichait « [ok] ».
needEnroll := lerr != nil || cfg.Priv == "" || f.code != "" needEnroll := lerr != nil || cfg.Priv == "" || f.code != ""
@@ -148,7 +148,7 @@ func remoteConnect(args []string, install bool) error {
func remoteRun() error { func remoteRun() error {
cfg, err := nodeclient.LoadConfig() cfg, err := nodeclient.LoadConfig()
if err != nil { 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) return nodeclient.RunService(cfg)
} }
+33 -33
View File
@@ -1,7 +1,7 @@
// ajean — single-binary LLM server manager + web UI for llama.cpp deployments. // loki — 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 // 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). // .syso/go:generate vivent là-bas, dans le dossier du package main).
package ajean package loki
import ( import (
"fmt" "fmt"
@@ -12,7 +12,7 @@ import (
const Version = "0.9.4" 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() { func Main() {
// Rattache la console du terminal parent si on est lancé depuis un shell // Rattache la console du terminal parent si on est lancé depuis un shell
// (Windows : binaire GUI). Retourne false au double-clic (aucune console) → // (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 // 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 // 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, // 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 { if noArgs && !haveConsole {
mustExit(cmdApp(args)) mustExit(cmdApp(args))
return return
@@ -107,7 +107,7 @@ func Main() {
case "uninstall": case "uninstall":
mustExit(cmdUninstall(args)) mustExit(cmdUninstall(args))
case "version", "-v", "--version": case "version", "-v", "--version":
fmt.Println("ajean", Version) fmt.Println("loki", Version)
case "help", "-h", "--help", "": case "help", "-h", "--help", "":
printHelp() printHelp()
default: default:
@@ -118,14 +118,14 @@ func Main() {
} }
func printHelp() { 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). (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 start | stop | restart gérer le service
status | logs état / logs en direct status | logs état / logs en direct
enable | disable auto-démarrage au boot 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 network [on|off|status] rendre l'endpoint OpenAI joignable depuis le réseau local
(HOST + règle de pare-feu sous Windows) (HOST + règle de pare-feu sous Windows)
Interface (ajean-ui) : Interface (loki-ui) :
ui [start|stop|restart|status] pilote le service d'interface ui [start|stop|restart|status] pilote le service d'interface
web [PORT] sert l'interface au premier plan (défaut :8090) — web [PORT] sert l'interface au premier plan (défaut :8090) —
ouvre aussi le tunnel si un jeton est enregistré 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 code code d'appairage (10 min, usage unique) pour le portail
link status | logout état du jeton / l'oublier 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] remote install <url> --machine ID --key CLÉ --code CODE [--allow shell,read,write,list]
appaire ce PC + installe le service (démarrage auto) appaire ce PC + installe le service (démarrage auto)
remote connect <url> … idem en avant-plan (pour tester) remote connect <url> … idem en avant-plan (pour tester)
@@ -179,8 +179,8 @@ Installation:
serve entrypoint du service : exec llama-server (usage interne) serve entrypoint du service : exec llama-server (usage interne)
Env: Env:
AJEAN_HOME racine des données (défaut : /etc/ajean, %%ProgramData%%\ajean sous Windows) LOKI_HOME racine des données (défaut : /etc/loki, %%ProgramData%%\loki sous Windows)
EDITOR éditeur pour 'ajean edit' (défaut : nano, notepad sous Windows) EDITOR éditeur pour 'loki edit' (défaut : nano, notepad sous Windows)
`, Version) `, Version)
} }
@@ -191,21 +191,21 @@ func mustExit(err error) {
} }
} }
// AjeanHome resolves the AJEAN data directory. // LokiHome resolves the Loki data directory.
// Précédence : $AJEAN_HOME → /etc/default/ajean (unix) → défaut plateforme. // Précédence : $LOKI_HOME → /etc/default/loki (unix) → défaut plateforme.
func AjeanHome() string { func LokiHome() string {
if h := os.Getenv("AJEAN_HOME"); h != "" { if h := os.Getenv("LOKI_HOME"); h != "" {
return h return h
} }
if h := readEtcDefault(); h != "" { if h := readEtcDefault(); h != "" {
return 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 { func readEtcDefault() string {
b, err := os.ReadFile("/etc/default/ajean") b, err := os.ReadFile("/etc/default/loki")
if err != nil { if err != nil {
return "" return ""
} }
@@ -215,30 +215,30 @@ func readEtcDefault() string {
continue continue
} }
s = strings.TrimPrefix(s, "export ") 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 strings.Trim(strings.TrimSpace(v), "\"'")
} }
} }
return "" 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 // tout le reste (config, préférences, conversation, clés, drapeaux) vit dans la
// base ajean.db — voir store.go. // base loki.db — voir store.go.
func backendsDir() string { return filepath.Join(AjeanHome(), "backends") } func backendsDir() string { return filepath.Join(LokiHome(), "backends") }
func binDir() string { return filepath.Join(AjeanHome(), "bin") } func binDir() string { return filepath.Join(LokiHome(), "bin") }
func presetsDir() string { return filepath.Join(AjeanHome(), "presets") } func presetsDir() string { return filepath.Join(LokiHome(), "presets") }
func memoryDir() string { return filepath.Join(AjeanHome(), "memory") } func memoryDir() string { return filepath.Join(LokiHome(), "memory") }
func modelsDir() string { return filepath.Join(AjeanHome(), "models") } func modelsDir() string { return filepath.Join(LokiHome(), "models") }
func workspaceDir() string { return filepath.Join(AjeanHome(), "workspace") } func workspaceDir() string { return filepath.Join(LokiHome(), "workspace") }
// serviceName est le nom de l'unité qui exécute llama-server. Son pendant est // 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 { func serviceName() string {
if n := os.Getenv("AJEAN_SERVICE"); n != "" { if n := os.Getenv("LOKI_SERVICE"); n != "" {
return n return n
} }
return "ajean-engine" return "loki-engine"
} }
// Color helpers (ANSI). Disabled when stdout is not a TTY. // Color helpers (ANSI). Disabled when stdout is not a TTY.
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"encoding/json" "encoding/json"
@@ -13,7 +13,7 @@ import (
bolterrors "go.etcd.io/bbolt/errors" 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, // Avant, chaque réglage avait son fichier : config.env, webprefs.json,
// conversation.json, .api_key, .link_token, .agent_enabled, model_dirs.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 // 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 // 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 // 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 // é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. // 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. // contention entre process ; dbMu la sérialise à l'intérieur du process.
var dbMu sync.Mutex 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. // withDB ouvre la base, exécute fn, puis referme — toujours, même en erreur.
func withDB(fn func(*bolt.DB) error) error { func withDB(fn func(*bolt.DB) error) error {
@@ -1,9 +1,9 @@
package ajean package loki
import "testing" import "testing"
// Le cache de lecture ne doit jamais servir une valeur périmée après une // 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. // démarrer sur la NOUVELLE configuration.
func TestCacheConfigInvalideParEcriture(t *testing.T) { func TestCacheConfigInvalideParEcriture(t *testing.T) {
testHome(t) testHome(t)
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"sync" "sync"
@@ -1,14 +1,14 @@
package ajean package loki
import "testing" 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 // test ne doit jamais poser (ni retirer) une règle entrante sur la machine qui
// le fait tourner. // le fait tourner.
func testHome(t *testing.T) string { func testHome(t *testing.T) string {
t.Helper() t.Helper()
home := t.TempDir() home := t.TempDir()
t.Setenv("AJEAN_HOME", home) t.Setenv("LOKI_HOME", home)
firewallInert = true firewallInert = true
t.Cleanup(func() { firewallInert = false }) t.Cleanup(func() { firewallInert = false })
return home 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 » // 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 // 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. // 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 // 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 ( import (
"bytes" "bytes"
@@ -1,16 +1,16 @@
//go:build darwin //go:build darwin
package ajean package loki
// sys_console_darwin.go — détection du lancement « par clic » sous macOS. // 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 // 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 // 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 // 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 // publie un bundle Loki.app dont le binaire vit dans
// AJEAN.app/Contents/MacOS/ajean. Si on tourne depuis là, c'est un double-clic // 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 // 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. // 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 { if p, err := filepath.EvalSymlinks(exe); err == nil {
exe = p exe = p
} }
// .../AJEAN.app/Contents/MacOS/ajean // .../Loki.app/Contents/MacOS/loki
if strings.HasSuffix(filepath.Dir(exe), "/Contents/MacOS") { if strings.HasSuffix(filepath.Dir(exe), "/Contents/MacOS") {
fixFinderPath() fixFinderPath()
return false return false
@@ -36,7 +36,7 @@ func setupConsole() bool {
return true 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 // é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 // 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 à // 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 // 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 // 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. // 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 { func appWarning() string {
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
@@ -53,14 +53,14 @@ func appWarning() string {
if !strings.Contains(exe, "/AppTranslocation/") { if !strings.Contains(exe, "/AppTranslocation/") {
return "" return ""
} }
return "AJEAN tourne depuis une copie temporaire en lecture seule (App Translocation de macOS). " + return "Loki 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 — " + "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." "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 // 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 // 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. // compilation d'un backend échoue alors que les outils sont bien installés.
func fixFinderPath() { func fixFinderPath() {
extra := []string{ extra := []string{
@@ -1,12 +1,12 @@
//go:build !windows && !darwin //go:build !windows && !darwin
package ajean package loki
// setupConsole : sous Linux, le process a toujours une vraie console/tty // setupConsole : sous Linux, le process a toujours une vraie console/tty
// standard (stdout hérité). Rien à faire → on signale simplement « on a une // standard (stdout hérité). Rien à faire → on signale simplement « on a une
// console ». Les cas « lancé par un clic » sont traités par // 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 // sys_console_windows.go (double-clic sur loki.exe) et sys_console_darwin.go
// (ouverture de AJEAN.app depuis le Finder). // (ouverture de Loki.app depuis le Finder).
func setupConsole() bool { return true } func setupConsole() bool { return true }
// appWarning : rien à signaler hors macOS (l'App Translocation est spécifique à // appWarning : rien à signaler hors macOS (l'App Translocation est spécifique à
@@ -1,19 +1,19 @@
//go:build windows //go:build windows
package ajean package loki
// sys_console_windows.go — gestion de la console sous Windows. // 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` // 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 // 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` // La même cause cassait la redirection et les tubes, et rendait `loki chat`
// inutilisable depuis un terminal (le shell et AJEAN se disputaient l'entrée). // inutilisable depuis un terminal (le shell et Loki se disputaient l'entrée).
// //
// EN CONTREPARTIE, Windows alloue une console à tout lancement sans terminal — // EN CONTREPARTIE, Windows alloue une console à tout lancement sans terminal —
// un double-clic. On la referme alors immédiatement : voir setupConsole. Les // un double-clic. On la referme alors immédiatement : voir setupConsole. Les
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"os" "os"
@@ -11,15 +11,15 @@ import (
// modèle de config.env : quatre copies qui divergeaient à la première // modèle de config.env : quatre copies qui divergeaient à la première
// modification. Il n'y en a plus qu'une. // modification. Il n'y en a plus qu'une.
// dataDirs est l'arborescence complète de $AJEAN_HOME. Rien d'autre n'y est // dataDirs est l'arborescence complète de $LOKI_HOME. Rien d'autre n'y est
// créé : tout le reste vit dans ajean.db. // créé : tout le reste vit dans loki.db.
func dataDirs() []string { 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 // defaultConfig est la configuration de départ d'une installation neuve. Les
// valeurs sont volontairement incomplètes (BIN et MODEL sont vides) : c'est // 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 { func defaultConfig() map[string]string {
host := "0.0.0.0" host := "0.0.0.0"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@@ -1,6 +1,6 @@
//go:build !windows //go:build !windows
package ajean package loki
// sys_disk_other.go — espace libre d'un système de fichiers via statfs(2). // 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 // 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 // sys_disk_windows.go — espace libre d'un volume via GetDiskFreeSpaceExW. On
// veut l'espace réellement disponible pour l'utilisateur courant (quotas // veut l'espace réellement disponible pour l'utilisateur courant (quotas
@@ -1,6 +1,6 @@
//go:build !windows //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 // 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 // en a un (ufw, firewalld, pf) c'est une affaire d'administration système qu'un
@@ -1,6 +1,6 @@
//go:build windows //go:build windows
package ajean package loki
import ( import (
"fmt" "fmt"
@@ -14,9 +14,9 @@ import (
// Sur une installation Windows fraîche, même avec HOST=0.0.0.0, le pare-feu // 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 // 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 // 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 // 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 // que ça a marché — firewallState relit la règle, et firewallManualHint donne la
// commande exacte à coller dans un terminal administrateur. // commande exacte à coller dans un terminal administrateur.
@@ -24,7 +24,7 @@ import (
// firewallRuleName identifie nos règles. Une par port : changer PORT dans la // firewallRuleName identifie nos règles. Une par port : changer PORT dans la
// configuration ne doit pas laisser une règle ouverte sur l'ancien. // configuration ne doit pas laisser une règle ouverte sur l'ancien.
func firewallRuleName(port int) string { 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 // 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 // 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 { func firewallManualHint(port int) string {
return fmt.Sprintf("le pare-feu Windows bloque encore le port %d. Ouvre un terminal ADMINISTRATEUR et lance :\n"+ 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`, ` 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 //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, // 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 // donc pas de boîte de dialogue. On se contente de provisionner les données au
// premier lancement. Ne relance jamais (renvoie toujours false). // premier lancement. Ne relance jamais (renvoie toujours false).
@@ -1,6 +1,6 @@
//go:build windows //go:build windows
package ajean package loki
import ( import (
"crypto/sha256" "crypto/sha256"
@@ -22,8 +22,8 @@ import (
// Premier lancement sous Windows. // Premier lancement sous Windows.
// //
// Avant : double-cliquer ajean-windows-amd64.exe déclenchait une installation // Avant : double-cliquer loki-windows-amd64.exe déclenchait une installation
// SILENCIEUSE (copie du binaire dans %ProgramData%\ajean\bin, ajout au PATH) dont // SILENCIEUSE (copie du binaire dans %ProgramData%\loki\bin, ajout au PATH) dont
// l'utilisateur ne voyait rien — cmdApp appelait cmdInstall sans console pour en // 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 // 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, // que ça lance ? Et surtout, deux copies du binaire coexistaient (celle du Bureau,
@@ -32,13 +32,13 @@ import (
// //
// Maintenant, deux cas seulement : // 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 // l'emplacement du fichier téléchargé ne donne pas une installation
// utilisable, la question n'avait donc qu'une réponse. Copie + PATH + // 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 // raccourcis, un message qui dit ce qui a été fait, et on démarre depuis la
// copie installée. // 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) : // 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 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 // 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) { if canonPath(exe) == canonPath(target) {
// On EST l'application installée. On en profite pour garantir que les // On EST l'application installée. On en profite pour garantir que les
// raccourcis existent : sans ça, quelqu'un qui perd son raccourci ne // 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 // 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. // fond, l'appel PowerShell ne doit pas retarder le démarrage.
go ensureShortcuts(target) go ensureShortcuts(target)
@@ -114,18 +114,18 @@ func appFirstRun() bool {
// un fichier, autant dire où le programme s'est installé et qu'il peut // un fichier, autant dire où le programme s'est installé et qu'il peut
// supprimer ce qu'il a téléchargé. // supprimer ce qu'il a téléchargé.
if _, err := installSelf(filepath.Dir(target)); err != nil { 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 // parfaitement utilisable depuis son emplacement actuel, le dossier de
// données étant ailleurs — on démarre donc au lieu d'abandonner. // 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 return false
} }
_, _ = addToUserPath(filepath.Dir(target)) _, _ = addToUserPath(filepath.Dir(target))
shortcuts := ensureShortcuts(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é.", "\n\nL'application va démarrer. Vous pouvez supprimer le fichier téléchargé.",
"AJEAN", mbIconInfo) "Loki", mbIconInfo)
return launch(target) return launch(target)
} }
@@ -193,7 +193,7 @@ func launch(target string) bool {
return cmd.Start() == nil 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 // 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 // mise à jour. Trois situations, trois comportements distincts, parce qu'elles
// n'appellent pas la même décision de l'utilisateur. // 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 // 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. // croire qu'on utilise la version qu'on vient de télécharger.
if messageBox( 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"+ " installée : "+installed+"\n"+
" ce fichier : "+Version+"\n\n"+ " ce fichier : "+Version+"\n\n"+
"Rien ne sera remplacé.\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.", "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 return true // rien à faire, on quitte sans démarrer
} }
ensureShortcuts(target) ensureShortcuts(target)
return launch(target) return launch(target)
case cmp > 0 && len(running) > 0: 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 // 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 // yeux : l'onglet qui s'ouvrirait serait servi par l'ancienne version. On
// pose donc la seule question qui compte. // pose donc la seule question qui compte.
if messageBox( 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"+ " version en cours : "+verLabel(installed)+"\n"+
" ce fichier : "+Version+" (plus récente)\n\n"+ " ce fichier : "+Version+" (plus récente)\n\n"+
"Fermer AJEAN et le redémarrer pour appliquer la mise à jour ?\n\n"+ "Fermer Loki et le redémarrer pour appliquer la mise à jour ?\n\n"+
"Répondre Non ouvre AJEAN dans sa version actuelle, sans rien mettre à jour.", "Répondre Non ouvre Loki dans sa version actuelle, sans rien mettre à jour.",
"Mise à jour d'AJEAN", mbYesNo|mbIconQuestion) != idYes { "Mise à jour d'Loki", mbYesNo|mbIconQuestion) != idYes {
ensureShortcuts(target) ensureShortcuts(target)
return launch(target) // l'instance en cours reprend la main (port occupé) return launch(target) // l'instance en cours reprend la main (port occupé)
} }
stopProcesses(running) stopProcesses(running)
if err := replaceInstalled(target); err != nil { 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.", messageBox("La mise à jour a échoué :\n\n"+err.Error()+"\n\nLoki va redémarrer dans sa version actuelle.",
"AJEAN", mbIconInfo) "Loki", mbIconInfo)
} }
ensureShortcuts(target) ensureShortcuts(target)
return launch(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 // 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 // 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 { func replaceInstalled(target string) error {
removeOldBinaries(target) removeOldBinaries(target)
old, err := renameAside(target) // nom unique, cf. renameAside 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 // 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. // le processus courant et toute autre copie sans rapport.
// //
// La comparaison se fait ICI, sur des chemins canonisés, et non dans le script // 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, // 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. // 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 // 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 // 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é). // toujours en silence et la mise à jour ne se ferait jamais (vérifié).
@@ -429,7 +429,7 @@ type vsFixedFileInfo struct {
FileDateLS uint32 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 à // 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 : // CHAQUE lancement du fichier téléchargé, pas seulement à l'installation :
// un raccourci perdu (nettoyage, migration de profil, dossier Démarrer // 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'))) { foreach ($d in @($progs, [Environment]::GetFolderPath('Desktop'))) {
if (-not $d) { continue } if (-not $d) { continue }
if (-not (Test-Path $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 # 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 # Le laisser en l'état condamnait l'utilisateur à relancer indéfiniment une
# version périmée par son propre raccourci. # version périmée par son propre raccourci.
if (Test-Path $lnk) { if (Test-Path $lnk) {
@@ -476,9 +476,9 @@ foreach ($d in @($progs, [Environment]::GetFolderPath('Desktop'))) {
$s=$w.CreateShortcut($lnk) $s=$w.CreateShortcut($lnk)
$s.TargetPath=$t $s.TargetPath=$t
$s.WorkingDirectory=(Split-Path $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 # 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. # démarrage minimisé garantit qu'elle n'est jamais peinte à l'écran.
$s.WindowStyle=7 $s.WindowStyle=7
$s.IconLocation="$t,0" $s.IconLocation="$t,0"
@@ -490,9 +490,9 @@ Write-Output ($done -join ';')`, psQuote(target))
cmd := hideCmd(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps)) cmd := hideCmd(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps))
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
if err != nil || strings.TrimSpace(string(out)) == "" { 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 // removeShortcuts efface les raccourcis posés par ensureShortcuts. Renvoie true
@@ -503,7 +503,7 @@ func removeShortcuts() bool {
$sm=[Environment]::GetFolderPath('StartMenu') $sm=[Environment]::GetFolderPath('StartMenu')
foreach ($d in @($sm, (Join-Path $sm 'Programs'), [Environment]::GetFolderPath('Desktop'))) { foreach ($d in @($sm, (Join-Path $sm 'Programs'), [Environment]::GetFolderPath('Desktop'))) {
if (-not $d) { continue } 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 {} } if (Test-Path $p) { try { Remove-Item $p -Force; $n++ } catch {} }
} }
Write-Output $n` Write-Output $n`
@@ -1,6 +1,6 @@
//go:build windows //go:build windows
package ajean package loki
import ( import (
"os" "os"
@@ -12,7 +12,7 @@ import (
// binaryVersion pilote la mise à jour automatique du binaire installé quand on // 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 // 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 // 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. // binaire en sous-système GUI). D'où ces tests sur la plomberie syscall.
func TestBinaryVersionReadsResource(t *testing.T) { func TestBinaryVersionReadsResource(t *testing.T) {
// kernel32.dll porte toujours une ressource de version. // 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 // Sur une machine vierge, le dossier bin n'existe pas encore : installSelf doit
// le créer. Sans ça, le premier lancement échouait sur « open // le créer. Sans ça, le premier lancement échouait sur « open
// C:\ProgramData\ajean\bin\ajean.exe: The system cannot find the path specified » // C:\ProgramData\loki\bin\loki.exe: The system cannot find the path specified »
// et AJEAN ne s'installait jamais. // et Loki ne s'installait jamais.
func TestInstallSelfCreatesBinDir(t *testing.T) { 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) dst, err := installSelf(binDir)
if err != nil { if err != nil {
t.Fatalf("installSelf sur un dossier absent : %v", err) 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. // cible existe déjà, et laisser l'ancien de côté sans le confondre avec la cible.
func TestReplaceInstalled(t *testing.T) { func TestReplaceInstalled(t *testing.T) {
dir := t.TempDir() 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 { if err := os.WriteFile(target, []byte("ancienne version"), 0o755); err != nil {
t.Fatal(err) 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 // 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 // restait fige sur une version perimee, et tout raccourci le visant relancait
// indefiniment l'ancienne version — qui affichait « une version plus recente // indefiniment l'ancienne version — qui affichait « une version plus recente
@@ -89,7 +89,7 @@ func TestBinaryVersionWithoutResource(t *testing.T) {
func TestReplaceExeSurchargeUnFichierVerrouille(t *testing.T) { func TestReplaceExeSurchargeUnFichierVerrouille(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
src := filepath.Join(dir, "neuf.exe") 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 { if err := os.WriteFile(src, []byte("VERSION-NEUVE"), 0o755); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -120,7 +120,7 @@ func TestReplaceExeSurchargeUnFichierVerrouille(t *testing.T) {
// echoue apres le renommage, l'ancien fichier revient a sa place. // echoue apres le renommage, l'ancien fichier revient a sa place.
func TestReplaceExeRestaureSiLaCopieEchoue(t *testing.T) { func TestReplaceExeRestaureSiLaCopieEchoue(t *testing.T) {
dir := t.TempDir() 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 { if err := os.WriteFile(dst, []byte("a-preserver"), 0o755); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1,6 +1,6 @@
//go:build darwin //go:build darwin
package ajean package loki
import ( import (
"fmt" "fmt"
@@ -17,7 +17,7 @@ import (
// Implémentation prudente, à valider sur une machine Apple. // Implémentation prudente, à valider sur une machine Apple.
// launchdPlistTemplate — champs : Label, binaire, argument, UserName, // 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 // KeepAlive/SuccessfulExit=false ≈ Restart=on-failure ; RunAtLoad relance au
// boot une fois chargé avec `-w`. // boot une fois chargé avec `-w`.
const launchdPlistTemplate = `<?xml version="1.0" encoding="UTF-8"?> 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>UserName</key><string>%s</string>
<key>WorkingDirectory</key><string>%s</string> <key>WorkingDirectory</key><string>%s</string>
<key>EnvironmentVariables</key> <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>RunAtLoad</key><true/>
<key>KeepAlive</key> <key>KeepAlive</key>
<dict><key>SuccessfulExit</key><false/></dict> <dict><key>SuccessfulExit</key><false/></dict>
@@ -48,12 +48,12 @@ func unitArgs() map[string]string {
return map[string]string{serviceName(): "serve", uiUnitName: "web"} return map[string]string{serviceName(): "serve", uiUnitName: "web"}
} }
func installServices(targetUser, ajeanHome string) error { func installServices(targetUser, lokiHome string) error {
exe := installedExePath() exe := installedExePath()
for name, arg := range unitArgs() { for name, arg := range unitArgs() {
logPath := filepath.Join(ajeanHome, name+".log") logPath := filepath.Join(lokiHome, name+".log")
plist := fmt.Sprintf(launchdPlistTemplate, 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) path := launchdPlistPath(name)
if err := os.WriteFile(path, []byte(plist), 0o644); err != nil { if err := os.WriteFile(path, []byte(plist), 0o644); err != nil {
return err return err
@@ -1,6 +1,6 @@
//go:build linux //go:build linux
package ajean package loki
import ( import (
"fmt" "fmt"
@@ -14,14 +14,14 @@ import (
// sudoersTemplate autorise l'utilisateur à piloter UNE unité sans mot de passe. // sudoersTemplate autorise l'utilisateur à piloter UNE unité sans mot de passe.
// Posé pour les deux : l'interface, qui tourne sans privilèges, doit pouvoir // 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). // 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 %[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 // engineUnitTemplate — le moteur : exec llama-server, supervisé directement par
// systemd. Champs : User, WorkingDirectory, ExecStart. // systemd. Champs : User, WorkingDirectory, ExecStart.
const engineUnitTemplate = `[Unit] const engineUnitTemplate = `[Unit]
Description=AJEAN — moteur llama.cpp Description=Loki — moteur llama.cpp
After=network.target After=network.target
[Service] [Service]
@@ -47,7 +47,7 @@ WantedBy=multi-user.target
// OpenAI, servis par un SEUL process (donc une seule conversation). // OpenAI, servis par un SEUL process (donc une seule conversation).
// Champs : unité du moteur (dépendance), User, WorkingDirectory, ExecStart. // Champs : unité du moteur (dépendance), User, WorkingDirectory, ExecStart.
const uiUnitTemplate = `[Unit] const uiUnitTemplate = `[Unit]
Description=AJEAN — interface web + accès distant Description=Loki — interface web + accès distant
After=network-online.target %s.service After=network-online.target %s.service
Wants=network-online.target Wants=network-online.target
@@ -63,11 +63,11 @@ RestartSec=5
WantedBy=multi-user.target WantedBy=multi-user.target
` `
func installServices(targetUser, ajeanHome string) error { func installServices(targetUser, lokiHome string) error {
svc, exe := serviceName(), installedExePath() svc, exe := serviceName(), installedExePath()
units := map[string]string{ units := map[string]string{
svc: fmt.Sprintf(engineUnitTemplate, targetUser, ajeanHome, exe+" serve"), svc: fmt.Sprintf(engineUnitTemplate, targetUser, lokiHome, exe+" serve"),
uiUnitName: fmt.Sprintf(uiUnitTemplate, svc, targetUser, ajeanHome, exe+" web"), uiUnitName: fmt.Sprintf(uiUnitTemplate, svc, targetUser, lokiHome, exe+" web"),
} }
for name, body := range units { for name, body := range units {
path := "/etc/systemd/system/" + name + ".service" path := "/etc/systemd/system/" + name + ".service"
@@ -1,6 +1,6 @@
//go:build !windows //go:build !windows
package ajean package loki
import ( import (
"fmt" "fmt"
@@ -23,7 +23,7 @@ import (
func cmdInstall(args []string) error { func cmdInstall(args []string) error {
if os.Geteuid() != 0 { 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") targetUser := os.Getenv("SUDO_USER")
if targetUser == "" { if targetUser == "" {
@@ -38,19 +38,19 @@ func cmdInstall(args []string) error {
if err != nil { if err != nil {
return fmt.Errorf("utilisateur '%s' introuvable: %w", targetUser, err) return fmt.Errorf("utilisateur '%s' introuvable: %w", targetUser, err)
} }
ajeanHome := defaultAjeanHome() lokiHome := defaultLokiHome()
if v := os.Getenv("AJEAN_HOME"); v != "" { if v := os.Getenv("LOKI_HOME"); v != "" {
ajeanHome = v lokiHome = v
} }
fmt.Printf("Installation pour utilisateur %s\n", cyan(targetUser)) 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) fmt.Printf(" services = %s + %s\n", serviceName(), uiUnitName)
// 0. Reprise d'une installation 0.7, si c'en est une. AVANT provisionDataDir : // 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 // elle déplace des dossiers entiers, donc la destination ne doit pas
// encore exister. Code temporaire, voir migrate_07.go. // 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) 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("✓")) 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 { if err := linkInstalledExe(); err != nil {
return err 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 { if err := os.MkdirAll("/etc/default", 0o755); err != nil {
return err return err
} }
defaults := fmt.Sprintf("# Généré par ajean install — racine des données\nAJEAN_HOME=%s\n", ajeanHome) defaults := fmt.Sprintf("# Généré par loki install — racine des données\nLOKI_HOME=%s\n", lokiHome)
if err := os.WriteFile("/etc/default/ajean", []byte(defaults), 0o644); err != nil { if err := os.WriteFile("/etc/default/loki", []byte(defaults), 0o644); err != nil {
return err 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). // 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 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. // commandes qu'il tape doivent pouvoir écrire dans la base.
chown(ajeanHome, u) chown(lokiHome, u)
fmt.Println() fmt.Println()
fmt.Printf("%s installation terminée.\n", green("[ok]")) fmt.Printf("%s installation terminée.\n", green("[ok]"))
fmt.Printf("\nProchaines étapes :\n") 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(" 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+" ajean edit")) 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 ajean start")) fmt.Printf(" 3. démarre le moteur : %s\n", bold("sudo loki start"))
fmt.Printf(" 4. vérifie : %s\n", bold("ajean test")) fmt.Printf(" 4. vérifie : %s\n", bold("loki test"))
fmt.Printf(" 5. démarre l'interface : %s (http://localhost:8090)\n", bold("sudo ajean ui start")) fmt.Printf(" 5. démarre l'interface : %s (http://localhost:8090)\n", bold("sudo loki ui start"))
return nil 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 // 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 // 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 // lien vers lui-même (« Too many levels of symbolic links »). On compare les
// chemins réels, résolus. // chemins réels, résolus.
@@ -135,15 +135,15 @@ func linkInstalledExe() error {
func cmdUninstall(args []string) error { func cmdUninstall(args []string) error {
if os.Geteuid() != 0 { 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() uninstallServices()
for _, p := range []string{"/etc/default/ajean", installedExePath()} { for _, p := range []string{"/etc/default/loki", installedExePath()} {
if err := os.Remove(p); err == nil { if err := os.Remove(p); err == nil {
fmt.Printf(" %s %s\n", green("✓"), p) 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é") fmt.Println(green("[ok]") + " désinstallé")
return nil return nil
} }
@@ -1,6 +1,6 @@
//go:build windows //go:build windows
package ajean package loki
import ( import (
"fmt" "fmt"
@@ -12,21 +12,21 @@ import (
) )
// On Windows there's no systemd unit, sudoers, or /usr/local/bin to populate. // 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 // 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 { func cmdInstall(args []string) error {
ajeanHome := AjeanHome() lokiHome := LokiHome()
fmt.Printf("Installation (Windows)\n") 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()) fmt.Printf(" service = %s\n", serviceName())
// 0. Reprise d'une installation 0.7, si c'en est une. AVANT provisionDataDir : // 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 // elle déplace des dossiers entiers, donc la destination ne doit pas
// encore exister. Code temporaire, voir migrate_07.go. // 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) 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("✓")) fmt.Printf(" %s arborescence prête\n", green("✓"))
// 2. Le binaire va dans AJEAN_HOME\bin, ajouté au PATH utilisateur, pour que // 2. Le binaire va dans LOKI_HOME\bin, ajouté au PATH utilisateur, pour que
// `ajean` soit appelable depuis n'importe quel shell (pendant Windows du // `loki` soit appelable depuis n'importe quel shell (pendant Windows du
// lien /usr/local/bin créé par l'installateur Unix). // lien /usr/local/bin créé par l'installateur Unix).
dir := binDir() dir := binDir()
onPath := false onPath := false
@@ -62,40 +62,40 @@ func cmdInstall(args []string) error {
fmt.Println() fmt.Println()
fmt.Printf("%s installation terminée.\n", green("[ok]")) fmt.Printf("%s installation terminée.\n", green("[ok]"))
fmt.Printf("\nProchaines étapes :\n") fmt.Printf("\nProchaines étapes :\n")
fmt.Printf(" 1. édite la config : %s (renseigne BIN, MODEL)\n", bold("ajean edit")) fmt.Printf(" 1. édite la config : %s (renseigne BIN, MODEL)\n", bold("loki edit"))
fmt.Printf(" 2. démarre le service: %s\n", bold("ajean start")) fmt.Printf(" 2. démarre le service: %s\n", bold("loki start"))
fmt.Printf(" 3. UI web : %s\n", bold("ajean web")) fmt.Printf(" 3. UI web : %s\n", bold("loki web"))
if onPath { 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 { } 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 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à // renvoie le chemin de destination. Sans effet si l'exe tourne déjà depuis là
// (ré-installation). // (ré-installation).
// //
// binDir est créé ici, et pas seulement par l'appelant : sur une machine vierge, // 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 // 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 // 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 » // C:\ProgramData\loki\bin\loki.exe: The system cannot find the path specified »
// et AJEAN démarrait depuis le fichier téléchargé, sans jamais s'installer. // et Loki démarrait depuis le fichier téléchargé, sans jamais s'installer.
func installSelf(binDir string) (string, error) { func installSelf(binDir string) (string, error) {
src, err := os.Executable() src, err := os.Executable()
if err != nil { if err != nil {
return "", err return "", err
} }
src, _ = filepath.EvalSymlinks(src) src, _ = filepath.EvalSymlinks(src)
dst := filepath.Join(binDir, "ajean.exe") dst := filepath.Join(binDir, "loki.exe")
if strings.EqualFold(src, dst) { if strings.EqualFold(src, dst) {
return dst, nil return dst, nil
} }
if err := os.MkdirAll(binDir, 0o755); err != nil { if err := os.MkdirAll(binDir, 0o755); err != nil {
return "", err 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. // (service en tache de fond), auquel cas Windows refuse de l'ecraser.
if err := replaceExe(src, dst); err != nil { if err := replaceExe(src, dst); err != nil {
return "", err 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 // 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, // 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 // 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 // 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. // 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. // Stop the background server if it's running.
_ = svcStop(false) _ = svcStop(false)
// Pull AJEAN_HOME\bin off the user PATH (best-effort). // Pull LOKI_HOME\bin off the user PATH (best-effort).
dir := binDir() dir := binDir()
if removed, err := removeFromUserPath(dir); err == nil && removed { if removed, err := removeFromUserPath(dir); err == nil && removed {
fmt.Printf(" %s %s retiré du PATH utilisateur\n", green("✓"), dir) 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). // Raccourcis posés à l'installation (menu Démarrer + Bureau).
if removeShortcuts() { if removeShortcuts() {
fmt.Printf(" %s raccourcis « AJEAN » supprimés\n", green("✓")) fmt.Printf(" %s raccourcis « Loki » supprimés\n", green("✓"))
} }
if !keepData { if !keepData {
ajeanHome := AjeanHome() lokiHome := LokiHome()
if err := os.RemoveAll(ajeanHome); err != nil { if err := os.RemoveAll(lokiHome); err != nil {
return fmt.Errorf("suppression de %s: %w", ajeanHome, err) 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 { } else {
fmt.Println(dim("(données utilisateur conservées — relance avec --purge pour tout supprimer)")) fmt.Println(dim("(données utilisateur conservées — relance avec --purge pour tout supprimer)"))
} }
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"encoding/json" "encoding/json"
@@ -11,19 +11,19 @@ import (
// //
// Le moteur écoute à l'adresse HOST. Sous Windows, l'installation pose // 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 // 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 // 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. // chercher : brancher un logiciel tiers sur le modèle qui tourne dans le salon.
// //
// Deux choses manquaient, et pas une seule : // Deux choses manquaient, et pas une seule :
// 1. HOST n'était réglable NULLE PART dans l'interface — il fallait connaître // 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 // 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 // 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 // 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 // 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 // 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") 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 { type netStatus struct {
Exposed bool `json:"exposed"` // le moteur écoute-t-il sur le réseau Exposed bool `json:"exposed"` // le moteur écoute-t-il sur le réseau
Host string `json:"host"` // adresse d'écoute effective Host string `json:"host"` // adresse d'écoute effective
@@ -103,7 +103,7 @@ func setLANExposure(on bool) (netStatus, error) {
return networkStatus(), nil return networkStatus(), nil
} }
// cmdNetwork : `ajean network [on|off|status]`. // cmdNetwork : `loki network [on|off|status]`.
func cmdNetwork(args []string) error { func cmdNetwork(args []string) error {
sub := "status" sub := "status"
if len(args) > 0 { 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]")) fmt.Printf("%s le moteur n'écoutera plus que sur cette machine\n", green("[ok]"))
} }
printNetHint(st) 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 return nil
case "status", "": case "status", "":
st := networkStatus() st := networkStatus()
@@ -136,7 +136,7 @@ func cmdNetwork(args []string) error {
case "ferme": case "ferme":
fmt.Printf(" pare-feu : %s\n", yellow("aucune règle pour ce port")) fmt.Printf(" pare-feu : %s\n", yellow("aucune règle pour ce port"))
default: 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 { if st.Exposed {
fmt.Printf(" endpoint OpenAI : %s\n", bold(st.URL)) fmt.Printf(" endpoint OpenAI : %s\n", bold(st.URL))
@@ -144,7 +144,7 @@ func cmdNetwork(args []string) error {
printNetHint(st) printNetHint(st)
return nil return nil
} }
return fmt.Errorf("usage: ajean network [on|off|status]") return fmt.Errorf("usage: loki network [on|off|status]")
} }
func printNetHint(st netStatus) { func printNetHint(st netStatus) {
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"encoding/json" "encoding/json"
@@ -1,4 +1,4 @@
package ajean package loki
import ( import (
"fmt" "fmt"
@@ -13,11 +13,11 @@ import (
// emplacements invisibles : on ne sait plus si le .exe qu'on a double-cliqué EST // 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 // 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, // 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 { type lokiPaths struct {
Home string `json:"home"` // AJEAN_HOME : racine des données Home string `json:"home"` // LOKI_HOME : racine des données
Database string `json:"database"` // ajean.db : config, préférences, conversation, clés Database string `json:"database"` // loki.db : config, préférences, conversation, clés
Exe string `json:"exe"` // binaire en cours d'exécution Exe string `json:"exe"` // binaire en cours d'exécution
Installed string `json:"installed"` // binaire installé (peut différer de Exe) Installed string `json:"installed"` // binaire installé (peut différer de Exe)
Workspace string `json:"workspace"` // dossier de travail du mode agent 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. // 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, // (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 { func installedExePath() string {
if runtime.GOOS == "windows" { 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() exe, _ := os.Executable()
if resolved, err := filepath.EvalSymlinks(exe); err == nil { if resolved, err := filepath.EvalSymlinks(exe); err == nil {
exe = resolved exe = resolved
} }
return ajeanPaths{ return lokiPaths{
Home: AjeanHome(), Home: LokiHome(),
Database: dbPath(), Database: dbPath(),
Exe: exe, Exe: exe,
Installed: installedExePath(), Installed: installedExePath(),
@@ -58,9 +58,9 @@ func currentPaths() ajeanPaths {
func cmdWhere(args []string) error { func cmdWhere(args []string) error {
p := currentPaths() p := currentPaths()
fmt.Printf("Emplacements AJEAN\n\n") fmt.Printf("Emplacements Loki\n\n")
for _, row := range [][2]string{ for _, row := range [][2]string{
{"données (AJEAN_HOME)", p.Home}, {"données (LOKI_HOME)", p.Home},
{"base", p.Database}, {"base", p.Database},
{"binaire en cours", p.Exe}, {"binaire en cours", p.Exe},
{"binaire installé", p.Installed}, {"binaire installé", p.Installed},
@@ -1,6 +1,6 @@
//go:build unix //go:build unix
package ajean package loki
import ( import (
"context" "context"
@@ -16,22 +16,22 @@ import (
"time" "time"
) )
// defaultAjeanHome est la racine des données quand ni $AJEAN_HOME ni // defaultLokiHome est la racine des données quand ni $LOKI_HOME ni
// /etc/default/ajean n'en imposent une. // /etc/default/loki n'en imposent une.
// //
// Linux : /etc/ajean (machine de prod, service systemd lancé par root). // Linux : /etc/loki (machine de prod, service systemd lancé par root).
// macOS : /etc/ajean n'est PAS écrivable par une app lancée depuis le Finder // macOS : /etc/loki 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, // (« 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 // 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. // un `sudo loki install` a déjà créé /etc/loki à notre nom.
func defaultAjeanHome() string { return unixHome("ajean") } func defaultLokiHome() string { return unixHome("loki") }
func unixHome(name string) string { func unixHome(name string) string {
if runtime.GOOS != "darwin" || os.Geteuid() == 0 { if runtime.GOOS != "darwin" || os.Geteuid() == 0 {
return "/etc/" + name return "/etc/" + name
} }
if isWritableDir("/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 { if home, err := os.UserHomeDir(); err == nil {
return filepath.Join(home, "Library", "Application Support", name) return filepath.Join(home, "Library", "Application Support", name)
@@ -46,7 +46,7 @@ func isWritableDir(dir string) bool {
if err != nil || !fi.IsDir() { if err != nil || !fi.IsDir() {
return false 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) f, err := os.OpenFile(probe, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil { if err != nil {
return false return false
@@ -56,7 +56,7 @@ func isWritableDir(dir string) bool {
return true 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" } func defaultEditor() string { return "nano" }
// hideCmd : no-op sur Unix (pas de fenêtre de console à masquer). // 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 // --- Supervision de processus détachés (worker de lien, service en mode
// utilisateur). Pendant Unix de sys_platform_windows.go. // 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. // place dans une nouvelle session, donc elle n'est pas tuée avec notre groupe.
func spawnDetached(name string, args ...string) *exec.Cmd { func spawnDetached(name string, args ...string) *exec.Cmd {
cmd := exec.Command(name, args...) cmd := exec.Command(name, args...)
@@ -1,6 +1,6 @@
//go:build windows //go:build windows
package ajean package loki
import ( import (
"context" "context"
@@ -15,7 +15,7 @@ import (
) )
// init makes the Windows console behave like a modern terminal: UTF-8 so the // 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 // mojibake (the default OEM codepage, e.g. cp850, renders "✓" as "├ö"), and VT
// processing so the ANSI colour/cursor escapes (including the build progress // processing so the ANSI colour/cursor escapes (including the build progress
// line) are interpreted instead of printed literally. Best-effort: a redirected // 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, // homeRoot is the parent directory of the data root: %ProgramData% (machine-wide,
// the closest analogue to /etc), falling back to %LOCALAPPDATA% for unprivileged // the closest analogue to /etc), falling back to %LOCALAPPDATA% for unprivileged
// setups. Ancien et nouveau dossier partagent ce parent, ce qui garantit que la // 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 { func homeRoot() string {
if pd := os.Getenv("ProgramData"); pd != "" { if pd := os.Getenv("ProgramData"); pd != "" {
return pd return pd
@@ -54,10 +54,10 @@ func homeRoot() string {
return os.TempDir() return os.TempDir()
} }
// defaultAjeanHome est la racine des données quand $AJEAN_HOME est absent. // defaultLokiHome est la racine des données quand $LOKI_HOME est absent.
func defaultAjeanHome() string { return filepath.Join(homeRoot(), "ajean") } 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" } func defaultEditor() string { return "notepad" }
// openBrowser ouvre l'URL dans le navigateur par défaut. rundll32 évite les // 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 // 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 // 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. // avec les flags déjà présents pour ne pas écraser un éventuel détachement.
func hideCmd(cmd *exec.Cmd) *exec.Cmd { 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 // 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). // parent (this is the detached process the service supervisor tracks).
// args[0] is the binary path; the rest are its arguments. // args[0] is the binary path; the rest are its arguments.
func execServer(bin string, args []string) error { 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 // 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 // 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. // Windows box.
func ensureCompiler() error { func ensureCompiler() error {
if msvcInstallVersion() != "" { 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à ; 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 2. ou copie (en admin) les fichiers de
%s %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 // 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 // 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 { func newShellCmd(ctx context.Context, command string) *exec.Cmd {
return hideCmd(exec.CommandContext(ctx, "cmd", "/C", command)) return hideCmd(exec.CommandContext(ctx, "cmd", "/C", command))
} }
Loaded 100 of 157 files, more files were not shown because too many files have changed in this diff. Show more