![MAIA](screenshot.png) # MAIA Assistant IA en ligne de commande : un seul agent (MAIA), le **LLM** se choisit parmi ceux de LiteLLM, l'interface utilise **Ink** (React pour CLI). ## Démarrer ```bash pnpm install mkdir -p ~/.maia && cp settings.example.json ~/.maia/settings.json # puis renseigner l'URL de LiteLLM pnpm dev # le modèle est choisi dans la liste au démarrage pnpm dev -- --model litellm:gpt-4o # nom du modèle tel que LiteLLM le connaît pnpm test # test de la boucle agent (provider factice) pnpm build && node packages/cli/dist/index.js ``` ## Lancer MAIA depuis un autre dossier `pnpm dev` s'exécute depuis la racine du dépôt : le dossier de travail est alors cette racine (`./MAIA.md`, `./.maia/skills`… y sont cherchés). Pour utiliser MAIA sur un autre projet `./X`, lancez-le depuis `./X` : ```bash pnpm build cd packages/cli && pnpm link --global # une fois : installe la commande `maia` cd ~/X && maia # le dossier de travail est ~/X # ou sans installer : cd ~/X && node /chemin/vers/maia/packages/cli/dist/index.js # ou en développement : MAIA_CWD=~/X pnpm --dir /chemin/vers/maia dev ``` Pourquoi pas `pnpm --filter … dev` : pnpm exécute alors le script depuis `packages/cli`, pas depuis le dossier où vous avez tapé la commande. MAIA tient compte de `MAIA_CWD`, puis de `INIT_CWD` (dossier où pnpm/npm a été lancé), puis de `process.cwd()`. ## Architecture | Paquet | Rôle | | --- | --- | | `core` | Contrats (`LLMProvider`, `Tool`, `AgentUI`…), boucle agent, sessions, `!`, `@fichier`, `§skill` | | `providers` | Adaptateurs LLM : `litellm` (provider unique : requêtes au format OpenAI vers LiteLLM, voir ci-dessous) | | `ui` | Interface Ink/React (journal, saisie, listes de choix, confirmations, spinner) | | `agent` | L'agent MAIA : contexte (MAIA.md / CLAUDE.md), skills `.maia/skills` + outils Read/Glob/Grep/Write/Edit/Bash/TodoWrite/WebFetch/CurrentModel | | `cli` | Point d'entrée : configuration, boucle de conversation | Résolution du modèle (pas de défaut : s'il n'est pas défini, il est demandé) : option `--model` > variable `MAIA_MODEL` > `.maia/config.json` (projet) > `~/.maia/config.json` > défauts. Il n'y a plus de notion de mode ni d'extensions : un seul agent, MAIA. ## Commandes | Saisie | Effet | | --- | --- | | `/help` | aide (commandes, skills) | | `/model` | recharge la liste des modèles LiteLLM et change de modèle (conversation conservée) | | `/clear` | supprime le contexte de la session et recharge `MAIA.md` / `CLAUDE.md` et les skills, comme au démarrage (la session garde son nom) | | `/rename ` · `/load [nom]` · `/new` | sessions (voir plus bas) | | `/exit` (ou Ctrl+D) | quitte (la session est sauvegardée) | | `!commande` · `@fichier` · `§skill` | commande shell, fichier injecté, skill (voir plus bas) | Option de lancement : `--model ` (ex. `litellm:gpt-4o`). ## Interface (Ink) 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). Un terminal interactif (TTY) est requis. Présentation : la bannière d'accueil reprend le style « MAIA » : masque pixel-art à gauche (demi-blocs ▀▄█, 20 × 20 pixels), 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 46 colonnes pour la version complète ; entre 25 et 45 : sans le masque ; en dessous : cadre à double bordure compact, titre centré ; une ligne vide sépare chaque message. Messages du journal, avec le composant `Badge` de `@inkjs/ui` : | Badge | Usage | Texte | | --- | --- | --- | | `INFO` (bleu) | information | bleu clair (`LIGHT_BLUE`) | | `WARN` (jaune) | avertissement | par défaut | | `FAIL` (rouge) | erreur | `#DC8ADD` (`ERROR_TEXT`) | | `ASK` (vert) | question posée à l'utilisateur (liste de choix, confirmation o/n), à gauche de la question | gras | 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. 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 `!`). | Contexte | Touches | | --- | --- | | Saisie | Entrée : envoyer · ←/→, Ctrl+A/E : se déplacer · ↑/↓ : historique · Ctrl+U/W : effacer · Ctrl+C ou Ctrl+D (saisie vide) : quitter · le collage multi-lignes est conservé | | Liste (`/model`, `/load`, choix initial) | ↑/↓ (ou j/k) · Entrée : valider · Échap : annuler | | Confirmation d'une action (Write, Edit, Bash…) | `o` : autoriser · `n` ou Échap : refuser · Entrée seule est ignorée (réponse explicite exigée) | | Pendant un tour de l'agent | Ctrl+C : interrompt la requête et restaure la conversation avant cette demande | ## Dossier de base Le dossier où MAIA est lancé (ex. `./X`) est la base de **tous** les chemins relatifs : fichiers lus, créés ou modifiés par les outils (Read, Write, Edit, Glob, Grep, Bash), `.maia/config.json`, `MAIA.md` / `CLAUDE.md` et `.maia/skills/`. Un chemin complet est utilisé tel quel (`~/…` 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. ## Commande `!` : exécuter une commande shell `!commande` exécute la commande **directement**, sans passer par le modèle. Deux syntaxes : | Saisie | Commande exécutée | | --- | --- | | `!ls` · `!ls -la src` | tout ce qui suit le `!` | | `!'ls -la'` | le texte entre guillemets simples (délimite la commande avec ses paramètres) | | `!'ls' -la src` · `!'ls -la' \| wc -l` | le texte entre guillemets, puis ce qui suit est ajouté comme paramètres : `ls -la src`, `ls -la \| wc -l` | | `!'echo "l\'ami"'` | `\'` donne un guillemet simple littéral dans la commande | | `!'ls -la` | erreur (guillemet non fermé) : rien n'est exécuté | Le reste du comportement : - elle tourne dans le dossier de lancement (voir « Dossier de base ») ; un `cd` n'a donc pas d'effet durable ; - 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. ## Références `@fichier` `@chemin/vers/fichier` dans votre message **injecte le contenu du fichier** dans le contexte de la demande (en plus de le laisser dans le texte) : | Saisie | Effet | | --- | --- | | `explique @src/a.ts` | injecte `src/a.ts` | | `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). - Pas d'autocomplétion des chemins à la saisie pour l'instant. ## Sessions (`./.maia/.sessions`) 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. - **Sauvegarde automatique à la sortie** (`/exit`, Ctrl+D, SIGTERM/SIGHUP). Une session sans aucun message n'est pas enregistrée. - **Nom par défaut** : `session_{date}_{heure}_{minutes}_{secondes}` (ex. `session_2026-10-13_01_04_05`). - `/rename ` : renomme la session courante ; ce nom sera utilisé à la sauvegarde. - `/load` : sans argument, liste les sessions enregistrées (sélection) ; `/load ` recharge cette session (le modèle courant est conservé). - `/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). Chaque session est un fichier JSON : elle peut contenir des extraits de fichiers ou de sorties de commandes. `.maia/` est ajouté au `.gitignore`. ## Skills : appel direct avec `§` `§nom_du_skill [arguments]` lance un skill de l'agent (ex. `§sk_test`) ; `§` seul liste les skills disponibles. L'agent expose ses skills via `ModeInitResult.skills` (contrat `AgentSkill` : `name`, `description`, `invoke(args)`), ce qui rend l'appel indépendant du format propre à l'agent. `invoke` retourne le prompt envoyé au LLM. Ce prompt contient les instructions du `SKILL.md` (`.maia/skills//`) (avec `$ARGUMENTS` / `ARGUMENTS:`), sans passer par l'outil `Skill` du modèle. ## Accès aux LLMs : LiteLLM MAIA ne parle qu'à un serveur [LiteLLM](https://docs.litellm.ai) : **toutes les requêtes de tous les agents sont envoyées au format OpenAI** (`chat/completions`, en streaming, avec appels d'outils) vers le endpoint OpenAI de LiteLLM. Au démarrage, le fichier **`~/.maia/settings.json`** (dossier de l'utilisateur, pas celui de lancement) est chargé : ```json { "LiteLLM_URL": "http://localhost:4000", "OpenAI_EndPoint": "/v1/chat/completions", "LiteLLM_Models": "/v1/models" } ``` - `LiteLLM_URL` : URL du serveur LiteLLM. - `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. Si le fichier est absent ou incomplet, MAIA s'arrête avec un message qui en donne un exemple. Choix du modèle : au démarrage (sauf si `--model` ou `MAIA_MODEL` le définit), MAIA récupère les modèles auprès de LiteLLM et demande de choisir celui de la session. `/model` rouvre cette liste en cours de session (la conversation est conservée). Spécification explicite : `litellm:` (ou le nom seul). ## Agent MAIA : contexte et skills 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//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 : ```json "permissions": { "allow": ["Bash(allow stop *)"], "ask": ["Bash(allow ask *)"], "deny": ["Bash(allow deny *)", "Read(./.deny.md)"] } ``` 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. Exemple pour éviter les confirmations sur les outils en lecture seule : voir `settings.example.json`. ## Outils TodoWrite, WebFetch et CurrentModel - **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. - Confirmation demandée par défaut ; règle `allow` possible : `WebFetch(domain:docs.example.com)` (voir « Permissions »). - 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é. ## Reste à faire - 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). - Agent : sous-agents + outil Task, hooks, serveurs MCP. - É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é). - `@fichier` dans le corps d'une commande personnalisée ; autocomplétion des chemins `@` à la saisie. - Liste à cocher (choix multiples) : n'existe pas encore ; elle recevrait le badge `ASK`. - 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). 17. **Outil `CurrentModel`** : indique au modèle quel LLM est utilisé dans la session. 18. **`/clear`** : supprime le contexte et recharge `MAIA.md` / `CLAUDE.md` (et les skills) comme au démarrage ; `/new` fait de même. 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).