@imenam/mcp-messenger 1.0.16 → 1.0.18
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/.env.example +16 -0
- package/README.md +85 -10
- package/dist/src/cli/setup-mcp.js +1 -0
- package/dist/src/cli/watch.js +5 -0
- package/dist/src/config.d.ts +31 -0
- package/dist/src/config.js +40 -0
- package/dist/src/core/store.d.ts +25 -0
- package/dist/src/core/store.js +58 -1
- package/dist/src/mcp/mailbox-registry.d.ts +81 -0
- package/dist/src/mcp/mailbox-registry.js +0 -0
- package/dist/src/mcp/resume-state.d.ts +34 -0
- package/dist/src/mcp/resume-state.js +14 -0
- package/dist/src/mcp/start.d.ts +25 -5
- package/dist/src/mcp/start.js +120 -9
- package/dist/src/mcp/tools.js +37 -0
- package/dist/src/server/app.js +48 -1
- package/dist/src/server/ui/app.js +181 -1
- package/dist/src/server/ui/index.html +48 -1
- package/dist/src/server/ui/styles.css +1560 -1409
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -60,3 +60,19 @@ MESSENGER_TOKEN=change-me
|
|
|
60
60
|
|
|
61
61
|
# Délai maximal d'un appel HTTP vers le serveur (ms).
|
|
62
62
|
MESSENGER_TIMEOUT_MS=15000
|
|
63
|
+
|
|
64
|
+
# Reprise de la boîte d'une session interrompue puis relancée.
|
|
65
|
+
# auto (défaut) : la session retrouve sa boîte quand c'est certain (l'hôte a redonné le
|
|
66
|
+
# même identifiant de session) ou sans ambiguïté (une seule boîte du projet n'est plus
|
|
67
|
+
# tenue par personne, et elle a servi).
|
|
68
|
+
# ask : rien n'est repris tout seul ; `whoami` présente les boîtes en attente, l'agent en
|
|
69
|
+
# rattache une avec `attach_to_mailbox`.
|
|
70
|
+
# off : chaque session repart d'une boîte neuve.
|
|
71
|
+
# Une boîte tenue par une session vivante n'est jamais reprise, quel que soit le réglage.
|
|
72
|
+
# MESSENGER_RESUME=auto
|
|
73
|
+
|
|
74
|
+
# Identifiant de la session hôte, celui qui relie une session reprise à celle qu'elle
|
|
75
|
+
# continue. Inutile à renseigner : il est lu dans l'environnement hérité de l'hôte
|
|
76
|
+
# (CLAUDE_CODE_SESSION_ID et équivalents). À poser soi-même pour des sessions lancées par
|
|
77
|
+
# script. Il n'est jamais écrit sur le disque en clair, seule son empreinte l'est.
|
|
78
|
+
# MESSENGER_SESSION_KEY=
|
package/README.md
CHANGED
|
@@ -312,20 +312,61 @@ d'ouverture : rien n'est jamais illisible, mais rien n'est aussi parlant qu'un n
|
|
|
312
312
|
Ces noms apparaissent dans `list_threads`, `list_messages`, `get_thread`, les
|
|
313
313
|
notifications du veilleur et la console web.
|
|
314
314
|
|
|
315
|
-
### Reprendre le travail d'une session
|
|
316
|
-
|
|
317
|
-
|
|
315
|
+
### Reprendre le travail d'une session interrompue
|
|
316
|
+
|
|
317
|
+
Une session relancée est un nouveau processus. Sans mémoire, elle se ferait allouer une
|
|
318
|
+
boîte neuve et laisserait derrière elle ses fils, ses non-lus et **l'adresse que ses
|
|
319
|
+
correspondants connaissent** ; répété, cela multiplie les boîtes pour un travail unique.
|
|
320
|
+
La session retrouve donc la sienne toute seule, par un **registre local**.
|
|
321
|
+
|
|
322
|
+
Chaque session y inscrit sa boîte, avec l'empreinte de son projet et la preuve qu'elle
|
|
323
|
+
vit : le processus qui la tient et un battement de cœur. **Une boîte tenue par une session
|
|
324
|
+
vivante n'est jamais reprise** — c'est ce qui distingue ce mécanisme d'une adresse
|
|
325
|
+
partagée, et ce qui protège deux sessions ouvertes en parallèle sur le même projet. Une
|
|
326
|
+
boîte n'est reprenable qu'une fois **orpheline** : session close proprement, processus
|
|
327
|
+
mort, ou battement arrêté depuis plusieurs minutes.
|
|
328
|
+
|
|
329
|
+
Au démarrage, trois chemins, du plus certain au plus approximatif :
|
|
330
|
+
|
|
331
|
+
1. **Le même identifiant de session.** L'hôte en donne un à sa session, et le processus MCP
|
|
332
|
+
en hérite. Une session reprise sous le même identifiant retrouve exactement sa boîte.
|
|
333
|
+
Rien n'est à configurer : la variable est lue dans l'environnement
|
|
334
|
+
(`CLAUDE_CODE_SESSION_ID` et équivalents), et `MESSENGER_SESSION_KEY` permet de la poser
|
|
335
|
+
soi-même pour des sessions lancées par script. Ni le projet ni cet identifiant ne sont
|
|
336
|
+
écrits en clair : le registre n'en garde que l'empreinte.
|
|
337
|
+
2. **La seule boîte orpheline du projet, si elle a servi.** Sans identifiant, ou s'il a
|
|
338
|
+
changé, une boîte du même projet que plus personne ne tient est reprise quand elle est
|
|
339
|
+
la seule à l'être. Une boîte vide ne l'est jamais : il n'y a rien à y reprendre.
|
|
340
|
+
3. **Le choix de l'agent.** Plusieurs orphelines, ou une reprise incertaine : rien n'est
|
|
341
|
+
pris en silence. `whoami` les liste avec leur nom, leur nombre de messages et leurs
|
|
342
|
+
non-lus, et l'agent rattache la bonne avec `attach_to_mailbox`. Les instructions du
|
|
343
|
+
serveur MCP lui disent aussi de rattacher une adresse `box_…` qu'il reconnaîtrait dans
|
|
344
|
+
son propre contexte : une session reprise le reçoit en entier.
|
|
345
|
+
|
|
346
|
+
`MESSENGER_RESUME` tranche pour ceux que la reprise automatique gêne : `auto` par défaut,
|
|
347
|
+
`ask` pour que tout passe par l'agent, `off` pour repartir chaque fois d'une boîte neuve.
|
|
348
|
+
|
|
349
|
+
Le journal du démarrage dit lequel des trois chemins a servi, et quelle variable a fourni
|
|
350
|
+
l'identifiant de session — c'est là qu'on vérifie ce que l'hôte transmet réellement.
|
|
351
|
+
|
|
352
|
+
Le rattachement manuel reste disponible en toutes circonstances :
|
|
318
353
|
|
|
319
354
|
> « Reprends le travail de la session précédente, sa boîte est `box_9f2c4a01b3d7e5f8`. »
|
|
320
355
|
|
|
321
356
|
L'agent appelle `attach_to_mailbox` avec cette adresse : il lit alors le courrier de cette
|
|
322
357
|
boîte, y compris les messages arrivés pendant l'interruption, et écrit sous cette adresse.
|
|
323
358
|
`attach_to_mailbox` sans argument le ramène à sa propre boîte. Plusieurs sessions peuvent
|
|
324
|
-
être attachées à la même boîte — c'est volontaire, et c'est ce qui permet la reprise.
|
|
359
|
+
être attachées à la même boîte — c'est volontaire, et c'est ce qui permet la reprise. La
|
|
360
|
+
boîte rattachée devient celle que la session travaille : c'est elle que sa prochaine
|
|
361
|
+
reprise retrouvera.
|
|
325
362
|
|
|
326
363
|
L'adresse est vérifiée avant le basculement : une faute de frappe renvoie
|
|
327
364
|
`mailbox not found` au lieu de créer silencieusement une boîte vide.
|
|
328
365
|
|
|
366
|
+
Le registre est un fichier par boîte, en 0600, sous `~/.mcp-messenger/mailboxes/`
|
|
367
|
+
(`MESSENGER_STATE_DIR` pour le déplacer). Il est purement local : le serveur n'en sait
|
|
368
|
+
rien, et une entrée qu'aucune session n'a touchée depuis un mois est oubliée.
|
|
369
|
+
|
|
329
370
|
### Adresse fixe (cas particulier)
|
|
330
371
|
|
|
331
372
|
Renseigner `MESSENGER_AGENT_ID` fige l'adresse : toutes les sessions utilisant cette
|
|
@@ -336,7 +377,7 @@ configuration partagent alors la même boîte. Utile pour un service unique et s
|
|
|
336
377
|
|
|
337
378
|
## Console web
|
|
338
379
|
|
|
339
|
-
`http://127.0.0.1:3600/ui/` affiche
|
|
380
|
+
`http://127.0.0.1:3600/ui/` affiche **tous les échanges** de la flotte,
|
|
340
381
|
organisés autour du fil — l'unité d'une correspondance, celle qui porte le sujet, les deux
|
|
341
382
|
correspondants et l'état de lecture :
|
|
342
383
|
|
|
@@ -345,7 +386,8 @@ correspondants et l'état de lecture :
|
|
|
345
386
|
chevron sur ses boîtes ; une boîte que son agent n'a pas nommée s'affiche à son adresse,
|
|
346
387
|
la seule chose qui la distingue alors de ses sœurs. Cliquer l'agent filtre les fils sur
|
|
347
388
|
**toutes ses sessions**, cliquer une boîte sur **cette seule session**. La colonne se
|
|
348
|
-
replie sur les seuls avatars d'agents quand la place manque
|
|
389
|
+
replie sur les seuls avatars d'agents quand la place manque, son bouton de repli restant
|
|
390
|
+
en place : c'est lui qui la rouvre.
|
|
349
391
|
|
|
350
392
|
**Le classement se fait en deux temps : d'abord ce qui a servi, ensuite ce qui est
|
|
351
393
|
vide ; puis, dans chacun des deux, la dernière activité, la plus récente en tête.** Une
|
|
@@ -398,7 +440,31 @@ navigateur de l'administrateur. Les routes `/admin` acceptent toujours le jeton
|
|
|
398
440
|
`Authorization: Bearer` pour les scripts. Le formulaire est limité à 10 tentatives par
|
|
399
441
|
quart d'heure et par IP.
|
|
400
442
|
|
|
401
|
-
La console n'
|
|
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.
|
|
446
|
+
|
|
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.
|
|
451
|
+
|
|
452
|
+
La purge se fait en deux temps, parce qu'elle ne se rattrape pas :
|
|
453
|
+
|
|
454
|
+
1. **L'aperçu** liste les boîtes concernées, chacune avec son nom, son adresse, son nombre
|
|
455
|
+
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.
|
|
460
|
+
|
|
461
|
+
**Ce qui disparaît.** Un message a deux copies, une par correspondant : supprimer une
|
|
462
|
+
boîte n'emporte que sa moitié. Le message lui-même ne s'efface qu'une fois sa dernière
|
|
463
|
+
copie partie, autrement dit quand plus personne ne pourrait le lire ; un fil vidé de ses
|
|
464
|
+
messages s'en va avec. Ce que l'autre côté a reçu reste intact, expéditeur compris.
|
|
465
|
+
|
|
466
|
+
Le registre local des boîtes n'a pas à être nettoyé après une purge : une entrée qui
|
|
467
|
+
désigne une boîte disparue n'est jamais reprise, et s'oublie d'elle-même.
|
|
402
468
|
|
|
403
469
|
---
|
|
404
470
|
|
|
@@ -465,6 +531,8 @@ la console (cookie), qui elle n'ouvre rien d'autre.
|
|
|
465
531
|
| `GET` | `/admin/overview` · `/admin/messages[/:id]` | Supervision, lecture seule |
|
|
466
532
|
| `GET` | `/admin/threads` | Fils du serveur entier (`agent` — une ou plusieurs adresses séparées par des virgules —, `search`, `unread_only`, `priority`, `limit`, `offset`) |
|
|
467
533
|
| `GET` | `/admin/threads/:id` | Fil complet, corps inclus |
|
|
534
|
+
| `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 |
|
|
468
536
|
| `POST` | `/ui/login` · `/ui/logout` | Session de la console (`{ "token": … }` → cookie `HttpOnly`) |
|
|
469
537
|
|
|
470
538
|
---
|
|
@@ -484,8 +552,12 @@ la console (cookie), qui elle n'ouvre rien d'autre.
|
|
|
484
552
|
dans les logs).
|
|
485
553
|
- **Isolation des boîtes** garantie côté serveur : les identifiants de messages fournis par
|
|
486
554
|
un agent sont systématiquement filtrés sur ses propres livraisons.
|
|
487
|
-
- La console web n'insère jamais de HTML fourni par un agent
|
|
488
|
-
|
|
555
|
+
- La console web n'insère jamais de HTML fourni par un agent : le corps des messages est
|
|
556
|
+
rendu en markdown par construction de nœuds, jamais par assignation de balisage, et la
|
|
557
|
+
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.
|
|
489
561
|
- Derrière un reverse proxy TLS, exposer le serveur en `https://` et donner cette URL aux
|
|
490
562
|
agents via `MESSENGER_SERVER_URL`.
|
|
491
563
|
|
|
@@ -518,6 +590,7 @@ src/
|
|
|
518
590
|
server/ui/markdown.js # rendu markdown du corps des messages, en nœuds DOM
|
|
519
591
|
cli/watch.ts # mode veilleur : une ligne sur stdout par message entrant
|
|
520
592
|
cli/watch-handoff.ts # relais 0600 entre la session MCP et son veilleur
|
|
593
|
+
mcp/mailbox-registry.ts # registre local des boîtes : reprise d'une session interrompue
|
|
521
594
|
client/ # client HTTP utilisé par le mode MCP
|
|
522
595
|
mcp/ # définition des outils et serveur MCP stdio
|
|
523
596
|
test/ # store, config, API HTTP, outils MCP, binaire réel
|
|
@@ -525,7 +598,9 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
|
|
|
525
598
|
|
|
526
599
|
### Tests
|
|
527
600
|
|
|
528
|
-
|
|
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
|
|
603
|
+
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
|
|
529
604
|
l'innocuité du rendu : ni HTML interprété, ni lien vers un autre schéma qu'`http`, `https`
|
|
530
605
|
ou `mailto`), le stockage et l'isolation des boîtes, la validation de configuration,
|
|
531
606
|
l'API HTTP et son authentification (refus systématique sans jeton, jeton partagé, jetons par
|
|
@@ -47,6 +47,7 @@ const OPTIONAL_VARS = [
|
|
|
47
47
|
["MESSENGER_AGENT_NAME", "nom de l'agent (le projet, typiquement) ; distinct du nom de la boîte, que l'agent pose lui-même"],
|
|
48
48
|
["MESSENGER_AGENT_DESCRIPTION", "à quoi sert cet agent, pour ses correspondants"],
|
|
49
49
|
["MESSENGER_TIMEOUT_MS", "délai maximal d'un appel HTTP (défaut : 15000)"],
|
|
50
|
+
["MESSENGER_RESUME", "reprise de la boîte d'une session interrompue : auto (défaut), ask, off"],
|
|
50
51
|
];
|
|
51
52
|
const CLAUDE_MD_MARKER = "<!-- mcp-messenger -->";
|
|
52
53
|
const CLAUDE_MD_BLOCK = `${CLAUDE_MD_MARKER}
|
package/dist/src/cli/watch.js
CHANGED
|
@@ -47,6 +47,11 @@ export function resolveWatchConfig(mailbox, env = process.env) {
|
|
|
47
47
|
agentName: null,
|
|
48
48
|
agentDescription: null,
|
|
49
49
|
timeoutMs: Number(env.MESSENGER_TIMEOUT_MS) || 15_000,
|
|
50
|
+
// Le veilleur écoute une adresse qu'on lui donne : il n'alloue rien et n'a donc
|
|
51
|
+
// aucune boîte à reprendre.
|
|
52
|
+
sessionKey: null,
|
|
53
|
+
sessionKeySource: null,
|
|
54
|
+
resume: "off",
|
|
50
55
|
},
|
|
51
56
|
};
|
|
52
57
|
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -22,6 +22,16 @@ export interface ServerConfig {
|
|
|
22
22
|
/** Plafond d'attente d'un `wait_for_message` (secondes). */
|
|
23
23
|
maxWaitSeconds: number;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Ce qu'une session fait des boîtes laissées par une session précédente du même projet.
|
|
27
|
+
*
|
|
28
|
+
* `auto` : reprise sans rien demander quand elle est certaine (clé de session identique)
|
|
29
|
+
* ou sans ambiguïté (une seule boîte orpheline, et elle a servi).
|
|
30
|
+
* `ask` : rien n'est repris tout seul, les candidates sont présentées à l'agent.
|
|
31
|
+
* `off` : aucune recherche, chaque session repart d'une boîte neuve.
|
|
32
|
+
*/
|
|
33
|
+
export declare const RESUME_MODES: readonly ["auto", "ask", "off"];
|
|
34
|
+
export type ResumeMode = (typeof RESUME_MODES)[number];
|
|
25
35
|
export interface AgentConfig {
|
|
26
36
|
/** null = une boîte propre à cette session est allouée au démarrage. */
|
|
27
37
|
agentId: string | null;
|
|
@@ -31,7 +41,28 @@ export interface AgentConfig {
|
|
|
31
41
|
agentName: string | null;
|
|
32
42
|
agentDescription: string | null;
|
|
33
43
|
timeoutMs: number;
|
|
44
|
+
/**
|
|
45
|
+
* Identifiant de la session hôte, s'il est lisible. C'est lui qui relie une session
|
|
46
|
+
* reprise à celle qu'elle continue ; il n'est jamais écrit sur le disque en clair.
|
|
47
|
+
*/
|
|
48
|
+
sessionKey: string | null;
|
|
49
|
+
/** D'où vient cette clé, pour que le journal puisse le dire. */
|
|
50
|
+
sessionKeySource: string | null;
|
|
51
|
+
resume: ResumeMode;
|
|
34
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Variables où chercher l'identifiant de la session hôte, dans l'ordre.
|
|
55
|
+
*
|
|
56
|
+
* Rien n'a besoin d'être configuré : le processus MCP est un enfant de l'hôte et hérite
|
|
57
|
+
* de son environnement. `MESSENGER_SESSION_KEY` vient en tête pour qui lance ses sessions
|
|
58
|
+
* autrement et veut poser la clé lui-même.
|
|
59
|
+
*/
|
|
60
|
+
export declare const SESSION_KEY_VARS: readonly ["MESSENGER_SESSION_KEY", "CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID", "CURSOR_SESSION_ID", "MCP_SESSION_ID"];
|
|
61
|
+
/** Première clé de session lisible dans l'environnement, avec la variable qui l'a donnée. */
|
|
62
|
+
export declare function readSessionKey(env?: NodeJS.ProcessEnv): {
|
|
63
|
+
key: string;
|
|
64
|
+
source: string;
|
|
65
|
+
} | null;
|
|
35
66
|
export declare function isLoopbackHost(host: string): boolean;
|
|
36
67
|
export declare function loadServerConfig(env?: NodeJS.ProcessEnv): ServerConfig;
|
|
37
68
|
export declare function loadAgentConfig(env?: NodeJS.ProcessEnv): AgentConfig;
|
package/dist/src/config.js
CHANGED
|
@@ -9,6 +9,38 @@ export class ConfigError extends Error {
|
|
|
9
9
|
}
|
|
10
10
|
/** Le serveur n'écoute jamais ailleurs : la boîte aux lettres reste locale à la machine. */
|
|
11
11
|
export const SERVER_HOST = "127.0.0.1";
|
|
12
|
+
/**
|
|
13
|
+
* Ce qu'une session fait des boîtes laissées par une session précédente du même projet.
|
|
14
|
+
*
|
|
15
|
+
* `auto` : reprise sans rien demander quand elle est certaine (clé de session identique)
|
|
16
|
+
* ou sans ambiguïté (une seule boîte orpheline, et elle a servi).
|
|
17
|
+
* `ask` : rien n'est repris tout seul, les candidates sont présentées à l'agent.
|
|
18
|
+
* `off` : aucune recherche, chaque session repart d'une boîte neuve.
|
|
19
|
+
*/
|
|
20
|
+
export const RESUME_MODES = ["auto", "ask", "off"];
|
|
21
|
+
/**
|
|
22
|
+
* Variables où chercher l'identifiant de la session hôte, dans l'ordre.
|
|
23
|
+
*
|
|
24
|
+
* Rien n'a besoin d'être configuré : le processus MCP est un enfant de l'hôte et hérite
|
|
25
|
+
* de son environnement. `MESSENGER_SESSION_KEY` vient en tête pour qui lance ses sessions
|
|
26
|
+
* autrement et veut poser la clé lui-même.
|
|
27
|
+
*/
|
|
28
|
+
export const SESSION_KEY_VARS = [
|
|
29
|
+
"MESSENGER_SESSION_KEY",
|
|
30
|
+
"CLAUDE_CODE_SESSION_ID",
|
|
31
|
+
"CLAUDE_SESSION_ID",
|
|
32
|
+
"CURSOR_SESSION_ID",
|
|
33
|
+
"MCP_SESSION_ID",
|
|
34
|
+
];
|
|
35
|
+
/** Première clé de session lisible dans l'environnement, avec la variable qui l'a donnée. */
|
|
36
|
+
export function readSessionKey(env = process.env) {
|
|
37
|
+
for (const name of SESSION_KEY_VARS) {
|
|
38
|
+
const value = nonEmpty(env[name]);
|
|
39
|
+
if (value)
|
|
40
|
+
return { key: value, source: name };
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
12
44
|
export function isLoopbackHost(host) {
|
|
13
45
|
return LOOPBACK_HOSTS.has(host.trim().toLowerCase().replace(/^\[|\]$/g, ""));
|
|
14
46
|
}
|
|
@@ -70,6 +102,11 @@ export function loadAgentConfig(env = process.env) {
|
|
|
70
102
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
71
103
|
throw new ConfigError(`MESSENGER_SERVER_URL must use http or https (got ${url.protocol}).`);
|
|
72
104
|
}
|
|
105
|
+
const resume = nonEmpty(env.MESSENGER_RESUME)?.toLowerCase() ?? "auto";
|
|
106
|
+
if (!RESUME_MODES.includes(resume)) {
|
|
107
|
+
throw new ConfigError(`MESSENGER_RESUME must be one of ${RESUME_MODES.join(", ")} (got "${resume}").`);
|
|
108
|
+
}
|
|
109
|
+
const session = readSessionKey(env);
|
|
73
110
|
return {
|
|
74
111
|
agentId,
|
|
75
112
|
serverUrl: rawUrl.replace(/\/+$/, ""),
|
|
@@ -77,6 +114,9 @@ export function loadAgentConfig(env = process.env) {
|
|
|
77
114
|
agentName: nonEmpty(env.MESSENGER_AGENT_NAME),
|
|
78
115
|
agentDescription: nonEmpty(env.MESSENGER_AGENT_DESCRIPTION),
|
|
79
116
|
timeoutMs: parseIntEnv(env.MESSENGER_TIMEOUT_MS, 15_000, "MESSENGER_TIMEOUT_MS"),
|
|
117
|
+
sessionKey: session?.key ?? null,
|
|
118
|
+
sessionKeySource: session?.source ?? null,
|
|
119
|
+
resume: resume,
|
|
80
120
|
};
|
|
81
121
|
}
|
|
82
122
|
function parseAgentTokens(raw) {
|
package/dist/src/core/store.d.ts
CHANGED
|
@@ -87,6 +87,31 @@ export declare class MessageStore extends EventEmitter<StoreEvents> {
|
|
|
87
87
|
stats(agentId: string): MailboxStats;
|
|
88
88
|
/** Purge les livraisons restées en corbeille au-delà de `days`. Renvoie le nombre supprimé. */
|
|
89
89
|
purgeTrash(days: number): number;
|
|
90
|
+
/**
|
|
91
|
+
* Boîtes dont plus rien ne bouge depuis `before`, avec ce que leur suppression coûterait.
|
|
92
|
+
*
|
|
93
|
+
* L'inactivité se mesure au plus récent de trois moments : la dernière connexion, la
|
|
94
|
+
* dernière livraison, et la création. Le dernier compte autant que les autres — une
|
|
95
|
+
* boîte allouée il y a une minute et qui n'a rien fait n'est pas une boîte à l'abandon,
|
|
96
|
+
* c'est une session qui n'a pas encore parlé.
|
|
97
|
+
*/
|
|
98
|
+
staleMailboxes(before: string): (AgentDirectoryEntry & {
|
|
99
|
+
messages: number;
|
|
100
|
+
})[];
|
|
101
|
+
/**
|
|
102
|
+
* Supprime ces boîtes et tout ce qui n'appartenait qu'à elles.
|
|
103
|
+
*
|
|
104
|
+
* Un message a deux copies, une par correspondant : effacer une boîte n'efface donc que
|
|
105
|
+
* sa moitié. Le message lui-même ne disparaît qu'une fois sa dernière copie partie —
|
|
106
|
+
* autrement dit quand plus personne ne peut le lire. Ce que l'autre côté a reçu reste
|
|
107
|
+
* intact, avec l'adresse de l'expéditeur telle qu'elle a été livrée.
|
|
108
|
+
*/
|
|
109
|
+
deleteMailboxes(ids: string[]): {
|
|
110
|
+
mailboxes: number;
|
|
111
|
+
deliveries: number;
|
|
112
|
+
messages: number;
|
|
113
|
+
threads: number;
|
|
114
|
+
};
|
|
90
115
|
/** Vue globale : tous les messages du serveur, indépendamment des boîtes. */
|
|
91
116
|
listAllMessages(opts?: {
|
|
92
117
|
search?: string;
|
package/dist/src/core/store.js
CHANGED
|
@@ -581,7 +581,64 @@ export class MessageStore extends EventEmitter {
|
|
|
581
581
|
this.dropOrphanMessages();
|
|
582
582
|
return removed;
|
|
583
583
|
}
|
|
584
|
-
|
|
584
|
+
/**
|
|
585
|
+
* Boîtes dont plus rien ne bouge depuis `before`, avec ce que leur suppression coûterait.
|
|
586
|
+
*
|
|
587
|
+
* L'inactivité se mesure au plus récent de trois moments : la dernière connexion, la
|
|
588
|
+
* dernière livraison, et la création. Le dernier compte autant que les autres — une
|
|
589
|
+
* boîte allouée il y a une minute et qui n'a rien fait n'est pas une boîte à l'abandon,
|
|
590
|
+
* c'est une session qui n'a pas encore parlé.
|
|
591
|
+
*/
|
|
592
|
+
staleMailboxes(before) {
|
|
593
|
+
const rows = this.db
|
|
594
|
+
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at,
|
|
595
|
+
(SELECT COUNT(*) FROM deliveries d WHERE d.agent_id = a.id) AS messages
|
|
596
|
+
FROM agents a
|
|
597
|
+
WHERE MAX(${LAST_ACTIVITY}, a.created_at) < ?
|
|
598
|
+
ORDER BY MAX(${LAST_ACTIVITY}, a.created_at) ASC`)
|
|
599
|
+
.all(before);
|
|
600
|
+
return rows.map((row) => ({
|
|
601
|
+
...row,
|
|
602
|
+
registered: Boolean(row.registered),
|
|
603
|
+
last_activity_at: row.last_activity_at || null,
|
|
604
|
+
}));
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Supprime ces boîtes et tout ce qui n'appartenait qu'à elles.
|
|
608
|
+
*
|
|
609
|
+
* Un message a deux copies, une par correspondant : effacer une boîte n'efface donc que
|
|
610
|
+
* sa moitié. Le message lui-même ne disparaît qu'une fois sa dernière copie partie —
|
|
611
|
+
* autrement dit quand plus personne ne peut le lire. Ce que l'autre côté a reçu reste
|
|
612
|
+
* intact, avec l'adresse de l'expéditeur telle qu'elle a été livrée.
|
|
613
|
+
*/
|
|
614
|
+
deleteMailboxes(ids) {
|
|
615
|
+
if (ids.length === 0)
|
|
616
|
+
return { mailboxes: 0, deliveries: 0, messages: 0, threads: 0 };
|
|
617
|
+
return this.db.transaction(() => {
|
|
618
|
+
const messagesBefore = this.db.prepare("SELECT COUNT(*) AS n FROM messages").get().n;
|
|
619
|
+
const threadsBefore = this.db.prepare("SELECT COUNT(*) AS n FROM threads").get().n;
|
|
620
|
+
let mailboxes = 0;
|
|
621
|
+
let deliveries = 0;
|
|
622
|
+
const dropDeliveries = this.db.prepare("DELETE FROM deliveries WHERE agent_id = ?");
|
|
623
|
+
const dropFolders = this.db.prepare("DELETE FROM folders WHERE agent_id = ?");
|
|
624
|
+
const dropAgent = this.db.prepare("DELETE FROM agents WHERE id = ?");
|
|
625
|
+
for (const id of ids) {
|
|
626
|
+
deliveries += dropDeliveries.run(id).changes;
|
|
627
|
+
dropFolders.run(id);
|
|
628
|
+
mailboxes += dropAgent.run(id).changes;
|
|
629
|
+
}
|
|
630
|
+
this.dropOrphanMessages();
|
|
631
|
+
const messagesAfter = this.db.prepare("SELECT COUNT(*) AS n FROM messages").get().n;
|
|
632
|
+
const threadsAfter = this.db.prepare("SELECT COUNT(*) AS n FROM threads").get().n;
|
|
633
|
+
return {
|
|
634
|
+
mailboxes,
|
|
635
|
+
deliveries,
|
|
636
|
+
messages: messagesBefore - messagesAfter,
|
|
637
|
+
threads: threadsBefore - threadsAfter,
|
|
638
|
+
};
|
|
639
|
+
})();
|
|
640
|
+
}
|
|
641
|
+
// --- Supervision (interface web, lecture seule sauf purge) ---------------
|
|
585
642
|
/** Vue globale : tous les messages du serveur, indépendamment des boîtes. */
|
|
586
643
|
listAllMessages(opts = {}) {
|
|
587
644
|
const limit = clamp(opts.limit ?? 50, 1, MAX_LIMIT);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registre local des boîtes tenues par les sessions MCP de cette machine.
|
|
3
|
+
*
|
|
4
|
+
* Une session reprise est un nouveau processus : sans mémoire, elle se ferait allouer une
|
|
5
|
+
* boîte neuve et perdrait ses fils, ses non-lus et son adresse — celle que ses
|
|
6
|
+
* correspondants connaissent. Le registre est cette mémoire, et il est purement local :
|
|
7
|
+
* le serveur n'en sait rien, rien ne part sur le réseau.
|
|
8
|
+
*
|
|
9
|
+
* Une entrée dit trois choses : quelle boîte, pour quel projet, et **tenue par qui**. Ce
|
|
10
|
+
* dernier point est ce qui rend la reprise sûre : tant que le processus inscrit vit et
|
|
11
|
+
* bat, sa boîte lui appartient et aucune autre session ne la prendra. Une boîte n'est
|
|
12
|
+
* reprenable qu'une fois **orpheline** : session close proprement, processus mort, ou
|
|
13
|
+
* battement arrêté depuis assez longtemps pour qu'un numéro de processus recyclé ne
|
|
14
|
+
* puisse pas faire illusion.
|
|
15
|
+
*
|
|
16
|
+
* Rien de lisible n'est écrit : le projet et la clé de session ne figurent que par leur
|
|
17
|
+
* empreinte, et les fichiers sont en 0600 comme les relais de veilleurs.
|
|
18
|
+
*
|
|
19
|
+
* Un processus ne tient qu'une entrée à la fois, celle de sa session : c'est vrai du mode
|
|
20
|
+
* MCP, où un processus est une session. Les tests qui simulent plusieurs sessions dans un
|
|
21
|
+
* même processus en tiennent compte.
|
|
22
|
+
*/
|
|
23
|
+
export interface RegistryEntry {
|
|
24
|
+
mailbox: string;
|
|
25
|
+
/** Empreinte du projet : même répertoire, même agent, même serveur. */
|
|
26
|
+
fingerprint: string;
|
|
27
|
+
/** Empreinte de l'identifiant de session de l'hôte, ou null s'il n'y en avait pas. */
|
|
28
|
+
session_key: string | null;
|
|
29
|
+
/** Processus MCP qui tient la boîte. Sa mort rend l'entrée orpheline. */
|
|
30
|
+
pid: number;
|
|
31
|
+
started_at: string;
|
|
32
|
+
heartbeat_at: string;
|
|
33
|
+
/** Posé à l'arrêt propre de la session : la boîte est libre sans attendre. */
|
|
34
|
+
closed_at: string | null;
|
|
35
|
+
}
|
|
36
|
+
/** Rythme du battement de cœur d'une session vivante. */
|
|
37
|
+
export declare const HEARTBEAT_MS = 60000;
|
|
38
|
+
/** Répertoire du registre. `MESSENGER_STATE_DIR` le déplace, comme pour les relais. */
|
|
39
|
+
export declare function registryDir(): string;
|
|
40
|
+
/**
|
|
41
|
+
* Ce qui fait qu'une boîte appartient au même travail : le répertoire d'où la session est
|
|
42
|
+
* lancée, le nom d'agent de sa configuration et le serveur qu'elle utilise. Deux sessions
|
|
43
|
+
* du même projet partagent cette empreinte ; deux projets ne la partagent jamais.
|
|
44
|
+
*/
|
|
45
|
+
export declare function projectFingerprint(input: {
|
|
46
|
+
cwd: string;
|
|
47
|
+
agentName: string | null;
|
|
48
|
+
serverUrl: string;
|
|
49
|
+
}): string;
|
|
50
|
+
/** L'identifiant de session de l'hôte n'est jamais écrit en clair : seule son empreinte l'est. */
|
|
51
|
+
export declare function sessionKeyDigest(key: string): string;
|
|
52
|
+
/** Toutes les entrées d'un projet, la plus récemment vivante en tête. */
|
|
53
|
+
export declare function listEntries(fingerprint: string): RegistryEntry[];
|
|
54
|
+
/** Vrai tant que le processus existe, même s'il appartient à un autre utilisateur. */
|
|
55
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
56
|
+
/** Une entrée est orpheline quand plus aucune session vivante ne la tient. */
|
|
57
|
+
export declare function isOrphan(entry: RegistryEntry, now?: number): boolean;
|
|
58
|
+
/** Boîtes du projet qu'aucune session vivante ne tient, la plus récente en tête. */
|
|
59
|
+
export declare function orphanEntries(fingerprint: string, now?: number): RegistryEntry[];
|
|
60
|
+
/** Inscrit la boîte de cette session, et se souvient de l'entrée pour la tenir à jour. */
|
|
61
|
+
export declare function claim(input: {
|
|
62
|
+
mailbox: string;
|
|
63
|
+
fingerprint: string;
|
|
64
|
+
sessionKey: string | null;
|
|
65
|
+
}): RegistryEntry;
|
|
66
|
+
/**
|
|
67
|
+
* Suit la session quand elle se rattache ailleurs (`attach_to_mailbox`) : c'est cette
|
|
68
|
+
* boîte-là qu'elle travaille, donc c'est elle que la prochaine reprise doit retrouver.
|
|
69
|
+
* L'entrée précédente est close, pas supprimée : la boîte qu'elle désigne reste reprenable.
|
|
70
|
+
*/
|
|
71
|
+
export declare function rebind(mailbox: string): void;
|
|
72
|
+
/** Dit que la session est toujours là. Sans ce battement, sa boîte finit par être reprenable. */
|
|
73
|
+
export declare function heartbeat(): void;
|
|
74
|
+
/** Libère la boîte à l'arrêt de la session : la reprise n'a pas à attendre l'expiration. */
|
|
75
|
+
export declare function releaseOwn(): void;
|
|
76
|
+
/** Entrée tenue par ce processus, pour les tests et le journal. */
|
|
77
|
+
export declare function ownEntry(): RegistryEntry | null;
|
|
78
|
+
/** Oublie les entrées qu'aucune session n'a touchées depuis longtemps. */
|
|
79
|
+
export declare function purge(now?: number): number;
|
|
80
|
+
/** Remet le module à zéro entre deux tests : le processus, lui, ne tient qu'une entrée. */
|
|
81
|
+
export declare function resetOwnEntry(): void;
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MessengerClient } from "../client/messenger-client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Ce que le démarrage a trouvé des sessions précédentes, gardé pour `whoami`.
|
|
4
|
+
*
|
|
5
|
+
* La reprise se décide avant que l'agent ne parle : il faut donc pouvoir la lui dire
|
|
6
|
+
* plus tard, au premier outil qui parle d'identité. L'état est attaché au client plutôt
|
|
7
|
+
* qu'au module — deux sessions cohabitent dans un même processus pendant les tests.
|
|
8
|
+
*/
|
|
9
|
+
export interface ResumeCandidate {
|
|
10
|
+
mailbox: string;
|
|
11
|
+
agent_name: string | null;
|
|
12
|
+
mailbox_name: string | null;
|
|
13
|
+
/** Messages présents dans la boîte, corbeille comprise : de quoi juger si elle a servi. */
|
|
14
|
+
messages: number;
|
|
15
|
+
unread: number;
|
|
16
|
+
last_activity: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface ResumeState {
|
|
19
|
+
/**
|
|
20
|
+
* Comment la boîte de cette session a été retrouvée :
|
|
21
|
+
* `session-key`, l'hôte a redonné le même identifiant de session — c'est une certitude ;
|
|
22
|
+
* `orphan`, une seule boîte du projet restait sans session pour la tenir ;
|
|
23
|
+
* null, la boîte est neuve.
|
|
24
|
+
*/
|
|
25
|
+
resumed: "session-key" | "orphan" | null;
|
|
26
|
+
/** Boîtes laissées par une session arrêtée, que l'agent peut rattacher lui-même. */
|
|
27
|
+
candidates: ResumeCandidate[];
|
|
28
|
+
/** Variable d'environnement qui a fourni l'identifiant de session, s'il y en avait une. */
|
|
29
|
+
keySource: string | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function setResumeState(client: MessengerClient, state: ResumeState): void;
|
|
32
|
+
export declare function getResumeState(client: MessengerClient): ResumeState | null;
|
|
33
|
+
/** Une boîte reprise n'est plus une boîte à proposer : l'agent y est déjà. */
|
|
34
|
+
export declare function forgetCandidate(client: MessengerClient, mailbox: string): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const states = new WeakMap();
|
|
2
|
+
export function setResumeState(client, state) {
|
|
3
|
+
states.set(client, state);
|
|
4
|
+
}
|
|
5
|
+
export function getResumeState(client) {
|
|
6
|
+
return states.get(client) ?? null;
|
|
7
|
+
}
|
|
8
|
+
/** Une boîte reprise n'est plus une boîte à proposer : l'agent y est déjà. */
|
|
9
|
+
export function forgetCandidate(client, mailbox) {
|
|
10
|
+
const state = states.get(client);
|
|
11
|
+
if (!state)
|
|
12
|
+
return;
|
|
13
|
+
states.set(client, { ...state, candidates: state.candidates.filter((c) => c.mailbox !== mailbox) });
|
|
14
|
+
}
|
package/dist/src/mcp/start.d.ts
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import { type AgentConfig } from "../config.js";
|
|
3
3
|
import { MessengerClient } from "../client/messenger-client.js";
|
|
4
|
+
import { type ResumeCandidate } from "./resume-state.js";
|
|
4
5
|
/** Serveur MCP prêt à être branché sur n'importe quel transport (stdio en production, mémoire en test). */
|
|
5
6
|
export declare function createMcpServer(client: MessengerClient): Server;
|
|
7
|
+
export interface Identity {
|
|
8
|
+
mailbox: string;
|
|
9
|
+
/** Non nul quand la boîte vient d'une session précédente. Voir `ResumeState`. */
|
|
10
|
+
resumed: "session-key" | "orphan" | null;
|
|
11
|
+
/** Boîtes orphelines du même projet qui n'ont pas été reprises d'office. */
|
|
12
|
+
candidates: ResumeCandidate[];
|
|
13
|
+
}
|
|
6
14
|
/**
|
|
7
15
|
* Donne son adresse à la session.
|
|
8
16
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
17
|
+
* Trois chemins, du plus certain au plus approximatif :
|
|
18
|
+
*
|
|
19
|
+
* 1. `MESSENGER_AGENT_ID` fixe l'adresse, partagée par toutes les sessions de cette
|
|
20
|
+
* configuration. Rien à chercher.
|
|
21
|
+
* 2. Le registre local rend à une session reprise la boîte qu'elle tenait : identique si
|
|
22
|
+
* l'hôte a redonné le même identifiant de session, sinon la seule boîte du projet
|
|
23
|
+
* qu'aucune session vivante ne tient plus et qui a réellement servi.
|
|
24
|
+
* 3. À défaut, une boîte neuve, comme avant. Les boîtes orphelines non reprises sont
|
|
25
|
+
* gardées de côté : `whoami` les propose, l'agent en rattache une s'il reconnaît la
|
|
26
|
+
* sienne dans son contexte.
|
|
27
|
+
*
|
|
28
|
+
* Une boîte tenue par une session vivante n'est jamais reprise : c'est ce qui distingue
|
|
29
|
+
* ce mécanisme d'une adresse partagée, et ce qui protège deux sessions ouvertes en
|
|
30
|
+
* parallèle sur le même projet.
|
|
13
31
|
*/
|
|
14
|
-
export declare function resolveIdentity(client: MessengerClient, config: AgentConfig
|
|
32
|
+
export declare function resolveIdentity(client: MessengerClient, config: AgentConfig, options?: {
|
|
33
|
+
cwd?: string;
|
|
34
|
+
}): Promise<Identity>;
|
|
15
35
|
/** Point d'entrée du mode MCP (mode par défaut, sans argument). */
|
|
16
36
|
export declare function runMcpMode(): Promise<void>;
|