@imenam/mcp-messenger 1.0.15 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -2
- package/dist/src/core/store.js +36 -1
- package/dist/src/server/ui/app.js +17 -2
- package/dist/src/server/ui/markdown.js +340 -0
- package/dist/src/server/ui/styles.css +145 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -362,7 +362,14 @@ correspondants et l'état de lecture :
|
|
|
362
362
|
la filtrent : tous, non lus, priorité haute ;
|
|
363
363
|
- **le lecteur du fil** : la conversation entière dans l'ordre, un bloc par message, avec
|
|
364
364
|
qui écrit à qui — en clair, pas en adresses — et une pastille disant si le destinataire
|
|
365
|
-
l'a lu
|
|
365
|
+
l'a lu. **Le corps d'un message est rendu comme du markdown** : titres, listes,
|
|
366
|
+
citations, tableaux, blocs de code, emphase et liens. C'est la langue dans laquelle les
|
|
367
|
+
agents s'écrivent, la console la lit donc ainsi plutôt que d'afficher sa syntaxe. Le
|
|
368
|
+
rendu construit des nœuds un par un et n'insère jamais de HTML : rien de ce qu'un agent
|
|
369
|
+
écrit ne peut devenir une balise, et seuls les liens `http`, `https` et `mailto` sont
|
|
370
|
+
suivis. Une image n'est jamais chargée, elle est montrée par son intitulé et son lien.
|
|
371
|
+
Les aperçus, eux, sont débarrassés de leur syntaxe par le serveur : ils tiennent sur une
|
|
372
|
+
ligne, où un dièse ou une paire d'astérisques ne serait que du bruit ;
|
|
366
373
|
- **le volet de détail** d'un message, ouvert à la demande : identifiants du message et du
|
|
367
374
|
fil, les deux noms et l'adresse de chaque boîte, et le tableau de distribution (rôle,
|
|
368
375
|
dossier, état). Tout ce qui se recopie a son bouton de copie ;
|
|
@@ -508,6 +515,7 @@ src/
|
|
|
508
515
|
server/watchers.ts # présence des veilleurs, en mémoire : état de connexion
|
|
509
516
|
server/start.ts # démarrage, arrêt propre, purge de la corbeille
|
|
510
517
|
server/ui/ # console web (HTML/CSS/JS sans dépendance)
|
|
518
|
+
server/ui/markdown.js # rendu markdown du corps des messages, en nœuds DOM
|
|
511
519
|
cli/watch.ts # mode veilleur : une ligne sur stdout par message entrant
|
|
512
520
|
cli/watch-handoff.ts # relais 0600 entre la session MCP et son veilleur
|
|
513
521
|
client/ # client HTTP utilisé par le mode MCP
|
|
@@ -517,7 +525,9 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
|
|
|
517
525
|
|
|
518
526
|
### Tests
|
|
519
527
|
|
|
520
|
-
|
|
528
|
+
187 tests couvrant : le rendu markdown des messages et l'aplatissement des aperçus (dont
|
|
529
|
+
l'innocuité du rendu : ni HTML interprété, ni lien vers un autre schéma qu'`http`, `https`
|
|
530
|
+
ou `mailto`), le stockage et l'isolation des boîtes, la validation de configuration,
|
|
521
531
|
l'API HTTP et son authentification (refus systématique sans jeton, jeton partagé, jetons par
|
|
522
532
|
agent, jeton de console distinct), les outils MCP de bout en bout (deux agents qui
|
|
523
533
|
conversent à travers un vrai serveur), l'allocation des boîtes par session — deux processus
|
package/dist/src/core/store.js
CHANGED
|
@@ -848,9 +848,44 @@ function clamp(value, min, max) {
|
|
|
848
848
|
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
849
849
|
}
|
|
850
850
|
function preview(body) {
|
|
851
|
-
const flat = body.replace(/\s+/g, " ").trim();
|
|
851
|
+
const flat = stripMarkdown(body).replace(/\s+/g, " ").trim();
|
|
852
852
|
return flat.length <= PREVIEW_LENGTH ? flat : `${flat.slice(0, PREVIEW_LENGTH - 1)}…`;
|
|
853
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* Le corps d'un message est du markdown ; un aperçu, non — il tient sur une ligne, dans
|
|
856
|
+
* une liste, sans mise en forme pour le porter. La syntaxe y devient donc du bruit : on
|
|
857
|
+
* garde ce que le markdown dit, on jette ce qui sert à le mettre en forme.
|
|
858
|
+
*
|
|
859
|
+
* C'est un aplatissement, pas une analyse : l'aperçu n'a pas à rendre compte de la
|
|
860
|
+
* structure du message, seulement à en donner les premiers mots lisibles.
|
|
861
|
+
*/
|
|
862
|
+
function stripMarkdown(body) {
|
|
863
|
+
return (body
|
|
864
|
+
// Un bloc de code n'a rien de lisible en 160 caractères : seule sa langue renseigne.
|
|
865
|
+
.replace(/^ {0,3}(```|~~~)[^\n]*\n[\s\S]*?(?:\n {0,3}\1[^\n]*|$)/gm, (block) => {
|
|
866
|
+
const language = /^ {0,3}(?:```|~~~)\s*([^\s`]+)/.exec(block)?.[1];
|
|
867
|
+
return language ? `[code ${language}]` : "[code]";
|
|
868
|
+
})
|
|
869
|
+
.replace(/`+([^`]+)`+/g, "$1")
|
|
870
|
+
// Images et liens : l'intitulé porte le sens, l'URL encombre.
|
|
871
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
872
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
873
|
+
.replace(/<((?:https?:\/\/|mailto:)[^\s>]+)>/g, "$1")
|
|
874
|
+
// Marqueurs de bloc en tête de ligne : titres, citations, puces, filets.
|
|
875
|
+
.replace(/^ {0,3}#{1,6}\s+/gm, "")
|
|
876
|
+
.replace(/^ {0,3}>[ \t]?/gm, "")
|
|
877
|
+
.replace(/^\s*([-*+]|\d{1,9}[.)])[ \t]+/gm, "· ")
|
|
878
|
+
.replace(/^ {0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/gm, " ")
|
|
879
|
+
// Un tableau perd sa grille : ses cellules restent, séparées.
|
|
880
|
+
.replace(/^ {0,3}\|?[ \t]*:?-+:?[ \t]*(\|[ \t]*:?-+:?[ \t]*)+\|?[ \t]*$/gm, " ")
|
|
881
|
+
.replace(/^[ \t]*\|/gm, "")
|
|
882
|
+
.replace(/\|[ \t]*$/gm, "")
|
|
883
|
+
.replace(/[ \t]*\|[ \t]*/g, " · ")
|
|
884
|
+
// Emphase : les marques tombent, le texte reste.
|
|
885
|
+
.replace(/(\*\*|__)(\S[\s\S]*?\S)\1/g, "$2")
|
|
886
|
+
.replace(/~~(\S[\s\S]*?\S)~~/g, "$1")
|
|
887
|
+
.replace(/(?<![\w*])([*_])(\S[^*_\n]*?\S)\1(?![\w*])/g, "$2"));
|
|
888
|
+
}
|
|
854
889
|
/** Échappe les jokers SQL : la recherche reste littérale, `%` cherche bien un `%`. */
|
|
855
890
|
function escapeLike(value) {
|
|
856
891
|
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Tout le contenu affiché provient d'agents : il est inséré via textContent uniquement,
|
|
4
4
|
// jamais par innerHTML. Les seuls fragments de balisage construits ici sont les icônes,
|
|
5
|
-
// écrites en dur dans ce fichier.
|
|
5
|
+
// écrites en dur dans ce fichier. Le corps des messages passe par `renderMarkdown`, qui
|
|
6
|
+
// bâtit lui aussi des nœuds un par un et n'ouvre donc pas non plus de brèche.
|
|
6
7
|
//
|
|
7
8
|
// L'authentification tient dans un cookie de session HttpOnly posé par `/ui/login` :
|
|
8
9
|
// ce script ne voit jamais le jeton et n'écrit rien dans le navigateur.
|
|
9
10
|
|
|
11
|
+
import { renderMarkdown } from "./markdown.js";
|
|
12
|
+
|
|
10
13
|
const REFRESH_MS = 5000;
|
|
11
14
|
|
|
12
15
|
// Une adresse ne dit rien à qui lit la console : elle sert de graine à la couleur de
|
|
@@ -687,7 +690,7 @@ function messageRow(message, isLast) {
|
|
|
687
690
|
if (message.subject && message.subject !== message.thread_title) {
|
|
688
691
|
column.appendChild(node("p", "message-subject", message.subject));
|
|
689
692
|
}
|
|
690
|
-
column.appendChild(
|
|
693
|
+
column.appendChild(messageText(message));
|
|
691
694
|
|
|
692
695
|
const foot = node("div", "message-foot");
|
|
693
696
|
foot.appendChild(
|
|
@@ -709,6 +712,18 @@ function messageRow(message, isLast) {
|
|
|
709
712
|
return item;
|
|
710
713
|
}
|
|
711
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Le corps d'un message, rendu comme du markdown : c'est la langue dans laquelle les
|
|
717
|
+
* agents s'écrivent. L'aperçu, lui, arrive déjà débarrassé de sa syntaxe par le serveur
|
|
718
|
+
* et reste donc du texte simple.
|
|
719
|
+
*/
|
|
720
|
+
function messageText(message) {
|
|
721
|
+
if (!message.body) return node("p", "message-text", message.preview || "");
|
|
722
|
+
const container = node("div", "message-text markdown");
|
|
723
|
+
container.appendChild(renderMarkdown(message.body));
|
|
724
|
+
return container;
|
|
725
|
+
}
|
|
726
|
+
|
|
712
727
|
// --- Volet de détail -------------------------------------------------------
|
|
713
728
|
|
|
714
729
|
function renderSheet(message) {
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendu Markdown de la console.
|
|
3
|
+
*
|
|
4
|
+
* Les agents s'écrivent en markdown : un corps de message affiché tel quel se lit mal
|
|
5
|
+
* (titres en dièses, listes en tirets, blocs de code noyés dans le texte). Ce module
|
|
6
|
+
* traduit ce markdown en DOM.
|
|
7
|
+
*
|
|
8
|
+
* Il construit des nœuds, jamais du HTML : tout texte venu d'un message passe par
|
|
9
|
+
* `textContent`, donc rien de ce qu'un agent écrit ne peut devenir une balise. Les seules
|
|
10
|
+
* URL suivies sont http, https et mailto ; le reste retombe en texte.
|
|
11
|
+
*
|
|
12
|
+
* La couverture est celle du markdown qu'écrivent les agents : titres, listes, citations,
|
|
13
|
+
* code, tableaux, filets, emphase, liens. Pas de HTML brut, pas de notes de bas de page.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^`\s]*)\s*$/;
|
|
17
|
+
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
18
|
+
const RULE = /^ {0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/;
|
|
19
|
+
const QUOTE = /^ {0,3}>[ \t]?(.*)$/;
|
|
20
|
+
const BULLET = /^(\s*)([-*+])[ \t]+(.*)$/;
|
|
21
|
+
const ORDERED = /^(\s*)(\d{1,9})[.)][ \t]+(.*)$/;
|
|
22
|
+
const TABLE_RULE = /^ {0,3}\|?[ \t]*:?-+:?[ \t]*(\|[ \t]*:?-+:?[ \t]*)+\|?[ \t]*$/;
|
|
23
|
+
const SAFE_HREF = /^(https?:\/\/|mailto:)/i;
|
|
24
|
+
|
|
25
|
+
/** Rend un corps de message et renvoie un fragment prêt à être inséré. */
|
|
26
|
+
export function renderMarkdown(source) {
|
|
27
|
+
const fragment = document.createDocumentFragment();
|
|
28
|
+
const lines = String(source ?? "")
|
|
29
|
+
.replace(/\r\n?/g, "\n")
|
|
30
|
+
.split("\n");
|
|
31
|
+
for (const child of parseBlocks(lines)) fragment.appendChild(child);
|
|
32
|
+
return fragment;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Blocs -----------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
function parseBlocks(lines) {
|
|
38
|
+
const out = [];
|
|
39
|
+
let i = 0;
|
|
40
|
+
|
|
41
|
+
while (i < lines.length) {
|
|
42
|
+
const line = lines[i];
|
|
43
|
+
|
|
44
|
+
if (!line.trim()) {
|
|
45
|
+
i += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fence = FENCE.exec(line);
|
|
50
|
+
if (fence) {
|
|
51
|
+
const closing = new RegExp(`^ {0,3}\\${fence[1][0]}{${fence[1].length},}\\s*$`);
|
|
52
|
+
const body = [];
|
|
53
|
+
i += 1;
|
|
54
|
+
while (i < lines.length && !closing.test(lines[i])) {
|
|
55
|
+
body.push(lines[i]);
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
i += 1; // la clôture, ou la fin du texte si le bloc n'a jamais été refermé
|
|
59
|
+
out.push(codeBlock(body.join("\n"), fence[2]));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const heading = HEADING.exec(line);
|
|
64
|
+
if (heading) {
|
|
65
|
+
const el = document.createElement(`h${heading[1].length}`);
|
|
66
|
+
el.className = "md-heading";
|
|
67
|
+
renderInline(heading[2], el);
|
|
68
|
+
out.push(el);
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (RULE.test(line)) {
|
|
74
|
+
const hr = document.createElement("hr");
|
|
75
|
+
hr.className = "md-rule";
|
|
76
|
+
out.push(hr);
|
|
77
|
+
i += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (QUOTE.test(line)) {
|
|
82
|
+
const inner = [];
|
|
83
|
+
while (i < lines.length && QUOTE.test(lines[i])) {
|
|
84
|
+
inner.push(QUOTE.exec(lines[i])[1]);
|
|
85
|
+
i += 1;
|
|
86
|
+
}
|
|
87
|
+
const quote = document.createElement("blockquote");
|
|
88
|
+
quote.className = "md-quote";
|
|
89
|
+
for (const child of parseBlocks(inner)) quote.appendChild(child);
|
|
90
|
+
out.push(quote);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (BULLET.test(line) || ORDERED.test(line)) {
|
|
95
|
+
const [list, next] = parseList(lines, i);
|
|
96
|
+
out.push(list);
|
|
97
|
+
i = next;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (opensTable(lines, i)) {
|
|
102
|
+
const [table, next] = parseTable(lines, i);
|
|
103
|
+
if (table) {
|
|
104
|
+
out.push(table);
|
|
105
|
+
i = next;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Paragraphe : tout jusqu'à la ligne vide ou au premier bloc d'un autre genre.
|
|
111
|
+
const text = [];
|
|
112
|
+
while (i < lines.length && lines[i].trim() && !(text.length > 0 && startsBlock(lines, i))) {
|
|
113
|
+
text.push(lines[i].trim());
|
|
114
|
+
i += 1;
|
|
115
|
+
}
|
|
116
|
+
const paragraph = document.createElement("p");
|
|
117
|
+
paragraph.className = "md-p";
|
|
118
|
+
renderInline(text.join("\n"), paragraph);
|
|
119
|
+
out.push(paragraph);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Une ligne qui, au milieu d'un paragraphe, ouvre malgré tout un autre bloc. */
|
|
126
|
+
function startsBlock(lines, i) {
|
|
127
|
+
const line = lines[i];
|
|
128
|
+
return (
|
|
129
|
+
FENCE.test(line) ||
|
|
130
|
+
HEADING.test(line) ||
|
|
131
|
+
RULE.test(line) ||
|
|
132
|
+
QUOTE.test(line) ||
|
|
133
|
+
BULLET.test(line) ||
|
|
134
|
+
ORDERED.test(line) ||
|
|
135
|
+
opensTable(lines, i)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function opensTable(lines, i) {
|
|
140
|
+
return i + 1 < lines.length && lines[i].includes("|") && TABLE_RULE.test(lines[i + 1]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function codeBlock(code, language) {
|
|
144
|
+
const pre = document.createElement("pre");
|
|
145
|
+
pre.className = "md-code";
|
|
146
|
+
const el = document.createElement("code");
|
|
147
|
+
if (language) {
|
|
148
|
+
el.className = `language-${language}`;
|
|
149
|
+
pre.dataset.language = language;
|
|
150
|
+
}
|
|
151
|
+
el.textContent = code;
|
|
152
|
+
pre.appendChild(el);
|
|
153
|
+
return pre;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Une liste et tout ce qu'elle contient. L'indentation seule décide de l'imbrication :
|
|
158
|
+
* une ligne plus rentrée que sa puce appartient à l'article ouvert, et c'est en le
|
|
159
|
+
* relisant comme un texte à part entière que la sous-liste apparaît.
|
|
160
|
+
*/
|
|
161
|
+
function parseList(lines, start) {
|
|
162
|
+
const ordered = !BULLET.test(lines[start]);
|
|
163
|
+
const first = ordered ? ORDERED.exec(lines[start]) : BULLET.exec(lines[start]);
|
|
164
|
+
const indent = first[1].length;
|
|
165
|
+
|
|
166
|
+
const list = document.createElement(ordered ? "ol" : "ul");
|
|
167
|
+
list.className = "md-list";
|
|
168
|
+
if (ordered && Number(first[2]) !== 1) list.start = Number(first[2]);
|
|
169
|
+
|
|
170
|
+
let i = start;
|
|
171
|
+
while (i < lines.length) {
|
|
172
|
+
if (!lines[i].trim()) {
|
|
173
|
+
// Une ligne vide ne ferme la liste que si plus rien derrière ne lui appartient.
|
|
174
|
+
if (!continues(lines, i, indent)) break;
|
|
175
|
+
i += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const match = ordered ? ORDERED.exec(lines[i]) : BULLET.exec(lines[i]);
|
|
180
|
+
if (!match || match[1].length !== indent) break;
|
|
181
|
+
|
|
182
|
+
const own = [match[3]];
|
|
183
|
+
i += 1;
|
|
184
|
+
while (i < lines.length && (lines[i].trim() ? leadingSpaces(lines[i]) > indent : continues(lines, i, indent + 1))) {
|
|
185
|
+
own.push(lines[i].slice(Math.min(leadingSpaces(lines[i]), indent + 2)));
|
|
186
|
+
i += 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const item = document.createElement("li");
|
|
190
|
+
item.className = "md-item";
|
|
191
|
+
const children = parseBlocks(own);
|
|
192
|
+
// Un article d'une seule ligne n'a pas besoin d'un paragraphe autour de son texte.
|
|
193
|
+
if (children.length === 1 && children[0].tagName === "P") item.append(...children[0].childNodes);
|
|
194
|
+
else for (const child of children) item.appendChild(child);
|
|
195
|
+
list.appendChild(item);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return [list, i];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Vrai si, après la ligne vide `i`, du texte reste rentré d'au moins `indent`. */
|
|
202
|
+
function continues(lines, i, indent) {
|
|
203
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
204
|
+
if (!lines[j].trim()) continue;
|
|
205
|
+
return leadingSpaces(lines[j]) >= indent;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function leadingSpaces(line) {
|
|
211
|
+
return line.length - line.replace(/^[ \t]*/, "").length;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseTable(lines, start) {
|
|
215
|
+
const header = splitRow(lines[start]);
|
|
216
|
+
const alignments = splitRow(lines[start + 1]).map((cell) => {
|
|
217
|
+
const left = cell.startsWith(":");
|
|
218
|
+
const right = cell.endsWith(":");
|
|
219
|
+
if (left && right) return "center";
|
|
220
|
+
if (right) return "right";
|
|
221
|
+
return left ? "left" : null;
|
|
222
|
+
});
|
|
223
|
+
if (header.length !== alignments.length) return [null, start];
|
|
224
|
+
|
|
225
|
+
const table = document.createElement("table");
|
|
226
|
+
table.className = "md-table";
|
|
227
|
+
const thead = document.createElement("thead");
|
|
228
|
+
thead.appendChild(row(header, alignments, "th"));
|
|
229
|
+
table.appendChild(thead);
|
|
230
|
+
|
|
231
|
+
const tbody = document.createElement("tbody");
|
|
232
|
+
let i = start + 2;
|
|
233
|
+
while (i < lines.length && lines[i].trim() && lines[i].includes("|")) {
|
|
234
|
+
tbody.appendChild(row(splitRow(lines[i]), alignments, "td"));
|
|
235
|
+
i += 1;
|
|
236
|
+
}
|
|
237
|
+
table.appendChild(tbody);
|
|
238
|
+
return [table, i];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function splitRow(line) {
|
|
242
|
+
return line
|
|
243
|
+
.trim()
|
|
244
|
+
.replace(/^\|/, "")
|
|
245
|
+
.replace(/\|$/, "")
|
|
246
|
+
.split("|")
|
|
247
|
+
.map((cell) => cell.trim());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function row(cells, alignments, tag) {
|
|
251
|
+
const tr = document.createElement("tr");
|
|
252
|
+
alignments.forEach((align, index) => {
|
|
253
|
+
const cell = document.createElement(tag);
|
|
254
|
+
if (align) cell.style.textAlign = align;
|
|
255
|
+
renderInline(cells[index] ?? "", cell);
|
|
256
|
+
tr.appendChild(cell);
|
|
257
|
+
});
|
|
258
|
+
return tr;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// --- Enrichissements de ligne ----------------------------------------------
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Les motifs sont confrontés au texte à chaque tour, et celui qui accroche le plus tôt
|
|
265
|
+
* l'emporte. Le code passe en premier à égalité de position : son contenu ne doit plus
|
|
266
|
+
* être relu, un `**` dans un extrait de code est du code et non de la graisse.
|
|
267
|
+
*/
|
|
268
|
+
const INLINE = [
|
|
269
|
+
{ re: /(`+)([\s\S]+?)\1/, build: (m) => textNode("code", "md-inline-code", m[2].trim()) },
|
|
270
|
+
// La cible tolère une paire de parenthèses : une URL en contient parfois, et le lien
|
|
271
|
+
// ne doit pas se refermer sur la première rencontrée.
|
|
272
|
+
{ re: /!\[([^\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+"[^"]*")?\)/, build: (m) => image(m[1], m[2]) },
|
|
273
|
+
{ re: /\[([^\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+"[^"]*")?\)/, build: (m) => link(m[2], m[1]) },
|
|
274
|
+
{ re: /<((?:https?:\/\/|mailto:)[^\s>]+)>/, build: (m) => link(m[1], m[1]) },
|
|
275
|
+
{ re: /(\*\*|__)(?=\S)([\s\S]+?\S)\1/, build: (m) => inlineNode("strong", "md-strong", m[2]) },
|
|
276
|
+
{ re: /(?<![\w*])(\*|_)(?=\S)([^*_\n]+?\S)\1(?![\w*])/, build: (m) => inlineNode("em", "md-em", m[2]) },
|
|
277
|
+
{ re: /~~(?=\S)([\s\S]+?\S)~~/, build: (m) => inlineNode("del", "md-del", m[1]) },
|
|
278
|
+
{ re: /(?<![("[])https?:\/\/[^\s<>"'`]*[^\s<>"'`.,;:!?)\]]/, build: (m) => link(m[0], m[0]) },
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
function renderInline(text, parent) {
|
|
282
|
+
let rest = text;
|
|
283
|
+
|
|
284
|
+
while (rest) {
|
|
285
|
+
let best = null;
|
|
286
|
+
for (const pattern of INLINE) {
|
|
287
|
+
const match = pattern.re.exec(rest);
|
|
288
|
+
if (match && (!best || match.index < best.match.index)) best = { match, pattern };
|
|
289
|
+
if (best && best.match.index === 0) break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!best) break;
|
|
293
|
+
appendText(parent, rest.slice(0, best.match.index));
|
|
294
|
+
parent.appendChild(best.pattern.build(best.match));
|
|
295
|
+
rest = rest.slice(best.match.index + best.match[0].length);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
appendText(parent, rest);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Les retours à la ligne d'un paragraphe sont voulus : un agent aligne, il n'habille pas. */
|
|
302
|
+
function appendText(parent, text) {
|
|
303
|
+
if (!text) return;
|
|
304
|
+
text.split("\n").forEach((part, index) => {
|
|
305
|
+
if (index > 0) parent.appendChild(document.createElement("br"));
|
|
306
|
+
if (part) parent.appendChild(document.createTextNode(part));
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function textNode(tag, className, text) {
|
|
311
|
+
const el = document.createElement(tag);
|
|
312
|
+
el.className = className;
|
|
313
|
+
el.textContent = text;
|
|
314
|
+
return el;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function inlineNode(tag, className, text) {
|
|
318
|
+
const el = document.createElement(tag);
|
|
319
|
+
el.className = className;
|
|
320
|
+
renderInline(text, el);
|
|
321
|
+
return el;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function link(href, label) {
|
|
325
|
+
if (!SAFE_HREF.test(href)) return document.createTextNode(label || href);
|
|
326
|
+
const el = document.createElement("a");
|
|
327
|
+
el.className = "md-link";
|
|
328
|
+
el.href = href;
|
|
329
|
+
el.target = "_blank";
|
|
330
|
+
el.rel = "noopener noreferrer";
|
|
331
|
+
appendText(el, label || href);
|
|
332
|
+
return el;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Une image distante est montrée par son intitulé et son lien : la console ne va rien chercher dehors. */
|
|
336
|
+
function image(alt, src) {
|
|
337
|
+
const el = link(src, alt || src);
|
|
338
|
+
if (el.nodeType === 1) el.classList.add("md-image-link");
|
|
339
|
+
return el;
|
|
340
|
+
}
|
|
@@ -1039,6 +1039,151 @@ body {
|
|
|
1039
1039
|
overflow-wrap: anywhere;
|
|
1040
1040
|
}
|
|
1041
1041
|
|
|
1042
|
+
/* Le markdown est déjà mis en forme : plus besoin de conserver les blancs du source. */
|
|
1043
|
+
.message-text.markdown {
|
|
1044
|
+
white-space: normal;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/* --- Corps de message rendu en markdown --- */
|
|
1048
|
+
|
|
1049
|
+
.markdown > *:first-child {
|
|
1050
|
+
margin-top: 0;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.markdown > *:last-child {
|
|
1054
|
+
margin-bottom: 0;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.md-p {
|
|
1058
|
+
margin: 0 0 8px;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.markdown .md-heading {
|
|
1062
|
+
margin: 14px 0 6px;
|
|
1063
|
+
line-height: 1.35;
|
|
1064
|
+
font-weight: 600;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.markdown h1.md-heading {
|
|
1068
|
+
font-size: 17px;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.markdown h2.md-heading {
|
|
1072
|
+
font-size: 15px;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
.markdown h3.md-heading,
|
|
1076
|
+
.markdown h4.md-heading,
|
|
1077
|
+
.markdown h5.md-heading,
|
|
1078
|
+
.markdown h6.md-heading {
|
|
1079
|
+
font-size: 14px;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
.markdown h4.md-heading,
|
|
1083
|
+
.markdown h5.md-heading,
|
|
1084
|
+
.markdown h6.md-heading {
|
|
1085
|
+
color: var(--muted);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
.md-list {
|
|
1089
|
+
margin: 0 0 8px;
|
|
1090
|
+
padding-left: 20px;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
.md-item {
|
|
1094
|
+
margin: 2px 0;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/* Un article qui porte une sous-liste passe par un paragraphe : il ne doit pas s'en écarter. */
|
|
1098
|
+
.md-item > .md-p {
|
|
1099
|
+
margin-bottom: 2px;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.md-list .md-list {
|
|
1103
|
+
margin: 2px 0;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.md-quote {
|
|
1107
|
+
margin: 0 0 8px;
|
|
1108
|
+
padding: 2px 0 2px 10px;
|
|
1109
|
+
border-left: 3px solid var(--border);
|
|
1110
|
+
color: var(--muted);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.md-rule {
|
|
1114
|
+
margin: 12px 0;
|
|
1115
|
+
border: 0;
|
|
1116
|
+
border-top: 1px solid var(--border);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/* La langue annoncée par l'agent, posée en coin du bloc : elle dit ce qu'on lit. */
|
|
1120
|
+
.md-code[data-language]::before {
|
|
1121
|
+
content: attr(data-language);
|
|
1122
|
+
display: block;
|
|
1123
|
+
margin-bottom: 6px;
|
|
1124
|
+
color: var(--muted);
|
|
1125
|
+
font-size: 11px;
|
|
1126
|
+
letter-spacing: 0.04em;
|
|
1127
|
+
text-transform: uppercase;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
.md-code {
|
|
1131
|
+
margin: 0 0 8px;
|
|
1132
|
+
padding: 10px 12px;
|
|
1133
|
+
border: 1px solid var(--border-soft);
|
|
1134
|
+
border-radius: var(--radius);
|
|
1135
|
+
background: var(--neutral-soft);
|
|
1136
|
+
overflow-x: auto;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
.md-code code {
|
|
1140
|
+
font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1141
|
+
white-space: pre;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
.md-inline-code {
|
|
1145
|
+
padding: 1px 5px;
|
|
1146
|
+
border-radius: 4px;
|
|
1147
|
+
background: var(--neutral-soft);
|
|
1148
|
+
font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1149
|
+
overflow-wrap: anywhere;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
.md-link {
|
|
1153
|
+
color: var(--accent);
|
|
1154
|
+
overflow-wrap: anywhere;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.md-strong {
|
|
1158
|
+
font-weight: 600;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.md-del {
|
|
1162
|
+
color: var(--muted);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.md-table {
|
|
1166
|
+
display: block;
|
|
1167
|
+
width: max-content;
|
|
1168
|
+
max-width: 100%;
|
|
1169
|
+
margin: 0 0 8px;
|
|
1170
|
+
border-collapse: collapse;
|
|
1171
|
+
overflow-x: auto;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
.md-table th,
|
|
1175
|
+
.md-table td {
|
|
1176
|
+
padding: 5px 10px;
|
|
1177
|
+
border: 1px solid var(--border-soft);
|
|
1178
|
+
text-align: left;
|
|
1179
|
+
vertical-align: top;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
.md-table th {
|
|
1183
|
+
background: var(--neutral-soft);
|
|
1184
|
+
font-weight: 600;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1042
1187
|
.message-foot {
|
|
1043
1188
|
display: flex;
|
|
1044
1189
|
align-items: center;
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const APP_VERSION = "1.0.
|
|
1
|
+
export declare const APP_VERSION = "1.0.16";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Généré par scripts/generate-version.js — ne pas modifier à la main.
|
|
2
|
-
export const APP_VERSION = "1.0.
|
|
2
|
+
export const APP_VERSION = "1.0.16";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imenam/mcp-messenger",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "npm run build && vitest run",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"@types/express": "^5.0.6",
|
|
50
50
|
"@types/node": "^22.20.1",
|
|
51
51
|
"@vitest/coverage-v8": "^3.2.7",
|
|
52
|
+
"jsdom": "^29.1.1",
|
|
52
53
|
"typescript": "^5.9.3",
|
|
53
54
|
"vitest": "^3.2.7"
|
|
54
55
|
},
|