@imenam/mcp-documentor 1.0.23
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 +74 -0
- package/dist/shared/schemas/doc.schema.d.ts +112 -0
- package/dist/shared/schemas/doc.schema.js +83 -0
- package/dist/src/core/embedding-engine.d.ts +23 -0
- package/dist/src/core/embedding-engine.js +119 -0
- package/dist/src/core/embedding-store.d.ts +15 -0
- package/dist/src/core/embedding-store.js +97 -0
- package/dist/src/core/migrate-embeddings.d.ts +3 -0
- package/dist/src/core/migrate-embeddings.js +30 -0
- package/dist/src/core/migrate-folders.d.ts +2 -0
- package/dist/src/core/migrate-folders.js +45 -0
- package/dist/src/core/storage-engine.d.ts +29 -0
- package/dist/src/core/storage-engine.js +191 -0
- package/dist/src/gui-worker.d.ts +1 -0
- package/dist/src/gui-worker.js +349 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +678 -0
- package/dist/src/tools/check-freshness.d.ts +18 -0
- package/dist/src/tools/check-freshness.js +167 -0
- package/dist/src/tools/check-links.d.ts +14 -0
- package/dist/src/tools/check-links.js +99 -0
- package/dist/src/tools/create-doc.d.ts +15 -0
- package/dist/src/tools/create-doc.js +78 -0
- package/dist/src/tools/delete-doc.d.ts +15 -0
- package/dist/src/tools/delete-doc.js +18 -0
- package/dist/src/tools/get-doc-metadata.d.ts +14 -0
- package/dist/src/tools/get-doc-metadata.js +40 -0
- package/dist/src/tools/get-doc-tree.d.ts +14 -0
- package/dist/src/tools/get-doc-tree.js +64 -0
- package/dist/src/tools/get-doc.d.ts +14 -0
- package/dist/src/tools/get-doc.js +55 -0
- package/dist/src/tools/git-diff-doc.d.ts +14 -0
- package/dist/src/tools/git-diff-doc.js +93 -0
- package/dist/src/tools/list-docs.d.ts +14 -0
- package/dist/src/tools/list-docs.js +78 -0
- package/dist/src/tools/list-projects.d.ts +14 -0
- package/dist/src/tools/list-projects.js +16 -0
- package/dist/src/tools/move-doc.d.ts +15 -0
- package/dist/src/tools/move-doc.js +34 -0
- package/dist/src/tools/patch-doc.d.ts +15 -0
- package/dist/src/tools/patch-doc.js +37 -0
- package/dist/src/tools/refresh-file-snapshot.d.ts +14 -0
- package/dist/src/tools/refresh-file-snapshot.js +45 -0
- package/dist/src/tools/search-docs.d.ts +15 -0
- package/dist/src/tools/search-docs.js +26 -0
- package/dist/src/tools/set-project.d.ts +14 -0
- package/dist/src/tools/set-project.js +18 -0
- package/dist/src/tools/update-doc.d.ts +15 -0
- package/dist/src/tools/update-doc.js +66 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +2 -0
- package/gui/README.md +75 -0
- package/gui/dist/assets/index-BwyK1pan.css +1 -0
- package/gui/dist/assets/index-CzuFn4Pp.js +48 -0
- package/gui/dist/index.html +14 -0
- package/gui/dist/vite.svg +1 -0
- package/package.json +61 -0
- package/scripts/generate-version.js +20 -0
- package/scripts/migrate-links.js +258 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { createLogger } from "@imenam/mcp-gui-interface";
|
|
5
|
+
const logger = createLogger("StorageEngine");
|
|
6
|
+
export class StorageEngine {
|
|
7
|
+
rootDir;
|
|
8
|
+
constructor(rootDir) {
|
|
9
|
+
this.rootDir = rootDir;
|
|
10
|
+
}
|
|
11
|
+
getDataDir() {
|
|
12
|
+
const envDir = process.env.MCP_DATA_DIR;
|
|
13
|
+
return envDir && envDir.trim() !== "" ? envDir : path.join(this.rootDir, ".docs-data");
|
|
14
|
+
}
|
|
15
|
+
getProjectDir(project) {
|
|
16
|
+
const config = this.getConfig();
|
|
17
|
+
const proj = project ?? config.currentProject;
|
|
18
|
+
return path.join(this.getDataDir(), "projects", proj);
|
|
19
|
+
}
|
|
20
|
+
getConfigPath() {
|
|
21
|
+
return path.join(this.getDataDir(), "config.json");
|
|
22
|
+
}
|
|
23
|
+
getConfig() {
|
|
24
|
+
const configPath = this.getConfigPath();
|
|
25
|
+
if (fs.existsSync(configPath)) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
logger.warn("Failed to parse config.json, using defaults");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { currentProject: "default", projects: ["default"] };
|
|
34
|
+
}
|
|
35
|
+
saveConfig(config) {
|
|
36
|
+
const configPath = this.getConfigPath();
|
|
37
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
38
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
39
|
+
}
|
|
40
|
+
ensureProject(project) {
|
|
41
|
+
const projectDir = path.join(this.getDataDir(), "projects", project);
|
|
42
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
43
|
+
const config = this.getConfig();
|
|
44
|
+
if (!config.projects.includes(project)) {
|
|
45
|
+
config.projects.push(project);
|
|
46
|
+
this.saveConfig(config);
|
|
47
|
+
}
|
|
48
|
+
logger.info(`Project "${project}" ensured at ${projectDir}`);
|
|
49
|
+
}
|
|
50
|
+
walkMdFiles(dir) {
|
|
51
|
+
if (!fs.existsSync(dir))
|
|
52
|
+
return [];
|
|
53
|
+
const results = [];
|
|
54
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
55
|
+
const fullPath = path.join(dir, entry.name);
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
results.push(...this.walkMdFiles(fullPath));
|
|
58
|
+
}
|
|
59
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
60
|
+
results.push(fullPath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
docIdToFilePath(id, projectDir) {
|
|
66
|
+
const parts = id.split("/");
|
|
67
|
+
return path.join(projectDir, ...parts.slice(0, -1), parts[parts.length - 1] + ".md");
|
|
68
|
+
}
|
|
69
|
+
cleanEmptyDirs(dir, stopAt) {
|
|
70
|
+
if (dir === stopAt || !dir.startsWith(stopAt))
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
if (fs.readdirSync(dir).length === 0) {
|
|
74
|
+
fs.rmdirSync(dir);
|
|
75
|
+
this.cleanEmptyDirs(path.dirname(dir), stopAt);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore — dir may have been removed already
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async saveDoc(doc, project) {
|
|
83
|
+
const projectDir = this.getProjectDir(project);
|
|
84
|
+
const targetPath = this.docIdToFilePath(doc.id, projectDir);
|
|
85
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
86
|
+
const frontmatter = {
|
|
87
|
+
title: doc.title,
|
|
88
|
+
tracked_files: doc.tracked_files,
|
|
89
|
+
tags: doc.tags,
|
|
90
|
+
...(doc.related_docs?.length && { related_docs: doc.related_docs }),
|
|
91
|
+
createdAt: doc.createdAt,
|
|
92
|
+
updatedAt: doc.updatedAt,
|
|
93
|
+
};
|
|
94
|
+
const output = matter.stringify(doc.content, frontmatter);
|
|
95
|
+
fs.writeFileSync(targetPath, output, "utf-8");
|
|
96
|
+
logger.debug(`Doc saved: ${targetPath}`);
|
|
97
|
+
}
|
|
98
|
+
async getDoc(id, project) {
|
|
99
|
+
const projectDir = this.getProjectDir(project);
|
|
100
|
+
const filePath = this.docIdToFilePath(id, projectDir);
|
|
101
|
+
if (!fs.existsSync(filePath))
|
|
102
|
+
return null;
|
|
103
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
104
|
+
const { data, content } = matter(raw);
|
|
105
|
+
const parts = id.split("/");
|
|
106
|
+
const folder = parts.length > 1 ? parts.slice(0, -1).join("/") : undefined;
|
|
107
|
+
return {
|
|
108
|
+
id,
|
|
109
|
+
title: data.title ?? "",
|
|
110
|
+
content: content.trim(),
|
|
111
|
+
tracked_files: (data.tracked_files ?? []),
|
|
112
|
+
tags: (data.tags ?? []),
|
|
113
|
+
folder,
|
|
114
|
+
related_docs: (data.related_docs ?? []),
|
|
115
|
+
createdAt: data.createdAt ?? "",
|
|
116
|
+
updatedAt: data.updatedAt ?? "",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async listDocs(project) {
|
|
120
|
+
const projectDir = this.getProjectDir(project);
|
|
121
|
+
if (!fs.existsSync(projectDir))
|
|
122
|
+
return [];
|
|
123
|
+
const files = this.walkMdFiles(projectDir);
|
|
124
|
+
const summaries = [];
|
|
125
|
+
for (const filePath of files) {
|
|
126
|
+
try {
|
|
127
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
128
|
+
const { data } = matter(raw);
|
|
129
|
+
const stat = fs.statSync(filePath);
|
|
130
|
+
const relPath = path.relative(projectDir, filePath);
|
|
131
|
+
const id = relPath.replace(/\.md$/, "").split(path.sep).join("/");
|
|
132
|
+
const folder = id.includes("/") ? id.split("/").slice(0, -1).join("/") : undefined;
|
|
133
|
+
summaries.push({
|
|
134
|
+
id,
|
|
135
|
+
title: data.title ?? "",
|
|
136
|
+
createdAt: stat.birthtime.toISOString(),
|
|
137
|
+
updatedAt: data.updatedAt ?? "",
|
|
138
|
+
tags: (data.tags ?? []),
|
|
139
|
+
folder,
|
|
140
|
+
isStale: false,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
logger.warn(`Failed to read doc ${filePath}: ${err}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return summaries;
|
|
148
|
+
}
|
|
149
|
+
async resolveDoc(id) {
|
|
150
|
+
const config = this.getConfig();
|
|
151
|
+
for (const project of config.projects) {
|
|
152
|
+
const doc = await this.getDoc(id, project);
|
|
153
|
+
if (doc)
|
|
154
|
+
return { doc, project };
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
async deleteDoc(id, project) {
|
|
159
|
+
const projectDir = this.getProjectDir(project);
|
|
160
|
+
const filePath = this.docIdToFilePath(id, projectDir);
|
|
161
|
+
if (!fs.existsSync(filePath))
|
|
162
|
+
return false;
|
|
163
|
+
fs.unlinkSync(filePath);
|
|
164
|
+
this.cleanEmptyDirs(path.dirname(filePath), projectDir);
|
|
165
|
+
logger.info(`Doc deleted: ${filePath}`);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
async moveDoc(oldId, newId, project) {
|
|
169
|
+
const projectDir = this.getProjectDir(project);
|
|
170
|
+
const oldFilePath = this.docIdToFilePath(oldId, projectDir);
|
|
171
|
+
if (!fs.existsSync(oldFilePath))
|
|
172
|
+
return false;
|
|
173
|
+
const newFilePath = this.docIdToFilePath(newId, projectDir);
|
|
174
|
+
if (fs.existsSync(newFilePath)) {
|
|
175
|
+
throw new Error(`A document already exists at path "${newId}".`);
|
|
176
|
+
}
|
|
177
|
+
const doc = await this.getDoc(oldId, project);
|
|
178
|
+
if (!doc)
|
|
179
|
+
return false;
|
|
180
|
+
const newParts = newId.split("/");
|
|
181
|
+
doc.id = newId;
|
|
182
|
+
doc.folder = newParts.length > 1 ? newParts.slice(0, -1).join("/") : undefined;
|
|
183
|
+
doc.updatedAt = new Date().toISOString();
|
|
184
|
+
fs.mkdirSync(path.dirname(newFilePath), { recursive: true });
|
|
185
|
+
await this.saveDoc(doc, project);
|
|
186
|
+
fs.unlinkSync(oldFilePath);
|
|
187
|
+
this.cleanEmptyDirs(path.dirname(oldFilePath), projectDir);
|
|
188
|
+
logger.info(`Doc moved: ${oldFilePath} → ${newFilePath}`);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { streamSSE } from "hono/streaming";
|
|
5
|
+
import { ProxyClient } from "@imenam/mcp-gui-interface";
|
|
6
|
+
import { IPCMessageSchema } from "../shared/schemas/doc.schema.js";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { createLogger, setupLogging } from "@imenam/mcp-gui-interface";
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
function findProjectRoot(startDir) {
|
|
13
|
+
let current = startDir;
|
|
14
|
+
while (current !== path.dirname(current)) {
|
|
15
|
+
if (fs.existsSync(path.join(current, "gui")) || fs.existsSync(path.join(current, "package.json"))) {
|
|
16
|
+
return current;
|
|
17
|
+
}
|
|
18
|
+
current = path.dirname(current);
|
|
19
|
+
}
|
|
20
|
+
return startDir;
|
|
21
|
+
}
|
|
22
|
+
const rootDir = findProjectRoot(__dirname);
|
|
23
|
+
const staticRoot = path.join(rootDir, "gui/dist");
|
|
24
|
+
const absoluteStaticRoot = path.resolve(staticRoot);
|
|
25
|
+
setupLogging({ processLabel: "GUI-WORKER", logDir: path.join(rootDir, ".docs-data", "logs") });
|
|
26
|
+
const logger = createLogger("GUI-WORKER");
|
|
27
|
+
process.on("uncaughtException", (err) => {
|
|
28
|
+
logger.error(`Uncaught Exception: ${err.message}`);
|
|
29
|
+
logger.error(err.stack || "No stack trace");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
process.on("unhandledRejection", (reason) => {
|
|
33
|
+
logger.error(`Unhandled Rejection: ${reason}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
const parentPid = process.ppid;
|
|
37
|
+
const parentCheckInterval = setInterval(() => {
|
|
38
|
+
try {
|
|
39
|
+
process.kill(parentPid, 0);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
logger.info(`Parent process (PID ${parentPid}) is gone. Exiting.`);
|
|
43
|
+
clearInterval(parentCheckInterval);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
}, 2000);
|
|
47
|
+
const app = new Hono();
|
|
48
|
+
let proxyClient = null;
|
|
49
|
+
const sseControllers = new Set();
|
|
50
|
+
const pendingRequests = new Map();
|
|
51
|
+
function safeSend(message) {
|
|
52
|
+
if (process.send && process.connected) {
|
|
53
|
+
try {
|
|
54
|
+
return process.send(message);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logger.error(`[IPC] Failed to send message: ${err.message}`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
function ipcRequest(type, data, timeout = 5000) {
|
|
64
|
+
const correlationId = Math.random().toString(36).substring(7);
|
|
65
|
+
const message = { type, correlationId, data, timestamp: new Date().toISOString() };
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
if (pendingRequests.has(correlationId)) {
|
|
69
|
+
pendingRequests.delete(correlationId);
|
|
70
|
+
resolve({ error: "Request timed out" });
|
|
71
|
+
}
|
|
72
|
+
}, timeout);
|
|
73
|
+
pendingRequests.set(correlationId, {
|
|
74
|
+
resolve: (msg) => {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
resolve(msg);
|
|
77
|
+
},
|
|
78
|
+
reject: (err) => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
resolve({ error: err.message });
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const sent = safeSend(message);
|
|
84
|
+
if (!sent) {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
pendingRequests.delete(correlationId);
|
|
87
|
+
resolve({ error: "IPC not available" });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function setupIpcErrorHandlers() {
|
|
92
|
+
process.on("error", (err) => {
|
|
93
|
+
if (err.code === "ERR_IPC_CHANNEL_CLOSED")
|
|
94
|
+
return;
|
|
95
|
+
logger.error(`Process error: ${err.message}`);
|
|
96
|
+
});
|
|
97
|
+
process.on("disconnect", () => {
|
|
98
|
+
logger.debug("Disconnected from parent, exiting.");
|
|
99
|
+
process.exit(0);
|
|
100
|
+
});
|
|
101
|
+
process.on("message", (msg) => {
|
|
102
|
+
if (msg && msg.correlationId && pendingRequests.has(msg.correlationId)) {
|
|
103
|
+
const { resolve } = pendingRequests.get(msg.correlationId);
|
|
104
|
+
pendingRequests.delete(msg.correlationId);
|
|
105
|
+
resolve(msg);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (msg && msg.type === "GET_HEALTH") {
|
|
109
|
+
const memory = process.memoryUsage();
|
|
110
|
+
safeSend({
|
|
111
|
+
type: "HEALTH_RESPONSE",
|
|
112
|
+
correlationId: msg.correlationId,
|
|
113
|
+
data: { status: "ok", uptime: process.uptime(), memory: { rss: memory.rss, heapUsed: memory.heapUsed } },
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (msg && (msg.type === "INITIAL_STATE" || msg.type === "CONFIG_UPDATED")) {
|
|
119
|
+
for (const controller of sseControllers) {
|
|
120
|
+
try {
|
|
121
|
+
controller.writeSSE({ data: JSON.stringify(msg), event: "message" });
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
sseControllers.delete(controller);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// --- HTTP Routes ---
|
|
131
|
+
app.get("/proxy/health", (c) => {
|
|
132
|
+
const memory = process.memoryUsage();
|
|
133
|
+
return c.json({
|
|
134
|
+
status: "ok",
|
|
135
|
+
proxy: proxyClient ? proxyClient.getStatus() : process.env.PROXY_URL ? "error" : "standalone",
|
|
136
|
+
uptime: process.uptime(),
|
|
137
|
+
memory: { rss: memory.rss, heapUsed: memory.heapUsed },
|
|
138
|
+
ipc: process.connected ? "connected" : "disconnected",
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
app.get("/api/events", (c) => {
|
|
143
|
+
return streamSSE(c, async (stream) => {
|
|
144
|
+
sseControllers.add(stream);
|
|
145
|
+
stream.onAbort(() => { sseControllers.delete(stream); });
|
|
146
|
+
safeSend({ type: "GET_INITIAL_STATE", timestamp: new Date().toISOString() });
|
|
147
|
+
await stream.writeSSE({ data: "connected", event: "ping" });
|
|
148
|
+
while (true) {
|
|
149
|
+
await stream.sleep(30000);
|
|
150
|
+
try {
|
|
151
|
+
await stream.writeSSE({ data: "ping", event: "ping" });
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
sseControllers.delete(stream);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
app.get("/api/configurations", async (c) => {
|
|
161
|
+
const configRes = await ipcRequest("GET_CONFIG");
|
|
162
|
+
if (configRes.error)
|
|
163
|
+
return c.json([]);
|
|
164
|
+
const currentDataDir = configRes.data?.dataDir || "";
|
|
165
|
+
const currentModel = configRes.data?.model || "text-embedding-3-small";
|
|
166
|
+
return c.json([
|
|
167
|
+
{
|
|
168
|
+
key: "MCP_DATA_DIR",
|
|
169
|
+
label: "Data Directory",
|
|
170
|
+
type: "text",
|
|
171
|
+
description: "Répertoire de stockage des documentations",
|
|
172
|
+
current: currentDataDir,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
key: "MCP_DOCUMENTOR_OPENAI_API_KEY",
|
|
176
|
+
label: "OpenAI API Key",
|
|
177
|
+
type: "password",
|
|
178
|
+
description: "Clé API OpenAI pour la génération d'embeddings",
|
|
179
|
+
current: "***",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
key: "MCP_DOCUMENTOR_OPENAI_MODEL",
|
|
183
|
+
label: "Embedding Model",
|
|
184
|
+
type: "text",
|
|
185
|
+
description: "Modèle OpenAI pour les embeddings (défaut: text-embedding-3-small)",
|
|
186
|
+
current: currentModel,
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
});
|
|
190
|
+
app.post("/api/configurations/apply", async (c) => {
|
|
191
|
+
try {
|
|
192
|
+
const { key, value } = await c.req.json();
|
|
193
|
+
if (!key || value === undefined) {
|
|
194
|
+
return c.json({ success: false, error: "key and value are required" }, 400);
|
|
195
|
+
}
|
|
196
|
+
const result = await ipcRequest("UPDATE_CONFIG", { key, value });
|
|
197
|
+
if (result.error)
|
|
198
|
+
return c.json({ success: false, error: result.error }, 500);
|
|
199
|
+
return c.json({ success: true });
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
return c.json({ success: false, error: err.message }, 500);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
app.get("/api/config", async (c) => {
|
|
206
|
+
const result = await ipcRequest("GET_CONFIG");
|
|
207
|
+
if (result.error)
|
|
208
|
+
return c.json({ error: result.error }, 500);
|
|
209
|
+
return c.json(result.data);
|
|
210
|
+
});
|
|
211
|
+
app.get("/api/projects", async (c) => {
|
|
212
|
+
const result = await ipcRequest("LIST_PROJECTS");
|
|
213
|
+
if (result.error)
|
|
214
|
+
return c.json({ error: result.error }, 500);
|
|
215
|
+
return c.json(result.data);
|
|
216
|
+
});
|
|
217
|
+
app.put("/api/project", async (c) => {
|
|
218
|
+
const { project } = await c.req.json();
|
|
219
|
+
if (!project)
|
|
220
|
+
return c.json({ error: "project is required" }, 400);
|
|
221
|
+
const result = await ipcRequest("UPDATE_PROJECT", { project });
|
|
222
|
+
if (result.error)
|
|
223
|
+
return c.json({ error: result.error }, 500);
|
|
224
|
+
return c.json(result.data);
|
|
225
|
+
});
|
|
226
|
+
app.get("/api/docs", async (c) => {
|
|
227
|
+
const result = await ipcRequest("LIST_DOCS");
|
|
228
|
+
if (result.error)
|
|
229
|
+
return c.json({ error: result.error }, 500);
|
|
230
|
+
return c.json(result.data);
|
|
231
|
+
});
|
|
232
|
+
app.get("/api/doc/*", async (c) => {
|
|
233
|
+
const id = decodeURIComponent(c.req.path.slice("/api/doc/".length));
|
|
234
|
+
const result = await ipcRequest("GET_DOC", { id });
|
|
235
|
+
if (result.error)
|
|
236
|
+
return c.json({ error: result.error }, 400);
|
|
237
|
+
return c.json(result.data);
|
|
238
|
+
});
|
|
239
|
+
app.get("/api/freshness/*", async (c) => {
|
|
240
|
+
const id = decodeURIComponent(c.req.path.slice("/api/freshness/".length));
|
|
241
|
+
const result = await ipcRequest("CHECK_FRESHNESS", { id });
|
|
242
|
+
if (result.error)
|
|
243
|
+
return c.json({ error: result.error }, 400);
|
|
244
|
+
return c.json(result.data);
|
|
245
|
+
});
|
|
246
|
+
app.get("/api/git-diff/*", async (c) => {
|
|
247
|
+
const id = decodeURIComponent(c.req.path.slice("/api/git-diff/".length));
|
|
248
|
+
const result = await ipcRequest("GIT_DIFF", { id }, 15000);
|
|
249
|
+
if (result.error)
|
|
250
|
+
return c.json({ error: result.error }, 400);
|
|
251
|
+
return c.json(result.data);
|
|
252
|
+
});
|
|
253
|
+
app.post("/api/search", async (c) => {
|
|
254
|
+
const { query, top_k, all_projects } = await c.req.json();
|
|
255
|
+
if (!query)
|
|
256
|
+
return c.json({ error: "query is required" }, 400);
|
|
257
|
+
const result = await ipcRequest("SEARCH_DOCS", { query, top_k: top_k || 5, all_projects: !!all_projects });
|
|
258
|
+
if (result.error)
|
|
259
|
+
return c.json({ error: result.error }, 500);
|
|
260
|
+
return c.json(result.data);
|
|
261
|
+
});
|
|
262
|
+
// Static files
|
|
263
|
+
app.use("/assets/*", serveStatic({ root: path.relative(process.cwd(), absoluteStaticRoot) }));
|
|
264
|
+
// SPA Fallback
|
|
265
|
+
app.get("/*", (c, next) => {
|
|
266
|
+
const urlPath = c.req.path;
|
|
267
|
+
if (urlPath.startsWith("/api") || urlPath.startsWith("/proxy") || urlPath.startsWith("/assets")) {
|
|
268
|
+
return next();
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
const indexPath = path.join(absoluteStaticRoot, "index.html");
|
|
272
|
+
if (fs.existsSync(indexPath)) {
|
|
273
|
+
const html = fs.readFileSync(indexPath, "utf8");
|
|
274
|
+
return c.html(html);
|
|
275
|
+
}
|
|
276
|
+
return c.text("GUI Build not found at " + absoluteStaticRoot, 404);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return next();
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
async function startServer() {
|
|
283
|
+
if (!process.env.PROXY_URL) {
|
|
284
|
+
process.stderr.write('[GUI] PROXY_URL non défini — arrêt.\n');
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
const proxyUrl = process.env.PROXY_URL;
|
|
288
|
+
const appPath = process.env.APP_PATH || "/documentor-gui";
|
|
289
|
+
const appName = process.env.APP_NAME || "MCP Documentor GUI";
|
|
290
|
+
logger.info(`Starting server with proxyUrl=${proxyUrl}`);
|
|
291
|
+
proxyClient = new ProxyClient(proxyUrl);
|
|
292
|
+
let result;
|
|
293
|
+
try {
|
|
294
|
+
// Second arg: fallbackPort (required by published @imenam/mcp-gui-interface API); unused when proxy assigns the port.
|
|
295
|
+
result = await proxyClient.register({ path: appPath, name: appName }, 0);
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
process.stderr.write('[GUI] Enregistrement proxy échoué — arrêt.\n');
|
|
299
|
+
logger.error(`Proxy registration error: ${e.message}`);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (result.error === "HTTP 409") {
|
|
304
|
+
logger.info("Another GUI instance already registered.");
|
|
305
|
+
safeSend(IPCMessageSchema.parse({
|
|
306
|
+
type: "ALREADY_RUNNING",
|
|
307
|
+
data: { url: `${proxyUrl}${appPath}` },
|
|
308
|
+
timestamp: new Date().toISOString(),
|
|
309
|
+
}));
|
|
310
|
+
process.exit(0);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (!result.success) {
|
|
314
|
+
process.stderr.write('[GUI] Enregistrement proxy échoué — arrêt.\n');
|
|
315
|
+
logger.error(`Proxy registration failed: ${result.error}`);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const finalPort = result.port;
|
|
320
|
+
const publicUrl = result.url || `${proxyUrl}${appPath}`;
|
|
321
|
+
logger.info(`Registered with proxy. Public URL: ${publicUrl}, Local Port: ${finalPort}`);
|
|
322
|
+
const cleanupProxy = async () => {
|
|
323
|
+
if (proxyClient)
|
|
324
|
+
await proxyClient.unregister();
|
|
325
|
+
process.exit(0);
|
|
326
|
+
};
|
|
327
|
+
process.on("SIGTERM", cleanupProxy);
|
|
328
|
+
process.on("SIGINT", cleanupProxy);
|
|
329
|
+
setupIpcErrorHandlers();
|
|
330
|
+
logger.info(`Starting Hono server on port ${finalPort}...`);
|
|
331
|
+
const serverInstance = serve({ fetch: app.fetch, port: finalPort, hostname: "0.0.0.0" }, (info) => {
|
|
332
|
+
logger.info(`Dashboard server listening on port ${info.port}`);
|
|
333
|
+
safeSend(IPCMessageSchema.parse({
|
|
334
|
+
type: "READY",
|
|
335
|
+
data: { port: info.port, isProxied: true, url: publicUrl },
|
|
336
|
+
timestamp: new Date().toISOString(),
|
|
337
|
+
}));
|
|
338
|
+
});
|
|
339
|
+
serverInstance.on("error", (err) => {
|
|
340
|
+
logger.error(`Server error: ${err.message}`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
startServer().catch((err) => {
|
|
345
|
+
logger.error(`Failed to start GUI Worker: ${err.message}`);
|
|
346
|
+
if (err.stack)
|
|
347
|
+
logger.error(err.stack);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
});
|