@imenam/mcp-documentor 1.0.72 → 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.
- package/README.md +271 -259
- package/dist/shared/schemas/doc.schema.d.ts +1 -4
- package/dist/shared/schemas/doc.schema.js +1 -4
- package/dist/src/cli/setup-mcp.d.ts +20 -1
- package/dist/src/cli/setup-mcp.js +65 -6
- package/dist/src/core/conflict-inspector.d.ts +13 -0
- package/dist/src/core/conflict-inspector.js +17 -0
- package/dist/src/core/git-sync.d.ts +11 -4
- package/dist/src/core/git-sync.js +33 -22
- package/dist/src/core/storage-engine.d.ts +14 -15
- package/dist/src/core/storage-engine.js +15 -51
- package/dist/src/gui-worker.js +91 -34
- package/dist/src/index.js +100 -110
- package/dist/src/tools/check-links.js +1 -1
- package/dist/src/tools/conflicts.js +50 -5
- package/dist/src/tools/list-projects.js +7 -5
- package/dist/src/tools/sync.js +14 -7
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/gui/dist/assets/{arc-KWKRz6LO.js → arc-CsM5OJTM.js} +1 -1
- package/gui/dist/assets/{architectureDiagram-3BPJPVTR-Cjfl98ZS.js → architectureDiagram-3BPJPVTR-BwIu1llT.js} +1 -1
- package/gui/dist/assets/{blockDiagram-GPEHLZMM-CtPUrnse.js → blockDiagram-GPEHLZMM-BVijVnPI.js} +1 -1
- package/gui/dist/assets/{c4Diagram-AAUBKEIU-Dh5_7IZy.js → c4Diagram-AAUBKEIU-D-rcZUZu.js} +1 -1
- package/gui/dist/assets/channel-DKPwwuQf.js +1 -0
- package/gui/dist/assets/{chunk-2J33WTMH-44T5JbXy.js → chunk-2J33WTMH-ByfTGREe.js} +1 -1
- package/gui/dist/assets/{chunk-4BX2VUAB-D1fEQ0Ng.js → chunk-4BX2VUAB-B_5oCXyz.js} +1 -1
- package/gui/dist/assets/{chunk-55IACEB6-M2lEB2Vn.js → chunk-55IACEB6-Bc2Kq9HD.js} +1 -1
- package/gui/dist/assets/{chunk-727SXJPM-B3A_Fp4f.js → chunk-727SXJPM-CVwmKEMQ.js} +1 -1
- package/gui/dist/assets/{chunk-AQP2D5EJ-CqkJXv3c.js → chunk-AQP2D5EJ-BkMuEFxD.js} +1 -1
- package/gui/dist/assets/{chunk-FMBD7UC4-Iyj_Ky8e.js → chunk-FMBD7UC4-CU7EDl9I.js} +1 -1
- package/gui/dist/assets/{chunk-ND2GUHAM-DE5R-C9U.js → chunk-ND2GUHAM-1b34Dtkn.js} +1 -1
- package/gui/dist/assets/{chunk-QZHKN3VN-BekBU-YP.js → chunk-QZHKN3VN-DnUfMecI.js} +1 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-CnCvVGW9.js +1 -0
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-CnCvVGW9.js +1 -0
- package/gui/dist/assets/{cose-bilkent-S5V4N54A-Dw2zOlxr.js → cose-bilkent-S5V4N54A-D9OBrxKr.js} +1 -1
- package/gui/dist/assets/{dagre-BM42HDAG-ebVZNyxI.js → dagre-BM42HDAG-zDqwL8xs.js} +1 -1
- package/gui/dist/assets/{diagram-2AECGRRQ-Bh5FUKy0.js → diagram-2AECGRRQ-ejbP8WIj.js} +1 -1
- package/gui/dist/assets/{diagram-5GNKFQAL-YJJTJy-Q.js → diagram-5GNKFQAL-Cv9WmtFO.js} +1 -1
- package/gui/dist/assets/{diagram-KO2AKTUF-D161kBvB.js → diagram-KO2AKTUF-BI0ybJiN.js} +1 -1
- package/gui/dist/assets/{diagram-LMA3HP47-Drcmx9J8.js → diagram-LMA3HP47-DlVGttLq.js} +1 -1
- package/gui/dist/assets/{diagram-OG6HWLK6-DRrxnW_H.js → diagram-OG6HWLK6-DjoAH-Fq.js} +1 -1
- package/gui/dist/assets/{erDiagram-TEJ5UH35-FrH4iEa3.js → erDiagram-TEJ5UH35-BVQHVGK4.js} +1 -1
- package/gui/dist/assets/{flowDiagram-I6XJVG4X-BMxSElvH.js → flowDiagram-I6XJVG4X-oZP2Z-6T.js} +1 -1
- package/gui/dist/assets/{ganttDiagram-6RSMTGT7-H09FGGUr.js → ganttDiagram-6RSMTGT7-dy4QyEOP.js} +1 -1
- package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-DLhPfXNp.js → gitGraphDiagram-PVQCEYII-BxgLy7Ln.js} +1 -1
- package/gui/dist/assets/{index-CdGsMXYw.js → index-Dtv0slFQ.js} +97 -97
- package/gui/dist/assets/index-QkCDSQ2A.css +1 -0
- package/gui/dist/assets/{infoDiagram-5YYISTIA-CV8ctX8t.js → infoDiagram-5YYISTIA-DNOdinpR.js} +1 -1
- package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-BeuHUSPO.js → ishikawaDiagram-YF4QCWOH-C4pQ434G.js} +1 -1
- package/gui/dist/assets/{journeyDiagram-JHISSGLW-CfAHwYXQ.js → journeyDiagram-JHISSGLW-CBrVYW94.js} +1 -1
- package/gui/dist/assets/{kanban-definition-UN3LZRKU-CFF4KWbn.js → kanban-definition-UN3LZRKU-Df-rgiY8.js} +1 -1
- package/gui/dist/assets/{linear-DwZpGeRp.js → linear-B_6fRXVt.js} +1 -1
- package/gui/dist/assets/{mindmap-definition-RKZ34NQL-BW0yW4ap.js → mindmap-definition-RKZ34NQL-CRAd_VDM.js} +1 -1
- package/gui/dist/assets/{pieDiagram-4H26LBE5-plBCN_c7.js → pieDiagram-4H26LBE5-CJkmLrDI.js} +1 -1
- package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-Dmp9sOq_.js → quadrantDiagram-W4KKPZXB-D-rDEcnF.js} +1 -1
- package/gui/dist/assets/{requirementDiagram-4Y6WPE33-CthKC15J.js → requirementDiagram-4Y6WPE33-DE8MhSMe.js} +1 -1
- package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-BCAzzfPU.js → sankeyDiagram-5OEKKPKP-BwlxtrUs.js} +1 -1
- package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-DgwOe82k.js → sequenceDiagram-3UESZ5HK-B4LjBzWg.js} +1 -1
- package/gui/dist/assets/{stateDiagram-AJRCARHV-B6k-x1TC.js → stateDiagram-AJRCARHV-Bv72afPK.js} +1 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-BKJDOkkR.js +1 -0
- package/gui/dist/assets/{timeline-definition-PNZ67QCA-CqwqJkZw.js → timeline-definition-PNZ67QCA-CBxgf8iW.js} +1 -1
- package/gui/dist/assets/{vennDiagram-CIIHVFJN-REFDrI5t.js → vennDiagram-CIIHVFJN-DPSdK06q.js} +1 -1
- package/gui/dist/assets/{wardley-L42UT6IY-53gLZWfR.js → wardley-L42UT6IY-JIAzuoPm.js} +1 -1
- package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-OPfKpNx-.js → wardleyDiagram-YWT4CUSO-YDfaF1wT.js} +1 -1
- package/gui/dist/assets/{xychartDiagram-2RQKCTM6-HWGySgFg.js → xychartDiagram-2RQKCTM6-DHlPryZX.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +65 -65
- package/dist/src/tools/set-project.d.ts +0 -14
- package/dist/src/tools/set-project.js +0 -18
- package/gui/dist/assets/channel-JejxY436.js +0 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-CdAqI_QA.js +0 -1
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-CdAqI_QA.js +0 -1
- package/gui/dist/assets/index-DAzSbGj_.css +0 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-bfw3SR0t.js +0 -1
package/dist/src/gui-worker.js
CHANGED
|
@@ -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() :
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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 =
|
|
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 (
|
|
722
|
-
process.stderr.write(
|
|
786
|
+
if (guiMode.kind === "disabled") {
|
|
787
|
+
process.stderr.write(`[GUI] ${guiMode.reason} — arrêt.\n`);
|
|
723
788
|
process.exit(0);
|
|
724
789
|
}
|
|
725
|
-
|
|
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
|
-
|
|
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}`);
|
package/dist/src/index.js
CHANGED
|
@@ -20,7 +20,6 @@ import { searchDocs } from "./tools/search-docs.js";
|
|
|
20
20
|
import { checkFreshness, computeFreshness } from "./tools/check-freshness.js";
|
|
21
21
|
import { findAndHashSection } from "./core/section-tracker.js";
|
|
22
22
|
import { toAbsolutePath } from "./core/path-resolver.js";
|
|
23
|
-
import { setProject } from "./tools/set-project.js";
|
|
24
23
|
import { listProjects } from "./tools/list-projects.js";
|
|
25
24
|
import { refreshFileSnapshot } from "./tools/refresh-file-snapshot.js";
|
|
26
25
|
import { getDocMetadata } from "./tools/get-doc-metadata.js";
|
|
@@ -50,8 +49,14 @@ const __dirname = path.dirname(__filename);
|
|
|
50
49
|
// Les flags CLI sont traités avant toute initialisation (storage, sqlite, logs) :
|
|
51
50
|
// ils s'exécutent dans le répertoire de l'utilisateur, pas dans un serveur MCP.
|
|
52
51
|
if (process.argv.slice(2).includes("--claude-setup-mcp")) {
|
|
53
|
-
const
|
|
54
|
-
|
|
52
|
+
const argv = process.argv.slice(2);
|
|
53
|
+
const { runSetupMcp, parseGuiExposure } = await import("./cli/setup-mcp.js");
|
|
54
|
+
const parsed = parseGuiExposure(argv);
|
|
55
|
+
if ("error" in parsed) {
|
|
56
|
+
console.error(`Erreur : ${parsed.error}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
process.exit(runSetupMcp({ cwd: process.cwd(), force: argv.includes("--force"), exposure: parsed.exposure }));
|
|
55
60
|
}
|
|
56
61
|
function findProjectRoot(startDir) {
|
|
57
62
|
let current = startDir;
|
|
@@ -73,6 +78,7 @@ function getConfig() {
|
|
|
73
78
|
}
|
|
74
79
|
return {
|
|
75
80
|
proxyUrl: process.env.PROXY_URL,
|
|
81
|
+
appPort: process.env.APP_PORT,
|
|
76
82
|
version: APP_VERSION,
|
|
77
83
|
};
|
|
78
84
|
}
|
|
@@ -80,7 +86,9 @@ getConfig();
|
|
|
80
86
|
const storageEngine = new StorageEngine(rootDir);
|
|
81
87
|
const embeddingStore = new EmbeddingStore();
|
|
82
88
|
const embeddingEngine = new EmbeddingEngine(embeddingStore);
|
|
83
|
-
|
|
89
|
+
for (const project of storageEngine.getConfig().projects) {
|
|
90
|
+
storageEngine.ensureProject(project);
|
|
91
|
+
}
|
|
84
92
|
const openaiModel = process.env.MCP_DOCUMENTOR_OPENAI_MODEL || "text-embedding-3-small";
|
|
85
93
|
embeddingStore.init(storageEngine.getDataDir(), openaiModel);
|
|
86
94
|
const historyStore = new HistoryStore();
|
|
@@ -123,8 +131,8 @@ function handleIPCMessage(msg) {
|
|
|
123
131
|
else if (msg.type === "GET_CONFIG") {
|
|
124
132
|
handleGetConfig(msg);
|
|
125
133
|
}
|
|
126
|
-
else if (msg.type === "
|
|
127
|
-
|
|
134
|
+
else if (msg.type === "CREATE_PROJECT") {
|
|
135
|
+
handleCreateProject(msg);
|
|
128
136
|
}
|
|
129
137
|
else if (msg.type === "LIST_PROJECTS") {
|
|
130
138
|
handleListProjects(msg);
|
|
@@ -188,25 +196,45 @@ function requireProjectFromIPC(msg) {
|
|
|
188
196
|
throw new Error("project is required for this operation");
|
|
189
197
|
return project;
|
|
190
198
|
}
|
|
199
|
+
/** Loads one project's summaries and flags each entry as stale/broken. */
|
|
200
|
+
async function listDocsWithFreshness(project) {
|
|
201
|
+
const docs = await storageEngine.listDocs(project);
|
|
202
|
+
for (const doc of docs) {
|
|
203
|
+
const full = await storageEngine.getDoc(doc.id, project);
|
|
204
|
+
if (full && full.tracked_files.length > 0) {
|
|
205
|
+
const report = computeFreshness(full.tracked_files, project);
|
|
206
|
+
doc.isStale = report.status === "stale" || report.status === "broken";
|
|
207
|
+
doc.isBroken = report.status === "broken";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return docs;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Tells every connected GUI that one project's docs changed. Clients showing
|
|
214
|
+
* that project refetch; the others ignore it. Broadcasting a full INITIAL_STATE
|
|
215
|
+
* here used to drag every client onto the writer's project.
|
|
216
|
+
*/
|
|
217
|
+
function broadcastDocsChanged(project) {
|
|
218
|
+
broadcastToGUI({
|
|
219
|
+
type: "DOCS_CHANGED",
|
|
220
|
+
data: { project },
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Server-side state only: the project list and server capabilities. Which
|
|
226
|
+
* project a client displays is that client's own business (URL + local
|
|
227
|
+
* preference), so no docs and no active project are pushed here.
|
|
228
|
+
*/
|
|
191
229
|
async function handleGetInitialState(msg) {
|
|
192
230
|
try {
|
|
193
231
|
const config = storageEngine.getConfig();
|
|
194
|
-
const docs = await storageEngine.listDocs();
|
|
195
|
-
for (const doc of docs) {
|
|
196
|
-
const full = await storageEngine.getDoc(doc.id); // doc.id is now the full path
|
|
197
|
-
if (full && full.tracked_files.length > 0) {
|
|
198
|
-
const report = computeFreshness(full.tracked_files, config.currentProject);
|
|
199
|
-
doc.isStale = report.status === "stale" || report.status === "broken";
|
|
200
|
-
doc.isBroken = report.status === "broken";
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
232
|
const bundles = storageEngine.getBundles();
|
|
204
233
|
broadcastToGUI({
|
|
205
234
|
type: "INITIAL_STATE",
|
|
206
235
|
correlationId: msg.correlationId,
|
|
207
236
|
data: {
|
|
208
237
|
config,
|
|
209
|
-
docs,
|
|
210
238
|
projects: config.projects,
|
|
211
239
|
bundles,
|
|
212
240
|
readOnly: isReadOnly(),
|
|
@@ -228,39 +256,31 @@ async function handleGetConfig(msg) {
|
|
|
228
256
|
timestamp: new Date().toISOString(),
|
|
229
257
|
});
|
|
230
258
|
}
|
|
231
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Creates an empty project. It does NOT change any server-side "active"
|
|
261
|
+
* project — there is none: the selector in the GUI is a per-client view.
|
|
262
|
+
*/
|
|
263
|
+
async function handleCreateProject(msg) {
|
|
232
264
|
try {
|
|
233
265
|
const project = path.basename(msg.data.project);
|
|
234
266
|
if (!project || project === "..")
|
|
235
267
|
throw new Error("Invalid project name");
|
|
236
268
|
storageEngine.ensureProject(project);
|
|
237
|
-
const config = storageEngine.getConfig();
|
|
238
|
-
config.currentProject = project;
|
|
239
|
-
storageEngine.saveConfig(config);
|
|
240
|
-
const docs = await storageEngine.listDocs();
|
|
241
|
-
for (const doc of docs) {
|
|
242
|
-
const full = await storageEngine.getDoc(doc.id);
|
|
243
|
-
if (full && full.tracked_files.length > 0) {
|
|
244
|
-
const report = computeFreshness(full.tracked_files, project);
|
|
245
|
-
doc.isStale = report.status === "stale" || report.status === "broken";
|
|
246
|
-
doc.isBroken = report.status === "broken";
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
269
|
broadcastToGUI({
|
|
250
|
-
type: "
|
|
270
|
+
type: "CREATE_PROJECT_RESPONSE",
|
|
251
271
|
correlationId: msg.correlationId,
|
|
252
|
-
data: { success: true,
|
|
272
|
+
data: { success: true, project, projects: storageEngine.getConfig().projects },
|
|
253
273
|
timestamp: new Date().toISOString(),
|
|
254
274
|
});
|
|
255
275
|
broadcastToGUI({
|
|
256
|
-
type: "
|
|
257
|
-
data: {
|
|
276
|
+
type: "PROJECTS_CHANGED",
|
|
277
|
+
data: { projects: storageEngine.getConfig().projects },
|
|
258
278
|
timestamp: new Date().toISOString(),
|
|
259
279
|
});
|
|
260
280
|
}
|
|
261
281
|
catch (err) {
|
|
262
282
|
broadcastToGUI({
|
|
263
|
-
type: "
|
|
283
|
+
type: "CREATE_PROJECT_RESPONSE",
|
|
264
284
|
correlationId: msg.correlationId,
|
|
265
285
|
error: err.message,
|
|
266
286
|
timestamp: new Date().toISOString(),
|
|
@@ -278,16 +298,7 @@ async function handleListProjects(msg) {
|
|
|
278
298
|
}
|
|
279
299
|
async function handleListDocs(msg) {
|
|
280
300
|
try {
|
|
281
|
-
const
|
|
282
|
-
const docs = await storageEngine.listDocs();
|
|
283
|
-
for (const doc of docs) {
|
|
284
|
-
const full = await storageEngine.getDoc(doc.id);
|
|
285
|
-
if (full && full.tracked_files.length > 0) {
|
|
286
|
-
const report = computeFreshness(full.tracked_files, currentProject);
|
|
287
|
-
doc.isStale = report.status === "stale" || report.status === "broken";
|
|
288
|
-
doc.isBroken = report.status === "broken";
|
|
289
|
-
}
|
|
290
|
-
}
|
|
301
|
+
const docs = await listDocsWithFreshness(requireProjectFromIPC(msg));
|
|
291
302
|
broadcastToGUI({
|
|
292
303
|
type: "LIST_DOCS_RESPONSE",
|
|
293
304
|
correlationId: msg.correlationId,
|
|
@@ -327,8 +338,7 @@ async function handleGetDoc(msg) {
|
|
|
327
338
|
}
|
|
328
339
|
async function handleListFoldersIPC(msg) {
|
|
329
340
|
try {
|
|
330
|
-
const
|
|
331
|
-
const docs = await storageEngine.listDocs(project);
|
|
341
|
+
const docs = await storageEngine.listDocs(requireProjectFromIPC(msg));
|
|
332
342
|
const folders = new Set();
|
|
333
343
|
for (const doc of docs) {
|
|
334
344
|
if (doc.folder) {
|
|
@@ -394,21 +404,7 @@ async function handleCreateDocIPC(msg) {
|
|
|
394
404
|
data: { success: true, id },
|
|
395
405
|
timestamp: new Date().toISOString(),
|
|
396
406
|
});
|
|
397
|
-
|
|
398
|
-
const docs = await storageEngine.listDocs();
|
|
399
|
-
for (const doc of docs) {
|
|
400
|
-
const full = await storageEngine.getDoc(doc.id);
|
|
401
|
-
if (full && full.tracked_files.length > 0) {
|
|
402
|
-
const report = computeFreshness(full.tracked_files, config.currentProject);
|
|
403
|
-
doc.isStale = report.status === "stale" || report.status === "broken";
|
|
404
|
-
doc.isBroken = report.status === "broken";
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
broadcastToGUI({
|
|
408
|
-
type: "INITIAL_STATE",
|
|
409
|
-
data: { config, docs, projects: config.projects },
|
|
410
|
-
timestamp: new Date().toISOString(),
|
|
411
|
-
});
|
|
407
|
+
broadcastDocsChanged(project);
|
|
412
408
|
}
|
|
413
409
|
catch (err) {
|
|
414
410
|
broadcastToGUI({
|
|
@@ -430,9 +426,10 @@ async function handleUpdateDocIPC(msg) {
|
|
|
430
426
|
return;
|
|
431
427
|
}
|
|
432
428
|
try {
|
|
429
|
+
const project = requireProjectFromIPC(msg);
|
|
433
430
|
const result = await updateDoc(storageEngine, embeddingEngine, {
|
|
434
431
|
path: msg.data.id,
|
|
435
|
-
project
|
|
432
|
+
project,
|
|
436
433
|
content: msg.data.content,
|
|
437
434
|
...(msg.data.title !== undefined && { title: msg.data.title }),
|
|
438
435
|
});
|
|
@@ -453,6 +450,7 @@ async function handleUpdateDocIPC(msg) {
|
|
|
453
450
|
data: { success: true, message },
|
|
454
451
|
timestamp: new Date().toISOString(),
|
|
455
452
|
});
|
|
453
|
+
broadcastDocsChanged(project);
|
|
456
454
|
}
|
|
457
455
|
catch (err) {
|
|
458
456
|
broadcastToGUI({
|
|
@@ -493,21 +491,7 @@ async function handleDeleteDocIPC(msg) {
|
|
|
493
491
|
data: { success: true, message },
|
|
494
492
|
timestamp: new Date().toISOString(),
|
|
495
493
|
});
|
|
496
|
-
|
|
497
|
-
const docs = await storageEngine.listDocs();
|
|
498
|
-
for (const doc of docs) {
|
|
499
|
-
const full = await storageEngine.getDoc(doc.id);
|
|
500
|
-
if (full && full.tracked_files.length > 0) {
|
|
501
|
-
const report = computeFreshness(full.tracked_files, config.currentProject);
|
|
502
|
-
doc.isStale = report.status === "stale" || report.status === "broken";
|
|
503
|
-
doc.isBroken = report.status === "broken";
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
broadcastToGUI({
|
|
507
|
-
type: "INITIAL_STATE",
|
|
508
|
-
data: { config, docs, projects: config.projects },
|
|
509
|
-
timestamp: new Date().toISOString(),
|
|
510
|
-
});
|
|
494
|
+
broadcastDocsChanged(project);
|
|
511
495
|
}
|
|
512
496
|
catch (err) {
|
|
513
497
|
broadcastToGUI({
|
|
@@ -603,9 +587,9 @@ async function handleSearchDocs(msg) {
|
|
|
603
587
|
});
|
|
604
588
|
}
|
|
605
589
|
else {
|
|
606
|
-
const
|
|
607
|
-
results = await embeddingEngine.search(query,
|
|
608
|
-
const doc = await storageEngine.getDoc(docPath);
|
|
590
|
+
const project = requireProjectFromIPC(msg);
|
|
591
|
+
results = await embeddingEngine.search(query, project, top_k || 5, async (docPath) => {
|
|
592
|
+
const doc = await storageEngine.getDoc(docPath, project);
|
|
609
593
|
if (!doc)
|
|
610
594
|
return null;
|
|
611
595
|
return { title: doc.title, content: doc.content };
|
|
@@ -832,9 +816,13 @@ function startGuiWorker() {
|
|
|
832
816
|
});
|
|
833
817
|
return child;
|
|
834
818
|
}
|
|
819
|
+
/** La GUI a besoin soit d'un proxy, soit d'un port local (mode standalone). */
|
|
820
|
+
function guiIsConfigured() {
|
|
821
|
+
return !!(process.env.PROXY_URL?.trim() || process.env.APP_PORT?.trim());
|
|
822
|
+
}
|
|
835
823
|
function launchGUI() {
|
|
836
|
-
if (!
|
|
837
|
-
logger.info('[MASTER] PROXY_URL
|
|
824
|
+
if (!guiIsConfigured()) {
|
|
825
|
+
logger.info('[MASTER] Ni PROXY_URL ni APP_PORT défini — GUI désactivée.');
|
|
838
826
|
guiStatus = "disabled";
|
|
839
827
|
return;
|
|
840
828
|
}
|
|
@@ -842,10 +830,10 @@ function launchGUI() {
|
|
|
842
830
|
startGuiWorker();
|
|
843
831
|
}
|
|
844
832
|
async function reconnectGui(args) {
|
|
845
|
-
getConfig(); // relit le .env au cas où PROXY_URL aurait été ajouté après le démarrage
|
|
846
|
-
if (!
|
|
833
|
+
getConfig(); // relit le .env au cas où PROXY_URL/APP_PORT aurait été ajouté après le démarrage
|
|
834
|
+
if (!guiIsConfigured()) {
|
|
847
835
|
return {
|
|
848
|
-
content: [{ type: "text", text: "Error: PROXY_URL is
|
|
836
|
+
content: [{ type: "text", text: "Error: neither PROXY_URL nor APP_PORT is set — the GUI is disabled. Add PROXY_URL (to expose the GUI through the proxy) or APP_PORT (to serve it directly on localhost) to the .env file, then call reconnect_gui again." }],
|
|
849
837
|
isError: true,
|
|
850
838
|
};
|
|
851
839
|
}
|
|
@@ -854,9 +842,9 @@ async function reconnectGui(args) {
|
|
|
854
842
|
const force = args?.force === true;
|
|
855
843
|
if (alive && !force) {
|
|
856
844
|
if (guiStatus === "starting") {
|
|
857
|
-
return { content: [{ type: "text", text: "GUI worker is already starting
|
|
845
|
+
return { content: [{ type: "text", text: "GUI worker is already starting. Wait a few seconds, then call reconnect_gui again if needed." }] };
|
|
858
846
|
}
|
|
859
|
-
return { content: [{ type: "text", text: `GUI is already
|
|
847
|
+
return { content: [{ type: "text", text: `GUI is already running${guiUrl ? ` at ${guiUrl}` : ""}. Nothing to do (use force=true to restart it anyway).` }] };
|
|
860
848
|
}
|
|
861
849
|
logger.info(`[MASTER] reconnect_gui: ${alive ? "restarting (force)" : "relaunching"} GUI worker...`);
|
|
862
850
|
const result = await new Promise((resolve) => {
|
|
@@ -867,20 +855,24 @@ async function reconnectGui(args) {
|
|
|
867
855
|
guiStartWaiter = (r) => { cleanup(); resolve(r); };
|
|
868
856
|
newChild.on("exit", onExit);
|
|
869
857
|
});
|
|
858
|
+
const proxyUrl = process.env.PROXY_URL?.trim();
|
|
870
859
|
if (result.outcome === "ready") {
|
|
871
|
-
return { content: [{ type: "text", text: `GUI reconnected
|
|
860
|
+
return { content: [{ type: "text", text: `GUI reconnected. URL: ${result.url}` }] };
|
|
872
861
|
}
|
|
873
862
|
if (result.outcome === "already_running") {
|
|
874
|
-
return { content: [{ type: "text", text: `GUI is already
|
|
863
|
+
return { content: [{ type: "text", text: `GUI is already served by another MCP instance. URL: ${result.url ?? "unknown"}` }] };
|
|
875
864
|
}
|
|
876
865
|
if (result.outcome === "exited") {
|
|
866
|
+
const hint = proxyUrl
|
|
867
|
+
? `The proxy server is probably still unreachable at ${proxyUrl}. Start the proxy server, then call reconnect_gui again.`
|
|
868
|
+
: `Port ${process.env.APP_PORT} is probably already taken by another process. Free it or set another APP_PORT, then call reconnect_gui again.`;
|
|
877
869
|
return {
|
|
878
|
-
content: [{ type: "text", text: `GUI worker exited before
|
|
870
|
+
content: [{ type: "text", text: `GUI worker exited before it was ready (code=${result.code}). ${hint}` }],
|
|
879
871
|
isError: true,
|
|
880
872
|
};
|
|
881
873
|
}
|
|
882
874
|
return {
|
|
883
|
-
content: [{ type: "text", text: `GUI worker started but did not confirm
|
|
875
|
+
content: [{ type: "text", text: `GUI worker started but did not confirm it was ready within 15s. Check the logs (${resolveLogDir(rootDir)}), then retry.` }],
|
|
884
876
|
isError: true,
|
|
885
877
|
};
|
|
886
878
|
}
|
|
@@ -1059,20 +1051,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1059
1051
|
required: [],
|
|
1060
1052
|
},
|
|
1061
1053
|
},
|
|
1062
|
-
{
|
|
1063
|
-
name: "set_project",
|
|
1064
|
-
description: "Switch the active project and update it as the default for all subsequent MCP tool operations. If the project name does not exist yet, it is created automatically. Also controls what the GUI displays.",
|
|
1065
|
-
inputSchema: {
|
|
1066
|
-
type: "object",
|
|
1067
|
-
properties: {
|
|
1068
|
-
project: { type: "string", description: "Project name to switch to (created if it does not exist)" },
|
|
1069
|
-
},
|
|
1070
|
-
required: ["project"],
|
|
1071
|
-
},
|
|
1072
|
-
},
|
|
1073
1054
|
{
|
|
1074
1055
|
name: "list_projects",
|
|
1075
|
-
description: "List all available documentation projects
|
|
1056
|
+
description: "List all available documentation projects. There is no active project: every tool call names the project it targets.",
|
|
1076
1057
|
inputSchema: {
|
|
1077
1058
|
type: "object",
|
|
1078
1059
|
properties: {},
|
|
@@ -1154,9 +1135,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1154
1135
|
inputSchema: {
|
|
1155
1136
|
type: "object",
|
|
1156
1137
|
properties: {
|
|
1157
|
-
project: { type: "string", description: "Project
|
|
1138
|
+
project: { type: "string", description: "Project whose entries are checked. Required — never defaulted to a current project." },
|
|
1158
1139
|
},
|
|
1159
|
-
required: [],
|
|
1140
|
+
required: ["project"],
|
|
1160
1141
|
},
|
|
1161
1142
|
},
|
|
1162
1143
|
{
|
|
@@ -1362,7 +1343,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1362
1343
|
type: "object",
|
|
1363
1344
|
properties: {
|
|
1364
1345
|
hunk: { type: "number", description: "Hunk number from get_conflict_hunks" },
|
|
1365
|
-
take: { type: "string", enum: ["local", "remote", "both", "none", "custom"], description: "'local' this machine, 'remote' the other machine, 'both' concatenates local then remote, 'none' drops the passage, 'custom' uses the provided content" },
|
|
1346
|
+
take: { type: "string", enum: ["local", "remote", "both", "none", "custom"], description: "'local' this machine, 'remote' the other machine, 'both' concatenates local then remote, 'none' drops the passage, 'custom' uses the provided content. 'both' is refused on frontmatter hunks, where it would duplicate a YAML key." },
|
|
1366
1347
|
content: { type: "string", description: "Replacement text, required when take is 'custom'" },
|
|
1367
1348
|
},
|
|
1368
1349
|
required: ["hunk", "take"],
|
|
@@ -1410,7 +1391,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1410
1391
|
case "check_freshness":
|
|
1411
1392
|
return await checkFreshness(storageEngine, args);
|
|
1412
1393
|
case "set_project":
|
|
1413
|
-
|
|
1394
|
+
// Removed: there is no server-side active project any more. Kept as an
|
|
1395
|
+
// explicit answer for agents that still remember the old tool.
|
|
1396
|
+
return {
|
|
1397
|
+
isError: true,
|
|
1398
|
+
content: [
|
|
1399
|
+
{
|
|
1400
|
+
type: "text",
|
|
1401
|
+
text: "set_project no longer exists: there is no active project. " +
|
|
1402
|
+
"Pass the project explicitly to every tool call (use list_projects to see them). " +
|
|
1403
|
+
"A project is created on demand by create_doc.",
|
|
1404
|
+
},
|
|
1405
|
+
],
|
|
1406
|
+
};
|
|
1414
1407
|
case "list_projects":
|
|
1415
1408
|
return await listProjects(storageEngine);
|
|
1416
1409
|
case "refresh_file_snapshot":
|
|
@@ -1505,9 +1498,6 @@ async function syncPullOnStartup() {
|
|
|
1505
1498
|
await reindexEmbeddings(storageEngine, embeddingEngine, embeddingStore, {});
|
|
1506
1499
|
}
|
|
1507
1500
|
}
|
|
1508
|
-
else if (res.status === "diverged") {
|
|
1509
|
-
logger.warn(`Startup sync: local history diverged (ahead ${res.ahead}, behind ${res.behind}). Started on local data — run sync_status / sync_docs to reconcile.`);
|
|
1510
|
-
}
|
|
1511
1501
|
else if (res.status === "conflict") {
|
|
1512
1502
|
logger.warn(`Startup sync: merge conflict in ${res.conflicted.length} file(s). Started on local data — resolve with resolve_sync_conflict.`);
|
|
1513
1503
|
}
|
|
@@ -38,7 +38,7 @@ export async function checkLinks(storageEngine, args) {
|
|
|
38
38
|
try {
|
|
39
39
|
const validated = CheckLinksArgsSchema.parse(args);
|
|
40
40
|
const config = storageEngine.getConfig();
|
|
41
|
-
const project = validated.project
|
|
41
|
+
const project = validated.project;
|
|
42
42
|
const knownProjects = new Set(config.projects);
|
|
43
43
|
const docs = await storageEngine.listDocs(project);
|
|
44
44
|
const validIds = new Set(docs.map((d) => d.id));
|