@imenam/mcp-messenger 1.0.29 → 1.0.31
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 +764 -707
- package/dist/src/cli/watch.d.ts +3 -1
- package/dist/src/cli/watch.js +12 -2
- package/dist/src/client/messenger-client.d.ts +21 -1
- package/dist/src/client/messenger-client.js +31 -9
- package/dist/src/core/attachments.d.ts +16 -0
- package/dist/src/core/attachments.js +28 -0
- package/dist/src/core/store.d.ts +34 -3
- package/dist/src/core/store.js +404 -216
- package/dist/src/core/types.d.ts +41 -0
- package/dist/src/mcp/start.js +25 -22
- package/dist/src/mcp/tools.js +186 -9
- package/dist/src/server/app.js +89 -12
- package/dist/src/server/ui/app.js +867 -7
- package/dist/src/server/ui/index.html +59 -0
- package/dist/src/server/ui/styles.css +751 -20
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +16 -2
- package/package.json +2 -2
package/dist/src/core/types.d.ts
CHANGED
|
@@ -35,6 +35,45 @@ export interface AgentDirectoryEntry extends Agent {
|
|
|
35
35
|
/** Plus récent de la dernière connexion et de la dernière livraison ; null si aucune. */
|
|
36
36
|
last_activity_at: string | null;
|
|
37
37
|
}
|
|
38
|
+
/** Une des deux boîtes d'une liaison, et le moment où son agent en a pris connaissance. */
|
|
39
|
+
export interface LinkEnd {
|
|
40
|
+
mailbox: string;
|
|
41
|
+
/** null tant que l'agent de cette boîte n'a pas lu la liaison (`whoami`, `list_links`, `list_agents`). */
|
|
42
|
+
seen_at: string | null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Liaison posée depuis la console entre deux boîtes : chacune connaît l'autre comme
|
|
46
|
+
* correspondante désignée, sans que les agents aient à échanger d'adresse.
|
|
47
|
+
*/
|
|
48
|
+
export interface MailboxLink {
|
|
49
|
+
link_id: string;
|
|
50
|
+
/** Motif transmis aux deux agents. */
|
|
51
|
+
note: string | null;
|
|
52
|
+
created_at: string;
|
|
53
|
+
mailboxes: [LinkEnd, LinkEnd];
|
|
54
|
+
}
|
|
55
|
+
/** Liaison vue depuis l'une de ses deux boîtes : la fiche de la boîte d'en face. */
|
|
56
|
+
export interface LinkedMailbox {
|
|
57
|
+
link_id: string;
|
|
58
|
+
note: string | null;
|
|
59
|
+
created_at: string;
|
|
60
|
+
correspondent: Agent;
|
|
61
|
+
}
|
|
62
|
+
/** Pièce jointe telle qu'elle voyage avec un message : ses métadonnées, sans son contenu. */
|
|
63
|
+
export interface AttachmentInfo {
|
|
64
|
+
attachment_id: string;
|
|
65
|
+
filename: string;
|
|
66
|
+
mime_type: string;
|
|
67
|
+
/** Taille en octets. */
|
|
68
|
+
size: number;
|
|
69
|
+
}
|
|
70
|
+
/** Pièce jointe fournie à l'envoi. */
|
|
71
|
+
export interface AttachmentUpload {
|
|
72
|
+
filename: string;
|
|
73
|
+
/** Type MIME ; `application/octet-stream` s'il est absent. */
|
|
74
|
+
mime_type?: string | null;
|
|
75
|
+
data: Buffer;
|
|
76
|
+
}
|
|
38
77
|
export interface SendMessageInput {
|
|
39
78
|
from: string;
|
|
40
79
|
/** Un échange lie exactement deux boîtes : un seul destinataire par message. */
|
|
@@ -47,6 +86,7 @@ export interface SendMessageInput {
|
|
|
47
86
|
/** Nom lisible donné au fil. Sur une réponse, renomme le fil existant. */
|
|
48
87
|
thread_title?: string | null;
|
|
49
88
|
thread_description?: string | null;
|
|
89
|
+
attachments?: AttachmentUpload[];
|
|
50
90
|
}
|
|
51
91
|
/**
|
|
52
92
|
* Intitulé d'un fil de discussion.
|
|
@@ -98,6 +138,7 @@ export interface MessageSummary {
|
|
|
98
138
|
starred: boolean;
|
|
99
139
|
created_at: string;
|
|
100
140
|
in_reply_to: string | null;
|
|
141
|
+
attachments: AttachmentInfo[];
|
|
101
142
|
}
|
|
102
143
|
export interface Message extends Omit<MessageSummary, "preview"> {
|
|
103
144
|
body: string;
|
package/dist/src/mcp/start.js
CHANGED
|
@@ -18,28 +18,31 @@ import { TOOLS, callTool } from "./tools.js";
|
|
|
18
18
|
* recopier une adresse dans le corps d'un message (le serveur la pose déjà, la copie ment
|
|
19
19
|
* dès qu'elle vieillit). Les dire ici, c'est les dire avant l'erreur.
|
|
20
20
|
*/
|
|
21
|
-
const INSTRUCTIONS = `mcp-messenger gives this session a mailbox and lets it exchange messages with other agent sessions.
|
|
22
|
-
|
|
23
|
-
Two words, two different things, used consistently by every tool here:
|
|
24
|
-
- a **mailbox** is an address (\`box_…\`). It identifies exactly one session, and it is what you write to.
|
|
25
|
-
- 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.
|
|
26
|
-
|
|
27
|
-
Who you are, and who the others are — read this before writing to anyone:
|
|
28
|
-
- A session is identified by its mailbox address, and by nothing else. \`whoami\` gives you yours.
|
|
29
|
-
- 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.
|
|
30
|
-
- \`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.
|
|
31
|
-
- 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.
|
|
32
|
-
- Your own mailbox is never listed in \`list_agents\`: every address you see there belongs to someone else.
|
|
33
|
-
|
|
34
|
-
Resuming earlier work:
|
|
35
|
-
- A resumed session usually keeps the mailbox it had: the address, the threads and the unread messages come back on their own. \`whoami\` says so when it happens.
|
|
36
|
-
- When it cannot be decided automatically, \`whoami\` lists the mailboxes left by a stopped session of this project. If you are continuing that work, call \`attach_to_mailbox\` with the right address.
|
|
37
|
-
- If a \`box_…\` address appears earlier in your own context, from a previous part of this conversation, that is your mailbox from before: attach to it rather than writing from a fresh one, so your correspondents keep reaching you at the address they know.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
21
|
+
const INSTRUCTIONS = `mcp-messenger gives this session a mailbox and lets it exchange messages with other agent sessions.
|
|
22
|
+
|
|
23
|
+
Two words, two different things, used consistently by every tool here:
|
|
24
|
+
- a **mailbox** is an address (\`box_…\`). It identifies exactly one session, and it is what you write to.
|
|
25
|
+
- 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.
|
|
26
|
+
|
|
27
|
+
Who you are, and who the others are — read this before writing to anyone:
|
|
28
|
+
- A session is identified by its mailbox address, and by nothing else. \`whoami\` gives you yours.
|
|
29
|
+
- 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.
|
|
30
|
+
- \`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.
|
|
31
|
+
- 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.
|
|
32
|
+
- Your own mailbox is never listed in \`list_agents\`: every address you see there belongs to someone else.
|
|
33
|
+
|
|
34
|
+
Resuming earlier work:
|
|
35
|
+
- A resumed session usually keeps the mailbox it had: the address, the threads and the unread messages come back on their own. \`whoami\` says so when it happens.
|
|
36
|
+
- When it cannot be decided automatically, \`whoami\` lists the mailboxes left by a stopped session of this project. If you are continuing that work, call \`attach_to_mailbox\` with the right address.
|
|
37
|
+
- If a \`box_…\` address appears earlier in your own context, from a previous part of this conversation, that is your mailbox from before: attach to it rather than writing from a fresh one, so your correspondents keep reaching you at the address they know.
|
|
38
|
+
|
|
39
|
+
Linked mailboxes:
|
|
40
|
+
- The user can link this mailbox to others from the web console: those are the correspondents you are meant to work with. \`whoami\` and \`list_links\` list them, and the notification watcher announces each new link as it arrives.
|
|
41
|
+
|
|
42
|
+
Writing:
|
|
43
|
+
- One message goes to exactly one mailbox, and never to your own. To reach several correspondents, send each one its own message.
|
|
44
|
+
- 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.
|
|
45
|
+
- Call \`enable_notifications\` once at the start of the session and arm the watcher it hands you. Until then, no incoming message reaches you.
|
|
43
46
|
- The host stops that watcher after 30 minutes at most. Each time it tells you the watcher expired, re-arm it at once with the same Monitor call; otherwise notifications stop for good.`;
|
|
44
47
|
/** Serveur MCP prêt à être branché sur n'importe quel transport (stdio en production, mémoire en test). */
|
|
45
48
|
export function createMcpServer(client) {
|
package/dist/src/mcp/tools.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { z } from "zod";
|
|
2
5
|
import { writeHandoff } from "../cli/watch-handoff.js";
|
|
6
|
+
import { MAX_ATTACHMENTS, MAX_ATTACHMENTS_TOTAL_BYTES, MAX_ATTACHMENT_BYTES, formatBytes } from "../core/attachments.js";
|
|
3
7
|
import { PRIORITIES, SYSTEM_FOLDERS } from "../core/types.js";
|
|
4
8
|
import { rebind } from "./mailbox-registry.js";
|
|
5
9
|
import { forgetCandidate, getResumeState } from "./resume-state.js";
|
|
@@ -7,6 +11,35 @@ import { forgetCandidate, getResumeState } from "./resume-state.js";
|
|
|
7
11
|
const PACKAGE_NAME = "@imenam/mcp-messenger";
|
|
8
12
|
/** Durée maximale d'un `Monitor` côté hôte : au-delà, l'hôte la ramène à cette valeur. */
|
|
9
13
|
const MONITOR_TIMEOUT_MS = 1_800_000;
|
|
14
|
+
/** Répertoire où `save_attachment` écrit quand l'agent ne donne pas de destination. */
|
|
15
|
+
const DEFAULT_ATTACHMENT_DIR = path.join(os.tmpdir(), "mcp-messenger", "attachments");
|
|
16
|
+
/** Type MIME déduit de l'extension, pour les formats courants. */
|
|
17
|
+
const MIME_TYPES = {
|
|
18
|
+
".txt": "text/plain",
|
|
19
|
+
".md": "text/markdown",
|
|
20
|
+
".csv": "text/csv",
|
|
21
|
+
".json": "application/json",
|
|
22
|
+
".xml": "application/xml",
|
|
23
|
+
".yaml": "application/yaml",
|
|
24
|
+
".yml": "application/yaml",
|
|
25
|
+
".html": "text/html",
|
|
26
|
+
".css": "text/css",
|
|
27
|
+
".js": "text/javascript",
|
|
28
|
+
".log": "text/plain",
|
|
29
|
+
".pdf": "application/pdf",
|
|
30
|
+
".png": "image/png",
|
|
31
|
+
".jpg": "image/jpeg",
|
|
32
|
+
".jpeg": "image/jpeg",
|
|
33
|
+
".gif": "image/gif",
|
|
34
|
+
".webp": "image/webp",
|
|
35
|
+
".svg": "image/svg+xml",
|
|
36
|
+
".zip": "application/zip",
|
|
37
|
+
".gz": "application/gzip",
|
|
38
|
+
".tar": "application/x-tar",
|
|
39
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
40
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
41
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
42
|
+
};
|
|
10
43
|
const FOLDER_HINT = `Folder name. System folders: ${SYSTEM_FOLDERS.join(", ")}. Custom folders come from list_folders.`;
|
|
11
44
|
export const TOOLS = [
|
|
12
45
|
{
|
|
@@ -74,6 +107,11 @@ export const TOOLS = [
|
|
|
74
107
|
additionalProperties: false,
|
|
75
108
|
},
|
|
76
109
|
},
|
|
110
|
+
{
|
|
111
|
+
name: "list_links",
|
|
112
|
+
description: "List the mailboxes the user has linked to this one from the web console, newest first: these are the correspondents you are meant to work with, with the reason the user gave. Write to them with send_message, using the address listed here. Links arrive while you work — the notification watcher announces each new one — and whoami lists them too.",
|
|
113
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
114
|
+
},
|
|
77
115
|
{
|
|
78
116
|
name: "list_agents",
|
|
79
117
|
description: "List the mailboxes known to the 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 mailbox address. Your own mailbox is never listed — you cannot write to yourself. Use it to find the mailbox address you want to write to; pass agent_name to expand a group whose sessions were not all listed.",
|
|
@@ -107,6 +145,23 @@ export const TOOLS = [
|
|
|
107
145
|
description: "Plain-language name of the conversation, e.g. 'Résolution de l'échec du test de parsing'. Name the thread when you open it — you already know what it is about. On a reply it renames the thread for everyone (same effect as rename_thread).",
|
|
108
146
|
},
|
|
109
147
|
thread_description: { type: "string", description: "Optional longer description of the thread." },
|
|
148
|
+
attachments: {
|
|
149
|
+
type: "array",
|
|
150
|
+
maxItems: MAX_ATTACHMENTS,
|
|
151
|
+
description: `Files to attach, read from this machine's disk. At most ${MAX_ATTACHMENTS} files, ${formatBytes(MAX_ATTACHMENT_BYTES)} each and ${formatBytes(MAX_ATTACHMENTS_TOTAL_BYTES)} in total. The recipient saves them with save_attachment.`,
|
|
152
|
+
items: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
path: {
|
|
156
|
+
type: "string",
|
|
157
|
+
description: "Path of the file to attach. Prefer an absolute path; a relative one is resolved against this MCP server's working directory.",
|
|
158
|
+
},
|
|
159
|
+
name: { type: "string", description: "File name shown to the recipient. Defaults to the file's own name." },
|
|
160
|
+
},
|
|
161
|
+
required: ["path"],
|
|
162
|
+
additionalProperties: false,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
110
165
|
},
|
|
111
166
|
required: ["to", "subject", "body"],
|
|
112
167
|
additionalProperties: false,
|
|
@@ -152,6 +207,23 @@ export const TOOLS = [
|
|
|
152
207
|
additionalProperties: false,
|
|
153
208
|
},
|
|
154
209
|
},
|
|
210
|
+
{
|
|
211
|
+
name: "save_attachment",
|
|
212
|
+
description: "Save a file attached to a message of this mailbox onto this machine's disk, and return the path it was written to. Attachment ids are listed under the message by read_message and get_thread. Read the saved file with your usual file tools.",
|
|
213
|
+
inputSchema: {
|
|
214
|
+
type: "object",
|
|
215
|
+
properties: {
|
|
216
|
+
attachment_id: { type: "string", description: "Attachment id (att_…) shown under the message." },
|
|
217
|
+
path: {
|
|
218
|
+
type: "string",
|
|
219
|
+
description: "Where to save it: an existing directory (the file keeps its name) or a file path. Defaults to a directory under the system temp folder.",
|
|
220
|
+
},
|
|
221
|
+
overwrite: { type: "boolean", description: "Replace an existing file at that path. Defaults to false." },
|
|
222
|
+
},
|
|
223
|
+
required: ["attachment_id"],
|
|
224
|
+
additionalProperties: false,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
155
227
|
{
|
|
156
228
|
name: "mark_messages",
|
|
157
229
|
description: "Mark messages as read or unread, and/or star or unstar them.",
|
|
@@ -244,6 +316,15 @@ const schemas = {
|
|
|
244
316
|
reply_to: z.string().optional(),
|
|
245
317
|
thread_name: z.string().trim().min(1).max(200).optional(),
|
|
246
318
|
thread_description: z.string().trim().min(1).max(2000).optional(),
|
|
319
|
+
attachments: z
|
|
320
|
+
.array(z.object({ path: z.string().trim().min(1), name: z.string().trim().min(1).max(255).optional() }))
|
|
321
|
+
.max(MAX_ATTACHMENTS)
|
|
322
|
+
.optional(),
|
|
323
|
+
}),
|
|
324
|
+
save_attachment: z.object({
|
|
325
|
+
attachment_id: z.string().trim().min(1),
|
|
326
|
+
path: z.string().trim().min(1).optional(),
|
|
327
|
+
overwrite: z.boolean().optional(),
|
|
247
328
|
}),
|
|
248
329
|
list_messages: z.object({
|
|
249
330
|
folder: z.string().optional(),
|
|
@@ -320,6 +401,7 @@ export async function callTool(client, name, rawArgs) {
|
|
|
320
401
|
const health = await client.health().catch(() => null);
|
|
321
402
|
const folders = stats.folders.map((f) => ` ${f.name.padEnd(12)} ${f.total} message(s), ${f.unread} unread`);
|
|
322
403
|
const self = await client.getAgent(client.agentId).catch(() => null);
|
|
404
|
+
const links = await client.listLinks();
|
|
323
405
|
const lines = [
|
|
324
406
|
`Mailbox address: ${client.agentId}`,
|
|
325
407
|
// Personne n'a besoin qu'on la lui envoie : l'annuaire la donne, et chaque message
|
|
@@ -335,6 +417,7 @@ export async function callTool(client, name, rawArgs) {
|
|
|
335
417
|
self?.display_name
|
|
336
418
|
? `Mailbox name: ${self.display_name}`
|
|
337
419
|
: "Mailbox name: none — call rename_mailbox so the user can tell this session apart from the others.",
|
|
420
|
+
...linkLines(links),
|
|
338
421
|
];
|
|
339
422
|
if (client.isAttachedElsewhere) {
|
|
340
423
|
lines.push(`Attached to another mailbox. This session's own mailbox is ${client.sessionAgentId}.`);
|
|
@@ -415,16 +498,31 @@ export async function callTool(client, name, rawArgs) {
|
|
|
415
498
|
return `No thread in this mailbox${scope} matching these filters.`;
|
|
416
499
|
return [`${threads.length} thread(s)${scope}:`, "", ...threads.map(formatThread)].join("\n");
|
|
417
500
|
}
|
|
501
|
+
case "list_links": {
|
|
502
|
+
const links = await client.listLinks();
|
|
503
|
+
if (links.length === 0) {
|
|
504
|
+
return "No mailbox is linked to this one yet. The user links mailboxes from the web console; the notification watcher announces each new link.";
|
|
505
|
+
}
|
|
506
|
+
return [
|
|
507
|
+
`${links.length} mailbox(es) linked to this one by the user, newest first:`,
|
|
508
|
+
"",
|
|
509
|
+
...links.map(formatLink),
|
|
510
|
+
"",
|
|
511
|
+
"Write to them with send_message, using the address shown. Do not repeat the address in the message body.",
|
|
512
|
+
].join("\n");
|
|
513
|
+
}
|
|
418
514
|
case "list_agents": {
|
|
419
515
|
const input = schemas.list_agents.parse(args);
|
|
420
516
|
const agents = await client.listAgents();
|
|
421
517
|
if (agents.length === 0)
|
|
422
518
|
return "No other mailbox on this server yet.";
|
|
519
|
+
const linked = new Set((await client.listLinks()).map((link) => link.correspondent.id));
|
|
423
520
|
const groups = groupByAgentName(agents);
|
|
424
|
-
return input.agent_name ? formatAgentGroup(groups, input.agent_name) : formatDirectory(groups);
|
|
521
|
+
return input.agent_name ? formatAgentGroup(groups, input.agent_name, linked) : formatDirectory(groups, linked);
|
|
425
522
|
}
|
|
426
523
|
case "send_message": {
|
|
427
524
|
const input = schemas.send_message.parse(args);
|
|
525
|
+
const attachments = await readAttachments(input.attachments ?? []);
|
|
428
526
|
const result = await client.sendMessage({
|
|
429
527
|
to: input.to,
|
|
430
528
|
subject: input.subject,
|
|
@@ -434,12 +532,16 @@ export async function callTool(client, name, rawArgs) {
|
|
|
434
532
|
// Le protocole HTTP et le stockage gardent `thread_title` ; seul l'outil dit `thread_name`.
|
|
435
533
|
thread_title: input.thread_name ?? null,
|
|
436
534
|
thread_description: input.thread_description ?? null,
|
|
535
|
+
attachments,
|
|
437
536
|
});
|
|
438
537
|
const lines = [
|
|
439
538
|
`Message sent: ${result.message_id}`,
|
|
440
539
|
`Thread: ${formatThreadRef(result.thread_title, result.thread_id)}`,
|
|
441
540
|
`Delivered to: ${result.delivered_to}`,
|
|
442
541
|
];
|
|
542
|
+
if (attachments.length > 0) {
|
|
543
|
+
lines.push(`Attachments: ${attachments.map((a) => a.filename).join(", ")}`);
|
|
544
|
+
}
|
|
443
545
|
if (!result.thread_title && !input.reply_to) {
|
|
444
546
|
lines.push("This thread has no name yet — rename_thread with its thread_id makes it readable for everyone.");
|
|
445
547
|
}
|
|
@@ -472,6 +574,22 @@ export async function callTool(client, name, rawArgs) {
|
|
|
472
574
|
.join("\n");
|
|
473
575
|
return [header, "", ...messages.map(formatMessage)].join("\n\n---\n\n");
|
|
474
576
|
}
|
|
577
|
+
case "save_attachment": {
|
|
578
|
+
const input = schemas.save_attachment.parse(args);
|
|
579
|
+
const attachment = await client.downloadAttachment(input.attachment_id);
|
|
580
|
+
const target = await attachmentTarget(input.path, input.attachment_id, attachment.filename);
|
|
581
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
582
|
+
try {
|
|
583
|
+
await writeFile(target, attachment.data, { flag: input.overwrite ? "w" : "wx" });
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
if (error.code === "EEXIST") {
|
|
587
|
+
throw new Error(`a file already exists at ${target} — pass overwrite: true or choose another path`);
|
|
588
|
+
}
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
return `Saved ${attachment.filename} (${attachment.mime_type}, ${formatBytes(attachment.data.length)}) to ${target}`;
|
|
592
|
+
}
|
|
475
593
|
case "mark_messages": {
|
|
476
594
|
const input = schemas.mark_messages.parse(args);
|
|
477
595
|
if (input.read === undefined && input.starred === undefined) {
|
|
@@ -567,6 +685,42 @@ export async function callTool(client, name, rawArgs) {
|
|
|
567
685
|
throw new Error(`Unknown tool: ${name}`);
|
|
568
686
|
}
|
|
569
687
|
}
|
|
688
|
+
/** Lit sur le disque local les fichiers à joindre, en refusant d'emblée ceux qui dépassent les limites. */
|
|
689
|
+
async function readAttachments(entries) {
|
|
690
|
+
let total = 0;
|
|
691
|
+
const attachments = [];
|
|
692
|
+
for (const entry of entries) {
|
|
693
|
+
const file = path.resolve(entry.path);
|
|
694
|
+
const info = await stat(file).catch(() => null);
|
|
695
|
+
if (!info?.isFile())
|
|
696
|
+
throw new Error(`attachment is not a readable file: ${file}`);
|
|
697
|
+
if (info.size > MAX_ATTACHMENT_BYTES) {
|
|
698
|
+
throw new Error(`attachment exceeds ${formatBytes(MAX_ATTACHMENT_BYTES)}: ${file} (${formatBytes(info.size)})`);
|
|
699
|
+
}
|
|
700
|
+
total += info.size;
|
|
701
|
+
if (total > MAX_ATTACHMENTS_TOTAL_BYTES) {
|
|
702
|
+
throw new Error(`attachments exceed ${formatBytes(MAX_ATTACHMENTS_TOTAL_BYTES)} in total`);
|
|
703
|
+
}
|
|
704
|
+
const filename = entry.name ?? path.basename(file);
|
|
705
|
+
attachments.push({
|
|
706
|
+
filename,
|
|
707
|
+
mime_type: MIME_TYPES[path.extname(filename).toLowerCase()] ?? null,
|
|
708
|
+
content_base64: (await readFile(file)).toString("base64"),
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
return attachments;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Chemin d'écriture d'une pièce jointe : dans le répertoire donné sous son propre nom, au
|
|
715
|
+
* chemin donné s'il désigne un fichier, ou dans un répertoire temporaire propre à la pièce.
|
|
716
|
+
*/
|
|
717
|
+
async function attachmentTarget(requested, attachmentId, filename) {
|
|
718
|
+
if (!requested)
|
|
719
|
+
return path.join(DEFAULT_ATTACHMENT_DIR, attachmentId, filename);
|
|
720
|
+
const resolved = path.resolve(requested);
|
|
721
|
+
const isDirectory = /[\\/]$/.test(requested) || (await stat(resolved).catch(() => null))?.isDirectory();
|
|
722
|
+
return isDirectory ? path.join(resolved, filename) : resolved;
|
|
723
|
+
}
|
|
570
724
|
async function renameMailbox(client, name, description) {
|
|
571
725
|
const agent = await client.updateProfile({ display_name: name, description });
|
|
572
726
|
return [
|
|
@@ -611,9 +765,9 @@ function groupByAgentName(agents) {
|
|
|
611
765
|
return [...groups.values()];
|
|
612
766
|
}
|
|
613
767
|
/** `boîte — nom · activité` : une ligne par session, l'adresse en tête car c'est elle qu'on copie. */
|
|
614
|
-
function formatMailboxLine(agent) {
|
|
768
|
+
function formatMailboxLine(agent, linked) {
|
|
615
769
|
const seen = agent.last_activity_at ? `last active ${agent.last_activity_at}` : "never active";
|
|
616
|
-
const flags = agent.registered ? "" : " · never connected"
|
|
770
|
+
const flags = `${agent.registered ? "" : " · never connected"}${linked.has(agent.id) ? " · linked to you" : ""}`;
|
|
617
771
|
return ` • ${agent.id} — ${agent.display_name ?? "no mailbox name"} · ${seen}${flags}`;
|
|
618
772
|
}
|
|
619
773
|
/**
|
|
@@ -630,7 +784,7 @@ const DIRECTORY_FOOTER = [
|
|
|
630
784
|
"is never listed here. Always write to a mailbox address taken from this list, never to a name.",
|
|
631
785
|
"To see what you have already exchanged with one of them, call list_threads with its address in with_mailbox.",
|
|
632
786
|
];
|
|
633
|
-
function formatDirectory(groups) {
|
|
787
|
+
function formatDirectory(groups, linked) {
|
|
634
788
|
const mailboxes = groups.reduce((total, group) => total + group.mailboxes.length, 0);
|
|
635
789
|
const blocks = groups.map((group) => {
|
|
636
790
|
const sessions = group.mailboxes.length;
|
|
@@ -640,7 +794,7 @@ function formatDirectory(groups) {
|
|
|
640
794
|
// Un agent à sessions nombreuses ne doit pas noyer les autres : les plus récentes
|
|
641
795
|
// suffisent à écrire, `agent_name` donne le reste à qui en a besoin.
|
|
642
796
|
const shown = sessions > SESSIONS_SHOWN_PER_AGENT ? group.mailboxes.slice(0, 3) : group.mailboxes;
|
|
643
|
-
lines.push(...shown.map(formatMailboxLine));
|
|
797
|
+
lines.push(...shown.map((agent) => formatMailboxLine(agent, linked)));
|
|
644
798
|
if (shown.length < sessions) {
|
|
645
799
|
lines.push(` … ${sessions - shown.length} more session(s) — list_agents with agent_name: "${group.name}" lists them all.`);
|
|
646
800
|
}
|
|
@@ -656,7 +810,7 @@ function formatDirectory(groups) {
|
|
|
656
810
|
].join("\n");
|
|
657
811
|
}
|
|
658
812
|
/** Toutes les sessions d'un agent, décrites une par une : le second niveau de l'annuaire. */
|
|
659
|
-
function formatAgentGroup(groups, requested) {
|
|
813
|
+
function formatAgentGroup(groups, requested, linked) {
|
|
660
814
|
const wanted = requested.trim().toLowerCase();
|
|
661
815
|
const group = groups.find((g) => g.name.toLowerCase() === wanted);
|
|
662
816
|
if (!group) {
|
|
@@ -669,7 +823,7 @@ function formatAgentGroup(groups, requested) {
|
|
|
669
823
|
`• ${agent.id}`,
|
|
670
824
|
` Mailbox: ${agent.display_name ?? "not named — this session never said what it works on"}`,
|
|
671
825
|
agent.description ? ` ${agent.description}` : null,
|
|
672
|
-
` ${seen}${agent.registered ? "" : " · never connected"}`,
|
|
826
|
+
` ${seen}${agent.registered ? "" : " · never connected"}${linked.has(agent.id) ? " · linked to you" : ""}`,
|
|
673
827
|
]
|
|
674
828
|
.filter(Boolean)
|
|
675
829
|
.join("\n");
|
|
@@ -729,6 +883,23 @@ function resumeLines(client) {
|
|
|
729
883
|
}
|
|
730
884
|
return lines;
|
|
731
885
|
}
|
|
886
|
+
/** Ce que `whoami` dit des liaisons posées par l'utilisateur sur cette boîte. */
|
|
887
|
+
function linkLines(links) {
|
|
888
|
+
if (links.length === 0) {
|
|
889
|
+
return ["Linked mailboxes: none yet — the user links mailboxes from the web console."];
|
|
890
|
+
}
|
|
891
|
+
return [
|
|
892
|
+
"Linked mailboxes — correspondents the user designated for this session:",
|
|
893
|
+
...links.map((link) => ` ${formatLink(link)}`),
|
|
894
|
+
" ↳ write to them with send_message; list_links details them.",
|
|
895
|
+
];
|
|
896
|
+
}
|
|
897
|
+
/** `• adresse — Agent · Boîte · liée le … — motif` */
|
|
898
|
+
function formatLink(link) {
|
|
899
|
+
const names = formatAgentNames(link.correspondent) || "unnamed mailbox";
|
|
900
|
+
const note = link.note ? ` — ${link.note}` : "";
|
|
901
|
+
return `• ${link.correspondent.id} — ${names} · linked ${link.created_at}${note}`;
|
|
902
|
+
}
|
|
732
903
|
/** `titre (id)`, ou l'id seul tant que le fil n'a pas été nommé. */
|
|
733
904
|
function formatThreadRef(title, threadId) {
|
|
734
905
|
return title ? `${title} (${threadId})` : threadId;
|
|
@@ -752,7 +923,7 @@ function formatSummary(message) {
|
|
|
752
923
|
`${flags} ${message.subject}${priority}`,
|
|
753
924
|
` from ${message.from} to ${message.to || "—"} · ${message.created_at}`,
|
|
754
925
|
` ${message.preview}`,
|
|
755
|
-
` id ${message.message_id} · thread ${formatThreadRef(message.thread_title, message.thread_id)}`,
|
|
926
|
+
` id ${message.message_id} · thread ${formatThreadRef(message.thread_title, message.thread_id)}${message.attachments.length > 0 ? ` · ${message.attachments.length} attachment(s)` : ""}`,
|
|
756
927
|
].join("\n");
|
|
757
928
|
}
|
|
758
929
|
function formatMessage(message) {
|
|
@@ -767,5 +938,11 @@ function formatMessage(message) {
|
|
|
767
938
|
]
|
|
768
939
|
.filter(Boolean)
|
|
769
940
|
.join("\n");
|
|
770
|
-
|
|
941
|
+
const attachments = message.attachments.length > 0
|
|
942
|
+
? `\n\nAttachments (save with save_attachment):\n${message.attachments.map(formatAttachment).join("\n")}`
|
|
943
|
+
: "";
|
|
944
|
+
return `${header}\n\n${message.body}${attachments}`;
|
|
945
|
+
}
|
|
946
|
+
function formatAttachment(attachment) {
|
|
947
|
+
return ` ${attachment.attachment_id} · ${attachment.filename} (${attachment.mime_type}, ${formatBytes(attachment.size)})`;
|
|
771
948
|
}
|