@imenam/mcp-documentor 1.0.74 → 1.0.76

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 (66) hide show
  1. package/README.md +271 -259
  2. package/dist/shared/schemas/doc.schema.d.ts +1 -4
  3. package/dist/shared/schemas/doc.schema.js +1 -4
  4. package/dist/src/cli/setup-mcp.d.ts +20 -1
  5. package/dist/src/cli/setup-mcp.js +65 -6
  6. package/dist/src/core/storage-engine.d.ts +14 -15
  7. package/dist/src/core/storage-engine.js +15 -51
  8. package/dist/src/gui-worker.js +91 -34
  9. package/dist/src/index.js +99 -106
  10. package/dist/src/tools/check-links.js +1 -1
  11. package/dist/src/tools/list-projects.js +7 -5
  12. package/dist/src/version.d.ts +1 -1
  13. package/dist/src/version.js +1 -1
  14. package/gui/dist/assets/{arc-BNuZ3dSL.js → arc-CsM5OJTM.js} +1 -1
  15. package/gui/dist/assets/{architectureDiagram-3BPJPVTR-D8g3tPQx.js → architectureDiagram-3BPJPVTR-BwIu1llT.js} +1 -1
  16. package/gui/dist/assets/{blockDiagram-GPEHLZMM-limhup8_.js → blockDiagram-GPEHLZMM-BVijVnPI.js} +1 -1
  17. package/gui/dist/assets/{c4Diagram-AAUBKEIU-Ct39JMIy.js → c4Diagram-AAUBKEIU-D-rcZUZu.js} +1 -1
  18. package/gui/dist/assets/channel-DKPwwuQf.js +1 -0
  19. package/gui/dist/assets/{chunk-2J33WTMH-BkLJTezj.js → chunk-2J33WTMH-ByfTGREe.js} +1 -1
  20. package/gui/dist/assets/{chunk-4BX2VUAB-CR5NbhDR.js → chunk-4BX2VUAB-B_5oCXyz.js} +1 -1
  21. package/gui/dist/assets/{chunk-55IACEB6-CtQYMnJS.js → chunk-55IACEB6-Bc2Kq9HD.js} +1 -1
  22. package/gui/dist/assets/{chunk-727SXJPM-B2q7tUn8.js → chunk-727SXJPM-CVwmKEMQ.js} +1 -1
  23. package/gui/dist/assets/{chunk-AQP2D5EJ-Bbcnrfrp.js → chunk-AQP2D5EJ-BkMuEFxD.js} +1 -1
  24. package/gui/dist/assets/{chunk-FMBD7UC4-D8gd4zRe.js → chunk-FMBD7UC4-CU7EDl9I.js} +1 -1
  25. package/gui/dist/assets/{chunk-ND2GUHAM-DiZKzZtG.js → chunk-ND2GUHAM-1b34Dtkn.js} +1 -1
  26. package/gui/dist/assets/{chunk-QZHKN3VN-CrKx-e2_.js → chunk-QZHKN3VN-DnUfMecI.js} +1 -1
  27. package/gui/dist/assets/classDiagram-4FO5ZUOK-CnCvVGW9.js +1 -0
  28. package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-CnCvVGW9.js +1 -0
  29. package/gui/dist/assets/{cose-bilkent-S5V4N54A-BDUCVkTl.js → cose-bilkent-S5V4N54A-D9OBrxKr.js} +1 -1
  30. package/gui/dist/assets/{dagre-BM42HDAG-DkoYBHXK.js → dagre-BM42HDAG-zDqwL8xs.js} +1 -1
  31. package/gui/dist/assets/{diagram-2AECGRRQ-BT1YJHon.js → diagram-2AECGRRQ-ejbP8WIj.js} +1 -1
  32. package/gui/dist/assets/{diagram-5GNKFQAL-C00r6F35.js → diagram-5GNKFQAL-Cv9WmtFO.js} +1 -1
  33. package/gui/dist/assets/{diagram-KO2AKTUF-B4wcN5a_.js → diagram-KO2AKTUF-BI0ybJiN.js} +1 -1
  34. package/gui/dist/assets/{diagram-LMA3HP47-B9FqCfel.js → diagram-LMA3HP47-DlVGttLq.js} +1 -1
  35. package/gui/dist/assets/{diagram-OG6HWLK6-COPCDzdG.js → diagram-OG6HWLK6-DjoAH-Fq.js} +1 -1
  36. package/gui/dist/assets/{erDiagram-TEJ5UH35-BGQGT2wN.js → erDiagram-TEJ5UH35-BVQHVGK4.js} +1 -1
  37. package/gui/dist/assets/{flowDiagram-I6XJVG4X-A7jKWUpi.js → flowDiagram-I6XJVG4X-oZP2Z-6T.js} +1 -1
  38. package/gui/dist/assets/{ganttDiagram-6RSMTGT7-NhFkpY-x.js → ganttDiagram-6RSMTGT7-dy4QyEOP.js} +1 -1
  39. package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-z0AqyYG4.js → gitGraphDiagram-PVQCEYII-BxgLy7Ln.js} +1 -1
  40. package/gui/dist/assets/{index-Dub5OLKk.js → index-Dtv0slFQ.js} +66 -66
  41. package/gui/dist/assets/{infoDiagram-5YYISTIA-D69UR9u9.js → infoDiagram-5YYISTIA-DNOdinpR.js} +1 -1
  42. package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-DSTqIoro.js → ishikawaDiagram-YF4QCWOH-C4pQ434G.js} +1 -1
  43. package/gui/dist/assets/{journeyDiagram-JHISSGLW-CekXdcoE.js → journeyDiagram-JHISSGLW-CBrVYW94.js} +1 -1
  44. package/gui/dist/assets/{kanban-definition-UN3LZRKU-CseGvFK9.js → kanban-definition-UN3LZRKU-Df-rgiY8.js} +1 -1
  45. package/gui/dist/assets/{linear-BtVl_9_C.js → linear-B_6fRXVt.js} +1 -1
  46. package/gui/dist/assets/{mindmap-definition-RKZ34NQL-XLjQIU37.js → mindmap-definition-RKZ34NQL-CRAd_VDM.js} +1 -1
  47. package/gui/dist/assets/{pieDiagram-4H26LBE5-BcvUzWoW.js → pieDiagram-4H26LBE5-CJkmLrDI.js} +1 -1
  48. package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-BzoYEmNW.js → quadrantDiagram-W4KKPZXB-D-rDEcnF.js} +1 -1
  49. package/gui/dist/assets/{requirementDiagram-4Y6WPE33-icKC1_Nd.js → requirementDiagram-4Y6WPE33-DE8MhSMe.js} +1 -1
  50. package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-mi3i-2dZ.js → sankeyDiagram-5OEKKPKP-BwlxtrUs.js} +1 -1
  51. package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-XrcDkRlM.js → sequenceDiagram-3UESZ5HK-B4LjBzWg.js} +1 -1
  52. package/gui/dist/assets/{stateDiagram-AJRCARHV-UXaqrCNT.js → stateDiagram-AJRCARHV-Bv72afPK.js} +1 -1
  53. package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-BKJDOkkR.js +1 -0
  54. package/gui/dist/assets/{timeline-definition-PNZ67QCA-yd6607p6.js → timeline-definition-PNZ67QCA-CBxgf8iW.js} +1 -1
  55. package/gui/dist/assets/{vennDiagram-CIIHVFJN-BsRqUcIp.js → vennDiagram-CIIHVFJN-DPSdK06q.js} +1 -1
  56. package/gui/dist/assets/{wardley-L42UT6IY-BmdA7yk0.js → wardley-L42UT6IY-JIAzuoPm.js} +1 -1
  57. package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-CRdmckD8.js → wardleyDiagram-YWT4CUSO-YDfaF1wT.js} +1 -1
  58. package/gui/dist/assets/{xychartDiagram-2RQKCTM6-DWpNIQY-.js → xychartDiagram-2RQKCTM6-DHlPryZX.js} +1 -1
  59. package/gui/dist/index.html +1 -1
  60. package/package.json +1 -1
  61. package/dist/src/tools/set-project.d.ts +0 -14
  62. package/dist/src/tools/set-project.js +0 -18
  63. package/gui/dist/assets/channel-uoEIgtym.js +0 -1
  64. package/gui/dist/assets/classDiagram-4FO5ZUOK-DmhYb54l.js +0 -1
  65. package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-DmhYb54l.js +0 -1
  66. package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-Bhdu4lv4.js +0 -1
