@imenam/mcp-messenger 1.0.10 → 1.0.11
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 +35 -10
- package/dist/src/core/store.d.ts +16 -1
- package/dist/src/core/store.js +242 -152
- package/dist/src/core/types.d.ts +22 -0
- package/dist/src/mcp/start.js +5 -1
- package/dist/src/mcp/tools.js +29 -16
- package/dist/src/server/app.js +22 -0
- package/dist/src/server/ui/app.js +703 -372
- package/dist/src/server/ui/index.html +92 -27
- package/dist/src/server/ui/styles.css +1147 -368
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/dist/src/core/store.js
CHANGED
|
@@ -23,12 +23,26 @@ const THREAD_DESCRIPTION_MAX = 2000;
|
|
|
23
23
|
* touchée. Une boîte qui ne fait que recevoir reste vivante, alors que `last_seen_at`
|
|
24
24
|
* seul la ferait paraître abandonnée.
|
|
25
25
|
*/
|
|
26
|
-
const LAST_ACTIVITY = `MAX(
|
|
27
|
-
COALESCE(a.last_seen_at, ''),
|
|
28
|
-
COALESCE((SELECT MAX(d.created_at) FROM deliveries d WHERE d.agent_id = a.id), '')
|
|
26
|
+
const LAST_ACTIVITY = `MAX(
|
|
27
|
+
COALESCE(a.last_seen_at, ''),
|
|
28
|
+
COALESCE((SELECT MAX(d.created_at) FROM deliveries d WHERE d.agent_id = a.id), '')
|
|
29
29
|
)`;
|
|
30
30
|
/** Colonnes du fil jointes à tout message : le titre voyage avec chaque livraison. */
|
|
31
31
|
const THREAD_JOIN = "LEFT JOIN threads t ON t.id = m.thread_id";
|
|
32
|
+
/**
|
|
33
|
+
* Colonnes de livraison pour les vues de supervision.
|
|
34
|
+
*
|
|
35
|
+
* La console ne lit aucune boîte en particulier : elle n'a donc pas de copie « à elle »
|
|
36
|
+
* d'où tirer l'état du message. C'est la copie du destinataire qui fait foi — « ce message
|
|
37
|
+
* a-t-il été lu par celui à qui il était adressé ? » est la seule question que la console
|
|
38
|
+
* pose. Un message n'ayant qu'une copie d'expéditeur (destinataire jamais enregistré) n'a
|
|
39
|
+
* pas de ligne ici : `read_at` reste alors nul, donc non lu.
|
|
40
|
+
*/
|
|
41
|
+
const ADMIN_DELIVERY = `'to' AS role,
|
|
42
|
+
(SELECT dr.read_at FROM deliveries dr WHERE dr.message_id = m.id AND dr.role != 'sender'
|
|
43
|
+
ORDER BY dr.agent_id LIMIT 1) AS read_at,
|
|
44
|
+
COALESCE((SELECT dr.folder FROM deliveries dr WHERE dr.message_id = m.id AND dr.role != 'sender'
|
|
45
|
+
ORDER BY dr.agent_id LIMIT 1), 'inbox') AS folder`;
|
|
32
46
|
/**
|
|
33
47
|
* Boîte aux lettres persistée en SQLite.
|
|
34
48
|
*
|
|
@@ -50,84 +64,84 @@ export class MessageStore extends EventEmitter {
|
|
|
50
64
|
this.migrate();
|
|
51
65
|
}
|
|
52
66
|
migrate() {
|
|
53
|
-
this.db.exec(`
|
|
54
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
55
|
-
id TEXT PRIMARY KEY,
|
|
56
|
-
agent_name TEXT,
|
|
57
|
-
agent_description TEXT,
|
|
58
|
-
display_name TEXT,
|
|
59
|
-
description TEXT,
|
|
60
|
-
created_at TEXT NOT NULL,
|
|
61
|
-
last_seen_at TEXT,
|
|
62
|
-
registered INTEGER NOT NULL DEFAULT 0
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
66
|
-
id TEXT PRIMARY KEY,
|
|
67
|
-
thread_id TEXT NOT NULL,
|
|
68
|
-
sender_id TEXT NOT NULL,
|
|
69
|
-
subject TEXT NOT NULL,
|
|
70
|
-
body TEXT NOT NULL,
|
|
71
|
-
priority TEXT NOT NULL DEFAULT 'normal',
|
|
72
|
-
in_reply_to TEXT,
|
|
73
|
-
created_at TEXT NOT NULL
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
CREATE TABLE IF NOT EXISTS deliveries (
|
|
77
|
-
id TEXT PRIMARY KEY,
|
|
78
|
-
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
79
|
-
agent_id TEXT NOT NULL,
|
|
80
|
-
role TEXT NOT NULL,
|
|
81
|
-
folder TEXT NOT NULL,
|
|
82
|
-
read_at TEXT,
|
|
83
|
-
starred INTEGER NOT NULL DEFAULT 0,
|
|
84
|
-
created_at TEXT NOT NULL,
|
|
85
|
-
UNIQUE (message_id, agent_id, role)
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
CREATE TABLE IF NOT EXISTS threads (
|
|
89
|
-
id TEXT PRIMARY KEY,
|
|
90
|
-
title TEXT,
|
|
91
|
-
description TEXT,
|
|
92
|
-
named_by TEXT,
|
|
93
|
-
created_at TEXT NOT NULL,
|
|
94
|
-
updated_at TEXT
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
CREATE TABLE IF NOT EXISTS folders (
|
|
98
|
-
agent_id TEXT NOT NULL,
|
|
99
|
-
name TEXT NOT NULL,
|
|
100
|
-
created_at TEXT NOT NULL,
|
|
101
|
-
PRIMARY KEY (agent_id, name)
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
CREATE INDEX IF NOT EXISTS idx_deliveries_mailbox ON deliveries (agent_id, folder, created_at DESC);
|
|
105
|
-
CREATE INDEX IF NOT EXISTS idx_deliveries_message ON deliveries (message_id);
|
|
106
|
-
CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages (thread_id, created_at);
|
|
67
|
+
this.db.exec(`
|
|
68
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
69
|
+
id TEXT PRIMARY KEY,
|
|
70
|
+
agent_name TEXT,
|
|
71
|
+
agent_description TEXT,
|
|
72
|
+
display_name TEXT,
|
|
73
|
+
description TEXT,
|
|
74
|
+
created_at TEXT NOT NULL,
|
|
75
|
+
last_seen_at TEXT,
|
|
76
|
+
registered INTEGER NOT NULL DEFAULT 0
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
80
|
+
id TEXT PRIMARY KEY,
|
|
81
|
+
thread_id TEXT NOT NULL,
|
|
82
|
+
sender_id TEXT NOT NULL,
|
|
83
|
+
subject TEXT NOT NULL,
|
|
84
|
+
body TEXT NOT NULL,
|
|
85
|
+
priority TEXT NOT NULL DEFAULT 'normal',
|
|
86
|
+
in_reply_to TEXT,
|
|
87
|
+
created_at TEXT NOT NULL
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
CREATE TABLE IF NOT EXISTS deliveries (
|
|
91
|
+
id TEXT PRIMARY KEY,
|
|
92
|
+
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
93
|
+
agent_id TEXT NOT NULL,
|
|
94
|
+
role TEXT NOT NULL,
|
|
95
|
+
folder TEXT NOT NULL,
|
|
96
|
+
read_at TEXT,
|
|
97
|
+
starred INTEGER NOT NULL DEFAULT 0,
|
|
98
|
+
created_at TEXT NOT NULL,
|
|
99
|
+
UNIQUE (message_id, agent_id, role)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
103
|
+
id TEXT PRIMARY KEY,
|
|
104
|
+
title TEXT,
|
|
105
|
+
description TEXT,
|
|
106
|
+
named_by TEXT,
|
|
107
|
+
created_at TEXT NOT NULL,
|
|
108
|
+
updated_at TEXT
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
CREATE TABLE IF NOT EXISTS folders (
|
|
112
|
+
agent_id TEXT NOT NULL,
|
|
113
|
+
name TEXT NOT NULL,
|
|
114
|
+
created_at TEXT NOT NULL,
|
|
115
|
+
PRIMARY KEY (agent_id, name)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_deliveries_mailbox ON deliveries (agent_id, folder, created_at DESC);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_deliveries_message ON deliveries (message_id);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages (thread_id, created_at);
|
|
107
121
|
`);
|
|
108
122
|
// Bases antérieures à la séparation agent / boîte : `display_name` y tenait les deux
|
|
109
123
|
// rôles, et venait presque toujours de la configuration (le nom du projet). On le
|
|
110
124
|
// déplace vers l'identité de l'agent ; la boîte repart sans nom, à poser par l'agent.
|
|
111
125
|
const agentColumns = this.db.prepare("PRAGMA table_info(agents)").all().map((c) => c.name);
|
|
112
126
|
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;
|
|
127
|
+
this.db.exec(`
|
|
128
|
+
ALTER TABLE agents ADD COLUMN agent_name TEXT;
|
|
129
|
+
ALTER TABLE agents ADD COLUMN agent_description TEXT;
|
|
130
|
+
UPDATE agents
|
|
131
|
+
SET agent_name = display_name,
|
|
132
|
+
agent_description = description,
|
|
133
|
+
display_name = NULL,
|
|
134
|
+
description = NULL;
|
|
121
135
|
`);
|
|
122
136
|
}
|
|
123
137
|
// Bases antérieures aux fils nommés : chaque fil déjà présent reçoit sa ligne, sans
|
|
124
138
|
// titre. L'affichage retombe alors sur le sujet d'ouverture, comme avant.
|
|
125
|
-
this.db.exec(`
|
|
126
|
-
INSERT INTO threads (id, title, description, named_by, created_at, updated_at)
|
|
127
|
-
SELECT m.thread_id, NULL, NULL, NULL, MIN(m.created_at), NULL
|
|
128
|
-
FROM messages m
|
|
129
|
-
WHERE NOT EXISTS (SELECT 1 FROM threads t WHERE t.id = m.thread_id)
|
|
130
|
-
GROUP BY m.thread_id;
|
|
139
|
+
this.db.exec(`
|
|
140
|
+
INSERT INTO threads (id, title, description, named_by, created_at, updated_at)
|
|
141
|
+
SELECT m.thread_id, NULL, NULL, NULL, MIN(m.created_at), NULL
|
|
142
|
+
FROM messages m
|
|
143
|
+
WHERE NOT EXISTS (SELECT 1 FROM threads t WHERE t.id = m.thread_id)
|
|
144
|
+
GROUP BY m.thread_id;
|
|
131
145
|
`);
|
|
132
146
|
}
|
|
133
147
|
close() {
|
|
@@ -144,19 +158,19 @@ export class MessageStore extends EventEmitter {
|
|
|
144
158
|
const existing = this.db.prepare("SELECT id FROM agents WHERE id = ?").get(id);
|
|
145
159
|
if (!existing) {
|
|
146
160
|
this.db
|
|
147
|
-
.prepare(`INSERT INTO agents (id, agent_name, agent_description, display_name, description, created_at, last_seen_at, registered)
|
|
161
|
+
.prepare(`INSERT INTO agents (id, agent_name, agent_description, display_name, description, created_at, last_seen_at, registered)
|
|
148
162
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
149
163
|
.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);
|
|
150
164
|
}
|
|
151
165
|
else {
|
|
152
166
|
this.db
|
|
153
|
-
.prepare(`UPDATE agents
|
|
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
|
|
167
|
+
.prepare(`UPDATE agents
|
|
168
|
+
SET agent_name = COALESCE(?, agent_name),
|
|
169
|
+
agent_description = COALESCE(?, agent_description),
|
|
170
|
+
display_name = COALESCE(?, display_name),
|
|
171
|
+
description = COALESCE(?, description),
|
|
172
|
+
registered = MAX(registered, ?),
|
|
173
|
+
last_seen_at = CASE WHEN ? = 1 THEN ? ELSE last_seen_at END
|
|
160
174
|
WHERE id = ?`)
|
|
161
175
|
.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);
|
|
162
176
|
}
|
|
@@ -178,9 +192,9 @@ export class MessageStore extends EventEmitter {
|
|
|
178
192
|
listAgents(exclude) {
|
|
179
193
|
const excluded = exclude ? normalizeAgentId(exclude) : "";
|
|
180
194
|
const rows = this.db
|
|
181
|
-
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at
|
|
182
|
-
FROM agents a
|
|
183
|
-
WHERE a.id != ?
|
|
195
|
+
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at
|
|
196
|
+
FROM agents a
|
|
197
|
+
WHERE a.id != ?
|
|
184
198
|
ORDER BY last_activity_at DESC, a.id`)
|
|
185
199
|
.all(excluded);
|
|
186
200
|
return rows.map((r) => ({
|
|
@@ -237,18 +251,18 @@ export class MessageStore extends EventEmitter {
|
|
|
237
251
|
// Le fil naît avec son premier message ; sur une réponse, la ligne existe déjà et
|
|
238
252
|
// n'est touchée que si l'expéditeur fournit un nouveau titre.
|
|
239
253
|
this.db
|
|
240
|
-
.prepare(`INSERT INTO threads (id, title, description, named_by, created_at, updated_at)
|
|
241
|
-
VALUES (?, NULL, NULL, NULL, ?, NULL)
|
|
254
|
+
.prepare(`INSERT INTO threads (id, title, description, named_by, created_at, updated_at)
|
|
255
|
+
VALUES (?, NULL, NULL, NULL, ?, NULL)
|
|
242
256
|
ON CONFLICT (id) DO NOTHING`)
|
|
243
257
|
.run(threadId, now);
|
|
244
258
|
if (title !== null || threadDescription !== null) {
|
|
245
259
|
this.renameThreadRow(threadId, from, title, threadDescription, now);
|
|
246
260
|
}
|
|
247
261
|
this.db
|
|
248
|
-
.prepare(`INSERT INTO messages (id, thread_id, sender_id, subject, body, priority, in_reply_to, created_at)
|
|
262
|
+
.prepare(`INSERT INTO messages (id, thread_id, sender_id, subject, body, priority, in_reply_to, created_at)
|
|
249
263
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
250
264
|
.run(messageId, threadId, from, subject, input.body, priority, inReplyTo, now);
|
|
251
|
-
const insert = this.db.prepare(`INSERT INTO deliveries (id, message_id, agent_id, role, folder, read_at, starred, created_at)
|
|
265
|
+
const insert = this.db.prepare(`INSERT INTO deliveries (id, message_id, agent_id, role, folder, read_at, starred, created_at)
|
|
252
266
|
VALUES (?, ?, ?, ?, ?, ?, 0, ?)`);
|
|
253
267
|
for (const d of deliveries) {
|
|
254
268
|
insert.run(newId("dlv"), messageId, d.agent_id, d.role, d.folder, d.read_at, now);
|
|
@@ -293,13 +307,13 @@ export class MessageStore extends EventEmitter {
|
|
|
293
307
|
params.push(like, like);
|
|
294
308
|
}
|
|
295
309
|
const rows = this.db
|
|
296
|
-
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
297
|
-
m.priority, m.in_reply_to, m.created_at, d.folder, d.role, d.read_at, d.starred
|
|
298
|
-
FROM deliveries d
|
|
299
|
-
JOIN messages m ON m.id = d.message_id
|
|
300
|
-
${THREAD_JOIN}
|
|
301
|
-
WHERE ${where.join(" AND ")}
|
|
302
|
-
ORDER BY m.created_at DESC, m.id DESC
|
|
310
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
311
|
+
m.priority, m.in_reply_to, m.created_at, d.folder, d.role, d.read_at, d.starred
|
|
312
|
+
FROM deliveries d
|
|
313
|
+
JOIN messages m ON m.id = d.message_id
|
|
314
|
+
${THREAD_JOIN}
|
|
315
|
+
WHERE ${where.join(" AND ")}
|
|
316
|
+
ORDER BY m.created_at DESC, m.id DESC
|
|
303
317
|
LIMIT ? OFFSET ?`)
|
|
304
318
|
.all(...params, limit, offset);
|
|
305
319
|
const recipients = this.recipientsFor(rows.map((r) => r.message_id));
|
|
@@ -307,13 +321,13 @@ export class MessageStore extends EventEmitter {
|
|
|
307
321
|
}
|
|
308
322
|
getMessage(agentId, messageId, markRead = false) {
|
|
309
323
|
const row = this.db
|
|
310
|
-
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
311
|
-
m.priority, m.in_reply_to, m.created_at, d.folder, d.role, d.read_at, d.starred
|
|
312
|
-
FROM deliveries d
|
|
313
|
-
JOIN messages m ON m.id = d.message_id
|
|
314
|
-
${THREAD_JOIN}
|
|
315
|
-
WHERE d.agent_id = ? AND d.message_id = ?
|
|
316
|
-
ORDER BY CASE d.role WHEN 'to' THEN 0 ELSE 1 END
|
|
324
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
325
|
+
m.priority, m.in_reply_to, m.created_at, d.folder, d.role, d.read_at, d.starred
|
|
326
|
+
FROM deliveries d
|
|
327
|
+
JOIN messages m ON m.id = d.message_id
|
|
328
|
+
${THREAD_JOIN}
|
|
329
|
+
WHERE d.agent_id = ? AND d.message_id = ?
|
|
330
|
+
ORDER BY CASE d.role WHEN 'to' THEN 0 ELSE 1 END
|
|
317
331
|
LIMIT 1`)
|
|
318
332
|
.get(agentId, messageId);
|
|
319
333
|
if (!row)
|
|
@@ -331,14 +345,14 @@ export class MessageStore extends EventEmitter {
|
|
|
331
345
|
/** Tous les messages d'un fil visibles par l'agent, du plus ancien au plus récent. */
|
|
332
346
|
getThread(agentId, threadId) {
|
|
333
347
|
const rows = this.db
|
|
334
|
-
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
335
|
-
m.priority, m.in_reply_to, m.created_at,
|
|
336
|
-
MIN(d.folder) AS folder, MIN(d.role) AS role, MIN(d.read_at) AS read_at, MAX(d.starred) AS starred
|
|
337
|
-
FROM deliveries d
|
|
338
|
-
JOIN messages m ON m.id = d.message_id
|
|
339
|
-
${THREAD_JOIN}
|
|
340
|
-
WHERE d.agent_id = ? AND m.thread_id = ?
|
|
341
|
-
GROUP BY m.id
|
|
348
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
349
|
+
m.priority, m.in_reply_to, m.created_at,
|
|
350
|
+
MIN(d.folder) AS folder, MIN(d.role) AS role, MIN(d.read_at) AS read_at, MAX(d.starred) AS starred
|
|
351
|
+
FROM deliveries d
|
|
352
|
+
JOIN messages m ON m.id = d.message_id
|
|
353
|
+
${THREAD_JOIN}
|
|
354
|
+
WHERE d.agent_id = ? AND m.thread_id = ?
|
|
355
|
+
GROUP BY m.id
|
|
342
356
|
ORDER BY m.created_at ASC`)
|
|
343
357
|
.all(agentId, threadId);
|
|
344
358
|
if (rows.length === 0)
|
|
@@ -384,7 +398,7 @@ export class MessageStore extends EventEmitter {
|
|
|
384
398
|
// qu'elle ait écrit ou seulement reçu. Les paramètres du WHERE se lient avant ceux du
|
|
385
399
|
// HAVING : cette condition doit donc être poussée avant celles de la recherche.
|
|
386
400
|
if (opts.with_agent) {
|
|
387
|
-
where.push(`EXISTS (SELECT 1 FROM deliveries d2 JOIN messages m2 ON m2.id = d2.message_id
|
|
401
|
+
where.push(`EXISTS (SELECT 1 FROM deliveries d2 JOIN messages m2 ON m2.id = d2.message_id
|
|
388
402
|
WHERE m2.thread_id = m.thread_id AND d2.agent_id = ?)`);
|
|
389
403
|
params.push(normalizeAgentId(opts.with_agent));
|
|
390
404
|
}
|
|
@@ -397,20 +411,20 @@ export class MessageStore extends EventEmitter {
|
|
|
397
411
|
params.push(like, like, like);
|
|
398
412
|
}
|
|
399
413
|
const rows = this.db
|
|
400
|
-
.prepare(`SELECT m.thread_id,
|
|
401
|
-
t.title, t.description, t.named_by, t.created_at, t.updated_at,
|
|
402
|
-
(SELECT r.subject FROM messages r WHERE r.thread_id = m.thread_id
|
|
403
|
-
ORDER BY r.created_at ASC, r.id ASC LIMIT 1) AS subject,
|
|
404
|
-
COUNT(DISTINCT m.id) AS messages,
|
|
405
|
-
COUNT(DISTINCT CASE WHEN d.read_at IS NULL AND d.role != 'sender' THEN m.id END) AS unread,
|
|
406
|
-
MAX(m.created_at) AS last_message_at
|
|
407
|
-
FROM deliveries d
|
|
408
|
-
JOIN messages m ON m.id = d.message_id
|
|
409
|
-
${THREAD_JOIN}
|
|
410
|
-
WHERE ${where.join(" AND ")}
|
|
411
|
-
GROUP BY m.thread_id
|
|
412
|
-
${having.length ? `HAVING ${having.join(" AND ")}` : ""}
|
|
413
|
-
ORDER BY last_message_at DESC
|
|
414
|
+
.prepare(`SELECT m.thread_id,
|
|
415
|
+
t.title, t.description, t.named_by, t.created_at, t.updated_at,
|
|
416
|
+
(SELECT r.subject FROM messages r WHERE r.thread_id = m.thread_id
|
|
417
|
+
ORDER BY r.created_at ASC, r.id ASC LIMIT 1) AS subject,
|
|
418
|
+
COUNT(DISTINCT m.id) AS messages,
|
|
419
|
+
COUNT(DISTINCT CASE WHEN d.read_at IS NULL AND d.role != 'sender' THEN m.id END) AS unread,
|
|
420
|
+
MAX(m.created_at) AS last_message_at
|
|
421
|
+
FROM deliveries d
|
|
422
|
+
JOIN messages m ON m.id = d.message_id
|
|
423
|
+
${THREAD_JOIN}
|
|
424
|
+
WHERE ${where.join(" AND ")}
|
|
425
|
+
GROUP BY m.thread_id
|
|
426
|
+
${having.length ? `HAVING ${having.join(" AND ")}` : ""}
|
|
427
|
+
ORDER BY last_message_at DESC
|
|
414
428
|
LIMIT ? OFFSET ?`)
|
|
415
429
|
.all(...params, limit, offset);
|
|
416
430
|
const participants = this.participantsFor(rows.map((r) => r.thread_id));
|
|
@@ -433,18 +447,18 @@ export class MessageStore extends EventEmitter {
|
|
|
433
447
|
/** Écriture brute du nom : un champ absent (`null`) laisse l'ancienne valeur en place. */
|
|
434
448
|
renameThreadRow(threadId, agentId, title, description, now) {
|
|
435
449
|
this.db
|
|
436
|
-
.prepare(`INSERT INTO threads (id, title, description, named_by, created_at, updated_at)
|
|
437
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
438
|
-
ON CONFLICT (id) DO UPDATE SET
|
|
439
|
-
title = COALESCE(excluded.title, threads.title),
|
|
440
|
-
description = COALESCE(excluded.description, threads.description),
|
|
441
|
-
named_by = excluded.named_by,
|
|
450
|
+
.prepare(`INSERT INTO threads (id, title, description, named_by, created_at, updated_at)
|
|
451
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
452
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
453
|
+
title = COALESCE(excluded.title, threads.title),
|
|
454
|
+
description = COALESCE(excluded.description, threads.description),
|
|
455
|
+
named_by = excluded.named_by,
|
|
442
456
|
updated_at = excluded.updated_at`)
|
|
443
457
|
.run(threadId, title, description, agentId, now, now);
|
|
444
458
|
}
|
|
445
459
|
assertThreadVisible(agentId, threadId) {
|
|
446
460
|
const row = this.db
|
|
447
|
-
.prepare(`SELECT 1 FROM deliveries d JOIN messages m ON m.id = d.message_id
|
|
461
|
+
.prepare(`SELECT 1 FROM deliveries d JOIN messages m ON m.id = d.message_id
|
|
448
462
|
WHERE d.agent_id = ? AND m.thread_id = ? LIMIT 1`)
|
|
449
463
|
.get(agentId, threadId);
|
|
450
464
|
if (!row)
|
|
@@ -457,9 +471,9 @@ export class MessageStore extends EventEmitter {
|
|
|
457
471
|
return map;
|
|
458
472
|
const placeholders = ids.map(() => "?").join(",");
|
|
459
473
|
const rows = this.db
|
|
460
|
-
.prepare(`SELECT DISTINCT m.thread_id, d.agent_id
|
|
461
|
-
FROM deliveries d JOIN messages m ON m.id = d.message_id
|
|
462
|
-
WHERE m.thread_id IN (${placeholders})
|
|
474
|
+
.prepare(`SELECT DISTINCT m.thread_id, d.agent_id
|
|
475
|
+
FROM deliveries d JOIN messages m ON m.id = d.message_id
|
|
476
|
+
WHERE m.thread_id IN (${placeholders})
|
|
463
477
|
ORDER BY d.agent_id`)
|
|
464
478
|
.all(...ids);
|
|
465
479
|
for (const row of rows) {
|
|
@@ -505,7 +519,7 @@ export class MessageStore extends EventEmitter {
|
|
|
505
519
|
listFolders(agentId) {
|
|
506
520
|
const custom = this.db.prepare("SELECT name FROM folders WHERE agent_id = ? ORDER BY name").all(agentId);
|
|
507
521
|
const counts = this.db
|
|
508
|
-
.prepare(`SELECT folder, COUNT(*) AS total, SUM(CASE WHEN read_at IS NULL THEN 1 ELSE 0 END) AS unread
|
|
522
|
+
.prepare(`SELECT folder, COUNT(*) AS total, SUM(CASE WHEN read_at IS NULL THEN 1 ELSE 0 END) AS unread
|
|
509
523
|
FROM deliveries WHERE agent_id = ? GROUP BY folder`)
|
|
510
524
|
.all(agentId);
|
|
511
525
|
const byName = new Map(counts.map((c) => [c.folder, c]));
|
|
@@ -544,7 +558,7 @@ export class MessageStore extends EventEmitter {
|
|
|
544
558
|
if (!exists)
|
|
545
559
|
throw new MessengerError(`folder not found: ${folder}`, 404);
|
|
546
560
|
const moved = this.db
|
|
547
|
-
.prepare(`UPDATE deliveries SET folder = CASE role WHEN 'sender' THEN 'sent' ELSE 'inbox' END
|
|
561
|
+
.prepare(`UPDATE deliveries SET folder = CASE role WHEN 'sender' THEN 'sent' ELSE 'inbox' END
|
|
548
562
|
WHERE agent_id = ? AND folder = ?`)
|
|
549
563
|
.run(agentId, folder).changes;
|
|
550
564
|
this.db.prepare("DELETE FROM folders WHERE agent_id = ? AND name = ?").run(agentId, folder);
|
|
@@ -588,17 +602,93 @@ export class MessageStore extends EventEmitter {
|
|
|
588
602
|
params.push(opts.thread_id);
|
|
589
603
|
}
|
|
590
604
|
const rows = this.db
|
|
591
|
-
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
592
|
-
m.priority, m.in_reply_to, m.created_at,
|
|
593
|
-
FROM messages m
|
|
594
|
-
${THREAD_JOIN}
|
|
595
|
-
WHERE ${where.join(" AND ")}
|
|
596
|
-
ORDER BY m.created_at DESC, m.id DESC
|
|
605
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
606
|
+
m.priority, m.in_reply_to, m.created_at, 0 AS starred, ${ADMIN_DELIVERY}
|
|
607
|
+
FROM messages m
|
|
608
|
+
${THREAD_JOIN}
|
|
609
|
+
WHERE ${where.join(" AND ")}
|
|
610
|
+
ORDER BY m.created_at DESC, m.id DESC
|
|
597
611
|
LIMIT ? OFFSET ?`)
|
|
598
612
|
.all(...params, limit, offset);
|
|
599
613
|
const recipients = this.recipientsFor(rows.map((r) => r.message_id));
|
|
600
614
|
return rows.map((row) => this.toSummary(row, recipients));
|
|
601
615
|
}
|
|
616
|
+
/**
|
|
617
|
+
* Fils du serveur entier, le plus récemment actif en tête.
|
|
618
|
+
*
|
|
619
|
+
* La console est organisée autour du fil : c'est lui qui porte le sujet, les deux
|
|
620
|
+
* correspondants et l'état de lecture. Chaque ligne est autonome — dernier message,
|
|
621
|
+
* aperçu, priorité la plus haute du fil — pour que la liste s'affiche d'une requête.
|
|
622
|
+
*/
|
|
623
|
+
listAllThreadsAdmin(opts = {}) {
|
|
624
|
+
const limit = clamp(opts.limit ?? 50, 1, MAX_LIMIT);
|
|
625
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
626
|
+
const where = ["1 = 1"];
|
|
627
|
+
const having = [];
|
|
628
|
+
const params = [];
|
|
629
|
+
// Un agent « participe » au fil dès qu'il y a une livraison, qu'il ait écrit ou reçu :
|
|
630
|
+
// c'est la correspondance qui est filtrée, pas les messages qu'il a signés.
|
|
631
|
+
if (opts.agent) {
|
|
632
|
+
where.push(`EXISTS (SELECT 1 FROM deliveries d JOIN messages mp ON mp.id = d.message_id
|
|
633
|
+
WHERE mp.thread_id = m.thread_id AND d.agent_id = ?)`);
|
|
634
|
+
params.push(opts.agent);
|
|
635
|
+
}
|
|
636
|
+
// La recherche porte sur le fil entier : son nom, sa description, et le texte de
|
|
637
|
+
// n'importe lequel de ses messages. Un fil non nommé reste ainsi trouvable.
|
|
638
|
+
if (opts.search) {
|
|
639
|
+
const like = `%${escapeLike(opts.search)}%`;
|
|
640
|
+
where.push(`(COALESCE(t.title, '') LIKE ? ESCAPE '\\' OR COALESCE(t.description, '') LIKE ? ESCAPE '\\'
|
|
641
|
+
OR EXISTS (SELECT 1 FROM messages ms WHERE ms.thread_id = m.thread_id
|
|
642
|
+
AND (ms.subject LIKE ? ESCAPE '\\' OR ms.body LIKE ? ESCAPE '\\')))`);
|
|
643
|
+
params.push(like, like, like, like);
|
|
644
|
+
}
|
|
645
|
+
if (opts.unread_only)
|
|
646
|
+
having.push("unread > 0");
|
|
647
|
+
if (opts.priority)
|
|
648
|
+
having.push("top_priority = ?");
|
|
649
|
+
const rows = this.db
|
|
650
|
+
.prepare(`SELECT m.thread_id,
|
|
651
|
+
t.title, t.description, t.named_by, t.created_at, t.updated_at,
|
|
652
|
+
(SELECT r.subject FROM messages r WHERE r.thread_id = m.thread_id
|
|
653
|
+
ORDER BY r.created_at ASC, r.id ASC LIMIT 1) AS subject,
|
|
654
|
+
COUNT(*) AS messages,
|
|
655
|
+
MAX(m.created_at) AS last_message_at,
|
|
656
|
+
(SELECT r.sender_id FROM messages r WHERE r.thread_id = m.thread_id
|
|
657
|
+
ORDER BY r.created_at DESC, r.id DESC LIMIT 1) AS last_message_from,
|
|
658
|
+
(SELECT r.body FROM messages r WHERE r.thread_id = m.thread_id
|
|
659
|
+
ORDER BY r.created_at DESC, r.id DESC LIMIT 1) AS last_body,
|
|
660
|
+
(SELECT COUNT(*) FROM deliveries d JOIN messages mu ON mu.id = d.message_id
|
|
661
|
+
WHERE mu.thread_id = m.thread_id AND d.role != 'sender'
|
|
662
|
+
AND d.read_at IS NULL AND d.folder != 'trash') AS unread,
|
|
663
|
+
CASE WHEN SUM(m.priority = 'high') > 0 THEN 'high'
|
|
664
|
+
WHEN SUM(m.priority = 'normal') > 0 THEN 'normal'
|
|
665
|
+
ELSE 'low' END AS top_priority
|
|
666
|
+
FROM messages m
|
|
667
|
+
${THREAD_JOIN}
|
|
668
|
+
WHERE ${where.join(" AND ")}
|
|
669
|
+
GROUP BY m.thread_id
|
|
670
|
+
${having.length ? `HAVING ${having.join(" AND ")}` : ""}
|
|
671
|
+
ORDER BY last_message_at DESC
|
|
672
|
+
LIMIT ? OFFSET ?`)
|
|
673
|
+
.all(...params, ...(opts.priority ? [opts.priority] : []), limit, offset);
|
|
674
|
+
const participants = this.participantsFor(rows.map((r) => r.thread_id));
|
|
675
|
+
return rows.map((row) => ({
|
|
676
|
+
thread_id: row.thread_id,
|
|
677
|
+
title: row.title,
|
|
678
|
+
description: row.description,
|
|
679
|
+
named_by: row.named_by,
|
|
680
|
+
created_at: row.created_at ?? row.last_message_at,
|
|
681
|
+
updated_at: row.updated_at,
|
|
682
|
+
subject: row.subject,
|
|
683
|
+
messages: row.messages,
|
|
684
|
+
unread: row.unread,
|
|
685
|
+
participants: participants.get(row.thread_id) ?? [],
|
|
686
|
+
last_message_at: row.last_message_at,
|
|
687
|
+
last_message_from: row.last_message_from,
|
|
688
|
+
last_message_preview: preview(row.last_body ?? ""),
|
|
689
|
+
priority: row.top_priority,
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
602
692
|
/**
|
|
603
693
|
* Fil complet vu du serveur, du plus ancien au plus récent, corps inclus.
|
|
604
694
|
*
|
|
@@ -608,11 +698,11 @@ export class MessageStore extends EventEmitter {
|
|
|
608
698
|
*/
|
|
609
699
|
listThreadAdmin(threadId) {
|
|
610
700
|
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,
|
|
613
|
-
FROM messages m
|
|
614
|
-
${THREAD_JOIN}
|
|
615
|
-
WHERE m.thread_id = ?
|
|
701
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
702
|
+
m.priority, m.in_reply_to, m.created_at, 0 AS starred, ${ADMIN_DELIVERY}
|
|
703
|
+
FROM messages m
|
|
704
|
+
${THREAD_JOIN}
|
|
705
|
+
WHERE m.thread_id = ?
|
|
616
706
|
ORDER BY m.created_at ASC, m.id ASC`)
|
|
617
707
|
.all(threadId);
|
|
618
708
|
const recipients = this.recipientsFor(rows.map((r) => r.message_id));
|
|
@@ -621,8 +711,8 @@ export class MessageStore extends EventEmitter {
|
|
|
621
711
|
/** Message complet vu du serveur, avec l'état de lecture de chaque destinataire. */
|
|
622
712
|
getMessageAdmin(messageId) {
|
|
623
713
|
const row = this.db
|
|
624
|
-
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
625
|
-
m.priority, m.in_reply_to, m.created_at,
|
|
714
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
715
|
+
m.priority, m.in_reply_to, m.created_at, 0 AS starred, ${ADMIN_DELIVERY}
|
|
626
716
|
FROM messages m ${THREAD_JOIN} WHERE m.id = ?`)
|
|
627
717
|
.get(messageId);
|
|
628
718
|
if (!row)
|
|
@@ -646,9 +736,9 @@ export class MessageStore extends EventEmitter {
|
|
|
646
736
|
const threads = this.db.prepare("SELECT COUNT(DISTINCT thread_id) AS n FROM messages").get().n;
|
|
647
737
|
const sentByAgent = new Map(this.db.prepare("SELECT sender_id, COUNT(*) AS n FROM messages GROUP BY sender_id").all().map((r) => [r.sender_id, r.n]));
|
|
648
738
|
const receivedByAgent = new Map(this.db
|
|
649
|
-
.prepare(`SELECT agent_id,
|
|
650
|
-
COUNT(*) AS received,
|
|
651
|
-
SUM(CASE WHEN read_at IS NULL AND folder != 'trash' THEN 1 ELSE 0 END) AS unread
|
|
739
|
+
.prepare(`SELECT agent_id,
|
|
740
|
+
COUNT(*) AS received,
|
|
741
|
+
SUM(CASE WHEN read_at IS NULL AND folder != 'trash' THEN 1 ELSE 0 END) AS unread
|
|
652
742
|
FROM deliveries WHERE role != 'sender' GROUP BY agent_id`)
|
|
653
743
|
.all().map((r) => [r.agent_id, r]));
|
|
654
744
|
const agents = this.listAgents().map((agent) => ({
|
|
@@ -695,7 +785,7 @@ export class MessageStore extends EventEmitter {
|
|
|
695
785
|
return map;
|
|
696
786
|
const placeholders = ids.map(() => "?").join(",");
|
|
697
787
|
const rows = this.db
|
|
698
|
-
.prepare(`SELECT message_id, agent_id FROM deliveries
|
|
788
|
+
.prepare(`SELECT message_id, agent_id FROM deliveries
|
|
699
789
|
WHERE message_id IN (${placeholders}) AND role = 'to'`)
|
|
700
790
|
.all(...ids);
|
|
701
791
|
for (const row of rows)
|
package/dist/src/core/types.d.ts
CHANGED
|
@@ -152,3 +152,25 @@ export interface MailboxStats {
|
|
|
152
152
|
/** Absent des réponses du store : ajouté par le serveur, qui seul voit les connexions. */
|
|
153
153
|
watcher?: WatcherStatus;
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Fil vu depuis la console de supervision, toutes boîtes confondues.
|
|
157
|
+
*
|
|
158
|
+
* La console lit le serveur, pas une boîte : elle n'a donc pas de point de vue depuis
|
|
159
|
+
* lequel compter les non-lus d'un fil. `unread` compte ici toutes les livraisons non lues
|
|
160
|
+
* du fil, quel que soit le destinataire, et `participants` liste les deux côtés.
|
|
161
|
+
*
|
|
162
|
+
* Le dernier message est résumé sur place : la liste des fils doit tenir en une requête,
|
|
163
|
+
* sans un aller-retour par fil pour afficher son aperçu.
|
|
164
|
+
*/
|
|
165
|
+
export interface AdminThreadSummary extends ThreadInfo {
|
|
166
|
+
/** Sujet du message d'ouverture : intitulé de repli tant que le fil n'est pas nommé. */
|
|
167
|
+
subject: string;
|
|
168
|
+
messages: number;
|
|
169
|
+
unread: number;
|
|
170
|
+
participants: string[];
|
|
171
|
+
last_message_at: string;
|
|
172
|
+
last_message_from: string;
|
|
173
|
+
last_message_preview: string;
|
|
174
|
+
/** Priorité la plus haute rencontrée dans le fil : ce qui doit sauter aux yeux. */
|
|
175
|
+
priority: Priority;
|
|
176
|
+
}
|
package/dist/src/mcp/start.js
CHANGED
|
@@ -18,6 +18,10 @@ import { TOOLS, callTool } from "./tools.js";
|
|
|
18
18
|
*/
|
|
19
19
|
const INSTRUCTIONS = `mcp-messenger gives this session a mailbox and lets it exchange messages with other agent sessions.
|
|
20
20
|
|
|
21
|
+
Two words, two different things, used consistently by every tool here:
|
|
22
|
+
- a **mailbox** is an address (\`box_…\`). It identifies exactly one session, and it is what you write to.
|
|
23
|
+
- an **agent name** is the project a session works for. It is a label, never an address, and every session of that project carries it.
|
|
24
|
+
|
|
21
25
|
Who you are, and who the others are — read this before writing to anyone:
|
|
22
26
|
- A session is identified by its mailbox address, and by nothing else. \`whoami\` gives you yours.
|
|
23
27
|
- 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.
|
|
@@ -26,7 +30,7 @@ Who you are, and who the others are — read this before writing to anyone:
|
|
|
26
30
|
- Your own mailbox is never listed in \`list_agents\`: every address you see there belongs to someone else.
|
|
27
31
|
|
|
28
32
|
Writing:
|
|
29
|
-
- One message goes to exactly one
|
|
33
|
+
- One message goes to exactly one mailbox, and never to your own. To reach several correspondents, send each one its own message.
|
|
30
34
|
- Never write a mailbox address in the body of a message. The server attaches the sender and the recipient to every message, and the reader sees them: an address copied by hand adds nothing and misleads as soon as it is stale or mistaken.
|
|
31
35
|
- Call \`enable_notifications\` once at the start of the session and arm the watcher it hands you. Until then, no incoming message reaches you.`;
|
|
32
36
|
/** Serveur MCP prêt à être branché sur n'importe quel transport (stdio en production, mémoire en test). */
|