@imenam/mcp-messenger 1.0.18 → 1.0.19

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 CHANGED
@@ -395,8 +395,9 @@ 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é passe de même
399
- derrière les autres. Chaque boîte affiche la date qui la classe, et deux dates
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
@@ -417,6 +418,10 @@ correspondants et l'état de lecture :
417
418
  dossier, état). Tout ce qui se recopie a son bouton de copie ;
418
419
  - une recherche unique, qui couvre le nom d'un fil, sa description et le texte de ses
419
420
  messages ;
421
+ - **des panneaux redimensionnables** : chaque ligne de séparation se saisit à la souris,
422
+ se règle aussi aux flèches du clavier une fois qu'elle a le focus, et rend sa largeur
423
+ d'origine sur un double-clic. Les largeurs choisies sont retenues par le navigateur d'une
424
+ visite à l'autre ;
420
425
  - un rafraîchissement automatique toutes les 5 secondes, débrayable.
421
426
 
422
427
  **Les adresses ne sont pas de l'information de lecture.** La console résout partout
@@ -440,23 +445,29 @@ navigateur de l'administrateur. Les routes `/admin` acceptent toujours le jeton
440
445
  `Authorization: Bearer` pour les scripts. Le formulaire est limité à 10 tentatives par
441
446
  quart d'heure et par IP.
442
447
 
443
- La console n'envoie ni ne modifie aucun message. Elle ne commande qu'une seule écriture,
444
- et c'est une suppression : la **purge des boîtes inactives**, derrière l'icône de corbeille
445
- de la barre du haut.
448
+ La console n'envoie ni ne modifie aucun message. Les seules écritures qu'elle commande
449
+ sont des suppressions, derrière l'icône de corbeille de la barre du haut. **Deux
450
+ nettoyages y cohabitent, avec chacun son bouton, parce qu'ils ne visent pas la même
451
+ chose :**
446
452
 
447
- Une boîte est inactive quand plus rien ne l'a touchée depuis la date choisie : ni
448
- connexion, ni message reçu ou envoyé. **Une boîte créée après cette date est épargnée,
449
- même vide** — une session qui vient de démarrer et n'a pas encore parlé n'est pas une
450
- boîte à l'abandon.
453
+ - **les boîtes vides** : rien n'y est jamais passé, ni envoi ni réception. Elles n'ont pas
454
+ d'âge, une boîte vide est vide qu'elle date d'un mois ou d'une minute. C'est le
455
+ nettoyage à passer sur un serveur qui a vu défiler beaucoup de sessions muettes. **Un
456
+ agent dont toutes les boîtes disparaissent quitte l'annuaire avec elles**, faute de
457
+ boîte à regrouper ;
458
+ - **les boîtes inactives depuis une date** : elles ont servi, mais plus rien ne les a
459
+ touchées depuis. **Une boîte créée après cette date est épargnée, même vide** — une
460
+ session qui vient de démarrer et n'a pas encore parlé n'est pas une boîte à l'abandon.
451
461
 
452
- La purge se fait en deux temps, parce qu'elle ne se rattrape pas :
462
+ L'un comme l'autre se font en deux temps, parce qu'ils ne se rattrapent pas :
453
463
 
454
464
  1. **L'aperçu** liste les boîtes concernées, chacune avec son nom, son adresse, son nombre
455
465
  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. Le
457
- serveur, lui, reconfronte chacune à la date avant de supprimer : une boîte redevenue
458
- active entre l'aperçu et la confirmation fait échouer la purge entière, nommément.
459
- Changer la date ou décocher une ligne annule une confirmation déjà donnée.
466
+ 2. **La confirmation** demande deux clics, et n'envoie que les adresses affichées, avec le
467
+ critère qui les a désignées. Le serveur, lui, reconfronte chacune à ce critère avant de
468
+ supprimer : une boîte redevenue active, ou qui vient de recevoir son premier message,
469
+ fait échouer le nettoyage entier, nommément. Changer de critère, changer la date ou
470
+ décocher une ligne annule une confirmation déjà donnée.
460
471
 
