@imenam/mcp-messenger 1.0.5 → 1.0.7
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 +6 -3
- package/README.md +63 -30
- package/dist/src/cli/generate-token.d.ts +2 -2
- package/dist/src/cli/generate-token.js +2 -2
- package/dist/src/cli/setup-mcp.js +2 -2
- package/dist/src/cli/watch.js +2 -2
- package/dist/src/client/messenger-client.d.ts +2 -3
- package/dist/src/client/messenger-client.js +3 -3
- package/dist/src/config.d.ts +3 -2
- package/dist/src/config.js +2 -2
- package/dist/src/core/store.d.ts +23 -8
- package/dist/src/core/store.js +65 -42
- package/dist/src/core/types.d.ts +19 -9
- package/dist/src/core/types.js +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/mcp/tools.js +102 -61
- package/dist/src/server/app.js +88 -72
- package/dist/src/server/console-session.d.ts +30 -0
- package/dist/src/server/console-session.js +85 -0
- package/dist/src/server/start.js +1 -2
- package/dist/src/server/ui/app.js +50 -25
- package/dist/src/server/ui/index.html +5 -4
- package/dist/src/server/ui/styles.css +6 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -13,7 +13,8 @@ MESSENGER_PORT=3600
|
|
|
13
13
|
# Chemin du fichier SQLite (défaut : <cwd>/data/messenger.db).
|
|
14
14
|
# MESSENGER_DB_PATH=/var/lib/mcp-messenger/messenger.db
|
|
15
15
|
|
|
16
|
-
# Jeton exigé sur TOUTE requête
|
|
16
|
+
# Jeton exigé sur TOUTE requête de données (API, /health, données de la console).
|
|
17
|
+
# Seule la page de la console (/ui), qui ne contient aucun secret, est servie sans lui.
|
|
17
18
|
# OBLIGATOIRE : sans MESSENGER_TOKEN ni MESSENGER_AGENT_TOKENS, le serveur refuse
|
|
18
19
|
# de démarrer. Générer avec : npx @imenam/mcp-messenger --generate-token
|
|
19
20
|
MESSENGER_TOKEN=change-me
|
|
@@ -23,8 +24,10 @@ MESSENGER_TOKEN=change-me
|
|
|
23
24
|
# Format : agent_id:token,agent_id:token
|
|
24
25
|
# MESSENGER_AGENT_TOKENS=planner:tok-planner,coder:tok-coder
|
|
25
26
|
|
|
26
|
-
# Jeton de la console web
|
|
27
|
-
#
|
|
27
|
+
# Jeton de la console web, saisi une fois dans son formulaire (/ui) : le serveur ouvre
|
|
28
|
+
# alors une session de 12 h dans un cookie HttpOnly, sans jamais y mettre le jeton.
|
|
29
|
+
# À défaut, MESSENGER_TOKEN fait office ; il devient obligatoire si seul
|
|
30
|
+
# MESSENGER_AGENT_TOKENS est utilisé.
|
|
28
31
|
# MESSENGER_ADMIN_TOKEN=change-me-too
|
|
29
32
|
|
|
30
33
|
# Rate limiting (par IP).
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ npx @imenam/mcp-messenger --generate-token
|
|
|
43
43
|
MESSENGER_TOKEN=<le-jeton> npx @imenam/mcp-messenger --server
|
|
44
44
|
# → listening on http://127.0.0.1:3600 (loopback only)
|
|
45
45
|
# → auth: shared token — required on every request
|
|
46
|
-
# → web console: http://127.0.0.1:3600/ui
|
|
46
|
+
# → web console: http://127.0.0.1:3600/ui/ (asks for MESSENGER_ADMIN_TOKEN once)
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Deux invariants, non négociables et vérifiés par les tests :
|
|
@@ -51,9 +51,10 @@ Deux invariants, non négociables et vérifiés par les tests :
|
|
|
51
51
|
1. **Le serveur écoute toujours sur `127.0.0.1`.** `MESSENGER_HOST` n'est pas un réglage :
|
|
52
52
|
toute autre valeur fait échouer le démarrage. Pour joindre la boîte depuis une autre
|
|
53
53
|
machine, il faut placer un reverse proxy (TLS) devant.
|
|
54
|
-
2. **Un jeton est obligatoire et vérifié sur chaque requête
|
|
55
|
-
console
|
|
56
|
-
|
|
54
|
+
2. **Un jeton est obligatoire et vérifié sur chaque requête de données** — API, `/health`
|
|
55
|
+
et données de la console (`/admin`) comprises. Seule la page de la console, qui ne
|
|
56
|
+
contient aucun secret, est servie librement. Sans `MESSENGER_TOKEN` ni
|
|
57
|
+
`MESSENGER_AGENT_TOKENS`, le serveur refuse de démarrer.
|
|
57
58
|
|
|
58
59
|
### 2. Déclarer le MCP côté agent
|
|
59
60
|
|
|
@@ -96,7 +97,7 @@ la session MCP démarre quand même et l'enregistrement se fera au premier appel
|
|
|
96
97
|
|
|
97
98
|
L'agent A appelle `send_message` avec `to: ["coder"]`, l'agent B voit le message via
|
|
98
99
|
`list_messages` puis `read_message`, et répond avec `reply_to` pour rester dans le fil.
|
|
99
|
-
Le fil se nomme au passage (`
|
|
100
|
+
Le fil se nomme au passage (`thread_name` sur `send_message`, ou `rename_thread` plus tard).
|
|
100
101
|
`wait_for_message` permet d'attendre une réponse sans faire tourner une boucle d'appels : il ne
|
|
101
102
|
rend la main que sur un message arrivé *après* le début de l'attente, les non-lus déjà en boîte
|
|
102
103
|
ne l'interrompent pas.
|
|
@@ -176,10 +177,11 @@ superviseur capable de lire les lignes d'un processus peut la brancher. Sans rel
|
|
|
176
177
|
|---|---|
|
|
177
178
|
| `whoami` | Adresse de cette session, serveur connecté, veilleur armé ou non, compteurs par dossier |
|
|
178
179
|
| `attach_to_mailbox` | Se brancher sur une autre boîte via son adresse ; sans argument, retour à la sienne |
|
|
179
|
-
| `
|
|
180
|
+
| `rename_mailbox` | Nommer ou renommer sa boîte (de quoi la session s'occupe) ; le nom de l'agent, posé par la configuration, reste |
|
|
181
|
+
| `rename_thread` | Nommer ou renommer un fil (`thread_id`) ; le nom est partagé par tous ses participants |
|
|
180
182
|
| `list_threads` | Carte des conversations : nom, participants, non-lus, dernière activité |
|
|
181
|
-
| `list_agents` | Annuaire : qui est joignable, du plus récemment actif au plus ancien |
|
|
182
|
-
| `send_message` |
|
|
183
|
+
| `list_agents` | Annuaire : qui est joignable, du plus récemment actif au plus ancien ; la boîte de l'appelant n'y figure pas |
|
|
184
|
+
| `send_message` | Écrire à un agent — un seul destinataire par message (`to`, `priority`, `reply_to`, `thread_name`) |
|
|
183
185
|
| `list_messages` | Lister la boîte (filtres : dossier, non lus, expéditeur, fil, texte libre) |
|
|
184
186
|
| `read_message` | Lire un message en entier ; le marque lu par défaut |
|
|
185
187
|
| `get_thread` | Relire toute une conversation, du plus ancien au plus récent |
|
|
@@ -193,12 +195,18 @@ superviseur capable de lire les lignes d'un processus peut la brancher. Sans rel
|
|
|
193
195
|
|
|
194
196
|
**Noms lisibles** — voir « Nommer les boîtes et les fils » ci-dessous.
|
|
195
197
|
|
|
198
|
+
**Un message, deux boîtes** — un échange lie exactement deux agents *distincts* : `to` prend
|
|
199
|
+
une seule adresse, il n'y a ni diffusion ni copie, et une boîte ne peut pas s'écrire à
|
|
200
|
+
elle-même. Pour toucher plusieurs agents, on leur envoie un message chacun, dans son propre
|
|
201
|
+
fil. C'est aussi pourquoi `list_agents` ne montre jamais l'appelant : la seule adresse qu'il
|
|
202
|
+
ne peut pas viser n'a pas à figurer dans son annuaire.
|
|
203
|
+
|
|
196
204
|
**Dossiers système** — toujours présents, non supprimables : `inbox`, `sent`, `archive`,
|
|
197
205
|
`trash`. Les dossiers personnalisés sont propres à chaque boîte.
|
|
198
206
|
|
|
199
207
|
**Ce que voit un agent** : uniquement les livraisons de la boîte à laquelle il est attaché. Un message adressé à `coder`
|
|
200
208
|
est invisible pour tout autre agent, y compris via son identifiant. Supprimer un message ne
|
|
201
|
-
touche que sa propre copie :
|
|
209
|
+
touche que sa propre copie : l'autre partie garde la sienne.
|
|
202
210
|
|
|
203
211
|
---
|
|
204
212
|
|
|
@@ -220,25 +228,45 @@ ont deux boîtes distinctes et ne se volent pas leur courrier.
|
|
|
220
228
|
2. Transmets-la à l'agent B, qui l'utilise dans `send_message`.
|
|
221
229
|
|
|
222
230
|
Comme les adresses sont opaques, demande aux agents de nommer leur boîte
|
|
223
|
-
(`
|
|
231
|
+
(`rename_mailbox`) : le nom apparaît dans `list_agents` et dans la console web, ce
|
|
224
232
|
qui permet de retrouver une adresse perdue.
|
|
225
233
|
|
|
234
|
+
### Deux noms par boîte : l'agent et la boîte
|
|
235
|
+
|
|
236
|
+
Une boîte porte deux identités, qui répondent à deux questions différentes et ne changent
|
|
237
|
+
pas au même rythme :
|
|
238
|
+
|
|
239
|
+
| | D'où vient-il ? | De quoi s'occupe-t-il ? |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| Champ | `agent_name`, `agent_description` | `display_name`, `description` |
|
|
242
|
+
| Posé par | la configuration (`MESSENGER_AGENT_NAME`, `MESSENGER_AGENT_DESCRIPTION`) | l'agent lui-même, via `rename_mailbox` |
|
|
243
|
+
| Change | jamais depuis une session | librement, au fil du travail |
|
|
244
|
+
| Exemple | `Ai Corrector Dev` | `Correction du parseur de copies` |
|
|
245
|
+
|
|
246
|
+
Le nom de l'agent seul ne distingue pas deux sessions ouvertes sur le même projet ; le nom
|
|
247
|
+
de la boîte seul ne dit pas d'où l'agent écrit. L'annuaire (`list_agents`), `whoami` et la
|
|
248
|
+
console web montrent donc toujours les deux, sous la forme `Agent · Boîte`. Renommer la
|
|
249
|
+
boîte ne touche jamais au nom de l'agent.
|
|
250
|
+
|
|
226
251
|
### Nommer les boîtes et les fils
|
|
227
252
|
|
|
228
253
|
Les identifiants (`box_…`, `thr_…`) ne disent rien de ce qui se passe. Deux noms en langue
|
|
229
|
-
naturelle viennent par-dessus,
|
|
254
|
+
naturelle viennent par-dessus, un outil pour chacun :
|
|
230
255
|
|
|
231
256
|
```
|
|
232
|
-
|
|
233
|
-
→ nomme la boîte de la session
|
|
257
|
+
rename_mailbox(name: "Développement des tests unitaires du parser")
|
|
258
|
+
→ nomme la boîte de la session (le nom de l'agent reste)
|
|
234
259
|
|
|
235
|
-
|
|
260
|
+
rename_thread(thread_id: "thr_…", name: "Le test échoue sur un bug applicatif")
|
|
236
261
|
→ nomme le fil
|
|
237
262
|
```
|
|
238
263
|
|
|
239
|
-
Le plus simple reste de nommer un fil au moment où on l'ouvre, via `
|
|
264
|
+
Le plus simple reste de nommer un fil au moment où on l'ouvre, via `thread_name` sur
|
|
240
265
|
`send_message` : l'agent qui écrit le premier message sait déjà de quoi il parle.
|
|
241
266
|
|
|
267
|
+
L'ancien outil `set_label`, qui cumulait les deux rôles, reste accepté sans être publié :
|
|
268
|
+
une session lancée avant la mise à jour peut encore l'appeler.
|
|
269
|
+
|
|
242
270
|
Un fil n'a **qu'un seul nom, partagé par tous ses participants** — ils discutent du même
|
|
243
271
|
sujet, ils doivent lire le même intitulé. N'importe quel participant peut le renommer, et
|
|
244
272
|
le dernier a le dernier mot ; le serveur retient qui (`named_by`). Un agent extérieur au
|
|
@@ -273,8 +301,7 @@ configuration partagent alors la même boîte. Utile pour un service unique et s
|
|
|
273
301
|
|
|
274
302
|
## Console web
|
|
275
303
|
|
|
276
|
-
`http://127.0.0.1:3600/ui
|
|
277
|
-
**tous les échanges** de la flotte :
|
|
304
|
+
`http://127.0.0.1:3600/ui/` affiche, en lecture seule, **tous les échanges** de la flotte :
|
|
278
305
|
|
|
279
306
|
- la liste des agents avec leurs compteurs (envoyés, reçus, non lus) ;
|
|
280
307
|
- tous les messages, filtrables par agent, par fil ou par recherche texte ;
|
|
@@ -282,14 +309,18 @@ configuration partagent alors la même boîte. Utile pour un service unique et s
|
|
|
282
309
|
- le fil complet d'une conversation ;
|
|
283
310
|
- un rafraîchissement automatique toutes les 5 secondes.
|
|
284
311
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
`
|
|
288
|
-
|
|
289
|
-
|
|
312
|
+
À l'ouverture, la console demande `MESSENGER_ADMIN_TOKEN` dans un formulaire. Le serveur
|
|
313
|
+
le vérifie (`POST /ui/login`) et ouvre une **session** de 12 heures, portée par un cookie
|
|
314
|
+
`HttpOnly` + `SameSite=Strict` (+ `Secure` en HTTPS). Le cookie ne contient jamais le
|
|
315
|
+
jeton, seulement un identifiant de session opaque, oublié au redémarrage du serveur ou
|
|
316
|
+
sur « Se déconnecter ». Le navigateur ne stocke rien d'autre, et le jeton n'apparaît
|
|
317
|
+
jamais dans une URL.
|
|
290
318
|
|
|
291
|
-
|
|
292
|
-
|
|
319
|
+
Cette session n'est reconnue que sur `/ui` et `/admin` : elle ne permet pas d'appeler
|
|
320
|
+
l'API des agents. Un site tiers ne peut donc pas faire agir un agent à travers le
|
|
321
|
+
navigateur de l'administrateur. Les routes `/admin` acceptent toujours le jeton admin en
|
|
322
|
+
`Authorization: Bearer` pour les scripts. Le formulaire est limité à 10 tentatives par
|
|
323
|
+
quart d'heure et par IP.
|
|
293
324
|
|
|
294
325
|
La console n'écrit jamais : elle ne peut ni envoyer, ni supprimer, ni modifier.
|
|
295
326
|
|
|
@@ -321,7 +352,7 @@ Toutes les variables sont documentées dans [`.env.example`](.env.example). L'es
|
|
|
321
352
|
| `MESSENGER_AGENT_ID` | — | Adresse **fixe**. Vide (recommandé) = une boîte par session |
|
|
322
353
|
| `MESSENGER_SERVER_URL` | ✅ | `http://` ou `https://`, local ou distant |
|
|
323
354
|
| `MESSENGER_TOKEN` | ✅ | Jeton présenté au serveur, sur chaque appel |
|
|
324
|
-
| `MESSENGER_AGENT_NAME` | — | Nom
|
|
355
|
+
| `MESSENGER_AGENT_NAME` | — | Nom de l'agent (le projet, typiquement), affiché à côté du nom de la boîte |
|
|
325
356
|
| `MESSENGER_AGENT_DESCRIPTION` | — | À quoi sert cet agent, pour ses correspondants |
|
|
326
357
|
| `MESSENGER_TIMEOUT_MS` | `15000` | Délai maximal d'un appel HTTP |
|
|
327
358
|
| `MESSENGER_STATE_DIR` | — | Répertoire des relais vers le veilleur (défaut : `~/.mcp-messenger`) |
|
|
@@ -330,16 +361,17 @@ Toutes les variables sont documentées dans [`.env.example`](.env.example). L'es
|
|
|
330
361
|
|
|
331
362
|
## API HTTP
|
|
332
363
|
|
|
333
|
-
**Toutes** les routes exigent un jeton, présenté
|
|
334
|
-
<token
|
|
335
|
-
|
|
336
|
-
|
|
364
|
+
**Toutes** les routes de données exigent un jeton, présenté par `Authorization: Bearer
|
|
365
|
+
<token>` ou par l'en-tête `X-Messenger-Token` — jamais dans l'URL. Les routes `/api/*`
|
|
366
|
+
exigent en plus l'en-tête `X-Agent-Id`, et le jeton doit correspondre à cet agent lorsque
|
|
367
|
+
`MESSENGER_AGENT_TOKENS` est utilisé. Les routes `/admin/*` acceptent aussi la session de
|
|
368
|
+
la console (cookie), qui elle n'ouvre rien d'autre.
|
|
337
369
|
|
|
338
370
|
| Méthode | Route | Rôle |
|
|
339
371
|
|---|---|---|
|
|
340
372
|
| `GET` | `/health` | État du service |
|
|
341
373
|
| `POST` | `/api/agents/register` | S'inscrire à l'annuaire (`fresh: true` = exige une adresse neuve) |
|
|
342
|
-
| `GET` | `/api/agents` | Annuaire des boîtes |
|
|
374
|
+
| `GET` | `/api/agents` | Annuaire des boîtes, sauf celle de l'appelant |
|
|
343
375
|
| `GET` | `/api/agents/:id` | Une boîte, ou 404 |
|
|
344
376
|
| `PATCH` | `/api/agents/me` | Renommer la boîte courante |
|
|
345
377
|
| `GET` | `/api/mailbox` | Compteurs de la boîte, et présence d'un veilleur (`watcher`) |
|
|
@@ -355,6 +387,7 @@ cet agent lorsque `MESSENGER_AGENT_TOKENS` est utilisé.
|
|
|
355
387
|
| `GET` `POST` `DELETE` | `/api/folders[/:name]` | Gérer les dossiers |
|
|
356
388
|
| `GET` | `/api/wait` | Attente longue (`timeout_seconds`, `folder`, `watcher`) |
|
|
357
389
|
| `GET` | `/admin/overview` · `/admin/messages[/:id]` · `/admin/threads/:id` | Supervision, lecture seule |
|
|
390
|
+
| `POST` | `/ui/login` · `/ui/logout` | Session de la console (`{ "token": … }` → cookie `HttpOnly`) |
|
|
358
391
|
|
|
359
392
|
---
|
|
360
393
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Génère un jeton aléatoire cryptographiquement sûr.
|
|
3
3
|
*
|
|
4
|
-
* L'encodage `base64url`
|
|
5
|
-
*
|
|
4
|
+
* L'encodage `base64url` évite tout caractère à échapper (`+`, `/`, `=`) : le jeton se
|
|
5
|
+
* copie tel quel dans un `.env`, un `.mcp.json` ou un en-tête HTTP.
|
|
6
6
|
*/
|
|
7
7
|
export declare function generateToken(bytes?: number): string;
|
|
@@ -4,8 +4,8 @@ const DEFAULT_BYTES = 32;
|
|
|
4
4
|
/**
|
|
5
5
|
* Génère un jeton aléatoire cryptographiquement sûr.
|
|
6
6
|
*
|
|
7
|
-
* L'encodage `base64url`
|
|
8
|
-
*
|
|
7
|
+
* L'encodage `base64url` évite tout caractère à échapper (`+`, `/`, `=`) : le jeton se
|
|
8
|
+
* copie tel quel dans un `.env`, un `.mcp.json` ou un en-tête HTTP.
|
|
9
9
|
*/
|
|
10
10
|
export function generateToken(bytes = DEFAULT_BYTES) {
|
|
11
11
|
return randomBytes(bytes).toString("base64url");
|
|
@@ -44,7 +44,7 @@ const REQUIRED_VARS = [
|
|
|
44
44
|
];
|
|
45
45
|
const OPTIONAL_VARS = [
|
|
46
46
|
["MESSENGER_AGENT_ID", "adresse fixe : à laisser vide pour une boîte par session"],
|
|
47
|
-
["MESSENGER_AGENT_NAME", "nom
|
|
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
50
|
];
|
|
@@ -140,7 +140,7 @@ export function runSetupMcp({ cwd, force }) {
|
|
|
140
140
|
console.log("");
|
|
141
141
|
console.log("Le serveur boîte aux lettres doit tourner à part, avec le même jeton :");
|
|
142
142
|
console.log(` MESSENGER_TOKEN=<votre-jeton> npx -y ${PACKAGE_NAME} --server`);
|
|
143
|
-
console.log(` console web : ${DEFAULT_SERVER_URL}/ui
|
|
143
|
+
console.log(` console web : ${DEFAULT_SERVER_URL}/ui/ (le jeton y est demandé une fois)`);
|
|
144
144
|
console.log("");
|
|
145
145
|
console.log("Chaque session d'agent reçoit sa propre boîte : demandez son adresse à l'agent");
|
|
146
146
|
console.log("(outil whoami) pour la transmettre à un autre agent, ou pour la reprendre plus tard.");
|
package/dist/src/cli/watch.js
CHANGED
|
@@ -44,8 +44,8 @@ export function resolveWatchConfig(mailbox, env = process.env) {
|
|
|
44
44
|
agentId: target,
|
|
45
45
|
serverUrl: serverUrl.replace(/\/+$/, ""),
|
|
46
46
|
token: handoff?.token ?? env.MESSENGER_TOKEN?.trim() ?? null,
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
agentName: null,
|
|
48
|
+
agentDescription: null,
|
|
49
49
|
timeoutMs: Number(env.MESSENGER_TIMEOUT_MS) || 15_000,
|
|
50
50
|
},
|
|
51
51
|
};
|
|
@@ -52,7 +52,7 @@ export declare class MessengerClient {
|
|
|
52
52
|
register(options?: {
|
|
53
53
|
fresh?: boolean;
|
|
54
54
|
}): Promise<Agent>;
|
|
55
|
-
/** Renomme la boîte courante, pour
|
|
55
|
+
/** Renomme la boîte courante (pas l'agent), pour dire de quoi cette session s'occupe. */
|
|
56
56
|
updateProfile(profile: {
|
|
57
57
|
display_name?: string | null;
|
|
58
58
|
description?: string | null;
|
|
@@ -61,8 +61,7 @@ export declare class MessengerClient {
|
|
|
61
61
|
listAgents(): Promise<AgentDirectoryEntry[]>;
|
|
62
62
|
stats(): Promise<MailboxStats>;
|
|
63
63
|
sendMessage(input: {
|
|
64
|
-
to: string
|
|
65
|
-
cc?: string[];
|
|
64
|
+
to: string;
|
|
66
65
|
subject: string;
|
|
67
66
|
body: string;
|
|
68
67
|
priority?: Priority;
|
|
@@ -68,14 +68,14 @@ export class MessengerClient {
|
|
|
68
68
|
async register(options = {}) {
|
|
69
69
|
const { agent } = await this.request("POST", "/api/agents/register", {
|
|
70
70
|
body: {
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
agent_name: this.config.agentName,
|
|
72
|
+
agent_description: this.config.agentDescription,
|
|
73
73
|
fresh: options.fresh ?? false,
|
|
74
74
|
},
|
|
75
75
|
});
|
|
76
76
|
return agent;
|
|
77
77
|
}
|
|
78
|
-
/** Renomme la boîte courante, pour
|
|
78
|
+
/** Renomme la boîte courante (pas l'agent), pour dire de quoi cette session s'occupe. */
|
|
79
79
|
async updateProfile(profile) {
|
|
80
80
|
const { agent } = await this.request("PATCH", "/api/agents/me", { body: profile });
|
|
81
81
|
return agent;
|
package/dist/src/config.d.ts
CHANGED
|
@@ -27,8 +27,9 @@ export interface AgentConfig {
|
|
|
27
27
|
agentId: string | null;
|
|
28
28
|
serverUrl: string;
|
|
29
29
|
token: string | null;
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
/** Identité de l'agent, stable : d'où il écrit (le projet). Distincte du nom de la boîte. */
|
|
31
|
+
agentName: string | null;
|
|
32
|
+
agentDescription: string | null;
|
|
32
33
|
timeoutMs: number;
|
|
33
34
|
}
|
|
34
35
|
export declare function isLoopbackHost(host: string): boolean;
|
package/dist/src/config.js
CHANGED
|
@@ -74,8 +74,8 @@ export function loadAgentConfig(env = process.env) {
|
|
|
74
74
|
agentId,
|
|
75
75
|
serverUrl: rawUrl.replace(/\/+$/, ""),
|
|
76
76
|
token: nonEmpty(env.MESSENGER_TOKEN),
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
agentName: nonEmpty(env.MESSENGER_AGENT_NAME),
|
|
78
|
+
agentDescription: nonEmpty(env.MESSENGER_AGENT_DESCRIPTION),
|
|
79
79
|
timeoutMs: parseIntEnv(env.MESSENGER_TIMEOUT_MS, 15_000, "MESSENGER_TIMEOUT_MS"),
|
|
80
80
|
};
|
|
81
81
|
}
|
package/dist/src/core/store.d.ts
CHANGED
|
@@ -5,6 +5,15 @@ export declare class MessengerError extends Error {
|
|
|
5
5
|
readonly status: number;
|
|
6
6
|
constructor(message: string, status?: number);
|
|
7
7
|
}
|
|
8
|
+
export interface EnsureAgentOptions {
|
|
9
|
+
registered?: boolean;
|
|
10
|
+
/** Identité de l'agent : d'où il vient. Posée par la configuration. */
|
|
11
|
+
agent_name?: string | null;
|
|
12
|
+
agent_description?: string | null;
|
|
13
|
+
/** Nom de la boîte : de quoi cette session s'occupe. Posé par l'agent lui-même. */
|
|
14
|
+
display_name?: string | null;
|
|
15
|
+
description?: string | null;
|
|
16
|
+
}
|
|
8
17
|
export interface StoreEvents {
|
|
9
18
|
/** Émis pour chaque destinataire d'un message, y compris la copie « sent » de l'expéditeur. */
|
|
10
19
|
message: [{
|
|
@@ -26,16 +35,21 @@ export declare class MessageStore extends EventEmitter<StoreEvents> {
|
|
|
26
35
|
constructor(dbPath: string);
|
|
27
36
|
private migrate;
|
|
28
37
|
close(): void;
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Crée la fiche au besoin. `registered: false` = agent connu seulement comme destinataire.
|
|
40
|
+
* Chaque champ omis (ou null) laisse la valeur en place : l'enregistrement pose l'identité
|
|
41
|
+
* de l'agent sans toucher au nom de la boîte, et `rename_mailbox` fait l'inverse.
|
|
42
|
+
*/
|
|
43
|
+
ensureAgent(id: string, opts?: EnsureAgentOptions): Agent;
|
|
35
44
|
touchAgent(id: string): void;
|
|
36
45
|
getAgent(id: string): Agent | null;
|
|
37
|
-
/**
|
|
38
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Annuaire classé par activité décroissante : ce qui bouge est en tête, l'id ne trie rien.
|
|
48
|
+
*
|
|
49
|
+
* `exclude` retire une adresse du résultat : l'annuaire servi à un agent ne lui montre pas
|
|
50
|
+
* sa propre boîte, la seule à laquelle il ne peut pas écrire.
|
|
51
|
+
*/
|
|
52
|
+
listAgents(exclude?: string): AgentDirectoryEntry[];
|
|
39
53
|
sendMessage(input: SendMessageInput): SendMessageResult;
|
|
40
54
|
listMessages(agentId: string, opts?: ListMessagesOptions): MessageSummary[];
|
|
41
55
|
getMessage(agentId: string, messageId: string, markRead?: boolean): Message;
|
|
@@ -103,6 +117,7 @@ export declare class MessageStore extends EventEmitter<StoreEvents> {
|
|
|
103
117
|
/** Filtre les ids fournis sur ceux réellement livrés à cet agent (isolation des boîtes). */
|
|
104
118
|
private ownedMessageIds;
|
|
105
119
|
private assertFolderExists;
|
|
120
|
+
/** Destinataire de chaque message demandé, par identifiant de message. */
|
|
106
121
|
private recipientsFor;
|
|
107
122
|
private toSummary;
|
|
108
123
|
private toMessage;
|
package/dist/src/core/store.js
CHANGED
|
@@ -52,9 +52,11 @@ export class MessageStore extends EventEmitter {
|
|
|
52
52
|
migrate() {
|
|
53
53
|
this.db.exec(`
|
|
54
54
|
CREATE TABLE IF NOT EXISTS agents (
|
|
55
|
-
id
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
agent_name TEXT,
|
|
57
|
+
agent_description TEXT,
|
|
58
|
+
display_name TEXT,
|
|
59
|
+
description TEXT,
|
|
58
60
|
created_at TEXT NOT NULL,
|
|
59
61
|
last_seen_at TEXT,
|
|
60
62
|
registered INTEGER NOT NULL DEFAULT 0
|
|
@@ -103,6 +105,21 @@ export class MessageStore extends EventEmitter {
|
|
|
103
105
|
CREATE INDEX IF NOT EXISTS idx_deliveries_message ON deliveries (message_id);
|
|
104
106
|
CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages (thread_id, created_at);
|
|
105
107
|
`);
|
|
108
|
+
// Bases antérieures à la séparation agent / boîte : `display_name` y tenait les deux
|
|
109
|
+
// rôles, et venait presque toujours de la configuration (le nom du projet). On le
|
|
110
|
+
// déplace vers l'identité de l'agent ; la boîte repart sans nom, à poser par l'agent.
|
|
111
|
+
const agentColumns = this.db.prepare("PRAGMA table_info(agents)").all().map((c) => c.name);
|
|
112
|
+
if (!agentColumns.includes("agent_name")) {
|
|
113
|
+
this.db.exec(`
|
|
114
|
+
ALTER TABLE agents ADD COLUMN agent_name TEXT;
|
|
115
|
+
ALTER TABLE agents ADD COLUMN agent_description TEXT;
|
|
116
|
+
UPDATE agents
|
|
117
|
+
SET agent_name = display_name,
|
|
118
|
+
agent_description = description,
|
|
119
|
+
display_name = NULL,
|
|
120
|
+
description = NULL;
|
|
121
|
+
`);
|
|
122
|
+
}
|
|
106
123
|
// Bases antérieures aux fils nommés : chaque fil déjà présent reçoit sa ligne, sans
|
|
107
124
|
// titre. L'affichage retombe alors sur le sujet d'ouverture, comme avant.
|
|
108
125
|
this.db.exec(`
|
|
@@ -117,25 +134,31 @@ export class MessageStore extends EventEmitter {
|
|
|
117
134
|
this.db.close();
|
|
118
135
|
}
|
|
119
136
|
// --- Agents -------------------------------------------------------------
|
|
120
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Crée la fiche au besoin. `registered: false` = agent connu seulement comme destinataire.
|
|
139
|
+
* Chaque champ omis (ou null) laisse la valeur en place : l'enregistrement pose l'identité
|
|
140
|
+
* de l'agent sans toucher au nom de la boîte, et `rename_mailbox` fait l'inverse.
|
|
141
|
+
*/
|
|
121
142
|
ensureAgent(id, opts = {}) {
|
|
122
143
|
const now = new Date().toISOString();
|
|
123
144
|
const existing = this.db.prepare("SELECT id FROM agents WHERE id = ?").get(id);
|
|
124
145
|
if (!existing) {
|
|
125
146
|
this.db
|
|
126
|
-
.prepare(`INSERT INTO agents (id, display_name, description, created_at, last_seen_at, registered)
|
|
127
|
-
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
128
|
-
.run(id, opts.display_name ?? null, opts.description ?? null, now, opts.registered ? now : null, opts.registered ? 1 : 0);
|
|
147
|
+
.prepare(`INSERT INTO agents (id, agent_name, agent_description, display_name, description, created_at, last_seen_at, registered)
|
|
148
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
149
|
+
.run(id, opts.agent_name ?? null, opts.agent_description ?? null, opts.display_name ?? null, opts.description ?? null, now, opts.registered ? now : null, opts.registered ? 1 : 0);
|
|
129
150
|
}
|
|
130
151
|
else {
|
|
131
152
|
this.db
|
|
132
153
|
.prepare(`UPDATE agents
|
|
133
|
-
SET
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
SET agent_name = COALESCE(?, agent_name),
|
|
155
|
+
agent_description = COALESCE(?, agent_description),
|
|
156
|
+
display_name = COALESCE(?, display_name),
|
|
157
|
+
description = COALESCE(?, description),
|
|
158
|
+
registered = MAX(registered, ?),
|
|
159
|
+
last_seen_at = CASE WHEN ? = 1 THEN ? ELSE last_seen_at END
|
|
137
160
|
WHERE id = ?`)
|
|
138
|
-
.run(opts.display_name ?? null, opts.description ?? null, opts.registered ? 1 : 0, opts.registered ? 1 : 0, now, id);
|
|
161
|
+
.run(opts.agent_name ?? null, opts.agent_description ?? null, opts.display_name ?? null, opts.description ?? null, opts.registered ? 1 : 0, opts.registered ? 1 : 0, now, id);
|
|
139
162
|
}
|
|
140
163
|
return this.getAgent(id);
|
|
141
164
|
}
|
|
@@ -146,13 +169,20 @@ export class MessageStore extends EventEmitter {
|
|
|
146
169
|
const row = this.db.prepare("SELECT * FROM agents WHERE id = ?").get(id);
|
|
147
170
|
return row ? { ...row, registered: Boolean(row.registered) } : null;
|
|
148
171
|
}
|
|
149
|
-
/**
|
|
150
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Annuaire classé par activité décroissante : ce qui bouge est en tête, l'id ne trie rien.
|
|
174
|
+
*
|
|
175
|
+
* `exclude` retire une adresse du résultat : l'annuaire servi à un agent ne lui montre pas
|
|
176
|
+
* sa propre boîte, la seule à laquelle il ne peut pas écrire.
|
|
177
|
+
*/
|
|
178
|
+
listAgents(exclude) {
|
|
179
|
+
const excluded = exclude ? normalizeAgentId(exclude) : "";
|
|
151
180
|
const rows = this.db
|
|
152
181
|
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at
|
|
153
182
|
FROM agents a
|
|
183
|
+
WHERE a.id != ?
|
|
154
184
|
ORDER BY last_activity_at DESC, a.id`)
|
|
155
|
-
.all();
|
|
185
|
+
.all(excluded);
|
|
156
186
|
return rows.map((r) => ({
|
|
157
187
|
...r,
|
|
158
188
|
registered: Boolean(r.registered),
|
|
@@ -169,11 +199,14 @@ export class MessageStore extends EventEmitter {
|
|
|
169
199
|
const from = normalizeAgentId(input.from);
|
|
170
200
|
if (!from)
|
|
171
201
|
throw new MessengerError("sender agent id is required");
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
202
|
+
// Un message va d'une boîte à une seule autre : pas de diffusion, pas de copie.
|
|
203
|
+
const to = normalizeAgentId(input.to);
|
|
204
|
+
if (!to)
|
|
205
|
+
throw new MessengerError("a recipient is required in `to`");
|
|
206
|
+
// Une boîte ne se parle pas à elle-même : la copie « sent » et la copie « inbox »
|
|
207
|
+
// désigneraient le même agent, et le fil n'aurait qu'un seul participant.
|
|
208
|
+
if (to === from)
|
|
209
|
+
throw new MessengerError("an agent cannot send a message to itself");
|
|
177
210
|
const title = normalizeLabel(input.thread_title, THREAD_TITLE_MAX, "thread_title");
|
|
178
211
|
const threadDescription = normalizeLabel(input.thread_description, THREAD_DESCRIPTION_MAX, "thread_description");
|
|
179
212
|
let threadId;
|
|
@@ -191,18 +224,16 @@ export class MessageStore extends EventEmitter {
|
|
|
191
224
|
const messageId = newId("msg");
|
|
192
225
|
const now = new Date().toISOString();
|
|
193
226
|
const priority = input.priority ?? "normal";
|
|
194
|
-
const
|
|
227
|
+
const unknownRecipient = !this.getAgent(to)?.registered;
|
|
195
228
|
const deliveries = [
|
|
196
|
-
|
|
197
|
-
...cc.map((agent_id) => ({ agent_id, role: "cc", folder: "inbox", read_at: null })),
|
|
229
|
+
{ agent_id: to, role: "to", folder: "inbox", read_at: null },
|
|
198
230
|
{ agent_id: from, role: "sender", folder: "sent", read_at: now },
|
|
199
231
|
];
|
|
200
232
|
this.db.transaction(() => {
|
|
201
233
|
this.ensureAgent(from);
|
|
202
234
|
// Écrire est le signe d'activité le plus fiable : l'annuaire s'appuie dessus.
|
|
203
235
|
this.touchAgent(from);
|
|
204
|
-
|
|
205
|
-
this.ensureAgent(id);
|
|
236
|
+
this.ensureAgent(to);
|
|
206
237
|
// Le fil naît avec son premier message ; sur une réponse, la ligne existe déjà et
|
|
207
238
|
// n'est touchée que si l'expéditeur fournit un nouveau titre.
|
|
208
239
|
this.db
|
|
@@ -231,8 +262,8 @@ export class MessageStore extends EventEmitter {
|
|
|
231
262
|
thread_id: threadId,
|
|
232
263
|
thread_title: this.getThreadInfo(threadId)?.title ?? null,
|
|
233
264
|
created_at: now,
|
|
234
|
-
delivered_to:
|
|
235
|
-
|
|
265
|
+
delivered_to: to,
|
|
266
|
+
unknown_recipient: unknownRecipient,
|
|
236
267
|
};
|
|
237
268
|
}
|
|
238
269
|
// --- Lecture ------------------------------------------------------------
|
|
@@ -282,7 +313,7 @@ export class MessageStore extends EventEmitter {
|
|
|
282
313
|
JOIN messages m ON m.id = d.message_id
|
|
283
314
|
${THREAD_JOIN}
|
|
284
315
|
WHERE d.agent_id = ? AND d.message_id = ?
|
|
285
|
-
ORDER BY CASE d.role WHEN 'to' THEN 0
|
|
316
|
+
ORDER BY CASE d.role WHEN 'to' THEN 0 ELSE 1 END
|
|
286
317
|
LIMIT 1`)
|
|
287
318
|
.get(agentId, messageId);
|
|
288
319
|
if (!row)
|
|
@@ -628,6 +659,7 @@ export class MessageStore extends EventEmitter {
|
|
|
628
659
|
if (!row)
|
|
629
660
|
throw new MessengerError(`unknown folder: ${folder}`, 404);
|
|
630
661
|
}
|
|
662
|
+
/** Destinataire de chaque message demandé, par identifiant de message. */
|
|
631
663
|
recipientsFor(messageIds) {
|
|
632
664
|
const map = new Map();
|
|
633
665
|
const ids = dedupe(messageIds);
|
|
@@ -635,29 +667,20 @@ export class MessageStore extends EventEmitter {
|
|
|
635
667
|
return map;
|
|
636
668
|
const placeholders = ids.map(() => "?").join(",");
|
|
637
669
|
const rows = this.db
|
|
638
|
-
.prepare(`SELECT message_id, agent_id
|
|
639
|
-
WHERE message_id IN (${placeholders}) AND role
|
|
640
|
-
ORDER BY agent_id`)
|
|
670
|
+
.prepare(`SELECT message_id, agent_id FROM deliveries
|
|
671
|
+
WHERE message_id IN (${placeholders}) AND role = 'to'`)
|
|
641
672
|
.all(...ids);
|
|
642
|
-
for (const row of rows)
|
|
643
|
-
|
|
644
|
-
if (row.role === "to")
|
|
645
|
-
entry.to.push(row.agent_id);
|
|
646
|
-
else if (row.role === "cc")
|
|
647
|
-
entry.cc.push(row.agent_id);
|
|
648
|
-
map.set(row.message_id, entry);
|
|
649
|
-
}
|
|
673
|
+
for (const row of rows)
|
|
674
|
+
map.set(row.message_id, row.agent_id);
|
|
650
675
|
return map;
|
|
651
676
|
}
|
|
652
677
|
toSummary(row, recipients) {
|
|
653
|
-
const rcpt = recipients.get(row.message_id) ?? { to: [], cc: [] };
|
|
654
678
|
return {
|
|
655
679
|
message_id: row.message_id,
|
|
656
680
|
thread_id: row.thread_id,
|
|
657
681
|
thread_title: row.thread_title,
|
|
658
682
|
from: row.sender_id,
|
|
659
|
-
to:
|
|
660
|
-
cc: rcpt.cc,
|
|
683
|
+
to: recipients.get(row.message_id) ?? "",
|
|
661
684
|
subject: row.subject,
|
|
662
685
|
preview: preview(row.body),
|
|
663
686
|
priority: row.priority,
|