Modifications du projet
This commit is contained in:
parent
3bfd5cc629
commit
ad2cf661c3
29 changed files with 1279 additions and 56 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,4 +3,3 @@ dist
|
||||||
*.log
|
*.log
|
||||||
.env
|
.env
|
||||||
.maia
|
.maia
|
||||||
generated_page.md
|
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -68,7 +68,7 @@ Le CLI garde une façade impérative (`InkUI` : `info`, `select`, `readInput`, `
|
||||||
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).
|
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.
|
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é ;
|
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é ;
|
||||||
une ligne vide sépare chaque message.
|
une ligne vide sépare chaque message.
|
||||||
|
|
||||||
Messages du journal, avec le composant `Badge` de `@inkjs/ui` :
|
Messages du journal, avec le composant `Badge` de `@inkjs/ui` :
|
||||||
|
|
@ -80,8 +80,14 @@ Messages du journal, avec le composant `Badge` de `@inkjs/ui` :
|
||||||
| `FAIL` (rouge) | erreur | `#DC8ADD` (`ERROR_TEXT`) |
|
| `FAIL` (rouge) | erreur | `#DC8ADD` (`ERROR_TEXT`) |
|
||||||
| `ASK` (vert) | question posée à l'utilisateur (liste de choix, confirmation o/n), à gauche de la question | gras |
|
| `ASK` (vert) | question posée à l'utilisateur (liste de choix, confirmation o/n), à gauche de la question | gras |
|
||||||
|
|
||||||
|
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.
|
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é** : mis à jour après chaque réponse du LLM (donc après chaque appel d'outil intermédiaire) avec la consommation réelle renvoyée par LiteLLM (`stream_options.include_usage` : tokens de la requête + de la réponse). Sans consommation renvoyée, une estimation (≈ 4 caractères par token : prompt système, outils, messages) est utilisée. L'estimation sert aussi au démarrage, après `/clear`, `/new`, `/load` et après un tour interrompu.
|
||||||
|
|
||||||
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.
|
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 `!`).
|
À 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 `!`).
|
||||||
|
|
||||||
|
|
@ -267,6 +273,7 @@ Exemple pour éviter les confirmations sur les outils en lecture seule : voir `s
|
||||||
- É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é).
|
- É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.
|
- `@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`.
|
- Liste à cocher (choix multiples) : n'existe pas encore ; elle recevrait le badge `ASK`.
|
||||||
|
- 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é.
|
- 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).
|
- 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).
|
||||||
|
|
||||||
|
|
@ -294,3 +301,6 @@ Journal des évolutions, dans l'ordre :
|
||||||
18. **`/clear`** : supprime le contexte et recharge `MAIA.md` / `CLAUDE.md` (et les skills) comme au démarrage ; `/new` fait de même.
|
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).
|
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.
|
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`.
|
||||||
|
|
|
||||||
33
generated_page.md
Normal file
33
generated_page.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Les Mystères de l'Univers Numérique
|
||||||
|
|
||||||
|
## Architecture Décentralisée et Résilience des Systèmes
|
||||||
|
|
||||||
|
Dans le vaste paysage technologique moderne, les systèmes décentralisés représentent une avancée majeure. Ils permettent une meilleure distribution des charges et une résilience accrue face aux défaillances.
|
||||||
|
|
||||||
|
## Innovation Quantique et Calcul de Demain
|
||||||
|
|
||||||
|
La révolution quantique approche. Les ordinateurs quantiques promettent de résoudre des problèmes complexes en temps polynomial, transformant complètement nos capacités de calcul actuelles.
|
||||||
|
|
||||||
|
## Sécurité Cryptographique et Protection des Données
|
||||||
|
|
||||||
|
La cryptographie post-quantique devient essentielle. Avec l'émergence des ordinateurs quantiques, les algorithmes classiques pourraient être compromis, d'où la nécessité de nouvelles approches.
|
||||||
|
|
||||||
|
## Intelligence Artificielle et Apprentissage Profond
|
||||||
|
|
||||||
|
Les réseaux de neurones profonds continuent d'évoluer. L'IA générative ouvre des perspectives inédites dans la création, l'automatisation et l'assistance intelligente.
|
||||||
|
|
||||||
|
## Durabilité Écologique du Développement Logiciel
|
||||||
|
|
||||||
|
La conscience écologique grandit dans l'industrie tech. Optimiser les algorithmes et réduire la consommation énergétique devient une priorité stratégique.
|
||||||
|
|
||||||
|
## Collaboration Distribuée et Télétravail Technologique
|
||||||
|
|
||||||
|
Les outils collaboratifs modernes permettent des équipes vraiment distribuées. Le télétravail en tech s'accompagne d'une infrastructure logicielle sophistiquée et intuitive.
|
||||||
|
|
||||||
|
## Automatisation DevOps et Intégration Continue
|
||||||
|
|
||||||
|
Le DevOps transforme la façon dont nous livrons les applications. L'intégration continue et le déploiement continu deviennent la norme industrielle incontournable.
|
||||||
|
|
||||||
|
## Microservices et Architecture Modulaire
|
||||||
|
|
||||||
|
L'approche microservices rompt avec les monolithes. Elle offre une flexibilité accrue, une évolutivité meilleure et une maintenance simplifiée pour les grandes applications.
|
||||||
|
|
@ -6,11 +6,13 @@ import {
|
||||||
SessionStore,
|
SessionStore,
|
||||||
createSession,
|
createSession,
|
||||||
ensureMaiaHome,
|
ensureMaiaHome,
|
||||||
|
estimateSessionTokens,
|
||||||
findSkill,
|
findSkill,
|
||||||
normalizeSessionName,
|
normalizeSessionName,
|
||||||
parseShellInvocation,
|
parseShellInvocation,
|
||||||
resolveFileReferences,
|
resolveFileReferences,
|
||||||
parseSkillInvocation,
|
parseSkillInvocation,
|
||||||
|
queryContextWindow,
|
||||||
runTurn,
|
runTurn,
|
||||||
type AgentSession,
|
type AgentSession,
|
||||||
type Message,
|
type Message,
|
||||||
|
|
@ -24,6 +26,7 @@ import { runUserShell } from "./user-shell";
|
||||||
|
|
||||||
const REPLAY_RESULT_MAX = 600;
|
const REPLAY_RESULT_MAX = 600;
|
||||||
const REPLAY_USER_MAX = 1500; // les messages peuvent contenir des fichiers ou sorties de commandes joints
|
const REPLAY_USER_MAX = 1500; // les messages peuvent contenir des fichiers ou sorties de commandes joints
|
||||||
|
const CONTEXT_QUERY_TIMEOUT_MS = 60_000; // question « taille maximale du contexte » posée au LLM
|
||||||
|
|
||||||
/** Réaffiche le contenu d'une session rechargée (échanges, appels d'outils, résultats tronqués). */
|
/** Réaffiche le contenu d'une session rechargée (échanges, appels d'outils, résultats tronqués). */
|
||||||
function replayMessages(ui: InkUI, session: AgentSession, messages: Message[]): void {
|
function replayMessages(ui: InkUI, session: AgentSession, messages: Message[]): void {
|
||||||
|
|
@ -152,7 +155,51 @@ async function run(ui: InkUI, flags: { model?: string }): Promise<void> {
|
||||||
log: (m) => ui.info(m),
|
log: (m) => ui.info(m),
|
||||||
warn: (m) => ui.warn(m),
|
warn: (m) => ui.warn(m),
|
||||||
});
|
});
|
||||||
return createSession({ provider: createProvider(modelSpec, settings), init, ui, cwd });
|
const created = createSession({ provider: createProvider(modelSpec, settings), init, ui, cwd });
|
||||||
|
refreshContext(created); // le contexte de départ (prompt système, MAIA.md, outils) occupe déjà des tokens
|
||||||
|
return created;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Recalcule (estimation) la taille du contexte et met à jour la barre : nouvelle conversation, session rechargée, tour interrompu. */
|
||||||
|
const refreshContext = (target: AgentSession): void => {
|
||||||
|
target.contextTokens = estimateSessionTokens(target);
|
||||||
|
ui.onContextUsage(target.contextTokens);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tailles maximales de contexte déjà demandées au LLM (une seule question par modèle et par exécution)
|
||||||
|
const contextWindows = new Map<string, number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Après la sélection d'un modèle : demande au LLM la taille maximale de son contexte (en tokens) et la donne à la barre
|
||||||
|
* de progression. Si la réponse est introuvable ou illisible, la barre est masquée (l'application continue).
|
||||||
|
*/
|
||||||
|
const resolveContextWindow = async (spec: string): Promise<void> => {
|
||||||
|
let max = contextWindows.get(spec) ?? null;
|
||||||
|
if (max === null) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), CONTEXT_QUERY_TIMEOUT_MS);
|
||||||
|
ui.setInterruptHandler(() => controller.abort()); // Ctrl+C abandonne la question
|
||||||
|
ui.setBusy("Recherche de la taille maximale du contexte…");
|
||||||
|
try {
|
||||||
|
const { tokens, answer } = await queryContextWindow(createProvider(spec, settings), controller.signal);
|
||||||
|
if (tokens === null) {
|
||||||
|
const shown = answer.length > 120 ? `${answer.slice(0, 120)}…` : answer;
|
||||||
|
ui.warn(`Taille maximale du contexte non déterminée (réponse du modèle : « ${shown} »). Barre de progression masquée.`);
|
||||||
|
} else {
|
||||||
|
max = tokens;
|
||||||
|
contextWindows.set(spec, tokens);
|
||||||
|
ui.info(`Taille maximale du contexte : ${tokens.toLocaleString("fr-FR")} tokens (réponse du modèle)`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const why = controller.signal.aborted ? "question abandonnée (délai dépassé ou Ctrl+C)" : (err as Error).message;
|
||||||
|
ui.warn(`Taille maximale du contexte indisponible : ${why}. Barre de progression masquée.`);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
ui.setBusy(null);
|
||||||
|
ui.setInterruptHandler(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.setContextMax(max);
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.intro("Maia");
|
ui.intro("Maia");
|
||||||
|
|
@ -170,8 +217,10 @@ async function run(ui: InkUI, flags: { model?: string }): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Modèle de la session : explicite (flag, variable d'environnement, config) sinon choisi par l'utilisateur
|
// 3. Modèle de la session : explicite (flag, variable d'environnement, config) sinon choisi par l'utilisateur
|
||||||
let modelSpec = config.model;
|
let modelSpec: string;
|
||||||
if (!modelSpec) {
|
if (config.model) {
|
||||||
|
modelSpec = config.model;
|
||||||
|
} else {
|
||||||
const picked = await pickModel();
|
const picked = await pickModel();
|
||||||
if (!picked) {
|
if (!picked) {
|
||||||
ui.outro("Aucun modèle choisi");
|
ui.outro("Aucun modèle choisi");
|
||||||
|
|
@ -190,6 +239,7 @@ async function run(ui: InkUI, flags: { model?: string }): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ui.info(`Modèle : ${modelSpec}`);
|
ui.info(`Modèle : ${modelSpec}`);
|
||||||
|
await resolveContextWindow(modelSpec);
|
||||||
|
|
||||||
// Sauvegarde de la session courante (silencieuse si elle est vide ou inchangée). Retourne son nom si écrite.
|
// Sauvegarde de la session courante (silencieuse si elle est vide ou inchangée). Retourne son nom si écrite.
|
||||||
const saveSession = (): string | null => {
|
const saveSession = (): string | null => {
|
||||||
|
|
@ -351,6 +401,7 @@ async function run(ui: InkUI, flags: { model?: string }): Promise<void> {
|
||||||
session.pendingContext.length = 0;
|
session.pendingContext.length = 0;
|
||||||
session.pendingContext.push(...saved.pendingContext);
|
session.pendingContext.push(...saved.pendingContext);
|
||||||
sessions.restore(saved);
|
sessions.restore(saved);
|
||||||
|
refreshContext(session);
|
||||||
ui.info(`Session chargée : ${saved.name} (${saved.messages.length} messages · modèle ${modelSpec})`);
|
ui.info(`Session chargée : ${saved.name} (${saved.messages.length} messages · modèle ${modelSpec})`);
|
||||||
if (session.pendingContext.length > 0) {
|
if (session.pendingContext.length > 0) {
|
||||||
ui.info(`Contexte en attente (commandes « ! ») : ${session.pendingContext.length} élément(s), joint(s) à votre prochaine demande.`);
|
ui.info(`Contexte en attente (commandes « ! ») : ${session.pendingContext.length} élément(s), joint(s) à votre prochaine demande.`);
|
||||||
|
|
@ -378,6 +429,7 @@ async function run(ui: InkUI, flags: { model?: string }): Promise<void> {
|
||||||
session.provider = createProvider(choice, settings);
|
session.provider = createProvider(choice, settings);
|
||||||
modelSpec = choice;
|
modelSpec = choice;
|
||||||
ui.info(`Modèle actif : ${modelSpec}`);
|
ui.info(`Modèle actif : ${modelSpec}`);
|
||||||
|
await resolveContextWindow(modelSpec);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ui.error((err as Error).message);
|
ui.error((err as Error).message);
|
||||||
}
|
}
|
||||||
|
|
@ -418,6 +470,7 @@ async function run(ui: InkUI, flags: { model?: string }): Promise<void> {
|
||||||
if (controller.signal.aborted) {
|
if (controller.signal.aborted) {
|
||||||
session.messages.length = mark; // évite un historique incohérent (appel d'outil sans résultat)
|
session.messages.length = mark; // évite un historique incohérent (appel d'outil sans résultat)
|
||||||
session.pendingContext = contextBackup; // le contexte des commandes « ! » n'est pas perdu
|
session.pendingContext = contextBackup; // le contexte des commandes « ! » n'est pas perdu
|
||||||
|
refreshContext(session);
|
||||||
ui.warn("Interrompu (Ctrl+C) : la conversation est restaurée avant cette demande.");
|
ui.warn("Interrompu (Ctrl+C) : la conversation est restaurée avant cette demande.");
|
||||||
} else {
|
} else {
|
||||||
ui.error((err as Error).message);
|
ui.error((err as Error).message);
|
||||||
|
|
|
||||||
|
|
@ -122,3 +122,56 @@ describe("contexte en attente", () => {
|
||||||
expect(session.pendingContext).toEqual([]);
|
expect(session.pendingContext).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("taille du contexte", () => {
|
||||||
|
const echo: Tool = {
|
||||||
|
spec: { name: "echo", description: "echo", inputSchema: { type: "object" } },
|
||||||
|
permission: "auto",
|
||||||
|
execute: async () => "ok",
|
||||||
|
};
|
||||||
|
|
||||||
|
it("utilise la consommation renvoyée par le serveur après chaque réponse", async () => {
|
||||||
|
const seen: number[] = [];
|
||||||
|
let call = 0;
|
||||||
|
const provider: LLMProvider = {
|
||||||
|
id: "fake",
|
||||||
|
model: "fake",
|
||||||
|
capabilities: { tools: true },
|
||||||
|
async *chat(): AsyncIterable<StreamEvent> {
|
||||||
|
call++;
|
||||||
|
if (call === 1) {
|
||||||
|
yield { type: "tool_use", id: "t1", name: "echo", input: {} };
|
||||||
|
yield { type: "usage", inputTokens: 1000, outputTokens: 50 };
|
||||||
|
} else {
|
||||||
|
yield { type: "text_delta", text: "fin" };
|
||||||
|
yield { type: "usage", inputTokens: 1200, outputTokens: 30 };
|
||||||
|
}
|
||||||
|
yield { type: "done", stopReason: call === 1 ? "tool_use" : "end_turn" };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const session = createSession({
|
||||||
|
provider,
|
||||||
|
init: { systemPrompt: "t", tools: [echo] },
|
||||||
|
ui: { ...silentUI, onContextUsage: (n) => seen.push(n) },
|
||||||
|
cwd: process.cwd(),
|
||||||
|
});
|
||||||
|
await runTurn(session, "go");
|
||||||
|
expect(seen).toEqual([1050, 1230]);
|
||||||
|
expect(session.contextTokens).toBe(1230);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sans consommation renvoyée : estimation à partir de la conversation", async () => {
|
||||||
|
const seen: number[] = [];
|
||||||
|
const session = createSession({
|
||||||
|
provider: fakeProvider("echo"),
|
||||||
|
init: { systemPrompt: "x".repeat(4000), tools: [echo] },
|
||||||
|
ui: { ...silentUI, onContextUsage: (n) => seen.push(n) },
|
||||||
|
cwd: process.cwd(),
|
||||||
|
});
|
||||||
|
await runTurn(session, "go");
|
||||||
|
expect(seen).toHaveLength(2);
|
||||||
|
expect(seen[0]).toBeGreaterThanOrEqual(1000); // prompt système de 4000 caractères
|
||||||
|
expect(seen[1]).toBeGreaterThan(seen[0]!);
|
||||||
|
expect(session.contextTokens).toBe(seen[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { estimateTokens } from "./context";
|
||||||
import type {
|
import type {
|
||||||
AgentSkill,
|
AgentSkill,
|
||||||
AgentUI,
|
AgentUI,
|
||||||
|
|
@ -22,6 +23,8 @@ export interface AgentSession {
|
||||||
permissionSources: PermissionSource[];
|
permissionSources: PermissionSource[];
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
|
/** Taille du contexte (tokens) après la dernière réponse du LLM : valeur renvoyée par le serveur, sinon estimation. */
|
||||||
|
contextTokens: number;
|
||||||
/** Contexte ajouté par l'utilisateur hors conversation (ex. résultat d'une commande "!"), joint à sa prochaine demande. */
|
/** Contexte ajouté par l'utilisateur hors conversation (ex. résultat d'une commande "!"), joint à sa prochaine demande. */
|
||||||
pendingContext: string[];
|
pendingContext: string[];
|
||||||
ui: AgentUI;
|
ui: AgentUI;
|
||||||
|
|
@ -44,6 +47,7 @@ export function createSession(opts: {
|
||||||
permissionSources: opts.init.permissionSources ?? [],
|
permissionSources: opts.init.permissionSources ?? [],
|
||||||
env: opts.init.env,
|
env: opts.init.env,
|
||||||
messages: [],
|
messages: [],
|
||||||
|
contextTokens: 0,
|
||||||
pendingContext: [],
|
pendingContext: [],
|
||||||
ui: opts.ui,
|
ui: opts.ui,
|
||||||
cwd: opts.cwd,
|
cwd: opts.cwd,
|
||||||
|
|
@ -69,6 +73,7 @@ export async function runTurn(
|
||||||
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
||||||
const assistant: ContentBlock[] = [];
|
const assistant: ContentBlock[] = [];
|
||||||
let text = "";
|
let text = "";
|
||||||
|
let usedTokens: number | null = null;
|
||||||
|
|
||||||
ui.onThinking(true);
|
ui.onThinking(true);
|
||||||
let first = true;
|
let first = true;
|
||||||
|
|
@ -90,6 +95,9 @@ export async function runTurn(
|
||||||
ui.onText(ev.text);
|
ui.onText(ev.text);
|
||||||
} else if (ev.type === "tool_use") {
|
} else if (ev.type === "tool_use") {
|
||||||
assistant.push({ type: "tool_use", id: ev.id, name: ev.name, input: ev.input });
|
assistant.push({ type: "tool_use", id: ev.id, name: ev.name, input: ev.input });
|
||||||
|
} else if (ev.type === "usage") {
|
||||||
|
// Requête + réponse : c'est la taille du contexte une fois la réponse ajoutée à la conversation
|
||||||
|
usedTokens = ev.inputTokens + ev.outputTokens;
|
||||||
}
|
}
|
||||||
// "done" : la fin du flux suffit, les appels d'outils décident de la suite
|
// "done" : la fin du flux suffit, les appels d'outils décident de la suite
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +110,8 @@ export async function runTurn(
|
||||||
assistant.unshift({ type: "text", text });
|
assistant.unshift({ type: "text", text });
|
||||||
}
|
}
|
||||||
session.messages.push({ role: "assistant", content: assistant });
|
session.messages.push({ role: "assistant", content: assistant });
|
||||||
|
session.contextTokens = usedTokens ?? estimateSessionTokens(session);
|
||||||
|
ui.onContextUsage?.(session.contextTokens);
|
||||||
|
|
||||||
const calls = assistant.filter((b): b is ToolUseBlock => b.type === "tool_use");
|
const calls = assistant.filter((b): b is ToolUseBlock => b.type === "tool_use");
|
||||||
if (calls.length === 0) return;
|
if (calls.length === 0) return;
|
||||||
|
|
@ -161,3 +171,8 @@ async function executeToolCall(
|
||||||
return result(msg, true);
|
return result(msg, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Estimation de la taille du contexte d'une session (sans réponse du serveur) : prompt système, outils et messages. */
|
||||||
|
export function estimateSessionTokens(session: Pick<AgentSession, "system" | "messages" | "tools">): number {
|
||||||
|
return estimateTokens(session.system, session.messages, [...session.tools.values()].map((t) => t.spec));
|
||||||
|
}
|
||||||
|
|
|
||||||
72
packages/core/src/context.test.ts
Normal file
72
packages/core/src/context.test.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { CONTEXT_WINDOW_QUESTION, estimateTokens, parseTokenCount, queryContextWindow } from "./context";
|
||||||
|
import type { LLMProvider, StreamEvent } from "./types";
|
||||||
|
|
||||||
|
describe("parseTokenCount", () => {
|
||||||
|
it.each([
|
||||||
|
["128000", 128_000],
|
||||||
|
["128 000", 128_000],
|
||||||
|
["128 000 tokens", 128_000],
|
||||||
|
["131,072", 131_072],
|
||||||
|
["1.000.000", 1_000_000],
|
||||||
|
["200k", 200_000],
|
||||||
|
["200K tokens", 200_000],
|
||||||
|
["1M", 1_000_000],
|
||||||
|
["1,5 million de tokens", 1_500_000],
|
||||||
|
["2 millions", 2_000_000],
|
||||||
|
["1.5M", 1_500_000],
|
||||||
|
["Environ 128 000 tokens.", 128_000],
|
||||||
|
])("%s → %d", (text, expected) => {
|
||||||
|
expect(parseTokenCount(text)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignore les nombres du nom du modèle", () => {
|
||||||
|
expect(parseTokenCount("GPT-4o a un contexte de 128 000 tokens")).toBe(128_000);
|
||||||
|
expect(parseTokenCount("Claude 3.5 Sonnet : 200k tokens")).toBe(200_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("une valeur avec unité l'emporte sur une valeur plus grande sans unité", () => {
|
||||||
|
expect(parseTokenCount("sortie 16 384 ; contexte 128k tokens ; ref 5000000")).toBe(128_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sans valeur exploitable → null", () => {
|
||||||
|
expect(parseTokenCount("Je ne sais pas.")).toBeNull();
|
||||||
|
expect(parseTokenCount("")).toBeNull();
|
||||||
|
expect(parseTokenCount("42")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("estimateTokens", () => {
|
||||||
|
it("≈ 4 caractères par token, outils compris", () => {
|
||||||
|
expect(estimateTokens("a".repeat(400), [])).toBeGreaterThanOrEqual(100);
|
||||||
|
const without = estimateTokens("s", []);
|
||||||
|
const withTools = estimateTokens("s", [], [{ name: "T", description: "d".repeat(400), inputSchema: {} }]);
|
||||||
|
expect(withTools).toBeGreaterThan(without + 90);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("queryContextWindow", () => {
|
||||||
|
const provider = (reply: string, seen: { text?: string } = {}): LLMProvider => ({
|
||||||
|
id: "fake",
|
||||||
|
model: "fake",
|
||||||
|
capabilities: { tools: false },
|
||||||
|
async *chat(req): AsyncIterable<StreamEvent> {
|
||||||
|
seen.text = JSON.stringify(req.messages);
|
||||||
|
yield { type: "text_delta", text: reply.slice(0, 3) };
|
||||||
|
yield { type: "text_delta", text: reply.slice(3) };
|
||||||
|
yield { type: "done", stopReason: "end_turn" };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pose la question et lit le nombre de tokens", async () => {
|
||||||
|
const seen: { text?: string } = {};
|
||||||
|
const r = await queryContextWindow(provider("200 000 tokens", seen));
|
||||||
|
expect(r).toEqual({ tokens: 200_000, answer: "200 000 tokens" });
|
||||||
|
expect(seen.text).toContain(CONTEXT_WINDOW_QUESTION);
|
||||||
|
expect(CONTEXT_WINDOW_QUESTION).toMatch(/Taille maximale du contexte pour ce modèle/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("réponse illisible : tokens null, réponse conservée", async () => {
|
||||||
|
expect(await queryContextWindow(provider("Aucune idée"))).toEqual({ tokens: null, answer: "Aucune idée" });
|
||||||
|
});
|
||||||
|
});
|
||||||
77
packages/core/src/context.ts
Normal file
77
packages/core/src/context.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Taille du contexte : estimation locale, lecture d'une taille en tokens, question posée au LLM.
|
||||||
|
import type { LLMProvider, Message, ToolSpec } from "./types";
|
||||||
|
|
||||||
|
/** Estimation grossière (≈ 4 caractères par token) quand le serveur ne renvoie pas la consommation réelle. */
|
||||||
|
export function estimateTokens(system: string, messages: Message[], tools: ToolSpec[] = []): number {
|
||||||
|
const chars = system.length + JSON.stringify(messages).length + (tools.length > 0 ? JSON.stringify(tools).length : 0);
|
||||||
|
return Math.ceil(chars / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Question posée au LLM après le choix du modèle (la consigne finale facilite la lecture de la réponse). */
|
||||||
|
export const CONTEXT_WINDOW_QUESTION =
|
||||||
|
"Taille maximale du contexte pour ce modèle, en tokens ? Réponds uniquement par un nombre entier, sans autre texte.";
|
||||||
|
|
||||||
|
const MIN_CONTEXT = 1_000;
|
||||||
|
const MAX_CONTEXT = 100_000_000;
|
||||||
|
|
||||||
|
const MULTIPLIERS: Record<string, number> = {
|
||||||
|
k: 1_000,
|
||||||
|
mille: 1_000,
|
||||||
|
m: 1_000_000,
|
||||||
|
million: 1_000_000,
|
||||||
|
millions: 1_000_000,
|
||||||
|
milliard: 1_000_000_000,
|
||||||
|
milliards: 1_000_000_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 128 000 · 128,000 · 128.000 · 1.5 · 1,5 · 200000 — suivi éventuel d'un multiplicateur (k, M, mille, million…) ou de « token »
|
||||||
|
const NUMBER = /(\d{1,3}(?:[ ., ]\d{3})+(?!\d)|\d+(?:[.,]\d+)?)\s*(milliards?|millions?|mille|[kKmM](?![A-Za-zÀ-ÿ])|(?=tokens?\b))?/gi;
|
||||||
|
|
||||||
|
function toNumber(raw: string): number {
|
||||||
|
if (/^\d{1,3}([ ., ]\d{3})+$/.test(raw)) return Number(raw.replace(/[ ., ]/g, ""));
|
||||||
|
return Number(raw.replace(",", "."));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait une taille de contexte (en tokens) de la réponse d'un LLM : « 128000 », « 128 000 tokens », « 200k », « 1M »,
|
||||||
|
* « 1,5 million »… Une valeur avec unité l'emporte ; sinon la plus grande valeur plausible (1 000 à 100 000 000).
|
||||||
|
* Retourne null si rien d'exploitable.
|
||||||
|
*/
|
||||||
|
export function parseTokenCount(text: string): number | null {
|
||||||
|
let best: { value: number; unit: boolean } | null = null;
|
||||||
|
for (const m of text.matchAll(NUMBER)) {
|
||||||
|
const base = toNumber(m[1] ?? "");
|
||||||
|
const suffix = (m[2] ?? "").toLowerCase();
|
||||||
|
const value = Math.round(base * (MULTIPLIERS[suffix] ?? 1));
|
||||||
|
if (!Number.isFinite(value) || value < MIN_CONTEXT || value > MAX_CONTEXT) continue;
|
||||||
|
const unit = suffix !== "" || /^\s*tokens?\b/i.test(text.slice((m.index ?? 0) + m[0].length));
|
||||||
|
if (!best || (unit && !best.unit) || (unit === best.unit && value > best.value)) best = { value, unit };
|
||||||
|
}
|
||||||
|
return best ? best.value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextWindowAnswer {
|
||||||
|
/** Taille maximale en tokens, null si la réponse n'a pas pu être lue. */
|
||||||
|
tokens: number | null;
|
||||||
|
/** Réponse brute du LLM (utile pour informer l'utilisateur en cas d'échec). */
|
||||||
|
answer: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Demande au LLM la taille maximale de son contexte. Lève une erreur si l'appel échoue (réseau, HTTP, annulation). */
|
||||||
|
export async function queryContextWindow(
|
||||||
|
provider: LLMProvider,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<ContextWindowAnswer> {
|
||||||
|
let answer = "";
|
||||||
|
for await (const ev of provider.chat(
|
||||||
|
{
|
||||||
|
system: "Tu réponds de façon brève et exacte.",
|
||||||
|
messages: [{ role: "user", content: [{ type: "text", text: CONTEXT_WINDOW_QUESTION }] }],
|
||||||
|
tools: [],
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
)) {
|
||||||
|
if (ev.type === "text_delta") answer += ev.text;
|
||||||
|
}
|
||||||
|
return { tokens: parseTokenCount(answer), answer: answer.trim() };
|
||||||
|
}
|
||||||
|
|
@ -4,3 +4,4 @@ export * from "./skills";
|
||||||
export * from "./shell";
|
export * from "./shell";
|
||||||
export * from "./fileref";
|
export * from "./fileref";
|
||||||
export * from "./sessions";
|
export * from "./sessions";
|
||||||
|
export * from "./context";
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ export type StopReason = "end_turn" | "tool_use" | "max_tokens" | "other";
|
||||||
export type StreamEvent =
|
export type StreamEvent =
|
||||||
| { type: "text_delta"; text: string }
|
| { type: "text_delta"; text: string }
|
||||||
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }
|
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }
|
||||||
|
/** Consommation de la requête, quand le serveur la renvoie (avant "done"). */
|
||||||
|
| { type: "usage"; inputTokens: number; outputTokens: number }
|
||||||
| { type: "done"; stopReason: StopReason };
|
| { type: "done"; stopReason: StopReason };
|
||||||
|
|
||||||
export interface LLMProvider {
|
export interface LLMProvider {
|
||||||
|
|
@ -89,6 +91,8 @@ export interface AgentUI {
|
||||||
onToolCall(name: string, summary: string): void;
|
onToolCall(name: string, summary: string): void;
|
||||||
onToolResult(name: string, output: string, isError: boolean): void;
|
onToolResult(name: string, output: string, isError: boolean): void;
|
||||||
confirm(message: string): Promise<boolean>;
|
confirm(message: string): Promise<boolean>;
|
||||||
|
/** Taille courante du contexte (en tokens), après chaque réponse du LLM. */
|
||||||
|
onContextUsage?(tokens: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Initialisation de l'agent ----------
|
// ---------- Initialisation de l'agent ----------
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ describe("format OpenAI", () => {
|
||||||
{ role: "user", content: [{ type: "text", text: "suite" }, { type: "tool_result", toolUseId: "c1", content: "R" }] },
|
{ role: "user", content: [{ type: "text", text: "suite" }, { type: "tool_result", toolUseId: "c1", content: "R" }] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(body).toMatchObject({ model: "m", stream: true, max_tokens: 100 });
|
expect(body).toMatchObject({ model: "m", stream: true, max_tokens: 100, stream_options: { include_usage: true } });
|
||||||
const msgs = body.messages as { role: string }[];
|
const msgs = body.messages as { role: string }[];
|
||||||
expect(msgs.map((m) => m.role)).toEqual(["system", "user", "assistant", "tool", "user"]);
|
expect(msgs.map((m) => m.role)).toEqual(["system", "user", "assistant", "tool", "user"]);
|
||||||
expect(msgs[2]).toMatchObject({ tool_calls: [{ id: "c1", function: { name: "Read", arguments: '{"a":1}' } }] });
|
expect(msgs[2]).toMatchObject({ tool_calls: [{ id: "c1", function: { name: "Read", arguments: '{"a":1}' } }] });
|
||||||
|
|
@ -95,6 +95,18 @@ describe("format OpenAI", () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("streaming : consommation (tokens) lue dans le dernier morceau, avant « done »", async () => {
|
||||||
|
const raw =
|
||||||
|
sse({ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }] }) +
|
||||||
|
sse({ choices: [], usage: { prompt_tokens: 1200, completion_tokens: 30, total_tokens: 1230 } }) +
|
||||||
|
"data: [DONE]\n\n";
|
||||||
|
expect(await collect(parseSse(chunks(raw)))).toEqual([
|
||||||
|
{ type: "text_delta", text: "ok" },
|
||||||
|
{ type: "usage", inputTokens: 1200, outputTokens: 30 },
|
||||||
|
{ type: "done", stopReason: "end_turn" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("erreur renvoyée dans le flux", async () => {
|
it("erreur renvoyée dans le flux", async () => {
|
||||||
await expect(collect(parseSse(chunks(sse({ error: { message: "boom" } }))))).rejects.toThrow(/boom/);
|
await expect(collect(parseSse(chunks(sse({ error: { message: "boom" } }))))).rejects.toThrow(/boom/);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ export function buildChatBody(model: string, req: ChatRequest): Record<string, u
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const body: Record<string, unknown> = { model, messages, stream: true };
|
// include_usage : le serveur renvoie la consommation réelle (tokens) dans le dernier morceau du flux
|
||||||
|
const body: Record<string, unknown> = { model, messages, stream: true, stream_options: { include_usage: true } };
|
||||||
if (req.maxTokens) body.max_tokens = req.maxTokens;
|
if (req.maxTokens) body.max_tokens = req.maxTokens;
|
||||||
if (req.tools.length > 0) {
|
if (req.tools.length > 0) {
|
||||||
body.tools = req.tools.map((t) => ({
|
body.tools = req.tools.map((t) => ({
|
||||||
|
|
@ -39,6 +40,7 @@ export function buildChatBody(model: string, req: ChatRequest): Record<string, u
|
||||||
|
|
||||||
interface Chunk {
|
interface Chunk {
|
||||||
error?: { message?: string };
|
error?: { message?: string };
|
||||||
|
usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number } | null;
|
||||||
choices?: {
|
choices?: {
|
||||||
finish_reason?: string | null;
|
finish_reason?: string | null;
|
||||||
delta?: {
|
delta?: {
|
||||||
|
|
@ -60,6 +62,8 @@ export async function* parseSse(body: AsyncIterable<Uint8Array>): AsyncGenerator
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
const calls = new Map<number, { id: string; name: string; args: string }>();
|
const calls = new Map<number, { id: string; name: string; args: string }>();
|
||||||
let finish: string | null | undefined;
|
let finish: string | null | undefined;
|
||||||
|
// objet plutôt que variable : l'assignation se fait dans un générateur imbriqué (TypeScript ne la verrait pas)
|
||||||
|
const seen: { usage: { inputTokens: number; outputTokens: number } | null } = { usage: null };
|
||||||
let buffer = "";
|
let buffer = "";
|
||||||
|
|
||||||
function* handleLine(line: string): Generator<StreamEvent> {
|
function* handleLine(line: string): Generator<StreamEvent> {
|
||||||
|
|
@ -73,6 +77,12 @@ export async function* parseSse(body: AsyncIterable<Uint8Array>): AsyncGenerator
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (chunk.error) throw new Error(`Erreur du serveur LiteLLM : ${chunk.error.message ?? JSON.stringify(chunk.error)}`);
|
if (chunk.error) throw new Error(`Erreur du serveur LiteLLM : ${chunk.error.message ?? JSON.stringify(chunk.error)}`);
|
||||||
|
// Le morceau d'usage arrive en dernier, avec une liste `choices` vide
|
||||||
|
if (chunk.usage && typeof chunk.usage.prompt_tokens === "number") {
|
||||||
|
const input = chunk.usage.prompt_tokens;
|
||||||
|
const output = chunk.usage.completion_tokens ?? Math.max(0, (chunk.usage.total_tokens ?? input) - input);
|
||||||
|
seen.usage = { inputTokens: input, outputTokens: output };
|
||||||
|
}
|
||||||
const choice = chunk.choices?.[0];
|
const choice = chunk.choices?.[0];
|
||||||
if (!choice) return;
|
if (!choice) return;
|
||||||
const delta = choice.delta;
|
const delta = choice.delta;
|
||||||
|
|
@ -111,5 +121,6 @@ export async function* parseSse(body: AsyncIterable<Uint8Array>): AsyncGenerator
|
||||||
}
|
}
|
||||||
yield { type: "tool_use", id: call.id || `call_${++n}`, name: call.name, input };
|
yield { type: "tool_use", id: call.id || `call_${++n}`, name: call.name, input };
|
||||||
}
|
}
|
||||||
|
if (seen.usage) yield { type: "usage", ...seen.usage };
|
||||||
yield { type: "done", stopReason: stopReason(finish, calls.size > 0) };
|
yield { type: "done", stopReason: stopReason(finish, calls.size > 0) };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
packages/ui/assets/mask.png
Normal file
BIN
packages/ui/assets/mask.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
|
|
@ -12,6 +12,7 @@
|
||||||
"@maia/core": "workspace:*",
|
"@maia/core": "workspace:*",
|
||||||
"cli-spinners": "^2.9.2",
|
"cli-spinners": "^2.9.2",
|
||||||
"ink": "^7.0.3",
|
"ink": "^7.0.3",
|
||||||
|
"markdansi": "^0.2.0",
|
||||||
"react": "^19.2.0"
|
"react": "^19.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
65
packages/ui/scripts/make-mask.py
Normal file
65
packages/ui/scripts/make-mask.py
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Génère src/mask-data.ts à partir de assets/mask.png (nécessite Pillow : pip install pillow).
|
||||||
|
|
||||||
|
Usage : python3 scripts/make-mask.py [taille] (32 par défaut ; l'image affichée fait taille × taille pixels,
|
||||||
|
soit taille colonnes × taille/2 lignes de terminal)
|
||||||
|
|
||||||
|
Le fond uni (couleur des coins) est rendu transparent, l'image est recadrée au carré puis réduite
|
||||||
|
en moyennant les couleurs : aucune pixelisation par blocs, aucune réduction de palette.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from collections import deque
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
N = int(sys.argv[1]) if len(sys.argv) > 1 else 32
|
||||||
|
root = Path(__file__).resolve().parent.parent
|
||||||
|
im = Image.open(root / "assets" / "mask.png").convert("RGB")
|
||||||
|
W, H = im.size
|
||||||
|
px = im.load()
|
||||||
|
bg = px[0, 0]
|
||||||
|
near = lambda c: all(abs(c[i] - bg[i]) <= 12 for i in range(3))
|
||||||
|
|
||||||
|
# Fond = zone connectée aux coins et proche de la couleur du fond (le visage crème est isolé par son contour noir).
|
||||||
|
transparent = [[False] * W for _ in range(H)]
|
||||||
|
queue = deque([(0, 0), (W - 1, 0), (0, H - 1), (W - 1, H - 1)])
|
||||||
|
while queue:
|
||||||
|
x, y = queue.popleft()
|
||||||
|
if x < 0 or y < 0 or x >= W or y >= H or transparent[y][x] or not near(px[x, y]):
|
||||||
|
continue
|
||||||
|
transparent[y][x] = True
|
||||||
|
queue.extend([(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)])
|
||||||
|
|
||||||
|
xs = [x for y in range(H) for x in range(W) if not transparent[y][x]]
|
||||||
|
ys = [y for y in range(H) for x in range(W) if not transparent[y][x]]
|
||||||
|
x0, x1, y0, y1 = min(xs), max(xs) + 1, min(ys), max(ys) + 1
|
||||||
|
side = max(x1 - x0, y1 - y0)
|
||||||
|
ox = x0 - (side - (x1 - x0)) // 2
|
||||||
|
oy = y0 - (side - (y1 - y0)) // 2
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for gy in range(N):
|
||||||
|
row = ""
|
||||||
|
for gx in range(N):
|
||||||
|
total = opaque = 0
|
||||||
|
acc = [0, 0, 0]
|
||||||
|
for y in range(int(oy + gy * side / N), int(oy + (gy + 1) * side / N) + 1):
|
||||||
|
for x in range(int(ox + gx * side / N), int(ox + (gx + 1) * side / N) + 1):
|
||||||
|
total += 1
|
||||||
|
if 0 <= x < W and 0 <= y < H and not transparent[y][x]:
|
||||||
|
opaque += 1
|
||||||
|
for i in range(3):
|
||||||
|
acc[i] += px[x, y][i]
|
||||||
|
# moyenne des pixels opaques uniquement (évite le liseré clair du fond) ; transparent si moins de la moitié est opaque
|
||||||
|
row += "......" if opaque * 2 < total or opaque == 0 else "".join("%02x" % round(a / opaque) for a in acc)
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
lines = "\n".join(f' "{r}",' for r in rows)
|
||||||
|
(root / "src" / "mask-data.ts").write_text(
|
||||||
|
"// Fichier généré par scripts/make-mask.py à partir de assets/mask.png : ne pas modifier à la main.\n"
|
||||||
|
"// Une chaîne par ligne de pixels ; chaque pixel = 6 chiffres hexadécimaux rrggbb, \"......\" = transparent.\n"
|
||||||
|
f"export const MASK_SIZE = {N};\n\n"
|
||||||
|
f"export const MASK_DATA: string[] = [\n{lines}\n];\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
print(f"{N}x{N} écrit dans src/mask-data.ts")
|
||||||
|
|
@ -90,7 +90,7 @@ export function Banner({ title }: { title: string }) {
|
||||||
|
|
||||||
if (columns < MASK_WIDTH + GAP + titleWidth(title)) return right;
|
if (columns < MASK_WIDTH + GAP + titleWidth(title)) return right;
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box alignItems="center">
|
||||||
<PixelBlock lines={MASK_LINES} />
|
<PixelBlock lines={MASK_LINES} />
|
||||||
<Box marginLeft={GAP}>{right}</Box>
|
<Box marginLeft={GAP}>{right}</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { Badge } from "@inkjs/ui";
|
||||||
import { Box, Static, Text, useInput } from "ink";
|
import { Box, Static, Text, useInput } from "ink";
|
||||||
import { Banner } from "./banner";
|
import { Banner } from "./banner";
|
||||||
import { ERROR_TEXT, LIGHT_BLUE } from "./colors";
|
import { ERROR_TEXT, LIGHT_BLUE } from "./colors";
|
||||||
|
import { ContextBar } from "./context-bar";
|
||||||
|
import { Markdown } from "./markdown";
|
||||||
import type {
|
import type {
|
||||||
ConfirmPromptState,
|
ConfirmPromptState,
|
||||||
InputPromptState,
|
InputPromptState,
|
||||||
|
|
@ -12,6 +14,7 @@ import type {
|
||||||
PermissionFile,
|
PermissionFile,
|
||||||
PromptState,
|
PromptState,
|
||||||
SelectPromptState,
|
SelectPromptState,
|
||||||
|
UIState,
|
||||||
UIStore,
|
UIStore,
|
||||||
} from "./store";
|
} from "./store";
|
||||||
|
|
||||||
|
|
@ -108,7 +111,8 @@ function LogLine({ item }: { item: LogItem }) {
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
case "assistant":
|
case "assistant":
|
||||||
return <Text>{item.text}</Text>;
|
// Réponse finale rendue en markdown (voir markdown.tsx) ; le texte en cours de streaming reste brut.
|
||||||
|
return <Markdown text={item.text} />;
|
||||||
case "permissions":
|
case "permissions":
|
||||||
return <PermissionsView files={item.permissions ?? []} />;
|
return <PermissionsView files={item.permissions ?? []} />;
|
||||||
case "shell":
|
case "shell":
|
||||||
|
|
@ -346,10 +350,15 @@ function ConfirmPrompt({ prompt }: { prompt: ConfirmPromptState }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PromptView({ prompt }: { prompt: PromptState }) {
|
function PromptView({ prompt, state }: { prompt: PromptState; state: UIState }) {
|
||||||
switch (prompt.kind) {
|
switch (prompt.kind) {
|
||||||
case "input":
|
case "input":
|
||||||
return <InputPrompt prompt={prompt} />;
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<InputPrompt prompt={prompt} />
|
||||||
|
{state.contextMax !== null ? <ContextBar used={state.contextUsed} max={state.contextMax} /> : null}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
case "select":
|
case "select":
|
||||||
return <SelectPrompt prompt={prompt} />;
|
return <SelectPrompt prompt={prompt} />;
|
||||||
case "confirm":
|
case "confirm":
|
||||||
|
|
@ -390,7 +399,7 @@ export function App({ store }: { store: UIStore }) {
|
||||||
<Spinner label={state.busy} />
|
<Spinner label={state.busy} />
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
{state.prompt ? <PromptView prompt={state.prompt} /> : null}
|
{state.prompt ? <PromptView prompt={state.prompt} state={state} /> : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
packages/ui/src/context-bar.tsx
Normal file
24
packages/ui/src/context-bar.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Barre de progression du contexte (sous la zone de saisie, alignée à droite, de gauche à droite).
|
||||||
|
import React from "react";
|
||||||
|
import { Box, Text, useStdout } from "ink";
|
||||||
|
import { contextBarModel, formatTokens } from "./context-model";
|
||||||
|
|
||||||
|
const COLORS = { ok: "green", warn: "yellow", danger: "red" } as const;
|
||||||
|
const EMPTY_COLOR = "#555555";
|
||||||
|
|
||||||
|
export function ContextBar({ used, max }: { used: number; max: number }) {
|
||||||
|
const { stdout } = useStdout();
|
||||||
|
const columns = stdout?.columns ?? 80;
|
||||||
|
const pct = Math.round(Math.min(1, used / Math.max(1, max)) * 100);
|
||||||
|
const label = `${formatTokens(used)} / ${formatTokens(max)} · ${pct}% `;
|
||||||
|
// Marge d'1 colonne de chaque côté ; le libellé passe avant les carrés
|
||||||
|
const model = contextBarModel(used, max, Math.max(1, columns - label.length - 2));
|
||||||
|
const scale = model.tokensPerSquare === 10_000 ? "" : ` (1■ = ${formatTokens(model.tokensPerSquare)})`;
|
||||||
|
return (
|
||||||
|
<Box justifyContent="flex-end" paddingRight={1}>
|
||||||
|
<Text dimColor>{label.trimEnd() + scale + " "}</Text>
|
||||||
|
<Text color={COLORS[model.level]}>{"■".repeat(model.filled)}</Text>
|
||||||
|
<Text color={EMPTY_COLOR}>{"■".repeat(model.total - model.filled)}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
packages/ui/src/context-model.test.ts
Normal file
47
packages/ui/src/context-model.test.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { TOKENS_PER_SQUARE, contextBarModel, formatTokens } from "./context-model";
|
||||||
|
|
||||||
|
describe("contextBarModel", () => {
|
||||||
|
it("1 carré = 10 000 tokens : 200 000 tokens → 20 carrés", () => {
|
||||||
|
expect(TOKENS_PER_SQUARE).toBe(10_000);
|
||||||
|
expect(contextBarModel(0, 200_000)).toMatchObject({ total: 20, filled: 0, tokensPerSquare: 10_000, level: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("la barre se remplit de la gauche vers la droite selon l'usage", () => {
|
||||||
|
expect(contextBarModel(50_000, 200_000).filled).toBe(5);
|
||||||
|
expect(contextBarModel(100_000, 200_000)).toMatchObject({ filled: 10, ratio: 0.5, level: "ok" });
|
||||||
|
expect(contextBarModel(130_000, 200_000)).toMatchObject({ filled: 13, level: "warn" });
|
||||||
|
expect(contextBarModel(180_000, 200_000)).toMatchObject({ filled: 18, level: "danger" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("un contexte non vide montre au moins un carré ; un contexte plein les remplit tous", () => {
|
||||||
|
expect(contextBarModel(300, 200_000).filled).toBe(1);
|
||||||
|
expect(contextBarModel(200_000, 200_000)).toMatchObject({ filled: 20, ratio: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dépassement du maximum : plafonné", () => {
|
||||||
|
expect(contextBarModel(250_000, 200_000)).toMatchObject({ filled: 20, ratio: 1, level: "danger" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("taille non multiple de 10 000 : carré partiel arrondi vers le haut au total", () => {
|
||||||
|
expect(contextBarModel(0, 128_000).total).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("barre trop large pour le terminal : échelle agrandie par paliers de 10 000", () => {
|
||||||
|
const m = contextBarModel(500_000, 1_000_000, 60); // 100 carrés demandés, 60 disponibles
|
||||||
|
expect(m.tokensPerSquare).toBe(20_000);
|
||||||
|
expect(m.total).toBe(50);
|
||||||
|
expect(m.filled).toBe(25);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatTokens", () => {
|
||||||
|
it.each([
|
||||||
|
[950, "950"],
|
||||||
|
[12_345, "12.3k"],
|
||||||
|
[128_000, "128k"],
|
||||||
|
[200_000, "200k"],
|
||||||
|
[1_048_576, "1.05M"],
|
||||||
|
[1_000_000, "1M"],
|
||||||
|
])("%d → %s", (n, expected) => expect(formatTokens(n)).toBe(expected));
|
||||||
|
});
|
||||||
47
packages/ui/src/context-model.ts
Normal file
47
packages/ui/src/context-model.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Barre de progression du contexte : calculs purs (sans React), testables seuls.
|
||||||
|
|
||||||
|
/** Échelle par défaut : 1 carré = 10 000 tokens. */
|
||||||
|
export const TOKENS_PER_SQUARE = 10_000;
|
||||||
|
|
||||||
|
export interface ContextBarModel {
|
||||||
|
/** Nombre total de carrés. */
|
||||||
|
total: number;
|
||||||
|
/** Carrés remplis (au moins 1 dès que le contexte n'est pas vide). */
|
||||||
|
filled: number;
|
||||||
|
/** Tokens représentés par un carré (10 000, sauf si la barre est trop large pour le terminal). */
|
||||||
|
tokensPerSquare: number;
|
||||||
|
/** Part du contexte utilisée, de 0 à 1 (plafonnée). */
|
||||||
|
ratio: number;
|
||||||
|
/** vert : confortable · jaune : à surveiller · rouge : presque plein. */
|
||||||
|
level: "ok" | "warn" | "danger";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param used tokens du contexte actuel
|
||||||
|
* @param max taille maximale du contexte du modèle
|
||||||
|
* @param maxSquares largeur disponible pour la barre ; au-delà, l'échelle est agrandie (1 carré = plus de 10 000 tokens)
|
||||||
|
*/
|
||||||
|
export function contextBarModel(used: number, max: number, maxSquares = Infinity): ContextBarModel {
|
||||||
|
const safeMax = Math.max(1, max);
|
||||||
|
const safeUsed = Math.max(0, used);
|
||||||
|
let tokensPerSquare = TOKENS_PER_SQUARE;
|
||||||
|
if (Math.ceil(safeMax / tokensPerSquare) > maxSquares) {
|
||||||
|
// Échelle agrandie par paliers de 10 000 pour que la barre tienne dans la largeur disponible
|
||||||
|
tokensPerSquare = Math.ceil(safeMax / Math.max(1, maxSquares) / TOKENS_PER_SQUARE) * TOKENS_PER_SQUARE;
|
||||||
|
}
|
||||||
|
const total = Math.max(1, Math.ceil(safeMax / tokensPerSquare));
|
||||||
|
const ratio = Math.min(1, safeUsed / safeMax);
|
||||||
|
const filled = safeUsed === 0 ? 0 : Math.min(total, Math.max(1, Math.round(safeUsed / tokensPerSquare)));
|
||||||
|
const level = ratio >= 0.85 ? "danger" : ratio >= 0.6 ? "warn" : "ok";
|
||||||
|
return { total, filled, tokensPerSquare, ratio, level };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 950 → "950" · 12 345 → "12.3k" · 200 000 → "200k" · 1 048 576 → "1.05M". */
|
||||||
|
export function formatTokens(n: number): string {
|
||||||
|
if (n < 1_000) return String(Math.round(n));
|
||||||
|
if (n < 1_000_000) {
|
||||||
|
const k = n / 1_000;
|
||||||
|
return `${k >= 100 ? Math.round(k) : Math.round(k * 10) / 10}k`;
|
||||||
|
}
|
||||||
|
return `${Math.round((n / 1_000_000) * 100) / 100}M`;
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
||||||
import { render, type Instance } from "ink";
|
import { render, type Instance } from "ink";
|
||||||
import type { AgentUI } from "@maia/core";
|
import type { AgentUI } from "@maia/core";
|
||||||
import { App } from "./components";
|
import { App } from "./components";
|
||||||
import { UIStore, type Choice } from "./store";
|
import { UIStore, type Choice, type PermissionFile } from "./store";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface Ink (React pour CLI). Façade impérative au-dessus d'un UIStore : le CLI appelle des méthodes
|
* Interface Ink (React pour CLI). Façade impérative au-dessus d'un UIStore : le CLI appelle des méthodes
|
||||||
|
|
@ -116,4 +116,13 @@ export class InkUI implements AgentUI {
|
||||||
onToolResult(name: string, output: string, isError: boolean): void {
|
onToolResult(name: string, output: string, isError: boolean): void {
|
||||||
this.start().toolResult(name, output, isError);
|
this.start().toolResult(name, output, isError);
|
||||||
}
|
}
|
||||||
|
/** Taille du contexte (tokens) après chaque réponse du LLM : met à jour la barre de progression. */
|
||||||
|
onContextUsage(tokens: number): void {
|
||||||
|
this.start().setContextUsed(tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Taille maximale du contexte du modèle (null : inconnue, la barre est masquée). */
|
||||||
|
setContextMax(tokens: number | null): void {
|
||||||
|
this.start().setContextMax(tokens);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
packages/ui/src/markdown.test.ts
Normal file
26
packages/ui/src/markdown.test.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { renderMarkdown } from "./markdown";
|
||||||
|
|
||||||
|
describe("renderMarkdown", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
delete process.env.MAIA_NO_MARKDOWN;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rend du markdown sans laisser les marqueurs de titre ni de gras", () => {
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const plain = renderMarkdown("# Titre\n\nUn mot **important**.", 60).replace(/\u001b\[[0-9;]*m/g, "");
|
||||||
|
expect(plain).toContain("Titre");
|
||||||
|
expect(plain).toContain("important");
|
||||||
|
expect(plain).not.toContain("**");
|
||||||
|
expect(plain).not.toMatch(/^# /m);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ne termine pas par un saut de ligne", () => {
|
||||||
|
expect(renderMarkdown("Bonjour", 60).endsWith("\n")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renvoie le texte brut si MAIA_NO_MARKDOWN=1", () => {
|
||||||
|
process.env.MAIA_NO_MARKDOWN = "1";
|
||||||
|
expect(renderMarkdown("# Titre", 60)).toBe("# Titre");
|
||||||
|
});
|
||||||
|
});
|
||||||
34
packages/ui/src/markdown.tsx
Normal file
34
packages/ui/src/markdown.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
// Rendu markdown des réponses de l'agent.
|
||||||
|
//
|
||||||
|
// Composant volontairement isolé : c'est le SEUL fichier qui connaît `markdansi`.
|
||||||
|
// Pour le retirer : supprimer ce fichier, retirer `markdansi` de package.json et
|
||||||
|
// remplacer `<Markdown text=… />` par `<Text>{…}</Text>` dans components.tsx (un seul endroit).
|
||||||
|
import React from "react";
|
||||||
|
import { Text } from "ink";
|
||||||
|
import { render } from "markdansi";
|
||||||
|
|
||||||
|
/** Largeur de repli quand le terminal ne la fournit pas (sortie redirigée, etc.). */
|
||||||
|
const DEFAULT_WIDTH = 80;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit du markdown en texte ANSI. Ne lève jamais d'exception :
|
||||||
|
* en cas d'échec de markdansi (ou si MAIA_NO_MARKDOWN=1), renvoie le texte brut.
|
||||||
|
*/
|
||||||
|
export function renderMarkdown(text: string, width: number = terminalWidth()): string {
|
||||||
|
if (process.env.MAIA_NO_MARKDOWN === "1") return text;
|
||||||
|
try {
|
||||||
|
// trimEnd : markdansi termine par un saut de ligne, qui ajouterait une ligne vide sous la réponse.
|
||||||
|
return render(text, { width }).trimEnd();
|
||||||
|
} catch {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function terminalWidth(): number {
|
||||||
|
const columns = process.stdout.columns;
|
||||||
|
return typeof columns === "number" && columns > 0 ? columns : DEFAULT_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Markdown({ text }: { text: string }) {
|
||||||
|
return <Text>{renderMarkdown(text)}</Text>;
|
||||||
|
}
|
||||||
38
packages/ui/src/mask-data.ts
Normal file
38
packages/ui/src/mask-data.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Fichier généré par scripts/make-mask.py à partir de assets/mask.png : ne pas modifier à la main.
|
||||||
|
// Une chaîne par ligne de pixels ; chaque pixel = 6 chiffres hexadécimaux rrggbb, "......" = transparent.
|
||||||
|
export const MASK_SIZE = 32;
|
||||||
|
|
||||||
|
export const MASK_DATA: string[] = [
|
||||||
|
"..........................................................................................1514141d1c1c..........................................................................................",
|
||||||
|
"................................................1515151d1a0b1c1c1c..................1b1b1b7868205f531a212121..................1a1a1a1c19091a1a1a................................................",
|
||||||
|
"................................................222014ddbe3b5449181d1d1d............3e3613f7d442ebca3f2d2811............1818186a5c1dcbaf37161512................................................",
|
||||||
|
"..................1a1a19171615..................433a13f7d442e6c63e403813......191919ac942ec4a835d3b5398f7b261e1e1e......564b18ebca3ff9d643312c16..................171615191919..................",
|
||||||
|
"..................201c10baa132473e161b1b1b......594c18af972f72621fcbaf361d1c16282414f4d1417f6f239c872adebf3b1a1914242113e0c13c574b18cbaf373d3511......171716564b18ab942e0b0b0b..................",
|
||||||
|
"..................211e14f2d041f8d5427565201a1a19534816e7c63e362f0ff9d643544917483e13ffda447f6f239c872afeda442b260c70611eeac93f352e0ff8d54337300f1f1e1a8e7b26fdd843d5b839111111..................",
|
||||||
|
"..................151412e0c13ca48d2cf5d24267591c322b0dfeda44504516c2a6349b852a5b4e19ffda447f6f239c872affda443f3611b79d31a68e2d6c5d1df3d141161306837123ecca3faa922ec4a834141414..................",
|
||||||
|
"..................161615c4a93562551ba38c2cedcb3f2a250bd4b6399782297a6921d5b739887525ffda44837224a08a2bffda44776720e9c83e5e511ab39a30b89e313c3410f8d5428774247e6d22a8902d191919..................",
|
||||||
|
"..................1a1a1a9d872ad0b2383d3411f9d542b49b30d4b639e5c43da08a2bffda44ffda44ffda44eecc40f4d041ffda44ffda44ffda44968129f3d041c4a835c5a935eccb3f332c0ee5c43d806e22........................",
|
||||||
|
"........................66591cffda445e511aa78f2dffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda448a77257a6921ffda444b4115........................",
|
||||||
|
"........................282414eac93fcdaf37534717ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44fdd944443a13e3c23dd7b93a1f1d16........................",
|
||||||
|
"........................222222786720ffda44f0ce40ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44f1ce41feda445c5019..............................",
|
||||||
|
"..............................1c1a11d1b438ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44ffda44b99f32161512..............................",
|
||||||
|
"..............................181818322b0e64581b64581b64581b64581b64581b64581b64581b64581b64581b64581b695b1de1cd45b8a63764581ba1912ff0dd4b7d6e2364581b27220b1c1c1c..............................",
|
||||||
|
"........................1b15144a2c261f25108aa74489a74489a74489a74489a74489a74489a74489a74489a74489a7448daa46b9e15cabcf5589a744a2c551bde55e95b44a88a4431b180d4d2e281a1615........................",
|
||||||
|
"..................1b1b1b693e368c53482b3315b6dd5b53652a4c5c264c5c264c5c264c5c264c5c264c5c264c5c264c5c264c5c264c5c264c5c264c5c264c5c264c5c26607430bbe25d2622139b5c5058342d1c1c1c..................",
|
||||||
|
"..................18131294584c8c53482b3315abcf552a260cb79f32b79e31b79e31b79e31b79e31b79e31b79e31b79e31b79e31b79e31b79e31b79e31b79e31b49c312a2c11bae15d2622139b5c50834e430f0f0f..................",
|
||||||
|
"..................171110975a4e8c53482b3315abcf5527230ba9912da9912da9912da9912da9912da9912da9912da9912da9912da9912da9912da9912da9912da68f2d282b10bae15d2622139b5c50864f450a0a0a..................",
|
||||||
|
"..................171110975a4e8c53482b3315abcf551b1b19464444241512462a242415124543431a14146f2d2d6228281d1c1c3937372c1a17462a241c110f514f4f232717bae15d2622139b5c50864f450a0a0a..................",
|
||||||
|
"..................0e0b0b36201c351f1b2a3215abcf55393836a7a2a24127229b5c50422722a5a0a0392c2cf76363db5858413e3e89858553312b9a5c50301d19bfb9b9313524bae15d1b1c0e3a221e311d190a0a0a..................",
|
||||||
|
"............2d17178d3939bf4d4da743432b3215abcf55393836eee6e6484545241614484444eee5e5392c2cf76363db5858413e3ee0d8d83a35352316145c5959f5ecec313524bae15d271f12ba4b4bbc4c4c823434251616............",
|
||||||
|
"......391a1ae25b5ba643435e26265c25252b3215abcf55393836fff6f6fbf1f1e0d8d8fbf1f1e9e1e1352020fa6565e65d5d322b2bf6ededf8eeeee1d9d9fdf4f4fbf2f2313524bae15d221d10632828622828b74949d556562b1616......",
|
||||||
|
"151313c04d4d88373742091f9e164a86133f2a3115abcf55393836fff6f6fff5f5fff5f5fff5f5847f7f803434ff6666ff66666429299f9a9afff5f5fff5f5fff5f5fbf2f2313524bae15d21191097154798154838091aa54343a24141151515",
|
||||||
|
"301818f161612e0d14ca1c5fd81e66c31b5c2c3116abcf55393836fff6f6fff5f5fff5f5fff5f53e3c3cbb4b4bff6666ff66669f4040595757fff5f5fff5f5fff5f5fbf2f2313524bae15d2d1a16d71e66d81e66be1a5aa53645ec5f5f170e0e",
|
||||||
|
"371717e75d5d2e0815d81e66d81e66c81c5f2d2e16b0d558373833fff6f6fff5f5fff5f5cdc6c6413f3f260f0f5d25255d25251c0b0b525050d9d1d1fff5f5fff5f5f6eded303621b8de5b2f1617d81e66d81e66d71e66e74363f964641c0c0c",
|
||||||
|
"221717e85d5d481c1d931445d81e66d21d63281a13b7de5b2f3421f3eaeafff5f5c9c2c237311ba48e2cbfa433bfa433bfa433bea433968128322f22ddd5d5fff5f5d9d1d1333d1ba6c9532f0a16d81e66d81e667e113b7d3133ce5353151313",
|
||||||
|
"1918187c3232d7565640181b3a081b32091712080898b84c435121bab4b4fff5f5676464ae962fceb037ad942ead942ead942ead942ed7b83a887525837f7ffff5f59e9999586b2c839e4116080936081937081a4e1f20e45c5c5e2626......",
|
||||||
|
"......151111823535ec5f5fd45555df5a5a8034346378317f9a3f5f5c5cfff5f55b5958c0a533a28b2c6e5e1e6e5e1e6e5e1e6e5e1eb299309a8429787474fcf3f345444294b44a4e5e279a3f3fda5858d85757e85d5d6c2c2c161515......",
|
||||||
|
"............1b1b1b2817174a1e1e431c1c1f1616242a15afd457333c1dbdb7b7a7a1a14c4216e2c33dfdd843fdd843fdd843fcd843d4b639383114c1bbbba29d9d424f21a8cb53181b11221515461d1d471d1d2316161e1e1e............",
|
||||||
|
"..........................................1f1f1f62773197b74b2a2f1a303424181c0d21240e3132123132123132123132121d200d1b1f103135252e361ba4c7524d5d26262626..........................................",
|
||||||
|
"................................................181a138aa845bbe35dbbe25dbbe25dbbe25dbbe25dbbe25dbbe25dbbe25dbbe25dbbe25dbbe25dbbe35d78923c171815................................................",
|
||||||
|
"......................................................14160f181d0c181d0c181d0c181d0c181d0c181d0c181d0c181d0c181d0c181d0c181d0c181d0c151712......................................................",
|
||||||
|
];
|
||||||
|
|
@ -45,15 +45,24 @@ describe("pixelsToRuns", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("masque", () => {
|
describe("masque", () => {
|
||||||
it("fait 20 × 20 pixels, que des couleurs de la palette ou du transparent", () => {
|
it("fait 32 × 32 pixels, soit 16 lignes de terminal", () => {
|
||||||
const grid = buildMaskGrid();
|
const grid = buildMaskGrid();
|
||||||
expect(grid).toHaveLength(20);
|
expect(MASK_WIDTH).toBe(32);
|
||||||
|
expect(grid).toHaveLength(MASK_WIDTH);
|
||||||
for (const row of grid) expect(row).toHaveLength(MASK_WIDTH);
|
for (const row of grid) expect(row).toHaveLength(MASK_WIDTH);
|
||||||
expect(pixelsToRuns(grid)).toHaveLength(10); // 10 lignes de terminal
|
expect(pixelsToRuns(grid)).toHaveLength(16);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("est symétrique gauche/droite", () => {
|
it("n'a que des couleurs #rrggbb ou du transparent, avec fond transparent et image au centre", () => {
|
||||||
for (const row of buildMaskGrid()) expect([...row].reverse()).toEqual(row);
|
const grid = buildMaskGrid();
|
||||||
|
for (const row of grid) for (const p of row) expect(p === null || /^#[0-9a-f]{6}$/.test(p)).toBe(true);
|
||||||
|
expect(grid[0]![0]).toBeNull();
|
||||||
|
expect(grid[16]![16]).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("n'est pas pixelisé en aplats : beaucoup de couleurs différentes (couleurs réelles)", () => {
|
||||||
|
const colors = new Set(buildMaskGrid().flat().filter((p) => p !== null));
|
||||||
|
expect(colors.size).toBeGreaterThan(30);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Pixel-art pour le terminal, sans dépendance à React : chaque caractère « demi-bloc » (▀ ▄ █) affiche
|
// Pixel-art pour le terminal, sans dépendance à React : chaque caractère « demi-bloc » (▀ ▄ █) affiche
|
||||||
// deux pixels superposés, ce qui donne des pixels carrés (une cellule de terminal est ~2 fois plus haute que large).
|
// deux pixels superposés, ce qui donne des pixels carrés (une cellule de terminal est ~2 fois plus haute que large).
|
||||||
import { hslToHex } from "./colors";
|
import { hslToHex } from "./colors";
|
||||||
|
import { MASK_DATA, MASK_SIZE } from "./mask-data";
|
||||||
|
|
||||||
/** Couleur "#rrggbb", ou null pour un pixel transparent. */
|
/** Couleur "#rrggbb", ou null pour un pixel transparent. */
|
||||||
export type Pixel = string | null;
|
export type Pixel = string | null;
|
||||||
|
|
@ -49,45 +50,22 @@ export function pixelsToRuns(grid: Pixel[][]): Run[][] {
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────────── Masque (20 × 20 pixels → 10 lignes de terminal) ─────────────
|
// ───────────── Masque : image affichée telle quelle (couleurs réelles, sans pixelisation) ─────────────
|
||||||
|
// Les pixels viennent de mask-data.ts, généré depuis assets/mask.png par scripts/make-mask.py.
|
||||||
|
|
||||||
const PALETTE: Record<string, string> = {
|
/** Largeur du masque en colonnes (= nombre de pixels par ligne ; 1 pixel = 1 colonne de terminal). */
|
||||||
Y: "#F7C61B", // jaune (couronne, mâchoire)
|
export const MASK_WIDTH = MASK_SIZE;
|
||||||
O: "#E8962A", // orange (bandeau)
|
|
||||||
G: "#3DDC4B", // vert (contour du visage)
|
|
||||||
K: "#111418", // noir (traits)
|
|
||||||
W: "#F6E7D2", // crème (visage)
|
|
||||||
R: "#E0364F", // rouge (nez, oreilles)
|
|
||||||
B: "#8A4B2D", // brun (yeux, côtés)
|
|
||||||
};
|
|
||||||
|
|
||||||
const MASK = [
|
|
||||||
".........YY.........",
|
|
||||||
"...Y.....YY.....Y...",
|
|
||||||
"...YY...YYYY...YY...",
|
|
||||||
"..YYY...YYYY...YYY..",
|
|
||||||
"..YYYY.YYYYYY.YYYY..",
|
|
||||||
"..YYYYYYYYYYYYYYYY..",
|
|
||||||
"...OOOOOOOOOOOOOO...",
|
|
||||||
"...GGGGGGGGGGGGGG...",
|
|
||||||
".BBGKKKKKKKKKKKKGBB.",
|
|
||||||
".BBGKYYYYYYYYYYKGBB.",
|
|
||||||
".BBGKWWWWWWWWWWKGBB.",
|
|
||||||
"..RGKWBBWWWWBBWKGR..",
|
|
||||||
"RRRGKWBBWWWWBBWKGRRR",
|
|
||||||
"RRRGKWWWWRRWWWWKGRRR",
|
|
||||||
"RRRGKWWWRRRRWWWKGRRR",
|
|
||||||
"RRRGKWWWWRRWWWWKGRRR",
|
|
||||||
".RRGKWWYYYYYYWWKGRR.",
|
|
||||||
"..RGKWWWYYYYWWWKGR..",
|
|
||||||
"...GKKKKKKKKKKKKG...",
|
|
||||||
"...GGGGGGGGGGGGGG...",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const MASK_WIDTH = 20;
|
|
||||||
|
|
||||||
|
/** Décode les lignes de mask-data.ts : 6 chiffres hexadécimaux par pixel, "......" = transparent. */
|
||||||
export function buildMaskGrid(): Pixel[][] {
|
export function buildMaskGrid(): Pixel[][] {
|
||||||
return MASK.map((row) => [...row].map((ch) => PALETTE[ch] ?? null));
|
return MASK_DATA.map((row) => {
|
||||||
|
const pixels: Pixel[] = [];
|
||||||
|
for (let i = 0; i + 6 <= row.length; i += 6) {
|
||||||
|
const hex = row.slice(i, i + 6);
|
||||||
|
pixels.push(hex === "......" ? null : `#${hex}`);
|
||||||
|
}
|
||||||
|
return pixels;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────────── Titre en gros caractères (police 5 × 7) ─────────────
|
// ───────────── Titre en gros caractères (police 5 × 7) ─────────────
|
||||||
|
|
|
||||||
|
|
@ -120,4 +120,16 @@ describe("UIStore", () => {
|
||||||
expect(ko).toMatchObject({ detail: "(aucune sortie)\n[code de sortie : 1]", failed: true });
|
expect(ko).toMatchObject({ detail: "(aucune sortie)\n[code de sortie : 1]", failed: true });
|
||||||
expect(big?.detail).toContain("caractères omis à l'affichage");
|
expect(big?.detail).toContain("caractères omis à l'affichage");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("contexte : maximum inconnu au départ (barre masquée), puis usage et maximum renseignés", () => {
|
||||||
|
const s = new UIStore();
|
||||||
|
expect(s.getSnapshot()).toMatchObject({ contextUsed: 0, contextMax: null });
|
||||||
|
s.setContextMax(200_000);
|
||||||
|
s.setContextUsed(45_678.4);
|
||||||
|
expect(s.getSnapshot()).toMatchObject({ contextUsed: 45_678, contextMax: 200_000 });
|
||||||
|
s.setContextMax(null);
|
||||||
|
expect(s.getSnapshot().contextMax).toBeNull();
|
||||||
|
s.setContextMax(0);
|
||||||
|
expect(s.getSnapshot().contextMax).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -74,13 +74,17 @@ export interface UIState {
|
||||||
/** Libellé du spinner, ou null. */
|
/** Libellé du spinner, ou null. */
|
||||||
busy: string | null;
|
busy: string | null;
|
||||||
prompt: PromptState | null;
|
prompt: PromptState | null;
|
||||||
|
/** Tokens du contexte de la conversation (mis à jour après chaque réponse du LLM). */
|
||||||
|
contextUsed: number;
|
||||||
|
/** Taille maximale du contexte du modèle, null tant qu'elle n'est pas connue (la barre est alors masquée). */
|
||||||
|
contextMax: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RESULT_PREVIEW_MAX = 600;
|
const RESULT_PREVIEW_MAX = 600;
|
||||||
const SHELL_PREVIEW_MAX = 4000;
|
const SHELL_PREVIEW_MAX = 4000;
|
||||||
|
|
||||||
export class UIStore {
|
export class UIStore {
|
||||||
private state: UIState = { items: [], streaming: "", busy: null, prompt: null };
|
private state: UIState = { items: [], streaming: "", busy: null, prompt: null, contextUsed: 0, contextMax: null };
|
||||||
private listeners = new Set<() => void>();
|
private listeners = new Set<() => void>();
|
||||||
private nextId = 1;
|
private nextId = 1;
|
||||||
private history: string[] = [];
|
private history: string[] = [];
|
||||||
|
|
@ -136,6 +140,16 @@ export class UIStore {
|
||||||
this.commit({ items: this.withItem("permissions", "Permissions", { permissions: files }) });
|
this.commit({ items: this.withItem("permissions", "Permissions", { permissions: files }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Contexte actuel de la conversation, en tokens. */
|
||||||
|
setContextUsed(tokens: number): void {
|
||||||
|
this.commit({ contextUsed: Math.max(0, Math.round(tokens)) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Taille maximale du contexte du modèle (null : inconnue, barre masquée). */
|
||||||
|
setContextMax(tokens: number | null): void {
|
||||||
|
this.commit({ contextMax: tokens !== null && tokens > 0 ? Math.round(tokens) : null });
|
||||||
|
}
|
||||||
|
|
||||||
setBusy(label: string | null): void {
|
setBusy(label: string | null): void {
|
||||||
this.commit({ busy: label });
|
this.commit({ busy: label });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
570
pnpm-lock.yaml
570
pnpm-lock.yaml
|
|
@ -111,6 +111,9 @@ importers:
|
||||||
ink:
|
ink:
|
||||||
specifier: ^7.0.3
|
specifier: ^7.0.3
|
||||||
version: 7.1.1(@types/react@19.3.0)(react@19.3.1)
|
version: 7.1.1(@types/react@19.3.0)(react@19.3.1)
|
||||||
|
markdansi:
|
||||||
|
specifier: ^0.2.0
|
||||||
|
version: 0.2.1
|
||||||
react:
|
react:
|
||||||
specifier: ^19.2.0
|
specifier: ^19.2.0
|
||||||
version: 19.3.1
|
version: 19.3.1
|
||||||
|
|
@ -728,9 +731,18 @@ packages:
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@types/debug@4.1.13':
|
||||||
|
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
|
||||||
|
|
||||||
'@types/estree@1.0.9':
|
'@types/estree@1.0.9':
|
||||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||||
|
|
||||||
|
'@types/mdast@4.0.4':
|
||||||
|
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
||||||
|
|
||||||
|
'@types/ms@2.1.0':
|
||||||
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
'@types/node@22.20.5':
|
'@types/node@22.20.5':
|
||||||
resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==}
|
resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==}
|
||||||
|
|
||||||
|
|
@ -743,6 +755,9 @@ packages:
|
||||||
'@types/turndown@5.0.6':
|
'@types/turndown@5.0.6':
|
||||||
resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==}
|
resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==}
|
||||||
|
|
||||||
|
'@types/unist@3.0.3':
|
||||||
|
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||||
|
|
||||||
'@vitest/expect@2.1.9':
|
'@vitest/expect@2.1.9':
|
||||||
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
|
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
|
||||||
|
|
||||||
|
|
@ -810,6 +825,9 @@ packages:
|
||||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ccount@2.0.1:
|
||||||
|
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||||
|
|
||||||
chai@5.3.3:
|
chai@5.3.3:
|
||||||
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
|
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -818,6 +836,9 @@ packages:
|
||||||
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
|
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
|
||||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||||
|
|
||||||
|
character-entities@2.0.2:
|
||||||
|
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
||||||
|
|
||||||
check-error@2.1.3:
|
check-error@2.1.3:
|
||||||
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
|
|
@ -877,6 +898,9 @@ packages:
|
||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decode-named-character-reference@1.3.0:
|
||||||
|
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
|
||||||
|
|
||||||
deep-eql@5.0.2:
|
deep-eql@5.0.2:
|
||||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -885,6 +909,13 @@ packages:
|
||||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
dequal@2.0.3:
|
||||||
|
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
devlop@1.1.0:
|
||||||
|
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||||
|
|
||||||
environment@1.1.0:
|
environment@1.1.0:
|
||||||
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
|
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -914,6 +945,10 @@ packages:
|
||||||
resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
|
resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
escape-string-regexp@5.0.0:
|
||||||
|
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
estree-walker@3.0.3:
|
estree-walker@3.0.3:
|
||||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
|
|
||||||
|
|
@ -946,6 +981,10 @@ packages:
|
||||||
resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==}
|
resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
has-flag@5.0.1:
|
||||||
|
resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
ignore@6.0.2:
|
ignore@6.0.2:
|
||||||
resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==}
|
resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
@ -995,12 +1034,143 @@ packages:
|
||||||
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
|
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
|
longest-streak@3.1.0:
|
||||||
|
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||||
|
|
||||||
loupe@3.2.1:
|
loupe@3.2.1:
|
||||||
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
markdansi@0.2.1:
|
||||||
|
resolution: {integrity: sha512-ri9ce2cmpcbL0A0FJ5a3WtIxMBRE8fPNEnSAz1CZGCfZ7MazmBEGmreOpBhXiaQZ31FXT+GYUi+86bxYpCTo8w==}
|
||||||
|
engines: {node: '>=22'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
markdown-table@3.0.4:
|
||||||
|
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||||
|
|
||||||
|
mdast-util-find-and-replace@3.0.3:
|
||||||
|
resolution: {integrity: sha512-xPgpDNl0/OXHsI7TlaIs22lWnH3KlpvZdXG1ET/m/YT2Hhdkx8lJV4kLphE6l9simyM4KPDFBwas08BgEob6Jw==}
|
||||||
|
|
||||||
|
mdast-util-from-markdown@2.1.0:
|
||||||
|
resolution: {integrity: sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==}
|
||||||
|
|
||||||
|
mdast-util-gfm-autolink-literal@2.0.1:
|
||||||
|
resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==}
|
||||||
|
|
||||||
|
mdast-util-gfm-footnote@2.1.0:
|
||||||
|
resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
|
||||||
|
|
||||||
|
mdast-util-gfm-strikethrough@2.0.1:
|
||||||
|
resolution: {integrity: sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==}
|
||||||
|
|
||||||
|
mdast-util-gfm-table@2.0.0:
|
||||||
|
resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==}
|
||||||
|
|
||||||
|
mdast-util-gfm-task-list-item@2.0.0:
|
||||||
|
resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==}
|
||||||
|
|
||||||
|
mdast-util-gfm@3.1.0:
|
||||||
|
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
|
||||||
|
|
||||||
|
mdast-util-phrasing@4.1.0:
|
||||||
|
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
|
||||||
|
|
||||||
|
mdast-util-to-markdown@2.2.0:
|
||||||
|
resolution: {integrity: sha512-2Jn/ADrHFFyyniBMi0bncTbJqU78K3/TEYvzvwrWWzD9MYPUa/Gm4r3nOeKFQg/PDM5BsbDYdc8ackW4NTY6NA==}
|
||||||
|
|
||||||
|
mdast-util-to-string@4.0.0:
|
||||||
|
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
|
||||||
|
|
||||||
|
micromark-core-commonmark@2.0.4:
|
||||||
|
resolution: {integrity: sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==}
|
||||||
|
|
||||||
|
micromark-extension-gfm-autolink-literal@2.1.0:
|
||||||
|
resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==}
|
||||||
|
|
||||||
|
micromark-extension-gfm-footnote@2.1.0:
|
||||||
|
resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==}
|
||||||
|
|
||||||
|
micromark-extension-gfm-strikethrough@2.1.0:
|
||||||
|
resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==}
|
||||||
|
|
||||||
|
micromark-extension-gfm-table@2.1.2:
|
||||||
|
resolution: {integrity: sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==}
|
||||||
|
|
||||||
|
micromark-extension-gfm-tagfilter@2.0.0:
|
||||||
|
resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==}
|
||||||
|
|
||||||
|
micromark-extension-gfm-task-list-item@2.1.0:
|
||||||
|
resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==}
|
||||||
|
|
||||||
|
micromark-extension-gfm@3.0.0:
|
||||||
|
resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
|
||||||
|
|
||||||
|
micromark-factory-destination@2.0.1:
|
||||||
|
resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
|
||||||
|
|
||||||
|
micromark-factory-label@2.0.1:
|
||||||
|
resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
|
||||||
|
|
||||||
|
micromark-factory-space@2.1.0:
|
||||||
|
resolution: {integrity: sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==}
|
||||||
|
|
||||||
|
micromark-factory-title@2.0.1:
|
||||||
|
resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==}
|
||||||
|
|
||||||
|
micromark-factory-whitespace@2.0.1:
|
||||||
|
resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==}
|
||||||
|
|
||||||
|
micromark-util-character@2.1.1:
|
||||||
|
resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
|
||||||
|
|
||||||
|
micromark-util-chunked@2.0.1:
|
||||||
|
resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==}
|
||||||
|
|
||||||
|
micromark-util-classify-character@2.0.1:
|
||||||
|
resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==}
|
||||||
|
|
||||||
|
micromark-util-combine-extensions@2.0.1:
|
||||||
|
resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==}
|
||||||
|
|
||||||
|
micromark-util-decode-numeric-character-reference@2.0.2:
|
||||||
|
resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==}
|
||||||
|
|
||||||
|
micromark-util-decode-string@2.0.1:
|
||||||
|
resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==}
|
||||||
|
|
||||||
|
micromark-util-edit-map@1.0.0:
|
||||||
|
resolution: {integrity: sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==}
|
||||||
|
|
||||||
|
micromark-util-encode@2.0.1:
|
||||||
|
resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
|
||||||
|
|
||||||
|
micromark-util-html-tag-name@2.0.1:
|
||||||
|
resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
|
||||||
|
|
||||||
|
micromark-util-normalize-identifier@2.0.1:
|
||||||
|
resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==}
|
||||||
|
|
||||||
|
micromark-util-resolve-all@2.0.1:
|
||||||
|
resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==}
|
||||||
|
|
||||||
|
micromark-util-sanitize-uri@2.0.1:
|
||||||
|
resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
|
||||||
|
|
||||||
|
micromark-util-subtokenize@2.1.0:
|
||||||
|
resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==}
|
||||||
|
|
||||||
|
micromark-util-symbol@2.0.1:
|
||||||
|
resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
|
||||||
|
|
||||||
|
micromark-util-types@2.0.3:
|
||||||
|
resolution: {integrity: sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==}
|
||||||
|
|
||||||
|
micromark@4.0.3:
|
||||||
|
resolution: {integrity: sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==}
|
||||||
|
|
||||||
mimic-fn@2.1.0:
|
mimic-fn@2.1.0:
|
||||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -1152,6 +1322,14 @@ packages:
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
supports-color@10.2.2:
|
||||||
|
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
supports-hyperlinks@4.6.0:
|
||||||
|
resolution: {integrity: sha512-FL/q/YePS4tFFTNiBOR+zPk2ZrKheqNtmTJ/9OUZ4e1wKHJofDbGQSmSsxS0XNnabSkqexryCaXGvEKz1vf9UQ==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
tagged-tag@1.0.0:
|
tagged-tag@1.0.0:
|
||||||
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
@ -1239,6 +1417,18 @@ packages:
|
||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
|
unist-util-is@6.0.1:
|
||||||
|
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
|
||||||
|
|
||||||
|
unist-util-stringify-position@4.0.0:
|
||||||
|
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
|
||||||
|
|
||||||
|
unist-util-visit-parents@6.0.2:
|
||||||
|
resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
|
||||||
|
|
||||||
|
unist-util-visit@5.1.0:
|
||||||
|
resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
|
||||||
|
|
||||||
vite-node@2.1.9:
|
vite-node@2.1.9:
|
||||||
resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
|
resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
|
@ -1328,6 +1518,9 @@ packages:
|
||||||
yoga-layout@3.2.1:
|
yoga-layout@3.2.1:
|
||||||
resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
|
resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
|
||||||
|
|
||||||
|
zwitch@2.0.4:
|
||||||
|
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@alcalzone/ansi-tokenize@0.3.1':
|
'@alcalzone/ansi-tokenize@0.3.1':
|
||||||
|
|
@ -1662,8 +1855,18 @@ snapshots:
|
||||||
'@rollup/rollup-win32-x64-msvc@4.64.6':
|
'@rollup/rollup-win32-x64-msvc@4.64.6':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/debug@4.1.13':
|
||||||
|
dependencies:
|
||||||
|
'@types/ms': 2.1.0
|
||||||
|
|
||||||
'@types/estree@1.0.9': {}
|
'@types/estree@1.0.9': {}
|
||||||
|
|
||||||
|
'@types/mdast@4.0.4':
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/node@22.20.5':
|
'@types/node@22.20.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
@ -1676,6 +1879,8 @@ snapshots:
|
||||||
|
|
||||||
'@types/turndown@5.0.6': {}
|
'@types/turndown@5.0.6': {}
|
||||||
|
|
||||||
|
'@types/unist@3.0.3': {}
|
||||||
|
|
||||||
'@vitest/expect@2.1.9':
|
'@vitest/expect@2.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 2.1.9
|
'@vitest/spy': 2.1.9
|
||||||
|
|
@ -1739,6 +1944,8 @@ snapshots:
|
||||||
|
|
||||||
cac@6.7.14: {}
|
cac@6.7.14: {}
|
||||||
|
|
||||||
|
ccount@2.0.1: {}
|
||||||
|
|
||||||
chai@5.3.3:
|
chai@5.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
assertion-error: 2.0.1
|
assertion-error: 2.0.1
|
||||||
|
|
@ -1749,6 +1956,8 @@ snapshots:
|
||||||
|
|
||||||
chalk@5.6.2: {}
|
chalk@5.6.2: {}
|
||||||
|
|
||||||
|
character-entities@2.0.2: {}
|
||||||
|
|
||||||
check-error@2.1.3: {}
|
check-error@2.1.3: {}
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
|
|
@ -1788,10 +1997,20 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decode-named-character-reference@1.3.0:
|
||||||
|
dependencies:
|
||||||
|
character-entities: 2.0.2
|
||||||
|
|
||||||
deep-eql@5.0.2: {}
|
deep-eql@5.0.2: {}
|
||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
|
dequal@2.0.3: {}
|
||||||
|
|
||||||
|
devlop@1.1.0:
|
||||||
|
dependencies:
|
||||||
|
dequal: 2.0.3
|
||||||
|
|
||||||
environment@1.1.0: {}
|
environment@1.1.0: {}
|
||||||
|
|
||||||
es-module-lexer@1.7.0: {}
|
es-module-lexer@1.7.0: {}
|
||||||
|
|
@ -1884,6 +2103,8 @@ snapshots:
|
||||||
|
|
||||||
escape-string-regexp@2.0.0: {}
|
escape-string-regexp@2.0.0: {}
|
||||||
|
|
||||||
|
escape-string-regexp@5.0.0: {}
|
||||||
|
|
||||||
estree-walker@3.0.3:
|
estree-walker@3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.9
|
'@types/estree': 1.0.9
|
||||||
|
|
@ -1909,6 +2130,8 @@ snapshots:
|
||||||
|
|
||||||
get-east-asian-width@1.7.0: {}
|
get-east-asian-width@1.7.0: {}
|
||||||
|
|
||||||
|
has-flag@5.0.1: {}
|
||||||
|
|
||||||
ignore@6.0.2: {}
|
ignore@6.0.2: {}
|
||||||
|
|
||||||
indent-string@5.0.0: {}
|
indent-string@5.0.0: {}
|
||||||
|
|
@ -1963,12 +2186,331 @@ snapshots:
|
||||||
|
|
||||||
load-tsconfig@0.2.5: {}
|
load-tsconfig@0.2.5: {}
|
||||||
|
|
||||||
|
longest-streak@3.1.0: {}
|
||||||
|
|
||||||
loupe@3.2.1: {}
|
loupe@3.2.1: {}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.6.0
|
'@jridgewell/sourcemap-codec': 1.6.0
|
||||||
|
|
||||||
|
markdansi@0.2.1:
|
||||||
|
dependencies:
|
||||||
|
chalk: 5.6.2
|
||||||
|
mdast-util-from-markdown: 2.1.0
|
||||||
|
mdast-util-gfm: 3.1.0
|
||||||
|
micromark: 4.0.3
|
||||||
|
micromark-extension-gfm: 3.0.0
|
||||||
|
micromark-util-combine-extensions: 2.0.1
|
||||||
|
string-width: 8.3.0
|
||||||
|
strip-ansi: 7.2.0
|
||||||
|
supports-hyperlinks: 4.6.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
markdown-table@3.0.4: {}
|
||||||
|
|
||||||
|
mdast-util-find-and-replace@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/mdast': 4.0.4
|
||||||
|
escape-string-regexp: 5.0.0
|
||||||
|
unist-util-is: 6.0.1
|
||||||
|
unist-util-visit-parents: 6.0.2
|
||||||
|
|
||||||
|
mdast-util-from-markdown@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/mdast': 4.0.4
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
decode-named-character-reference: 1.3.0
|
||||||
|
devlop: 1.1.0
|
||||||
|
mdast-util-to-string: 4.0.0
|
||||||
|
micromark: 4.0.3
|
||||||
|
micromark-util-decode-numeric-character-reference: 2.0.2
|
||||||
|
micromark-util-decode-string: 2.0.1
|
||||||
|
micromark-util-normalize-identifier: 2.0.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
unist-util-stringify-position: 4.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
mdast-util-gfm-autolink-literal@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/mdast': 4.0.4
|
||||||
|
ccount: 2.0.1
|
||||||
|
devlop: 1.1.0
|
||||||
|
mdast-util-find-and-replace: 3.0.3
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
|
||||||
|
mdast-util-gfm-footnote@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/mdast': 4.0.4
|
||||||
|
devlop: 1.1.0
|
||||||
|
mdast-util-from-markdown: 2.1.0
|
||||||
|
mdast-util-to-markdown: 2.2.0
|
||||||
|
micromark-util-normalize-identifier: 2.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
mdast-util-gfm-strikethrough@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/mdast': 4.0.4
|
||||||
|
mdast-util-from-markdown: 2.1.0
|
||||||
|
mdast-util-to-markdown: 2.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
mdast-util-gfm-table@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/mdast': 4.0.4
|
||||||
|
devlop: 1.1.0
|
||||||
|
markdown-table: 3.0.4
|
||||||
|
mdast-util-from-markdown: 2.1.0
|
||||||
|
mdast-util-to-markdown: 2.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
mdast-util-gfm-task-list-item@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/mdast': 4.0.4
|
||||||
|
devlop: 1.1.0
|
||||||
|
mdast-util-from-markdown: 2.1.0
|
||||||
|
mdast-util-to-markdown: 2.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
mdast-util-gfm@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
mdast-util-from-markdown: 2.1.0
|
||||||
|
mdast-util-gfm-autolink-literal: 2.0.1
|
||||||
|
mdast-util-gfm-footnote: 2.1.0
|
||||||
|
mdast-util-gfm-strikethrough: 2.0.1
|
||||||
|
mdast-util-gfm-table: 2.0.0
|
||||||
|
mdast-util-gfm-task-list-item: 2.0.0
|
||||||
|
mdast-util-to-markdown: 2.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
mdast-util-phrasing@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/mdast': 4.0.4
|
||||||
|
unist-util-is: 6.0.1
|
||||||
|
|
||||||
|
mdast-util-to-markdown@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/mdast': 4.0.4
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
longest-streak: 3.1.0
|
||||||
|
mdast-util-phrasing: 4.1.0
|
||||||
|
mdast-util-to-string: 4.0.0
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-classify-character: 2.0.1
|
||||||
|
micromark-util-decode-string: 2.0.1
|
||||||
|
micromark-util-html-tag-name: 2.0.1
|
||||||
|
unist-util-visit: 5.1.0
|
||||||
|
zwitch: 2.0.4
|
||||||
|
|
||||||
|
mdast-util-to-string@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/mdast': 4.0.4
|
||||||
|
|
||||||
|
micromark-core-commonmark@2.0.4:
|
||||||
|
dependencies:
|
||||||
|
decode-named-character-reference: 1.3.0
|
||||||
|
devlop: 1.1.0
|
||||||
|
micromark-factory-destination: 2.0.1
|
||||||
|
micromark-factory-label: 2.0.1
|
||||||
|
micromark-factory-space: 2.1.0
|
||||||
|
micromark-factory-title: 2.0.1
|
||||||
|
micromark-factory-whitespace: 2.0.1
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-chunked: 2.0.1
|
||||||
|
micromark-util-classify-character: 2.0.1
|
||||||
|
micromark-util-edit-map: 1.0.0
|
||||||
|
micromark-util-html-tag-name: 2.0.1
|
||||||
|
micromark-util-normalize-identifier: 2.0.1
|
||||||
|
micromark-util-resolve-all: 2.0.1
|
||||||
|
micromark-util-subtokenize: 2.1.0
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-extension-gfm-autolink-literal@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-sanitize-uri: 2.0.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-extension-gfm-footnote@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
devlop: 1.1.0
|
||||||
|
micromark-core-commonmark: 2.0.4
|
||||||
|
micromark-factory-space: 2.1.0
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-normalize-identifier: 2.0.1
|
||||||
|
micromark-util-sanitize-uri: 2.0.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-extension-gfm-strikethrough@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
devlop: 1.1.0
|
||||||
|
micromark-util-chunked: 2.0.1
|
||||||
|
micromark-util-classify-character: 2.0.1
|
||||||
|
micromark-util-resolve-all: 2.0.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-extension-gfm-table@2.1.2:
|
||||||
|
dependencies:
|
||||||
|
devlop: 1.1.0
|
||||||
|
micromark-factory-space: 2.1.0
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-extension-gfm-tagfilter@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-extension-gfm-task-list-item@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
devlop: 1.1.0
|
||||||
|
micromark-factory-space: 2.1.0
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-extension-gfm@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
micromark-extension-gfm-autolink-literal: 2.1.0
|
||||||
|
micromark-extension-gfm-footnote: 2.1.0
|
||||||
|
micromark-extension-gfm-strikethrough: 2.1.0
|
||||||
|
micromark-extension-gfm-table: 2.1.2
|
||||||
|
micromark-extension-gfm-tagfilter: 2.0.0
|
||||||
|
micromark-extension-gfm-task-list-item: 2.1.0
|
||||||
|
micromark-util-combine-extensions: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-factory-destination@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-factory-label@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
devlop: 1.1.0
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-factory-space@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-factory-title@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
micromark-factory-space: 2.1.0
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-factory-whitespace@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
micromark-factory-space: 2.1.0
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-util-character@2.1.1:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-util-chunked@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
|
||||||
|
micromark-util-classify-character@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-util-combine-extensions@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-chunked: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-util-decode-numeric-character-reference@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
|
||||||
|
micromark-util-decode-string@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
decode-named-character-reference: 1.3.0
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-decode-numeric-character-reference: 2.0.2
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
|
||||||
|
micromark-util-edit-map@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-util-encode@2.0.1: {}
|
||||||
|
|
||||||
|
micromark-util-html-tag-name@2.0.1: {}
|
||||||
|
|
||||||
|
micromark-util-normalize-identifier@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
|
||||||
|
micromark-util-resolve-all@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-util-sanitize-uri@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-encode: 2.0.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
|
||||||
|
micromark-util-subtokenize@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
devlop: 1.1.0
|
||||||
|
micromark-util-chunked: 2.0.1
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
|
||||||
|
micromark-util-symbol@2.0.1: {}
|
||||||
|
|
||||||
|
micromark-util-types@2.0.3: {}
|
||||||
|
|
||||||
|
micromark@4.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/debug': 4.1.13
|
||||||
|
debug: 4.4.3
|
||||||
|
decode-named-character-reference: 1.3.0
|
||||||
|
devlop: 1.1.0
|
||||||
|
micromark-core-commonmark: 2.0.4
|
||||||
|
micromark-factory-space: 2.1.0
|
||||||
|
micromark-util-character: 2.1.1
|
||||||
|
micromark-util-chunked: 2.0.1
|
||||||
|
micromark-util-combine-extensions: 2.0.1
|
||||||
|
micromark-util-decode-numeric-character-reference: 2.0.2
|
||||||
|
micromark-util-edit-map: 1.0.0
|
||||||
|
micromark-util-encode: 2.0.1
|
||||||
|
micromark-util-normalize-identifier: 2.0.1
|
||||||
|
micromark-util-resolve-all: 2.0.1
|
||||||
|
micromark-util-sanitize-uri: 2.0.1
|
||||||
|
micromark-util-subtokenize: 2.1.0
|
||||||
|
micromark-util-symbol: 2.0.1
|
||||||
|
micromark-util-types: 2.0.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
mimic-fn@2.1.0: {}
|
mimic-fn@2.1.0: {}
|
||||||
|
|
||||||
mlly@1.8.2:
|
mlly@1.8.2:
|
||||||
|
|
@ -2119,6 +2661,13 @@ snapshots:
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
ts-interface-checker: 0.1.13
|
ts-interface-checker: 0.1.13
|
||||||
|
|
||||||
|
supports-color@10.2.2: {}
|
||||||
|
|
||||||
|
supports-hyperlinks@4.6.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 5.0.1
|
||||||
|
supports-color: 10.2.2
|
||||||
|
|
||||||
tagged-tag@1.0.0: {}
|
tagged-tag@1.0.0: {}
|
||||||
|
|
||||||
terminal-size@4.0.1: {}
|
terminal-size@4.0.1: {}
|
||||||
|
|
@ -2198,6 +2747,25 @@ snapshots:
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
|
unist-util-is@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
|
unist-util-stringify-position@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
|
unist-util-visit-parents@6.0.2:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
unist-util-is: 6.0.1
|
||||||
|
|
||||||
|
unist-util-visit@5.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
unist-util-is: 6.0.1
|
||||||
|
unist-util-visit-parents: 6.0.2
|
||||||
|
|
||||||
vite-node@2.1.9(@types/node@22.20.5):
|
vite-node@2.1.9(@types/node@22.20.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
|
|
@ -2277,3 +2845,5 @@ snapshots:
|
||||||
ws@8.22.0: {}
|
ws@8.22.0: {}
|
||||||
|
|
||||||
yoga-layout@3.2.1: {}
|
yoga-layout@3.2.1: {}
|
||||||
|
|
||||||
|
zwitch@2.0.4: {}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue