@imenam/mcp-messenger 1.0.8 → 1.0.10
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 +24 -7
- package/dist/src/client/messenger-client.d.ts +1 -0
- package/dist/src/core/store.d.ts +10 -0
- package/dist/src/core/store.js +29 -1
- package/dist/src/core/types.d.ts +7 -0
- package/dist/src/mcp/start.js +2 -1
- package/dist/src/mcp/tools.js +134 -31
- package/dist/src/server/app.js +3 -2
- package/dist/src/server/ui/app.js +57 -5
- package/dist/src/server/ui/styles.css +40 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -179,8 +179,8 @@ superviseur capable de lire les lignes d'un processus peut la brancher. Sans rel
|
|
|
179
179
|
| `attach_to_mailbox` | Se brancher sur une autre boîte via son adresse ; sans argument, retour à la sienne |
|
|
180
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
181
|
| `rename_thread` | Nommer ou renommer un fil (`thread_id`) ; le nom est partagé par tous ses participants |
|
|
182
|
-
| `list_threads` | Carte des conversations : nom, participants, non-lus, dernière activité |
|
|
183
|
-
| `list_agents` | Annuaire :
|
|
182
|
+
| `list_threads` | Carte des conversations : nom, participants, non-lus, dernière activité ; `with_agent` ne garde que celles partagées avec une boîte |
|
|
183
|
+
| `list_agents` | Annuaire groupé par agent : un projet, ses sessions ; `agent_name` déplie un groupe. La boîte de l'appelant n'y figure pas |
|
|
184
184
|
| `send_message` | Écrire à un agent — un seul destinataire par message (`to`, `priority`, `reply_to`, `thread_name`) |
|
|
185
185
|
| `list_messages` | Lister la boîte (filtres : dossier, non lus, expéditeur, fil, texte libre) |
|
|
186
186
|
| `read_message` | Lire un message en entier ; le marque lu par défaut |
|
|
@@ -224,9 +224,19 @@ ont deux boîtes distinctes et ne se volent pas leur courrier.
|
|
|
224
224
|
|
|
225
225
|
### Faire communiquer deux agents
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
Le plus souvent, il n'y a rien à faire : l'agent B ouvre `list_agents` et y trouve
|
|
228
|
+
l'adresse de A. C'est le chemin normal, et le seul qui ne puisse pas se tromper.
|
|
229
|
+
|
|
230
|
+
Si tu veux désigner toi-même l'interlocuteur :
|
|
231
|
+
|
|
232
|
+
1. Demande son adresse à l'agent A (`whoami`) — il te la donne, c'est à toi qu'elle est due.
|
|
228
233
|
2. Transmets-la à l'agent B, qui l'utilise dans `send_message`.
|
|
229
234
|
|
|
235
|
+
Dans les deux cas, une adresse ne circule jamais **dans** un message : le serveur y attache
|
|
236
|
+
déjà l'expéditeur et le destinataire. Un agent qui recopie une adresse dans un corps de
|
|
237
|
+
message écrit une information que le lecteur a déjà, et qui sera fausse le jour où il se
|
|
238
|
+
trompe de session.
|
|
239
|
+
|
|
230
240
|
Comme les adresses sont opaques, demande aux agents de nommer leur boîte
|
|
231
241
|
(`rename_mailbox`) : le nom apparaît dans `list_agents` et dans la console web, ce
|
|
232
242
|
qui permet de retrouver une adresse perdue.
|
|
@@ -244,12 +254,18 @@ pas au même rythme :
|
|
|
244
254
|
| Exemple | `Ai Corrector Dev` | `Correction du parseur de copies` |
|
|
245
255
|
|
|
246
256
|
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.
|
|
248
|
-
|
|
249
|
-
boîte
|
|
257
|
+
de la boîte seul ne dit pas d'où l'agent écrit. `whoami` et la console web montrent donc
|
|
258
|
+
toujours les deux, et l'annuaire (`list_agents`) les met l'un sous l'autre : le nom de
|
|
259
|
+
l'agent en tête de groupe, une ligne par boîte en dessous. Renommer la boîte ne touche
|
|
260
|
+
jamais au nom de l'agent.
|
|
250
261
|
|
|
251
262
|
**Un nom d'agent ne désigne personne.** C'est le piège de ce modèle : plusieurs boîtes
|
|
252
|
-
portent le même `agent_name`, une par session lancée depuis la même configuration.
|
|
263
|
+
portent le même `agent_name`, une par session lancée depuis la même configuration.
|
|
264
|
+
L'annuaire le rend visible en groupant les boîtes sous leur nom d'agent — un groupe à
|
|
265
|
+
plusieurs lignes, ce sont plusieurs sessions vivantes du même projet, chacune à sa propre
|
|
266
|
+
adresse. Un groupe nombreux n'affiche que ses trois sessions les plus récentes ;
|
|
267
|
+
`list_agents` avec `agent_name` donne le reste, sans imposer ce second appel dans le cas
|
|
268
|
+
courant. Un agent
|
|
253
269
|
qui croise son propre nom d'agent dans l'annuaire regarde une *autre* session du même
|
|
254
270
|
projet, jamais la sienne — sa boîte, elle, n'y figure pas du tout. Seule l'adresse identifie
|
|
255
271
|
une session. Ce rappel est servi à trois endroits, du plus tôt au plus tard : les
|
|
@@ -319,6 +335,7 @@ configuration partagent alors la même boîte. Utile pour un service unique et s
|
|
|
319
335
|
- la liste des agents avec leurs compteurs (envoyés, reçus, non lus) ;
|
|
320
336
|
- tous les messages, filtrables par agent, par fil ou par recherche texte ;
|
|
321
337
|
- le contenu d'un message et son état de distribution (qui l'a reçu, dans quel dossier, lu ou non) ;
|
|
338
|
+
- sous le message ouvert, les échanges qui l'ont précédé, repliés par défaut et dépliables d'un bouton, comme dans une boîte mail ;
|
|
322
339
|
- le fil complet d'une conversation ;
|
|
323
340
|
- un rafraîchissement automatique toutes les 5 secondes.
|
|
324
341
|
|
package/dist/src/core/store.d.ts
CHANGED
|
@@ -95,6 +95,16 @@ export declare class MessageStore extends EventEmitter<StoreEvents> {
|
|
|
95
95
|
limit?: number;
|
|
96
96
|
offset?: number;
|
|
97
97
|
}): MessageSummary[];
|
|
98
|
+
/**
|
|
99
|
+
* Fil complet vu du serveur, du plus ancien au plus récent, corps inclus.
|
|
100
|
+
*
|
|
101
|
+
* La console affiche les échanges antérieurs sous le message ouvert : elle a besoin du
|
|
102
|
+
* texte, pas seulement de l'aperçu. Les deux sont renvoyés, l'aperçu servant aux vues
|
|
103
|
+
* qui ne déplient rien.
|
|
104
|
+
*/
|
|
105
|
+
listThreadAdmin(threadId: string): (MessageSummary & {
|
|
106
|
+
body: string;
|
|
107
|
+
})[];
|
|
98
108
|
/** Message complet vu du serveur, avec l'état de lecture de chaque destinataire. */
|
|
99
109
|
getMessageAdmin(messageId: string): Message & {
|
|
100
110
|
deliveries: {
|
package/dist/src/core/store.js
CHANGED
|
@@ -376,9 +376,18 @@ export class MessageStore extends EventEmitter {
|
|
|
376
376
|
const limit = clamp(opts.limit ?? 25, 1, MAX_LIMIT);
|
|
377
377
|
const offset = Math.max(0, opts.offset ?? 0);
|
|
378
378
|
const having = [];
|
|
379
|
+
const where = ["d.agent_id = ?", "d.folder != 'trash'"];
|
|
379
380
|
const params = [agentId];
|
|
380
381
|
if (opts.unread_only)
|
|
381
382
|
having.push("unread > 0");
|
|
383
|
+
// Correspondance avec une boîte donnée : le fil compte dès qu'elle y a une livraison,
|
|
384
|
+
// qu'elle ait écrit ou seulement reçu. Les paramètres du WHERE se lient avant ceux du
|
|
385
|
+
// HAVING : cette condition doit donc être poussée avant celles de la recherche.
|
|
386
|
+
if (opts.with_agent) {
|
|
387
|
+
where.push(`EXISTS (SELECT 1 FROM deliveries d2 JOIN messages m2 ON m2.id = d2.message_id
|
|
388
|
+
WHERE m2.thread_id = m.thread_id AND d2.agent_id = ?)`);
|
|
389
|
+
params.push(normalizeAgentId(opts.with_agent));
|
|
390
|
+
}
|
|
382
391
|
if (opts.search) {
|
|
383
392
|
// Le titre prime, mais un fil non nommé ne doit pas devenir introuvable :
|
|
384
393
|
// la recherche couvre aussi le sujet d'ouverture.
|
|
@@ -398,7 +407,7 @@ export class MessageStore extends EventEmitter {
|
|
|
398
407
|
FROM deliveries d
|
|
399
408
|
JOIN messages m ON m.id = d.message_id
|
|
400
409
|
${THREAD_JOIN}
|
|
401
|
-
WHERE
|
|
410
|
+
WHERE ${where.join(" AND ")}
|
|
402
411
|
GROUP BY m.thread_id
|
|
403
412
|
${having.length ? `HAVING ${having.join(" AND ")}` : ""}
|
|
404
413
|
ORDER BY last_message_at DESC
|
|
@@ -590,6 +599,25 @@ export class MessageStore extends EventEmitter {
|
|
|
590
599
|
const recipients = this.recipientsFor(rows.map((r) => r.message_id));
|
|
591
600
|
return rows.map((row) => this.toSummary(row, recipients));
|
|
592
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* Fil complet vu du serveur, du plus ancien au plus récent, corps inclus.
|
|
604
|
+
*
|
|
605
|
+
* La console affiche les échanges antérieurs sous le message ouvert : elle a besoin du
|
|
606
|
+
* texte, pas seulement de l'aperçu. Les deux sont renvoyés, l'aperçu servant aux vues
|
|
607
|
+
* qui ne déplient rien.
|
|
608
|
+
*/
|
|
609
|
+
listThreadAdmin(threadId) {
|
|
610
|
+
const rows = this.db
|
|
611
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
612
|
+
m.priority, m.in_reply_to, m.created_at, 'inbox' AS folder, 'to' AS role, NULL AS read_at, 0 AS starred
|
|
613
|
+
FROM messages m
|
|
614
|
+
${THREAD_JOIN}
|
|
615
|
+
WHERE m.thread_id = ?
|
|
616
|
+
ORDER BY m.created_at ASC, m.id ASC`)
|
|
617
|
+
.all(threadId);
|
|
618
|
+
const recipients = this.recipientsFor(rows.map((r) => r.message_id));
|
|
619
|
+
return rows.map((row) => ({ ...this.toSummary(row, recipients), body: row.body }));
|
|
620
|
+
}
|
|
593
621
|
/** Message complet vu du serveur, avec l'état de lecture de chaque destinataire. */
|
|
594
622
|
getMessageAdmin(messageId) {
|
|
595
623
|
const row = this.db
|
package/dist/src/core/types.d.ts
CHANGED
|
@@ -111,6 +111,13 @@ export interface FolderInfo {
|
|
|
111
111
|
export interface ListThreadsOptions {
|
|
112
112
|
/** Ne garder que les fils ayant au moins un message non lu. */
|
|
113
113
|
unread_only?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Adresse d'un correspondant : ne garder que les fils qu'il partage avec cette boîte.
|
|
116
|
+
*
|
|
117
|
+
* L'unité d'une correspondance est le fil, pas le message : « ce que j'ai échangé avec
|
|
118
|
+
* cette boîte » se lit ici, et non en filtrant les messages sur leur expéditeur.
|
|
119
|
+
*/
|
|
120
|
+
with_agent?: string;
|
|
114
121
|
/** Texte libre confronté au titre, à la description et au sujet d'ouverture. */
|
|
115
122
|
search?: string;
|
|
116
123
|
limit?: number;
|
package/dist/src/mcp/start.js
CHANGED
|
@@ -21,7 +21,8 @@ const INSTRUCTIONS = `mcp-messenger gives this session a mailbox and lets it exc
|
|
|
21
21
|
Who you are, and who the others are — read this before writing to anyone:
|
|
22
22
|
- A session is identified by its mailbox address, and by nothing else. \`whoami\` gives you yours.
|
|
23
23
|
- The agent name (e.g. "Ai Corrector Dev") comes from the configuration and says which project the agent works on. It is NOT unique: every session started from that configuration carries it.
|
|
24
|
-
-
|
|
24
|
+
- \`list_agents\` groups mailboxes under their agent name: one group per project, one line per session. A group holding several lines is several live sessions of that project, each with its own address — pick one of those addresses, never the group name.
|
|
25
|
+
- One of those groups may carry the same agent name as yours. Recognising your own agent name there does not mean you are looking at yourself — those are other sessions of the same project, each a different agent with its own mailbox. Compare addresses, never names.
|
|
25
26
|
- Your own mailbox is never listed in \`list_agents\`: every address you see there belongs to someone else.
|
|
26
27
|
|
|
27
28
|
Writing:
|
package/dist/src/mcp/tools.js
CHANGED
|
@@ -7,7 +7,7 @@ const FOLDER_HINT = `Folder name. System folders: ${SYSTEM_FOLDERS.join(", ")}.
|
|
|
7
7
|
export const TOOLS = [
|
|
8
8
|
{
|
|
9
9
|
name: "whoami",
|
|
10
|
-
description: "Return this session's mailbox address, the server it is connected to, and unread counts per folder. The address is what
|
|
10
|
+
description: "Return this session's mailbox address, the server it is connected to, and unread counts per folder. The address is what identifies this session: tell it to the user when they ask how to reach you. Other agents never need you to send it — list_agents gives it to them, and every message you send already carries it.",
|
|
11
11
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
12
12
|
},
|
|
13
13
|
{
|
|
@@ -55,11 +55,15 @@ export const TOOLS = [
|
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
name: "list_threads",
|
|
58
|
-
description: "List the conversations visible from this mailbox, most recently active first, with their name, participants and unread count. This is the readable map of the ongoing work: prefer it over list_messages when you want to know what is being discussed rather than what arrived last. A thread with no name falls back to the subject of its opening message — name it with rename_thread.",
|
|
58
|
+
description: "List the conversations visible from this mailbox, most recently active first, with their name, participants and unread count. This is the readable map of the ongoing work: prefer it over list_messages when you want to know what is being discussed rather than what arrived last. Set with_agent to see everything exchanged with one mailbox. A thread with no name falls back to the subject of its opening message — name it with rename_thread.",
|
|
59
59
|
inputSchema: {
|
|
60
60
|
type: "object",
|
|
61
61
|
properties: {
|
|
62
62
|
unread_only: { type: "boolean", description: "Keep only threads with unread messages." },
|
|
63
|
+
with_agent: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "Mailbox address of a correspondent: keep only the threads shared with that mailbox. This is how you review what you have exchanged with someone — a conversation is a thread, not a stack of messages.",
|
|
66
|
+
},
|
|
63
67
|
search: { type: "string", description: "Free text matched against thread name, description and subject." },
|
|
64
68
|
limit: { type: "number", description: "1-100, defaults to 25." },
|
|
65
69
|
},
|
|
@@ -68,8 +72,17 @@ export const TOOLS = [
|
|
|
68
72
|
},
|
|
69
73
|
{
|
|
70
74
|
name: "list_agents",
|
|
71
|
-
description: "List the other agents known to the mailbox server, most recently active first, with
|
|
72
|
-
inputSchema: {
|
|
75
|
+
description: "List the other agents known to the mailbox server, grouped by agent name — one group per project, one line per session mailbox inside it, most recently active first. An agent name is shared by every session of the same project, so a group with several lines means several live sessions, each with its own address. Your own mailbox is never listed — you cannot write to yourself. Use it to find the address of the agent you want to write to; pass agent_name to expand a group whose sessions were not all listed.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
agent_name: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Name of one agent (project) to expand: lists every one of its session mailboxes in full. Omit it to get the whole directory, grouped.",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
},
|
|
73
86
|
},
|
|
74
87
|
{
|
|
75
88
|
name: "send_message",
|
|
@@ -276,9 +289,11 @@ const schemas = {
|
|
|
276
289
|
}),
|
|
277
290
|
list_threads: z.object({
|
|
278
291
|
unread_only: z.boolean().optional(),
|
|
292
|
+
with_agent: z.string().trim().min(1).optional(),
|
|
279
293
|
search: z.string().trim().min(1).optional(),
|
|
280
294
|
limit: z.number().int().optional(),
|
|
281
295
|
}),
|
|
296
|
+
list_agents: z.object({ agent_name: z.string().trim().min(1).optional() }),
|
|
282
297
|
create_folder: z.object({ name: z.string().min(1) }),
|
|
283
298
|
delete_folder: z.object({ name: z.string().min(1) }),
|
|
284
299
|
wait_for_message: z.object({ timeout_seconds: z.number().int().optional(), folder: z.string().optional() }),
|
|
@@ -299,7 +314,10 @@ export async function callTool(client, name, rawArgs) {
|
|
|
299
314
|
const self = await client.getAgent(client.agentId).catch(() => null);
|
|
300
315
|
const lines = [
|
|
301
316
|
`Mailbox address: ${client.agentId}`,
|
|
302
|
-
|
|
317
|
+
// Personne n'a besoin qu'on la lui envoie : l'annuaire la donne, et chaque message
|
|
318
|
+
// la porte déjà. Le seul destinataire légitime est l'utilisateur, qui le demande.
|
|
319
|
+
" ↳ this identifies this session. Other agents read it from list_agents and from the",
|
|
320
|
+
" messages you send them — tell it to the user if asked, never inside a message body.",
|
|
303
321
|
self?.agent_name
|
|
304
322
|
? `Agent: ${self.agent_name}`
|
|
305
323
|
: "Agent: unnamed (set MESSENGER_AGENT_NAME in the MCP configuration)",
|
|
@@ -369,38 +387,18 @@ export async function callTool(client, name, rawArgs) {
|
|
|
369
387
|
case "list_threads": {
|
|
370
388
|
const input = schemas.list_threads.parse(args);
|
|
371
389
|
const threads = await client.listThreads(input);
|
|
390
|
+
const scope = input.with_agent ? ` with ${input.with_agent}` : "";
|
|
372
391
|
if (threads.length === 0)
|
|
373
|
-
return
|
|
374
|
-
return [`${threads.length} thread(s):`, "", ...threads.map(formatThread)].join("\n");
|
|
392
|
+
return `No thread in this mailbox${scope} matching these filters.`;
|
|
393
|
+
return [`${threads.length} thread(s)${scope}:`, "", ...threads.map(formatThread)].join("\n");
|
|
375
394
|
}
|
|
376
395
|
case "list_agents": {
|
|
396
|
+
const input = schemas.list_agents.parse(args);
|
|
377
397
|
const agents = await client.listAgents();
|
|
378
398
|
if (agents.length === 0)
|
|
379
399
|
return "No other agent on this server yet.";
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
const flags = agent.registered ? "" : "never connected";
|
|
383
|
-
const names = formatAgentNames(agent);
|
|
384
|
-
const title = names ? `${agent.id} — ${names}` : agent.id;
|
|
385
|
-
// L'annuaire est classé par cette date : l'afficher rend l'ordre lisible.
|
|
386
|
-
const seen = agent.last_activity_at ? `last active ${agent.last_activity_at}` : "never active";
|
|
387
|
-
return [
|
|
388
|
-
`• ${title}${flags ? ` (${flags})` : ""}`,
|
|
389
|
-
` ${agent.agent_description ?? agent.description ?? "no description"}`,
|
|
390
|
-
` ${seen}`,
|
|
391
|
-
].join("\n");
|
|
392
|
-
})
|
|
393
|
-
.join("\n");
|
|
394
|
-
// Un nom d'agent est celui d'un projet, pas d'une session : plusieurs boîtes le
|
|
395
|
-
// portent, y compris celle de l'appelant. Sans ce rappel, un agent croise son propre
|
|
396
|
-
// nom d'agent dans cette liste et prend la boîte d'une autre session pour la sienne.
|
|
397
|
-
return [
|
|
398
|
-
directory,
|
|
399
|
-
"",
|
|
400
|
-
"Addresses identify sessions; agent names do not. Several mailboxes above may carry the same",
|
|
401
|
-
"agent name as yours — they are other sessions of the same project, never you: your own mailbox",
|
|
402
|
-
"is never listed here. Always write to an address taken from this list, never to a name.",
|
|
403
|
-
].join("\n");
|
|
400
|
+
const groups = groupByAgentName(agents);
|
|
401
|
+
return input.agent_name ? formatAgentGroup(groups, input.agent_name) : formatDirectory(groups);
|
|
404
402
|
}
|
|
405
403
|
case "send_message": {
|
|
406
404
|
const input = schemas.send_message.parse(args);
|
|
@@ -552,6 +550,111 @@ async function renameMailbox(client, name, description) {
|
|
|
552
550
|
function formatAgentNames(agent) {
|
|
553
551
|
return [agent.agent_name, agent.display_name].filter(Boolean).join(" · ");
|
|
554
552
|
}
|
|
553
|
+
/** Boîtes détaillées sous un nom d'agent avant de renvoyer le reste à `agent_name`. */
|
|
554
|
+
const SESSIONS_SHOWN_PER_AGENT = 4;
|
|
555
|
+
const NO_AGENT_NAME = "unnamed agent";
|
|
556
|
+
/**
|
|
557
|
+
* Deux boîtes sans nom d'agent ne sont pas deux sessions du même projet : rien ne le dit.
|
|
558
|
+
* Chacune reste donc son propre groupe, là où un nom partagé les rassemble.
|
|
559
|
+
*/
|
|
560
|
+
function groupByAgentName(agents) {
|
|
561
|
+
const groups = new Map();
|
|
562
|
+
for (const agent of agents) {
|
|
563
|
+
const name = agent.agent_name?.trim() ?? "";
|
|
564
|
+
const key = name ? name.toLowerCase() : `${agent.id}`;
|
|
565
|
+
const group = groups.get(key);
|
|
566
|
+
if (group) {
|
|
567
|
+
group.mailboxes.push(agent);
|
|
568
|
+
group.description ??= agent.agent_description;
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
groups.set(key, {
|
|
572
|
+
name: name || NO_AGENT_NAME,
|
|
573
|
+
key,
|
|
574
|
+
description: agent.agent_description,
|
|
575
|
+
mailboxes: [agent],
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// L'annuaire arrive classé par activité décroissante : l'ordre d'insertion place en tête
|
|
580
|
+
// l'agent dont une session a bougé le plus récemment, et ses boîtes dans le même ordre.
|
|
581
|
+
return [...groups.values()];
|
|
582
|
+
}
|
|
583
|
+
/** `boîte — nom · activité` : une ligne par session, l'adresse en tête car c'est elle qu'on copie. */
|
|
584
|
+
function formatMailboxLine(agent) {
|
|
585
|
+
const seen = agent.last_activity_at ? `last active ${agent.last_activity_at}` : "never active";
|
|
586
|
+
const flags = agent.registered ? "" : " · never connected";
|
|
587
|
+
return ` • ${agent.id} — ${agent.display_name ?? "no mailbox name"} · ${seen}${flags}`;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Rappel joint à toute réponse de l'annuaire.
|
|
591
|
+
*
|
|
592
|
+
* Un nom d'agent est celui d'un projet, pas d'une session : plusieurs boîtes le portent, y
|
|
593
|
+
* compris celle de l'appelant. Sans ce rappel, un agent croise son propre nom d'agent dans
|
|
594
|
+
* cette liste et prend la boîte d'une autre session pour la sienne. Le groupement rend le
|
|
595
|
+
* nom encore plus visible que l'adresse : la mise en garde compte d'autant plus.
|
|
596
|
+
*/
|
|
597
|
+
const DIRECTORY_FOOTER = [
|
|
598
|
+
"Addresses identify sessions; agent names do not. Several mailboxes above may carry the same",
|
|
599
|
+
"agent name as yours — they are other sessions of the same project, never you: your own mailbox",
|
|
600
|
+
"is never listed here. Always write to an address taken from this list, never to a name.",
|
|
601
|
+
"To see what you have already exchanged with one of them, call list_threads with its address in with_agent.",
|
|
602
|
+
];
|
|
603
|
+
function formatDirectory(groups) {
|
|
604
|
+
const mailboxes = groups.reduce((total, group) => total + group.mailboxes.length, 0);
|
|
605
|
+
const blocks = groups.map((group) => {
|
|
606
|
+
const sessions = group.mailboxes.length;
|
|
607
|
+
const lines = [`${group.name} — ${sessions} session(s)`];
|
|
608
|
+
if (group.description)
|
|
609
|
+
lines.push(` ${group.description}`);
|
|
610
|
+
// Un agent à sessions nombreuses ne doit pas noyer les autres : les plus récentes
|
|
611
|
+
// suffisent à écrire, `agent_name` donne le reste à qui en a besoin.
|
|
612
|
+
const shown = sessions > SESSIONS_SHOWN_PER_AGENT ? group.mailboxes.slice(0, 3) : group.mailboxes;
|
|
613
|
+
lines.push(...shown.map(formatMailboxLine));
|
|
614
|
+
if (shown.length < sessions) {
|
|
615
|
+
lines.push(` … ${sessions - shown.length} more session(s) — list_agents with agent_name: "${group.name}" lists them all.`);
|
|
616
|
+
}
|
|
617
|
+
return lines.join("\n");
|
|
618
|
+
});
|
|
619
|
+
// Un agent par bloc, séparés par une ligne vide : le groupe est l'unité de lecture.
|
|
620
|
+
return [
|
|
621
|
+
`${groups.length} agent(s), ${mailboxes} mailbox(es):`,
|
|
622
|
+
"",
|
|
623
|
+
blocks.join("\n\n"),
|
|
624
|
+
"",
|
|
625
|
+
...DIRECTORY_FOOTER,
|
|
626
|
+
].join("\n");
|
|
627
|
+
}
|
|
628
|
+
/** Toutes les sessions d'un agent, décrites une par une : le second niveau de l'annuaire. */
|
|
629
|
+
function formatAgentGroup(groups, requested) {
|
|
630
|
+
const wanted = requested.trim().toLowerCase();
|
|
631
|
+
const group = groups.find((g) => g.name.toLowerCase() === wanted);
|
|
632
|
+
if (!group) {
|
|
633
|
+
const known = groups.map((g) => g.name).join(", ");
|
|
634
|
+
throw new Error(`No agent named "${requested}" on this server. Known agent names: ${known}`);
|
|
635
|
+
}
|
|
636
|
+
const blocks = group.mailboxes.map((agent) => {
|
|
637
|
+
const seen = agent.last_activity_at ? `last active ${agent.last_activity_at}` : "never active";
|
|
638
|
+
return [
|
|
639
|
+
`• ${agent.id}`,
|
|
640
|
+
` Mailbox: ${agent.display_name ?? "not named — this session never said what it works on"}`,
|
|
641
|
+
agent.description ? ` ${agent.description}` : null,
|
|
642
|
+
` ${seen}${agent.registered ? "" : " · never connected"}`,
|
|
643
|
+
]
|
|
644
|
+
.filter(Boolean)
|
|
645
|
+
.join("\n");
|
|
646
|
+
});
|
|
647
|
+
return [
|
|
648
|
+
`${group.name} — ${group.mailboxes.length} session(s), most recently active first:`,
|
|
649
|
+
group.description ?? null,
|
|
650
|
+
"",
|
|
651
|
+
...blocks,
|
|
652
|
+
"",
|
|
653
|
+
...DIRECTORY_FOOTER,
|
|
654
|
+
]
|
|
655
|
+
.filter((line) => line !== null)
|
|
656
|
+
.join("\n");
|
|
657
|
+
}
|
|
555
658
|
async function renameThread(client, threadId, name, description) {
|
|
556
659
|
const thread = await client.setThreadLabel(threadId, { title: name, description });
|
|
557
660
|
return [
|
package/dist/src/server/app.js
CHANGED
|
@@ -45,6 +45,7 @@ const threadLabelSchema = z
|
|
|
45
45
|
});
|
|
46
46
|
const listThreadsQuerySchema = z.object({
|
|
47
47
|
unread_only: z.coerce.boolean().optional(),
|
|
48
|
+
with_agent: z.string().trim().min(1).optional(),
|
|
48
49
|
search: z.string().trim().min(1).optional(),
|
|
49
50
|
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
50
51
|
offset: z.coerce.number().int().min(0).optional(),
|
|
@@ -163,10 +164,10 @@ export function createApp({ store, config }) {
|
|
|
163
164
|
});
|
|
164
165
|
admin.get("/threads/:id", (req, res) => {
|
|
165
166
|
const threadId = String(req.params.id);
|
|
166
|
-
const messages = store.
|
|
167
|
+
const messages = store.listThreadAdmin(threadId);
|
|
167
168
|
if (messages.length === 0)
|
|
168
169
|
throw new MessengerError(`thread not found: ${threadId}`, 404);
|
|
169
|
-
res.json({ thread_id: threadId, thread: store.getThreadInfo(threadId), messages
|
|
170
|
+
res.json({ thread_id: threadId, thread: store.getThreadInfo(threadId), messages });
|
|
170
171
|
});
|
|
171
172
|
app.use("/admin", admin);
|
|
172
173
|
// Porte d'entrée de tout le reste : aucune requête n'est servie sans jeton valide. Le
|
|
@@ -12,6 +12,9 @@ const state = {
|
|
|
12
12
|
selectedMessageId: null,
|
|
13
13
|
threadId: null,
|
|
14
14
|
threadTitle: null,
|
|
15
|
+
// Historique déplié ou non sous le message ouvert. En mémoire de page, comme le reste :
|
|
16
|
+
// rien n'est écrit dans le navigateur, mais le choix tient d'un message à l'autre.
|
|
17
|
+
historyExpanded: false,
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
const el = {
|
|
@@ -147,14 +150,59 @@ async function openMessage(messageId) {
|
|
|
147
150
|
state.selectedMessageId = messageId;
|
|
148
151
|
try {
|
|
149
152
|
const message = await api("/admin/messages/" + encodeURIComponent(messageId));
|
|
150
|
-
|
|
153
|
+
// Le fil est chargé avec le message : déplier l'historique ne coûte pas d'aller-retour.
|
|
154
|
+
// Son échec n'empêche pas de lire le message, il ne fait que priver de l'historique.
|
|
155
|
+
const thread = await api("/admin/threads/" + encodeURIComponent(message.thread_id)).catch(() => null);
|
|
156
|
+
renderDetail(message, thread);
|
|
151
157
|
await refresh({ keepDetail: true });
|
|
152
158
|
} catch (error) {
|
|
153
159
|
setDetail(node("div", "error", error.message), false);
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Les messages du fil antérieurs à celui-ci, du plus récent au plus ancien.
|
|
165
|
+
*
|
|
166
|
+
* Un message se lit comme un mail : ce qui l'a précédé l'explique, ce qui l'a suivi appartient
|
|
167
|
+
* à la suite du fil et se lit dans la vue du fil complet.
|
|
168
|
+
*/
|
|
169
|
+
function earlierMessages(message, thread) {
|
|
170
|
+
const messages = (thread && thread.messages) || [];
|
|
171
|
+
const index = messages.findIndex((m) => m.message_id === message.message_id);
|
|
172
|
+
const earlier = index === -1 ? messages.filter((m) => m.created_at < message.created_at) : messages.slice(0, index);
|
|
173
|
+
return earlier.slice().reverse();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Bouton d'ouverture et bloc cité : replié par défaut, comme le fait une boîte mail. */
|
|
177
|
+
function renderHistory(container, earlier) {
|
|
178
|
+
if (earlier.length === 0) return;
|
|
179
|
+
const label = () =>
|
|
180
|
+
(state.historyExpanded ? "Masquer" : "Afficher") + " les " + earlier.length + " message(s) précédent(s)";
|
|
181
|
+
const toggle = node("button", "history-toggle", label());
|
|
182
|
+
const history = node("div", "history");
|
|
183
|
+
history.hidden = !state.historyExpanded;
|
|
184
|
+
|
|
185
|
+
for (const previous of earlier) {
|
|
186
|
+
const block = node("div", "quoted");
|
|
187
|
+
block.appendChild(
|
|
188
|
+
node("div", "quoted-head", previous.from + " → " + (previous.to || "—") + " · " + formatDate(previous.created_at))
|
|
189
|
+
);
|
|
190
|
+
block.appendChild(node("div", "quoted-subject", previous.subject));
|
|
191
|
+
// `body` vient du fil complet ; l'aperçu ne sert que si une réponse ancienne n'en a pas.
|
|
192
|
+
block.appendChild(node("div", "quoted-body", previous.body || previous.preview || ""));
|
|
193
|
+
history.appendChild(block);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
toggle.addEventListener("click", () => {
|
|
197
|
+
state.historyExpanded = !state.historyExpanded;
|
|
198
|
+
history.hidden = !state.historyExpanded;
|
|
199
|
+
toggle.textContent = label();
|
|
200
|
+
});
|
|
201
|
+
container.appendChild(toggle);
|
|
202
|
+
container.appendChild(history);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function renderDetail(message, thread) {
|
|
158
206
|
const container = node("div", "detail");
|
|
159
207
|
container.appendChild(node("h3", null, message.subject));
|
|
160
208
|
container.appendChild(
|
|
@@ -176,12 +224,16 @@ function renderDetail(message) {
|
|
|
176
224
|
)
|
|
177
225
|
);
|
|
178
226
|
|
|
179
|
-
|
|
227
|
+
container.appendChild(node("div", "body", message.body));
|
|
228
|
+
|
|
229
|
+
// Sous le corps, comme dans une boîte mail : d'abord ce qui a mené à ce message, replié ;
|
|
230
|
+
// la vue chronologique du fil entier reste une action à part, elle ne se déplie pas ici.
|
|
231
|
+
renderHistory(container, earlierMessages(message, thread));
|
|
232
|
+
|
|
233
|
+
const threadButton = node("button", "thread-link", "Voir le fil complet");
|
|
180
234
|
threadButton.addEventListener("click", () => showThread(message.thread_id));
|
|
181
235
|
container.appendChild(threadButton);
|
|
182
236
|
|
|
183
|
-
container.appendChild(node("div", "body", message.body));
|
|
184
|
-
|
|
185
237
|
const deliveries = node("div", "deliveries");
|
|
186
238
|
deliveries.appendChild(node("h2", null, "Distribution"));
|
|
187
239
|
const list = node("ul");
|
|
@@ -304,6 +304,46 @@ ul {
|
|
|
304
304
|
padding-top: 12px;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/* Historique du fil sous le message : discret replié, cité déplié. */
|
|
308
|
+
.detail .history-toggle {
|
|
309
|
+
margin-top: 14px;
|
|
310
|
+
color: var(--accent);
|
|
311
|
+
border-style: dashed;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.detail .history {
|
|
315
|
+
margin-top: 10px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* Bordure d'accent à gauche : la convention du texte cité dans un client mail. */
|
|
319
|
+
.detail .quoted {
|
|
320
|
+
border-left: 2px solid var(--border);
|
|
321
|
+
padding: 4px 0 4px 12px;
|
|
322
|
+
margin-top: 12px;
|
|
323
|
+
color: var(--muted);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.detail .quoted-head {
|
|
327
|
+
font-size: 12px;
|
|
328
|
+
word-break: break-all;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.detail .quoted-subject {
|
|
332
|
+
font-weight: 600;
|
|
333
|
+
color: var(--text);
|
|
334
|
+
margin-top: 2px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.detail .quoted-body {
|
|
338
|
+
white-space: pre-wrap;
|
|
339
|
+
word-wrap: break-word;
|
|
340
|
+
margin-top: 6px;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.detail .thread-link {
|
|
344
|
+
margin-top: 14px;
|
|
345
|
+
}
|
|
346
|
+
|
|
307
347
|
.detail .deliveries {
|
|
308
348
|
margin-top: 16px;
|
|
309
349
|
border-top: 1px solid var(--border);
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const APP_VERSION = "1.0.
|
|
1
|
+
export declare const APP_VERSION = "1.0.10";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Généré par scripts/generate-version.js — ne pas modifier à la main.
|
|
2
|
-
export const APP_VERSION = "1.0.
|
|
2
|
+
export const APP_VERSION = "1.0.10";
|