diff --git a/.github/workflows/mac-build.yml b/.github/workflows/mac-build.yml index 16a3235..390047a 100644 --- a/.github/workflows/mac-build.yml +++ b/.github/workflows/mac-build.yml @@ -18,17 +18,17 @@ jobs: mkdir -p dist CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 \ go build -trimpath -ldflags="-s -w" -o dist/ajean-darwin-arm64 ./cmd/ajean - mkdir -p /tmp/jean.iconset + mkdir -p /tmp/ajean.iconset sips -s format png 'cmd/ajean/icon.ico' --out /tmp/icon.png >/dev/null 2>&1 || true for s in 16 32 128 256 512; do - sips -z $s $s /tmp/icon.png --out "/tmp/jean.iconset/icon_${s}x${s}.png" >/dev/null 2>&1 || true + sips -z $s $s /tmp/icon.png --out "/tmp/ajean.iconset/icon_${s}x${s}.png" >/dev/null 2>&1 || true done - iconutil -c icns /tmp/jean.iconset -o /tmp/jean.icns >/dev/null 2>&1 || true + iconutil -c icns /tmp/ajean.iconset -o /tmp/ajean.icns >/dev/null 2>&1 || true app="dist/AJEAN.app" mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" cp dist/ajean-darwin-arm64 "$app/Contents/MacOS/ajean" chmod +x "$app/Contents/MacOS/ajean" - cp /tmp/jean.icns "$app/Contents/Resources/jean.icns" 2>/dev/null || true + cp /tmp/ajean.icns "$app/Contents/Resources/ajean.icns" 2>/dev/null || true printf '%s\n' \ '' \ '' \ @@ -48,5 +48,5 @@ jobs: ls -l dist - uses: actions/upload-artifact@v4 with: - name: Jean-macos-arm64 - path: dist/Jean-macos-arm64.zip + name: AJEAN-macos-arm64 + path: dist/AJEAN-macos-arm64.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c98b884..5635688 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,9 @@ # Release : sur tag vX.Y.Z, compile les 6 cibles et publie la release GitHub # avec les binaires (mêmes noms d'assets que les releases manuelles historiques) -# ET le fichier SHA256SUMS que `jean update` (v0.4.6+) vérifie avant d'installer. +# ET le fichier SHA256SUMS que `ajean update` (v0.4.6+) vérifie avant d'installer. # ⚠️ 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 -# plus aux binaires font échouer `jean update` (vécu sur v0.4.7). +# plus aux binaires font échouer `ajean 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 # menus passe par Cocoa → CGO, pas cross-compilable depuis Linux) ; Linux et @@ -34,29 +34,29 @@ jobs: CGO_CFLAGS="-arch x86_64" CGO_LDFLAGS="-arch x86_64" \ go build -trimpath -ldflags="-s -w" -o dist/ajean-darwin-amd64 ./cmd/ajean file dist/ajean-darwin-* - - name: bundles Jean.app + - name: bundles AJEAN.app run: | # Un binaire nu double-cliqué ouvrirait le Terminal. On publie donc - # aussi un vrai bundle Jean.app (zippé pour garder le bit exécutable) : - # au clic, le Finder lance Contents/MacOS/jean, qui détecte le bundle + # aussi un vrai bundle AJEAN.app (zippé pour garder le bit exécutable) : + # au clic, le Finder lance Contents/MacOS/ajean, qui détecte le bundle # et bascule en mode « application » (UI web + navigateur + icône dans - # la barre de menus), comme le double-clic sur jean.exe sous Windows. + # la barre de menus), comme le double-clic sur ajean.exe sous Windows. # 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 # (cmd/ajean/icon.png, voir tools/gen-icon) : plus de conversion depuis # le .ico, dont l'échec silencieux donnait une app sans icône. - mkdir -p /tmp/jean.iconset + mkdir -p /tmp/ajean.iconset for s in 16 32 128 256 512; do - sips -z $s $s 'cmd/ajean/icon.png' --out "/tmp/jean.iconset/icon_${s}x${s}.png" >/dev/null + sips -z $s $s 'cmd/ajean/icon.png' --out "/tmp/ajean.iconset/icon_${s}x${s}.png" >/dev/null done - iconutil -c icns /tmp/jean.iconset -o /tmp/jean.icns + iconutil -c icns /tmp/ajean.iconset -o /tmp/ajean.icns v="${GITHUB_REF_NAME#v}" for arch in amd64 arm64; do app="dist/mac-$arch/AJEAN.app" mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" cp "dist/ajean-darwin-$arch" "$app/Contents/MacOS/ajean" chmod +x "$app/Contents/MacOS/ajean" - cp /tmp/jean.icns "$app/Contents/Resources/ajean.icns" 2>/dev/null || true + cp /tmp/ajean.icns "$app/Contents/Resources/ajean.icns" 2>/dev/null || true printf '%s\n' \ '' \ '' \ @@ -80,7 +80,7 @@ jobs: # Nom hérité conservé : les liens de téléchargement déjà publiés # (site, mails, messages) pointent dessus et doivent continuer de # fonctionner. Même contenu, deux noms. - cp "dist/AJEAN-macos-$arch.zip" "dist/Jean-macos-$arch.zip" + cp "dist/AJEAN-macos-$arch.zip" "dist/AJEAN-macos-$arch.zip" rm -rf "dist/mac-$arch" done ls -l dist @@ -112,22 +112,10 @@ jobs: with: name: macos path: dist - - name: assets hérités jean-* (transition) - run: | - # Le parc déjà installé ne sait chercher QUE « jean-- » : sans - # ces copies, aucun des utilisateurs actuels ne pourrait se mettre à - # jour — pas même vers la version qui apprend à lire « ajean-* ». - # Après cette étape SEULEMENT, donc une fois les binaires macOS - # récupérés, les deux jeux de noms sont complets. - # À supprimer quand le parc aura basculé (quelques versions). - for f in dist/ajean-*; do - cp "$f" "dist/jean-${f#dist/ajean-}" - done - ls -l dist - name: SHA256SUMS run: | - chmod +x dist/ajean-darwin-* dist/jean-darwin-* - (cd dist && sha256sum ajean-* jean-* *.zip > SHA256SUMS) + chmod +x dist/ajean-darwin-* + (cd dist && sha256sum ajean-* *.zip > SHA256SUMS) ls -l dist && cat dist/SHA256SUMS - name: publier la release uses: softprops/action-gh-release@v2 diff --git a/README.md b/README.md index 7483e50..cd6679b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Aucune dépendance à l'exécution, aucun flag CMake à retenir, aucun conteneur ### 1. Le binaire ```bash -curl -L -o jean https://github.com/nathaninline/ajean/releases/latest/download/jean-linux-amd64 +curl -L -o ajean https://github.com/nathaninline/ajean/releases/latest/download/ajean-linux-amd64 chmod +x ajean sudo mv ajean /usr/local/bin/ajean ``` @@ -78,7 +78,7 @@ Service : start | stop | restart gérer le service status | logs état / logs en direct enable | disable démarrage au boot - edit éditer $AJEAN_HOME/config.env + edit éditer la configuration dans $EDITOR test | bench [N] vérifier que le modèle répond / mesurer les tok/s vram | gpu [index…] VRAM / choix des GPU (gpu all = tous) set-api-key [clé] protéger le moteur d'inférence (Bearer) @@ -90,7 +90,7 @@ Capacités de l'IA : internet [on|off|engine |url |key ] accès web Presets et moteur : - switch [N] changer de preset (configs/) + switch [N] changer de preset (presets/) llamacpp install|update|status Accès distant (ajean.link) : @@ -107,7 +107,11 @@ Installation : ## Configuration -Tout vit sous **`$AJEAN_HOME`** (`/etc/ajean` sous Linux/macOS, `%ProgramData%\ajean` sous Windows). Le service lit `config.env` : +Tout vit sous **`$AJEAN_HOME`** (`/etc/ajean` sous Linux/macOS, `%ProgramData%\ajean` sous Windows), qui contient exactement six dossiers — `backends/`, `bin/`, `presets/`, `memory/`, `models/`, `workspace/` — et la base `ajean.db`. + +Cette base, un unique fichier [bbolt](https://github.com/etcd-io/bbolt), remplace la dizaine de fichiers d'état d'autrefois : configuration, préférences de l'interface, conversation, clés et interrupteurs y vivent désormais ensemble. Restent des fichiers ce qui se lit et s'édite à la main : les presets (`presets/*.env`), les pages de mémoire (`memory/*.md`) et, bien sûr, les modèles. + +La configuration du moteur s'édite avec `ajean edit`, qui la déroule au format `clé=valeur` dans `$EDITOR` : | Clé | Signification | Défaut | |-----|---------------|--------| @@ -127,9 +131,9 @@ Tout vit sous **`$AJEAN_HOME`** (`/etc/ajean` sous Linux/macOS, `%ProgramData%\a | `CRAWL4AI_URL` / `CRAWL4AI_KEY` | serveur d'accès internet | — | | `EXTRA_ARGS` | ajouté tel quel à la ligne de commande du moteur | — | -La clé API (`ajean set-api-key`) vit dans `$AJEAN_HOME/.api_key`, à part, afin de survivre aux changements de preset. +La clé API (`ajean set-api-key`) est rangée hors de la configuration, afin de survivre aux changements de preset. -**Modèles sur un autre disque.** Les `.gguf` n'ont pas à résider dans `$AJEAN_HOME` : dans l'éditeur de preset de l'interface, section *Modèle → Dossiers de modèles*, ajoutez le dossier voulu. Ses modèles apparaissent dans la liste, groupés par dossier. La liste est enregistrée dans `model_dirs.json`, donc conservée d'un preset à l'autre. +**Modèles sur un autre disque.** Les `.gguf` n'ont pas à résider dans `$AJEAN_HOME/models` : dans l'éditeur de preset de l'interface, section *Modèle → Dossiers de modèles*, ajoutez le dossier voulu. Ses modèles apparaissent dans la liste, groupés par dossier. La liste est enregistrée dans la base, donc conservée d'un preset à l'autre. ### Variables d'environnement @@ -137,7 +141,7 @@ La clé API (`ajean set-api-key`) vit dans `$AJEAN_HOME/.api_key`, à part, afin |----------|------|--------| | `AJEAN_HOME` | racine des données | `/etc/ajean`, `%ProgramData%\ajean` | | `AJEAN_MODEL_DIRS` | dossiers de modèles (séparés par `:`, `;` sous Windows) | — | -| `JEAN_SERVICE` | nom de l'unité systemd | `ajean` | +| `AJEAN_SERVICE` | nom de l'unité systemd | `ajean` | | `HF_TOKEN` | token Hugging Face pour les modèles privés | — | | `AJEAN_DL_CONNS` | connexions parallèles au téléchargement | — | | `EDITOR` | éditeur pour `ajean edit` | `nano` / `notepad` | @@ -146,7 +150,7 @@ La clé API (`ajean set-api-key`) vit dans `$AJEAN_HOME/.api_key`, à part, afin ### Mémoire -L'IA tient des pages Markdown sous `$AJEAN_HOME/MEMORY/`, relues et mises à jour entre les sessions. Trois modes, indépendants du mode agent : +L'IA tient des pages Markdown sous `$AJEAN_HOME/memory/`, relues et mises à jour entre les sessions. Trois modes, indépendants du mode agent : ```bash ajean memory always # (défaut) elle cherche avant de répondre et enregistre d'elle-même @@ -182,7 +186,7 @@ Les outils web ne sont proposés au modèle que si le mode agent est actif, l'ac AJEAN parle le [Model Context Protocol](https://modelcontextprotocol.io) : on y branche des serveurs tiers (fichiers, bases de données, API…) et leurs outils s'ajoutent à ceux de l'IA, nommés `mcp____`. -La configuration se fait depuis l'interface web (section *Serveurs MCP*) ou en éditant `$AJEAN_HOME/mcp.json`, au **même format que Claude Desktop**, si bien qu'un fichier existant se réutilise tel quel : +La configuration se fait depuis l'interface web (section *Serveurs MCP*) (section *Serveurs MCP*). Le format des serveurs est celui de Claude Desktop, si bien qu'une configuration existante se recopie telle quelle : ```json { @@ -199,7 +203,7 @@ Les transports **stdio** et **HTTP** sont pris en charge. Comme le terminal, un - **Pas de systemd** : `ajean start` lance le service en arrière-plan (suivi par fichier PID) ; `stop`, `restart`, `status` et `logs` agissent dessus, sans droits administrateur. `enable` / `disable` ne sont pas gérés, il faut passer par une tâche planifiée. - `AJEAN_HOME` vaut `%ProgramData%\ajean` (repli `%LOCALAPPDATA%\ajean`). -- `ajean install` crée seulement le dossier de données et un `config.env` de départ. +- `ajean install` crée seulement l'arborescence de données et une configuration de départ. - Le terminal de l'IA passe par `cmd.exe`. Elle le sait, et écrit ses fichiers par l'outil dédié plutôt que par le shell, ce qui lui permet de produire des scripts contenant des guillemets. ```powershell diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ff93edc..14dc299 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,33 +1,23 @@ -Correctifs d'interface dans la foulée de la 0.7.11. Tout ce qui bougeait, sautait ou clignotait après un chargement se tient enfin tranquille. +Le renommage est terminé. Plus rien ne s'appelle jean : ni le binaire, ni les services, ni les variables, ni les dossiers. Et le dossier de données, qui s'était couvert d'une douzaine de petits fichiers d'état, tient désormais dans six dossiers et une base. -## L'éditeur de preset ne s'ouvre plus au milieu +**Cette version ne reprend pas les installations précédentes.** Il n'y a aucune migration : ni des dossiers, ni des unités système, ni des réglages. Réinstallez, puis retéléchargez vos modèles ou repointez-les avec *Dossiers de modèles*. -Ouvrir un preset après en avoir fait défiler un autre rouvrait la fiche à la position précédente. La remise à zéro du défilement existait, mais elle était faite avant l'affichage : écrire une position de défilement sur un élément non affiché ne fait rien du tout, le navigateur restaurait donc l'ancienne. Elle se fait maintenant après l'affichage, puis une seconde fois une fois le contenu en place. +## Une base à la place des fichiers d'état -## Le sélecteur de moteur ne glisse plus tout seul +`config.env`, `webprefs.json`, `conversation.json`, `model_dirs.json`, `mcp.json`, `.api_key`, `.web_key`, `.link_token`, `.agent_enabled`, `.internet_enabled` et le reste ont disparu. Tout cela vit maintenant dans **`ajean.db`**, un fichier unique en [bbolt](https://github.com/etcd-io/bbolt) — pur Go, transactionnel, sans dépendance système. -En passant d'un preset à l'autre, on voyait le curseur du sélecteur (précompilé, compilé, personnalisé) glisser de l'ancienne valeur vers la nouvelle une fois le chargement terminé. Le changement d'état avait pourtant lieu pendant que le formulaire était caché : WebKit diffère les transitions des éléments non rendus et les rejoue à leur retour à l'écran. Les transitions du formulaire sont donc coupées tant qu'il est masqué, et pendant l'image où il réapparaît. +Ce n'est pas qu'un rangement. Chaque fichier avait sa façon d'être écrit, et donc sa façon de rater une écriture concurrente : l'interface et un tour de chat qui touchaient au même réglage au même instant pouvaient en perdre un. Une transaction remplace tout ça. Une bascule de preset, en particulier, remplace la configuration d'un bloc : il n'existe plus d'instant où elle serait à moitié l'ancienne et à moitié la nouvelle. -Dans le même esprit, le voile de chargement de la modale est devenu opaque, sous le titre resté lisible. Rien du preset précédent ne peut plus apparaître une fraction de seconde. +Restent des fichiers ceux qui sont faits pour être lus, édités et sauvegardés à la main : les presets, les pages de mémoire, les modèles. `ajean edit` déroule donc la configuration au format `clé=valeur` dans votre éditeur, puis la relit — le fichier n'existe que le temps de l'édition. -## Le menu ne saute plus au chargement +## Six dossiers, et rien d'autre -Plusieurs blocs (config active, jauges machine, liste des presets, état de l'accès internet, serveurs MCP) sont vides tant que le serveur n'a pas répondu, puis prennent leur taille réelle : tout le menu se décalait d'un cran, ce qui se voyait surtout sur les sections du bas. Leur hauteur est désormais mémorisée d'une session à l'autre et réservée dès le démarrage, puis rendue quand le vrai contenu arrive. La réserve tient la place, elle n'affiche jamais d'information fausse. +`$AJEAN_HOME` contient exactement `backends/`, `bin/`, `presets/`, `memory/`, `models/`, `workspace/`, plus la base. `configs/` devient `presets/`, `MEMORY/` devient `memory/`, et les `.gguf` ont enfin leur `models/` au lieu d'être posés à la racine. -Les champs Crawl4AI, eux, étaient visibles au départ puis repliés dès que l'état arrivait, le moteur par défaut étant l'intégré. Ils partent maintenant masqués. La section Moteur ne grandit plus non plus : son texte d'en-tête part à sa taille définitive et la ligne d'état des trois moteurs a sa hauteur réservée. +## Ce qui a été retiré -Au passage, un chargement en échec (accès distant coupé par exemple) n'interrompt plus les autres. +Tout le code écrit pour ménager les installations « jean » : la migration du dossier de données et ses reprises après échec, la migration de l'agencement système (unités, `/etc/default`, réécriture des chemins), l'élévation Windows qu'elle demandait, la résolution du nom d'unité réellement installée, la reprise des fichiers PID et des skills, les alias `jean` posés à l'installation, les variables `JEAN_*` lues en second. -## Détails +La CLI perd ses alias hérités — `skills`, `machine`, `tools`, `web-access`, `mem`, `upgrade`, `self-update`, `paths`, `llama` — et son aide est réorganisée. Chaque commande a désormais un seul nom. -Les pastilles « actif » et « inactif » disparaissent de l'accès internet et des serveurs MCP : l'interrupteur et la liste le disent déjà. Reste la seule information utile, l'anomalie « injoignable », qui signale que les outils web ne sont pas fournis au modèle. - -Dans la mémoire, la ligne « Voir la mémoire » perd son chevron et son libellé est enfin centré. Créer une page annonçait « Nouveau Page » et demandait un « Nom du preset » : chaque type a maintenant ses propres libellés. - -## Mise à jour - -``` -ajean update -``` - -Chaque correction a été vérifiée en mesurant le rendu réel, position et hauteur des éléments avant et après chargement, pas seulement à l'oeil. +Les releases ne publient plus qu'un jeu de binaires, `ajean--`. La double publication qui accompagnait la transition n'a plus d'objet. diff --git a/internal/ajean/backend_serve.go b/internal/ajean/backend_serve.go index 5a2dc95..ad40de5 100644 --- a/internal/ajean/backend_serve.go +++ b/internal/ajean/backend_serve.go @@ -100,9 +100,7 @@ func cmdServe(args []string) error { llmArgs = append(llmArgs, "--api-key", k) } // EXTRA_ARGS is appended verbatim, split on whitespace like the shell would. - for _, a := range trimSplit(cfg["EXTRA_ARGS"], " ") { - llmArgs = append(llmArgs, a) - } + llmArgs = append(llmArgs, trimSplit(cfg["EXTRA_ARGS"], " ")...) // Working dir = AJEAN_HOME so relative paths in EXTRA_ARGS (e.g. --mmproj // mmproj-F16.gguf) still resolve. diff --git a/internal/ajean/chat_internet.go b/internal/ajean/chat_internet.go index dd82c0e..061d155 100644 --- a/internal/ajean/chat_internet.go +++ b/internal/ajean/chat_internet.go @@ -471,7 +471,6 @@ type searchResult struct { } var ( - htmlTagRe = regexp.MustCompile(`<[^>]+>`) wsRe = regexp.MustCompile(`\s+`) numEntityRe = regexp.MustCompile(`&#(\d+);`) ddgHeadRe = regexp.MustCompile(`^##\s+\[([^\]]+)\]\(([^)]+)\)\s*$`) diff --git a/internal/ajean/chat_memory.go b/internal/ajean/chat_memory.go index dead68e..57f8b21 100644 --- a/internal/ajean/chat_memory.go +++ b/internal/ajean/chat_memory.go @@ -368,23 +368,3 @@ func cmdMemory(args []string) error { fmt.Printf("%s mémoire : %s — %s\n", green("[ok]"), bold(string(m)), label[m]) return nil } - -// memorySystemPrompt liste les pages mémoire à injecter quand le mode agent est -// actif, pour que l'IA sache ce qu'elle a déjà retenu. -func memorySystemPrompt(caps Caps) string { - if !caps.Agent { - return "" - } - list := MemList() - var b strings.Builder - b.WriteString("Memory: persistent Markdown pages under MEMORY/. Use mem_search FIRST whenever the user asks about something you might already know (preferences, ongoing projects, past decisions). Open the most relevant page with mem_read. Save anything worth keeping across sessions with mem_add (one topic per page, descriptive kebab-case name); update a page with mem_edit (old must match exactly once). The first content line is a short title (#).\n") - if len(list) == 0 { - b.WriteString("Pages: none yet.") - return b.String() - } - b.WriteString("Pages:\n") - for _, p := range list { - fmt.Fprintf(&b, "- %s: %s\n", p.Name, p.Title) - } - return strings.TrimRight(b.String(), "\n") -} diff --git a/internal/ajean/mcp_config.go b/internal/ajean/mcp_config.go index 4c2c97f..678de75 100644 --- a/internal/ajean/mcp_config.go +++ b/internal/ajean/mcp_config.go @@ -112,11 +112,6 @@ func (c MCPServerConfig) Validate() error { return nil } -// mcpConfigFile est la représentation sur disque de mcp.json. -type mcpConfigFile struct { - MCPServers map[string]MCPServerConfig `json:"mcpServers"` -} - // mcpConfigMu sérialise les accès concurrents à la déclaration des serveurs // MCP (l'UI web et les tours de chat peuvent lire/écrire en parallèle). var mcpConfigMu sync.Mutex diff --git a/internal/ajean/relay_e2e.go b/internal/ajean/relay_e2e.go index 735ba25..3c8f0c7 100644 --- a/internal/ajean/relay_e2e.go +++ b/internal/ajean/relay_e2e.go @@ -257,34 +257,3 @@ func writeE2EResp(w http.ResponseWriter, gcm cipher.AEAD, status int, body []byt w.Header().Set("Content-Type", "text/plain") _, _ = w.Write([]byte(base64.StdEncoding.EncodeToString(sealed))) } - -// e2eSeal (côté agent : utilisé uniquement pour les tests) scelle un secret vers -// une clé publique X25519. La version navigateur est en WASM (wasmclient). -func e2eSeal(agentPub, secret []byte) ([]byte, error) { - pub, err := ecdh.X25519().NewPublicKey(agentPub) - if err != nil { - return nil, err - } - eph, err := ecdh.X25519().GenerateKey(rand.Reader) - if err != nil { - return nil, err - } - shared, err := eph.ECDH(pub) - if err != nil { - return nil, err - } - key := sealKey(shared, eph.PublicKey().Bytes(), agentPub) - gcm, err := newGCM(key) - if err != nil { - return nil, err - } - nonce := make([]byte, 12) - if _, err := rand.Read(nonce); err != nil { - return nil, err - } - ct := gcm.Seal(nil, nonce, secret, nil) - out := append([]byte{}, eph.PublicKey().Bytes()...) - out = append(out, nonce...) - out = append(out, ct...) - return out, nil -} diff --git a/internal/ajean/run.go b/internal/ajean/run.go index da539f8..2d2eb1d 100644 --- a/internal/ajean/run.go +++ b/internal/ajean/run.go @@ -10,7 +10,7 @@ import ( "strings" ) -const Version = "0.7.13" +const Version = "0.8.0" // Main est le vrai main() du binaire (cmd/ajean ne fait que l'appeler). func Main() { diff --git a/internal/ajean/sys_brand_icon.go b/internal/ajean/sys_brand_icon.go index be375f5..1a63afc 100644 --- a/internal/ajean/sys_brand_icon.go +++ b/internal/ajean/sys_brand_icon.go @@ -32,7 +32,7 @@ var glyphRects = [][4]float64{{6, 3, 2, 2}, {6, 5, 2, 2}, {4, 7, 2, 2}} // brandIconImage dessine l'icône à la taille n. bg peint le carré arrondi, fg le // « j ». Les deux peuvent être transparents : c'est ce qui produit l'icône -// « template » de macOS (voir brandTemplatePNG). +// « template » de macOS. func brandIconImage(n int, bg, fg color.RGBA) *image.RGBA { const r = 2.0 // rayon des coins, en unités de la grille 12 outside := func(gx, gy float64) bool { @@ -84,14 +84,6 @@ func encodePNG(img *image.RGBA) []byte { // Exporté pour le générateur d'icône du .exe (tools/gen-icon). func BrandIconPNG(n int) []byte { return encodePNG(brandIconImage(n, brandBlack, brandWhite)) } -// brandTemplatePNG rend la variante « template » attendue par macOS : seule la -// couche alpha compte, le système colore la forme selon le thème de la barre de -// menus. Le « j » est donc DÉCOUPÉ (transparent) dans un carré opaque — sans ça, -// une icône entièrement noire disparaît sur une barre de menus sombre. -func brandTemplatePNG(n int) []byte { - return encodePNG(brandIconImage(n, brandBlack, brandClear)) -} - // BrandICO emballe une ou plusieurs tailles PNG dans un conteneur .ico. Windows // accepte le PNG comme image d'une entrée .ico (alpha conservé pour les coins // arrondis). Plusieurs tailles = un rendu net partout, de la barre des tâches diff --git a/internal/ajean/sys_platform_unix.go b/internal/ajean/sys_platform_unix.go index 42416e6..103f7d1 100644 --- a/internal/ajean/sys_platform_unix.go +++ b/internal/ajean/sys_platform_unix.go @@ -16,9 +16,8 @@ import ( "time" ) -// defaultAjeanHome is the data root when neither $AJEAN_HOME/$AJEAN_HOME nor -// /etc/default/* override it. legacyDefaultHome est son équivalent d'avant le -// renommage, migré au premier lancement (voir sys_migrate.go). +// defaultAjeanHome est la racine des données quand ni $AJEAN_HOME ni +// /etc/default/ajean n'en imposent une. // // Linux : /etc/ajean (machine de prod, service systemd lancé par root). // macOS : /etc/ajean n'est PAS écrivable par une app lancée depuis le Finder @@ -27,8 +26,6 @@ import ( // un `sudo ajean install` a déjà créé /etc/ajean à notre nom. func defaultAjeanHome() string { return unixHome("ajean") } -func legacyDefaultHome() string { return unixHome("ajean") } - func unixHome(name string) string { if runtime.GOOS != "darwin" || os.Geteuid() == 0 { return "/etc/" + name diff --git a/internal/ajean/sys_platform_windows.go b/internal/ajean/sys_platform_windows.go index 32bfe7c..2064385 100644 --- a/internal/ajean/sys_platform_windows.go +++ b/internal/ajean/sys_platform_windows.go @@ -54,12 +54,9 @@ func homeRoot() string { return os.TempDir() } -// defaultAjeanHome is the data root when no AJEAN_HOME/AJEAN_HOME is set. +// defaultAjeanHome est la racine des données quand $AJEAN_HOME est absent. func defaultAjeanHome() string { return filepath.Join(homeRoot(), "ajean") } -// legacyDefaultHome is the pre-rename data root, migrated on first run. -func legacyDefaultHome() string { return filepath.Join(homeRoot(), "ajean") } - // defaultEditor is used by `ajean edit` when $EDITOR is unset. func defaultEditor() string { return "notepad" } diff --git a/internal/ajean/ui/index.html b/internal/ajean/ui/index.html index c03f443..d3edac5 100644 --- a/internal/ajean/ui/index.html +++ b/internal/ajean/ui/index.html @@ -22,7 +22,7 @@ garde `display-mode` en second pour les PWA Android/desktop à manifeste. --> - + @@ -180,7 +180,7 @@ trois îlots pour un même réglage. -->
-
Adresse locale (réseau) — modèle jean
+
Adresse locale (réseau) — modèle ajean
@@ -210,7 +210,7 @@
Accès distant -
ajean.link?Accédez à ce serveur Jean depuis n'importe où, sans ouvrir de port, à travers le relais ajean.link. Le trafic est chiffré de bout en bout : le relais ne voit rien. Abonnement 4,80 €/mois.
+
ajean.link?Accédez à ce serveur AJEAN depuis n'importe où, sans ouvrir de port, à travers le relais ajean.link. Le trafic est chiffré de bout en bout : le relais ne voit rien. Abonnement 4,80 €/mois.

Ouvrez l'accès distant à ce serveur en quelques clics, sans terminal.

diff --git a/internal/ajean/ui/src/js/00-md.js b/internal/ajean/ui/src/js/00-md.js index 5576741..a8c52a2 100644 --- a/internal/ajean/ui/src/js/00-md.js +++ b/internal/ajean/ui/src/js/00-md.js @@ -56,13 +56,13 @@ function toggleSide(){ document.getElementById('side').classList.toggle('open'); let _sysT = null; function saveSys(){ const v = document.getElementById('sysprompt').value; - localStorage.setItem('jean.sys', v); + localStorage.setItem('ajean.sys', v); clearTimeout(_sysT); _sysT = setTimeout(()=>{ jpost('/api/sysprompt', {text:v}).catch(()=>{}); }, 600); } async function loadSys(){ try{ const d = await jget('/api/sysprompt'); - if(d && d.ok){ document.getElementById('sysprompt').value = d.text || ''; localStorage.setItem('jean.sys', d.text || ''); } + if(d && d.ok){ document.getElementById('sysprompt').value = d.text || ''; localStorage.setItem('ajean.sys', d.text || ''); } }catch(e){} } diff --git a/internal/ajean/ui/src/js/01-theme.js b/internal/ajean/ui/src/js/01-theme.js index ef59a87..90bf3b1 100644 --- a/internal/ajean/ui/src/js/01-theme.js +++ b/internal/ajean/ui/src/js/01-theme.js @@ -9,7 +9,7 @@ function applyTheme(id){ // désormais la variante, déduite du suffixe. const dark = id==='dark' || (typeof id==='string' && id.endsWith('-dark')); document.documentElement.setAttribute('data-theme', dark?'dark':'light'); - try{ localStorage.setItem('jean-theme', dark?'dark':'light'); }catch(e){} + try{ localStorage.setItem('ajean-theme', dark?'dark':'light'); }catch(e){} const sw=document.getElementById('theme-dark'); if(sw) sw.checked=dark; } function toggleThemeDark(){ @@ -17,7 +17,7 @@ function toggleThemeDark(){ savePrefs(); } function initTheme(){ - let id='light'; try{ id=localStorage.getItem('jean-theme')||'light'; }catch(e){} + let id='light'; try{ id=localStorage.getItem('ajean-theme')||'light'; }catch(e){} applyTheme(id); } // ===== Affichage ============================================================ @@ -34,7 +34,7 @@ const VIEW_OPTS=[ function viewOn(id){ return document.documentElement.getAttribute('data-'+id)==='1'; } function applyView(id, on){ document.documentElement.setAttribute('data-'+id, on?'1':'0'); - try{ localStorage.setItem('jean-'+id, on?'1':'0'); }catch(e){} + try{ localStorage.setItem('ajean-'+id, on?'1':'0'); }catch(e){} const box=document.getElementById('view-'+id); if(box) box.checked=!!on; // La barre latérale peut rester ouverte quand on repasse en mode fixe. if(id==='hide-side' && !on){ @@ -46,7 +46,7 @@ function applyView(id, on){ function toggleView(id){ applyView(id, document.getElementById('view-'+id).checked); savePrefs(); } function initView(){ VIEW_OPTS.forEach(o=>{ - let v=null; try{ v=localStorage.getItem('jean-'+o.id); }catch(e){} + let v=null; try{ v=localStorage.getItem('ajean-'+o.id); }catch(e){} applyView(o.id, v==='1'); }); } diff --git a/internal/ajean/ui/src/js/02-prefs.js b/internal/ajean/ui/src/js/02-prefs.js index 072ee7a..cab0b73 100644 --- a/internal/ajean/ui/src/js/02-prefs.js +++ b/internal/ajean/ui/src/js/02-prefs.js @@ -1,11 +1,11 @@ // ===== Persistance côté serveur (partagée entre appareils) ================== -// L'apparence est aussi enregistrée sur la machine jean (/api/prefs) : ainsi le +// L'apparence est aussi enregistrée sur la machine ajean (/api/prefs) : ainsi le // thème/affichage choisi sur un appareil se retrouve sur tous les autres. Le // localStorage reste utilisé pour appliquer instantanément au chargement (sans // flash), puis loadPrefs() aligne sur la valeur du serveur (source de vérité). function savePrefs(){ let theme='light'; - try{ theme=localStorage.getItem('jean-theme')||'light'; }catch(e){} + try{ theme=localStorage.getItem('ajean-theme')||'light'; }catch(e){} const p={theme}; VIEW_OPTS.forEach(o=>{ p[o.id.replace('-','_')] = viewOn(o.id)?'1':'0'; }); jpost('/api/prefs', p).catch(()=>{}); @@ -27,4 +27,4 @@ async function loadPrefs(){ } }catch(e){} } -document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initView(); document.getElementById('sysprompt').value = localStorage.getItem('jean.sys') || ''; loadSys(); restoreChat(); }); +document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initView(); document.getElementById('sysprompt').value = localStorage.getItem('ajean.sys') || ''; loadSys(); restoreChat(); }); diff --git a/internal/ajean/ui/src/js/03-ui-kit.js b/internal/ajean/ui/src/js/03-ui-kit.js index 12acac1..6c3560a 100644 --- a/internal/ajean/ui/src/js/03-ui-kit.js +++ b/internal/ajean/ui/src/js/03-ui-kit.js @@ -49,7 +49,7 @@ function _askKey(e){ else if(e.key==='Enter'){ e.preventDefault(); e.stopPropagation(); askResolve(true); } } function _openAsk(kind, message, opts){ - // Compat 2 conventions : (message, opts) [UI jean] ET l'objet unique + // Compat 2 conventions : (message, opts) [UI ajean] ET l'objet unique // {title,msg,yes,no,placeholder} passé par e2e.js sur ajean.link (server.html, // où cette fonction remplace le window.askConfirm du bootstrap boîte noire). if(message && typeof message==='object'){ const o=message; opts={title:o.title, okText:o.yes, cancelText:o.no, placeholder:o.placeholder, default:o.default, danger:o.danger}; message=o.msg||''; } diff --git a/internal/ajean/ui/src/js/04-api.js b/internal/ajean/ui/src/js/04-api.js index ede9e28..6b3dcbb 100644 --- a/internal/ajean/ui/src/js/04-api.js +++ b/internal/ajean/ui/src/js/04-api.js @@ -1,13 +1,13 @@ // Clé de pilotage : envoyée en Authorization: Bearer sur chaque appel /api/*. // Sur un 401 on la (re)demande et on rejoue la requête. Stockée en localStorage. -let TOKEN = localStorage.getItem('jean.key') || ''; +let TOKEN = localStorage.getItem('ajean.key') || ''; function authHeaders(h){ h = Object.assign({}, h||{}); if(TOKEN) h['Authorization']='Bearer '+TOKEN; return h; } // Base d'URL : "" en local (page à "/"), "/u/" servie via le relais ajean.link. // Garde les chemins absolus (/api/…) corrects derrière le tunnel. const API_BASE = location.pathname.replace(/\/(index\.html)?$/, ''); // Délai maximal d'un appel /api/* ordinaire. Sans lui, une requête que le // navigateur met en file d'attente (plafond de ~6 connexions par domaine, atteint -// dès qu'on laisse traîner plusieurs onglets Jean) reste suspendue POUR TOUJOURS : +// dès qu'on laisse traîner plusieurs onglets AJEAN) reste suspendue POUR TOUJOURS : // ni réponse, ni erreur, et une UI figée sur « chargement… » sans rien à afficher. // Mieux vaut une erreur franche. Les flux longs (SSE) passent leur propre signal // et ne sont donc jamais concernés. @@ -30,8 +30,8 @@ async function jfetch(u, opts){ } finally{ if(timer) clearTimeout(timer); } if(r.status === 401){ - const k = await askPrompt('Clé de pilotage jean requise :', {title:'Authentification', placeholder:'clé…'}); - if(k){ TOKEN = k.trim(); localStorage.setItem('jean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); } + const k = await askPrompt('Clé de pilotage ajean requise :', {title:'Authentification', placeholder:'clé…'}); + if(k){ TOKEN = k.trim(); localStorage.setItem('ajean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); } } return r; } diff --git a/internal/ajean/ui/src/js/06-settings.js b/internal/ajean/ui/src/js/06-settings.js index 0b22d1d..09bd086 100644 --- a/internal/ajean/ui/src/js/06-settings.js +++ b/internal/ajean/ui/src/js/06-settings.js @@ -9,7 +9,7 @@ const HRESERVE=['cfg','vram','ram-details','presets','internet-status','mcp-list function reserveHeights(){ HRESERVE.forEach(id=>{ try{ - const h=parseInt(localStorage.getItem('jean-h-'+id)||'0',10); + const h=parseInt(localStorage.getItem('ajean-h-'+id)||'0',10); const el=document.getElementById(id); if(el && h>0) el.style.minHeight=h+'px'; }catch(e){} @@ -19,7 +19,7 @@ function releaseHeights(){ HRESERVE.forEach(id=>{ const el=document.getElementById(id); if(!el) return; el.style.minHeight=''; - try{ localStorage.setItem('jean-h-'+id, String(el.offsetHeight||0)); }catch(e){} + try{ localStorage.setItem('ajean-h-'+id, String(el.offsetHeight||0)); }catch(e){} }); } document.addEventListener('DOMContentLoaded', reserveHeights); @@ -98,7 +98,7 @@ async function loadPresets(){ }); } // « Mode agent » = accès machine + skills réunis en un seul interrupteur. -// Quand il est actif, un « a » blanc apparaît en fondu devant « jean » → « ajean ». +// Quand il est actif, un « a » blanc apparaît en fondu devant « ajean » → « ajean ». async function loadAgent(){ const s=await jget('/api/agent'); const on = s.enabled; diff --git a/internal/ajean/ui/src/js/07-models.js b/internal/ajean/ui/src/js/07-models.js index 6b20b8b..cffd025 100644 --- a/internal/ajean/ui/src/js/07-models.js +++ b/internal/ajean/ui/src/js/07-models.js @@ -194,7 +194,7 @@ async function populateModelPicker(){ const list = await jget('/api/models'); const cur = currentModelInTextarea(); // Les modèles peuvent venir de plusieurs dossiers (disque externe…) : on les - // regroupe par dossier, JEAN_HOME d'abord (l'API les renvoie dans cet ordre). + // regroupe par dossier, AJEAN_HOME d'abord (l'API les renvoie dans cet ordre). const groups = []; for(const m of (list||[])){ let g = groups.find(x => x.dir === m.dir); @@ -213,7 +213,7 @@ async function populateModelPicker(){ opts += ''; } html += groups.length > 1 - ? ''+opts+'' + ? ''+opts+'' : opts; } // MODEL pointe vers un fichier qu'on ne trouve dans aucun dossier déclaré : @@ -224,7 +224,7 @@ async function populateModelPicker(){ sel.innerHTML = html; if(cur && !matched) setModelDirsOpen(true); } -// ---- Dossiers de modèles : JEAN_HOME + dossiers ajoutés (disque externe…) --- +// ---- Dossiers de modèles : AJEAN_HOME + dossiers ajoutés (disque externe…) --- // Réglage rare : replié derrière une ligne, comme l'éditeur de .env brut. function setModelDirsOpen(open){ const b = document.getElementById('m-dirs-body'); @@ -253,7 +253,7 @@ async function populateModelDirs(){ info.className = 'muted'; const n = x.count >= 0 ? (x.count + ' modèle' + (x.count>1?'s':'')) : 'illisible'; const free = x.free >= 0 ? ', ' + fmtSize(x.free) + ' libres' : ''; - info.textContent = '(' + n + free + (x.home ? ', dossier jean' : '') + ')'; + info.textContent = '(' + n + free + (x.home ? ', dossier ajean' : '') + ')'; row.append(p, info); if(!x.home){ const del = document.createElement('button'); @@ -324,13 +324,13 @@ function underDir(p, dir){ } let beFastPath = '', beOptPath = '', beFastDir = ''; async function populateBackend(){ - // Chemins des deux moteurs gérés + liste des backends détectés (dossier jean). + // Chemins des deux moteurs gérés + liste des backends détectés (dossier ajean). let lc = {}; try{ lc = await jget('/api/llamacpp'); }catch(_){} beFastPath = (lc.prebuilt && lc.prebuilt.bin) || ''; beFastDir = (lc.prebuilt && lc.prebuilt.dir) || ''; beOptPath = lc.bin || ''; // Menu « backend détecté » du mode personnalisé : tout ce qu'on trouve dans - // le dossier backends de jean (l'utilisateur peut y déposer son propre build). + // le dossier backends de ajean (l'utilisateur peut y déposer son propre build). const detected = await jget('/api/backends'); const sel = document.getElementById('m-backend-detected'); let html = ''; @@ -664,7 +664,7 @@ function populateSettings(){ // --- Décodage spéculatif (--spec-type / --spec-draft-n-max) ---------------- // Sélectionne le type courant. --spec-type accepte en réalité une LISTE séparée // par des virgules ; une valeur composée (ou un type sorti après cette version -// de jean) ne correspondrait à aucune option et serait silencieusement effacée +// de ajean) ne correspondrait à aucune option et serait silencieusement effacée // au premier changement. On l'ajoute donc telle quelle à la liste plutôt que de // la perdre. function setSpecType(v){ @@ -860,7 +860,7 @@ function watchDownload(fname){ e.bar.className = 'pe-bar done'; e.bar.firstElementChild.style.width = '100%'; stop(); await Promise.all([populateModelPicker(), populateDlDirs()]); - // Le modèle a pu atterrir hors du dossier jean : l'option porte alors le + // Le modèle a pu atterrir hors du dossier ajean : l'option porte alors le // chemin complet, pas le simple nom de fichier. const sel = document.getElementById('m-model'); const opt = Array.from(sel.options).find(o => samePath(baseName(o.value), fname)); diff --git a/internal/ajean/ui/src/js/08-chat-render.js b/internal/ajean/ui/src/js/08-chat-render.js index 992e869..1f5e890 100644 --- a/internal/ajean/ui/src/js/08-chat-render.js +++ b/internal/ajean/ui/src/js/08-chat-render.js @@ -115,7 +115,7 @@ function setLabelCounts(el, add, del){ if(del) cnt.appendChild(Object.assign(document.createElement('span'),{className:'d',textContent:'-'+del})); lab.appendChild(cnt); } -// Ligne de mesures sous une réponse (prefill / decode). Les étiquettes VOUS/JEAN +// Ligne de mesures sous une réponse (prefill / decode). Les étiquettes VOUS/AJEAN // sont masquées dans cette mise en page, donc les chiffres qu'on y écrivait // avaient disparu : ils ont leur propre ligne, discrète, sous le texte. Masquée // par la préférence d'affichage « masquer la vitesse de génération » (CSS). @@ -276,7 +276,7 @@ async function compactContext(){ } // Persistance de la conversation : on garde user+assistant en localStorage pour // survivre à un refresh (les bulles tool/reasoning sont éphémères, non stockées). -function saveChat(){ try{ localStorage.setItem('jean.chat', JSON.stringify(msgs)); }catch(e){} } +function saveChat(){ try{ localStorage.setItem('ajean.chat', JSON.stringify(msgs)); }catch(e){} } // Source de vérité = SERVEUR. Au chargement on ouvre le flux d'abonnement // permanent (connectStream), qui rejoue tout le fil depuis le serveur — texte, // appels d'outils, vitesses, raisonnement — puis suit le direct. Plus de diff --git a/internal/ajean/ui/src/js/09-stream.js b/internal/ajean/ui/src/js/09-stream.js index e7c422a..0e8777b 100644 --- a/internal/ajean/ui/src/js/09-stream.js +++ b/internal/ajean/ui/src/js/09-stream.js @@ -1,5 +1,5 @@ // ===== Conversation SERVEUR (source de vérité, partagée entre appareils) ===== -// L'historique et la génération vivent sur le serveur jean. Le client ouvre un +// L'historique et la génération vivent sur le serveur ajean. Le client ouvre un // flux d'ABONNEMENT permanent (SSE) qui rejoue le journal depuis lastSeq puis // suit le direct. Fermer l'onglet n'arrête plus la génération (détachée côté // serveur) ; se reconnecter rejoue tout le fil, détails compris. @@ -197,7 +197,7 @@ function handleDelta(d){ } // Flux d'abonnement permanent + reconnexion auto (from=lastSeq → pas de // re-téléchargement complet après une coupure / bascule d'appareil). -// Un onglet caché RELÂCHE son flux SSE. Sans ça, chaque onglet Jean laissé +// Un onglet caché RELÂCHE son flux SSE. Sans ça, chaque onglet AJEAN laissé // ouvert monopolise une des ~6 connexions simultanées autorisées par domaine : // au-delà, toute requête (journal, installation du moteur…) reste en file // d'attente sans jamais partir ni échouer — un blocage silencieux très diff --git a/internal/ajean/ui/src/js/13-remote.js b/internal/ajean/ui/src/js/13-remote.js index cc158fa..ac2f4fa 100644 --- a/internal/ajean/ui/src/js/13-remote.js +++ b/internal/ajean/ui/src/js/13-remote.js @@ -1,7 +1,7 @@ // Accès distant (ajean.link) — piloté depuis l'UI, sans terminal. // « Connecter » ouvre une popup app.ajean.link/connect.html qui gère compte + // abonnement, puis renvoie une clé de liaison par postMessage. On la POSTe à -// l'agent local (/api/link/connect) qui fait le `jean link` (token + service). +// l'agent local (/api/link/connect) qui fait le `ajean link` (token + service). const AJEAN_APP_ORIGIN = 'https://app.ajean.link'; diff --git a/internal/ajean/ui/src/styles.css b/internal/ajean/ui/src/styles.css index a4a8704..1f8f7a6 100644 --- a/internal/ajean/ui/src/styles.css +++ b/internal/ajean/ui/src/styles.css @@ -146,7 +146,7 @@ h1{margin:0;font-size:18px;color:var(--accent)} .ask-ok.danger{border-color:var(--err);color:var(--err)} #brand-a{display:inline-flex;line-height:0;color:var(--text);margin-right:2px} /* Mode agent : le A passe en couleur d'accent (il était masqué hors agent, - la marque se lisant alors « JEAN »). L'état agent reste signalé par sa + la marque se lisant alors « AJEAN »). L'état agent reste signalé par sa pastille dans les réglages. */ #brand.agent #brand-a{color:var(--accent)} /* interrupteur (switch) */ @@ -935,7 +935,7 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5} /* ===== Mise en page de la conversation ============================= Colonne de lecture centrée, réponses SANS bulle (texte nu sur le fond), question de l'utilisateur en pastille arrondie à droite, composer en - « pilule », pas d'étiquettes VOUS/JEAN. Ces règles surchargent la mise en + « pilule », pas d'étiquettes VOUS/AJEAN. Ces règles surchargent la mise en page générique plus haut : elles doivent donc rester APRÈS elle. */ body{font-size:15px} h1{color:var(--text);font-weight:600;letter-spacing:-.01em} @@ -949,7 +949,7 @@ h1{color:var(--text);font-weight:600;letter-spacing:-.01em} /* Question : pastille arrondie à droite, texte lisible sur fond clair OU sombre. */ #chat .msg.user{background:var(--bubble);color:var(--text);border-radius:20px;padding:11px 18px;max-width:75%;line-height:1.6} #chat .msg.user .label{color:var(--dim)} -/* Étiquettes VOUS / JEAN : inutiles ici (l'alignement suffit), sauf pour les +/* Étiquettes VOUS / AJEAN : inutiles ici (l'alignement suffit), sauf pour les bulles repliables (raisonnement / outils) où le label est le bouton de repli. */ #chat .msg .label{display:none} #chat .msg.collapsible .label{display:block}