@@ -4,8 +4,9 @@ const SERVER_NAME = "mcp-documentor";
4
4
  const PACKAGE_NAME = "@imenam/mcp-documentor";
5
5
  /**
6
6
  * Valeurs volontairement vides : chaque variable est inactive tant qu'elle n'est
7
- * pas remplie (PROXY_URL vide = GUI désactivée, clé vide = pas d'embeddings),
8
- * donc le fichier généré est utilisable tel quel, jamais à moitié cassé.
7
+ * pas remplie (PROXY_URL et APP_PORT vides = GUI désactivée, clé vide = pas
8
+ * d'embeddings), donc le fichier généré est utilisable tel quel, jamais à
9
+ * moitié cassé.
9
10
  */
10
11
  /** Slashes POSIX : évite les backslashes échappés dans le JSON, et Node les accepte sous Windows. */
11
12
  function toPosixPath(dir) {
@@ -23,14 +24,18 @@ function toDisplayName(folder) {
23
24
  .map(capitalize)
24
25
  .join(" ");
25
26
  }
26
- function buildServerEntry(codeRoot) {
27
+ function buildServerEntry(codeRoot, exposure) {
27
28
  // Le nom du dossier courant sert de base aux identifiants d'affichage côté proxy.
28
29
  const folder = path.basename(codeRoot) || "documentor";
30
+ // Un port explicite désactive le placeholder proxy : les deux variables ne
31
+ // doivent jamais être renseignées ensemble, PROXY_URL primant au runtime.
32
+ const proxyUrl = exposure.appPort !== undefined ? "" : exposure.proxyUrl ?? "http://localhost:";
29
33
  return {
30
34
  command: "npx",
31
35
  args: ["-y", PACKAGE_NAME],
32
36
  env: {
33
- PROXY_URL: "http://localhost:",
37
+ PROXY_URL: proxyUrl,
38
+ APP_PORT: exposure.appPort !== undefined ? String(exposure.appPort) : "",
34
39
  APP_GROUP: capitalize(folder),
35
40
  APP_NAME: `Documentor ${toDisplayName(folder)}`,
36
41
  APP_PATH: `/documentor-${folder}`,
@@ -47,6 +52,7 @@ function buildServerEntry(codeRoot) {
47
52
  }
48
53
  const GENERATED_VARS = [
49
54
  ["PROXY_URL", "URL du proxy pour exposer la GUI (compléter le port)"],
55
+ ["APP_PORT", "port local si aucun proxy — la GUI est servie sur http://localhost:<port>"],
50
56
  ["APP_GROUP", "groupe d'affichage dans le dashboard du proxy"],
51
57
  ["APP_NAME", "nom affiché de la GUI dans le dashboard du proxy"],
52
58
  ["APP_PATH", "chemin de montage de la GUI derrière le proxy"],
@@ -64,7 +70,53 @@ const OPTIONAL_VARS = [
64
70
  ["READ_ONLY", "1/true pour désactiver toutes les écritures"],
65
71
  ["SYNC_PULL_ON_START", "false pour désactiver le pull au démarrage"],
66
72
  ];
67
- export function runSetupMcp({ cwd, force }) {
73
+ /** Lit `--flag=valeur` (tirets ou underscores acceptés). */
74
+ function readFlag(argv, name) {
75
+ const prefixes = [`--${name}=`, `--${name.replace(/_/g, "-")}=`];
76
+ for (const arg of argv) {
77
+ const prefix = prefixes.find((p) => arg.startsWith(p));
78
+ if (prefix)
79
+ return arg.slice(prefix.length).trim();
80
+ }
81
+ return undefined;
82
+ }
83
+ /**
84
+ * Traduit `--proxy_url=` / `--app_port=` en mode d'exposition de la GUI.
85
+ * Les deux ensemble sont refusés : au runtime PROXY_URL primerait toujours,
86
+ * donc un fichier portant les deux serait silencieusement trompeur.
87
+ */
88
+ export function parseGuiExposure(argv) {
89
+ const rawProxyUrl = readFlag(argv, "proxy_url");
90
+ const rawAppPort = readFlag(argv, "app_port");
91
+ if (rawProxyUrl !== undefined && rawAppPort !== undefined) {
92
+ return { error: "--proxy_url et --app_port sont exclusifs : la GUI passe par le proxy, ou écoute sur un port local." };
93
+ }
94
+ if (rawProxyUrl !== undefined) {
95
+ if (rawProxyUrl === "")
96
+ return { error: "--proxy_url attend une URL, ex : --proxy_url=http://localhost:3000" };
97
+ let parsed;
98
+ try {
99
+ parsed = new URL(rawProxyUrl);
100
+ }
101
+ catch {
102
+ return { error: `--proxy_url invalide : "${rawProxyUrl}" n'est pas une URL absolue (ex : http://localhost:3000).` };
103
+ }
104
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
105
+ return { error: `--proxy_url invalide : protocole "${parsed.protocol}" non supporté (http ou https attendu).` };
106
+ }
107
+ // Une barre finale casserait la concaténation PROXY_URL + APP_PATH.
108
+ return { exposure: { proxyUrl: rawProxyUrl.replace(/\/+$/, "") } };
109
+ }
110
+ if (rawAppPort !== undefined) {
111
+ const port = Number(rawAppPort);
112
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
113
+ return { error: `--app_port invalide : "${rawAppPort}" n'est pas un entier entre 1 et 65535.` };
114
+ }
115
+ return { exposure: { appPort: port } };
116
+ }
117
+ return { exposure: {} };
118
+ }
119
+ export function runSetupMcp({ cwd, force, exposure = {} }) {
68
120
  const target = path.join(cwd, ".mcp.json");
69
121
  let config = {};
70
122
  const exists = fs.existsSync(target);
@@ -94,11 +146,17 @@ export function runSetupMcp({ cwd, force }) {
94
146
  console.error("Relancez avec --force pour écraser cette entrée (les autres serveurs sont préservés).");
95
147
  return 1;
96
148
  }
97
- config.mcpServers[SERVER_NAME] = buildServerEntry(cwd);
149
+ config.mcpServers[SERVER_NAME] = buildServerEntry(cwd, exposure);
98
150
  fs.writeFileSync(target, JSON.stringify(config, null, 2) + "\n", "utf-8");
99
151
  const action = alreadyPresent ? "remplacée" : exists ? "ajoutée" : "créée";
100
152
  console.log(`Configuration ${SERVER_NAME} ${action} dans ${target}`);
101
153
  console.log("");
154
+ if (exposure.appPort !== undefined) {
155
+ console.log(`GUI en accès direct : http://localhost:${exposure.appPort} (sans proxy).`);
156
+ }
157
+ else if (exposure.proxyUrl !== undefined) {
158
+ console.log(`GUI exposée via le proxy ${exposure.proxyUrl}.`);
159
+ }
102
160
  console.log('À compléter dans le champ "env" :');
103
161
  for (const [name, description] of GENERATED_VARS) {
104
162
  console.log(` ${name.padEnd(30)} ${description}`);
@@ -110,5 +168,6 @@ export function runSetupMcp({ cwd, force }) {
110
168
  }
111
169
  console.log("");
112
170
  console.log("Une variable laissée vide est simplement inactive.");
171
+ console.log("PROXY_URL et APP_PORT s'excluent : PROXY_URL prime si les deux sont renseignés.");
113
172
  return 0;
114
173
  }
@@ -1,7 +1,6 @@
1
1
  import type { DocEntry, DocSummary, ExportBundle } from "../../shared/schemas/doc.schema.js";
2
2
  import type { HistoryStore } from "./history-store.js";
3
3
  interface GlobalConfig {
4
- currentProject: string;
5
4
  projects: string[];
6
5
  foldersMigrated?: boolean;
7
6
  }
@@ -22,16 +21,16 @@ export declare class StorageEngine {
22
21
  constructor(rootDir: string);
23
22
  setHistoryStore(store: HistoryStore): void;
24
23
  getDataDir(): string;
25
- getProjectDir(project?: string): string;
24
+ /**
25
+ * There is no "current project" fallback: every caller names the project it
26
+ * targets. The UI's project selector is a client-side view preference only.
27
+ */
28
+ getProjectDir(project: string): string;
26
29
  private getConfigPath;
27
- private getLocalStatePath;
28
- private readLocalState;
29
- private writeLocalState;
30
30
  /**
31
- * Returns the merged config. The shared part (projects list, migration flags)
32
- * comes from config.json (versioned), while currentProject is a machine-local
33
- * preference sourced from local-state.json (gitignored). Legacy config.json
34
- * files that still embed currentProject are migrated transparently.
31
+ * Reads config.json (versioned): the project list and migration flags.
32
+ * A legacy currentProject key is stripped on read — project selection is a
33
+ * per-client UI concern and is no longer server state.
35
34
  */
36
35
  getConfig(): GlobalConfig;
37
36
  saveConfig(config: GlobalConfig): void;
@@ -41,13 +40,13 @@ export declare class StorageEngine {
41
40
  private walkMdFiles;
42
41
  private docIdToFilePath;
43
42
  private cleanEmptyDirs;
44
- saveDoc(doc: DocEntry, project?: string, changeType?: "create" | "update" | "patch"): Promise<void>;
45
- getDoc(id: string, project?: string): Promise<DocEntry | null>;
46
- listDocs(project?: string): Promise<DocSummary[]>;
47
- deleteDoc(id: string, project?: string): Promise<boolean>;
48
- moveDoc(oldId: string, newId: string, project?: string): Promise<boolean>;
43
+ saveDoc(doc: DocEntry, project: string, changeType?: "create" | "update" | "patch"): Promise<void>;
44
+ getDoc(id: string, project: string): Promise<DocEntry | null>;
45
+ listDocs(project: string): Promise<DocSummary[]>;
46
+ deleteDoc(id: string, project: string): Promise<boolean>;
47
+ moveDoc(oldId: string, newId: string, project: string): Promise<boolean>;
49
48
  getBundles(): ExportBundle[];
50
49
  saveBundles(bundles: ExportBundle[]): void;
51
- rewriteLinks(oldId: string, newId: string, project?: string): Promise<string[]>;
50
+ rewriteLinks(oldId: string, newId: string, project: string): Promise<string[]>;
52
51
  }
53
52
  export {};
@@ -19,39 +19,20 @@ export class StorageEngine {
19
19
  const envDir = process.env.MCP_DATA_DIR;
20
20
  return envDir && envDir.trim() !== "" ? envDir : path.join(this.rootDir, ".docs-data");
21
21
  }
22
+ /**
23
+ * There is no "current project" fallback: every caller names the project it
24
+ * targets. The UI's project selector is a client-side view preference only.
25
+ */
22
26
  getProjectDir(project) {
23
- const config = this.getConfig();
24
- const proj = project ?? config.currentProject;
25
- return path.join(this.getDataDir(), "projects", proj);
27
+ return path.join(this.getDataDir(), "projects", project);
26
28
  }
27
29
  getConfigPath() {
28
30
  return path.join(this.getDataDir(), "config.json");
29
31
  }
30
- getLocalStatePath() {
31
- return path.join(this.getDataDir(), "local-state.json");
32
- }
33
- readLocalState() {
34
- const p = this.getLocalStatePath();
35
- if (fs.existsSync(p)) {
36
- try {
37
- return JSON.parse(fs.readFileSync(p, "utf-8"));
38
- }
39
- catch {
40
- logger.warn("Failed to parse local-state.json, using defaults");
41
- }
42
- }
43
- return {};
44
- }
45
- writeLocalState(state) {
46
- const p = this.getLocalStatePath();
47
- fs.mkdirSync(path.dirname(p), { recursive: true });
48
- fs.writeFileSync(p, JSON.stringify(state, null, 2), "utf-8");
49
- }
50
32
  /**
51
- * Returns the merged config. The shared part (projects list, migration flags)
52
- * comes from config.json (versioned), while currentProject is a machine-local
53
- * preference sourced from local-state.json (gitignored). Legacy config.json
54
- * files that still embed currentProject are migrated transparently.
33
+ * Reads config.json (versioned): the project list and migration flags.
34
+ * A legacy currentProject key is stripped on read — project selection is a
35
+ * per-client UI concern and is no longer server state.
55
36
  */
56
37
  getConfig() {
57
38
  const configPath = this.getConfigPath();
@@ -64,35 +45,20 @@ export class StorageEngine {
64
45
  logger.warn("Failed to parse config.json, using defaults");
65
46
  }
66
47
  }
67
- const local = this.readLocalState();
68
- const projects = shared.projects ?? ["default"];
69
- // Migrate a legacy currentProject embedded in config.json into local-state.
70
- if (shared.currentProject !== undefined && local.currentProject === undefined) {
71
- local.currentProject = shared.currentProject;
72
- this.writeLocalState(local);
48
+ if (shared.currentProject !== undefined) {
73
49
  delete shared.currentProject;
74
50
  fs.writeFileSync(configPath, JSON.stringify(shared, null, 2), "utf-8");
75
- logger.info("Migrated currentProject from config.json to local-state.json");
51
+ logger.info("Dropped obsolete currentProject from config.json");
76
52
  }
77
53
  return {
78
- currentProject: local.currentProject ?? projects[0] ?? "default",
79
- projects,
54
+ projects: shared.projects ?? ["default"],
80
55
  foldersMigrated: shared.foldersMigrated,
81
56
  };
82
57
  }
83
58
  saveConfig(config) {
84
- // Shared, versioned part → config.json (currentProject is intentionally excluded).
85
59
  const configPath = this.getConfigPath();
86
- const shared = {
87
- projects: config.projects,
88
- foldersMigrated: config.foldersMigrated,
89
- };
90
60
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
91
- fs.writeFileSync(configPath, JSON.stringify(shared, null, 2), "utf-8");
92
- // Machine-local part → local-state.json.
93
- const local = this.readLocalState();
94
- local.currentProject = config.currentProject;
95
- this.writeLocalState(local);
61
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
96
62
  }
97
63
  /** Reads sync settings from environment variables (admin-controlled, not agent-settable). */
98
64
  getSyncConfig() {
@@ -146,8 +112,7 @@ export class StorageEngine {
146
112
  }
147
113
  }
148
114
  async saveDoc(doc, project, changeType = "update") {
149
- const config = this.getConfig();
150
- const proj = project ?? config.currentProject;
115
+ const proj = project;
151
116
  const projectDir = this.getProjectDir(project);
152
117
  const targetPath = this.docIdToFilePath(doc.id, projectDir);
153
118
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
@@ -220,8 +185,7 @@ export class StorageEngine {
220
185
  // existed in several. Doc-targeted callers must name their project and go through
221
186
  // getDoc(id, project) — see core/project-scope.ts.
222
187
  async deleteDoc(id, project) {
223
- const config = this.getConfig();
224
- const proj = project ?? config.currentProject;
188
+ const proj = project;
225
189
  const projectDir = this.getProjectDir(project);
226
190
  const filePath = this.docIdToFilePath(id, projectDir);
227
191
  if (!fs.existsSync(filePath))
@@ -277,7 +241,7 @@ export class StorageEngine {
277
241
  }
278
242
  async rewriteLinks(oldId, newId, project) {
279
243
  const config = this.getConfig();
280
- const proj = project ?? config.currentProject;
244
+ const proj = project;
281
245
  const updated = [];
282
246
  // Within the same project: rewrite same-project refs and explicit self-cross-project refs
283
247
  const sameProjDocs = await this.listDocs(proj);
@@ -32,6 +32,22 @@ const dataDir = process.env.MCP_DATA_DIR?.trim() || path.join(rootDir, ".docs-da
32
32
  const staticRoot = path.join(rootDir, "gui/dist");
33
33
  const absoluteStaticRoot = path.resolve(staticRoot);
34
34
  const configuredAppPath = process.env.APP_PATH || "/documentor-gui";
35
+ function resolveGuiMode() {
36
+ const proxyUrl = process.env.PROXY_URL?.trim();
37
+ if (proxyUrl)
38
+ return { kind: "proxy", proxyUrl };
39
+ const rawPort = process.env.APP_PORT?.trim();
40
+ if (!rawPort) {
41
+ return { kind: "disabled", reason: "PROXY_URL et APP_PORT non définis" };
42
+ }
43
+ const port = Number(rawPort);
44
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
45
+ return { kind: "disabled", reason: `APP_PORT invalide (${rawPort}) — attendu : entier entre 1 et 65535` };
46
+ }
47
+ return { kind: "standalone", port };
48
+ }
49
+ const guiMode = resolveGuiMode();
50
+ const isStandalone = guiMode.kind === "standalone";
35
51
  setupLogging({ processLabel: "mcp-documentor:GUI", logDir: resolveLogDir(rootDir) });
36
52
  const logger = createLogger("mcp-documentor:GUI");
37
53
  process.on("uncaughtException", (err) => {
@@ -208,7 +224,7 @@ app.get("/proxy/health", (c) => {
208
224
  const memory = process.memoryUsage();
209
225
  return c.json({
210
226
  status: "ok",
211
- proxy: proxyClient ? proxyClient.getStatus() : process.env.PROXY_URL ? "error" : "standalone",
227
+ proxy: proxyClient ? proxyClient.getStatus() : isStandalone ? "standalone" : "error",
212
228
  uptime: process.uptime(),
213
229
  memory: { rss: memory.rss, heapUsed: memory.heapUsed },
214
230
  ipc: process.connected ? "connected" : "disconnected",
@@ -343,22 +359,29 @@ app.get("/api/projects", async (c) => {
343
359
  });
344
360
  app.get("/api/folders", async (c) => {
345
361
  const project = c.req.query("project");
346
- const result = await ipcRequest("LIST_FOLDERS", project ? { project } : {});
362
+ if (!project)
363
+ return c.json({ error: "project query param is required" }, 400);
364
+ const result = await ipcRequest("LIST_FOLDERS", { project });
347
365
  if (result.error)
348
366
  return c.json({ error: result.error }, 500);
349
367
  return c.json(result.data);
350
368
  });
351
- app.put("/api/project", async (c) => {
369
+ // Creates an empty project. Selecting a project is a client-side concern:
370
+ // the server holds no active project.
371
+ app.post("/api/projects", async (c) => {
352
372
  const { project } = await c.req.json();
353
373
  if (!project)
354
374
  return c.json({ error: "project is required" }, 400);
355
- const result = await ipcRequest("UPDATE_PROJECT", { project });
375
+ const result = await ipcRequest("CREATE_PROJECT", { project });
356
376
  if (result.error)
357
377
  return c.json({ error: result.error }, 500);
358
378
  return c.json(result.data);
359
379
  });
360
380
  app.get("/api/docs", async (c) => {
361
- const result = await ipcRequest("LIST_DOCS");
381
+ const project = c.req.query("project");
382
+ if (!project)
383
+ return c.json({ error: "project query param is required" }, 400);
384
+ const result = await ipcRequest("LIST_DOCS", { project });
362
385
  if (result.error)
363
386
  return c.json({ error: result.error }, 500);
364
387
  return c.json(result.data);
@@ -475,10 +498,13 @@ app.get("/api/history-version/*", async (c) => {
475
498
  return c.json(result.data);
476
499
  });
477
500
  app.post("/api/search", async (c) => {
478
- const { query, top_k, all_projects } = await c.req.json();
501
+ const { query, top_k, all_projects, project } = await c.req.json();
479
502
  if (!query)
480
503
  return c.json({ error: "query is required" }, 400);
481
- const result = await ipcRequest("SEARCH_DOCS", { query, top_k: top_k || 5, all_projects: !!all_projects });
504
+ if (!all_projects && !project) {
505
+ return c.json({ error: "project is required unless all_projects is set" }, 400);
506
+ }
507
+ const result = await ipcRequest("SEARCH_DOCS", { query, top_k: top_k || 5, all_projects: !!all_projects, project });
482
508
  if (result.error)
483
509
  return c.json({ error: result.error }, 500);
484
510
  return c.json(result.data);
@@ -522,14 +548,9 @@ app.get("/api/export-zip/:id", async (c) => {
522
548
  const bundle = bundles.find((b) => b.id === id);
523
549
  if (!bundle)
524
550
  return c.json({ error: "Bundle not found" }, 404);
525
- let currentProject = "default";
526
- try {
527
- const configPath = path.join(dataDir, "config.json");
528
- if (fs.existsSync(configPath)) {
529
- currentProject = JSON.parse(fs.readFileSync(configPath, "utf-8")).currentProject ?? "default";
530
- }
531
- }
532
- catch { /* use default */ }
551
+ // Legacy bundles may hold bare doc ids; they are resolved against the project
552
+ // the caller is viewing. Fully qualified "project:doc-id" refs ignore it.
553
+ const fallbackProject = c.req.query("project");
533
554
  const flat = c.req.query('flat') === '1';
534
555
  const includeHeaders = c.req.query('headers') === '1';
535
556
  const zip = new JSZip();
@@ -541,10 +562,15 @@ app.get("/api/export-zip/:id", async (c) => {
541
562
  project = ref.slice(0, colonIdx);
542
563
  docId = ref.slice(colonIdx + 1);
543
564
  }
544
- else {
545
- project = currentProject;
565
+ else if (fallbackProject) {
566
+ project = fallbackProject;
546
567
  docId = ref;
547
568
  }
569
+ else {
570
+ zip.file(`${ref}.md`, `# [Unqualified doc reference: ${ref} — no project given]
571
+ `);
572
+ continue;
573
+ }
548
574
  const docFilePath = path.join(dataDir, "projects", project, ...docId.split("/")) + ".md";
549
575
  let content;
550
576
  if (!fs.existsSync(docFilePath)) {
@@ -704,8 +730,9 @@ app.get("/*", (c, next) => {
704
730
  const indexPath = path.join(absoluteStaticRoot, "index.html");
705
731
  if (fs.existsSync(indexPath)) {
706
732
  // ?__pdf=1 : accès direct depuis Puppeteer — pas de préfixe proxy dans <base href>
733
+ // En mode standalone la GUI est déjà servie à la racine : même règle.
707
734
  const isPdfRender = c.req.query('__pdf') === '1';
708
- const baseHref = isPdfRender ? '/' : getHtmlBaseHref(configuredAppPath);
735
+ const baseHref = isStandalone || isPdfRender ? '/' : getHtmlBaseHref(configuredAppPath);
709
736
  const html = fs
710
737
  .readFileSync(indexPath, "utf8")
711
738
  .replace("<head>", `<head>\n <base href="${baseHref}" />`);
@@ -717,12 +744,54 @@ app.get("/*", (c, next) => {
717
744
  return next();
718
745
  }
719
746
  });
747
+ /**
748
+ * Lance Hono sur `port` et prévient le master. `publicUrl` est l'URL que
749
+ * l'utilisateur doit ouvrir : via le proxy, ou directement en localhost.
750
+ */
751
+ function listen(port, publicUrl, isProxied) {
752
+ logger.info(`Starting Hono server on port ${port}...`);
753
+ const serverInstance = serve({ fetch: app.fetch, port, hostname: "0.0.0.0" }, (info) => {
754
+ logger.info(`Dashboard server listening on port ${info.port}`);
755
+ safeSend(IPCMessageSchema.parse({
756
+ type: "READY",
757
+ data: { port: info.port, isProxied, url: publicUrl },
758
+ timestamp: new Date().toISOString(),
759
+ }));
760
+ });
761
+ serverInstance.on("error", (err) => {
762
+ if (err.code === "EADDRINUSE") {
763
+ // Sortie propre (code 0) : le master ne doit pas relancer un worker de plus.
764
+ logger.error(`Port ${port} already in use — another GUI instance is running.`);
765
+ safeSend(IPCMessageSchema.parse({
766
+ type: "ALREADY_RUNNING",
767
+ data: { url: publicUrl },
768
+ timestamp: new Date().toISOString(),
769
+ }));
770
+ process.exit(0);
771
+ }
772
+ logger.error(`Server error: ${err.message}`);
773
+ process.exit(1);
774
+ });
775
+ }
776
+ /** Sans proxy : la GUI écoute sur APP_PORT et est servie à la racine. */
777
+ function startStandalone(port) {
778
+ const publicUrl = `http://localhost:${port}`;
779
+ logger.info(`Standalone mode (no PROXY_URL): serving GUI on ${publicUrl}`);
780
+ localPort = port;
781
+ guiPublicUrl = publicUrl;
782
+ setupIpcErrorHandlers();
783
+ listen(port, publicUrl, false);
784
+ }
720
785
  async function startServer() {
721
- if (!process.env.PROXY_URL) {
722
- process.stderr.write('[GUI] PROXY_URL non défini — arrêt.\n');
786
+ if (guiMode.kind === "disabled") {
787
+ process.stderr.write(`[GUI] ${guiMode.reason} — arrêt.\n`);
723
788
  process.exit(0);
724
789
  }
725
- const proxyUrl = process.env.PROXY_URL;
790
+ if (guiMode.kind === "standalone") {
791
+ startStandalone(guiMode.port);
792
+ return;
793
+ }
794
+ const proxyUrl = guiMode.proxyUrl;
726
795
  const appPath = configuredAppPath;
727
796
  const appName = process.env.APP_NAME || "MCP Documentor GUI";
728
797
  logger.info(`Starting server with proxyUrl=${proxyUrl}`);
@@ -769,19 +838,7 @@ async function startServer() {
769
838
  process.on("SIGTERM", cleanupProxy);
770
839
  process.on("SIGINT", cleanupProxy);
771
840
  setupIpcErrorHandlers();
772
- logger.info(`Starting Hono server on port ${finalPort}...`);
773
- const serverInstance = serve({ fetch: app.fetch, port: finalPort, hostname: "0.0.0.0" }, (info) => {
774
- logger.info(`Dashboard server listening on port ${info.port}`);
775
- safeSend(IPCMessageSchema.parse({
776
- type: "READY",
777
- data: { port: info.port, isProxied: true, url: publicUrl },
778
- timestamp: new Date().toISOString(),
779
- }));
780
- });
781
- serverInstance.on("error", (err) => {
782
- logger.error(`Server error: ${err.message}`);
783
- process.exit(1);
784
- });
841
+ listen(finalPort, publicUrl, true);
785
842
  }
786
843
  startServer().catch((err) => {
787
844
  logger.error(`Failed to start GUI Worker: ${err.message}`);