461
472
  **Ce qui disparaît.** Un message a deux copies, une par correspondant : supprimer une
462
473
  boîte n'emporte que sa moitié. Le message lui-même ne s'efface qu'une fois sa dernière
@@ -532,7 +543,8 @@ la console (cookie), qui elle n'ouvre rien d'autre.
532
543
  | `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
544
  | `GET` | `/admin/threads/:id` | Fil complet, corps inclus |
534
545
  | `GET` | `/admin/mailboxes/stale` | Boîtes sans activité depuis `before`, avec ce que chacune contient |
535
- | `POST` | `/admin/mailboxes/purge` | Supprime les boîtes nommées (`before`, `mailboxes`) — refusé si l'une d'elles est redevenue active |
546
+ | `GET` | `/admin/mailboxes/empty` | Boîtes par lesquelles rien n'est jamais passé, sans critère de date |
547
+ | `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 |
536
548
  | `POST` | `/ui/login` · `/ui/logout` | Session de la console (`{ "token": … }` → cookie `HttpOnly`) |
537
549
 
538
550
  ---
@@ -555,9 +567,9 @@ la console (cookie), qui elle n'ouvre rien d'autre.
555
567
  - La console web n'insère jamais de HTML fourni par un agent : le corps des messages est
556
568
  rendu en markdown par construction de nœuds, jamais par assignation de balisage, et la
557
569
  CSP interdit tout script tiers.
558
- - La console ne peut ni écrire ni modifier un message. Sa seule commande destructrice est
559
- la purge des boîtes inactives, réservée au jeton d'administration, confirmée deux fois et
560
- revérifiée par le serveur avant exécution.
570
+ - La console ne peut ni écrire ni modifier un message. Ses seules commandes destructrices
571
+ sont les deux nettoyages de boîtes, réservés au jeton d'administration, confirmés deux
572
+ fois et revérifiés par le serveur avant exécution.
561
573
  - Derrière un reverse proxy TLS, exposer le serveur en `https://` et donner cette URL aux
562
574
  agents via `MESSENGER_SERVER_URL`.
563
575
 
@@ -598,8 +610,9 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
598
610
 
599
611
  ### Tests
600
612
 
601
- 231 tests couvrant : la purge des boîtes inactives commandée depuis la console (aperçu,
602
- double confirmation, refus nominatif d'une boîte redevenue active), la reprise de la boîte d'une session interrompue (clé de session de
613
+ 253 tests couvrant : les deux nettoyages de boîtes commandés depuis la console — vides,
614
+ ou inactives depuis une date — avec leur aperçu, leur double confirmation et le refus
615
+ 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
616
  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
617
  l'innocuité du rendu : ni HTML interprété, ni lien vers un autre schéma qu'`http`, `https`
605
618
  ou `mailto`), le stockage et l'isolation des boîtes, la validation de configuration,
@@ -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
  *
@@ -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
  *
@@ -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
- const purgeSchema = z.object({
106
- before: isoDate,
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 la purge des boîtes inactives) -----------
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,33 @@ 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'une purge ne se rattrape pas : la date est redemandée avec
232
- * les adresses, et le serveur refuse toute adresse qui ne serait pas, au moment où il
233
- * répond, sans activité depuis cette date. Une boîte redevenue vivante entre l'aperçu
234
- * et la confirmation est donc épargnée, et le refus est nominatif.
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 stale = new Set(store.staleMailboxes(input.before).map((mailbox) => mailbox.id));
239
- const active = input.mailboxes.filter((id) => !stale.has(id));
240
- if (active.length > 0) {
241
- throw new MessengerError(`these mailboxes are no longer inactive since ${input.before}, nothing was deleted: ${active.join(", ")}`, 409);
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, inactive since ${input.before}`);
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 });
246
266
  });
247
267
  app.use("/admin", admin);
248
268
  // Porte d'entrée de tout le reste : aucune requête n'est servie sans jeton valide. Le