@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,93 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { GitDiffDocArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
4
|
+
import { computeFreshness } from "./check-freshness.js";
|
|
5
|
+
function getGitDiffForFile(tracked) {
|
|
6
|
+
const filePath = tracked.path;
|
|
7
|
+
const fileDir = path.dirname(filePath);
|
|
8
|
+
let repoRoot;
|
|
9
|
+
try {
|
|
10
|
+
repoRoot = execSync(`git -C "${fileDir}" rev-parse --show-toplevel`, { encoding: "utf-8" }).trim();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return ` [ERROR] Not a git repository: ${filePath}`;
|
|
14
|
+
}
|
|
15
|
+
const relativeFilePath = path.relative(repoRoot, filePath).replace(/\\/g, "/");
|
|
16
|
+
let commitHash;
|
|
17
|
+
try {
|
|
18
|
+
commitHash = execSync(`git -C "${repoRoot}" log --before="${tracked.lastModifiedAt}" -1 --format=%H -- "${relativeFilePath}"`, { encoding: "utf-8" }).trim();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return ` [ERROR] Could not retrieve git log for: ${filePath}`;
|
|
22
|
+
}
|
|
23
|
+
if (!commitHash) {
|
|
24
|
+
return ` [INFO] No commit found before snapshot date (${tracked.lastModifiedAt}) for: ${filePath}`;
|
|
25
|
+
}
|
|
26
|
+
let diff;
|
|
27
|
+
try {
|
|
28
|
+
diff = execSync(`git -C "${repoRoot}" diff ${commitHash} -- "${relativeFilePath}"`, { encoding: "utf-8" }).trim();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return ` [ERROR] Could not compute diff for: ${filePath}`;
|
|
32
|
+
}
|
|
33
|
+
if (!diff) {
|
|
34
|
+
return ` [INFO] No changes since snapshot (commit ${commitHash.slice(0, 7)}) for: ${filePath}`;
|
|
35
|
+
}
|
|
36
|
+
return diff;
|
|
37
|
+
}
|
|
38
|
+
export async function gitDiffDoc(storageEngine, args) {
|
|
39
|
+
try {
|
|
40
|
+
const validated = GitDiffDocArgsSchema.parse(args);
|
|
41
|
+
const docSections = [];
|
|
42
|
+
const seenFilePaths = new Set();
|
|
43
|
+
for (const docPath of validated.paths) {
|
|
44
|
+
const resolved = await storageEngine.resolveDoc(docPath);
|
|
45
|
+
if (!resolved) {
|
|
46
|
+
docSections.push(`## ${docPath}\n\n[ERROR] Documentation entry not found in any project.`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const { doc } = resolved;
|
|
50
|
+
if (!doc.tracked_files || doc.tracked_files.length === 0) {
|
|
51
|
+
docSections.push(`## ${doc.title}\n\n[INFO] No tracked files.`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
let filesToDiff;
|
|
55
|
+
if (validated.file_paths) {
|
|
56
|
+
filesToDiff = doc.tracked_files.filter((tf) => validated.file_paths.includes(tf.path));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const freshness = computeFreshness(doc.tracked_files);
|
|
60
|
+
const changedPaths = new Set([...freshness.modified, ...freshness.missing]);
|
|
61
|
+
filesToDiff = doc.tracked_files.filter((tf) => changedPaths.has(tf.path));
|
|
62
|
+
}
|
|
63
|
+
if (filesToDiff.length === 0) {
|
|
64
|
+
const msg = validated.file_paths
|
|
65
|
+
? "[INFO] No matching tracked files for the provided file_paths."
|
|
66
|
+
: "[INFO] No modified or missing files detected since last snapshot. Documentation is up to date.";
|
|
67
|
+
docSections.push(`## ${doc.title}\n\n${msg}`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const fileSections = filesToDiff
|
|
71
|
+
.filter((tf) => {
|
|
72
|
+
if (seenFilePaths.has(tf.path))
|
|
73
|
+
return false;
|
|
74
|
+
seenFilePaths.add(tf.path);
|
|
75
|
+
return true;
|
|
76
|
+
})
|
|
77
|
+
.map((tf) => {
|
|
78
|
+
const diff = getGitDiffForFile(tf);
|
|
79
|
+
return `### ${tf.path}\nSnapshot date: ${tf.lastModifiedAt}\n\n${diff}`;
|
|
80
|
+
});
|
|
81
|
+
if (fileSections.length === 0) {
|
|
82
|
+
docSections.push(`## ${doc.title} [${docPath}]\n\n[INFO] All tracked files already shown in a previous documentation section.`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
docSections.push(`## ${doc.title} [${docPath}]\n\n${fileSections.join("\n\n---\n\n")}`);
|
|
86
|
+
}
|
|
87
|
+
return { content: [{ type: "text", text: docSections.join("\n\n═══════════════════════════════════\n\n") }] };
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
91
|
+
return { isError: true, content: [{ type: "text", text: `Error computing git diff: ${message}` }] };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
export declare function listDocs(storageEngine: StorageEngine, args?: unknown): Promise<{
|
|
3
|
+
content: {
|
|
4
|
+
type: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
isError?: undefined;
|
|
8
|
+
} | {
|
|
9
|
+
isError: boolean;
|
|
10
|
+
content: {
|
|
11
|
+
type: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { computeFreshness } from "./check-freshness.js";
|
|
2
|
+
function renderProjectDocs(summaries, totalLabel) {
|
|
3
|
+
if (summaries.length === 0)
|
|
4
|
+
return " (no entries)";
|
|
5
|
+
const groups = new Map();
|
|
6
|
+
for (const s of summaries) {
|
|
7
|
+
const key = s.folder ?? "";
|
|
8
|
+
if (!groups.has(key))
|
|
9
|
+
groups.set(key, []);
|
|
10
|
+
groups.get(key).push(s);
|
|
11
|
+
}
|
|
12
|
+
const sortedKeys = [...groups.keys()].sort((a, b) => {
|
|
13
|
+
if (a === "")
|
|
14
|
+
return 1;
|
|
15
|
+
if (b === "")
|
|
16
|
+
return -1;
|
|
17
|
+
return a.localeCompare(b);
|
|
18
|
+
});
|
|
19
|
+
const lines = [`${totalLabel}`];
|
|
20
|
+
for (const key of sortedKeys) {
|
|
21
|
+
if (key)
|
|
22
|
+
lines.push(`\n 📁 ${key}`);
|
|
23
|
+
for (const s of groups.get(key)) {
|
|
24
|
+
const staleTag = s.isStale ? " [STALE]" : "";
|
|
25
|
+
const tags = s.tags.length > 0 ? ` (${s.tags.join(", ")})` : "";
|
|
26
|
+
const indent = key ? " " : " ";
|
|
27
|
+
lines.push(`${indent}- ${s.title} [${s.id}]${tags}${staleTag} — updated ${s.updatedAt}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
|
32
|
+
export async function listDocs(storageEngine, args) {
|
|
33
|
+
try {
|
|
34
|
+
const { project } = (args ?? {});
|
|
35
|
+
const config = storageEngine.getConfig();
|
|
36
|
+
const projectsToList = project ? [project] : config.projects;
|
|
37
|
+
const isMultiProject = projectsToList.length > 1;
|
|
38
|
+
if (!isMultiProject) {
|
|
39
|
+
const proj = projectsToList[0];
|
|
40
|
+
const summaries = await storageEngine.listDocs(proj);
|
|
41
|
+
for (const summary of summaries) {
|
|
42
|
+
const doc = await storageEngine.getDoc(summary.id, proj);
|
|
43
|
+
if (doc && doc.tracked_files.length > 0) {
|
|
44
|
+
const report = computeFreshness(doc.tracked_files);
|
|
45
|
+
summary.isStale = report.status === "stale";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (summaries.length === 0) {
|
|
49
|
+
return { content: [{ type: "text", text: `No documentation entries found in project "${proj}".` }] };
|
|
50
|
+
}
|
|
51
|
+
const rendered = renderProjectDocs(summaries, `Documentation entries (${summaries.length}):`);
|
|
52
|
+
return { content: [{ type: "text", text: rendered }] };
|
|
53
|
+
}
|
|
54
|
+
// Multi-project: aggregate with project headers
|
|
55
|
+
const lines = [];
|
|
56
|
+
let grandTotal = 0;
|
|
57
|
+
for (const proj of projectsToList) {
|
|
58
|
+
const summaries = await storageEngine.listDocs(proj);
|
|
59
|
+
for (const summary of summaries) {
|
|
60
|
+
const doc = await storageEngine.getDoc(summary.id, proj);
|
|
61
|
+
if (doc && doc.tracked_files.length > 0) {
|
|
62
|
+
const report = computeFreshness(doc.tracked_files);
|
|
63
|
+
summary.isStale = report.status === "stale";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
grandTotal += summaries.length;
|
|
67
|
+
lines.push(`\n🗂 ${proj} (${summaries.length})`);
|
|
68
|
+
lines.push(renderProjectDocs(summaries, ""));
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: `Documentation entries — all projects (${grandTotal} total):${lines.join("\n")}` }],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
76
|
+
return { isError: true, content: [{ type: "text", text: `Error listing docs: ${message}` }] };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
export declare function listProjects(storageEngine: StorageEngine): Promise<{
|
|
3
|
+
content: {
|
|
4
|
+
type: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
isError?: undefined;
|
|
8
|
+
} | {
|
|
9
|
+
isError: boolean;
|
|
10
|
+
content: {
|
|
11
|
+
type: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export async function listProjects(storageEngine) {
|
|
2
|
+
try {
|
|
3
|
+
const config = storageEngine.getConfig();
|
|
4
|
+
const lines = config.projects.map((p) => {
|
|
5
|
+
const marker = p === config.currentProject ? " (active)" : "";
|
|
6
|
+
return `- ${p}${marker}`;
|
|
7
|
+
});
|
|
8
|
+
return {
|
|
9
|
+
content: [{ type: "text", text: `Projects:\n\n${lines.join("\n")}\n\nCurrent project: ${config.currentProject}` }],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14
|
+
return { isError: true, content: [{ type: "text", text: `Error listing projects: ${message}` }] };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
import type { EmbeddingEngine } from "../core/embedding-engine.js";
|
|
3
|
+
export declare function moveDoc(storageEngine: StorageEngine, embeddingEngine: EmbeddingEngine, args: unknown): Promise<{
|
|
4
|
+
isError: boolean;
|
|
5
|
+
content: {
|
|
6
|
+
type: string;
|
|
7
|
+
text: string;
|
|
8
|
+
}[];
|
|
9
|
+
} | {
|
|
10
|
+
content: {
|
|
11
|
+
type: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
isError?: undefined;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { MoveDocArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
2
|
+
export async function moveDoc(storageEngine, embeddingEngine, args) {
|
|
3
|
+
try {
|
|
4
|
+
const validated = MoveDocArgsSchema.parse(args);
|
|
5
|
+
const resolved = await storageEngine.resolveDoc(validated.path);
|
|
6
|
+
if (!resolved) {
|
|
7
|
+
return {
|
|
8
|
+
isError: true,
|
|
9
|
+
content: [{ type: "text", text: `Documentation entry "${validated.path}" not found in any project.` }],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const { project: resolvedProject } = resolved;
|
|
13
|
+
const moved = await storageEngine.moveDoc(validated.path, validated.new_path, resolvedProject);
|
|
14
|
+
if (!moved) {
|
|
15
|
+
return {
|
|
16
|
+
isError: true,
|
|
17
|
+
content: [{ type: "text", text: `Failed to move "${validated.path}".` }],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
await embeddingEngine.moveEmbedding(validated.path, validated.new_path, resolvedProject);
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: "text",
|
|
25
|
+
text: `Documentation entry moved successfully.\nFrom: "${validated.path}"\nTo: "${validated.new_path}"\n\nNote: if other documents reference "${validated.path}" in their related_docs or via doc:// links, use check_links to detect broken references.`,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32
|
+
return { isError: true, content: [{ type: "text", text: `Error moving doc: ${message}` }] };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
import type { EmbeddingEngine } from "../core/embedding-engine.js";
|
|
3
|
+
export declare function patchDoc(storageEngine: StorageEngine, embeddingEngine: EmbeddingEngine, args: unknown): Promise<{
|
|
4
|
+
isError: boolean;
|
|
5
|
+
content: {
|
|
6
|
+
type: string;
|
|
7
|
+
text: string;
|
|
8
|
+
}[];
|
|
9
|
+
} | {
|
|
10
|
+
content: {
|
|
11
|
+
type: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
isError?: undefined;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PatchDocArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
2
|
+
export async function patchDoc(storageEngine, embeddingEngine, args) {
|
|
3
|
+
try {
|
|
4
|
+
const validated = PatchDocArgsSchema.parse(args);
|
|
5
|
+
const resolved = await storageEngine.resolveDoc(validated.path);
|
|
6
|
+
if (!resolved) {
|
|
7
|
+
return { isError: true, content: [{ type: "text", text: `Documentation entry "${validated.path}" not found in any project.` }] };
|
|
8
|
+
}
|
|
9
|
+
const { doc, project: resolvedProject } = resolved;
|
|
10
|
+
if (!doc.content.includes(validated.search)) {
|
|
11
|
+
const preview = doc.content.slice(0, 300).replace(/\n/g, "↵");
|
|
12
|
+
return {
|
|
13
|
+
isError: true,
|
|
14
|
+
content: [{
|
|
15
|
+
type: "text",
|
|
16
|
+
text: `Text to replace not found in "${validated.path}" (project: "${resolvedProject}").\nDoc content starts with: "${preview}…"\nTip: use get_doc to retrieve the exact current content before patching.`,
|
|
17
|
+
}],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
doc.content = doc.content.replace(validated.search, validated.replacement);
|
|
21
|
+
doc.updatedAt = new Date().toISOString();
|
|
22
|
+
await storageEngine.saveDoc(doc, resolvedProject);
|
|
23
|
+
if (embeddingEngine.isConfigured()) {
|
|
24
|
+
const embedding = await embeddingEngine.generateEmbedding(doc.content);
|
|
25
|
+
if (embedding) {
|
|
26
|
+
await embeddingEngine.saveEmbedding(validated.path, embedding, resolvedProject);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text", text: `Documentation entry "${validated.path}" patched successfully.` }],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
35
|
+
return { isError: true, content: [{ type: "text", text: `Error patching doc: ${message}` }] };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
export declare function refreshFileSnapshot(storageEngine: StorageEngine, args: unknown): Promise<{
|
|
3
|
+
content: {
|
|
4
|
+
type: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
isError?: undefined;
|
|
8
|
+
} | {
|
|
9
|
+
isError: boolean;
|
|
10
|
+
content: {
|
|
11
|
+
type: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { RefreshFileSnapshotArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
3
|
+
import { computeFileHash } from "./check-freshness.js";
|
|
4
|
+
export async function refreshFileSnapshot(storageEngine, args) {
|
|
5
|
+
try {
|
|
6
|
+
const validated = RefreshFileSnapshotArgsSchema.parse(args);
|
|
7
|
+
const docSections = [];
|
|
8
|
+
for (const docPath of validated.paths) {
|
|
9
|
+
const resolved = await storageEngine.resolveDoc(docPath);
|
|
10
|
+
if (!resolved) {
|
|
11
|
+
docSections.push(`**${docPath}**: not found in any project.`);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const { doc, project: resolvedProject } = resolved;
|
|
15
|
+
const refreshed = [];
|
|
16
|
+
const missing = [];
|
|
17
|
+
const skipped = [];
|
|
18
|
+
for (const tracked of doc.tracked_files) {
|
|
19
|
+
if (validated.file_paths && !validated.file_paths.includes(tracked.path)) {
|
|
20
|
+
skipped.push(tracked.path);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!fs.existsSync(tracked.path)) {
|
|
24
|
+
missing.push(tracked.path);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
tracked.lastModifiedAt = fs.statSync(tracked.path).mtime.toISOString();
|
|
28
|
+
tracked.contentHash = computeFileHash(tracked.path);
|
|
29
|
+
refreshed.push(tracked.path);
|
|
30
|
+
}
|
|
31
|
+
await storageEngine.saveDoc(doc, resolvedProject);
|
|
32
|
+
let section = `**${doc.title}** [${docPath}]: refreshed ${refreshed.length}, missing ${missing.length}, skipped ${skipped.length}`;
|
|
33
|
+
if (refreshed.length > 0)
|
|
34
|
+
section += `\n${refreshed.map((f) => ` ✓ ${f}`).join("\n")}`;
|
|
35
|
+
if (missing.length > 0)
|
|
36
|
+
section += `\n${missing.map((f) => ` ✗ ${f}`).join("\n")}`;
|
|
37
|
+
docSections.push(section);
|
|
38
|
+
}
|
|
39
|
+
return { content: [{ type: "text", text: docSections.join("\n\n") }] };
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
43
|
+
return { isError: true, content: [{ type: "text", text: `Error refreshing snapshot: ${message}` }] };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
import type { EmbeddingEngine } from "../core/embedding-engine.js";
|
|
3
|
+
export declare function searchDocs(storageEngine: StorageEngine, embeddingEngine: EmbeddingEngine, args: unknown): Promise<{
|
|
4
|
+
content: {
|
|
5
|
+
type: string;
|
|
6
|
+
text: string;
|
|
7
|
+
}[];
|
|
8
|
+
isError?: undefined;
|
|
9
|
+
} | {
|
|
10
|
+
isError: boolean;
|
|
11
|
+
content: {
|
|
12
|
+
type: string;
|
|
13
|
+
text: string;
|
|
14
|
+
}[];
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SearchDocsArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
2
|
+
export async function searchDocs(storageEngine, embeddingEngine, args) {
|
|
3
|
+
try {
|
|
4
|
+
const validated = SearchDocsArgsSchema.parse(args);
|
|
5
|
+
const currentProject = storageEngine.getConfig().currentProject;
|
|
6
|
+
const results = await embeddingEngine.search(validated.query, currentProject, validated.top_k, async (id) => {
|
|
7
|
+
const doc = await storageEngine.getDoc(id);
|
|
8
|
+
if (!doc)
|
|
9
|
+
return null;
|
|
10
|
+
return { title: doc.title, content: doc.content };
|
|
11
|
+
});
|
|
12
|
+
if (results.length === 0) {
|
|
13
|
+
return { content: [{ type: "text", text: "No matching documentation entries found." }] };
|
|
14
|
+
}
|
|
15
|
+
const lines = results.map((r, i) => {
|
|
16
|
+
return `${i + 1}. **${r.title}** [${r.id}] — score: ${(r.score * 100).toFixed(1)}%\n ${r.excerpt}...`;
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text: `Search results for "${validated.query}":\n\n${lines.join("\n\n")}` }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24
|
+
return { isError: true, content: [{ type: "text", text: `Error searching docs: ${message}` }] };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
export declare function setProject(storageEngine: StorageEngine, args: unknown): Promise<{
|
|
3
|
+
content: {
|
|
4
|
+
type: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
isError?: undefined;
|
|
8
|
+
} | {
|
|
9
|
+
isError: boolean;
|
|
10
|
+
content: {
|
|
11
|
+
type: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SetProjectArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
2
|
+
export async function setProject(storageEngine, args) {
|
|
3
|
+
try {
|
|
4
|
+
const validated = SetProjectArgsSchema.parse(args);
|
|
5
|
+
const project = validated.project.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
6
|
+
storageEngine.ensureProject(project);
|
|
7
|
+
const config = storageEngine.getConfig();
|
|
8
|
+
config.currentProject = project;
|
|
9
|
+
storageEngine.saveConfig(config);
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: "text", text: `Switched to project "${project}". All subsequent operations will use this project.` }],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
16
|
+
return { isError: true, content: [{ type: "text", text: `Error setting project: ${message}` }] };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
import type { EmbeddingEngine } from "../core/embedding-engine.js";
|
|
3
|
+
export declare function updateDoc(storageEngine: StorageEngine, embeddingEngine: EmbeddingEngine, args: unknown): Promise<{
|
|
4
|
+
isError: boolean;
|
|
5
|
+
content: {
|
|
6
|
+
type: string;
|
|
7
|
+
text: string;
|
|
8
|
+
}[];
|
|
9
|
+
} | {
|
|
10
|
+
content: {
|
|
11
|
+
type: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
isError?: undefined;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { UpdateDocArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
4
|
+
import { computeFileHash } from "./check-freshness.js";
|
|
5
|
+
function snapshotTrackedFiles(filePaths) {
|
|
6
|
+
const tracked = [];
|
|
7
|
+
for (const fp of filePaths) {
|
|
8
|
+
const absolutePath = path.resolve(process.cwd(), fp);
|
|
9
|
+
let mtime = new Date().toISOString();
|
|
10
|
+
let contentHash;
|
|
11
|
+
if (fs.existsSync(absolutePath)) {
|
|
12
|
+
mtime = fs.statSync(absolutePath).mtime.toISOString();
|
|
13
|
+
contentHash = computeFileHash(absolutePath);
|
|
14
|
+
}
|
|
15
|
+
tracked.push({ path: absolutePath, lastModifiedAt: mtime, ...(contentHash !== undefined && { contentHash }) });
|
|
16
|
+
}
|
|
17
|
+
return tracked;
|
|
18
|
+
}
|
|
19
|
+
export async function updateDoc(storageEngine, embeddingEngine, args) {
|
|
20
|
+
try {
|
|
21
|
+
const validated = UpdateDocArgsSchema.parse(args);
|
|
22
|
+
const resolved = await storageEngine.resolveDoc(validated.path);
|
|
23
|
+
if (!resolved) {
|
|
24
|
+
return { isError: true, content: [{ type: "text", text: `Documentation entry "${validated.path}" not found in any project.` }] };
|
|
25
|
+
}
|
|
26
|
+
const { doc, project: resolvedProject } = resolved;
|
|
27
|
+
if (validated.content !== undefined) {
|
|
28
|
+
doc.content = validated.content;
|
|
29
|
+
}
|
|
30
|
+
if (validated.tracked_files !== undefined) {
|
|
31
|
+
doc.tracked_files = snapshotTrackedFiles(validated.tracked_files);
|
|
32
|
+
}
|
|
33
|
+
if (validated.tags !== undefined) {
|
|
34
|
+
doc.tags = validated.tags;
|
|
35
|
+
}
|
|
36
|
+
if (validated.related_docs !== undefined) {
|
|
37
|
+
doc.related_docs = validated.related_docs;
|
|
38
|
+
}
|
|
39
|
+
doc.updatedAt = new Date().toISOString();
|
|
40
|
+
await storageEngine.saveDoc(doc, resolvedProject);
|
|
41
|
+
if (validated.content !== undefined && embeddingEngine.isConfigured()) {
|
|
42
|
+
const embedding = await embeddingEngine.generateEmbedding(validated.content);
|
|
43
|
+
if (embedding) {
|
|
44
|
+
await embeddingEngine.saveEmbedding(validated.path, embedding, resolvedProject);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const changes = [];
|
|
48
|
+
if (validated.content !== undefined)
|
|
49
|
+
changes.push("content");
|
|
50
|
+
if (validated.tracked_files !== undefined)
|
|
51
|
+
changes.push(`tracked_files (${doc.tracked_files.length} files)`);
|
|
52
|
+
if (validated.tags !== undefined)
|
|
53
|
+
changes.push(`tags (${doc.tags.join(", ") || "none"})`);
|
|
54
|
+
if (validated.related_docs !== undefined)
|
|
55
|
+
changes.push(`related_docs (${doc.related_docs.join(", ") || "none"})`);
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{ type: "text", text: `Documentation entry "${validated.path}" updated.\nChanged: ${changes.join(", ")}` },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
64
|
+
return { isError: true, content: [{ type: "text", text: `Error updating doc: ${message}` }] };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const APP_VERSION = "1.0.23";
|
package/gui/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# React + TypeScript + Vite
|
|
2
|
+
|
|
3
|
+
This app is the browser UI for MCP Documentor. Shared MCP/proxy helpers live in the Node package `@imenam/mcp-gui-interface` (used from the repo root server and `gui-worker`, not from this Vite bundle).
|
|
4
|
+
|
|
5
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
6
|
+
|
|
7
|
+
Currently, two official plugins are available:
|
|
8
|
+
|
|
9
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
|
10
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
11
|
+
|
|
12
|
+
## React Compiler
|
|
13
|
+
|
|
14
|
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
15
|
+
|
|
16
|
+
## Expanding the ESLint configuration
|
|
17
|
+
|
|
18
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
export default defineConfig([
|
|
22
|
+
globalIgnores(['dist']),
|
|
23
|
+
{
|
|
24
|
+
files: ['**/*.{ts,tsx}'],
|
|
25
|
+
extends: [
|
|
26
|
+
// Other configs...
|
|
27
|
+
|
|
28
|
+
// Remove tseslint.configs.recommended and replace with this
|
|
29
|
+
tseslint.configs.recommendedTypeChecked,
|
|
30
|
+
// Alternatively, use this for stricter rules
|
|
31
|
+
tseslint.configs.strictTypeChecked,
|
|
32
|
+
// Optionally, add this for stylistic rules
|
|
33
|
+
tseslint.configs.stylisticTypeChecked,
|
|
34
|
+
|
|
35
|
+
// Other configs...
|
|
36
|
+
],
|
|
37
|
+
languageOptions: {
|
|
38
|
+
parserOptions: {
|
|
39
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
40
|
+
tsconfigRootDir: import.meta.dirname,
|
|
41
|
+
},
|
|
42
|
+
// other options...
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// eslint.config.js
|
|
52
|
+
import reactX from 'eslint-plugin-react-x'
|
|
53
|
+
import reactDom from 'eslint-plugin-react-dom'
|
|
54
|
+
|
|
55
|
+
export default defineConfig([
|
|
56
|
+
globalIgnores(['dist']),
|
|
57
|
+
{
|
|
58
|
+
files: ['**/*.{ts,tsx}'],
|
|
59
|
+
extends: [
|
|
60
|
+
// Other configs...
|
|
61
|
+
// Enable lint rules for React
|
|
62
|
+
reactX.configs['recommended-typescript'],
|
|
63
|
+
// Enable lint rules for React DOM
|
|
64
|
+
reactDom.configs.recommended,
|
|
65
|
+
],
|
|
66
|
+
languageOptions: {
|
|
67
|
+
parserOptions: {
|
|
68
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
69
|
+
tsconfigRootDir: import.meta.dirname,
|
|
70
|
+
},
|
|
71
|
+
// other options...
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
])
|
|
75
|
+
```
|