Geeek.org • Blog Dev & High Tech 100% Indépendant

J'ai pour habitude de présenter mon portfolio comme tout le monde : une page, une bio, quelques liens. Cette fois, j'ai eu envie de quelque chose de moins sage. Terminal.com[1] est un portail personnel qui prend la forme d'un terminal en plein écran : vous y arrivez, une fausse connexion SSH s'établit, le motd s'affiche, et la main vous est rendue. À partir de là, tout se visite à la commande.

Au départ, l'objectif était purement esthétique : retrouver l'allure d'un terminal phosphore, écran vert type CRT avec sa pluie de caractères façon Matrix, ou monochrome ambré, scanlines, grain et curseur qui clignote. Puis le projet a dérivé vers quelque chose de plus intéressant : reproduire le fonctionnement réel d'un shell, entièrement côté navigateur, et lui greffer une capacité d'intelligence artificielle locale. C'est cette seconde partie qui m'a occupé le plus longtemps, et dont je veux vous parler ici.

Tester directement, sans rien installer (un navigateur récent compatible WebGPU suffit) :

Un shell complet, sans serveur

Le site est statique. Il est construit avec Astro[2] et Tailwind CSS, sans framework JavaScript côté client pour le rendu : tout le poids fonctionnel tient dans un moteur de shell maison.

Le point que je trouve le plus satisfaisant, cela reste le faux système de fichiers. Ce n'est pas une simulation en mémoire : c'est une véritable arborescence sur disque, dans un dossier root/, que le build parcourt pour générer l'arbre de fichiers et le registre des commandes. Chaque commande est un simple fichier Markdown dans root/bin/. Pour comprendre comment, il faut s'arrêter une seconde sur une notion que peu de monde connaît en dehors des habitués des générateurs de sites statiques : le frontmatter.

Le frontmatter, c'est un petit bloc de métadonnées placé tout en haut d'un fichier texte, délimité par deux lignes de trois tirets, et rédigé le plus souvent en YAML sous forme de paires clé/valeur. Le corps du document vient ensuite. Cette convention, héritée d'outils comme Jekyll puis reprise par Hugo, Astro et beaucoup d'autres, permet au générateur de lire le titre, la date ou les tags d'un article sans que ces informations ne se mélangent au contenu lui-même. La plupart des gens qui écrivent en Markdown ignorent jusqu'à son existence, alors qu'ils en croisent presque à chaque fois qu'ils touchent à un site statique.

Dans Terminal.com, je détourne ce mécanisme pour décrire mes commandes. Le frontmatter porte le nom, la description, d'éventuels alias, une page de manuel, et un bloc js qui est exécuté si la commande est dynamique. Une commande sans js affiche simplement son corps Markdown. Voici par exemple la commande date au complet :

---
name: date
desc: date et heure
js: |
  ctx.line(new Date().toString());
---

Ajouter une commande revient donc à déposer un fichier Markdown, et le moteur la découvre toute seule.

Ce bloc js reçoit un objet ctx qui expose tout ce dont une commande a besoin : les arguments, des fonctions d'affichage, des aides de temporisation, la saisie interactive, la navigation dans l'arborescence, et les mutations persistantes du système de fichiers (les mkdir, touch et rm sont conservés par navigateur dans le localStorage).

Côté commandes intégrées, j'ai cherché à couvrir l'essentiel d'un shell : navigation et manipulation de fichiers (ls, cd, cat, tree, find, grep), identité système (whoami, uname, su, theme), réseau (nslookup, ping, checkip, weather), un peu de crypto pour le plaisir (base64, sha256sum, et même un hashcat qui force du MD5 en multicœur via les Web Workers), sans oublier l'historique persistant, l'autocomplétion au Tab et l'édition de ligne à la Ctrl+A/E/U/K/W. Il y a aussi une commande msg qui m'envoie une notification Web Push, pratique pour recevoir un mot d'un visiteur même sans onglet ouvert.

Un point de vigilance côté sécurité : les commandes dynamiques s'exécutent via AsyncFunction, donc une forme d'eval. La politique de sécurité de contenu déployée est une couche de durcissement, pas un bac à sable, puisqu'elle doit autoriser unsafe-eval pour le moteur. Il ne faut donc charger que des commandes de confiance. C'est un compromis assumé pour un portail personnel mono-auteur.

Le pari : embarquer un LLM dans le terminal

La vraie originalité du projet, cela tient au mécanisme de chargement de LLM. J'utilise WebLLM[3], le moteur d'inférence du projet MLC qui exécute des modèles de langage directement dans le navigateur, accéléré par WebGPU[4], sans serveur et sans appel réseau pour l'inférence. L'API est compatible OpenAI, ce qui simplifie beaucoup l'intégration.

Plutôt que de laisser chaque commande gérer son moteur dans son coin, j'ai centralisé toute la mécanique dans un module unique, propriétaire exclusif du moteur WebLLM. C'est le seul endroit qui télécharge les poids, conserve le moteur résident, lance les générations et comptabilise les tokens. Les commandes ne touchent jamais directement au moteur : elles passent par une façade qui leur fournit la demande de consentement et la barre de progression dans le terminal. L'état courant (modèle chargé, tokens consommés en entrée et en sortie, progression du téléchargement) vit sur un emplacement global et alimente un widget en haut à droite de l'écran.

