| packages | ||
| .gitignore | ||
| llm_avis.md | ||
| MAIA.md | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| screenshot.png | ||
| settings.example.json | ||
| tsconfig.base.json | ||
| tsconfig.json | ||
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
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 :
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) |
/permissions |
affiche les règles de permissions chargées, par fichier : badges ALLOW, ASK, DENY suivis des règles |
/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 <nom> · /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 <spec> (ex. litellm:gpt-4o).
Interface (Ink)
L'interface est une application React rendue par 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.
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 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
cdn'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é.
/clearl'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
@motqui 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.tsintrouvable). - 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 <nom>: renomme la session courante ; ce nom sera utilisé à la sauvegarde./load: sans argument, liste les sessions enregistrées (sélection) ;/load <nom>recharge cette session (le modèle courant est conservé)./new: nouvelle session, nouveau contexte (MAIA.md/CLAUDE.mdet 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/<nom>/) (avec $ARGUMENTS / ARGUMENTS:), sans passer par l'outil Skill du modèle.
Accès aux LLMs : LiteLLM
MAIA ne parle qu'à un serveur LiteLLM : 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é :
{
"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_URLpour envoyer une requête au format OpenAI.LiteLLM_Models: suffixe ajouté àLiteLLM_URLpour récupérer la liste des modèles disponibles.- Optionnel :
LiteLLM_API_Key(ou variable d'environnementLITELLM_API_KEY), envoyée enAuthorization: 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:<nom du modèle> (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/<nom_du_skill>/SKILL.md(ouskill.md), chargés depuis trois emplacements. Le plus spécifique l'emporte en cas de même nom :- le dossier utilisateur :
~/.maia/skills; - le dossier parent du dossier de lancement : ex.
/X/.maia/skillspour un lancement dans/X/Y; - le dossier de lancement : ex.
/X/Y/.maia/skills.
Le fichier commence par un en-tête YAML :
description(objectif du skill) etname(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'outilSkill, ou lancé directement avec§nom. - le dossier utilisateur :
---
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 :
"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 (
&&,;,|) : undenyou unasks'applique dès qu'une des parties correspond, y compris cachée dans$(…), des backticks ou des parenthèses ; unallowne s'applique que si toutes les parties sont couvertes ; - un
allowne couvre jamais une commande avec redirection (>) ou substitution ($(…), backticks) : elle repasse enask.
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.
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, formeactiveFormpour 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_lengthcaractères renvoyés). Pas de JavaScript exécuté, pas de PDF ni d'images.- Confirmation demandée par défaut ; règle
allowpossible :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é.
- Confirmation demandée par défaut ; règle
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/uiavec 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
.maiace que supprimait l'abandon de.claude: variables d'environnement et slash-commandes personnalisées (le code des commandes existe encore danspackages/agent/src/commands.ts, il n'est plus appelé). @fichierdans 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 :
- 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). - Provider SAP AI Core puis choix du modèle au démarrage et commande
/model. (Remplacé ensuite par LiteLLM, voir 12.) - Outils Glob et Grep (JavaScript pur, sans ripgrep).
- Configuration de l'agent (lot 2) : fichiers de contexte, permissions, slash-commandes, skills. (Abandonné avec les dossiers
.claude, voir 13.) §skill: appel direct d'un skill de l'agent.- Dossier de lancement : tous les chemins relatifs partent du dossier où MAIA est lancé (
MAIA_CWD>INIT_CWD>cwd). - 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
greenBrightavec spinnerarrow3, attente « En réflexion » avecdots10enyellowBright. - Outils TodoWrite et WebFetch (lot 3, première partie).
!commande(avec la syntaxe!'commande avec paramètres') et@fichier.- Renommage en MAIA (titre « Maia ») et sessions :
./.maia/.sessions, sauvegarde automatique,/rename,/load(avec réaffichage du contenu),/new. - Corrections : import manquant de
normalizeSessionName,session.toolsest uneMap, en-tête de skill avec espace avant les deux-points (description : …). - 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_Keyoptionnel). Suppression de SAP AI Core, Anthropic, OpenAI et Ollama. - Renommage technique
magiccode→maia(packages@maia/*, commandemaia, variablesMAIA_*, dossier.maia) ; textes « MagicCode » → « MAIA » ; bannière « Un seul agent, plusieurs modèles ». - Skills dans
.maia/skills(dossier de lancement, dossier parent, dossier utilisateur) ; les dossiers.claudene sont plus lus. - Badges
@inkjs/ui:INFO,WARN,FAIL(texte d'erreur#DC8ADD) etASKdevant les questions. - Suppression des modes et des extensions : un seul agent MAIA (
packages/agent), contexte de départMAIA.mdsinonCLAUDE.md(messageINFO, ouWARNsi aucun). - Outil
CurrentModel: indique au modèle quel LLM est utilisé dans la session. /clear: supprime le contexte et rechargeMAIA.md/CLAUDE.md(et les skills) comme au démarrage ;/newfait de même.- Permissions granulaires : règles
allow/ask/denypar outil (OutilouOutil(motif)), évaluées dans l'ordre deny > ask > allow,askpar défaut ; définies dans~/.maia/settings.jsonet./.maia/settings.json(pas le dossier parent). /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.
