@imenam/mcp-organizer 0.0.0-stage → 1.0.2
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 +548 -3
- package/dist/shared/logic/first-account.d.ts +13 -0
- package/dist/shared/logic/first-account.js +15 -0
- package/dist/shared/logic/labels.d.ts +5 -0
- package/dist/shared/logic/labels.js +17 -0
- package/dist/shared/logic/positions.d.ts +9 -0
- package/dist/shared/logic/positions.js +19 -0
- package/dist/shared/logic/spaces.d.ts +13 -0
- package/dist/shared/logic/spaces.js +48 -0
- package/dist/shared/logic/test-summary.d.ts +28 -0
- package/dist/shared/logic/test-summary.js +35 -0
- package/dist/shared/schemas/agent-api.schema.d.ts +269 -0
- package/dist/shared/schemas/agent-api.schema.js +97 -0
- package/dist/shared/schemas/agent-tools.schema.d.ts +818 -0
- package/dist/shared/schemas/agent-tools.schema.js +198 -0
- package/dist/shared/schemas/api.schema.d.ts +249 -0
- package/dist/shared/schemas/api.schema.js +116 -0
- package/dist/shared/schemas/entities.schema.d.ts +472 -0
- package/dist/shared/schemas/entities.schema.js +262 -0
- package/dist/shared/schemas/enums.schema.d.ts +135 -0
- package/dist/shared/schemas/enums.schema.js +58 -0
- package/dist/shared/schemas/gui-api.schema.d.ts +53 -0
- package/dist/shared/schemas/gui-api.schema.js +20 -0
- package/dist/shared/schemas/ideas-api.schema.d.ts +81 -0
- package/dist/shared/schemas/ideas-api.schema.js +40 -0
- package/dist/shared/schemas/notifications-api.schema.d.ts +87 -0
- package/dist/shared/schemas/notifications-api.schema.js +58 -0
- package/dist/shared/schemas/tasks-api.schema.d.ts +675 -0
- package/dist/shared/schemas/tasks-api.schema.js +225 -0
- package/dist/shared/schemas/tokens-api.schema.d.ts +106 -0
- package/dist/shared/schemas/tokens-api.schema.js +42 -0
- package/dist/shared/schemas/transcription-api.schema.d.ts +15 -0
- package/dist/shared/schemas/transcription-api.schema.js +12 -0
- package/dist/src/agent-api/checklist.d.ts +3 -0
- package/dist/src/agent-api/checklist.js +127 -0
- package/dist/src/agent-api/ideas.d.ts +3 -0
- package/dist/src/agent-api/ideas.js +93 -0
- package/dist/src/agent-api/notifications.d.ts +16 -0
- package/dist/src/agent-api/notifications.js +91 -0
- package/dist/src/agent-api/requests.d.ts +3 -0
- package/dist/src/agent-api/requests.js +60 -0
- package/dist/src/agent-api/router.d.ts +3 -0
- package/dist/src/agent-api/router.js +41 -0
- package/dist/src/agent-api/tasks.d.ts +2 -0
- package/dist/src/agent-api/tasks.js +290 -0
- package/dist/src/agent-api/types.d.ts +41 -0
- package/dist/src/agent-api/types.js +14 -0
- package/dist/src/cli/backup.d.ts +12 -0
- package/dist/src/cli/backup.js +126 -0
- package/dist/src/cli/prompt.d.ts +7 -0
- package/dist/src/cli/prompt.js +40 -0
- package/dist/src/cli/set-user.d.ts +2 -0
- package/dist/src/cli/set-user.js +114 -0
- package/dist/src/cli/setup-mcp.d.ts +10 -0
- package/dist/src/cli/setup-mcp.js +110 -0
- package/dist/src/cli/setup.d.ts +39 -0
- package/dist/src/cli/setup.js +626 -0
- package/dist/src/cli/watch.d.ts +4 -0
- package/dist/src/cli/watch.js +62 -0
- package/dist/src/core/accounts.d.ts +129 -0
- package/dist/src/core/accounts.js +284 -0
- package/dist/src/core/agent-tokens.d.ts +31 -0
- package/dist/src/core/agent-tokens.js +120 -0
- package/dist/src/core/agent.d.ts +71 -0
- package/dist/src/core/agent.js +358 -0
- package/dist/src/core/attachments.d.ts +21 -0
- package/dist/src/core/attachments.js +44 -0
- package/dist/src/core/data-dir.d.ts +5 -0
- package/dist/src/core/data-dir.js +11 -0
- package/dist/src/core/db.d.ts +13 -0
- package/dist/src/core/db.js +38 -0
- package/dist/src/core/env-file.d.ts +11 -0
- package/dist/src/core/env-file.js +36 -0
- package/dist/src/core/errors.d.ts +24 -0
- package/dist/src/core/errors.js +42 -0
- package/dist/src/core/history.d.ts +34 -0
- package/dist/src/core/history.js +279 -0
- package/dist/src/core/ideas.d.ts +16 -0
- package/dist/src/core/ideas.js +100 -0
- package/dist/src/core/ids.d.ts +2 -0
- package/dist/src/core/ids.js +5 -0
- package/dist/src/core/log-dir.d.ts +2 -0
- package/dist/src/core/log-dir.js +9 -0
- package/dist/src/core/logger.d.ts +14 -0
- package/dist/src/core/logger.js +37 -0
- package/dist/src/core/mail.d.ts +43 -0
- package/dist/src/core/mail.js +44 -0
- package/dist/src/core/mcp-entry.d.ts +20 -0
- package/dist/src/core/mcp-entry.js +15 -0
- package/dist/src/core/migrations.d.ts +18 -0
- package/dist/src/core/migrations.js +404 -0
- package/dist/src/core/native-sqlite.d.ts +2 -0
- package/dist/src/core/native-sqlite.js +23 -0
- package/dist/src/core/notifications.d.ts +46 -0
- package/dist/src/core/notifications.js +173 -0
- package/dist/src/core/project-root.d.ts +2 -0
- package/dist/src/core/project-root.js +13 -0
- package/dist/src/core/requests.d.ts +50 -0
- package/dist/src/core/requests.js +240 -0
- package/dist/src/core/snapshots.d.ts +32 -0
- package/dist/src/core/snapshots.js +35 -0
- package/dist/src/core/spaces.d.ts +66 -0
- package/dist/src/core/spaces.js +252 -0
- package/dist/src/core/store.d.ts +577 -0
- package/dist/src/core/store.js +2119 -0
- package/dist/src/core/task-history.d.ts +7 -0
- package/dist/src/core/task-history.js +41 -0
- package/dist/src/core/tasks.d.ts +58 -0
- package/dist/src/core/tasks.js +389 -0
- package/dist/src/core/test-tasks.d.ts +19 -0
- package/dist/src/core/test-tasks.js +135 -0
- package/dist/src/core/transcription.d.ts +45 -0
- package/dist/src/core/transcription.js +92 -0
- package/dist/src/core/watchers.d.ts +30 -0
- package/dist/src/core/watchers.js +50 -0
- package/dist/src/deploy/env-render.d.ts +26 -0
- package/dist/src/deploy/env-render.js +98 -0
- package/dist/src/deploy/templates.d.ts +38 -0
- package/dist/src/deploy/templates.js +130 -0
- package/dist/src/gui-api.d.ts +37 -0
- package/dist/src/gui-api.js +383 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +110 -0
- package/dist/src/mcp/client.d.ts +38 -0
- package/dist/src/mcp/client.js +106 -0
- package/dist/src/mcp/notifications.d.ts +13 -0
- package/dist/src/mcp/notifications.js +54 -0
- package/dist/src/mcp/start.d.ts +14 -0
- package/dist/src/mcp/start.js +123 -0
- package/dist/src/mcp/tools.d.ts +46 -0
- package/dist/src/mcp/tools.js +348 -0
- package/dist/src/mcp/watch-handoff.d.ts +35 -0
- package/dist/src/mcp/watch-handoff.js +91 -0
- package/dist/src/server/app.d.ts +25 -0
- package/dist/src/server/app.js +305 -0
- package/dist/src/server/config.d.ts +23 -0
- package/dist/src/server/config.js +75 -0
- package/dist/src/server/limits.d.ts +2 -0
- package/dist/src/server/limits.js +4 -0
- package/dist/src/server/login-limiter.d.ts +36 -0
- package/dist/src/server/login-limiter.js +81 -0
- package/dist/src/server/start.d.ts +5 -0
- package/dist/src/server/start.js +88 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +16 -0
- package/gui/dist/assets/index-BVN0agCc.css +1 -0
- package/gui/dist/assets/index-DI1_kr81.js +70 -0
- package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/gui/dist/index.html +13 -0
- package/package.json +66 -4
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Erreur métier : son message dit à l'appelant ce qui ne va pas et quoi faire. */
|
|
2
|
+
export class OrganizerError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "OrganizerError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Entité introuvable. */
|
|
9
|
+
export class NotFoundError extends OrganizerError {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "NotFoundError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Mutation refusée parce qu'elle violerait un invariant du modèle. */
|
|
16
|
+
export class InvariantError extends OrganizerError {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "InvariantError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Requête sans session valide, ou identifiants refusés. */
|
|
23
|
+
export class UnauthorizedError extends OrganizerError {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "UnauthorizedError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Action refusée à l'utilisateur connecté par les règles de droits. */
|
|
30
|
+
export class ForbiddenError extends OrganizerError {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "ForbiddenError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Erreur de configuration : le message suffit, sans pile d'appels. */
|
|
37
|
+
export class ConfigError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "ConfigError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Mutation } from "./store.js";
|
|
2
|
+
import { type AgentAction, type TaskSnapshot } from "../../shared/schemas/entities.schema.js";
|
|
3
|
+
/**
|
|
4
|
+
* Retour à un état antérieur : annulation d'une action de l'agent, après
|
|
5
|
+
* comparaison de l'état courant de l'entité avec celui que l'action a
|
|
6
|
+
* laissé, et restauration des champs d'une version de tâche.
|
|
7
|
+
*/
|
|
8
|
+
/** Champs d'une tâche comparés et restaurés par l'historique. */
|
|
9
|
+
declare const TASK_FIELDS: readonly ["title", "description", "status", "priority", "type", "parentId", "testedTaskId"];
|
|
10
|
+
/**
|
|
11
|
+
* Annule une action de l'agent dans la mutation `tx` (acteur utilisateur).
|
|
12
|
+
* L'état courant de l'entité doit être celui que l'action a laissé ; sinon le
|
|
13
|
+
* refus nomme l'auteur de la modification. Une création est supprimée, une
|
|
14
|
+
* suppression recréée, une modification ramenée à l'état d'avant.
|
|
15
|
+
*/
|
|
16
|
+
export declare function undoAgentAction(tx: Mutation, action: AgentAction): void;
|
|
17
|
+
/** Libellés français des champs restaurables d'une tâche ; « position » est le rang dans une même colonne. */
|
|
18
|
+
export declare const TASK_FIELD_LABELS: Record<(typeof TASK_FIELDS)[number] | "position" | "assigneeIds" | "tagIds", string>;
|
|
19
|
+
/**
|
|
20
|
+
* Champs qui diffèrent entre deux instantanés de tâche, dans l'ordre des
|
|
21
|
+
* libellés. La position ne compte que dans une même colonne : un changement
|
|
22
|
+
* de colonne la change toujours.
|
|
23
|
+
*/
|
|
24
|
+
export declare function changedTaskFields(from: TaskSnapshot, to: TaskSnapshot): (keyof typeof TASK_FIELD_LABELS)[];
|
|
25
|
+
/**
|
|
26
|
+
* Ramène une tâche aux champs de `target` (titre, description, statut,
|
|
27
|
+
* priorité, type, parent, tâche testée, position, assignés, tags) et renvoie
|
|
28
|
+
* les champs changés. La tâche reprend sa position d'alors si elle est
|
|
29
|
+
* libre ; revenue dans une autre colonne où cette position est prise, elle va
|
|
30
|
+
* en fin de colonne ; dans sa colonne, une position prise rend la
|
|
31
|
+
* restauration impossible.
|
|
32
|
+
*/
|
|
33
|
+
export declare function restoreTaskFields(tx: Mutation, taskId: string, target: TaskSnapshot): (keyof typeof TASK_FIELD_LABELS)[];
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { InvariantError, NotFoundError } from "./errors.js";
|
|
2
|
+
import { CheckpointSnapshotSchema, isTracked, snapshotOf, taskIdOfAction } from "./snapshots.js";
|
|
3
|
+
import { ChecklistItemSchema, CommentSchema, IdeaSchema, TaskSnapshotSchema, TestCheckpointSchema, } from "../../shared/schemas/entities.schema.js";
|
|
4
|
+
/**
|
|
5
|
+
* Retour à un état antérieur : annulation d'une action de l'agent, après
|
|
6
|
+
* comparaison de l'état courant de l'entité avec celui que l'action a
|
|
7
|
+
* laissé, et restauration des champs d'une version de tâche.
|
|
8
|
+
*/
|
|
9
|
+
/** Champs d'une tâche comparés et restaurés par l'historique. */
|
|
10
|
+
const TASK_FIELDS = ["title", "description", "status", "priority", "type", "parentId", "testedTaskId"];
|
|
11
|
+
/** Partie d'un instantané qui doit être identique pour qu'une action soit annulable sans écraser une modification. */
|
|
12
|
+
function comparable(entityType, snapshot) {
|
|
13
|
+
switch (entityType) {
|
|
14
|
+
case "task": {
|
|
15
|
+
const task = TaskSnapshotSchema.parse(snapshot);
|
|
16
|
+
return [...TASK_FIELDS.map((field) => task[field]), task.position, [...task.assigneeIds].sort(), [...task.tagIds].sort()];
|
|
17
|
+
}
|
|
18
|
+
case "checklist_item": {
|
|
19
|
+
const item = ChecklistItemSchema.parse(snapshot);
|
|
20
|
+
return [item.text, item.done];
|
|
21
|
+
}
|
|
22
|
+
case "test_checkpoint":
|
|
23
|
+
return [TestCheckpointSchema.parse(snapshot).text];
|
|
24
|
+
case "comment":
|
|
25
|
+
return [CommentSchema.parse(snapshot).body];
|
|
26
|
+
case "idea": {
|
|
27
|
+
const idea = IdeaSchema.parse(snapshot);
|
|
28
|
+
return [idea.text, idea.promotedTaskId, idea.archivedAt !== null];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function sameState(entityType, a, b) {
|
|
33
|
+
return JSON.stringify(comparable(entityType, a)) === JSON.stringify(comparable(entityType, b));
|
|
34
|
+
}
|
|
35
|
+
/** Désignation d'une entité dans les messages, avec l'accord du participe. */
|
|
36
|
+
function describe(store, entityType, snapshot) {
|
|
37
|
+
switch (entityType) {
|
|
38
|
+
case "task": {
|
|
39
|
+
const task = TaskSnapshotSchema.parse(snapshot);
|
|
40
|
+
const space = store.findSpace(task.spaceId);
|
|
41
|
+
return { label: `La tâche ${space ? `${space.keyPrefix}-${task.number}` : `« ${task.title} »`}`, feminine: true };
|
|
42
|
+
}
|
|
43
|
+
case "checklist_item":
|
|
44
|
+
return { label: `L'élément de checklist « ${ChecklistItemSchema.parse(snapshot).text} »`, feminine: false };
|
|
45
|
+
case "test_checkpoint":
|
|
46
|
+
return { label: `Le point de vérification « ${TestCheckpointSchema.parse(snapshot).text} »`, feminine: false };
|
|
47
|
+
case "comment":
|
|
48
|
+
return { label: "Le commentaire de l'agent", feminine: false };
|
|
49
|
+
case "idea":
|
|
50
|
+
return { label: `L'idée « ${excerpt(IdeaSchema.parse(snapshot).text)} »`, feminine: true };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function excerpt(text) {
|
|
54
|
+
const line = text.trim().split(/\r?\n/)[0];
|
|
55
|
+
return line.length > 60 ? `${line.slice(0, 59)}…` : line;
|
|
56
|
+
}
|
|
57
|
+
/** Auteur de la dernière modification d'une entité, pour le refus d'une annulation. */
|
|
58
|
+
function lastModifier(store, entityType, entityId, action) {
|
|
59
|
+
const author = store.lastChangeAuthor(entityType, entityId);
|
|
60
|
+
if (!author)
|
|
61
|
+
return "quelqu'un d'autre";
|
|
62
|
+
if (author.actorType === "agent")
|
|
63
|
+
return author.requestId === action.requestId ? "une action plus récente de l'agent dans ce lot" : "l'agent";
|
|
64
|
+
const user = author.actorId === null ? null : store.findUser(author.actorId);
|
|
65
|
+
return user ? (user.displayName ?? user.email) : "un ancien membre";
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Annule une action de l'agent dans la mutation `tx` (acteur utilisateur).
|
|
69
|
+
* L'état courant de l'entité doit être celui que l'action a laissé ; sinon le
|
|
70
|
+
* refus nomme l'auteur de la modification. Une création est supprimée, une
|
|
71
|
+
* suppression recréée, une modification ramenée à l'état d'avant.
|
|
72
|
+
*/
|
|
73
|
+
export function undoAgentAction(tx, action) {
|
|
74
|
+
if (action.undoneAt !== null)
|
|
75
|
+
throw new InvariantError("Cette action est déjà annulée.");
|
|
76
|
+
if (!isTracked(action.entityType))
|
|
77
|
+
throw new InvariantError(`Une action sur ${action.entityType} ne s'annule pas.`);
|
|
78
|
+
const entityType = action.entityType;
|
|
79
|
+
const store = tx.store;
|
|
80
|
+
const current = snapshotOf(store, entityType, action.entityId);
|
|
81
|
+
if (action.after === null) {
|
|
82
|
+
const { label } = describe(store, entityType, action.before);
|
|
83
|
+
if (current !== null)
|
|
84
|
+
throw new InvariantError(`${label} existe de nouveau : rien à recréer.`);
|
|
85
|
+
recreate(tx, entityType, action.before);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const { label, feminine } = describe(store, entityType, current ?? action.after);
|
|
89
|
+
const agreement = feminine ? "e" : "";
|
|
90
|
+
if (current === null) {
|
|
91
|
+
throw new InvariantError(`${label} a été supprimé${agreement} depuis par ${lastModifier(store, entityType, action.entityId, action)} : annulation impossible.`);
|
|
92
|
+
}
|
|
93
|
+
if (!sameState(entityType, current, action.after)) {
|
|
94
|
+
throw new InvariantError(`${label} a été modifié${agreement} depuis par ${lastModifier(store, entityType, action.entityId, action)} : l'annulation écraserait cette modification.`);
|
|
95
|
+
}
|
|
96
|
+
if (action.before === null)
|
|
97
|
+
remove(tx, entityType, current, label);
|
|
98
|
+
else
|
|
99
|
+
restore(tx, entityType, action.entityId, action.before);
|
|
100
|
+
}
|
|
101
|
+
tx.markAgentActionUndone(action.id, taskIdOfAction(action));
|
|
102
|
+
}
|
|
103
|
+
/** Supprime une entité créée par l'agent ; une tâche qui porte d'autres éléments est refusée. */
|
|
104
|
+
function remove(tx, entityType, current, label) {
|
|
105
|
+
const store = tx.store;
|
|
106
|
+
switch (entityType) {
|
|
107
|
+
case "task": {
|
|
108
|
+
const task = TaskSnapshotSchema.parse(current);
|
|
109
|
+
const dependents = [
|
|
110
|
+
[store.listChildren(task.id).length, "des sous-tâches"],
|
|
111
|
+
[store.listChecklist(task.id).length, "une checklist"],
|
|
112
|
+
[store.listCheckpoints(task.id).length, "des points de vérification"],
|
|
113
|
+
[store.listComments(task.id).length, "des commentaires"],
|
|
114
|
+
[store.listAttachments(task.id).length, "des pièces jointes"],
|
|
115
|
+
];
|
|
116
|
+
const found = dependents.filter(([count]) => count > 0).map(([, what]) => what);
|
|
117
|
+
if (found.length > 0) {
|
|
118
|
+
throw new InvariantError(`${label} porte ${found.join(", ")} : annulez d'abord les actions qui les ont ajoutés, ou supprimez-les.`);
|
|
119
|
+
}
|
|
120
|
+
tx.deleteTask(task.id);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
case "checklist_item":
|
|
124
|
+
tx.deleteChecklistItem(ChecklistItemSchema.parse(current).id);
|
|
125
|
+
return;
|
|
126
|
+
case "test_checkpoint": {
|
|
127
|
+
const checkpoint = CheckpointSnapshotSchema.parse(current);
|
|
128
|
+
if (checkpoint.results.length > 0)
|
|
129
|
+
throw new InvariantError(`${label} a reçu des résultats de test depuis : annulation impossible sans les perdre.`);
|
|
130
|
+
tx.deleteCheckpoint(checkpoint.id);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
case "comment":
|
|
134
|
+
tx.deleteAgentComment(CommentSchema.parse(current).id);
|
|
135
|
+
return;
|
|
136
|
+
case "idea":
|
|
137
|
+
tx.deleteIdea(IdeaSchema.parse(current).id);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Recrée une entité supprimée par l'agent. */
|
|
142
|
+
function recreate(tx, entityType, before) {
|
|
143
|
+
switch (entityType) {
|
|
144
|
+
case "checklist_item": {
|
|
145
|
+
const item = ChecklistItemSchema.parse(before);
|
|
146
|
+
if (!tx.store.findTask(item.taskId))
|
|
147
|
+
throw new InvariantError(`La tâche de l'élément « ${item.text} » n'existe plus : rien à recréer.`);
|
|
148
|
+
tx.restoreChecklistItem(item);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
case "test_checkpoint": {
|
|
152
|
+
const checkpoint = CheckpointSnapshotSchema.parse(before);
|
|
153
|
+
if (!tx.store.findTask(checkpoint.taskId))
|
|
154
|
+
throw new InvariantError(`La tâche du point « ${checkpoint.text} » n'existe plus : rien à recréer.`);
|
|
155
|
+
const { results, ...fields } = checkpoint;
|
|
156
|
+
tx.restoreCheckpoint(fields, results);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
default:
|
|
160
|
+
throw new InvariantError(`La suppression d'un élément de type ${entityType} ne s'annule pas.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Ramène une entité modifiée par l'agent à son état d'avant. */
|
|
164
|
+
function restore(tx, entityType, entityId, before) {
|
|
165
|
+
switch (entityType) {
|
|
166
|
+
case "task":
|
|
167
|
+
restoreTaskFields(tx, entityId, TaskSnapshotSchema.parse(before));
|
|
168
|
+
return;
|
|
169
|
+
case "checklist_item": {
|
|
170
|
+
const item = ChecklistItemSchema.parse(before);
|
|
171
|
+
tx.updateChecklistItem(entityId, { text: item.text, done: item.done });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
case "test_checkpoint":
|
|
175
|
+
tx.updateCheckpoint(entityId, TestCheckpointSchema.parse(before).text);
|
|
176
|
+
return;
|
|
177
|
+
case "idea": {
|
|
178
|
+
const idea = IdeaSchema.parse(before);
|
|
179
|
+
if (idea.promotedTaskId !== null && !tx.store.findTask(idea.promotedTaskId)) {
|
|
180
|
+
throw new InvariantError("La tâche issue de cette idée n'existe plus : annulation impossible.");
|
|
181
|
+
}
|
|
182
|
+
tx.updateIdea(entityId, { text: idea.text, promotedTaskId: idea.promotedTaskId, archived: idea.archivedAt !== null });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
case "comment":
|
|
186
|
+
throw new InvariantError("La modification d'un commentaire ne s'annule pas.");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Libellés français des champs restaurables d'une tâche ; « position » est le rang dans une même colonne. */
|
|
190
|
+
export const TASK_FIELD_LABELS = {
|
|
191
|
+
title: "Titre",
|
|
192
|
+
description: "Description",
|
|
193
|
+
status: "Statut",
|
|
194
|
+
priority: "Priorité",
|
|
195
|
+
type: "Type",
|
|
196
|
+
parentId: "Tâche parente",
|
|
197
|
+
testedTaskId: "Tâche testée",
|
|
198
|
+
position: "Position",
|
|
199
|
+
assigneeIds: "Assignés",
|
|
200
|
+
tagIds: "Tags",
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Champs qui diffèrent entre deux instantanés de tâche, dans l'ordre des
|
|
204
|
+
* libellés. La position ne compte que dans une même colonne : un changement
|
|
205
|
+
* de colonne la change toujours.
|
|
206
|
+
*/
|
|
207
|
+
export function changedTaskFields(from, to) {
|
|
208
|
+
const changed = TASK_FIELDS.filter((field) => from[field] !== to[field]);
|
|
209
|
+
if (from.status === to.status && from.parentId === to.parentId && from.position !== to.position)
|
|
210
|
+
changed.push("position");
|
|
211
|
+
if (JSON.stringify([...from.assigneeIds].sort()) !== JSON.stringify([...to.assigneeIds].sort()))
|
|
212
|
+
changed.push("assigneeIds");
|
|
213
|
+
if (JSON.stringify([...from.tagIds].sort()) !== JSON.stringify([...to.tagIds].sort()))
|
|
214
|
+
changed.push("tagIds");
|
|
215
|
+
return changed;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Ramène une tâche aux champs de `target` (titre, description, statut,
|
|
219
|
+
* priorité, type, parent, tâche testée, position, assignés, tags) et renvoie
|
|
220
|
+
* les champs changés. La tâche reprend sa position d'alors si elle est
|
|
221
|
+
* libre ; revenue dans une autre colonne où cette position est prise, elle va
|
|
222
|
+
* en fin de colonne ; dans sa colonne, une position prise rend la
|
|
223
|
+
* restauration impossible.
|
|
224
|
+
*/
|
|
225
|
+
export function restoreTaskFields(tx, taskId, target) {
|
|
226
|
+
const store = tx.store;
|
|
227
|
+
const current = store.taskSnapshot(taskId);
|
|
228
|
+
const changed = changedTaskFields(current, target);
|
|
229
|
+
const patch = {};
|
|
230
|
+
if (target.title !== current.title)
|
|
231
|
+
patch.title = target.title;
|
|
232
|
+
if (target.description !== current.description)
|
|
233
|
+
patch.description = target.description;
|
|
234
|
+
if (target.status !== current.status)
|
|
235
|
+
patch.status = target.status;
|
|
236
|
+
if (target.priority !== current.priority)
|
|
237
|
+
patch.priority = target.priority;
|
|
238
|
+
if (target.type !== current.type)
|
|
239
|
+
patch.type = target.type;
|
|
240
|
+
if (target.parentId !== current.parentId) {
|
|
241
|
+
if (target.parentId !== null && !store.findTask(target.parentId))
|
|
242
|
+
throw new InvariantError("La tâche parente d'alors n'existe plus : restauration impossible.");
|
|
243
|
+
patch.parentId = target.parentId;
|
|
244
|
+
}
|
|
245
|
+
if (target.testedTaskId !== current.testedTaskId) {
|
|
246
|
+
if (target.testedTaskId !== null && !store.findTask(target.testedTaskId))
|
|
247
|
+
throw new InvariantError("La tâche testée d'alors n'existe plus : restauration impossible.");
|
|
248
|
+
patch.testedTaskId = target.testedTaskId;
|
|
249
|
+
}
|
|
250
|
+
const sameColumn = patch.status === undefined && patch.parentId === undefined;
|
|
251
|
+
if (!sameColumn || target.position !== current.position) {
|
|
252
|
+
const column = store.listColumn(current.spaceId, target.parentId, target.status, taskId);
|
|
253
|
+
if (!column.some((task) => task.position === target.position))
|
|
254
|
+
patch.position = target.position;
|
|
255
|
+
else if (sameColumn)
|
|
256
|
+
throw new InvariantError("La position d'alors est désormais occupée par une autre tâche de la colonne : restauration impossible.");
|
|
257
|
+
}
|
|
258
|
+
if (Object.keys(patch).length > 0)
|
|
259
|
+
tx.updateTask(taskId, patch);
|
|
260
|
+
if (changed.includes("assigneeIds")) {
|
|
261
|
+
for (const userId of target.assigneeIds) {
|
|
262
|
+
if (!store.isMember(current.spaceId, userId)) {
|
|
263
|
+
const user = store.findUser(userId);
|
|
264
|
+
throw new InvariantError(`${user ? (user.displayName ?? user.email) : "Un ancien assigné"} n'est plus membre de l'espace : restauration impossible.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
tx.setAssignees(taskId, target.assigneeIds);
|
|
268
|
+
}
|
|
269
|
+
if (changed.includes("tagIds")) {
|
|
270
|
+
const names = target.tagIds.map((tagId) => {
|
|
271
|
+
const tag = store.findTag(tagId);
|
|
272
|
+
if (!tag)
|
|
273
|
+
throw new NotFoundError(`Un tag de cet état n'existe plus : restauration impossible.`);
|
|
274
|
+
return tag.name;
|
|
275
|
+
});
|
|
276
|
+
tx.setTags(taskId, names);
|
|
277
|
+
}
|
|
278
|
+
return changed;
|
|
279
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
import type { IdeaListResponse, IdeaView, PromoteIdeaRequest } from "../../shared/schemas/ideas-api.schema.js";
|
|
3
|
+
/** Idées de l'espace, de la plus récente à la plus ancienne, avec le nombre d'idées actives. */
|
|
4
|
+
export declare function listIdeas(store: Store, userId: string, spaceId: string): IdeaListResponse;
|
|
5
|
+
/** Ajoute une idée saisie par l'utilisateur. */
|
|
6
|
+
export declare function createIdea(store: Store, userId: string, spaceId: string, text: string): IdeaView;
|
|
7
|
+
/** Archive ou désarchive une idée. */
|
|
8
|
+
export declare function setIdeaArchived(store: Store, userId: string, ideaId: string, archived: boolean): IdeaView;
|
|
9
|
+
/**
|
|
10
|
+
* Promeut une idée en tâche (colonne « À faire ») avec le titre, le type et la
|
|
11
|
+
* priorité choisis ; l'idée devient la description de la tâche et sort de la
|
|
12
|
+
* liste active.
|
|
13
|
+
*/
|
|
14
|
+
export declare function promoteIdea(store: Store, userId: string, ideaId: string, input: PromoteIdeaRequest): IdeaView;
|
|
15
|
+
/** Confie la promotion d'une idée à l'agent : demande `idea_promotion`, dans l'espace de l'idée, avec l'idée pour contexte. */
|
|
16
|
+
export declare function delegatePromotion(store: Store, userId: string, ideaId: string): IdeaView;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { InvariantError, NotFoundError } from "./errors.js";
|
|
2
|
+
import { requireMember } from "./spaces.js";
|
|
3
|
+
/**
|
|
4
|
+
* Boîte à idées d'un espace, pour ses membres : liste, ajout, archivage,
|
|
5
|
+
* promotion manuelle en tâche et promotion confiée à l'agent (demande
|
|
6
|
+
* `idea_promotion` qui porte l'idée).
|
|
7
|
+
*/
|
|
8
|
+
const asUser = (userId) => ({ type: "user", userId });
|
|
9
|
+
/** Idée d'un espace dont `userId` est membre ; un non-membre reçoit la même réponse qu'une idée inexistante. */
|
|
10
|
+
function requireIdea(store, ideaId, userId) {
|
|
11
|
+
const idea = store.findIdea(ideaId);
|
|
12
|
+
if (!idea || !store.isMember(idea.spaceId, userId))
|
|
13
|
+
throw new NotFoundError("Idée introuvable, ou vous n'êtes pas membre de son espace.");
|
|
14
|
+
return idea;
|
|
15
|
+
}
|
|
16
|
+
function ideaView(store, idea, promotion) {
|
|
17
|
+
const author = store.findUser(idea.authorId);
|
|
18
|
+
const task = idea.promotedTaskId === null ? null : store.findTask(idea.promotedTaskId);
|
|
19
|
+
return {
|
|
20
|
+
id: idea.id,
|
|
21
|
+
text: idea.text,
|
|
22
|
+
origin: idea.origin,
|
|
23
|
+
authorId: idea.authorId,
|
|
24
|
+
authorName: author ? (author.displayName ?? author.email) : "Ancien membre",
|
|
25
|
+
createdAt: idea.createdAt,
|
|
26
|
+
archivedAt: idea.archivedAt,
|
|
27
|
+
promotedTask: task ? { id: task.id, key: `${store.getSpace(task.spaceId).keyPrefix}-${task.number}`, title: task.title } : null,
|
|
28
|
+
pendingPromotion: promotion && (promotion.status === "pending" || promotion.status === "processing") ? { requestId: promotion.id, status: promotion.status } : null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Promotion confiée à l'agent encore ouverte sur une idée. */
|
|
32
|
+
function openPromotion(store, idea) {
|
|
33
|
+
return store.listOpenPromotionRequests(idea.spaceId).find((request) => request.contextIdeaId === idea.id) ?? null;
|
|
34
|
+
}
|
|
35
|
+
function viewOf(store, ideaId) {
|
|
36
|
+
const idea = store.getIdea(ideaId);
|
|
37
|
+
return ideaView(store, idea, openPromotion(store, idea));
|
|
38
|
+
}
|
|
39
|
+
/** Vrai pour une idée de la liste active : ni archivée ni promue. */
|
|
40
|
+
function isActive(idea) {
|
|
41
|
+
return idea.archivedAt === null && idea.promotedTaskId === null;
|
|
42
|
+
}
|
|
43
|
+
/** Idée encore à promouvoir : ni archivée, ni déjà promue, ni confiée à l'agent. */
|
|
44
|
+
function requirePromotable(store, idea) {
|
|
45
|
+
if (idea.promotedTaskId !== null)
|
|
46
|
+
throw new InvariantError("Cette idée est déjà promue en tâche.");
|
|
47
|
+
if (idea.archivedAt !== null)
|
|
48
|
+
throw new InvariantError("Cette idée est archivée : désarchivez-la avant de la promouvoir.");
|
|
49
|
+
if (openPromotion(store, idea))
|
|
50
|
+
throw new InvariantError("La promotion de cette idée est déjà confiée à l'agent : annulez sa demande sur la page Agent pour la promouvoir vous-même.");
|
|
51
|
+
}
|
|
52
|
+
/** Idées de l'espace, de la plus récente à la plus ancienne, avec le nombre d'idées actives. */
|
|
53
|
+
export function listIdeas(store, userId, spaceId) {
|
|
54
|
+
requireMember(store, spaceId, userId);
|
|
55
|
+
const promotions = new Map(store.listOpenPromotionRequests(spaceId).map((request) => [request.contextIdeaId, request]));
|
|
56
|
+
const ideas = store.listIdeas(spaceId);
|
|
57
|
+
return {
|
|
58
|
+
ideas: ideas.map((idea) => ideaView(store, idea, promotions.get(idea.id) ?? null)),
|
|
59
|
+
activeCount: ideas.filter(isActive).length,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Ajoute une idée saisie par l'utilisateur. */
|
|
63
|
+
export function createIdea(store, userId, spaceId, text) {
|
|
64
|
+
const idea = store.mutate(asUser(userId), (tx) => {
|
|
65
|
+
requireMember(tx.store, spaceId, userId);
|
|
66
|
+
return tx.createIdea({ spaceId, text, origin: "manual" });
|
|
67
|
+
});
|
|
68
|
+
return viewOf(store, idea.id);
|
|
69
|
+
}
|
|
70
|
+
/** Archive ou désarchive une idée. */
|
|
71
|
+
export function setIdeaArchived(store, userId, ideaId, archived) {
|
|
72
|
+
store.mutate(asUser(userId), (tx) => {
|
|
73
|
+
requireIdea(tx.store, ideaId, userId);
|
|
74
|
+
tx.updateIdea(ideaId, { archived });
|
|
75
|
+
});
|
|
76
|
+
return viewOf(store, ideaId);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Promeut une idée en tâche (colonne « À faire ») avec le titre, le type et la
|
|
80
|
+
* priorité choisis ; l'idée devient la description de la tâche et sort de la
|
|
81
|
+
* liste active.
|
|
82
|
+
*/
|
|
83
|
+
export function promoteIdea(store, userId, ideaId, input) {
|
|
84
|
+
store.mutate(asUser(userId), (tx) => {
|
|
85
|
+
const idea = requireIdea(tx.store, ideaId, userId);
|
|
86
|
+
requirePromotable(tx.store, idea);
|
|
87
|
+
const task = tx.createTask({ spaceId: idea.spaceId, title: input.title, type: input.type, priority: input.priority, description: idea.text });
|
|
88
|
+
tx.updateIdea(idea.id, { promotedTaskId: task.id });
|
|
89
|
+
});
|
|
90
|
+
return viewOf(store, ideaId);
|
|
91
|
+
}
|
|
92
|
+
/** Confie la promotion d'une idée à l'agent : demande `idea_promotion`, dans l'espace de l'idée, avec l'idée pour contexte. */
|
|
93
|
+
export function delegatePromotion(store, userId, ideaId) {
|
|
94
|
+
store.mutate(asUser(userId), (tx) => {
|
|
95
|
+
const idea = requireIdea(tx.store, ideaId, userId);
|
|
96
|
+
requirePromotable(tx.store, idea);
|
|
97
|
+
tx.createAgentRequest({ targetSpaceId: idea.spaceId, kind: "idea_promotion", text: idea.text, contextIdeaId: idea.id });
|
|
98
|
+
});
|
|
99
|
+
return viewOf(store, ideaId);
|
|
100
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveDataDir } from "./data-dir.js";
|
|
3
|
+
/** Répertoire des logs : MCP_LOG_DIR s'il est renseigné, sinon <dataDir>/logs. */
|
|
4
|
+
export function resolveLogDir(rootDir) {
|
|
5
|
+
const configured = process.env.MCP_LOG_DIR?.trim();
|
|
6
|
+
if (configured)
|
|
7
|
+
return path.resolve(configured);
|
|
8
|
+
return path.join(resolveDataDir(rootDir), "logs");
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface Logger {
|
|
2
|
+
debug: (message: string, ...meta: unknown[]) => void;
|
|
3
|
+
info: (message: string, ...meta: unknown[]) => void;
|
|
4
|
+
warn: (message: string, ...meta: unknown[]) => void;
|
|
5
|
+
error: (message: string, ...meta: unknown[]) => void;
|
|
6
|
+
}
|
|
7
|
+
/** Nom du fichier de journal dans le répertoire des journaux. */
|
|
8
|
+
export declare const LOG_FILENAME = "server.log";
|
|
9
|
+
/**
|
|
10
|
+
* Ouvre le fichier de journal de `logDir` pour le processus `label`. Un
|
|
11
|
+
* répertoire impossible à créer est une erreur, levée à l'appelant.
|
|
12
|
+
*/
|
|
13
|
+
export declare function setupLogging(label: string, logDir: string): void;
|
|
14
|
+
export declare function createLogger(scope: string): Logger;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { inspect } from "node:util";
|
|
4
|
+
/** Nom du fichier de journal dans le répertoire des journaux. */
|
|
5
|
+
export const LOG_FILENAME = "server.log";
|
|
6
|
+
let stream = null;
|
|
7
|
+
let processLabel = "mcp-organizer";
|
|
8
|
+
function serialize(value) {
|
|
9
|
+
return typeof value === "string" ? value : inspect(value, { depth: 5, breakLength: 120, compact: true });
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Ouvre le fichier de journal de `logDir` pour le processus `label`. Un
|
|
13
|
+
* répertoire impossible à créer est une erreur, levée à l'appelant.
|
|
14
|
+
*/
|
|
15
|
+
export function setupLogging(label, logDir) {
|
|
16
|
+
processLabel = label;
|
|
17
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
18
|
+
stream = fs.createWriteStream(path.join(logDir, LOG_FILENAME), { flags: "a", encoding: "utf8" });
|
|
19
|
+
stream.on("error", (err) => {
|
|
20
|
+
stream = null;
|
|
21
|
+
process.stderr.write(`[${processLabel}] Cannot write the log file in ${logDir}: ${err.message}\n`);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function write(level, scope, message, meta) {
|
|
25
|
+
const text = meta.length === 0 ? message : `${message} ${meta.map(serialize).join(" ")}`;
|
|
26
|
+
const line = `[${new Date().toISOString()}] [PID ${process.pid}] [${processLabel}] [${level}] [${scope}] ${text}\n`;
|
|
27
|
+
process.stderr.write(line);
|
|
28
|
+
stream?.write(line);
|
|
29
|
+
}
|
|
30
|
+
export function createLogger(scope) {
|
|
31
|
+
return {
|
|
32
|
+
debug: (message, ...meta) => write("DEBUG", scope, message, meta),
|
|
33
|
+
info: (message, ...meta) => write("INFO", scope, message, meta),
|
|
34
|
+
warn: (message, ...meta) => write("WARN", scope, message, meta),
|
|
35
|
+
error: (message, ...meta) => write("ERROR", scope, message, meta),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Message envoyé par e-mail, en texte brut. */
|
|
2
|
+
export interface MailMessage {
|
|
3
|
+
to: string;
|
|
4
|
+
subject: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
/** Issue d'un envoi : `reason` dit pourquoi l'e-mail n'est pas parti. */
|
|
8
|
+
export type MailOutcome = {
|
|
9
|
+
sent: true;
|
|
10
|
+
} | {
|
|
11
|
+
sent: false;
|
|
12
|
+
reason: string;
|
|
13
|
+
};
|
|
14
|
+
/** Transport d'envoi : celui de nodemailer, ou un transport de test. */
|
|
15
|
+
export interface MailTransport {
|
|
16
|
+
sendMail(message: {
|
|
17
|
+
from: string;
|
|
18
|
+
to: string;
|
|
19
|
+
subject: string;
|
|
20
|
+
text: string;
|
|
21
|
+
}): Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Envoi des e-mails. Sans configuration, l'envoi est désactivé et
|
|
25
|
+
* `disabledReason` dit quoi configurer ; chaque envoi renvoie alors cette raison.
|
|
26
|
+
*/
|
|
27
|
+
export interface Mailer {
|
|
28
|
+
readonly enabled: boolean;
|
|
29
|
+
readonly disabledReason: string | null;
|
|
30
|
+
send(message: MailMessage): Promise<MailOutcome>;
|
|
31
|
+
}
|
|
32
|
+
export interface MailConfig {
|
|
33
|
+
/** URL SMTP (`smtp://utilisateur:motdepasse@hôte:587`, `smtps://…`). */
|
|
34
|
+
smtpUrl?: string;
|
|
35
|
+
/** Expéditeur (`Organizer <organizer@example.com>`). */
|
|
36
|
+
from?: string;
|
|
37
|
+
/** Transport imposé, à la place de celui construit depuis `smtpUrl`. */
|
|
38
|
+
transport?: MailTransport;
|
|
39
|
+
}
|
|
40
|
+
/** Construit le mailer depuis la configuration (`SMTP_URL`, `MAIL_FROM`). */
|
|
41
|
+
export declare function createMailer(config: MailConfig): Mailer;
|
|
42
|
+
/** Mailer construit depuis les variables d'environnement `SMTP_URL` et `MAIL_FROM`. */
|
|
43
|
+
export declare function mailerFromEnv(env?: NodeJS.ProcessEnv): Mailer;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import nodemailer from "nodemailer";
|
|
2
|
+
/** Mailer désactivé : chaque envoi renvoie `reason`. */
|
|
3
|
+
function disabledMailer(reason) {
|
|
4
|
+
return {
|
|
5
|
+
enabled: false,
|
|
6
|
+
disabledReason: reason,
|
|
7
|
+
send: async () => ({ sent: false, reason }),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** Construit le mailer depuis la configuration (`SMTP_URL`, `MAIL_FROM`). */
|
|
11
|
+
export function createMailer(config) {
|
|
12
|
+
const smtpUrl = config.smtpUrl?.trim();
|
|
13
|
+
const from = config.from?.trim();
|
|
14
|
+
if (!config.transport && !smtpUrl) {
|
|
15
|
+
return disabledMailer("Envoi d'e-mail désactivé : SMTP_URL n'est pas défini dans la configuration du site.");
|
|
16
|
+
}
|
|
17
|
+
if (!from) {
|
|
18
|
+
return disabledMailer("Envoi d'e-mail désactivé : MAIL_FROM (adresse de l'expéditeur) n'est pas défini dans la configuration du site.");
|
|
19
|
+
}
|
|
20
|
+
let transport;
|
|
21
|
+
try {
|
|
22
|
+
transport = config.transport ?? nodemailer.createTransport(smtpUrl);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return disabledMailer(`Envoi d'e-mail désactivé : SMTP_URL est invalide (${err?.message ?? err}).`);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
enabled: true,
|
|
29
|
+
disabledReason: null,
|
|
30
|
+
send: async (message) => {
|
|
31
|
+
try {
|
|
32
|
+
await transport.sendMail({ from, to: message.to, subject: message.subject, text: message.text });
|
|
33
|
+
return { sent: true };
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return { sent: false, reason: `L'envoi de l'e-mail a échoué : ${err?.message ?? err}` };
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Mailer construit depuis les variables d'environnement `SMTP_URL` et `MAIL_FROM`. */
|
|
42
|
+
export function mailerFromEnv(env = process.env) {
|
|
43
|
+
return createMailer({ smtpUrl: env.SMTP_URL, from: env.MAIL_FROM });
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entrée `mcp-organizer` d'un fichier `.mcp.json` : le mode MCP lancé par
|
|
3
|
+
* `npx`, relié à un site par son URL publique et un jeton d'agent. Le setup
|
|
4
|
+
* l'écrit ; la création d'un jeton dans la GUI la renvoie à copier.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SERVER_NAME = "mcp-organizer";
|
|
7
|
+
export declare const PACKAGE_NAME = "@imenam/mcp-organizer";
|
|
8
|
+
/** Entrée d'un serveur dans `.mcp.json`. */
|
|
9
|
+
export interface McpEntry {
|
|
10
|
+
command: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
env: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
/** Entrée du mode MCP relié à un site : URL publique du site et jeton d'agent. */
|
|
15
|
+
export declare function buildAgentEntry(input: {
|
|
16
|
+
url: string;
|
|
17
|
+
token: string;
|
|
18
|
+
}): McpEntry;
|
|
19
|
+
/** Texte à coller dans `.mcp.json` : l'entrée sous `mcpServers`. */
|
|
20
|
+
export declare function mcpJsonText(entry: McpEntry): string;
|