D'où viennent les modèles, et comment ils sont chargés

Le moteur WebLLM lui-même, je l'héberge sur mon propre serveur, dans un fichier /vendor/web-llm-<version>.js d'environ 6 Mo. Il n'est pas embarqué dans la page : je le charge à la demande, par import dynamique, la toute première fois qu'une commande réclame un modèle, puis je le conserve pour la durée de la session. Tant que vous ne touchez à aucune commande d'IA, rien n'est téléchargé.

Le catalogue des modèles, lui, est celui que WebLLM fournit par défaut, à savoir son registre prebuiltAppConfig. Lorsque vous confirmez un modèle, le moteur va chercher deux choses sur le réseau : les poids quantifiés, au format MLC, depuis Hugging Face (l'organisation officielle mlc-ai[5]), et la bibliothèque de calcul compilée pour WebGPU, un fichier WebAssembly fourni par le projet MLC. Une fonction de rappel sur la progression alimente la barre que vous voyez défiler dans le terminal.

Les deux ressources sont ensuite rangées dans le cache du navigateur. Le second chargement du même modèle devient donc instantané et fonctionne hors ligne. Point important pour la confidentialité : ce téléchargement initial est le seul moment où le navigateur sort sur le réseau. L'inférence, elle, se déroule intégralement en local, sans le moindre appel vers un serveur.

Quelques détails d'implémentation dont je suis assez content :

  • Aucun modèle n'est chargé par défaut. Au premier usage, la commande propose un modèle et vous demande de confirmer son téléchargement. Rien ne part sans votre accord.
  • Le module choisit automatiquement la bonne quantification selon votre carte graphique : il interroge l'adaptateur WebGPU et retient q4f16 si la fonctionnalité shader-f16 est disponible, sinon q4f32. Et si un GPU annonce le support f16 mais échoue à compiler les shaders, il bascule en q4f32 en repli.
  • Les modèles sont mis en cache dans le navigateur. Une fois téléchargé, un modèle est réutilisé instantanément, y compris hors ligne, et je fournis de quoi lister et purger ce cache via la commande llm.
  • Le modèle résident est partagé. Si vous l'avez chargé pour une commande, les deux autres le réutilisent sans rien retélécharger.

Reste une question très concrète : quel modèle votre machine peut-elle réellement charger ? J'y réponds avec une commande dédiée, webllmfit, inspirée de llmfit.org, dont je vous parlais dans un précédent article, mais branchée sur le vrai catalogue WebLLM. Elle sonde les capacités WebGPU de la machine (l'adaptateur, le support de shader-f16, les tailles maximales de buffers) ainsi qu'une estimation de la mémoire disponible, puis croise ces informations avec la VRAM requise et les fonctionnalités exigées par chaque modèle. Chacun reçoit alors un verdict : FIT lorsqu'il tient confortablement, TIGHT lorsqu'il passe de justesse, NO lorsqu'il est trop gros ou réclame une fonctionnalité GPU absente. La liste est triée pour faire remonter en premier le plus gros modèle exécutable. Le budget mémoire reste une estimation, car les navigateurs n'exposent pas la VRAM totale, mais cela donne une idée fiable avant de lancer un téléchargement de plusieurs gigaoctets.

Et pour piloter tout cela à la main, il y a la commande llm, qui sert de console au module central. Sans argument, elle affiche le modèle actuellement chargé et les tokens consommés en entrée et en sortie. llm --list énumère les modèles conseillés, llm --load <id> en charge un explicitement, et llm --unload libère la mémoire GPU. C'est aussi par elle que l'on change de modèle en cours de route, ou que l'on gère le cache du navigateur : llm --cache liste ce qui est stocké, llm --rm supprime un modèle et llm --rm-all vide tout. Les opérations de cache ne réclament d'ailleurs pas WebGPU et fonctionnent dans n'importe quel navigateur.

L'argument qui me plaît le plus dans cette approche : vos échanges avec le modèle ne quittent jamais votre machine. Pas de serveur d'inférence, pas de clé d'API, pas de donnée envoyée ailleurs.

Trois exemples de commandes pour démontrer les capacités de WebLLM

Pour montrer ce que cette base permet, j'ai écrit trois commandes très différentes, du chatbot trivial à l'agent autonome.

miaougpt, le chatbot de terminal

miaougpt est le cas le plus simple : un chat en mode terminal. Vous écrivez, l'assistant répond, jeton par jeton en streaming. Il a une légère personnalité féline, glisse un miaulement de temps en temps, et répond dans votre langue. J'y ai ajouté la synthèse vocale via la Web Speech API : /voice fr-FR fait lire les réponses à voix haute. Le modèle par défaut est un Qwen2.5 1.5B, suffisant pour discuter.

Denree, l'agent autonome appelé par "?"

