mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Rebaptise AJEAN en Loki (fork, lignée conservée)
- module github.com/R0m1k3/Loki, cmd/loki, internal/loki (package loki) - LOKI_HOME, LOKI_MODEL_DIRS, LOKI_SERVICE, LOKI_DL_CONNS ; /etc/loki ; units loki-engine / loki-ui ; binaire et aide CLI - updateRepo pointe sur R0m1k3/Loki (l'auto-update ne tirera plus les binaires AJEAN amont) Conservé à l'identique : le domaine ajean.link (service de tunnel amont), les littéraux de migration 0.7.x (migrate_07.go), RELEASE_NOTES.md et LICENSE (historique et licence de l'amont). go build/vet/test : verts.
This commit is contained in:
157 files changed
+928
-928
No files matched your search
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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.
@@ -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,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
-1
@@ -1,4 +1,4 @@
|
|||||||
package ajean
|
package loki
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
+2
-2
@@ -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 {
|
||||||
+3
-3
@@ -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") != ""})
|
||||||
}
|
}
|
||||||
+3
-3
@@ -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)
|
||||||
+3
-3
@@ -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
-1
@@ -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
-1
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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`
|
||||||
+9
-9
@@ -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
Reference in new issue
Block a user