L'interface est une application React rendue par [Ink](https://github.com/vadimdemedes/ink) (Ink 7 : React ≥ 19.2 ; vérifiez la version de Node qu'il exige).
Le CLI garde une façade impérative (`InkUI` : `info`, `select`, `readInput`, `confirm`, `withSpinner`…) au-dessus d'un `UIStore` sans dépendance à React,
ce qui rend la logique testable (`store.test.ts`). Les composants (saisie, liste, confirmation, spinner) sont écrits à la main ; seuls les badges viennent de [`@inkjs/ui`](https://github.com/vadimdemedes/ink-ui).
Présentation : la bannière d'accueil reprend le style « MAIA » : image à gauche, affichée telle quelle en couleurs réelles (demi-blocs ▀▄█ : 32 × 32 pixels, soit 32 colonnes × 16 lignes ; sans pixelisation). Elle vient de `packages/ui/assets/mask.png` ; `python3 packages/ui/scripts/make-mask.py [taille]` (Pillow) régénère `src/mask-data.ts` à une autre taille, titre « Maia » en gros caractères pixel avec ombre et dégradé arc-en-ciel à droite, puis deux textes (vert : « Assistant IA en ligne de commande… », rose : « Un seul agent, plusieurs modèles »). Terminal d'au moins 58 colonnes pour la version complète ; entre 25 et 57 : sans le masque ; en dessous : cadre à double bordure compact, titre centré ;
Les réponses de l'agent sont rendues en markdown (titres, listes, code, tableaux, gras…) avec [`markdansi`](https://github.com/steipete/markdansi). Tout est isolé dans `packages/ui/src/markdown.tsx` : pour le retirer, supprimer ce fichier, la dépendance `markdansi` et remplacer `<Markdown />` par `<Text>` dans `components.tsx`. Le texte en cours de streaming reste brut ; la mise en forme apparaît quand la réponse est terminée. `MAIA_NO_MARKDOWN=1` désactive le rendu sans toucher au code, et un échec de `markdansi` retombe sur le texte brut.
Les couleurs sont dans `packages/ui/src/colors.ts`. Une fois la réponse validée, la question est remplacée par la ligne `◆ Question : choix` dans le journal.
Barre de contexte : sous la zone de saisie, en bas à droite, une barre de carrés ■ (de gauche à droite) montre la taille du contexte de la conversation par rapport à la taille maximale du modèle, à l'échelle **1 carré = 10 000 tokens** (200 000 tokens → 20 carrés), avec le libellé « 45.7k / 200k · 23% ». Les carrés sont verts, jaunes à partir de 60 %, rouges à partir de 85 %. Si la barre ne tient pas dans le terminal (très grands contextes), l'échelle est agrandie par paliers de 10 000 et indiquée (`1■ = 20k`).
- **Taille maximale** : après le choix du modèle (au démarrage, avec `--model`/config, et avec `/model`), MAIA demande au LLM « Taille maximale du contexte pour ce modèle » (réponse en tokens, lue par `parseTokenCount` : `128000`, `128 000 tokens`, `200k`, `1,5 million`…). La valeur est annoncée (INFO) ; une réponse par modèle et par exécution. Si la réponse est illisible, ou si la question échoue ou dépasse 60 s (Ctrl+C l'abandonne), un avertissement s'affiche et la barre est masquée. Un LLM peut se tromper sur sa propre limite : la valeur affichée en INFO permet de la vérifier.
- **Contexte utilisé** = tokens de tout ce que la session enverrait au modèle : prompt système (MAIA.md…), définition des outils, messages (texte, appels d'outils et leurs résultats, +4 tokens de structure par message) et contexte en attente (résultats de commandes `!`, fichiers `@`). Le calcul est **local et déterministe**, avec [js-tiktoken](https://github.com/dqbd/tiktoken) (vocabulaire `o200k_base`) ; la consommation renvoyée par le serveur n'est plus utilisée, car elle faisait varier la valeur. C'est une **somme** des comptes de chaque élément : la valeur ne fait qu'augmenter quand la conversation s'enrichit, et ne diminue que si le contexte est réduit (`/clear`, `/compact`, tour interrompu, autre session). Le comptage est refait **avant chaque saisie** de l'utilisateur (et après chaque réponse du LLM) ; le compte de chaque message est mémorisé, donc le recalcul reste instantané. Pour les modèles qui n'utilisent pas ce vocabulaire (Claude, Mistral…), la valeur est une bonne approximation, pas le compte exact du serveur.
La zone de saisie est encadrée par une bordure en pointillés (┈) **au-dessus et au-dessous** seulement, en `greenBright`, sans bords gauche ni droit.
À sa gauche, le spinner `arrow3` de [cli-spinners](https://github.com/sindresorhus/cli-spinners) s'anime en continu. Pendant l'attente de la réponse après validation de la saisie, « En réflexion » s'affiche avec le spinner `dots10` à sa gauche, en `yellowBright` (même spinner pour les autres attentes : chargement des modèles, exécution d'une commande `!`).
(`~/…` désigne le dossier personnel). Ce dossier est capturé une fois au démarrage, affiché (« Dossier de travail : … ») et transmis aux outils et aux providers ;
les règles de permissions (`Read(./.env)`, `Edit(src/**)`) sont elles aussi relatives à ce dossier. Seul `~/.maia` (config, `settings.json` et skills utilisateur) est lu depuis le dossier personnel.
- stdout et stderr sont affichés dans l'ordre d'arrivée, avec le code de sortie en cas d'échec ; durée maximale 2 minutes ;
- aucune confirmation n'est demandée (c'est vous qui la tapez) et les règles de permissions des outils ne s'appliquent pas ;
- le résultat est ajouté au contexte de **votre prochaine demande** (jusqu'à 30 000 caractères, milieu tronqué au-delà), pour que le modèle sache ce qui a été lancé. `/clear` l'efface ;
- Ctrl+C interrompt la commande ; `!` seul affiche l'aide. Les programmes interactifs (vim, less, saisie au clavier…) ne sont pas pris en charge.
| `compare @a.ts et @ b.ts` | plusieurs fichiers ; la forme `@ chemin` (avec espace) est acceptée |
| `lis @'mon dossier/a b.txt'` | guillemets simples pour un chemin avec espaces (`\'` : guillemet littéral) |
| `résume @docs` | un dossier : la liste de ses entrées est injectée |
| `@/chemin/complet.txt` · `@~/notes.md` | chemin complet, ou depuis le dossier personnel |
- Chemins relatifs au dossier de lancement (voir « Dossier de base »). Aucune confirmation : c'est vous qui citez le fichier, les règles de permissions des outils ne s'appliquent pas.
- Un `@mot` qui ne désigne aucun fichier existant reste du texte ordinaire (`@mention`, `@Override`, `jean@exemple.fr`) ; un avertissement n'apparaît que s'il ressemble à un chemin (`@src/x.ts` introuvable).
- Limites : 2 Mo par fichier, 50 000 caractères injectés par fichier (milieu tronqué au-delà) et 150 000 par message ; les fichiers binaires sont ignorés ; un même fichier cité plusieurs fois n'est injecté qu'une fois.
- Le contenu est marqué comme donnée, pas comme instructions. Les `@fichier` écrits *dans* le corps d'une commande personnalisée ne sont pas encore traités (seuls ceux de votre saisie le sont).
Au lancement, `./.maia` et `./.maia/.sessions` sont créés dans le dossier d'exécution s'ils n'existent pas. Les sessions sont une fonctionnalité de l'application.
-`/new` : nouvelle session, nouveau contexte (`MAIA.md` / `CLAUDE.md` et skills rechargés) ; l'utilisateur est averti que la session quittée est sauvegardée (sous son nom).
-`OpenAI_EndPoint` : suffixe ajouté à `LiteLLM_URL` pour envoyer une requête au format OpenAI.
-`LiteLLM_Models` : suffixe ajouté à `LiteLLM_URL` pour récupérer la liste des modèles disponibles.
- Optionnel : `LiteLLM_API_Key` (ou variable d'environnement `LITELLM_API_KEY`), envoyée en `Authorization: Bearer …`. Ce fichier peut donc contenir un secret : ne pas le versionner.
- Optionnel : `MaxOutputTokens` (entier, ex. `16000`) : `max_tokens` envoyé à chaque requête. Sans lui, le serveur applique sa limite par défaut, parfois courte : un long fichier écrit par l'outil `Write` peut être coupé (le paramètre `content` arrive alors absent). MAIA affiche un avertissement quand la réponse est coupée (finish_reason `length`, ou sortie qui tombe pile sur une limite usuelle 4096/8192… avec un appel d'outil : certains serveurs « réparent » alors l'appel et annoncent `tool_calls`).
Cas constaté avec `anthropic--claude-4.5-sonnet` : `completion_tokens: 4096` et `Write` reçu avec `path` seul → fixer par ex. `"MaxOutputTokens": 32000`.
- Diagnostic : `MAIA_DEBUG_LLM=1 maia` enregistre le flux brut renvoyé par LiteLLM dans `maia-llm-debug.log` (dossier temporaire ; `MAIA_DEBUG_LLM=/chemin/fichier.log` pour choisir le fichier).
Au lancement, le contexte de départ est chargé depuis le **dossier de lancement** : `MAIA.md` s'il existe, sinon `CLAUDE.md` (s'il existe).
Le fichier chargé est indiqué par un message `INFO` ; si aucun des deux n'existe, un `WARN` le signale. Seul ce dossier est regardé (pas les dossiers parents).
Les dossiers `.claude` ne sont **plus lus** (ni `settings.json`, ni `commands/`, ni `skills/`, ni `.claude/CLAUDE.md`, ni `~/.claude`).
- **Skills** : `.maia/skills/<nom_du_skill>/SKILL.md` (ou `skill.md`), chargés depuis trois emplacements. Le plus spécifique l'emporte en cas de même nom :
1. le dossier utilisateur : `~/.maia/skills` ;
2. le dossier parent du dossier de lancement : ex. `/X/.maia/skills` pour un lancement dans `/X/Y` ;
3. le dossier de lancement : ex. `/X/Y/.maia/skills`.
Le fichier commence par un en-tête YAML : `description` (objectif du skill) et `name` (optionnel ; s'il est présent, il doit être identique au nom du dossier, qui fait foi).
Seule la liste (nom + description) est mise dans le prompt ; le contenu est chargé à la demande par l'outil `Skill`, ou lancé directement avec `§nom`.
```
---
name: sk_test # optionnel, = nom du dossier
description: Lance les tests du projet et résume les échecs
---
Instructions du skill… ($ARGUMENTS = arguments passés avec §sk_test)
Conséquence : il n'y a plus de variables d'environnement venant de la configuration de l'agent ni de slash-commandes personnalisées. Les permissions sont décrites dans la section suivante.
## Permissions
Les permissions s'appliquent aux **outils** (intégrés ou non). Trois types de règles :
| Type | Effet |
| --- | --- |
| `allow` | l'outil est utilisé sans approbation manuelle |
| `ask` | l'utilisateur doit confirmer chaque utilisation (badge `ASK`, réponse `o`/`n`) |
| `deny` | l'outil ne peut pas être utilisé (l'agent reçoit un refus) |
**Ordre d'évaluation : deny, puis ask, puis allow.** La première famille qui correspond décide, quelle que soit la précision des règles : un `allow` étroit comme `Bash(aws s3 ls)` ne fait pas exception à un `deny` large comme `Bash(aws *)`, ni à un `ask` plus large. **Sans règle applicable, c'est `ask` qui s'applique** (pour tous les outils, y compris Read, Glob, Grep…).
Où les définir : `~/.maia/settings.json` (utilisateur) et `./.maia/settings.json` (dossier de lancement). Les listes des deux fichiers s'additionnent. Le fichier `../.maia/settings.json` du dossier parent n'est **pas** pris en compte. Dans chaque fichier, la propriété `permissions` contient une liste par type, tous facultatifs :
Format d'une règle : `Outil` (tous les appels de l'outil) ou `Outil(motif)` :
| Outil | Le motif porte sur | Exemples |
| --- | --- | --- |
| `Bash` | la commande ; `*` = n'importe quelle suite ; `cmd *` couvre `cmd` avec ou sans arguments (mais pas `cmdx`) ; sans `*` : commande exacte | `Bash(allow stop *)`, `Bash(git status)`, `Bash(npm run test:*)` |
| `Read`, `Write`, `Edit`, `Glob`, `Grep` | le chemin (relatif au dossier de lancement, motifs glob) | `Read(./.deny.md)`, `Edit(src/**)` |
| `WebFetch` | le domaine | `WebFetch(domain:docs.example.com)`, `WebFetch(domain:*.example.org)` |
| `Skill` | le nom du skill | `Skill(pdf)` |
| autres outils (ex. ajoutés plus tard) | une valeur texte de l'entrée de l'outil (`*` accepté) | `mcp__db__query(SELECT *)` |
Précisions sur `Bash` :
- commande composée (`&&`, `;`, `|`) : un `deny` ou un `ask` s'applique dès qu'**une** des parties correspond, y compris cachée dans `$(…)`, des backticks ou des parenthèses ; un `allow` ne s'applique que si **toutes** les parties sont couvertes ;
- un `allow` ne couvre jamais une commande avec redirection (`>`) ou substitution (`$(…)`, backticks) : elle repasse en `ask`.
Au démarrage, MAIA indique (`INFO`) les fichiers chargés et le nombre de règles, ou qu'aucune règle n'est définie ; un JSON invalide ou une règle mal formée donne un `WARN` et est ignoré. Les `deny` sont du « meilleur effort » : ils ne remplacent pas un vrai bac à sable. Les commandes `!` et les références `@fichier` tapées par l'utilisateur ne passent pas par ces règles.
Attention : `./.maia/settings.json` d'un dépôt tiers peut contenir des règles `allow` larges ; relisez-le avant de lancer MAIA dans un projet que vous ne connaissez pas.
La commande `/permissions` affiche les règles actives, fichier par fichier, avec un badge `ALLOW` (vert), `ASK` (jaune) et `DENY` (rouge) suivi des règles du type, une par ligne ; seuls les types non vides sont montrés. Après avoir modifié un `settings.json`, `/clear` recharge la configuration.
- **TodoWrite** : liste de tâches de la session (`pending` / `in_progress` / `completed`, forme `activeForm` pour la tâche en cours). Chaque appel remplace la liste ; le résultat (☐ ▶ ☑) est affiché à l'utilisateur et renvoyé au modèle. Un avertissement est ajouté si plusieurs tâches sont « en cours ».
- **CurrentModel** : renvoie le modèle LLM utilisé par la session (valeur à jour après un `/model`). Sans paramètre, sans confirmation ; le modèle est lu au moment de l'appel.
- **WebFetch** : télécharge une URL http(s) (HTML converti en Markdown avec `turndown`, JSON mis en forme, texte brut ; 5 Mo et 30 s maximum, `max_length` caractères renvoyés). Pas de JavaScript exécuté, pas de PDF ni d'images.
- Une redirection vers un **autre domaine** n'est pas suivie : l'outil renvoie la nouvelle URL, que le modèle doit rappeler (ce qui repasse par la confirmation).
- Le contenu est signalé au modèle comme non fiable (risque d'injection de prompt) ; il ne passe par aucun second modèle de résumé.
L'utilisateur peut créer ses propres outils programmatiques : le LLM les appelle comme n'importe quel autre outil quand leur description correspond au besoin. Ils sont chargés au démarrage et rechargés par `/clear` et `/new`.
- **Où** : uniquement dans `./.maia/tools` du dossier de lancement (jamais le dossier parent ni le dossier de l'utilisateur). Un sous-dossier = un outil : `./.maia/tools/addition/TOOL.json` (`tool.json` accepté aussi) et ses scripts. Un exemple complet est fourni dans `examples/tools/addition` ; exemple de sous-agent : `examples/agents/code_reviewer` (à copier dans `./.maia/agents`).(à copier dans `./.maia/tools/`).
| `description` | **Obligatoire** : dit au LLM à quoi sert l'outil et quand l'appeler. |
| `name` | Facultatif, le nom du dossier par défaut. Lettres, chiffres, `_` et `-` (64 caractères maximum). Ne peut pas reprendre le nom d'un outil intégré (Read, Bash…) ni d'un autre outil personnalisé. |
| `command.parameters` | Paramètres fournis par le LLM. `type` : `"number"` ou `"string"`. `default` : valeur utilisée si le LLM n'en donne pas (ex. `{{param2}}` devient `0`) ; sans `default`, le paramètre est obligatoire. `description` facultative par paramètre. |
| `command.run` | Ligne de commande ; chaque `{{paramètre}}` est remplacé par sa valeur. `./` désigne le dossier de l'outil (`./test.js` = `./.maia/tools/addition/test.js`) et c'est le dossier d'exécution. |
- **Sécurité : pas de shell.** La ligne `run` est découpée en arguments (guillemets simples ou doubles possibles) et exécutée directement : une valeur envoyée par le LLM reste **toujours un seul argument** (`; rm -rf /` ou `$(cmd)` sont reçus tels quels par le script, jamais interprétés). En contrepartie, `run` n'accepte ni tubes `|`, ni redirections `>`, ni variables `$HOME` ; pour cela, appeler un script (`sh ./script.sh {{p}}`). Le programme (premier mot) ne peut pas être un `{{paramètre}}`. Sous Windows, les scripts `.cmd`/`.bat` ne sont pas lancés directement (utiliser `node`, `python`, ou `cmd /c ./script.cmd`).
- **Exécution** : timeout de 2 minutes, sortie limitée à 30 000 caractères ; un code de sortie non nul est renvoyé au LLM comme une erreur (avec la sortie d'erreur du script). Le script reçoit `MAIA_CWD` (dossier de lancement de MAIA, pour retrouver le projet) et `MAIA_TOOL_DIR`.
- **Permissions** : un outil personnalisé exécute un programme, donc il demande confirmation par défaut. Les règles de permissions s'appliquent avec son nom : `"Addition"` dans `allow` l'autorise sans demander, `"Addition(5)"` cible une valeur de paramètre.
- **Erreurs** : une définition invalide (description absente, type inconnu, `{{param}}` non déclaré, JSON cassé…) est signalée par un avertissement (`WARN`) et l'outil est ignoré ; les autres restent chargés. Une virgule en trop avant `]` ou `}` est tolérée. Au chargement, les outils valides sont annoncés (`INFO`).
## Exécution des outils : premier plan et parallèle
- Le **raisonnement du LLM reste bloquant** : l'utilisateur attend la réponse, ce qui préserve l'intégrité du contexte.
- **1 outil demandé** : exécuté au premier plan (annonce, confirmation éventuelle, exécution, résultat).
- **2 outils ou plus** dans la même réponse : chaque outil part dans sa **propre tâche, en arrière-plan**, dès qu'il est autorisé. L'agent attend la fin de **toutes** les tâches, puis renvoie les résultats au LLM **dans l'ordre où il les a demandés**. Une erreur ou un refus n'annule pas les autres outils.
- Le spinner indique « Outils en parallèle : n/total terminés (…) ».
- Limites : ce sont des tâches asynchrones Node (pas des threads système ; les processus lancés par Bash ou les outils personnalisés tournent réellement en parallèle) ; 8 outils au plus en même temps (`MAX_PARALLEL_TOOLS`) ; les confirmations sont posées une par une ; les appels qui portent sur le même fichier (Read/Write/Edit) ou TodoWrite passent l'un après l'autre (`Tool.lockKey`).
Le compactage résume toute la conversation de la session pour libérer du contexte.
1. La conversation actuelle est sauvegardée telle quelle sous son nom.
2. Le LLM reçoit la demande « Faire un résumé compacté de nos échanges… » (5 rubriques : Contexte du projet, Objectif actuel, Fichiers pertinents, Décisions/Conclusions, Prochaines étapes), limité à 4000 tokens. Les commandes `!` et fichiers `@` pas encore envoyés font partie de ce qui est résumé.
3. Le contexte est supprimé comme avec `/clear`, mais `MAIA.md` / `CLAUDE.md` ne sont **pas** rechargés : le résumé les remplace comme contexte de départ (dans le prompt système). Outils, skills et permissions sont rechargés.
4. La session continue sous le nom `{nom d'origine}__{n}` : `xxx` → `xxx__1` → `xxx__2`… Après `__1000`, le suivant reprend à `__1` et écrase ce fichier. Le fichier d'avant compactage reste intact ; le fichier `xxx__n` contient le résumé (`startContext`) et la suite de la conversation, et `/load xxx__n` le restaure.
5. Le nombre de tokens est recalculé et la barre de progression mise à jour.
Déclenchement automatique : dès que le contexte dépasse `Seuil_max = Max_Token_Context - (4000 + 500)` (4000 = tokens du résumé, 500 = marge d'erreur). La taille maximale est celle demandée au LLM au choix du modèle ; si elle est inconnue (barre masquée), seul `/compact` fonctionne. Le contrôle a lieu avant l'envoi de chaque demande et après chaque tour.
L'appel est **bloquant** (pas de tâche de fond). Ctrl+C l'annule sans rien modifier ; en cas d'erreur, la conversation reste inchangée.
**Contexte refusé par le serveur** : si LiteLLM répond « Input is too long » (ou équivalent), MAIA compacte automatiquement la conversation (résultats d'outils tronqués à 1 500 caractères dans la demande de résumé, pour qu'elle tienne), puis reprend la demande une seule fois. Si le compactage échoue aussi, l'erreur d'origine est affichée (utilisez alors `/new`). Les sous-agents n'ont pas ce mécanisme.
MAIA peut confier une tâche à un **sous-agent** : un processus Node autonome, avec sa propre conversation (vide) et sa propre définition. L'agent principal le lance avec l'outil `Agent` (`agent` = nom, `task` = tâche complète : le sous-agent ne voit rien de la conversation principale). Seul le **résumé final** du sous-agent revient dans la conversation principale. Plusieurs appels `Agent` dans une même réponse s'exécutent en parallèle.
Définition : un dossier par agent, avec un fichier `AGENT.md`, dans `./.maia/agents/<nom>/` (dossier de lancement) ou `~/.maia/agents/<nom>/` (utilisateur) ; le projet l'emporte sur l'utilisateur à nom égal. Les agents chargés sont listés au démarrage (« Agents chargés : ») et avec `/agents`.
```markdown
---
name: code_reviewer # doit être le nom du dossier
description: Révise du code (sécurité, anti-patterns). Ne pas l'utiliser pour écrire du code.
model: sonnet # optionnel — défaut : modèle de l'agent principal
tools: Read, Grep, Write # optionnel — défaut : mêmes outils que l'agent principal (sans Agent)
permissions: # optionnel — s'ajoutent aux règles héritées
allow: [Read, Grep]
deny: [Bash]
effort: medium # low | medium | high — défaut medium
skills: [api-docs, testing-guide] # optionnel — défaut : tous les skills
trace: false # défaut false : true enregistre la conversation (voir plus bas)
---
# Instructions system
Tu es un expert en révision de code.
# Missions
- identifier les problèmes de sécurité
- détecter les anti-patterns
- suggérer des améliorations
# Output
Pour chaque problème : catégorie (CRITICAL / HIGH / MEDIUM / LOW), description, code concerné, solution recommandée.
```
- Les trois sections « Instructions system », « Missions » et « Output » sont obligatoires (titres Markdown de n'importe quel niveau) ; un agent invalide est ignoré avec un avertissement qui dit pourquoi.
- **Héritage** : sans `tools`, `skills` ou `permissions`, le sous-agent a les mêmes outils (y compris les outils personnalisés), skills et règles que l'agent principal. Les règles `permissions` de l'agent **s'ajoutent** aux règles héritées : `deny > ask > allow` reste vrai, un `deny` ou `ask` hérité n'est jamais levé. Un sous-agent ne peut pas lancer de sous-agent.
- **Résumé** : la dernière réponse du sous-agent suit la section `Output` ; s'il épuise son budget d'étapes sans conclure, un appel supplémentaire lui demande ce résumé.
- **Effort** : borne le nombre d'étapes du sous-agent (low 10, medium 25, high 50) et module son prompt.
- **Modèle** : identifiant exact ou fragment (`sonnet` → premier modèle LiteLLM dont le nom contient « sonnet ») ; introuvable → modèle de l'agent principal, avec avertissement.
- **Autorisations** : si le sous-agent doit demander une confirmation, la question s'affiche dans l'interface principale, préfixée `[sous-agent nom]` (une question à la fois). Ctrl+C interrompt aussi les sous-agents en cours.
- **Fin de mission** : un message INFO est affiché dans l'interface principale (durée, nombre d'appels d'outils) ; un échec est signalé par un avertissement et renvoyé à l'agent principal comme erreur d'outil. Délai maximal : 30 minutes.
- **Trace** (`trace: true`) : la conversation du sous-agent est enregistrée dans `./.maia/.sessions/` sous le nom `{session de l'agent principal}-{agent}`, ex. `session_2026-10-20_00_01_53-agent_1.json` (`_2`, `_3`… si le nom est pris). Sans `trace`, rien n'est enregistré.
- Provider LiteLLM : validation sur un vrai serveur (streaming SSE et appels d'outils testés seulement sur des flux simulés) ; compatibilité de `@inkjs/ui` avec Ink 7 à confirmer à l'usage (l'affichage des badges a été vu fonctionner par l'utilisateur).
- Éventuellement rétablir sous `.maia` ce que supprimait l'abandon de `.claude` : variables d'environnement et slash-commandes personnalisées (le code des commandes existe encore dans `packages/agent/src/commands.ts`, il n'est plus appelé).
- Taille de contexte : lue en interrogeant le LLM (demande de l'utilisateur) ; la route `/model/info` de LiteLLM (`max_input_tokens`) serait plus fiable, et un réglage manuel dans `settings.json` pourrait corriger une réponse erronée.
- Nerd Fonts (icônes) : proposé comme réglage optionnel dans `settings.json`, non réalisé.
- Tests : les suites vitest et le typecheck sont à lancer après `pnpm install` (écrits sans avoir pu être exécutés dans l'environnement de développement).
## Travaux réalisés
Journal des évolutions, dans l'ordre :
1.**Squelette** : monorepo pnpm (TypeScript strict, tsup, vitest), contrats `LLMProvider` / `Tool` / `AgentUI`, boucle agent avec appels d'outils, agent par défaut de type « Claude Code » (outils Read, Write, Edit, Bash).
2.**Provider SAP AI Core** puis **choix du modèle** au démarrage et commande `/model`. *(Remplacé ensuite par LiteLLM, voir 12.)*
3.**Outils Glob et Grep** (JavaScript pur, sans ripgrep).
4.**Configuration de l'agent (lot 2)** : fichiers de contexte, permissions, slash-commandes, skills. *(Abandonné avec les dossiers `.claude`, voir 13.)*
5.**`§skill`** : appel direct d'un skill de l'agent.
6.**Dossier de lancement** : tous les chemins relatifs partent du dossier où MAIA est lancé (`MAIA_CWD` > `INIT_CWD` > `cwd`).
7.**Interface Ink** (remplace Clack.js) : bannière « MAIA » avec masque pixel-art et titre arc-en-ciel, messages en bleu clair, zone de saisie en pointillés `greenBright` avec spinner `arrow3`, attente « En réflexion » avec `dots10` en `yellowBright`.
8.**Outils TodoWrite et WebFetch** (lot 3, première partie).
9.**`!commande`** (avec la syntaxe `!'commande avec paramètres'`) et **`@fichier`**.
10.**Renommage en MAIA** (titre « Maia ») et **sessions** : `./.maia/.sessions`, sauvegarde automatique, `/rename`, `/load` (avec réaffichage du contenu), `/new`.
11.**Corrections** : import manquant de `normalizeSessionName`, `session.tools` est une `Map`, en-tête de skill avec espace avant les deux-points (`description : …`).
12.**LiteLLM** : un seul provider, requêtes au format OpenAI en streaming vers le endpoint OpenAI de LiteLLM ; configuration `~/.maia/settings.json` (`LiteLLM_URL`, `OpenAI_EndPoint`, `LiteLLM_Models`, `LiteLLM_API_Key` optionnel). Suppression de SAP AI Core, Anthropic, OpenAI et Ollama.
13.**Renommage technique**`magiccode` → `maia` (packages `@maia/*`, commande `maia`, variables `MAIA_*`, dossier `.maia`) ; textes « MagicCode » → « MAIA » ; bannière « Un seul agent, plusieurs modèles ».
14.**Skills dans `.maia/skills`** (dossier de lancement, dossier parent, dossier utilisateur) ; les dossiers `.claude` ne sont plus lus.
15.**Badges `@inkjs/ui`** : `INFO`, `WARN`, `FAIL` (texte d'erreur `#DC8ADD`) et `ASK` devant les questions.
16.**Suppression des modes et des extensions** : un seul agent MAIA (`packages/agent`), contexte de départ `MAIA.md` sinon `CLAUDE.md` (message `INFO`, ou `WARN` si aucun).
19.**Permissions granulaires** : règles `allow` / `ask` / `deny` par outil (`Outil` ou `Outil(motif)`), évaluées dans l'ordre deny > ask > allow, `ask` par défaut ; définies dans `~/.maia/settings.json` et `./.maia/settings.json` (pas le dossier parent).
20.**`/permissions`** : affiche les permissions chargées, par fichier et par type (badges ALLOW / ASK / DENY) ; les fichiers sont listés l'un sous l'autre au démarrage.
21.**Rendu markdown** des réponses de l'agent via `markdansi`, dans un seul composant isolé (`packages/ui/src/markdown.tsx`), désactivable avec `MAIA_NO_MARKDOWN=1`.
22.**Image de la bannière** : nouvelle image (visage à couronne), fond retiré, réduite à 32 × 32 en couleurs réelles (sans pixelisation ni réduction de palette), centrée verticalement à côté du titre ; données générées par `packages/ui/scripts/make-mask.py`.
23.**Barre de progression du contexte** sous la zone de saisie (1 carré = 10 000 tokens) : taille maximale demandée au LLM après la sélection du modèle, contexte utilisé mis à jour après chaque réponse (consommation renvoyée par LiteLLM, sinon estimation) ; correction d'un import de type manquant dans `ink-ui.tsx`. Valeur alignée sur le contexte réel de la session : contexte en attente compté, tokens de raisonnement exclus, estimation calibrée sur le serveur et signalée par `~`.
24.**Outils personnalisés** : `./.maia/tools/<nom>/TOOL.json` (dossier de lancement uniquement) ; paramètres `number`/`string` avec valeurs par défaut, `run` avec `{{paramètre}}` exécuté sans shell dans le dossier du tool (aucune injection possible), permission `ask` par défaut ; module `packages/agent/src/custom-tools.ts`, exemple `examples/tools/addition` ; exemple de sous-agent : `examples/agents/code_reviewer` (à copier dans `./.maia/agents`).
25.**Threading async des outils** : 1 outil au premier plan ; dès 2 outils, une tâche de fond par outil, attente de toutes les fins avant de répondre au LLM (résultats dans l'ordre demandé, 8 en parallèle au plus, verrou par fichier).
26.**Compactage** : `/compact` et compactage automatique au seuil `Max_Token_Context - (4000 + 500)` ; résumé en 5 rubriques comme contexte de départ (à la place de MAIA.md / CLAUDE.md), session renommée `xxx__n` (cycle 1..1000), barre de contexte recalculée.
27.**Correction de la barre de contexte** : comptage local avec js-tiktoken (`o200k_base`) de tout le contexte (système, outils, messages, contexte en attente), recalculé avant chaque saisie ; fin du mélange consommation du serveur / estimation calibrée (cause des variations) et du marqueur `~`.
28.**Documentation** : `SPECS_TECH.md`, spécifications techniques de toutes les fonctionnalités implémentées.
29.**Outil Write plus tolérant** : message d'erreur qui liste les paramètres reçus (tous les outils), alias de `content` acceptés, description du paramètre précisée.
30.**Appels d'outils coupés ou mal assemblés** : paramètre optionnel `MaxOutputTokens`, avertissement quand la réponse est coupée (`max_tokens`), assemblage plus tolérant des fragments d'arguments d'appels d'outils du flux SSE, journal de diagnostic `MAIA_DEBUG_LLM`.
31.**Coupure à 4096 tokens diagnostiquée** (log `MAIA_DEBUG_LLM`) : détection d'une sortie tombant sur une limite usuelle avec un appel d'outil, message d'erreur de `Write` qui conseille d'écrire en plusieurs morceaux.
32.**Outil Bash via `runShell`** : timeout et Ctrl+C arrêtent toute l'arborescence de processus (groupe de processus, SIGTERM puis SIGKILL) ; stdout et stderr fusionnés dans l'ordre d'arrivée, dernière ligne `[code de sortie : N]` ou état (délai dépassé, interrompu).
34.**Compactage sur « Input is too long »** : réponse du serveur reconnue (`isContextTooLongError`), compactage d'urgence (résultats d'outils tronqués dans la demande de résumé), reprise de la demande une fois.
35.**Barre de contexte pendant le travail de l'agent** : taille recomptée à chaque itération (après la demande, chaque réponse du LLM et chaque série de résultats d'outils) et barre affichée pendant les itérations, plus seulement à la saisie.
36.**Barre de contexte toujours affichée** : en bas de l'écran en permanence (saisie, itérations, outils, confirmations, listes) ; si la taille maximale du contexte est inconnue, seul le nombre de tokens utilisés est montré.
37.**Zone de saisie toujours visible** : grisée pendant le travail de l'agent (« Traitement en cours — Ctrl+C : interrompre »), et c'est elle qui porte les demandes d'autorisation (`ASK … (o/n)`), dans le même cadre en pointillés.
38.**Ligne d'état réservée** au-dessus de la zone de saisie (« En réflexion… », outils en cours) : toujours présente, vide hors traitement, pour que la zone de saisie ne remonte ni ne descende.