Denree est l'expérience la plus ambitieuse : une architecture d'agent capable de piloter les commandes du shell. Le nom est un clin d'œil à la Denrée, l'extraterrestre de La Soupe aux Choux[6]. Vous lui donnez un objectif en langage naturel, et il demande au modèle de choisir, étape par étape, quelle commande lancer. Il exécute la commande, lit sa sortie, la réinjecte dans le contexte, et recommence jusqu'à produire une réponse finale. Je l'ai associé à l'alias ?, donc on l'appelle très naturellement :

? comment créer un fichier
? quelle est mon ip

Faire raisonner correctement un petit modèle de 1,5 milliard de paramètres a demandé plusieurs astuces que je détaille volontiers, parce que ce sont elles qui font la différence entre un agent qui boucle et un agent qui répond :

  • À chaque tour, la décision du modèle est contrainte en JSON par génération guidée par schéma. L'agent ne part jamais en vrille : il renvoie toujours soit une commande à lancer, soit rien.
  • Un mini-RAG lexical, un BM25 allégé en JavaScript pur sur les pages de manuel, sélectionne les commandes les plus pertinentes pour l'objectif et injecte leur syntaxe exacte dans le contexte. Un petit modèle devine très mal les options d'une commande ; lui rappeler le bon SYNOPSIS évite la majorité des boucles.
  • La réponse finale n'est pas demandée au modèle pendant la boucle, mais extraite par une étape de synthèse séparée à partir des sorties réellement obtenues, avec une consigne stricte de ne rien inventer ni convertir. C'est ce qui rend le résultat fiable malgré la taille du modèle.
  • Une liste d'interdiction protège l'utilisateur : les commandes à effet de bord ou de contrôle (rm, su, sudo, msg, open, et les commandes d'IA elles-mêmes) sont bloquées pour l'agent.

Glaude, la note d'humour

Glaude est plus léger, et complètement assumé comme tel. C'est une parodie de Claude Code, incarnée par le Glaude, le paysan bourbonnais du même film. Il en reprend l'allure, bannière, boîte d'accueil, invite , mais sa spécialité est de pondre des sites web volontairement hideux et flashy : fonds fluo, dégradés arc-en-ciel, Comic Sans, balises <marquee>, texte qui clignote, emojis partout. L'esthétique GeoCities de 1997, en somme. Au lancement, il amorce un projet sous votre répertoire personnel, comme le ferait un vrai agent de code, écrit un index.html de départ, puis vous laisse lui décrire la page. Un /show ouvre un faux navigateur pour admirer le carnage, et un /download empaquette le tout en archive ZIP, le tout en JavaScript pur. Il s'appuie sur des modèles Qwen2.5-Coder.

En synthèse

Ce projet m'a surtout servi de terrain d'apprentissage sur l'IA locale. Faire tourner un modèle dans un onglet est désormais réaliste : WebGPU est disponible par défaut dans les versions récentes des principaux navigateurs, même s'il reste prudent de détecter sa présence et de prévoir un repli. Les modèles de quelques milliards de paramètres restent modestes, et c'est précisément ce qui rend l'exercice formateur : pour qu'un agent soit utile à cette échelle, il faut compenser par l'ingénierie, la contrainte de format, le RAG sur la documentation, la séparation entre décision et synthèse.

J'attends d'ailleurs avec une certaine impatience le courriel triomphant de la personne persuadée d'avoir piraté mon serveur parce qu'elle a vu défiler la connexion SSH, tapé quelques commandes et décroché un prompt root avec un su. Je la remercie par avance pour son rapport : ce shell n'est qu'une simulation, il tourne intégralement dans son propre navigateur, et derrière, il n'y a justement aucun serveur à compromettre.

Le tout est sous licence MIT et le dépôt est conçu pour être réutilisable : l'identité et la configuration tiennent dans un seul fichier, et le contenu visitable vit dans l'arborescence root/. Si vous voulez en faire votre propre portail terminal, ou simplement aller demander à Denree quelle est votre adresse IP, le code et la démo sont en ligne.

Notes et références


  1. Terminal.com, dépôt du projet sur GitHub : https://github.com/ltoinel/Terminal.com ↩︎

  2. Astro, le générateur de site statique utilisé : https://astro.build ↩︎

  3. WebLLM, moteur d'inférence LLM dans le navigateur (projet MLC, licence Apache 2.0) : https://github.com/mlc-ai/web-llm ↩︎

  4. WebGPU, l'API d'accès au GPU dans le navigateur (documentation MDN) : https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API ↩︎

  5. Hugging Face, organisation officielle mlc-ai, où sont publiés les modèles au format MLC chargés par WebLLM : https://huggingface.co/mlc-ai ↩︎

  6. La Soupe aux choux, film de Jean Girault (1981), d'où sont tirés le Glaude et la Denrée : https://fr.wikipedia.org/wiki/La_Soupe_aux_choux ↩︎


Vous êtes correctement abonné à Geeek.org
Bienvenue ! Vous êtes correctement connecté.
Parfait ! Vous êtes correctement inscrit.
Votre lien a expiré
Vérifiez vos emails et utiliser le lien magique pour vous connecter à ce site