@imenam/mcp-documentor 1.0.74 → 1.0.77

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 +101 -37
  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,29 @@ 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
+ /**
36
+ * Chemin réellement monté par le proxy. Il peut différer de `configuredAppPath`
37
+ * quand `APP_PATH_PREFIX` est défini : le client partagé préfixe alors le chemin
38
+ * naturel et renvoie le chemin final dans `result.path`. Le `<base href>` doit
39
+ * suivre ce chemin, sinon les assets de la SPA sont demandés au mauvais endroit.
40
+ */
41
+ let effectiveAppPath = configuredAppPath;
42
+ function resolveGuiMode() {
43
+ const proxyUrl = process.env.PROXY_URL?.trim();
44
+ if (proxyUrl)
45
+ return { kind: "proxy", proxyUrl };
46
+ const rawPort = process.env.APP_PORT?.trim();
47
+ if (!rawPort) {
48
+ return { kind: "disabled", reason: "PROXY_URL et APP_PORT non définis" };
49
+ }
50
+ const port = Number(rawPort);
51
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
52
+ return { kind: "disabled", reason: `APP_PORT invalide (${rawPort}) — attendu : entier entre 1 et 65535` };
53
+ }
54
+ return { kind: "standalone", port };
55
+ }
56
+ const guiMode = resolveGuiMode();
57
+ const isStandalone = guiMode.kind === "standalone";
35
58
  setupLogging({ processLabel: "mcp-documentor:GUI", logDir: resolveLogDir(rootDir) });
36
59
  const logger = createLogger("mcp-documentor:GUI");
