@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,18 @@
|
|
|
1
|
+
import type { TrackedFile, FreshnessReport } from "../../shared/schemas/doc.schema.js";
|
|
2
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
3
|
+
export declare function computeFileHash(absolutePath: string): string;
|
|
4
|
+
export declare function computeFreshness(trackedFiles: TrackedFile[]): FreshnessReport;
|
|
5
|
+
export declare function formatFreshnessReport(title: string, report: FreshnessReport): string;
|
|
6
|
+
export declare function checkFreshness(storageEngine: StorageEngine, args: unknown): Promise<{
|
|
7
|
+
isError: boolean;
|
|
8
|
+
content: {
|
|
9
|
+
type: string;
|
|
10
|
+
text: string;
|
|
11
|
+
}[];
|
|
12
|
+
} | {
|
|
13
|
+
content: {
|
|
14
|
+
type: string;
|
|
15
|
+
text: string;
|
|
16
|
+
}[];
|
|
17
|
+
isError?: undefined;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { CheckFreshnessArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
5
|
+
export function computeFileHash(absolutePath) {
|
|
6
|
+
return crypto.createHash("sha256").update(fs.readFileSync(absolutePath)).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
export function computeFreshness(trackedFiles) {
|
|
9
|
+
if (!trackedFiles || trackedFiles.length === 0) {
|
|
10
|
+
return { status: "no_tracking", modified: [], missing: [], unchanged: [], total: 0 };
|
|
11
|
+
}
|
|
12
|
+
const modified = [];
|
|
13
|
+
const missing = [];
|
|
14
|
+
const unchanged = [];
|
|
15
|
+
for (const tracked of trackedFiles) {
|
|
16
|
+
const absolutePath = path.resolve(process.cwd(), tracked.path);
|
|
17
|
+
if (!fs.existsSync(absolutePath)) {
|
|
18
|
+
missing.push(tracked.path);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (tracked.contentHash) {
|
|
22
|
+
const currentHash = computeFileHash(absolutePath);
|
|
23
|
+
if (currentHash !== tracked.contentHash) {
|
|
24
|
+
modified.push(tracked.path);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
unchanged.push(tracked.path);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const currentMtime = fs.statSync(absolutePath).mtime.getTime();
|
|
32
|
+
const storedMtime = new Date(tracked.lastModifiedAt).getTime();
|
|
33
|
+
const STALENESS_TOLERANCE_MS = 2000;
|
|
34
|
+
if (currentMtime > storedMtime + STALENESS_TOLERANCE_MS) {
|
|
35
|
+
modified.push(tracked.path);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
unchanged.push(tracked.path);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const isStale = modified.length > 0 || missing.length > 0;
|
|
43
|
+
return {
|
|
44
|
+
status: isStale ? "stale" : "up_to_date",
|
|
45
|
+
modified,
|
|
46
|
+
missing,
|
|
47
|
+
unchanged,
|
|
48
|
+
total: trackedFiles.length,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function formatFreshnessReport(title, report) {
|
|
52
|
+
if (report.status === "no_tracking") {
|
|
53
|
+
return `No tracked files for "${title}". Use tracked_files when creating or updating the doc to enable staleness detection.`;
|
|
54
|
+
}
|
|
55
|
+
let text = report.status === "stale"
|
|
56
|
+
? `STALE: "${title}" — tracked files have changed since the documentation was last updated.\n`
|
|
57
|
+
: `UP TO DATE: "${title}" — no tracked files have been modified.\n`;
|
|
58
|
+
text += `\nSummary: ${report.modified.length} modified, ${report.missing.length} missing, ${report.unchanged.length} unchanged out of ${report.total} tracked file(s).`;
|
|
59
|
+
if (report.modified.length > 0) {
|
|
60
|
+
text += `\n\nModified files:\n${report.modified.map((f) => ` - ${f}`).join("\n")}`;
|
|
61
|
+
}
|
|
62
|
+
if (report.missing.length > 0) {
|
|
63
|
+
text += `\n\nMissing files:\n${report.missing.map((f) => ` - ${f}`).join("\n")}`;
|
|
64
|
+
}
|
|
65
|
+
return text;
|
|
66
|
+
}
|
|
67
|
+
export async function checkFreshness(storageEngine, args) {
|
|
68
|
+
try {
|
|
69
|
+
const validated = CheckFreshnessArgsSchema.parse(args);
|
|
70
|
+
if (validated.path) {
|
|
71
|
+
const resolved = await storageEngine.resolveDoc(validated.path);
|
|
72
|
+
if (!resolved) {
|
|
73
|
+
return { isError: true, content: [{ type: "text", text: `Documentation entry "${validated.path}" not found in any project.` }] };
|
|
74
|
+
}
|
|
75
|
+
const { doc } = resolved;
|
|
76
|
+
const report = computeFreshness(doc.tracked_files);
|
|
77
|
+
return { content: [{ type: "text", text: formatFreshnessReport(doc.title, report) }] };
|
|
78
|
+
}
|
|
79
|
+
if (validated.project) {
|
|
80
|
+
const summaries = await storageEngine.listDocs(validated.project);
|
|
81
|
+
if (summaries.length === 0) {
|
|
82
|
+
return { content: [{ type: "text", text: `No documentation entries found in project "${validated.project}".` }] };
|
|
83
|
+
}
|
|
84
|
+
const stale = [];
|
|
85
|
+
const noTracking = [];
|
|
86
|
+
let upToDate = 0;
|
|
87
|
+
for (const summary of summaries) {
|
|
88
|
+
const doc = await storageEngine.getDoc(summary.id, validated.project);
|
|
89
|
+
if (!doc)
|
|
90
|
+
continue;
|
|
91
|
+
const report = computeFreshness(doc.tracked_files);
|
|
92
|
+
if (report.status === "no_tracking") {
|
|
93
|
+
noTracking.push(doc.title);
|
|
94
|
+
}
|
|
95
|
+
else if (report.status === "stale") {
|
|
96
|
+
const details = [
|
|
97
|
+
...report.modified.map((f) => ` • modified: ${f}`),
|
|
98
|
+
...report.missing.map((f) => ` • missing: ${f}`),
|
|
99
|
+
].join("\n");
|
|
100
|
+
stale.push(` - **${doc.title}** [${doc.id}]\n${details}`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
upToDate++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
let text = `# Freshness report — project "${validated.project}"\n\n`;
|
|
107
|
+
text += `${summaries.length} doc(s) scanned: **${stale.length} stale**, ${upToDate} up to date, ${noTracking.length} untracked.\n`;
|
|
108
|
+
if (stale.length > 0) {
|
|
109
|
+
text += `\n## Stale docs\n\n${stale.join("\n\n")}`;
|
|
110
|
+
}
|
|
111
|
+
if (noTracking.length > 0) {
|
|
112
|
+
text += `\n\n## Untracked docs (no staleness detection)\n${noTracking.map((t) => ` - ${t}`).join("\n")}`;
|
|
113
|
+
}
|
|
114
|
+
return { content: [{ type: "text", text }] };
|
|
115
|
+
}
|
|
116
|
+
// No id, no project → scan all projects
|
|
117
|
+
const config = storageEngine.getConfig();
|
|
118
|
+
const allProjects = config.projects;
|
|
119
|
+
const projectBlocks = [];
|
|
120
|
+
let totalDocs = 0;
|
|
121
|
+
let totalStale = 0;
|
|
122
|
+
for (const proj of allProjects) {
|
|
123
|
+
const summaries = await storageEngine.listDocs(proj);
|
|
124
|
+
const stale = [];
|
|
125
|
+
const noTracking = [];
|
|
126
|
+
let upToDate = 0;
|
|
127
|
+
for (const summary of summaries) {
|
|
128
|
+
const doc = await storageEngine.getDoc(summary.id, proj);
|
|
129
|
+
if (!doc)
|
|
130
|
+
continue;
|
|
131
|
+
const report = computeFreshness(doc.tracked_files);
|
|
132
|
+
if (report.status === "no_tracking") {
|
|
133
|
+
noTracking.push(doc.title);
|
|
134
|
+
}
|
|
135
|
+
else if (report.status === "stale") {
|
|
136
|
+
const details = [
|
|
137
|
+
...report.modified.map((f) => ` • modified: ${f}`),
|
|
138
|
+
...report.missing.map((f) => ` • missing: ${f}`),
|
|
139
|
+
].join("\n");
|
|
140
|
+
stale.push(` - **${doc.title}** [${doc.id}]\n${details}`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
upToDate++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
totalDocs += summaries.length;
|
|
147
|
+
totalStale += stale.length;
|
|
148
|
+
let block = `## ${proj}\n\n`;
|
|
149
|
+
block += `${summaries.length} doc(s): **${stale.length} stale**, ${upToDate} up to date, ${noTracking.length} untracked.`;
|
|
150
|
+
if (stale.length > 0) {
|
|
151
|
+
block += `\n\n### Stale docs\n\n${stale.join("\n\n")}`;
|
|
152
|
+
}
|
|
153
|
+
if (noTracking.length > 0) {
|
|
154
|
+
block += `\n\n### Untracked docs\n${noTracking.map((t) => ` - ${t}`).join("\n")}`;
|
|
155
|
+
}
|
|
156
|
+
projectBlocks.push(block);
|
|
157
|
+
}
|
|
158
|
+
let text = `# Freshness report — all projects (${allProjects.length})\n\n`;
|
|
159
|
+
text += `Total: ${totalDocs} doc(s) across all projects, **${totalStale} stale**.\n\n`;
|
|
160
|
+
text += projectBlocks.join("\n\n---\n\n");
|
|
161
|
+
return { content: [{ type: "text", text }] };
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
165
|
+
return { isError: true, content: [{ type: "text", text: `Error checking freshness: ${message}` }] };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
export declare function checkLinks(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,99 @@
|
|
|
1
|
+
import GithubSlugger from "github-slugger";
|
|
2
|
+
import { CheckLinksArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
3
|
+
function getValidAnchors(content) {
|
|
4
|
+
const slugger = new GithubSlugger();
|
|
5
|
+
const headingRegex = /^#{1,6}\s+(.+)$/gm;
|
|
6
|
+
const anchors = new Set();
|
|
7
|
+
let match;
|
|
8
|
+
while ((match = headingRegex.exec(content)) !== null) {
|
|
9
|
+
anchors.add(slugger.slug(match[1].trim()));
|
|
10
|
+
}
|
|
11
|
+
return anchors;
|
|
12
|
+
}
|
|
13
|
+
function extractAnchorLinks(content) {
|
|
14
|
+
const linkRegex = /\[[^\]]*\]\(#([^)]+)\)/g;
|
|
15
|
+
const anchors = [];
|
|
16
|
+
let match;
|
|
17
|
+
while ((match = linkRegex.exec(content)) !== null) {
|
|
18
|
+
anchors.push(match[1]);
|
|
19
|
+
}
|
|
20
|
+
return anchors;
|
|
21
|
+
}
|
|
22
|
+
function extractDocLinks(content) {
|
|
23
|
+
const linkRegex = /\[[^\]]*\]\(doc:\/\/([^)]+)\)/g;
|
|
24
|
+
const ids = [];
|
|
25
|
+
let match;
|
|
26
|
+
while ((match = linkRegex.exec(content)) !== null) {
|
|
27
|
+
ids.push(match[1]);
|
|
28
|
+
}
|
|
29
|
+
return ids;
|
|
30
|
+
}
|
|
31
|
+
export async function checkLinks(storageEngine, args) {
|
|
32
|
+
try {
|
|
33
|
+
const validated = CheckLinksArgsSchema.parse(args);
|
|
34
|
+
const config = storageEngine.getConfig();
|
|
35
|
+
const project = validated.project ?? config.currentProject;
|
|
36
|
+
const docs = await storageEngine.listDocs(project);
|
|
37
|
+
const validIds = new Set(docs.map((d) => d.id));
|
|
38
|
+
const brokenRefs = [];
|
|
39
|
+
const brokenDocLinks = [];
|
|
40
|
+
const brokenAnchors = [];
|
|
41
|
+
for (const summary of docs) {
|
|
42
|
+
const doc = await storageEngine.getDoc(summary.id, project);
|
|
43
|
+
if (!doc)
|
|
44
|
+
continue;
|
|
45
|
+
for (const ref of doc.related_docs ?? []) {
|
|
46
|
+
if (!validIds.has(ref)) {
|
|
47
|
+
brokenRefs.push({ sourceId: summary.id, brokenId: ref });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const targetId of extractDocLinks(doc.content)) {
|
|
51
|
+
if (!validIds.has(targetId)) {
|
|
52
|
+
brokenDocLinks.push({ sourceId: summary.id, targetId });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const anchorLinks = extractAnchorLinks(doc.content);
|
|
56
|
+
if (anchorLinks.length > 0) {
|
|
57
|
+
const validAnchors = getValidAnchors(doc.content);
|
|
58
|
+
for (const anchor of anchorLinks) {
|
|
59
|
+
if (!validAnchors.has(anchor)) {
|
|
60
|
+
brokenAnchors.push({ sourceId: summary.id, anchor });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const totalBroken = brokenRefs.length + brokenDocLinks.length + brokenAnchors.length;
|
|
66
|
+
if (totalBroken === 0) {
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: `✓ No broken links found in project "${project}" (${docs.length} docs checked).`,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const lines = [];
|
|
77
|
+
for (const b of brokenRefs) {
|
|
78
|
+
lines.push(` [related_doc] ${b.sourceId} → ${b.brokenId} (doc not found)`);
|
|
79
|
+
}
|
|
80
|
+
for (const b of brokenDocLinks) {
|
|
81
|
+
lines.push(` [doc://] ${b.sourceId} → doc://${b.targetId} (doc not found)`);
|
|
82
|
+
}
|
|
83
|
+
for (const b of brokenAnchors) {
|
|
84
|
+
lines.push(` [anchor] ${b.sourceId} → #${b.anchor} (heading not found)`);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: "text",
|
|
90
|
+
text: `Found ${totalBroken} broken link(s) in project "${project}" (${docs.length} docs checked):\n${lines.join("\n")}`,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
+
return { isError: true, content: [{ type: "text", text: `Error checking links: ${message}` }] };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -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 createDoc(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,78 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { CreateDocArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
4
|
+
import { computeFileHash } from "./check-freshness.js";
|
|
5
|
+
function slugify(title) {
|
|
6
|
+
return title
|
|
7
|
+
.normalize("NFD")
|
|
8
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
11
|
+
.replace(/^-|-$/g, "");
|
|
12
|
+
}
|
|
13
|
+
function snapshotTrackedFiles(filePaths) {
|
|
14
|
+
const tracked = [];
|
|
15
|
+
for (const fp of filePaths) {
|
|
16
|
+
const absolutePath = path.resolve(process.cwd(), fp);
|
|
17
|
+
let mtime = new Date().toISOString();
|
|
18
|
+
let contentHash;
|
|
19
|
+
if (fs.existsSync(absolutePath)) {
|
|
20
|
+
mtime = fs.statSync(absolutePath).mtime.toISOString();
|
|
21
|
+
contentHash = computeFileHash(absolutePath);
|
|
22
|
+
}
|
|
23
|
+
tracked.push({ path: absolutePath, lastModifiedAt: mtime, ...(contentHash !== undefined && { contentHash }) });
|
|
24
|
+
}
|
|
25
|
+
return tracked;
|
|
26
|
+
}
|
|
27
|
+
export async function createDoc(storageEngine, embeddingEngine, args) {
|
|
28
|
+
try {
|
|
29
|
+
const validated = CreateDocArgsSchema.parse(args);
|
|
30
|
+
const { project } = (args ?? {});
|
|
31
|
+
const slug = slugify(validated.title);
|
|
32
|
+
const id = validated.folder ? `${validated.folder}/${slug}` : slug;
|
|
33
|
+
const now = new Date().toISOString();
|
|
34
|
+
const existing = await storageEngine.getDoc(id, project);
|
|
35
|
+
if (existing) {
|
|
36
|
+
return {
|
|
37
|
+
isError: true,
|
|
38
|
+
content: [{ type: "text", text: `A documentation entry with path "${id}" already exists. Use update_doc to modify it.` }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (project) {
|
|
42
|
+
storageEngine.ensureProject(project);
|
|
43
|
+
}
|
|
44
|
+
const trackedFiles = validated.tracked_files ? snapshotTrackedFiles(validated.tracked_files) : [];
|
|
45
|
+
const folder = validated.folder;
|
|
46
|
+
const doc = {
|
|
47
|
+
id,
|
|
48
|
+
title: validated.title,
|
|
49
|
+
content: validated.content,
|
|
50
|
+
tracked_files: trackedFiles,
|
|
51
|
+
tags: validated.tags ?? [],
|
|
52
|
+
folder,
|
|
53
|
+
related_docs: validated.related_docs ?? [],
|
|
54
|
+
createdAt: now,
|
|
55
|
+
updatedAt: now,
|
|
56
|
+
};
|
|
57
|
+
const resolvedProject = project ?? storageEngine.getConfig().currentProject;
|
|
58
|
+
await storageEngine.saveDoc(doc, resolvedProject);
|
|
59
|
+
if (embeddingEngine.isConfigured()) {
|
|
60
|
+
const embedding = await embeddingEngine.generateEmbedding(validated.content);
|
|
61
|
+
if (embedding) {
|
|
62
|
+
await embeddingEngine.saveEmbedding(id, embedding, resolvedProject);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: `Documentation entry "${validated.title}" created successfully with path "${id}".\nFolder: ${validated.folder || "none"}\nTracked files: ${trackedFiles.length}\nTags: ${(validated.tags ?? []).join(", ") || "none"}`,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
76
|
+
return { isError: true, content: [{ type: "text", text: `Error creating doc: ${message}` }] };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -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 deleteDoc(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,18 @@
|
|
|
1
|
+
export async function deleteDoc(storageEngine, embeddingEngine, args) {
|
|
2
|
+
try {
|
|
3
|
+
const { path: docPath } = args;
|
|
4
|
+
const resolved = await storageEngine.resolveDoc(docPath);
|
|
5
|
+
if (!resolved) {
|
|
6
|
+
return { isError: true, content: [{ type: "text", text: `Documentation entry "${docPath}" not found in any project.` }] };
|
|
7
|
+
}
|
|
8
|
+
await storageEngine.deleteDoc(resolved.doc.id, resolved.project);
|
|
9
|
+
await embeddingEngine.removeEmbedding(resolved.doc.id, resolved.project);
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: "text", text: `Documentation entry "${docPath}" deleted from project "${resolved.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 deleting doc: ${message}` }] };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
export declare function getDocMetadata(storageEngine: StorageEngine, args: unknown): Promise<{
|
|
3
|
+
isError: boolean;
|
|
4
|
+
content: {
|
|
5
|
+
type: string;
|
|
6
|
+
text: string;
|
|
7
|
+
}[];
|
|
8
|
+
} | {
|
|
9
|
+
content: {
|
|
10
|
+
type: string;
|
|
11
|
+
text: string;
|
|
12
|
+
}[];
|
|
13
|
+
isError?: undefined;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { GetDocMetadataArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
2
|
+
import { computeFreshness } from "./check-freshness.js";
|
|
3
|
+
export async function getDocMetadata(storageEngine, args) {
|
|
4
|
+
try {
|
|
5
|
+
const validated = GetDocMetadataArgsSchema.parse(args);
|
|
6
|
+
const resolved = await storageEngine.resolveDoc(validated.path);
|
|
7
|
+
if (!resolved) {
|
|
8
|
+
return { isError: true, content: [{ type: "text", text: `Documentation entry "${validated.path}" not found in any project.` }] };
|
|
9
|
+
}
|
|
10
|
+
const { doc } = resolved;
|
|
11
|
+
const report = computeFreshness(doc.tracked_files);
|
|
12
|
+
const fileStatusIcon = (filePath) => {
|
|
13
|
+
if (report.modified.includes(filePath))
|
|
14
|
+
return "✗ modified";
|
|
15
|
+
if (report.missing.includes(filePath))
|
|
16
|
+
return "✗ missing";
|
|
17
|
+
return "✓ up to date";
|
|
18
|
+
};
|
|
19
|
+
let text = `**Path:** ${doc.id}\n`;
|
|
20
|
+
text += `**Title:** ${doc.title}\n`;
|
|
21
|
+
text += `**Tags:** ${doc.tags.length > 0 ? doc.tags.join(", ") : "none"}\n`;
|
|
22
|
+
text += `**Created:** ${doc.createdAt}\n`;
|
|
23
|
+
text += `**Updated:** ${doc.updatedAt}\n`;
|
|
24
|
+
text += `**Freshness:** ${report.status}\n`;
|
|
25
|
+
if (doc.tracked_files.length > 0) {
|
|
26
|
+
text += `\nTracked files (${doc.tracked_files.length} total):\n`;
|
|
27
|
+
text += doc.tracked_files
|
|
28
|
+
.map((f) => ` ${fileStatusIcon(f.path)} — ${f.path}\n Snapshot: ${f.lastModifiedAt}`)
|
|
29
|
+
.join("\n");
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
text += "\nNo tracked files.";
|
|
33
|
+
}
|
|
34
|
+
return { content: [{ type: "text", text }] };
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
+
return { isError: true, content: [{ type: "text", text: `Error getting metadata: ${message}` }] };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
export declare function getDocTree(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,64 @@
|
|
|
1
|
+
function getOrCreateFolder(nodes, name, fullPath) {
|
|
2
|
+
const existing = nodes.find((n) => n.type === "folder" && n.name === name);
|
|
3
|
+
if (existing)
|
|
4
|
+
return existing;
|
|
5
|
+
const newFolder = { type: "folder", name, path: fullPath, children: [] };
|
|
6
|
+
nodes.push(newFolder);
|
|
7
|
+
return newFolder;
|
|
8
|
+
}
|
|
9
|
+
function insertDoc(roots, folderPath, title, id) {
|
|
10
|
+
if (!folderPath) {
|
|
11
|
+
roots.push({ type: "doc", title, id });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const segments = folderPath.split("/");
|
|
15
|
+
let currentLevel = roots;
|
|
16
|
+
for (let i = 0; i < segments.length; i++) {
|
|
17
|
+
const segment = segments[i];
|
|
18
|
+
const fullPath = segments.slice(0, i + 1).join("/");
|
|
19
|
+
const folder = getOrCreateFolder(currentLevel, segment, fullPath);
|
|
20
|
+
currentLevel = folder.children;
|
|
21
|
+
}
|
|
22
|
+
currentLevel.push({ type: "doc", title, id });
|
|
23
|
+
}
|
|
24
|
+
function sortNodes(nodes) {
|
|
25
|
+
return [...nodes].sort((a, b) => {
|
|
26
|
+
if (a.type !== b.type)
|
|
27
|
+
return a.type === "folder" ? -1 : 1;
|
|
28
|
+
const nameA = a.type === "folder" ? a.name : a.title;
|
|
29
|
+
const nameB = b.type === "folder" ? b.name : b.title;
|
|
30
|
+
return nameA.localeCompare(nameB);
|
|
31
|
+
}).map((node) => node.type === "folder"
|
|
32
|
+
? { ...node, children: sortNodes(node.children) }
|
|
33
|
+
: node);
|
|
34
|
+
}
|
|
35
|
+
function buildTree(summaries) {
|
|
36
|
+
const roots = [];
|
|
37
|
+
for (const s of summaries) {
|
|
38
|
+
insertDoc(roots, s.folder, s.title, s.id);
|
|
39
|
+
}
|
|
40
|
+
return sortNodes(roots);
|
|
41
|
+
}
|
|
42
|
+
export async function getDocTree(storageEngine, args) {
|
|
43
|
+
try {
|
|
44
|
+
const { project } = (args ?? {});
|
|
45
|
+
const config = storageEngine.getConfig();
|
|
46
|
+
const projectsToList = project ? [project] : config.projects;
|
|
47
|
+
const result = [];
|
|
48
|
+
for (const proj of projectsToList) {
|
|
49
|
+
const summaries = await storageEngine.listDocs(proj);
|
|
50
|
+
result.push({
|
|
51
|
+
project: proj,
|
|
52
|
+
doc_count: summaries.length,
|
|
53
|
+
tree: buildTree(summaries),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
+
return { isError: true, content: [{ type: "text", text: `Error building doc tree: ${message}` }] };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
export declare function getDoc(storageEngine: StorageEngine, args: unknown): Promise<{
|
|
3
|
+
isError: boolean;
|
|
4
|
+
content: {
|
|
5
|
+
type: string;
|
|
6
|
+
text: string;
|
|
7
|
+
}[];
|
|
8
|
+
} | {
|
|
9
|
+
content: {
|
|
10
|
+
type: string;
|
|
11
|
+
text: string;
|
|
12
|
+
}[];
|
|
13
|
+
isError?: undefined;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { GetDocArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
2
|
+
import { computeFreshness } from "./check-freshness.js";
|
|
3
|
+
export async function getDoc(storageEngine, args) {
|
|
4
|
+
try {
|
|
5
|
+
const validated = GetDocArgsSchema.parse(args);
|
|
6
|
+
const resolved = await storageEngine.resolveDoc(validated.path);
|
|
7
|
+
if (!resolved) {
|
|
8
|
+
return { isError: true, content: [{ type: "text", text: `Documentation entry "${validated.path}" not found in any project.` }] };
|
|
9
|
+
}
|
|
10
|
+
const { doc, project: resolvedProject } = resolved;
|
|
11
|
+
const trackedFilesText = doc.tracked_files.length > 0
|
|
12
|
+
? doc.tracked_files.map((f) => ` - ${f.path} (snapshot: ${f.lastModifiedAt})`).join("\n")
|
|
13
|
+
: " (none)";
|
|
14
|
+
const freshnessReport = computeFreshness(doc.tracked_files);
|
|
15
|
+
const stalenessBanner = freshnessReport.status === "stale"
|
|
16
|
+
? [
|
|
17
|
+
`> [!WARNING]`,
|
|
18
|
+
`> **Cette documentation est potentiellement obsolète.**`,
|
|
19
|
+
`> Les fichiers suivants ont été modifiés depuis la dernière mise à jour de ce document :`,
|
|
20
|
+
...[
|
|
21
|
+
...freshnessReport.modified.map((f) => `> - \`${f}\` *(modifié)*`),
|
|
22
|
+
...freshnessReport.missing.map((f) => `> - \`${f}\` *(introuvable)*`),
|
|
23
|
+
],
|
|
24
|
+
`>`,
|
|
25
|
+
`> Vérifiez si le contenu ci-dessous est toujours valide. Si nécessaire, utilisez \`git_diff_doc\` pour voir les changements, puis mettez à jour la documentation avec \`patch_doc\` ou \`update_doc\`.`,
|
|
26
|
+
``,
|
|
27
|
+
].join("\n")
|
|
28
|
+
: "";
|
|
29
|
+
const text = [
|
|
30
|
+
stalenessBanner,
|
|
31
|
+
`# ${doc.title}`,
|
|
32
|
+
``,
|
|
33
|
+
`**ID:** ${doc.id}`,
|
|
34
|
+
`**Project:** ${resolvedProject}`,
|
|
35
|
+
`**Tags:** ${doc.tags.join(", ") || "none"}`,
|
|
36
|
+
`**Created:** ${doc.createdAt}`,
|
|
37
|
+
`**Updated:** ${doc.updatedAt}`,
|
|
38
|
+
``,
|
|
39
|
+
`## Tracked Files`,
|
|
40
|
+
trackedFilesText,
|
|
41
|
+
``,
|
|
42
|
+
`## Content`,
|
|
43
|
+
``,
|
|
44
|
+
doc.content,
|
|
45
|
+
``,
|
|
46
|
+
`---`,
|
|
47
|
+
`> **Agent instruction:** To modify this document, ALWAYS prefer \`patch_doc\` (targeted search-and-replace) over \`update_doc\` (full rewrite). Only use \`update_doc\` when the changes are so extensive that a patch is impractical. Using \`patch_doc\` preserves document integrity and avoids accidental data loss.`,
|
|
48
|
+
].join("\n");
|
|
49
|
+
return { content: [{ type: "text", text }] };
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
+
return { isError: true, content: [{ type: "text", text: `Error getting doc: ${message}` }] };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
export declare function gitDiffDoc(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
|
+
}>;
|