@imenam/mcp-messenger 1.0.18 → 1.0.20
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 +46 -21
- package/dist/src/core/store.d.ts +11 -0
- package/dist/src/core/store.js +21 -0
- package/dist/src/server/app.js +50 -13
- package/dist/src/server/ui/app.js +1444 -1172
- package/dist/src/server/ui/index.html +48 -6
- package/dist/src/server/ui/styles.css +172 -6
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -395,12 +395,18 @@ correspondants et l'état de lecture :
|
|
|
395
395
|
une par session ouverte, et beaucoup n'échangeront jamais rien. Ces boîtes-là sont
|
|
396
396
|
**masquées par défaut**, derrière un bouton en fin de dépli qui dit combien il y en a ;
|
|
397
397
|
les masquer ne cache jamais de courrier, une boîte sans réception n'ayant par
|
|
398
|
-
construction aucun non-lu. Un agent dont aucune session n'a rien échangé
|
|
399
|
-
derrière
|
|
398
|
+
construction aucun non-lu. **Un agent dont aucune session n'a rien échangé est masqué de
|
|
399
|
+
la même façon**, derrière un bouton en pied de colonne : même geste, un cran plus haut.
|
|
400
|
+
Chaque boîte affiche la date qui la classe, et deux dates
|
|
400
401
|
identiques sont départagées par l'adresse pour que l'ordre ne bouge pas d'un
|
|
401
402
|
rafraîchissement à l'autre ;
|
|
402
403
|
- **la liste des fils**, du plus récemment actif au plus ancien, chacun résumé par son
|
|
403
|
-
nom, ses correspondants, son dernier message et sa priorité la plus haute.
|
|
404
|
+
nom, ses correspondants, son dernier message et sa priorité la plus haute. Les deux
|
|
405
|
+
correspondants y sont d'abord **deux pastilles côte à côte**, séparées et non superposées
|
|
406
|
+
— leurs initiales doivent rester lisibles —, de la couleur que leur adresse leur donne
|
|
407
|
+
partout ailleurs : on voit qui parle avant de lire. Au survol, la
|
|
408
|
+
pastille dit d'où la boîte écrit, ce dont la session s'occupe, son adresse, et si c'est
|
|
409
|
+
elle qui a écrit en dernier. Trois puces
|
|
404
410
|
la filtrent : tous, non lus, priorité haute ;
|
|
405
411
|
- **le lecteur du fil** : la conversation entière dans l'ordre, un bloc par message, avec
|
|
406
412
|
qui écrit à qui — en clair, pas en adresses — et une pastille disant si le destinataire
|
|
@@ -417,6 +423,10 @@ correspondants et l'état de lecture :
|
|
|
417
423
|
dossier, état). Tout ce qui se recopie a son bouton de copie ;
|
|
418
424
|
- une recherche unique, qui couvre le nom d'un fil, sa description et le texte de ses
|
|
419
425
|
messages ;
|
|
426
|
+
- **des panneaux redimensionnables** : chaque ligne de séparation se saisit à la souris,
|
|
427
|
+
se règle aussi aux flèches du clavier une fois qu'elle a le focus, et rend sa largeur
|
|
428
|
+
d'origine sur un double-clic. Les largeurs choisies sont retenues par le navigateur d'une
|
|
429
|
+
visite à l'autre ;
|
|
420
430
|
- un rafraîchissement automatique toutes les 5 secondes, débrayable.
|
|
421
431
|
|
|
422
432
|
**Les adresses ne sont pas de l'information de lecture.** La console résout partout
|
|
@@ -440,23 +450,35 @@ navigateur de l'administrateur. Les routes `/admin` acceptent toujours le jeton
|
|
|
440
450
|
`Authorization: Bearer` pour les scripts. Le formulaire est limité à 10 tentatives par
|
|
441
451
|
quart d'heure et par IP.
|
|
442
452
|
|
|
443
|
-
La console n'envoie ni ne modifie aucun message.
|
|
444
|
-
|
|
445
|
-
|
|
453
|
+
La console n'envoie ni ne modifie aucun message. Les seules écritures qu'elle commande
|
|
454
|
+
sont des suppressions, derrière l'icône de corbeille de la barre du haut. **Deux
|
|
455
|
+
nettoyages y cohabitent, avec chacun son bouton, parce qu'ils ne visent pas la même
|
|
456
|
+
chose :**
|
|
446
457
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
458
|
+
- **les boîtes vides** : rien n'y est jamais passé, ni envoi ni réception. Elles n'ont pas
|
|
459
|
+
d'âge, une boîte vide est vide qu'elle date d'un mois ou d'une minute. C'est le
|
|
460
|
+
nettoyage à passer sur un serveur qui a vu défiler beaucoup de sessions muettes. **Un
|
|
461
|
+
agent dont toutes les boîtes disparaissent quitte l'annuaire avec elles**, faute de
|
|
462
|
+
boîte à regrouper ;
|
|
463
|
+
- **les boîtes inactives depuis une date** : elles ont servi, mais plus rien ne les a
|
|
464
|
+
touchées depuis. **Une boîte créée après cette date est épargnée, même vide** — une
|
|
465
|
+
session qui vient de démarrer et n'a pas encore parlé n'est pas une boîte à l'abandon.
|
|
451
466
|
|
|
452
|
-
|
|
467
|
+
S'y ajoute la suppression d'**une boîte en particulier** : chaque ligne de la colonne de
|
|
468
|
+
gauche porte une corbeille, révélée au survol ou au clavier. Elle n'efface rien d'elle-même,
|
|
469
|
+
elle ouvre la même fenêtre sur cette seule boîte, avec ce qu'elle contient sous les yeux.
|
|
470
|
+
C'est la seule des trois qui ne demande aucun critère : l'adresse vient d'être montrée du
|
|
471
|
+
doigt, il n'y a rien à revérifier.
|
|
472
|
+
|
|
473
|
+
Toutes se font en deux temps, parce qu'elles ne se rattrapent pas :
|
|
453
474
|
|
|
454
475
|
1. **L'aperçu** liste les boîtes concernées, chacune avec son nom, son adresse, son nombre
|
|
455
476
|
de messages et sa dernière activité. Chaque ligne se décoche.
|
|
456
|
-
2. **La confirmation** demande deux clics, et n'envoie que les adresses affichées
|
|
457
|
-
serveur, lui, reconfronte chacune à
|
|
458
|
-
|
|
459
|
-
|
|
477
|
+
2. **La confirmation** demande deux clics, et n'envoie que les adresses affichées, avec le
|
|
478
|
+
critère qui les a désignées. Le serveur, lui, reconfronte chacune à ce critère avant de
|
|
479
|
+
supprimer : une boîte redevenue active, ou qui vient de recevoir son premier message,
|
|
480
|
+
fait échouer le nettoyage entier, nommément. Changer de critère, changer la date ou
|
|
481
|
+
décocher une ligne annule une confirmation déjà donnée.
|
|
460
482
|
|
|
461
483
|
**Ce qui disparaît.** Un message a deux copies, une par correspondant : supprimer une
|
|
462
484
|
boîte n'emporte que sa moitié. Le message lui-même ne s'efface qu'une fois sa dernière
|
|
@@ -532,7 +554,9 @@ la console (cookie), qui elle n'ouvre rien d'autre.
|
|
|
532
554
|
| `GET` | `/admin/threads` | Fils du serveur entier (`agent` — une ou plusieurs adresses séparées par des virgules —, `search`, `unread_only`, `priority`, `limit`, `offset`) |
|
|
533
555
|
| `GET` | `/admin/threads/:id` | Fil complet, corps inclus |
|
|
534
556
|
| `GET` | `/admin/mailboxes/stale` | Boîtes sans activité depuis `before`, avec ce que chacune contient |
|
|
535
|
-
| `
|
|
557
|
+
| `GET` | `/admin/mailboxes/empty` | Boîtes par lesquelles rien n'est jamais passé, sans critère de date |
|
|
558
|
+
| `POST` | `/admin/mailboxes/purge` | Supprime les boîtes nommées (`mailboxes`, plus `before` **ou** `empty: true`) — refusé si l'une d'elles ne répond plus au critère |
|
|
559
|
+
| `DELETE` | `/admin/mailboxes/:id` | Supprime une boîte désignée, quel que soit son état — 404 si l'adresse est inconnue |
|
|
536
560
|
| `POST` | `/ui/login` · `/ui/logout` | Session de la console (`{ "token": … }` → cookie `HttpOnly`) |
|
|
537
561
|
|
|
538
562
|
---
|
|
@@ -555,9 +579,9 @@ la console (cookie), qui elle n'ouvre rien d'autre.
|
|
|
555
579
|
- La console web n'insère jamais de HTML fourni par un agent : le corps des messages est
|
|
556
580
|
rendu en markdown par construction de nœuds, jamais par assignation de balisage, et la
|
|
557
581
|
CSP interdit tout script tiers.
|
|
558
|
-
- La console ne peut ni écrire ni modifier un message.
|
|
559
|
-
|
|
560
|
-
|
|
582
|
+
- La console ne peut ni écrire ni modifier un message. Ses seules commandes destructrices
|
|
583
|
+
suppriment des boîtes : réservées au jeton d'administration, confirmées deux fois, et
|
|
584
|
+
revérifiées par le serveur quand elles portent sur un lot.
|
|
561
585
|
- Derrière un reverse proxy TLS, exposer le serveur en `https://` et donner cette URL aux
|
|
562
586
|
agents via `MESSENGER_SERVER_URL`.
|
|
563
587
|
|
|
@@ -598,8 +622,9 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
|
|
|
598
622
|
|
|
599
623
|
### Tests
|
|
600
624
|
|
|
601
|
-
|
|
602
|
-
|
|
625
|
+
266 tests couvrant : les suppressions de boîtes commandées depuis la console — une boîte
|
|
626
|
+
désignée à la main, les vides, celles sans activité depuis une date — avec leur aperçu,
|
|
627
|
+
leur double confirmation et le refus nominatif d'une boîte qui ne répond plus au critère, la reprise de la boîte d'une session interrompue (clé de session de
|
|
603
628
|
l'hôte, boîte orpheline, boîte d'une session vivante jamais reprise), le rendu markdown des messages et l'aplatissement des aperçus (dont
|
|
604
629
|
l'innocuité du rendu : ni HTML interprété, ni lien vers un autre schéma qu'`http`, `https`
|
|
605
630
|
ou `mailto`), le stockage et l'isolation des boîtes, la validation de configuration,
|
package/dist/src/core/store.d.ts
CHANGED
|
@@ -98,6 +98,17 @@ export declare class MessageStore extends EventEmitter<StoreEvents> {
|
|
|
98
98
|
staleMailboxes(before: string): (AgentDirectoryEntry & {
|
|
99
99
|
messages: number;
|
|
100
100
|
})[];
|
|
101
|
+
/**
|
|
102
|
+
* Boîtes par lesquelles rien n'est jamais passé, ni envoi ni réception.
|
|
103
|
+
*
|
|
104
|
+
* Elles n'ont pas d'âge : une boîte vide est vide, qu'elle date d'un mois ou d'une
|
|
105
|
+
* minute. C'est ce qui la distingue d'une boîte inactive, et ce qui rend ce nettoyage
|
|
106
|
+
* différent de la purge par date — la boîte d'une session qui vient d'ouvrir et n'a pas
|
|
107
|
+
* encore parlé en fait donc partie.
|
|
108
|
+
*/
|
|
109
|
+
emptyMailboxes(): (AgentDirectoryEntry & {
|
|
110
|
+
messages: number;
|
|
111
|
+
})[];
|
|
101
112
|
/**
|
|
102
113
|
* Supprime ces boîtes et tout ce qui n'appartenait qu'à elles.
|
|
103
114
|
*
|
package/dist/src/core/store.js
CHANGED
|
@@ -603,6 +603,27 @@ export class MessageStore extends EventEmitter {
|
|
|
603
603
|
last_activity_at: row.last_activity_at || null,
|
|
604
604
|
}));
|
|
605
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* Boîtes par lesquelles rien n'est jamais passé, ni envoi ni réception.
|
|
608
|
+
*
|
|
609
|
+
* Elles n'ont pas d'âge : une boîte vide est vide, qu'elle date d'un mois ou d'une
|
|
610
|
+
* minute. C'est ce qui la distingue d'une boîte inactive, et ce qui rend ce nettoyage
|
|
611
|
+
* différent de la purge par date — la boîte d'une session qui vient d'ouvrir et n'a pas
|
|
612
|
+
* encore parlé en fait donc partie.
|
|
613
|
+
*/
|
|
614
|
+
emptyMailboxes() {
|
|
615
|
+
const rows = this.db
|
|
616
|
+
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at, 0 AS messages
|
|
617
|
+
FROM agents a
|
|
618
|
+
WHERE NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.agent_id = a.id)
|
|
619
|
+
ORDER BY a.created_at ASC`)
|
|
620
|
+
.all();
|
|
621
|
+
return rows.map((row) => ({
|
|
622
|
+
...row,
|
|
623
|
+
registered: Boolean(row.registered),
|
|
624
|
+
last_activity_at: row.last_activity_at || null,
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
606
627
|
/**
|
|
607
628
|
* Supprime ces boîtes et tout ce qui n'appartenait qu'à elles.
|
|
608
629
|
*
|
package/dist/src/server/app.js
CHANGED
|
@@ -102,10 +102,21 @@ const isoDate = z
|
|
|
102
102
|
.regex(/^\d{4}-\d{2}-\d{2}(T[\d:.]+Z?)?$/, "expected a date such as 2026-08-01")
|
|
103
103
|
.refine((value) => !Number.isNaN(Date.parse(value)), "not a real date");
|
|
104
104
|
const staleSchema = z.object({ before: isoDate });
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Une suppression désigne ses boîtes, et rappelle à quel titre elles sont supprimables :
|
|
107
|
+
* `before`, elles n'ont plus d'activité depuis cette date ; `empty`, rien n'y est jamais
|
|
108
|
+
* passé. Les deux critères s'excluent — le serveur revérifie celui qu'on lui donne, il
|
|
109
|
+
* doit donc savoir lequel c'est.
|
|
110
|
+
*/
|
|
111
|
+
const purgeSchema = z
|
|
112
|
+
.object({
|
|
113
|
+
before: isoDate.optional(),
|
|
114
|
+
empty: z.literal(true).optional(),
|
|
107
115
|
/** Les adresses vues dans l'aperçu : on ne supprime jamais autre chose que ce qui a été montré. */
|
|
108
116
|
mailboxes: z.array(z.string().trim().min(1)).min(1).max(500),
|
|
117
|
+
})
|
|
118
|
+
.refine((input) => (input.before === undefined) !== (input.empty === undefined), {
|
|
119
|
+
message: "provide either `before` (inactive since that date) or `empty: true`, not both",
|
|
109
120
|
});
|
|
110
121
|
const adminListSchema = z.object({
|
|
111
122
|
search: z.string().trim().min(1).optional(),
|
|
@@ -175,7 +186,7 @@ export function createApp({ store, config }) {
|
|
|
175
186
|
app.get("/", (_req, res) => {
|
|
176
187
|
res.redirect("/ui/");
|
|
177
188
|
});
|
|
178
|
-
// --- Supervision (lecture, sauf
|
|
189
|
+
// --- Supervision (lecture, sauf les nettoyages de boîtes) ----------------
|
|
179
190
|
// Montée avant la porte d'entrée : son contrôle est au moins aussi strict (session de
|
|
180
191
|
// console ou jeton admin), et la session par cookie n'est reconnue nulle part ailleurs.
|
|
181
192
|
const admin = express.Router();
|
|
@@ -225,24 +236,50 @@ export function createApp({ store, config }) {
|
|
|
225
236
|
const { before } = staleSchema.parse(req.query);
|
|
226
237
|
res.json({ before, mailboxes: store.staleMailboxes(before) });
|
|
227
238
|
});
|
|
239
|
+
/**
|
|
240
|
+
* Boîtes par lesquelles rien n'est jamais passé. Sans date : une boîte vide l'est
|
|
241
|
+
* indépendamment de son âge, et un agent dont toutes les boîtes sont vides disparaît
|
|
242
|
+
* de l'annuaire du même coup, faute de boîte à regrouper.
|
|
243
|
+
*/
|
|
244
|
+
admin.get("/mailboxes/empty", (_req, res) => {
|
|
245
|
+
res.json({ mailboxes: store.emptyMailboxes() });
|
|
246
|
+
});
|
|
228
247
|
/**
|
|
229
248
|
* La seule route de la console qui écrit, et elle détruit.
|
|
230
249
|
*
|
|
231
|
-
* Deux garde-fous, parce qu'
|
|
232
|
-
* les adresses, et le serveur refuse toute adresse qui
|
|
233
|
-
*
|
|
234
|
-
*
|
|
250
|
+
* Deux garde-fous, parce qu'un nettoyage ne se rattrape pas : le critère est redemandé
|
|
251
|
+
* avec les adresses, et le serveur refuse toute adresse qui n'y répond plus au moment
|
|
252
|
+
* où il l'examine. Une boîte redevenue vivante, ou qui vient de recevoir son premier
|
|
253
|
+
* message, est donc épargnée — et le refus est nominatif, il porte sur le lot entier.
|
|
235
254
|
*/
|
|
236
255
|
admin.post("/mailboxes/purge", (req, res) => {
|
|
237
256
|
const input = purgeSchema.parse(req.body ?? {});
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
257
|
+
const reason = input.empty ? "empty" : `inactive since ${input.before}`;
|
|
258
|
+
const eligible = new Set((input.empty ? store.emptyMailboxes() : store.staleMailboxes(input.before)).map((mailbox) => mailbox.id));
|
|
259
|
+
const changed = input.mailboxes.filter((id) => !eligible.has(id));
|
|
260
|
+
if (changed.length > 0) {
|
|
261
|
+
throw new MessengerError(`these mailboxes are no longer ${reason}, nothing was deleted: ${changed.join(", ")}`, 409);
|
|
242
262
|
}
|
|
243
263
|
const removed = store.deleteMailboxes(input.mailboxes);
|
|
244
|
-
logger.warn(`admin purge: ${removed.mailboxes} mailbox(es) deleted,
|
|
245
|
-
res.json({ before: input.before, ...removed });
|
|
264
|
+
logger.warn(`admin purge: ${removed.mailboxes} mailbox(es) deleted, ${reason}`);
|
|
265
|
+
res.json({ before: input.before ?? null, empty: input.empty ?? false, ...removed });
|
|
266
|
+
});
|
|
267
|
+
/**
|
|
268
|
+
* Suppression d'une boîte désignée à la main, quel que soit son état.
|
|
269
|
+
*
|
|
270
|
+
* Pas de critère à revérifier ici, et c'est voulu : les deux nettoyages en lot se
|
|
271
|
+
* protègent d'une liste qui aurait vieilli entre l'aperçu et la confirmation, alors que
|
|
272
|
+
* celle-ci porte sur une boîte que l'on vient de montrer du doigt. La boîte doit
|
|
273
|
+
* exister — supprimer une adresse inconnue serait un geste sans effet présenté comme un
|
|
274
|
+
* succès.
|
|
275
|
+
*/
|
|
276
|
+
admin.delete("/mailboxes/:id", (req, res) => {
|
|
277
|
+
const id = normalizeAgentId(String(req.params.id));
|
|
278
|
+
if (!store.getAgent(id))
|
|
279
|
+
throw new MessengerError(`mailbox not found: ${id}`, 404);
|
|
280
|
+
const removed = store.deleteMailboxes([id]);
|
|
281
|
+
logger.warn(`admin delete: mailbox ${id} deleted (${removed.messages} message(s) erased)`);
|
|
282
|
+
res.json({ mailbox: id, ...removed });
|
|
246
283
|
});
|
|
247
284
|
app.use("/admin", admin);
|
|
248
285
|
// Porte d'entrée de tout le reste : aucune requête n'est servie sans jeton valide. Le
|