@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,126 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ATTACHMENTS_DIRNAME } from "../core/attachments.js";
|
|
5
|
+
import { BUSY_TIMEOUT_MS, databasePath } from "../core/db.js";
|
|
6
|
+
/**
|
|
7
|
+
* Commande `mcp-organizer --backup <fichier> [--force]` : sauvegarde du site,
|
|
8
|
+
* en service ou non.
|
|
9
|
+
*
|
|
10
|
+
* La base est copiée par l'API de sauvegarde de SQLite, qui produit un
|
|
11
|
+
* instantané cohérent même pendant les écritures du site. Les pièces jointes
|
|
12
|
+
* référencées par cet instantané sont ensuite copiées dans le dossier
|
|
13
|
+
* `<fichier>.attachments/`, sous leur nom de stockage. Une pièce jointe
|
|
14
|
+
* référencée mais introuvable fait échouer la sauvegarde, qui ne laisse alors
|
|
15
|
+
* rien derrière elle. Une destination existante est refusée, sauf `--force`
|
|
16
|
+
* qui la remplace. Hors Windows, le fichier de la base est créé avec les
|
|
17
|
+
* droits 600 et le dossier des pièces jointes avec les droits 700.
|
|
18
|
+
*/
|
|
19
|
+
const USAGE = "Usage : mcp-organizer --backup <fichier> [--force]";
|
|
20
|
+
/** Suffixe du dossier des pièces jointes, à côté du fichier de la base. */
|
|
21
|
+
export const ATTACHMENTS_BACKUP_SUFFIX = ".attachments";
|
|
22
|
+
/** Droits POSIX appliqués aux fichiers de la sauvegarde ; Windows ne les connaît pas. */
|
|
23
|
+
const PRIVATE_MODES = process.platform !== "win32";
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const rest = argv.slice(argv.indexOf("--backup") + 1);
|
|
26
|
+
const unknown = rest.filter((arg) => arg.startsWith("-") && arg !== "--force");
|
|
27
|
+
if (unknown.length > 0)
|
|
28
|
+
throw new Error(`Option inconnue : ${unknown.join(", ")}.\n${USAGE}`);
|
|
29
|
+
const positional = rest.filter((arg) => !arg.startsWith("-"));
|
|
30
|
+
if (positional.length === 0)
|
|
31
|
+
throw new Error(`--backup attend le fichier de sauvegarde à créer.\n${USAGE}`);
|
|
32
|
+
if (positional.length > 1)
|
|
33
|
+
throw new Error(`--backup attend un seul fichier (reçu : ${positional.join(", ")}).\n${USAGE}`);
|
|
34
|
+
return { target: path.resolve(positional[0]), force: rest.includes("--force") };
|
|
35
|
+
}
|
|
36
|
+
function megabytes(bytes) {
|
|
37
|
+
return `${(bytes / (1024 * 1024)).toFixed(1).replace(".", ",")} Mo`;
|
|
38
|
+
}
|
|
39
|
+
/** Sauvegarde la base de `dataDir` dans `target`, et ses pièces jointes à côté. */
|
|
40
|
+
export async function backupSite(dataDir, target, force) {
|
|
41
|
+
const source = databasePath(dataDir);
|
|
42
|
+
if (!fs.existsSync(source))
|
|
43
|
+
throw new Error(`Aucune base à sauvegarder : ${source} n'existe pas. Vérifiez le répertoire de données (MCP_ORGANIZER_DATA_DIR, ou --dotenv vers le .env du site).`);
|
|
44
|
+
const attachmentsDir = `${target}${ATTACHMENTS_BACKUP_SUFFIX}`;
|
|
45
|
+
if (path.resolve(target) === path.resolve(source))
|
|
46
|
+
throw new Error("La destination est la base elle-même : choisissez un autre fichier.");
|
|
47
|
+
const existing = [target, attachmentsDir].filter((entry) => fs.existsSync(entry));
|
|
48
|
+
if (existing.length > 0 && !force)
|
|
49
|
+
throw new Error(`La destination existe déjà : ${existing.join(", ")}. Choisissez un autre fichier, ou ajoutez --force pour la remplacer.`);
|
|
50
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
51
|
+
const partial = `${target}.partial`;
|
|
52
|
+
const partialAttachments = `${attachmentsDir}.partial`;
|
|
53
|
+
fs.rmSync(partial, { force: true });
|
|
54
|
+
fs.rmSync(partialAttachments, { recursive: true, force: true });
|
|
55
|
+
try {
|
|
56
|
+
// La copie contient les empreintes des mots de passe et des jetons : lisible par son seul propriétaire dès sa création.
|
|
57
|
+
fs.writeFileSync(partial, "", { mode: 0o600, flag: "wx" });
|
|
58
|
+
if (PRIVATE_MODES)
|
|
59
|
+
fs.chmodSync(partial, 0o600);
|
|
60
|
+
const db = new Database(source, { fileMustExist: true });
|
|
61
|
+
try {
|
|
62
|
+
db.pragma(`busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
63
|
+
await db.backup(partial);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
db.close();
|
|
67
|
+
}
|
|
68
|
+
// La copie devient un fichier autonome, sans journal WAL à côté.
|
|
69
|
+
const copy = new Database(partial, { fileMustExist: true });
|
|
70
|
+
let storagePaths;
|
|
71
|
+
try {
|
|
72
|
+
copy.pragma("journal_mode = DELETE");
|
|
73
|
+
const integrity = copy.pragma("integrity_check", { simple: true });
|
|
74
|
+
if (integrity !== "ok")
|
|
75
|
+
throw new Error(`La copie de la base est corrompue (integrity_check : ${String(integrity)}).`);
|
|
76
|
+
storagePaths = copy.prepare("SELECT storage_path FROM attachments ORDER BY storage_path").all().map((row) => row.storage_path);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
copy.close();
|
|
80
|
+
}
|
|
81
|
+
fs.mkdirSync(partialAttachments, { mode: 0o700 });
|
|
82
|
+
if (PRIVATE_MODES)
|
|
83
|
+
fs.chmodSync(partialAttachments, 0o700);
|
|
84
|
+
const attachmentsRoot = path.resolve(dataDir, ATTACHMENTS_DIRNAME) + path.sep;
|
|
85
|
+
const missing = [];
|
|
86
|
+
for (const storagePath of storagePaths) {
|
|
87
|
+
const file = path.resolve(dataDir, storagePath);
|
|
88
|
+
if (!file.startsWith(attachmentsRoot))
|
|
89
|
+
throw new Error(`Pièce jointe hors du dossier ${ATTACHMENTS_DIRNAME}/ : ${storagePath}.`);
|
|
90
|
+
if (!fs.existsSync(file)) {
|
|
91
|
+
missing.push(storagePath);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
fs.copyFileSync(file, path.join(partialAttachments, path.relative(attachmentsRoot, file)));
|
|
95
|
+
}
|
|
96
|
+
if (missing.length > 0) {
|
|
97
|
+
throw new Error(`${missing.length} pièce(s) jointe(s) référencée(s) par la base sont introuvables dans ${attachmentsRoot} (${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", …" : ""}). Relancez la sauvegarde ; si l'erreur persiste, ces fichiers ont disparu du répertoire de données.`);
|
|
98
|
+
}
|
|
99
|
+
if (force) {
|
|
100
|
+
fs.rmSync(target, { force: true });
|
|
101
|
+
fs.rmSync(attachmentsDir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
fs.renameSync(partial, target);
|
|
104
|
+
fs.renameSync(partialAttachments, attachmentsDir);
|
|
105
|
+
return { databaseFile: target, attachmentsDir, attachmentCount: storagePaths.length };
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
fs.rmSync(partial, { force: true });
|
|
109
|
+
fs.rmSync(partialAttachments, { recursive: true, force: true });
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Exécute la commande ; renvoie le code de sortie du processus. */
|
|
114
|
+
export async function runBackup(argv, dataDir) {
|
|
115
|
+
try {
|
|
116
|
+
const args = parseArgs(argv);
|
|
117
|
+
const result = await backupSite(dataDir, args.target, args.force);
|
|
118
|
+
process.stdout.write(`Base sauvegardée : ${result.databaseFile} (${megabytes(fs.statSync(result.databaseFile).size)}).\n` +
|
|
119
|
+
`Pièces jointes : ${result.attachmentCount} fichier(s) dans ${result.attachmentsDir}.\n`);
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
process.stderr.write(`Sauvegarde interrompue : ${err?.message ?? err}\n`);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Saisies au terminal des commandes interactives (`--set-user`, `--setup`). */
|
|
2
|
+
/** Lit une réponse sans écho : l'interface de lecture écrit dans une sortie qui jette tout. */
|
|
3
|
+
export declare function askSecret(question: string): Promise<string>;
|
|
4
|
+
/** Pose une question en clair et renvoie la réponse sans espaces de bord. */
|
|
5
|
+
export declare function askLine(question: string): Promise<string>;
|
|
6
|
+
/** Pose une question en clair jusqu'à obtenir une valeur non vide. */
|
|
7
|
+
export declare function ask(question: string): Promise<string>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Writable } from "node:stream";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
/** Saisies au terminal des commandes interactives (`--set-user`, `--setup`). */
|
|
4
|
+
/** Lit une réponse sans écho : l'interface de lecture écrit dans une sortie qui jette tout. */
|
|
5
|
+
export async function askSecret(question) {
|
|
6
|
+
process.stdout.write(` ${question} : `);
|
|
7
|
+
const sink = new Writable({
|
|
8
|
+
write(_chunk, _encoding, callback) {
|
|
9
|
+
callback();
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: sink, terminal: true });
|
|
13
|
+
try {
|
|
14
|
+
const answer = await rl.question("");
|
|
15
|
+
process.stdout.write("\n");
|
|
16
|
+
return answer;
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
rl.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Pose une question en clair et renvoie la réponse sans espaces de bord. */
|
|
23
|
+
export async function askLine(question) {
|
|
24
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
25
|
+
try {
|
|
26
|
+
return (await rl.question(` ${question} : `)).trim();
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
rl.close();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Pose une question en clair jusqu'à obtenir une valeur non vide. */
|
|
33
|
+
export async function ask(question) {
|
|
34
|
+
for (;;) {
|
|
35
|
+
const value = await askLine(question);
|
|
36
|
+
if (value !== "")
|
|
37
|
+
return value;
|
|
38
|
+
process.stderr.write(" ! Une valeur est attendue.\n");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ask, askSecret } from "./prompt.js";
|
|
2
|
+
import { setAccount } from "../core/accounts.js";
|
|
3
|
+
import { Store } from "../core/store.js";
|
|
4
|
+
import { MIN_PASSWORD_LENGTH } from "../../shared/schemas/api.schema.js";
|
|
5
|
+
/**
|
|
6
|
+
* Commande `mcp-organizer --set-user [email] [--name="Prénom Nom"]` : crée un
|
|
7
|
+
* compte actif (avec son espace personnel) ou remplace le mot de passe d'un
|
|
8
|
+
* compte existant, qui redevient actif.
|
|
9
|
+
*
|
|
10
|
+
* Le mot de passe n'est jamais passé en argument. Il est saisi sans écho, avec
|
|
11
|
+
* confirmation ; en mode non interactif (`--non-interactive`, ou entrée qui
|
|
12
|
+
* n'est pas un terminal), il vient de `ORGANIZER_ADMIN_PASSWORD`. L'adresse
|
|
13
|
+
* vient de l'argument, puis de `ORGANIZER_ADMIN_EMAIL`, puis de la saisie. Le
|
|
14
|
+
* nom affiché vient de `--name`, puis de `ORGANIZER_ADMIN_NAME`, puis de la
|
|
15
|
+
* saisie ; il est requis pour créer un compte, facultatif pour un compte qui
|
|
16
|
+
* en a déjà un.
|
|
17
|
+
*/
|
|
18
|
+
const USAGE = [
|
|
19
|
+
'Usage : mcp-organizer --set-user [email] [--name="Prénom Nom"] [--non-interactive]',
|
|
20
|
+
" Le mot de passe est demandé sans écho ; en mode non interactif,",
|
|
21
|
+
" il vient de ORGANIZER_ADMIN_PASSWORD.",
|
|
22
|
+
];
|
|
23
|
+
const NAME_FLAG = "--name=";
|
|
24
|
+
function fail(...lines) {
|
|
25
|
+
throw new Error(lines.join("\n"));
|
|
26
|
+
}
|
|
27
|
+
/** Arguments qui suivent `--set-user` : l'adresse facultative et les options. */
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const index = argv.indexOf("--set-user");
|
|
30
|
+
const rest = argv.slice(index + 1);
|
|
31
|
+
const positional = rest.filter((arg) => !arg.startsWith("-"));
|
|
32
|
+
if (positional.length > 1) {
|
|
33
|
+
fail("Le mot de passe ne se passe pas en argument : il resterait en clair dans l'historique du shell et la liste des processus.", "", ...USAGE);
|
|
34
|
+
}
|
|
35
|
+
const unknown = rest.filter((arg) => arg.startsWith("-") && arg !== "--non-interactive" && !arg.startsWith(NAME_FLAG));
|
|
36
|
+
if (unknown.length > 0)
|
|
37
|
+
fail(`Option inconnue : ${unknown.join(", ")}.`, "", ...USAGE);
|
|
38
|
+
const names = rest.filter((arg) => arg.startsWith(NAME_FLAG)).map((arg) => arg.slice(NAME_FLAG.length).trim());
|
|
39
|
+
if (names.length > 1)
|
|
40
|
+
fail("--name est donné plusieurs fois.", "", ...USAGE);
|
|
41
|
+
if (names[0] === "")
|
|
42
|
+
fail('--name attend le nom affiché : --name="Prénom Nom".', "", ...USAGE);
|
|
43
|
+
return { email: positional[0], name: names[0], nonInteractive: rest.includes("--non-interactive") || !process.stdin.isTTY };
|
|
44
|
+
}
|
|
45
|
+
async function readPassword(nonInteractive) {
|
|
46
|
+
const fromEnv = process.env.ORGANIZER_ADMIN_PASSWORD;
|
|
47
|
+
if (fromEnv) {
|
|
48
|
+
if (!nonInteractive)
|
|
49
|
+
process.stdout.write(" Mot de passe : repris de ORGANIZER_ADMIN_PASSWORD.\n");
|
|
50
|
+
if (fromEnv.length < MIN_PASSWORD_LENGTH)
|
|
51
|
+
fail(`ORGANIZER_ADMIN_PASSWORD doit faire au moins ${MIN_PASSWORD_LENGTH} caractères.`);
|
|
52
|
+
return fromEnv;
|
|
53
|
+
}
|
|
54
|
+
if (nonInteractive)
|
|
55
|
+
fail("Mode non interactif : ORGANIZER_ADMIN_PASSWORD est requis.");
|
|
56
|
+
for (;;) {
|
|
57
|
+
const first = await askSecret(`Mot de passe (${MIN_PASSWORD_LENGTH} caractères minimum)`);
|
|
58
|
+
if (first.length < MIN_PASSWORD_LENGTH) {
|
|
59
|
+
process.stderr.write(` ! Le mot de passe doit faire au moins ${MIN_PASSWORD_LENGTH} caractères.\n`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const second = await askSecret("Confirmation");
|
|
63
|
+
if (first !== second) {
|
|
64
|
+
process.stderr.write(" ! Les deux saisies diffèrent.\n");
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
return first;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Nom affiché à poser : celui de `--name` ou de `ORGANIZER_ADMIN_NAME`, sinon
|
|
72
|
+
* la saisie quand le compte n'en a pas encore. Undefined garde le nom actuel.
|
|
73
|
+
*/
|
|
74
|
+
async function readName(store, email, args) {
|
|
75
|
+
const given = args.name ?? process.env.ORGANIZER_ADMIN_NAME?.trim();
|
|
76
|
+
if (given)
|
|
77
|
+
return given;
|
|
78
|
+
if (store.findUserByEmail(email.trim().toLowerCase())?.displayName)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (args.nonInteractive) {
|
|
81
|
+
fail('Mode non interactif : le nom affiché du compte est requis. Passez --name="Prénom Nom" ou définissez ORGANIZER_ADMIN_NAME.', "", ...USAGE);
|
|
82
|
+
}
|
|
83
|
+
return ask("Nom affiché (ex. Matthieu Pin)");
|
|
84
|
+
}
|
|
85
|
+
/** Exécute la commande ; renvoie le code de sortie du processus. */
|
|
86
|
+
export async function runSetUser(argv, dataDir) {
|
|
87
|
+
try {
|
|
88
|
+
const args = parseArgs(argv);
|
|
89
|
+
let email = args.email ?? process.env.ORGANIZER_ADMIN_EMAIL?.trim();
|
|
90
|
+
if (!email) {
|
|
91
|
+
if (args.nonInteractive)
|
|
92
|
+
fail("Mode non interactif : passez l'adresse en argument ou définissez ORGANIZER_ADMIN_EMAIL.", "", ...USAGE);
|
|
93
|
+
process.stdout.write("Compte à créer, ou compte dont remplacer le mot de passe.\n");
|
|
94
|
+
email = await ask("Adresse e-mail");
|
|
95
|
+
}
|
|
96
|
+
const store = Store.open(dataDir);
|
|
97
|
+
try {
|
|
98
|
+
const displayName = await readName(store, email, args);
|
|
99
|
+
const password = await readPassword(args.nonInteractive);
|
|
100
|
+
const { created, user } = await setAccount(store, { email, password, displayName });
|
|
101
|
+
process.stdout.write(created
|
|
102
|
+
? `Compte ${user.email} (${user.displayName}) créé, avec son espace personnel (données : ${dataDir}).\n`
|
|
103
|
+
: `Compte ${user.email} (${user.displayName}) : mot de passe remplacé, compte actif, sessions ouvertes fermées.\n`);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
store.close();
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
process.stderr.write(`${err?.message ?? err}\n`);
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SetupOptions {
|
|
2
|
+
force: boolean;
|
|
3
|
+
/** URL publique du site, sans barre oblique finale. */
|
|
4
|
+
url: string;
|
|
5
|
+
token: string;
|
|
6
|
+
}
|
|
7
|
+
/** Lit les options de la commande ; une option inconnue, absente ou invalide est une erreur. */
|
|
8
|
+
export declare function parseSetupArgs(argv: readonly string[]): SetupOptions;
|
|
9
|
+
/** Exécute la commande dans `cwd` ; renvoie le code de sortie. */
|
|
10
|
+
export declare function runSetupMcp(argv: readonly string[], cwd: string): number;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { SERVER_NAME, buildAgentEntry } from "../core/mcp-entry.js";
|
|
4
|
+
import { AGENT_TOKEN_PREFIX } from "../../shared/schemas/tokens-api.schema.js";
|
|
5
|
+
/**
|
|
6
|
+
* Commande `mcp-organizer --claude-setup-mcp` : écrit ou fusionne l'entrée
|
|
7
|
+
* `mcp-organizer` de `<cwd>/.mcp.json` (mode MCP relié à un site par son URL
|
|
8
|
+
* et un jeton d'agent), en conservant les autres serveurs. Un fichier
|
|
9
|
+
* illisible fait échouer la commande sans rien écrire.
|
|
10
|
+
*/
|
|
11
|
+
const USAGE = "Usage : npx -y @imenam/mcp-organizer --claude-setup-mcp [--force] --url=<URL du site> --token=<jeton d'agent>";
|
|
12
|
+
/** Valeur d'une option `--nom=valeur`, ou undefined si elle est absente. */
|
|
13
|
+
function readFlag(argv, name) {
|
|
14
|
+
const prefix = `--${name}=`;
|
|
15
|
+
const values = argv.filter((arg) => arg.startsWith(prefix)).map((arg) => arg.slice(prefix.length).trim());
|
|
16
|
+
if (values.length > 1)
|
|
17
|
+
throw new Error(`--${name} est donné plusieurs fois.`);
|
|
18
|
+
return values[0];
|
|
19
|
+
}
|
|
20
|
+
function checkUrl(raw) {
|
|
21
|
+
if (raw === undefined || raw === "")
|
|
22
|
+
throw new Error(`--url est requis : l'URL publique du site mcp-organizer (ex : https://organizer.example.com).\n${USAGE}`);
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = new URL(raw);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error(`--url invalide : « ${raw} » n'est pas une URL absolue (ex : https://organizer.example.com).`);
|
|
29
|
+
}
|
|
30
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
31
|
+
throw new Error(`--url invalide : protocole « ${parsed.protocol} » non pris en charge (http ou https attendu).`);
|
|
32
|
+
if (parsed.search || parsed.hash || parsed.username || parsed.password)
|
|
33
|
+
throw new Error(`--url invalide : ni identifiants, ni paramètres, ni fragment (${raw}).`);
|
|
34
|
+
return parsed.href.replace(/\/+$/, "");
|
|
35
|
+
}
|
|
36
|
+
function checkToken(raw) {
|
|
37
|
+
if (raw === undefined || raw === "")
|
|
38
|
+
throw new Error(`--token est requis : un jeton d'agent créé sur le site (Paramètres > Agents).\n${USAGE}`);
|
|
39
|
+
if (!raw.startsWith(AGENT_TOKEN_PREFIX))
|
|
40
|
+
throw new Error(`--token invalide : un jeton d'agent commence par « ${AGENT_TOKEN_PREFIX} ». Copiez-le depuis Paramètres > Agents.`);
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
/** Lit les options de la commande ; une option inconnue, absente ou invalide est une erreur. */
|
|
44
|
+
export function parseSetupArgs(argv) {
|
|
45
|
+
const unknown = argv.filter((arg) => arg !== "--claude-setup-mcp" && arg !== "--force" && !arg.startsWith("--url=") && !arg.startsWith("--token="));
|
|
46
|
+
if (unknown.length > 0)
|
|
47
|
+
throw new Error(`Option inconnue : ${unknown.join(", ")}.\n${USAGE}`);
|
|
48
|
+
return { force: argv.includes("--force"), url: checkUrl(readFlag(argv, "url")), token: checkToken(readFlag(argv, "token")) };
|
|
49
|
+
}
|
|
50
|
+
/** Contenu actuel de `.mcp.json` : un objet, ou une erreur qui dit quoi corriger. */
|
|
51
|
+
function readConfig(target) {
|
|
52
|
+
if (!fs.existsSync(target))
|
|
53
|
+
return { config: {}, exists: false };
|
|
54
|
+
let raw;
|
|
55
|
+
try {
|
|
56
|
+
raw = fs.readFileSync(target, "utf-8");
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
throw new Error(`${target} est illisible (${err?.message ?? err}). Corrigez ses droits, puis relancez la commande.`);
|
|
60
|
+
}
|
|
61
|
+
if (raw.trim() === "")
|
|
62
|
+
return { config: {}, exists: true };
|
|
63
|
+
let config;
|
|
64
|
+
try {
|
|
65
|
+
config = JSON.parse(raw);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
throw new Error(`${target} contient du JSON invalide (${err?.message ?? err}). Corrigez le fichier ou supprimez-le, puis relancez la commande.`);
|
|
69
|
+
}
|
|
70
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
71
|
+
throw new Error(`${target} ne contient pas un objet JSON. Corrigez le fichier ou supprimez-le, puis relancez la commande.`);
|
|
72
|
+
}
|
|
73
|
+
const servers = config.mcpServers;
|
|
74
|
+
if (servers !== undefined && (typeof servers !== "object" || servers === null || Array.isArray(servers))) {
|
|
75
|
+
throw new Error(`${target} : « mcpServers » n'est pas un objet. Corrigez le fichier, puis relancez la commande.`);
|
|
76
|
+
}
|
|
77
|
+
return { config: config, exists: true };
|
|
78
|
+
}
|
|
79
|
+
/** Exécute la commande dans `cwd` ; renvoie le code de sortie. */
|
|
80
|
+
export function runSetupMcp(argv, cwd) {
|
|
81
|
+
try {
|
|
82
|
+
const options = parseSetupArgs(argv);
|
|
83
|
+
const target = path.join(cwd, ".mcp.json");
|
|
84
|
+
const { config, exists } = readConfig(target);
|
|
85
|
+
const servers = config.mcpServers ?? {};
|
|
86
|
+
const alreadyPresent = Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);
|
|
87
|
+
if (alreadyPresent && !options.force) {
|
|
88
|
+
throw new Error(`Le serveur « ${SERVER_NAME} » est déjà configuré dans ${target}.\nRelancez avec --force pour remplacer cette entrée (les autres serveurs sont conservés).`);
|
|
89
|
+
}
|
|
90
|
+
config.mcpServers = { ...servers, [SERVER_NAME]: buildAgentEntry({ url: options.url, token: options.token }) };
|
|
91
|
+
fs.writeFileSync(target, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
92
|
+
const action = alreadyPresent ? "remplacée" : exists ? "ajoutée" : "créée";
|
|
93
|
+
const lines = [
|
|
94
|
+
`Configuration ${SERVER_NAME} ${action} dans ${target}`,
|
|
95
|
+
"",
|
|
96
|
+
"Variables du champ « env » :",
|
|
97
|
+
` ORGANIZER_URL ${options.url} URL publique du site`,
|
|
98
|
+
` ORGANIZER_TOKEN ${options.token.slice(0, AGENT_TOKEN_PREFIX.length + 8)}… jeton d'agent : il couvre un espace, avec les droits de son créateur`,
|
|
99
|
+
"",
|
|
100
|
+
"Le jeton est un secret : .mcp.json ne doit pas être partagé ni versionné. Il se révoque depuis Paramètres > Agents.",
|
|
101
|
+
"",
|
|
102
|
+
];
|
|
103
|
+
process.stdout.write(lines.join("\n"));
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
process.stderr.write(`Erreur : ${err?.message ?? err}\n`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commande `mcp-organizer --setup` : installation du site.
|
|
3
|
+
*
|
|
4
|
+
* Elle demande (ou reçoit par options et variables `ORGANIZER_SETUP_*`) l'URL
|
|
5
|
+
* publique, l'écoute, le répertoire de données, la confiance dans le reverse
|
|
6
|
+
* proxy, la dictée (Groq) et l'e-mail (SMTP), puis le premier compte. Elle
|
|
7
|
+
* écrit le `.env` (fusionné avec l'existant, lisible par son seul
|
|
8
|
+
* propriétaire), l'unité systemd et le bloc nginx dans un dossier de sortie,
|
|
9
|
+
* crée le compte et affiche les étapes suivantes. Elle n'écrit aucun fichier
|
|
10
|
+
* système.
|
|
11
|
+
*
|
|
12
|
+
* Les secrets (clé Groq, URL SMTP, mot de passe) ne se passent jamais en
|
|
13
|
+
* argument : ils sont saisis sans écho, ou lus dans leur variable
|
|
14
|
+
* `ORGANIZER_SETUP_*`. Une valeur existante du `.env` est proposée par défaut
|
|
15
|
+
* et conservée en mode non interactif ; une valeur requise absente fait
|
|
16
|
+
* échouer la commande.
|
|
17
|
+
*/
|
|
18
|
+
/** Modèle de transcription Groq écrit par le setup. */
|
|
19
|
+
export declare const TRANSCRIBE_MODEL = "whisper-large-v3-turbo";
|
|
20
|
+
/** Compte système proposé pour exécuter le site. */
|
|
21
|
+
export declare const DEFAULT_SERVICE_USER = "mcp-organizer";
|
|
22
|
+
/** Fichiers écrits dans le dossier de sortie. */
|
|
23
|
+
export declare const SYSTEMD_UNIT_FILENAME = "mcp-organizer.service";
|
|
24
|
+
export declare const NGINX_FILENAME = "mcp-organizer.nginx.conf";
|
|
25
|
+
/** Options à valeur (`--nom=valeur`), chacune avec sa variable `ORGANIZER_SETUP_NOM`. */
|
|
26
|
+
declare const VALUE_OPTIONS: readonly ["public-url", "port", "host", "data-dir", "trust-proxy", "mail-from", "admin-email", "admin-name", "output-dir", "install-dir", "service-user", "node"];
|
|
27
|
+
type ValueOption = (typeof VALUE_OPTIONS)[number];
|
|
28
|
+
export declare const SETUP_USAGE = "Usage : mcp-organizer --setup [options]\n\nInstallation du site : \u00E9crit le .env, l'unit\u00E9 systemd et le bloc nginx, et cr\u00E9e\nle premier compte. Sans terminal, ou avec --non-interactive, rien n'est demand\u00E9 :\nchaque valeur requise vient d'une option ou de sa variable ORGANIZER_SETUP_<NOM>\n(--public-url \u2192 ORGANIZER_SETUP_PUBLIC_URL), sinon du .env existant.\n\n --public-url=<url> URL ouverte par les utilisateurs (https, ou http pour un h\u00F4te local)\n --port=<n> port d'\u00E9coute du site\n --host=<adresse> adresse d'\u00E9coute [127.0.0.1]\n --data-dir=<chemin> r\u00E9pertoire de donn\u00E9es [<package>/.organizer-data]\n --trust-proxy=oui|non lire l'adresse du client dans X-Forwarded-For (nginx)\n --mail-from=<adresse> exp\u00E9diteur des e-mails, avec SMTP\n --no-groq d\u00E9sactiver la dict\u00E9e vocale\n --no-smtp d\u00E9sactiver l'envoi d'e-mails\n --admin-email=<adresse> compte \u00E0 cr\u00E9er, ou dont remplacer le mot de passe\n --admin-name=<nom> nom affich\u00E9 de ce compte\n --dotenv=<fichier> fichier .env \u00E0 \u00E9crire [<package>/.env]\n --output-dir=<chemin> dossier de l'unit\u00E9 systemd et du bloc nginx [<package>/deploy]\n --install-dir=<chemin> r\u00E9pertoire du d\u00E9p\u00F4t dans l'unit\u00E9 [<package>]\n --service-user=<compte> compte syst\u00E8me de l'unit\u00E9 [mcp-organizer]\n --node=<chemin> ex\u00E9cutable Node.js de l'unit\u00E9 [node courant]\n --non-interactive ne rien demander\n\nSecrets, jamais en argument : saisis sans \u00E9cho, ou lus dans\n ORGANIZER_SETUP_GROQ_API_KEY cl\u00E9 de l'API Groq (dict\u00E9e)\n ORGANIZER_SETUP_SMTP_URL smtp://utilisateur:motdepasse@hote:587 ou smtps://\u2026\n ORGANIZER_SETUP_ADMIN_PASSWORD mot de passe du compte (8 caract\u00E8res minimum)";
|
|
29
|
+
interface ParsedArgs {
|
|
30
|
+
values: Partial<Record<ValueOption, string>>;
|
|
31
|
+
nonInteractive: boolean;
|
|
32
|
+
noGroq: boolean;
|
|
33
|
+
noSmtp: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Lit les options ; une option inconnue, répétée ou secrète est refusée. */
|
|
36
|
+
export declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
37
|
+
/** Exécute la commande ; renvoie le code de sortie du processus. */
|
|
38
|
+
export declare function runSetup(argv: readonly string[], rootDir: string): Promise<number>;
|
|
39
|
+
export {};
|