@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.
Files changed (165) hide show
  1. package/README.md +548 -3
  2. package/dist/shared/logic/first-account.d.ts +13 -0
  3. package/dist/shared/logic/first-account.js +15 -0
  4. package/dist/shared/logic/labels.d.ts +5 -0
  5. package/dist/shared/logic/labels.js +17 -0
  6. package/dist/shared/logic/positions.d.ts +9 -0
  7. package/dist/shared/logic/positions.js +19 -0
  8. package/dist/shared/logic/spaces.d.ts +13 -0
  9. package/dist/shared/logic/spaces.js +48 -0
  10. package/dist/shared/logic/test-summary.d.ts +28 -0
  11. package/dist/shared/logic/test-summary.js +35 -0
  12. package/dist/shared/schemas/agent-api.schema.d.ts +269 -0
  13. package/dist/shared/schemas/agent-api.schema.js +97 -0
  14. package/dist/shared/schemas/agent-tools.schema.d.ts +818 -0
  15. package/dist/shared/schemas/agent-tools.schema.js +198 -0
  16. package/dist/shared/schemas/api.schema.d.ts +249 -0
  17. package/dist/shared/schemas/api.schema.js +116 -0
  18. package/dist/shared/schemas/entities.schema.d.ts +472 -0
  19. package/dist/shared/schemas/entities.schema.js +262 -0
  20. package/dist/shared/schemas/enums.schema.d.ts +135 -0
  21. package/dist/shared/schemas/enums.schema.js +58 -0
  22. package/dist/shared/schemas/gui-api.schema.d.ts +53 -0
  23. package/dist/shared/schemas/gui-api.schema.js +20 -0
  24. package/dist/shared/schemas/ideas-api.schema.d.ts +81 -0
  25. package/dist/shared/schemas/ideas-api.schema.js +40 -0
  26. package/dist/shared/schemas/notifications-api.schema.d.ts +87 -0
  27. package/dist/shared/schemas/notifications-api.schema.js +58 -0
  28. package/dist/shared/schemas/tasks-api.schema.d.ts +675 -0
  29. package/dist/shared/schemas/tasks-api.schema.js +225 -0
  30. package/dist/shared/schemas/tokens-api.schema.d.ts +106 -0
  31. package/dist/shared/schemas/tokens-api.schema.js +42 -0
  32. package/dist/shared/schemas/transcription-api.schema.d.ts +15 -0
  33. package/dist/shared/schemas/transcription-api.schema.js +12 -0
  34. package/dist/src/agent-api/checklist.d.ts +3 -0
  35. package/dist/src/agent-api/checklist.js +127 -0
  36. package/dist/src/agent-api/ideas.d.ts +3 -0
  37. package/dist/src/agent-api/ideas.js +93 -0
  38. package/dist/src/agent-api/notifications.d.ts +16 -0
  39. package/dist/src/agent-api/notifications.js +91 -0
  40. package/dist/src/agent-api/requests.d.ts +3 -0
  41. package/dist/src/agent-api/requests.js +60 -0
  42. package/dist/src/agent-api/router.d.ts +3 -0
  43. package/dist/src/agent-api/router.js +41 -0
  44. package/dist/src/agent-api/tasks.d.ts +2 -0
  45. package/dist/src/agent-api/tasks.js +290 -0
  46. package/dist/src/agent-api/types.d.ts +41 -0
  47. package/dist/src/agent-api/types.js +14 -0
  48. package/dist/src/cli/backup.d.ts +12 -0
  49. package/dist/src/cli/backup.js +126 -0
  50. package/dist/src/cli/prompt.d.ts +7 -0
  51. package/dist/src/cli/prompt.js +40 -0
  52. package/dist/src/cli/set-user.d.ts +2 -0
  53. package/dist/src/cli/set-user.js +114 -0
  54. package/dist/src/cli/setup-mcp.d.ts +10 -0
  55. package/dist/src/cli/setup-mcp.js +110 -0
  56. package/dist/src/cli/setup.d.ts +39 -0
  57. package/dist/src/cli/setup.js +626 -0
  58. package/dist/src/cli/watch.d.ts +4 -0
  59. package/dist/src/cli/watch.js +62 -0
  60. package/dist/src/core/accounts.d.ts +129 -0
  61. package/dist/src/core/accounts.js +284 -0
  62. package/dist/src/core/agent-tokens.d.ts +31 -0
  63. package/dist/src/core/agent-tokens.js +120 -0
  64. package/dist/src/core/agent.d.ts +71 -0
  65. package/dist/src/core/agent.js +358 -0
  66. package/dist/src/core/attachments.d.ts +21 -0
  67. package/dist/src/core/attachments.js +44 -0
  68. package/dist/src/core/data-dir.d.ts +5 -0
  69. package/dist/src/core/data-dir.js +11 -0
  70. package/dist/src/core/db.d.ts +13 -0
  71. package/dist/src/core/db.js +38 -0
  72. package/dist/src/core/env-file.d.ts +11 -0
  73. package/dist/src/core/env-file.js +36 -0
  74. package/dist/src/core/errors.d.ts +24 -0
  75. package/dist/src/core/errors.js +42 -0
  76. package/dist/src/core/history.d.ts +34 -0
  77. package/dist/src/core/history.js +279 -0
  78. package/dist/src/core/ideas.d.ts +16 -0
  79. package/dist/src/core/ideas.js +100 -0
  80. package/dist/src/core/ids.d.ts +2 -0
  81. package/dist/src/core/ids.js +5 -0
  82. package/dist/src/core/log-dir.d.ts +2 -0
  83. package/dist/src/core/log-dir.js +9 -0
  84. package/dist/src/core/logger.d.ts +14 -0
  85. package/dist/src/core/logger.js +37 -0
  86. package/dist/src/core/mail.d.ts +43 -0
  87. package/dist/src/core/mail.js +44 -0
  88. package/dist/src/core/mcp-entry.d.ts +20 -0
  89. package/dist/src/core/mcp-entry.js +15 -0
  90. package/dist/src/core/migrations.d.ts +18 -0
  91. package/dist/src/core/migrations.js +404 -0
  92. package/dist/src/core/native-sqlite.d.ts +2 -0
  93. package/dist/src/core/native-sqlite.js +23 -0
  94. package/dist/src/core/notifications.d.ts +46 -0
  95. package/dist/src/core/notifications.js +173 -0
  96. package/dist/src/core/project-root.d.ts +2 -0
  97. package/dist/src/core/project-root.js +13 -0
  98. package/dist/src/core/requests.d.ts +50 -0
  99. package/dist/src/core/requests.js +240 -0
  100. package/dist/src/core/snapshots.d.ts +32 -0
  101. package/dist/src/core/snapshots.js +35 -0
  102. package/dist/src/core/spaces.d.ts +66 -0
  103. package/dist/src/core/spaces.js +252 -0
  104. package/dist/src/core/store.d.ts +577 -0
  105. package/dist/src/core/store.js +2119 -0
  106. package/dist/src/core/task-history.d.ts +7 -0
  107. package/dist/src/core/task-history.js +41 -0
  108. package/dist/src/core/tasks.d.ts +58 -0
  109. package/dist/src/core/tasks.js +389 -0
  110. package/dist/src/core/test-tasks.d.ts +19 -0
  111. package/dist/src/core/test-tasks.js +135 -0
  112. package/dist/src/core/transcription.d.ts +45 -0
  113. package/dist/src/core/transcription.js +92 -0
  114. package/dist/src/core/watchers.d.ts +30 -0
  115. package/dist/src/core/watchers.js +50 -0
  116. package/dist/src/deploy/env-render.d.ts +26 -0
  117. package/dist/src/deploy/env-render.js +98 -0
  118. package/dist/src/deploy/templates.d.ts +38 -0
  119. package/dist/src/deploy/templates.js +130 -0
  120. package/dist/src/gui-api.d.ts +37 -0
  121. package/dist/src/gui-api.js +383 -0
  122. package/dist/src/index.d.ts +2 -0
  123. package/dist/src/index.js +110 -0
  124. package/dist/src/mcp/client.d.ts +38 -0
  125. package/dist/src/mcp/client.js +106 -0
  126. package/dist/src/mcp/notifications.d.ts +13 -0
  127. package/dist/src/mcp/notifications.js +54 -0
  128. package/dist/src/mcp/start.d.ts +14 -0
  129. package/dist/src/mcp/start.js +123 -0
  130. package/dist/src/mcp/tools.d.ts +46 -0
  131. package/dist/src/mcp/tools.js +348 -0
  132. package/dist/src/mcp/watch-handoff.d.ts +35 -0
  133. package/dist/src/mcp/watch-handoff.js +91 -0
  134. package/dist/src/server/app.d.ts +25 -0
  135. package/dist/src/server/app.js +305 -0
  136. package/dist/src/server/config.d.ts +23 -0
  137. package/dist/src/server/config.js +75 -0
  138. package/dist/src/server/limits.d.ts +2 -0
  139. package/dist/src/server/limits.js +4 -0
  140. package/dist/src/server/login-limiter.d.ts +36 -0
  141. package/dist/src/server/login-limiter.js +81 -0
  142. package/dist/src/server/start.d.ts +5 -0
  143. package/dist/src/server/start.js +88 -0
  144. package/dist/src/version.d.ts +1 -0
  145. package/dist/src/version.js +16 -0
  146. package/gui/dist/assets/index-BVN0agCc.css +1 -0
  147. package/gui/dist/assets/index-DI1_kr81.js +70 -0
  148. package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  149. package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  150. package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  151. package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  152. package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  153. package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  154. package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  155. package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  156. package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  157. package/gui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  158. package/gui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  159. package/gui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  160. package/gui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  161. package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  162. package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  163. package/gui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  164. package/gui/dist/index.html +13 -0
  165. package/package.json +66 -4
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Dictée vocale : transcription d'un enregistrement par l'API Groq
3
+ * (compatible OpenAI), `POST <base>/audio/transcriptions` en multipart
4
+ * (`file`, `model`, `language=fr`, `response_format=json`). Sans clé ou sans
5
+ * modèle configuré, la transcription est désactivée et `disabledReason` dit
6
+ * quoi configurer.
7
+ */
8
+ /** URL de base de l'API Groq. */
9
+ export declare const GROQ_BASE_URL = "https://api.groq.com/openai/v1";
10
+ /**
11
+ * Variable réservée aux tests : remplace l'URL de base de l'API Groq par celle
12
+ * d'un serveur simulé. Elle ne fait pas partie de la configuration d'une installation.
13
+ */
14
+ export declare const TEST_BASE_URL_VARIABLE = "ORGANIZER_TEST_GROQ_BASE_URL";
15
+ /** Délai maximal d'une transcription par Groq. */
16
+ export declare const TRANSCRIPTION_TIMEOUT_MS = 90000;
17
+ /** Échec d'une transcription, avec le code HTTP à renvoyer au navigateur (celui de Groq quand Groq refuse). */
18
+ export declare class TranscriptionError extends Error {
19
+ readonly status: number;
20
+ constructor(status: number, message: string);
21
+ }
22
+ /** Enregistrement à transcrire. */
23
+ export interface Recording {
24
+ content: Buffer;
25
+ filename: string;
26
+ mime: string;
27
+ }
28
+ export interface Transcriber {
29
+ readonly enabled: boolean;
30
+ readonly disabledReason: string | null;
31
+ /** Texte transcrit ; lève `TranscriptionError`. */
32
+ transcribe(recording: Recording): Promise<string>;
33
+ }
34
+ export interface TranscriptionConfig {
35
+ /** Clé de l'API Groq (`GROQ_API_KEY`). */
36
+ apiKey?: string;
37
+ /** Modèle de transcription (`ORGANIZER_TRANSCRIBE_MODEL`). */
38
+ model?: string;
39
+ /** URL de base de l'API ; celle de Groq par défaut. */
40
+ baseUrl?: string;
41
+ timeoutMs?: number;
42
+ }
43
+ export declare function createTranscriber(config: TranscriptionConfig): Transcriber;
44
+ /** Transcription configurée par `GROQ_API_KEY` et `ORGANIZER_TRANSCRIBE_MODEL`. */
45
+ export declare function transcriberFromEnv(env?: NodeJS.ProcessEnv): Transcriber;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Dictée vocale : transcription d'un enregistrement par l'API Groq
3
+ * (compatible OpenAI), `POST <base>/audio/transcriptions` en multipart
4
+ * (`file`, `model`, `language=fr`, `response_format=json`). Sans clé ou sans
5
+ * modèle configuré, la transcription est désactivée et `disabledReason` dit
6
+ * quoi configurer.
7
+ */
8
+ /** URL de base de l'API Groq. */
9
+ export const GROQ_BASE_URL = "https://api.groq.com/openai/v1";
10
+ /**
11
+ * Variable réservée aux tests : remplace l'URL de base de l'API Groq par celle
12
+ * d'un serveur simulé. Elle ne fait pas partie de la configuration d'une installation.
13
+ */
14
+ export const TEST_BASE_URL_VARIABLE = "ORGANIZER_TEST_GROQ_BASE_URL";
15
+ /** Délai maximal d'une transcription par Groq. */
16
+ export const TRANSCRIPTION_TIMEOUT_MS = 90_000;
17
+ /** Échec d'une transcription, avec le code HTTP à renvoyer au navigateur (celui de Groq quand Groq refuse). */
18
+ export class TranscriptionError extends Error {
19
+ status;
20
+ constructor(status, message) {
21
+ super(message);
22
+ this.status = status;
23
+ this.name = "TranscriptionError";
24
+ }
25
+ }
26
+ function disabledTranscriber(reason) {
27
+ return {
28
+ enabled: false,
29
+ disabledReason: reason,
30
+ transcribe: async () => {
31
+ throw new TranscriptionError(503, reason);
32
+ },
33
+ };
34
+ }
35
+ /** Message d'erreur de Groq : `error.message` de son corps JSON, sinon le corps brut. */
36
+ function groqMessage(body) {
37
+ try {
38
+ const parsed = JSON.parse(body);
39
+ if (typeof parsed.error?.message === "string")
40
+ return parsed.error.message;
41
+ }
42
+ catch {
43
+ // Corps non JSON : renvoyé tel quel.
44
+ }
45
+ return body.trim() || "réponse vide";
46
+ }
47
+ export function createTranscriber(config) {
48
+ const apiKey = config.apiKey?.trim();
49
+ const model = config.model?.trim();
50
+ if (!apiKey)
51
+ return disabledTranscriber("Dictée désactivée : GROQ_API_KEY n'est pas défini dans la configuration du site.");
52
+ if (!model)
53
+ return disabledTranscriber("Dictée désactivée : ORGANIZER_TRANSCRIBE_MODEL (modèle de transcription Groq) n'est pas défini dans la configuration du site.");
54
+ const endpoint = `${(config.baseUrl?.trim() || GROQ_BASE_URL).replace(/\/+$/, "")}/audio/transcriptions`;
55
+ const timeoutMs = config.timeoutMs ?? TRANSCRIPTION_TIMEOUT_MS;
56
+ return {
57
+ enabled: true,
58
+ disabledReason: null,
59
+ transcribe: async (recording) => {
60
+ const form = new FormData();
61
+ form.append("file", new Blob([new Uint8Array(recording.content)], { type: recording.mime }), recording.filename);
62
+ form.append("model", model);
63
+ form.append("language", "fr");
64
+ form.append("response_format", "json");
65
+ let response;
66
+ try {
67
+ response = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${apiKey}` }, body: form, signal: AbortSignal.timeout(timeoutMs) });
68
+ }
69
+ catch (err) {
70
+ const reason = err?.name === "TimeoutError" ? `pas de réponse en ${Math.round(timeoutMs / 1000)} s` : (err?.cause?.message ?? err?.message ?? String(err));
71
+ throw new TranscriptionError(502, `L'API de transcription Groq est injoignable : ${reason}.`);
72
+ }
73
+ const body = await response.text();
74
+ if (!response.ok)
75
+ throw new TranscriptionError(response.status, `Groq a refusé la transcription (HTTP ${response.status}) : ${groqMessage(body)}`);
76
+ let text;
77
+ try {
78
+ text = JSON.parse(body).text;
79
+ }
80
+ catch {
81
+ text = undefined;
82
+ }
83
+ if (typeof text !== "string")
84
+ throw new TranscriptionError(502, `Réponse inattendue de l'API Groq : ${body.slice(0, 200)}`);
85
+ return text.trim();
86
+ },
87
+ };
88
+ }
89
+ /** Transcription configurée par `GROQ_API_KEY` et `ORGANIZER_TRANSCRIBE_MODEL`. */
90
+ export function transcriberFromEnv(env = process.env) {
91
+ return createTranscriber({ apiKey: env.GROQ_API_KEY, model: env.ORGANIZER_TRANSCRIBE_MODEL, baseUrl: env[TEST_BASE_URL_VARIABLE] });
92
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Présence des veilleurs de notifications, par jeton d'agent.
3
+ *
4
+ * Un veilleur enchaîne des attentes longues sur `GET /api/agent/requests/wait`.
5
+ * Le registre rend sa présence observable : `enable_notifications` répond
6
+ * « déjà armé », la page Agent de la GUI affiche « à l'écoute ».
7
+ *
8
+ * État de connexion, donc volatile : il vit en mémoire du processus du site et
9
+ * disparaît avec lui.
10
+ */
11
+ /**
12
+ * Délai de grâce entre deux attentes d'un même veilleur : le jeton reste
13
+ * « à l'écoute » pendant le court intervalle où aucune attente n'est ouverte.
14
+ */
15
+ export declare const WATCHER_GRACE_MS: number;
16
+ export interface WatcherStatus {
17
+ armed: boolean;
18
+ /** Début de la série d'attentes en cours, null sans veilleur. */
19
+ since: string | null;
20
+ /** Dernière activité connue d'un veilleur de ce jeton, null si aucune. */
21
+ lastSeen: string | null;
22
+ }
23
+ export declare class WatcherRegistry {
24
+ private readonly now;
25
+ private readonly presence;
26
+ constructor(now?: () => number);
27
+ /** Déclare une attente de veilleur du jeton `tokenId`. La fonction rendue la clôt ; elle doit toujours être appelée. */
28
+ begin(tokenId: string): () => void;
29
+ status(tokenId: string): WatcherStatus;
30
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Présence des veilleurs de notifications, par jeton d'agent.
3
+ *
4
+ * Un veilleur enchaîne des attentes longues sur `GET /api/agent/requests/wait`.
5
+ * Le registre rend sa présence observable : `enable_notifications` répond
6
+ * « déjà armé », la page Agent de la GUI affiche « à l'écoute ».
7
+ *
8
+ * État de connexion, donc volatile : il vit en mémoire du processus du site et
9
+ * disparaît avec lui.
10
+ */
11
+ /**
12
+ * Délai de grâce entre deux attentes d'un même veilleur : le jeton reste
13
+ * « à l'écoute » pendant le court intervalle où aucune attente n'est ouverte.
14
+ */
15
+ export const WATCHER_GRACE_MS = 2 * 60 * 1000;
16
+ export class WatcherRegistry {
17
+ now;
18
+ presence = new Map();
19
+ constructor(now = Date.now) {
20
+ this.now = now;
21
+ }
22
+ /** Déclare une attente de veilleur du jeton `tokenId`. La fonction rendue la clôt ; elle doit toujours être appelée. */
23
+ begin(tokenId) {
24
+ const now = this.now();
25
+ const current = this.presence.get(tokenId);
26
+ const continuing = current !== undefined && (current.inflight > 0 || now - current.lastSeen <= WATCHER_GRACE_MS);
27
+ this.presence.set(tokenId, continuing ? { ...current, inflight: current.inflight + 1, lastSeen: now } : { inflight: 1, since: now, lastSeen: now });
28
+ let released = false;
29
+ return () => {
30
+ if (released)
31
+ return;
32
+ released = true;
33
+ const active = this.presence.get(tokenId);
34
+ if (!active)
35
+ return;
36
+ this.presence.set(tokenId, { ...active, inflight: Math.max(0, active.inflight - 1), lastSeen: this.now() });
37
+ };
38
+ }
39
+ status(tokenId) {
40
+ const entry = this.presence.get(tokenId);
41
+ if (!entry)
42
+ return { armed: false, since: null, lastSeen: null };
43
+ const armed = entry.inflight > 0 || this.now() - entry.lastSeen <= WATCHER_GRACE_MS;
44
+ if (!armed) {
45
+ this.presence.delete(tokenId);
46
+ return { armed: false, since: null, lastSeen: new Date(entry.lastSeen).toISOString() };
47
+ }
48
+ return { armed: true, since: new Date(entry.since).toISOString(), lastSeen: new Date(entry.lastSeen).toISOString() };
49
+ }
50
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Écriture du fichier `.env` du site par `mcp-organizer --setup`.
3
+ *
4
+ * Un fichier existant est fusionné ligne à ligne : les commentaires et les
5
+ * variables que le setup ne gère pas restent tels quels, chaque variable gérée
6
+ * est réécrite à sa place, une variable gérée sans valeur est retirée, et les
7
+ * variables gérées absentes du fichier sont ajoutées à la fin.
8
+ */
9
+ /** Variables écrites par le setup, dans l'ordre d'un fichier neuf. */
10
+ export declare const MANAGED_ENV_KEYS: readonly ["ORGANIZER_PUBLIC_URL", "ORGANIZER_BASE_PATH", "PORT", "HOST", "ORGANIZER_TRUST_PROXY", "MCP_ORGANIZER_DATA_DIR", "GROQ_API_KEY", "ORGANIZER_TRANSCRIBE_MODEL", "SMTP_URL", "MAIL_FROM"];
11
+ export type ManagedEnvKey = (typeof MANAGED_ENV_KEYS)[number];
12
+ /**
13
+ * Variables d'amorçage du premier compte : le setup crée le compte en base et
14
+ * les retire du fichier, où le mot de passe resterait en clair.
15
+ */
16
+ export declare const REMOVED_ENV_KEYS: readonly ["ORGANIZER_ADMIN_EMAIL", "ORGANIZER_ADMIN_NAME", "ORGANIZER_ADMIN_PASSWORD"];
17
+ /** Valeur de chaque variable gérée ; `null` retire la variable du fichier. */
18
+ export type ManagedEnvValues = Record<ManagedEnvKey, string | null>;
19
+ /**
20
+ * Valeur écrite telle que `dotenv` la relit : brute si elle ne contient que
21
+ * des caractères sûrs, sinon entre guillemets simples (aucune interprétation),
22
+ * ou doubles si elle contient une apostrophe.
23
+ */
24
+ export declare function formatEnvValue(key: string, value: string): string;
25
+ /** Contenu du fichier `.env` : `existing` fusionné avec `values`, ou un fichier neuf. */
26
+ export declare function renderEnvFile(existing: string | undefined, values: ManagedEnvValues): string;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Écriture du fichier `.env` du site par `mcp-organizer --setup`.
3
+ *
4
+ * Un fichier existant est fusionné ligne à ligne : les commentaires et les
5
+ * variables que le setup ne gère pas restent tels quels, chaque variable gérée
6
+ * est réécrite à sa place, une variable gérée sans valeur est retirée, et les
7
+ * variables gérées absentes du fichier sont ajoutées à la fin.
8
+ */
9
+ /** Variables écrites par le setup, dans l'ordre d'un fichier neuf. */
10
+ export const MANAGED_ENV_KEYS = [
11
+ "ORGANIZER_PUBLIC_URL",
12
+ "ORGANIZER_BASE_PATH",
13
+ "PORT",
14
+ "HOST",
15
+ "ORGANIZER_TRUST_PROXY",
16
+ "MCP_ORGANIZER_DATA_DIR",
17
+ "GROQ_API_KEY",
18
+ "ORGANIZER_TRANSCRIBE_MODEL",
19
+ "SMTP_URL",
20
+ "MAIL_FROM",
21
+ ];
22
+ /**
23
+ * Variables d'amorçage du premier compte : le setup crée le compte en base et
24
+ * les retire du fichier, où le mot de passe resterait en clair.
25
+ */
26
+ export const REMOVED_ENV_KEYS = ["ORGANIZER_ADMIN_EMAIL", "ORGANIZER_ADMIN_NAME", "ORGANIZER_ADMIN_PASSWORD"];
27
+ const KEY_LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
28
+ const PLAIN_VALUE = /^[A-Za-z0-9_\-.:/@+,=%~]*$/;
29
+ /**
30
+ * Valeur écrite telle que `dotenv` la relit : brute si elle ne contient que
31
+ * des caractères sûrs, sinon entre guillemets simples (aucune interprétation),
32
+ * ou doubles si elle contient une apostrophe.
33
+ */
34
+ export function formatEnvValue(key, value) {
35
+ if (/[\r\n]/.test(value))
36
+ throw new Error(`${key} : une valeur sur plusieurs lignes n'est pas admise.`);
37
+ if (PLAIN_VALUE.test(value))
38
+ return value;
39
+ if (!value.includes("'"))
40
+ return `'${value}'`;
41
+ if (!value.includes('"') && !value.includes("\\"))
42
+ return `"${value}"`;
43
+ throw new Error(`${key} : la valeur contient à la fois une apostrophe et un guillemet ou une barre oblique inverse ; elle ne peut pas s'écrire dans un fichier .env.`);
44
+ }
45
+ const HEADER = [
46
+ "# Configuration du site mcp-organizer, écrite par « mcp-organizer --setup ».",
47
+ "# Relancer le setup réécrit les variables qu'il gère et conserve les autres.",
48
+ "# Ce fichier contient des secrets : il n'est lisible que par son propriétaire.",
49
+ ];
50
+ const COMMENTS = {
51
+ ORGANIZER_PUBLIC_URL: "# URL ouverte par les utilisateurs, derrière nginx.",
52
+ PORT: "# Écoute du site ; nginx le joint à cette adresse.",
53
+ MCP_ORGANIZER_DATA_DIR: "# Base SQLite, pièces jointes et journaux.",
54
+ GROQ_API_KEY: "# Dictée vocale (Groq).",
55
+ SMTP_URL: "# Envoi des e-mails (invitations, notifications).",
56
+ };
57
+ function managedLines(keys, values) {
58
+ const lines = [];
59
+ for (const key of keys) {
60
+ const value = values[key];
61
+ if (value === null)
62
+ continue;
63
+ const comment = COMMENTS[key];
64
+ if (comment)
65
+ lines.push("", comment);
66
+ lines.push(`${key}=${formatEnvValue(key, value)}`);
67
+ }
68
+ return lines;
69
+ }
70
+ /** Contenu du fichier `.env` : `existing` fusionné avec `values`, ou un fichier neuf. */
71
+ export function renderEnvFile(existing, values) {
72
+ if (existing === undefined || existing.trim() === "") {
73
+ return [...HEADER, ...managedLines(MANAGED_ENV_KEYS, values), ""].join("\n");
74
+ }
75
+ const managed = new Set(MANAGED_ENV_KEYS);
76
+ const removed = new Set(REMOVED_ENV_KEYS);
77
+ const written = new Set();
78
+ const lines = [];
79
+ for (const line of existing.replace(/\r\n/g, "\n").replace(/\n+$/, "").split("\n")) {
80
+ const key = KEY_LINE.exec(line)?.[1];
81
+ if (key === undefined || (!managed.has(key) && !removed.has(key))) {
82
+ lines.push(line);
83
+ continue;
84
+ }
85
+ if (removed.has(key) || written.has(key))
86
+ continue;
87
+ const value = values[key];
88
+ written.add(key);
89
+ if (value !== null)
90
+ lines.push(`${key}=${formatEnvValue(key, value)}`);
91
+ }
92
+ const missing = MANAGED_ENV_KEYS.filter((key) => !written.has(key) && values[key] !== null);
93
+ if (missing.length > 0) {
94
+ lines.push("", "# Ajouté par « mcp-organizer --setup ».", ...managedLines(missing, values).filter((line) => line !== "" && !line.startsWith("#")));
95
+ }
96
+ lines.push("");
97
+ return lines.join("\n");
98
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Fichiers de déploiement produits par `mcp-organizer --setup`, remplis avec
3
+ * les réponses : l'unité systemd du site et son bloc nginx.
4
+ */
5
+ export interface DeploymentAnswers {
6
+ /** URL publique sans barre oblique finale. */
7
+ publicUrl: string;
8
+ /** Chemin de base, `/` ou `/organizer`. */
9
+ basePath: string;
10
+ host: string;
11
+ port: number;
12
+ /** Répertoire de données, absolu. */
13
+ dataDir: string;
14
+ }
15
+ export interface DeploymentContext {
16
+ /** Racine du dépôt cloné, où se trouve `dist/src/index.js`. */
17
+ installDir: string;
18
+ /** Fichier `.env` passé au site par `--dotenv`. */
19
+ envFilePath: string;
20
+ /** Compte système qui exécute le site. */
21
+ serviceUser: string;
22
+ /** Exécutable Node.js. */
23
+ nodePath: string;
24
+ }
25
+ /** Nom de l'unité systemd et du site nginx. */
26
+ export declare const SERVICE_NAME = "mcp-organizer";
27
+ /** `client_max_body_size` de nginx : la plus grosse requête admise par le site, arrondie au Mo supérieur. */
28
+ export declare function nginxBodyLimit(): string;
29
+ /** Adresse par laquelle nginx joint le site : la boucle locale quand le site écoute sur toutes les interfaces. */
30
+ export declare function upstreamAddress(host: string, port: number): string;
31
+ /** Unité systemd du site : `mcp-organizer --server`, compte dédié, redémarrage, durcissement. */
32
+ export declare function renderSystemdUnit(answers: DeploymentAnswers, context: DeploymentContext): string;
33
+ /**
34
+ * Bloc nginx : HTTPS (redirection du port 80), proxy vers `HOST:PORT` avec les
35
+ * en-têtes `X-Forwarded-*`, taille des corps alignée sur le site, API jamais
36
+ * mise en cache. Publié sous un chemin, le préfixe est retiré avant le site.
37
+ */
38
+ export declare function renderNginx(answers: DeploymentAnswers): string;
@@ -0,0 +1,130 @@
1
+ import net from "node:net";
2
+ import { MAX_BODY_BYTES } from "../server/limits.js";
3
+ /** Nom de l'unité systemd et du site nginx. */
4
+ export const SERVICE_NAME = "mcp-organizer";
5
+ /** `client_max_body_size` de nginx : la plus grosse requête admise par le site, arrondie au Mo supérieur. */
6
+ export function nginxBodyLimit() {
7
+ return `${Math.ceil(MAX_BODY_BYTES / (1024 * 1024))}m`;
8
+ }
9
+ /** Adresse par laquelle nginx joint le site : la boucle locale quand le site écoute sur toutes les interfaces. */
10
+ export function upstreamAddress(host, port) {
11
+ if (host === "0.0.0.0")
12
+ return `127.0.0.1:${port}`;
13
+ if (host === "::")
14
+ return `[::1]:${port}`;
15
+ return net.isIPv6(host) ? `[${host}]:${port}` : `${host}:${port}`;
16
+ }
17
+ /** Argument d'une directive systemd, entre guillemets s'il contient un blanc. */
18
+ function systemdArg(value) {
19
+ if (/[\r\n]/.test(value))
20
+ throw new Error(`Chemin sur plusieurs lignes refusé dans l'unité systemd : ${JSON.stringify(value)}.`);
21
+ return /[\s"'\\]/.test(value) ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
22
+ }
23
+ function entryPoint(installDir) {
24
+ return `${installDir.replace(/[\\/]+$/, "")}/dist/src/index.js`;
25
+ }
26
+ /** Unité systemd du site : `mcp-organizer --server`, compte dédié, redémarrage, durcissement. */
27
+ export function renderSystemdUnit(answers, context) {
28
+ const privilegedPort = answers.port < 1024;
29
+ const capabilities = privilegedPort
30
+ ? ["# Port inférieur à 1024 : seule la capacité de s'y lier est accordée.", "AmbientCapabilities=CAP_NET_BIND_SERVICE", "CapabilityBoundingSet=CAP_NET_BIND_SERVICE"]
31
+ : ["CapabilityBoundingSet="];
32
+ return [
33
+ "# Unité systemd du site mcp-organizer, écrite par « mcp-organizer --setup ».",
34
+ `# Installation : cp ${SERVICE_NAME}.service /etc/systemd/system/ && systemctl daemon-reload && systemctl enable --now ${SERVICE_NAME}`,
35
+ "",
36
+ "[Unit]",
37
+ "Description=mcp-organizer (site web : interface, API, base)",
38
+ `Documentation=file:${context.installDir.replace(/[\\/]+$/, "")}/README.md`,
39
+ "After=network-online.target",
40
+ "Wants=network-online.target",
41
+ "",
42
+ "[Service]",
43
+ "Type=simple",
44
+ `User=${context.serviceUser}`,
45
+ `Group=${context.serviceUser}`,
46
+ `WorkingDirectory=${context.installDir}`,
47
+ "Environment=NODE_ENV=production",
48
+ "# La configuration est lue dans le fichier .env, à chaque démarrage.",
49
+ `ExecStart=${systemdArg(context.nodePath)} ${systemdArg(entryPoint(context.installDir))} --server ${systemdArg(`--dotenv=${context.envFilePath}`)}`,
50
+ "Restart=on-failure",
51
+ "RestartSec=5",
52
+ "TimeoutStopSec=30",
53
+ "UMask=0077",
54
+ "",
55
+ "# Durcissement : le site n'écrit que dans son répertoire de données.",
56
+ "NoNewPrivileges=true",
57
+ "PrivateTmp=true",
58
+ "PrivateDevices=true",
59
+ "ProtectSystem=strict",
60
+ "ProtectHome=read-only",
61
+ `ReadWritePaths=${systemdArg(answers.dataDir)}`,
62
+ "ProtectKernelTunables=true",
63
+ "ProtectKernelModules=true",
64
+ "ProtectKernelLogs=true",
65
+ "ProtectControlGroups=true",
66
+ "ProtectClock=true",
67
+ "ProtectHostname=true",
68
+ "RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX",
69
+ "RestrictNamespaces=true",
70
+ "RestrictRealtime=true",
71
+ "RestrictSUIDSGID=true",
72
+ "LockPersonality=true",
73
+ "SystemCallArchitectures=native",
74
+ ...capabilities,
75
+ "",
76
+ "StandardOutput=journal",
77
+ "StandardError=journal",
78
+ `SyslogIdentifier=${SERVICE_NAME}`,
79
+ "",
80
+ "[Install]",
81
+ "WantedBy=multi-user.target",
82
+ "",
83
+ ].join("\n");
84
+ }
85
+ /** Bloc `location` de l'API : jamais mis en cache, ni par nginx ni par le navigateur. */
86
+ function apiLocation(prefix, upstream, hsts) {
87
+ return [
88
+ ` location ${prefix}api/ {`,
89
+ ` proxy_pass ${upstream};`,
90
+ " proxy_cache off;",
91
+ " proxy_hide_header Cache-Control;",
92
+ ' add_header Cache-Control "no-store" always;',
93
+ ...(hsts ? [' add_header Strict-Transport-Security "max-age=31536000" always;'] : []),
94
+ " }",
95
+ ];
96
+ }
97
+ /**
98
+ * Bloc nginx : HTTPS (redirection du port 80), proxy vers `HOST:PORT` avec les
99
+ * en-têtes `X-Forwarded-*`, taille des corps alignée sur le site, API jamais
100
+ * mise en cache. Publié sous un chemin, le préfixe est retiré avant le site.
101
+ */
102
+ export function renderNginx(answers) {
103
+ const url = new URL(answers.publicUrl);
104
+ const https = url.protocol === "https:";
105
+ const serverName = url.hostname.replace(/^\[|\]$/g, "");
106
+ const upstreamHost = `http://${upstreamAddress(answers.host, answers.port)}`;
107
+ const prefix = answers.basePath === "/" ? "/" : `${answers.basePath}/`;
108
+ // proxy_pass avec un chemin retire le préfixe publié ; sans chemin, l'URI est transmise telle quelle.
109
+ const upstream = answers.basePath === "/" ? upstreamHost : `${upstreamHost}/`;
110
+ const apiUpstream = answers.basePath === "/" ? upstreamHost : `${upstreamHost}/api/`;
111
+ const lines = [
112
+ `# Bloc nginx du site mcp-organizer (${answers.publicUrl}), écrit par « mcp-organizer --setup ».`,
113
+ `# Le site écoute sur ${upstreamAddress(answers.host, answers.port)} ; nginx seul est exposé.`,
114
+ ...(answers.basePath === "/" ? [] : ["# Les blocs location peuvent aussi être placés dans le server existant de ce nom d'hôte."]),
115
+ "",
116
+ ];
117
+ if (https) {
118
+ lines.push("server {", " listen 80;", " listen [::]:80;", ` server_name ${serverName};`, " return 301 https://$host$request_uri;", "}", "", "server {",
119
+ // « listen … http2 » est accepté depuis nginx 1.9.5 (avec un avertissement à partir de 1.25.1) ; « http2 on; » exige 1.25.1.
120
+ " listen 443 ssl http2;", " listen [::]:443 ssl http2;", ` server_name ${serverName};`, "", " # Certificat Let's Encrypt (certbot certonly --nginx -d " + serverName + ").", ` ssl_certificate /etc/letsencrypt/live/${serverName}/fullchain.pem;`, ` ssl_certificate_key /etc/letsencrypt/live/${serverName}/privkey.pem;`, ' add_header Strict-Transport-Security "max-age=31536000" always;');
121
+ }
122
+ else {
123
+ lines.push("# URL publique en http : réservé à un essai local, sans TLS.", "server {", " listen 80;", ` server_name ${serverName};`);
124
+ }
125
+ lines.push("", " # Pièces jointes (20 Mo) et dictée (25 Mo), enveloppe du formulaire comprise.", ` client_max_body_size ${nginxBodyLimit()};`, "", " proxy_http_version 1.1;", " proxy_set_header Host $host;", " proxy_set_header X-Real-IP $remote_addr;", " proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;", " proxy_set_header X-Forwarded-Proto $scheme;", " proxy_set_header X-Forwarded-Host $host;", " # La dictée attend la réponse de Groq.", " proxy_read_timeout 120s;", "");
126
+ if (answers.basePath !== "/")
127
+ lines.push(` location = ${answers.basePath} {`, ` return 301 ${prefix};`, " }", "");
128
+ lines.push(...apiLocation(prefix, apiUpstream, https), "", ` location ${prefix} {`, ` proxy_pass ${upstream};`, " }", "}", "");
129
+ return lines.join("\n");
130
+ }
@@ -0,0 +1,37 @@
1
+ import { type ApiRequestData, type ApiResponseData, type ApiErrorKind } from "../shared/schemas/gui-api.schema.js";
2
+ import type { AttachmentStorage } from "./core/attachments.js";
3
+ import type { Mailer } from "./core/mail.js";
4
+ import type { Store } from "./core/store.js";
5
+ import { type Transcriber } from "./core/transcription.js";
6
+ /** Erreur d'une requête de la GUI, avec la nature qui fixe son code HTTP. */
7
+ export declare class ApiError extends Error {
8
+ readonly kind: ApiErrorKind;
9
+ /** Code HTTP imposé à la réponse, à la place de celui de `kind`. */
10
+ readonly status?: number | undefined;
11
+ constructor(kind: ApiErrorKind, message: string,
12
+ /** Code HTTP imposé à la réponse, à la place de celui de `kind`. */
13
+ status?: number | undefined);
14
+ }
15
+ /** Nature d'une erreur levée en traitant une requête de la GUI. */
16
+ export declare function errorKindOf(err: unknown): ApiErrorKind;
17
+ /** Code HTTP imposé par une erreur : celui que Groq a renvoyé pour une transcription refusée. */
18
+ export declare function errorStatusOf(err: unknown): number | undefined;
19
+ export interface GuiApiContext {
20
+ store: Store;
21
+ version: string;
22
+ mailer: Mailer;
23
+ /** URL publique de la GUI (`ORGANIZER_PUBLIC_URL`), base des liens envoyés par e-mail. */
24
+ publicUrl: string | undefined;
25
+ /** Fichiers des pièces jointes. */
26
+ attachments: AttachmentStorage;
27
+ /** Dictée vocale (API Groq). */
28
+ transcriber: Transcriber;
29
+ /** Horloge, imposée par les tests. */
30
+ now?: () => Date;
31
+ }
32
+ /**
33
+ * Traite une requête `/api/*` de la GUI. Hors des routes
34
+ * publiques (santé, connexion, déconnexion, liens d'accès), une session valide
35
+ * est exigée ; une session prolongée renvoie son nouveau cookie.
36
+ */
37
+ export declare function handleApiRequest(ctx: GuiApiContext, request: ApiRequestData): Promise<ApiResponseData>;