37
60
  process.on("uncaughtException", (err) => {
@@ -208,7 +231,7 @@ app.get("/proxy/health", (c) => {
208
231
  const memory = process.memoryUsage();
209
232
  return c.json({
210
233
  status: "ok",
211
- proxy: proxyClient ? proxyClient.getStatus() : process.env.PROXY_URL ? "error" : "standalone",
234
+ proxy: proxyClient ? proxyClient.getStatus() : isStandalone ? "standalone" : "error",
212
235
  uptime: process.uptime(),
213
236
  memory: { rss: memory.rss, heapUsed: memory.heapUsed },
214
237
  ipc: process.connected ? "connected" : "disconnected",
@@ -343,22 +366,29 @@ app.get("/api/projects", async (c) => {
343
366
  });
344
367
  app.get("/api/folders", async (c) => {
345
368
  const project = c.req.query("project");
346
- const result = await ipcRequest("LIST_FOLDERS", project ? { project } : {});
369
+ if (!project)
370
+ return c.json({ error: "project query param is required" }, 400);
371
+ const result = await ipcRequest("LIST_FOLDERS", { project });
347
372
  if (result.error)
348
373
  return c.json({ error: result.error }, 500);
349
374
  return c.json(result.data);
350
375
  });
351
- app.put("/api/project", async (c) => {
376
+ // Creates an empty project. Selecting a project is a client-side concern:
377
+ // the server holds no active project.
378
+ app.post("/api/projects", async (c) => {
352
379
  const { project } = await c.req.json();
353
380
  if (!project)
354
381
  return c.json({ error: "project is required" }, 400);
355
- const result = await ipcRequest("UPDATE_PROJECT", { project });
382
+ const result = await ipcRequest("CREATE_PROJECT", { project });
356
383
  if (result.error)
357
384
  return c.json({ error: result.error }, 500);
358
385
  return c.json(result.data);
359
386
  });
360
387
  app.get("/api/docs", async (c) => {
361
- const result = await ipcRequest("LIST_DOCS");
388
+ const project = c.req.query("project");
389
+ if (!project)
390
+ return c.json({ error: "project query param is required" }, 400);
391
+ const result = await ipcRequest("LIST_DOCS", { project });
362
392
  if (result.error)
363
393
  return c.json({ error: result.error }, 500);
364
394
  return c.json(result.data);
@@ -475,10 +505,13 @@ app.get("/api/history-version/*", async (c) => {
475
505
  return c.json(result.data);
476
506
  });
477
507
  app.post("/api/search", async (c) => {
478
- const { query, top_k, all_projects } = await c.req.json();
508
+ const { query, top_k, all_projects, project } = await c.req.json();
479
509
  if (!query)
480
510
  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 });
511
+ if (!all_projects && !project) {
512
+ return c.json({ error: "project is required unless all_projects is set" }, 400);
513
+ }
514
+ const result = await ipcRequest("SEARCH_DOCS", { query, top_k: top_k || 5, all_projects: !!all_projects, project });
482
515
  if (result.error)
483
516
  return c.json({ error: result.error }, 500);
484
517
  return c.json(result.data);
@@ -522,14 +555,9 @@ app.get("/api/export-zip/:id", async (c) => {
522
555
  const bundle = bundles.find((b) => b.id === id);
523
556
  if (!bundle)
524
557
  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 */ }
558
+ // Legacy bundles may hold bare doc ids; they are resolved against the project
559
+ // the caller is viewing. Fully qualified "project:doc-id" refs ignore it.
560
+ const fallbackProject = c.req.query("project");
533
561
  const flat = c.req.query('flat') === '1';
534
562
  const includeHeaders = c.req.query('headers') === '1';
535
563
  const zip = new JSZip();
@@ -541,10 +569,15 @@ app.get("/api/export-zip/:id", async (c) => {
541
569
  project = ref.slice(0, colonIdx);
542
570
  docId = ref.slice(colonIdx + 1);
543
571
  }
544
- else {
545
- project = currentProject;
572
+ else if (fallbackProject) {
573
+ project = fallbackProject;
546
574
  docId = ref;
547
575
  }
576
+ else {
577
+ zip.file(`${ref}.md`, `# [Unqualified doc reference: ${ref} — no project given]
578
+ `);
579
+ continue;
580
+ }
548
581
  const docFilePath = path.join(dataDir, "projects", project, ...docId.split("/")) + ".md";
549
582
  let content;
550
583
  if (!fs.existsSync(docFilePath)) {
@@ -704,8 +737,9 @@ app.get("/*", (c, next) => {
704
737
  const indexPath = path.join(absoluteStaticRoot, "index.html");
705
738
  if (fs.existsSync(indexPath)) {
706
739
  // ?__pdf=1 : accès direct depuis Puppeteer — pas de préfixe proxy dans <base href>
740
+ // En mode standalone la GUI est déjà servie à la racine : même règle.
707
741
  const isPdfRender = c.req.query('__pdf') === '1';
708
- const baseHref = isPdfRender ? '/' : getHtmlBaseHref(configuredAppPath);
742
+ const baseHref = isStandalone || isPdfRender ? '/' : getHtmlBaseHref(effectiveAppPath);
709
743
  const html = fs
710
744
  .readFileSync(indexPath, "utf8")
711
745
  .replace("<head>", `<head>\n <base href="${baseHref}" />`);
@@ -717,12 +751,54 @@ app.get("/*", (c, next) => {
717
751
  return next();
718
752
  }
719
753
  });
754
+ /**
755
+ * Lance Hono sur `port` et prévient le master. `publicUrl` est l'URL que
756
+ * l'utilisateur doit ouvrir : via le proxy, ou directement en localhost.
757
+ */
758
+ function listen(port, publicUrl, isProxied) {
759
+ logger.info(`Starting Hono server on port ${port}...`);
760
+ const serverInstance = serve({ fetch: app.fetch, port, hostname: "0.0.0.0" }, (info) => {
761
+ logger.info(`Dashboard server listening on port ${info.port}`);
762
+ safeSend(IPCMessageSchema.parse({
763
+ type: "READY",
764
+ data: { port: info.port, isProxied, url: publicUrl },
765
+ timestamp: new Date().toISOString(),
766
+ }));
767
+ });
768
+ serverInstance.on("error", (err) => {
769
+ if (err.code === "EADDRINUSE") {
770
+ // Sortie propre (code 0) : le master ne doit pas relancer un worker de plus.
771
+ logger.error(`Port ${port} already in use — another GUI instance is running.`);
772
+ safeSend(IPCMessageSchema.parse({
773
+ type: "ALREADY_RUNNING",
774
+ data: { url: publicUrl },
775
+ timestamp: new Date().toISOString(),
776
+ }));
777
+ process.exit(0);
778
+ }
779
+ logger.error(`Server error: ${err.message}`);
780
+ process.exit(1);
781
+ });
782
+ }
783
+ /** Sans proxy : la GUI écoute sur APP_PORT et est servie à la racine. */
784
+ function startStandalone(port) {
785
+ const publicUrl = `http://localhost:${port}`;
786
+ logger.info(`Standalone mode (no PROXY_URL): serving GUI on ${publicUrl}`);
787
+ localPort = port;
788
+ guiPublicUrl = publicUrl;
789
+ setupIpcErrorHandlers();
790
+ listen(port, publicUrl, false);
791
+ }
720
792
  async function startServer() {
721
- if (!process.env.PROXY_URL) {
722
- process.stderr.write('[GUI] PROXY_URL non défini — arrêt.\n');
793
+ if (guiMode.kind === "disabled") {
794
+ process.stderr.write(`[GUI] ${guiMode.reason} — arrêt.\n`);
723
795
  process.exit(0);
724
796
  }
725
- const proxyUrl = process.env.PROXY_URL;
797
+ if (guiMode.kind === "standalone") {
798
+ startStandalone(guiMode.port);
799
+ return;
800
+ }
801
+ const proxyUrl = guiMode.proxyUrl;
726
802
  const appPath = configuredAppPath;
727
803
  const appName = process.env.APP_NAME || "MCP Documentor GUI";
728
804
  logger.info(`Starting server with proxyUrl=${proxyUrl}`);
@@ -731,8 +807,7 @@ async function startServer() {
731
807
  try {
732
808
  // APP_GROUP place la GUI dans une section repliable du proxy (facultatif).
733
809
  const registerOptions = { path: appPath, name: appName, group: process.env.APP_GROUP };
734
- // Second arg: fallbackPort (required by published @imenam/mcp-gui-interface API); unused when proxy assigns the port.
735
- result = await proxyClient.register(registerOptions, 0);
810
+ result = await proxyClient.register(registerOptions);
736
811
  }
737
812
  catch (e) {
738
813
  process.stderr.write('[GUI] Enregistrement proxy échoué — arrêt.\n');
@@ -758,7 +833,8 @@ async function startServer() {
758
833
  }
759
834
  const finalPort = result.port;
760
835
  localPort = finalPort;
761
- const publicUrl = result.url || `${proxyUrl}${appPath}`;
836
+ effectiveAppPath = result.path ?? appPath;
837
+ const publicUrl = result.url || `${proxyUrl}${effectiveAppPath}`;
762
838
  guiPublicUrl = publicUrl;
763
839
  logger.info(`Registered with proxy. Public URL: ${publicUrl}, Local Port: ${finalPort}`);
764
840
  const cleanupProxy = async () => {
@@ -769,19 +845,7 @@ async function startServer() {
769
845
  process.on("SIGTERM", cleanupProxy);
770
846
  process.on("SIGINT", cleanupProxy);
771
847
  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
- });
848
+ listen(finalPort, publicUrl, true);
785
849
  }
786
850
  startServer().catch((err) => {
787
851
  logger.error(`Failed to start GUI Worker: ${err.message}`);