@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,626 @@
1
+ import dotenv from "dotenv";
2
+ import fs from "node:fs";
3
+ import net from "node:net";
4
+ import path from "node:path";
5
+ import { askLine, askSecret } from "./prompt.js";
6
+ import { resolveEnvFile } from "../core/env-file.js";
7
+ import { databasePath } from "../core/db.js";
8
+ import { setAccount } from "../core/accounts.js";
9
+ import { Store } from "../core/store.js";
10
+ import { DEFAULT_HOST, loadSiteConfig } from "../server/config.js";
11
+ import { renderEnvFile, REMOVED_ENV_KEYS } from "../deploy/env-render.js";
12
+ import { renderNginx, renderSystemdUnit, SERVICE_NAME, upstreamAddress } from "../deploy/templates.js";
13
+ import { EmailSchema } from "../../shared/schemas/entities.schema.js";
14
+ import { MIN_PASSWORD_LENGTH } from "../../shared/schemas/api.schema.js";
15
+ /**
16
+ * Commande `mcp-organizer --setup` : installation du site.
17
+ *
18
+ * Elle demande (ou reçoit par options et variables `ORGANIZER_SETUP_*`) l'URL
19
+ * publique, l'écoute, le répertoire de données, la confiance dans le reverse
20
+ * proxy, la dictée (Groq) et l'e-mail (SMTP), puis le premier compte. Elle
21
+ * écrit le `.env` (fusionné avec l'existant, lisible par son seul
22
+ * propriétaire), l'unité systemd et le bloc nginx dans un dossier de sortie,
23
+ * crée le compte et affiche les étapes suivantes. Elle n'écrit aucun fichier
24
+ * système.
25
+ *
26
+ * Les secrets (clé Groq, URL SMTP, mot de passe) ne se passent jamais en
27
+ * argument : ils sont saisis sans écho, ou lus dans leur variable
28
+ * `ORGANIZER_SETUP_*`. Une valeur existante du `.env` est proposée par défaut
29
+ * et conservée en mode non interactif ; une valeur requise absente fait
30
+ * échouer la commande.
31
+ */
32
+ /** Modèle de transcription Groq écrit par le setup. */
33
+ export const TRANSCRIBE_MODEL = "whisper-large-v3-turbo";
34
+ /** Compte système proposé pour exécuter le site. */
35
+ export const DEFAULT_SERVICE_USER = "mcp-organizer";
36
+ /** Fichiers écrits dans le dossier de sortie. */
37
+ export const SYSTEMD_UNIT_FILENAME = `${SERVICE_NAME}.service`;
38
+ export const NGINX_FILENAME = `${SERVICE_NAME}.nginx.conf`;
39
+ const ENV_PREFIX = "ORGANIZER_SETUP_";
40
+ /** Options à valeur (`--nom=valeur`), chacune avec sa variable `ORGANIZER_SETUP_NOM`. */
41
+ const VALUE_OPTIONS = [
42
+ "public-url",
43
+ "port",
44
+ "host",
45
+ "data-dir",
46
+ "trust-proxy",
47
+ "mail-from",
48
+ "admin-email",
49
+ "admin-name",
50
+ "output-dir",
51
+ "install-dir",
52
+ "service-user",
53
+ "node",
54
+ ];
55
+ const FLAG_OPTIONS = ["--setup", "--non-interactive", "--no-groq", "--no-smtp"];
56
+ /** Secrets refusés en argument : nom de l'option → variable à utiliser. */
57
+ const SECRET_OPTIONS = {
58
+ "groq-api-key": `${ENV_PREFIX}GROQ_API_KEY`,
59
+ "smtp-url": `${ENV_PREFIX}SMTP_URL`,
60
+ "admin-password": `${ENV_PREFIX}ADMIN_PASSWORD`,
61
+ password: `${ENV_PREFIX}ADMIN_PASSWORD`,
62
+ };
63
+ export const SETUP_USAGE = `Usage : mcp-organizer --setup [options]
64
+
65
+ Installation du site : écrit le .env, l'unité systemd et le bloc nginx, et crée
66
+ le premier compte. Sans terminal, ou avec --non-interactive, rien n'est demandé :
67
+ chaque valeur requise vient d'une option ou de sa variable ORGANIZER_SETUP_<NOM>
68
+ (--public-url → ORGANIZER_SETUP_PUBLIC_URL), sinon du .env existant.
69
+
70
+ --public-url=<url> URL ouverte par les utilisateurs (https, ou http pour un hôte local)
71
+ --port=<n> port d'écoute du site
72
+ --host=<adresse> adresse d'écoute [${DEFAULT_HOST}]
73
+ --data-dir=<chemin> répertoire de données [<package>/.organizer-data]
74
+ --trust-proxy=oui|non lire l'adresse du client dans X-Forwarded-For (nginx)
75
+ --mail-from=<adresse> expéditeur des e-mails, avec SMTP
76
+ --no-groq désactiver la dictée vocale
77
+ --no-smtp désactiver l'envoi d'e-mails
78
+ --admin-email=<adresse> compte à créer, ou dont remplacer le mot de passe
79
+ --admin-name=<nom> nom affiché de ce compte
80
+ --dotenv=<fichier> fichier .env à écrire [<package>/.env]
81
+ --output-dir=<chemin> dossier de l'unité systemd et du bloc nginx [<package>/deploy]
82
+ --install-dir=<chemin> répertoire du dépôt dans l'unité [<package>]
83
+ --service-user=<compte> compte système de l'unité [${DEFAULT_SERVICE_USER}]
84
+ --node=<chemin> exécutable Node.js de l'unité [node courant]
85
+ --non-interactive ne rien demander
86
+
87
+ Secrets, jamais en argument : saisis sans écho, ou lus dans
88
+ ORGANIZER_SETUP_GROQ_API_KEY clé de l'API Groq (dictée)
89
+ ORGANIZER_SETUP_SMTP_URL smtp://utilisateur:motdepasse@hote:587 ou smtps://…
90
+ ORGANIZER_SETUP_ADMIN_PASSWORD mot de passe du compte (${MIN_PASSWORD_LENGTH} caractères minimum)`;
91
+ class SetupError extends Error {
92
+ }
93
+ function fail(message) {
94
+ throw new SetupError(message);
95
+ }
96
+ const say = (line = "") => process.stdout.write(`${line}\n`);
97
+ const step = (title) => say(`\n== ${title}`);
98
+ function envNameOf(option) {
99
+ return `${ENV_PREFIX}${option.replace(/-/g, "_").toUpperCase()}`;
100
+ }
101
+ /** Lit les options ; une option inconnue, répétée ou secrète est refusée. */
102
+ export function parseArgs(argv) {
103
+ const values = {};
104
+ for (const arg of argv) {
105
+ if (FLAG_OPTIONS.includes(arg) || arg.startsWith("--dotenv="))
106
+ continue;
107
+ if (!arg.startsWith("--"))
108
+ fail(`Argument inattendu : « ${arg} ». Les valeurs se passent en --nom=valeur.\n\n${SETUP_USAGE}`);
109
+ const body = arg.slice(2);
110
+ const eq = body.indexOf("=");
111
+ const name = eq === -1 ? body : body.slice(0, eq);
112
+ if (name in SECRET_OPTIONS) {
113
+ fail(`--${name} est refusé : un secret passé en argument reste lisible dans l'historique du shell et la liste des processus. Saisissez-le sans écho, ou définissez ${SECRET_OPTIONS[name]}.`);
114
+ }
115
+ if (!VALUE_OPTIONS.includes(name))
116
+ fail(`Option inconnue : --${name}.\n\n${SETUP_USAGE}`);
117
+ if (eq === -1)
118
+ fail(`--${name} attend une valeur : --${name}=<valeur>.`);
119
+ const option = name;
120
+ if (values[option] !== undefined)
121
+ fail(`--${name} est donné plusieurs fois.`);
122
+ values[option] = body.slice(eq + 1).trim();
123
+ }
124
+ for (const option of VALUE_OPTIONS) {
125
+ if (values[option] !== undefined)
126
+ continue;
127
+ const fromEnv = process.env[envNameOf(option)]?.trim();
128
+ if (fromEnv)
129
+ values[option] = fromEnv;
130
+ }
131
+ return {
132
+ values,
133
+ nonInteractive: argv.includes("--non-interactive") || !process.stdin.isTTY,
134
+ noGroq: argv.includes("--no-groq"),
135
+ noSmtp: argv.includes("--no-smtp"),
136
+ };
137
+ }
138
+ function secretFromEnv(name) {
139
+ const value = process.env[`${ENV_PREFIX}${name}`];
140
+ return value === undefined || value === "" ? undefined : value;
141
+ }
142
+ // --- Validation --------------------------------------------------------------
143
+ const LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]"]);
144
+ function validatePublicUrl(raw) {
145
+ let url;
146
+ try {
147
+ url = new URL(raw);
148
+ }
149
+ catch {
150
+ throw new Error(`« ${raw} » n'est pas une URL absolue : attendu par exemple https://organizer.example.com.`);
151
+ }
152
+ if (url.protocol !== "https:" && url.protocol !== "http:")
153
+ throw new Error(`protocole ${url.protocol} refusé : https attendu.`);
154
+ if (url.search || url.hash || url.username || url.password)
155
+ throw new Error("l'URL ne doit contenir ni identifiants, ni paramètres, ni fragment.");
156
+ if (url.protocol === "http:" && !LOCAL_HOSTNAMES.has(url.hostname)) {
157
+ throw new Error(`un site exposé est servi en https (${url.hostname}) : http n'est admis que pour un essai local (localhost, 127.0.0.1).`);
158
+ }
159
+ const trimmed = url.pathname.replace(/\/+$/, "");
160
+ const basePath = trimmed === "" ? "/" : trimmed;
161
+ if (!/^\/[A-Za-z0-9._~\-/]*$/.test(basePath))
162
+ throw new Error(`chemin ${basePath} refusé : lettres, chiffres, « . _ ~ - / » seulement.`);
163
+ return { publicUrl: url.href.replace(/\/+$/, ""), basePath };
164
+ }
165
+ function validatePort(raw) {
166
+ const port = Number(raw);
167
+ if (!/^\d+$/.test(raw) || port < 1 || port > 65535)
168
+ throw new Error(`port invalide (${raw}) : entier entre 1 et 65535 attendu.`);
169
+ return port;
170
+ }
171
+ function validateHost(raw) {
172
+ if (net.isIP(raw) !== 0)
173
+ return raw;
174
+ if (/^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(raw))
175
+ return raw;
176
+ throw new Error(`adresse d'écoute invalide (${raw}) : une adresse IP ou un nom d'hôte, sans port ni crochets.`);
177
+ }
178
+ /** Adresse d'écoute de la boucle locale : `localhost`, `127.0.0.0/8` ou `::1`. */
179
+ function isLoopbackHost(host) {
180
+ const lower = host.toLowerCase();
181
+ if (lower === "localhost" || lower === "::1")
182
+ return true;
183
+ return net.isIPv4(lower) && lower.startsWith("127.");
184
+ }
185
+ function validateBoolean(raw) {
186
+ const value = raw.trim().toLowerCase();
187
+ if (["o", "oui", "y", "yes", "1", "true"].includes(value))
188
+ return true;
189
+ if (["n", "non", "no", "0", "false"].includes(value))
190
+ return false;
191
+ throw new Error(`réponse invalide (${raw}) : oui ou non attendu.`);
192
+ }
193
+ function validatePath(raw) {
194
+ if (raw.trim() === "")
195
+ throw new Error("chemin vide.");
196
+ return path.resolve(raw.trim());
197
+ }
198
+ /** Chemin de la machine cible : un chemin POSIX absolu est gardé tel quel, un autre est résolu ici. */
199
+ function validateTargetPath(raw) {
200
+ const value = raw.trim();
201
+ if (value.startsWith("/"))
202
+ return path.posix.normalize(value).replace(/\/+$/, "") || "/";
203
+ return validatePath(value);
204
+ }
205
+ function validateMailFrom(raw) {
206
+ if (!raw.includes("@"))
207
+ throw new Error(`expéditeur invalide (${raw}) : attendu par exemple Organizer <organizer@example.com>.`);
208
+ return raw;
209
+ }
210
+ function validateSmtpUrl(raw) {
211
+ let url;
212
+ try {
213
+ url = new URL(raw);
214
+ }
215
+ catch {
216
+ throw new Error("l'URL SMTP n'est pas une URL absolue : attendu smtp://utilisateur:motdepasse@hote:587 ou smtps://….");
217
+ }
218
+ if (url.protocol !== "smtp:" && url.protocol !== "smtps:")
219
+ throw new Error(`protocole ${url.protocol} refusé pour l'URL SMTP : smtp ou smtps attendu.`);
220
+ if (!url.hostname)
221
+ throw new Error("l'URL SMTP n'indique pas d'hôte.");
222
+ return raw;
223
+ }
224
+ function validateEmail(raw) {
225
+ const parsed = EmailSchema.safeParse(raw);
226
+ if (!parsed.success)
227
+ throw new Error(`adresse e-mail invalide (${raw}).`);
228
+ return parsed.data;
229
+ }
230
+ function validateName(raw) {
231
+ const name = raw.trim();
232
+ if (name === "")
233
+ throw new Error("nom affiché vide.");
234
+ return name;
235
+ }
236
+ function validateServiceUser(raw) {
237
+ if (!/^[a-z_][a-z0-9_-]*$/.test(raw))
238
+ throw new Error(`compte système invalide (${raw}) : minuscules, chiffres, « _ » et « - ».`);
239
+ return raw;
240
+ }
241
+ function validateNodePath(raw) {
242
+ if (raw.trim() === "")
243
+ throw new Error("chemin vide.");
244
+ return raw.trim();
245
+ }
246
+ class Prompter {
247
+ args;
248
+ constructor(args) {
249
+ this.args = args;
250
+ }
251
+ get interactive() {
252
+ return !this.args.nonInteractive;
253
+ }
254
+ provided(option) {
255
+ return this.args.values[option];
256
+ }
257
+ /** Valeur d'une question : option, puis `.env` existant, puis défaut, puis saisie. */
258
+ async value(spec) {
259
+ const provided = this.provided(spec.option);
260
+ if (provided !== undefined) {
261
+ try {
262
+ const value = spec.validate(provided);
263
+ say(` ${spec.question} : ${provided}`);
264
+ return value;
265
+ }
266
+ catch (err) {
267
+ fail(`--${spec.option} (ou ${envNameOf(spec.option)}) : ${err.message}`);
268
+ }
269
+ }
270
+ const fallback = spec.existing ?? spec.defaultValue;
271
+ if (!this.interactive) {
272
+ if (fallback === undefined)
273
+ fail(`Mode non interactif : --${spec.option} (ou ${envNameOf(spec.option)}) est requis.`);
274
+ try {
275
+ const value = spec.validate(fallback);
276
+ say(` ${spec.question} : ${fallback}${spec.existing !== undefined ? " (conservé)" : " (défaut)"}`);
277
+ return value;
278
+ }
279
+ catch (err) {
280
+ fail(`${spec.question} : la valeur ${spec.existing !== undefined ? "du .env existant" : "par défaut"} est invalide, ${err.message} Passez --${spec.option}.`);
281
+ }
282
+ }
283
+ const proposed = fallback ?? spec.suggestion;
284
+ for (;;) {
285
+ const raw = await askLine(`${spec.question}${proposed !== undefined ? ` [${proposed}]` : ""}`);
286
+ const candidate = raw === "" ? proposed : raw;
287
+ if (candidate === undefined) {
288
+ process.stderr.write(" ! Une valeur est attendue.\n");
289
+ continue;
290
+ }
291
+ try {
292
+ return spec.validate(candidate);
293
+ }
294
+ catch (err) {
295
+ process.stderr.write(` ! ${err.message}\n`);
296
+ }
297
+ }
298
+ }
299
+ }
300
+ /**
301
+ * Secret facultatif (clé Groq, URL SMTP) : variable `ORGANIZER_SETUP_*`, puis
302
+ * valeur existante, puis saisie sans écho. `null` désactive la fonction.
303
+ */
304
+ async function optionalSecret(prompter, spec) {
305
+ const fromEnv = secretFromEnv(spec.envName);
306
+ if (spec.disabled) {
307
+ if (fromEnv !== undefined)
308
+ fail(`${spec.disableFlag} et ${ENV_PREFIX}${spec.envName} sont incompatibles : retirez l'un des deux.`);
309
+ say(` ${spec.label} : désactivé (${spec.disableFlag}).`);
310
+ return null;
311
+ }
312
+ if (fromEnv !== undefined) {
313
+ try {
314
+ spec.validate(fromEnv);
315
+ }
316
+ catch (err) {
317
+ fail(`${ENV_PREFIX}${spec.envName} : ${err.message}`);
318
+ }
319
+ say(` ${spec.label} : repris de ${ENV_PREFIX}${spec.envName}.`);
320
+ return fromEnv;
321
+ }
322
+ if (!prompter.interactive) {
323
+ say(spec.existing !== undefined ? ` ${spec.label} : conservé.` : ` ${spec.label} : non configuré, fonction désactivée.`);
324
+ return spec.existing ?? null;
325
+ }
326
+ const hint = spec.existing !== undefined ? "Entrée pour conserver, « - » pour désactiver" : "Entrée pour désactiver";
327
+ for (;;) {
328
+ const raw = (await askSecret(`${spec.label}, non affiché (${hint})`)).trim();
329
+ if (raw === "")
330
+ return spec.existing ?? null;
331
+ if (raw === "-")
332
+ return null;
333
+ try {
334
+ return spec.validate(raw);
335
+ }
336
+ catch (err) {
337
+ process.stderr.write(` ! ${err.message}\n`);
338
+ }
339
+ }
340
+ }
341
+ async function readPassword(prompter) {
342
+ const fromEnv = secretFromEnv("ADMIN_PASSWORD");
343
+ if (fromEnv !== undefined) {
344
+ if (fromEnv.length < MIN_PASSWORD_LENGTH)
345
+ fail(`${ENV_PREFIX}ADMIN_PASSWORD doit faire au moins ${MIN_PASSWORD_LENGTH} caractères.`);
346
+ say(` Mot de passe : repris de ${ENV_PREFIX}ADMIN_PASSWORD.`);
347
+ return fromEnv;
348
+ }
349
+ if (!prompter.interactive)
350
+ fail(`Mode non interactif : ${ENV_PREFIX}ADMIN_PASSWORD est requis pour créer le compte.`);
351
+ for (;;) {
352
+ const first = await askSecret(`Mot de passe, non affiché (${MIN_PASSWORD_LENGTH} caractères minimum)`);
353
+ if (first.length < MIN_PASSWORD_LENGTH) {
354
+ process.stderr.write(` ! Le mot de passe doit faire au moins ${MIN_PASSWORD_LENGTH} caractères.\n`);
355
+ continue;
356
+ }
357
+ if ((await askSecret("Confirmation")) !== first) {
358
+ process.stderr.write(" ! Les deux saisies diffèrent.\n");
359
+ continue;
360
+ }
361
+ return first;
362
+ }
363
+ }
364
+ /** Résultat de `fn` sur la base existante du répertoire de données, `fallback` sans base. */
365
+ function withExistingStore(dataDir, fallback, fn) {
366
+ if (!fs.existsSync(databasePath(dataDir)))
367
+ return fallback;
368
+ const store = Store.open(dataDir);
369
+ try {
370
+ return fn(store);
371
+ }
372
+ finally {
373
+ store.close();
374
+ }
375
+ }
376
+ async function planAccount(prompter, dataDir) {
377
+ const accounts = { count: withExistingStore(dataDir, 0, (store) => store.countUsers()) };
378
+ const given = prompter.provided("admin-email");
379
+ let email;
380
+ if (given !== undefined) {
381
+ email = await prompter.value({ option: "admin-email", question: "Adresse e-mail du compte", validate: validateEmail });
382
+ }
383
+ else if (accounts.count === 0) {
384
+ if (!prompter.interactive)
385
+ fail("Mode non interactif : aucun compte n'existe, --admin-email (ou ORGANIZER_SETUP_ADMIN_EMAIL) est requis pour créer le premier.");
386
+ email = await prompter.value({ option: "admin-email", question: "Adresse e-mail du premier compte", validate: validateEmail });
387
+ }
388
+ else {
389
+ const answer = prompter.interactive ? await askLine(`${accounts.count} compte(s) en base. Adresse d'un compte à créer ou dont remplacer le mot de passe (Entrée pour ne rien changer)`) : "";
390
+ if (answer === "") {
391
+ if (prompter.provided("admin-name") !== undefined)
392
+ fail("--admin-name est donné sans --admin-email : indiquez le compte à nommer.");
393
+ if (secretFromEnv("ADMIN_PASSWORD") !== undefined)
394
+ fail(`${ENV_PREFIX}ADMIN_PASSWORD est défini sans --admin-email : indiquez le compte dont remplacer le mot de passe.`);
395
+ say(` Compte : inchangé (${accounts.count} compte(s) en base).`);
396
+ return null;
397
+ }
398
+ try {
399
+ email = validateEmail(answer);
400
+ }
401
+ catch (err) {
402
+ fail(err.message);
403
+ }
404
+ }
405
+ const current = withExistingStore(dataDir, undefined, (store) => store.findUserByEmail(email)?.displayName ?? undefined);
406
+ const displayName = prompter.provided("admin-name") !== undefined || current === undefined
407
+ ? await prompter.value({ option: "admin-name", question: "Nom affiché", validate: validateName })
408
+ : undefined;
409
+ if (current !== undefined && displayName === undefined)
410
+ say(` Nom affiché : ${current} (conservé).`);
411
+ const password = await readPassword(prompter);
412
+ return { email, displayName, password };
413
+ }
414
+ // --- Écriture ----------------------------------------------------------------
415
+ /** Écrit un fichier lisible par son seul propriétaire, par remplacement atomique. */
416
+ function writePrivateFile(target, content) {
417
+ fs.mkdirSync(path.dirname(target), { recursive: true });
418
+ const temporary = `${target}.${process.pid}.tmp`;
419
+ fs.writeFileSync(temporary, content, { mode: 0o600 });
420
+ try {
421
+ if (process.platform !== "win32")
422
+ fs.chmodSync(temporary, 0o600);
423
+ fs.renameSync(temporary, target);
424
+ }
425
+ catch (err) {
426
+ fs.rmSync(temporary, { force: true });
427
+ throw err;
428
+ }
429
+ }
430
+ function readExistingEnv(envFilePath) {
431
+ if (!fs.existsSync(envFilePath))
432
+ return undefined;
433
+ try {
434
+ return fs.readFileSync(envFilePath, "utf8");
435
+ }
436
+ catch (err) {
437
+ fail(`Le fichier ${envFilePath} existe mais ne peut pas être lu (${err?.message ?? err}) : corrigez ses droits, ou choisissez un autre fichier avec --dotenv.`);
438
+ }
439
+ }
440
+ function printNextSteps(answers, context, outputDir, local) {
441
+ step("Étapes suivantes");
442
+ const unit = path.join(outputDir, SYSTEMD_UNIT_FILENAME);
443
+ const nginx = path.join(outputDir, NGINX_FILENAME);
444
+ const serverName = new URL(answers.publicUrl).hostname;
445
+ if (local) {
446
+ say(" Essai local : lancez le site, puis ouvrez l'URL publique.");
447
+ say(` node ${path.join(context.installDir, "dist", "src", "index.js")} --server --dotenv=${context.envFilePath}`);
448
+ say(` ${answers.publicUrl}`);
449
+ return;
450
+ }
451
+ say(" À exécuter en root ; aucun fichier système n'a été écrit.");
452
+ say("");
453
+ say(" 1. Compte de service et droits :");
454
+ say(` useradd --system --no-create-home --shell /usr/sbin/nologin ${context.serviceUser}`);
455
+ say(` chown -R ${context.serviceUser}:${context.serviceUser} ${answers.dataDir}`);
456
+ say(` chown root:${context.serviceUser} ${context.envFilePath} && chmod 640 ${context.envFilePath}`);
457
+ say("");
458
+ say(" 2. Service :");
459
+ say(` cp ${unit} /etc/systemd/system/${SYSTEMD_UNIT_FILENAME}`);
460
+ say(" systemctl daemon-reload");
461
+ say(` systemctl enable --now ${SERVICE_NAME}`);
462
+ say("");
463
+ say(" 3. Certificat, puis nginx :");
464
+ say(` certbot certonly --nginx -d ${serverName}`);
465
+ say(` cp ${nginx} /etc/nginx/sites-available/${SERVICE_NAME}`);
466
+ say(` ln -s /etc/nginx/sites-available/${SERVICE_NAME} /etc/nginx/sites-enabled/${SERVICE_NAME}`);
467
+ say(" nginx -t && systemctl reload nginx");
468
+ say("");
469
+ say(" 4. Vérification :");
470
+ say(` systemctl status ${SERVICE_NAME}`);
471
+ say(` curl -sI http://${upstreamAddress(answers.host, answers.port)}/`);
472
+ say(` ouvrir ${answers.publicUrl}`);
473
+ say(` En cas d'échec : journalctl -u ${SERVICE_NAME} -n 50`);
474
+ }
475
+ // --- Commande ----------------------------------------------------------------
476
+ /** Exécute la commande ; renvoie le code de sortie du processus. */
477
+ export async function runSetup(argv, rootDir) {
478
+ try {
479
+ const args = parseArgs(argv);
480
+ const prompter = new Prompter(args);
481
+ const envFilePath = resolveEnvFile(argv, rootDir).path;
482
+ const existingText = readExistingEnv(envFilePath);
483
+ const existing = existingText === undefined ? {} : dotenv.parse(existingText);
484
+ const kept = (key) => (existing[key] === undefined || existing[key] === "" ? undefined : existing[key]);
485
+ say("mcp-organizer — installation du site");
486
+ say(existingText === undefined ? ` Nouveau fichier de configuration : ${envFilePath}` : ` Configuration existante relue : ${envFilePath}`);
487
+ step("Exposition");
488
+ const { publicUrl, basePath } = await prompter.value({
489
+ option: "public-url",
490
+ question: "URL publique du site",
491
+ existing: kept("ORGANIZER_PUBLIC_URL"),
492
+ suggestion: "https://organizer.example.com",
493
+ validate: validatePublicUrl,
494
+ });
495
+ const port = await prompter.value({ option: "port", question: "Port d'écoute", existing: kept("PORT"), suggestion: "4800", validate: validatePort });
496
+ const host = await prompter.value({ option: "host", question: "Adresse d'écoute", existing: kept("HOST"), defaultValue: DEFAULT_HOST, validate: validateHost });
497
+ const existingTrust = existing.ORGANIZER_TRUST_PROXY === undefined ? undefined : existing.ORGANIZER_TRUST_PROXY === "1" ? "oui" : existing.ORGANIZER_TRUST_PROXY === "" ? "non" : undefined;
498
+ const trustProxy = await prompter.value({
499
+ option: "trust-proxy",
500
+ question: "Adresse du client lue dans X-Forwarded-For (oui derrière nginx)",
501
+ existing: existingTrust,
502
+ suggestion: "oui",
503
+ validate: validateBoolean,
504
+ });
505
+ if (trustProxy && !isLoopbackHost(host)) {
506
+ fail(`--trust-proxy=oui avec l'adresse d'écoute ${host} est refusé : un site joignable sans passer par le reverse proxy reçoit des requêtes directes, dont l'en-tête X-Forwarded-For est écrit par le client lui-même. Il pourrait alors choisir l'adresse vue par le site et contourner la limitation des connexions. Gardez HOST=127.0.0.1 (ou ::1) derrière nginx, ou répondez non à --trust-proxy.`);
507
+ }
508
+ step("Données");
509
+ const dataDir = await prompter.value({
510
+ option: "data-dir",
511
+ question: "Répertoire de données",
512
+ existing: kept("MCP_ORGANIZER_DATA_DIR"),
513
+ defaultValue: path.join(rootDir, ".organizer-data"),
514
+ validate: validatePath,
515
+ });
516
+ step("Dictée vocale (Groq)");
517
+ const groqApiKey = await optionalSecret(prompter, {
518
+ envName: "GROQ_API_KEY",
519
+ label: "Clé de l'API Groq",
520
+ existing: kept("GROQ_API_KEY"),
521
+ disabled: args.noGroq,
522
+ disableFlag: "--no-groq",
523
+ validate: (raw) => raw,
524
+ });
525
+ step("E-mail (SMTP)");
526
+ if (args.noSmtp && prompter.provided("mail-from") !== undefined)
527
+ fail("--no-smtp et --mail-from sont incompatibles : retirez l'un des deux.");
528
+ const smtpUrl = await optionalSecret(prompter, {
529
+ envName: "SMTP_URL",
530
+ label: "URL SMTP",
531
+ existing: kept("SMTP_URL"),
532
+ disabled: args.noSmtp,
533
+ disableFlag: "--no-smtp",
534
+ validate: validateSmtpUrl,
535
+ });
536
+ let mailFrom = null;
537
+ if (smtpUrl === null) {
538
+ if (prompter.provided("mail-from") !== undefined)
539
+ fail("--mail-from est donné sans serveur SMTP : définissez ORGANIZER_SETUP_SMTP_URL, ou retirez --mail-from.");
540
+ }
541
+ else {
542
+ mailFrom = await prompter.value({ option: "mail-from", question: "Expéditeur des e-mails", existing: kept("MAIL_FROM"), suggestion: "Organizer <organizer@example.com>", validate: validateMailFrom });
543
+ }
544
+ step("Premier compte");
545
+ const account = await planAccount(prompter, dataDir);
546
+ const values = {
547
+ ORGANIZER_PUBLIC_URL: publicUrl,
548
+ ORGANIZER_BASE_PATH: basePath === "/" ? null : basePath,
549
+ PORT: String(port),
550
+ HOST: host,
551
+ // Vide plutôt qu'absent : la réponse « non » reste dans le fichier pour un setup relancé.
552
+ ORGANIZER_TRUST_PROXY: trustProxy ? "1" : "",
553
+ MCP_ORGANIZER_DATA_DIR: dataDir,
554
+ GROQ_API_KEY: groqApiKey,
555
+ ORGANIZER_TRANSCRIBE_MODEL: groqApiKey === null ? null : TRANSCRIBE_MODEL,
556
+ SMTP_URL: smtpUrl,
557
+ MAIL_FROM: mailFrom,
558
+ };
559
+ // Le site relira ce fichier : sa propre validation doit l'accepter.
560
+ try {
561
+ loadSiteConfig(Object.fromEntries(Object.entries(values).filter(([, value]) => value !== null)));
562
+ }
563
+ catch (err) {
564
+ fail(err.message);
565
+ }
566
+ const context = {
567
+ installDir: prompter.provided("install-dir") !== undefined ? validateOption("install-dir", prompter.provided("install-dir"), validateTargetPath) : rootDir,
568
+ envFilePath,
569
+ serviceUser: validateOption("service-user", prompter.provided("service-user") ?? DEFAULT_SERVICE_USER, validateServiceUser),
570
+ nodePath: validateOption("node", prompter.provided("node") ?? process.execPath, validateNodePath),
571
+ };
572
+ const outputDir = prompter.provided("output-dir") !== undefined ? validatePathOption("output-dir", prompter.provided("output-dir")) : path.join(rootDir, "deploy");
573
+ const answers = { publicUrl, basePath, host, port, dataDir };
574
+ step("Fichiers");
575
+ const envText = renderEnvFile(existingText, values);
576
+ writePrivateFile(envFilePath, envText);
577
+ say(` ${envFilePath}`);
578
+ if (process.platform === "win32")
579
+ say(" Windows : les droits POSIX ne s'appliquent pas ; restreignez l'accès au fichier s'il est partagé.");
580
+ else
581
+ say(" droits 600 : lisible par son seul propriétaire.");
582
+ const removed = REMOVED_ENV_KEYS.filter((key) => existing[key] !== undefined);
583
+ if (removed.length > 0)
584
+ say(` retirées : ${removed.join(", ")} (le compte vit en base).`);
585
+ fs.mkdirSync(outputDir, { recursive: true });
586
+ const unitPath = path.join(outputDir, SYSTEMD_UNIT_FILENAME);
587
+ const nginxPath = path.join(outputDir, NGINX_FILENAME);
588
+ fs.writeFileSync(unitPath, renderSystemdUnit(answers, context));
589
+ fs.writeFileSync(nginxPath, renderNginx(answers));
590
+ say(` ${unitPath}`);
591
+ say(` ${nginxPath}`);
592
+ if (account !== null) {
593
+ step("Compte");
594
+ const store = Store.open(dataDir);
595
+ try {
596
+ const { created, user } = await setAccount(store, account);
597
+ say(created ? ` Compte ${user.email} (${user.displayName}) créé, avec son espace personnel.` : ` Compte ${user.email} (${user.displayName}) : mot de passe remplacé, compte actif.`);
598
+ }
599
+ finally {
600
+ store.close();
601
+ }
602
+ }
603
+ step("Résumé");
604
+ say(` Site : ${publicUrl}, écoute sur ${upstreamAddress(host, port)}${trustProxy ? ", X-Forwarded-For pris en compte" : ""}.`);
605
+ say(` Données : ${dataDir}`);
606
+ say(` Dictée : ${groqApiKey === null ? "désactivée (aucune clé Groq)" : `activée (${TRANSCRIBE_MODEL})`}.`);
607
+ say(` E-mail : ${smtpUrl === null ? "désactivé (aucun serveur SMTP)" : `activé, expéditeur ${mailFrom}`}.`);
608
+ printNextSteps(answers, context, outputDir, new URL(publicUrl).protocol === "http:");
609
+ return 0;
610
+ }
611
+ catch (err) {
612
+ process.stderr.write(`Installation interrompue : ${err?.message ?? err}\n`);
613
+ return 1;
614
+ }
615
+ }
616
+ function validateOption(option, raw, validate) {
617
+ try {
618
+ return validate(raw);
619
+ }
620
+ catch (err) {
621
+ fail(`--${option} (ou ${envNameOf(option)}) : ${err.message}`);
622
+ }
623
+ }
624
+ function validatePathOption(option, raw) {
625
+ return validateOption(option, raw, validatePath);
626
+ }
@@ -0,0 +1,4 @@
1
+ import { type AgentRequestSummary } from "../../shared/schemas/agent-tools.schema.js";
2
+ /** Ligne d'une demande : une seule ligne, lisible telle quelle dans la notification. */
3
+ export declare function formatRequestEvent(request: AgentRequestSummary): string;
4
+ export declare function runWatchMode(tokenId: string | undefined): Promise<number>;