@imenam/mcp-documentor 1.0.86 → 1.0.87
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 +1 -1
- package/dist/shared/schemas/doc.schema.d.ts +2 -1
- package/dist/shared/schemas/doc.schema.js +12 -1
- package/dist/src/core/embedding-engine.d.ts +1 -1
- package/dist/src/core/embedding-engine.js +3 -3
- package/dist/src/core/embedding-store.d.ts +1 -1
- package/dist/src/core/embedding-store.js +4 -4
- package/dist/src/core/storage-engine.d.ts +12 -2
- package/dist/src/core/storage-engine.js +35 -15
- package/dist/src/index.js +5 -4
- package/dist/src/tools/move-doc.js +20 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -120,7 +120,7 @@ that work on a whole project (`list_docs`, `get_doc_tree`, `check_freshness` wit
|
|
|
120
120
|
| `get_doc_tree` | Return folder/doc structure for a project (or all projects) |
|
|
121
121
|
| `update_doc` | Fully replace content and/or metadata (`project` required; prefer `patch_doc` for small edits) |
|
|
122
122
|
| `patch_doc` | Search-and-replace a fragment of a doc’s content (`project` required) |
|
|
123
|
-
| `move_doc` | Change a doc’s path (
|
|
123
|
+
| `move_doc` | Change a doc’s path (`new_path`) and/or project (`new_project`); links are rewritten across all projects (`project` required) |
|
|
124
124
|
| `delete_doc` | Permanently delete a documentation entry (`project` required) |
|
|
125
125
|
| `search_docs` | Semantic search over docs (natural language; requires OpenAI) |
|
|
126
126
|
| `check_freshness` | Staleness for one doc (`project` required) or a full project scan |
|
|
@@ -156,8 +156,9 @@ export declare const DeleteDocArgsSchema: z.ZodObject<{
|
|
|
156
156
|
}, z.core.$strip>;
|
|
157
157
|
export declare const MoveDocArgsSchema: z.ZodObject<{
|
|
158
158
|
path: z.ZodString;
|
|
159
|
-
new_path: z.ZodString
|
|
159
|
+
new_path: z.ZodOptional<z.ZodString>;
|
|
160
160
|
project: z.ZodString;
|
|
161
|
+
new_project: z.ZodOptional<z.ZodString>;
|
|
161
162
|
}, z.core.$strip>;
|
|
162
163
|
export declare const CheckFreshnessArgsSchema: z.ZodObject<{
|
|
163
164
|
path: z.ZodOptional<z.ZodString>;
|
|
@@ -123,8 +123,19 @@ export const DeleteDocArgsSchema = z.object({
|
|
|
123
123
|
});
|
|
124
124
|
export const MoveDocArgsSchema = z.object({
|
|
125
125
|
path: z.string().describe("Current path of the documentation entry, e.g. 'backend/auth/authentication'"),
|
|
126
|
-
new_path: z
|
|
126
|
+
new_path: z
|
|
127
|
+
.string()
|
|
128
|
+
.min(1)
|
|
129
|
+
.optional()
|
|
130
|
+
.describe("New path for the documentation entry, e.g. 'backend/v2/authentication'. Changes folder and/or name. Omitted: the entry keeps its current path, which requires new_project."),
|
|
127
131
|
project: requiredProject("the move"),
|
|
132
|
+
new_project: z
|
|
133
|
+
.string()
|
|
134
|
+
.min(1)
|
|
135
|
+
.optional()
|
|
136
|
+
.describe("Project the entry is moved to. Omitted: the entry stays in `project`."),
|
|
137
|
+
}).refine((args) => args.new_path !== undefined || args.new_project !== undefined, {
|
|
138
|
+
message: "Provide new_path, new_project, or both.",
|
|
128
139
|
});
|
|
129
140
|
export const CheckFreshnessArgsSchema = z
|
|
130
141
|
.object({
|
|
@@ -11,7 +11,7 @@ export declare class EmbeddingEngine {
|
|
|
11
11
|
cosineSimilarity(a: number[], b: number[]): number;
|
|
12
12
|
saveEmbedding(docId: string, embedding: number[], project: string): Promise<void>;
|
|
13
13
|
removeEmbedding(docId: string, project: string): Promise<void>;
|
|
14
|
-
moveEmbedding(oldDocId: string, newDocId: string,
|
|
14
|
+
moveEmbedding(oldDocId: string, project: string, newDocId: string, newProject: string): Promise<void>;
|
|
15
15
|
search(query: string, project: string, topK: number, getDocContent: (id: string) => Promise<{
|
|
16
16
|
title: string;
|
|
17
17
|
content: string;
|
|
@@ -66,9 +66,9 @@ export class EmbeddingEngine {
|
|
|
66
66
|
this.store.remove(project, docId);
|
|
67
67
|
logger.debug(`Embedding removed for doc "${docId}" in project "${project}"`);
|
|
68
68
|
}
|
|
69
|
-
async moveEmbedding(oldDocId, newDocId,
|
|
70
|
-
this.store.move(project, oldDocId, newDocId);
|
|
71
|
-
logger.debug(`Embedding moved: "${oldDocId}" → "${
|
|
69
|
+
async moveEmbedding(oldDocId, project, newDocId, newProject) {
|
|
70
|
+
this.store.move(project, oldDocId, newProject, newDocId);
|
|
71
|
+
logger.debug(`Embedding moved: "${project}:${oldDocId}" → "${newProject}:${newDocId}"`);
|
|
72
72
|
}
|
|
73
73
|
async search(query, project, topK, getDocContent) {
|
|
74
74
|
return this.searchAcrossProjects(query, [project], topK, async (_proj, docId) => getDocContent(docId));
|
|
@@ -5,7 +5,7 @@ export declare class EmbeddingStore {
|
|
|
5
5
|
private getDb;
|
|
6
6
|
save(project: string, docId: string, embedding: number[]): void;
|
|
7
7
|
remove(project: string, docId: string): void;
|
|
8
|
-
move(project: string, oldDocId: string, newDocId: string): void;
|
|
8
|
+
move(project: string, oldDocId: string, newProject: string, newDocId: string): void;
|
|
9
9
|
loadAll(project: string): Map<string, number[]>;
|
|
10
10
|
loadAllProjects(projects: string[]): Array<{
|
|
11
11
|
project: string;
|
|
@@ -65,11 +65,11 @@ export class EmbeddingStore {
|
|
|
65
65
|
.run(project, docId);
|
|
66
66
|
logger.debug(`Embedding removed: [${project}] ${docId}`);
|
|
67
67
|
}
|
|
68
|
-
move(project, oldDocId, newDocId) {
|
|
68
|
+
move(project, oldDocId, newProject, newDocId) {
|
|
69
69
|
this.getDb()
|
|
70
|
-
.prepare("UPDATE embeddings SET doc_id = ? WHERE project = ? AND doc_id = ?")
|
|
71
|
-
.run(newDocId, project, oldDocId);
|
|
72
|
-
logger.debug(`Embedding moved: [${project}] ${oldDocId} → ${newDocId}`);
|
|
70
|
+
.prepare("UPDATE embeddings SET project = ?, doc_id = ? WHERE project = ? AND doc_id = ?")
|
|
71
|
+
.run(newProject, newDocId, project, oldDocId);
|
|
72
|
+
logger.debug(`Embedding moved: [${project}] ${oldDocId} → [${newProject}] ${newDocId}`);
|
|
73
73
|
}
|
|
74
74
|
loadAll(project) {
|
|
75
75
|
const rows = this.getDb()
|
|
@@ -58,9 +58,19 @@ export declare class StorageEngine {
|
|
|
58
58
|
*/
|
|
59
59
|
listDocsDetailed(project: string): Promise<DocListing[]>;
|
|
60
60
|
deleteDoc(id: string, project: string): Promise<boolean>;
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Moves a doc to `newId` in `newProject`. Across projects, the moved doc's own
|
|
63
|
+
* same-project references (`doc://id`, bare related_docs) are qualified with the
|
|
64
|
+
* source project so they keep pointing at the same entries; the history records
|
|
65
|
+
* a delete in the source project and a create in the target.
|
|
66
|
+
*/
|
|
67
|
+
moveDoc(oldId: string, project: string, newId: string, newProject: string): Promise<boolean>;
|
|
62
68
|
getBundles(): ExportBundle[];
|
|
63
69
|
saveBundles(bundles: ExportBundle[]): void;
|
|
64
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Rewrites every reference to `project:oldId` so it points at `newProject:newId`.
|
|
72
|
+
* In the source project, bare references become qualified when the doc left it.
|
|
73
|
+
*/
|
|
74
|
+
rewriteLinks(oldId: string, project: string, newId: string, newProject: string): Promise<string[]>;
|
|
65
75
|
}
|
|
66
76
|
export {};
|
|
@@ -227,24 +227,37 @@ export class StorageEngine {
|
|
|
227
227
|
logger.info(`Doc deleted: ${filePath}`);
|
|
228
228
|
return true;
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Moves a doc to `newId` in `newProject`. Across projects, the moved doc's own
|
|
232
|
+
* same-project references (`doc://id`, bare related_docs) are qualified with the
|
|
233
|
+
* source project so they keep pointing at the same entries; the history records
|
|
234
|
+
* a delete in the source project and a create in the target.
|
|
235
|
+
*/
|
|
236
|
+
async moveDoc(oldId, project, newId, newProject) {
|
|
231
237
|
const projectDir = this.getProjectDir(project);
|
|
232
238
|
const oldFilePath = this.docIdToFilePath(oldId, projectDir);
|
|
233
239
|
if (!fs.existsSync(oldFilePath))
|
|
234
240
|
return false;
|
|
235
|
-
const
|
|
241
|
+
const newProjectDir = this.getProjectDir(newProject);
|
|
242
|
+
const newFilePath = this.docIdToFilePath(newId, newProjectDir);
|
|
236
243
|
if (fs.existsSync(newFilePath)) {
|
|
237
|
-
throw new Error(`A document already exists at path "${newId}".`);
|
|
244
|
+
throw new Error(`A document already exists at path "${newId}" in project "${newProject}".`);
|
|
238
245
|
}
|
|
239
246
|
const doc = await this.getDoc(oldId, project);
|
|
240
247
|
if (!doc)
|
|
241
248
|
return false;
|
|
249
|
+
const crossProject = newProject !== project;
|
|
250
|
+
if (crossProject) {
|
|
251
|
+
this.historyStore?.record(project, doc, "delete");
|
|
252
|
+
doc.content = doc.content.replace(/doc:\/\/([^):#\s]+)(?=[)#\s]|$)/g, `doc://${project}:$1`);
|
|
253
|
+
doc.related_docs = doc.related_docs.map((ref) => (ref.includes(":") ? ref : `${project}:${ref}`));
|
|
254
|
+
}
|
|
242
255
|
const newParts = newId.split("/");
|
|
243
256
|
doc.id = newId;
|
|
244
257
|
doc.folder = newParts.length > 1 ? newParts.slice(0, -1).join("/") : undefined;
|
|
245
258
|
doc.updatedAt = new Date().toISOString();
|
|
246
259
|
fs.mkdirSync(path.dirname(newFilePath), { recursive: true });
|
|
247
|
-
await this.saveDoc(doc,
|
|
260
|
+
await this.saveDoc(doc, newProject, crossProject ? "create" : "update");
|
|
248
261
|
fs.unlinkSync(oldFilePath);
|
|
249
262
|
this.cleanEmptyDirs(path.dirname(oldFilePath), projectDir);
|
|
250
263
|
logger.info(`Doc moved: ${oldFilePath} → ${newFilePath}`);
|
|
@@ -265,22 +278,29 @@ export class StorageEngine {
|
|
|
265
278
|
const p = path.join(this.getDataDir(), "export-bundles.json");
|
|
266
279
|
fs.writeFileSync(p, JSON.stringify(bundles, null, 2), "utf-8");
|
|
267
280
|
}
|
|
268
|
-
|
|
281
|
+
/**
|
|
282
|
+
* Rewrites every reference to `project:oldId` so it points at `newProject:newId`.
|
|
283
|
+
* In the source project, bare references become qualified when the doc left it.
|
|
284
|
+
*/
|
|
285
|
+
async rewriteLinks(oldId, project, newId, newProject) {
|
|
269
286
|
const config = this.getConfig();
|
|
270
287
|
const proj = project;
|
|
288
|
+
const crossProject = newProject !== proj;
|
|
289
|
+
const qualifiedTarget = `${newProject}:${newId}`;
|
|
290
|
+
const bareTarget = crossProject ? qualifiedTarget : newId;
|
|
291
|
+
const boundary = "(?=[)#\\s]|$)";
|
|
271
292
|
const updated = [];
|
|
272
|
-
// Within the
|
|
293
|
+
// Within the source project: rewrite bare refs and explicit self-cross-project refs
|
|
273
294
|
const sameProjDocs = await this.listDocs(proj);
|
|
274
295
|
for (const summary of sameProjDocs) {
|
|
275
|
-
if (summary.id === newId)
|
|
296
|
+
if (!crossProject && summary.id === newId)
|
|
276
297
|
continue;
|
|
277
298
|
const doc = await this.getDoc(summary.id, proj);
|
|
278
299
|
if (!doc)
|
|
279
300
|
continue;
|
|
280
301
|
let changed = false;
|
|
281
|
-
let newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(oldId)}`, "g"), `doc://${
|
|
282
|
-
|
|
283
|
-
newContent = newContent.replace(new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}`, "g"), `doc://${proj}:${newId}`);
|
|
302
|
+
let newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(oldId)}${boundary}`, "g"), `doc://${bareTarget}`);
|
|
303
|
+
newContent = newContent.replace(new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}${boundary}`, "g"), `doc://${qualifiedTarget}`);
|
|
284
304
|
if (newContent !== doc.content) {
|
|
285
305
|
doc.content = newContent;
|
|
286
306
|
changed = true;
|
|
@@ -288,11 +308,11 @@ export class StorageEngine {
|
|
|
288
308
|
const newRelated = doc.related_docs.map((ref) => {
|
|
289
309
|
if (ref === oldId) {
|
|
290
310
|
changed = true;
|
|
291
|
-
return
|
|
311
|
+
return bareTarget;
|
|
292
312
|
}
|
|
293
313
|
if (ref === `${proj}:${oldId}`) {
|
|
294
314
|
changed = true;
|
|
295
|
-
return
|
|
315
|
+
return qualifiedTarget;
|
|
296
316
|
}
|
|
297
317
|
return ref;
|
|
298
318
|
});
|
|
@@ -303,7 +323,7 @@ export class StorageEngine {
|
|
|
303
323
|
updated.push(summary.id);
|
|
304
324
|
}
|
|
305
325
|
}
|
|
306
|
-
// In other projects: rewrite cross-project refs pointing to
|
|
326
|
+
// In other projects: rewrite cross-project refs pointing to the source project
|
|
307
327
|
for (const otherProj of config.projects) {
|
|
308
328
|
if (otherProj === proj)
|
|
309
329
|
continue;
|
|
@@ -313,7 +333,7 @@ export class StorageEngine {
|
|
|
313
333
|
if (!doc)
|
|
314
334
|
continue;
|
|
315
335
|
let changed = false;
|
|
316
|
-
const newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}`, "g"), `doc://${
|
|
336
|
+
const newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}${boundary}`, "g"), `doc://${qualifiedTarget}`);
|
|
317
337
|
if (newContent !== doc.content) {
|
|
318
338
|
doc.content = newContent;
|
|
319
339
|
changed = true;
|
|
@@ -321,7 +341,7 @@ export class StorageEngine {
|
|
|
321
341
|
const newRelated = doc.related_docs.map((ref) => {
|
|
322
342
|
if (ref === `${proj}:${oldId}`) {
|
|
323
343
|
changed = true;
|
|
324
|
-
return
|
|
344
|
+
return qualifiedTarget;
|
|
325
345
|
}
|
|
326
346
|
return ref;
|
|
327
347
|
});
|
package/dist/src/index.js
CHANGED
|
@@ -1129,15 +1129,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1129
1129
|
},
|
|
1130
1130
|
{
|
|
1131
1131
|
name: "move_doc",
|
|
1132
|
-
description: "Move a documentation entry to a new path (changes its folder and/or name). All existing doc:// links and related_docs references pointing to the old
|
|
1132
|
+
description: "Move a documentation entry to a new path (changes its folder and/or name) and/or to another project. All existing doc:// links and related_docs references pointing to the old location are automatically rewritten across all projects. When the entry changes project, its own same-project links are qualified with the source project so they keep their target, and its tracked_files are then resolved against the target project's code source. This is the only way to relocate a document — update_doc does not change the location. Provide new_path, new_project, or both.",
|
|
1133
1133
|
inputSchema: {
|
|
1134
1134
|
type: "object",
|
|
1135
1135
|
properties: {
|
|
1136
1136
|
path: { type: "string", description: "Current path of the documentation entry, e.g. 'backend/authentication'" },
|
|
1137
|
-
new_path: { type: "string", description: "New path for the documentation entry, e.g. 'backend/v2/authentication'. Can change folder, name, or both." },
|
|
1138
|
-
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION + "
|
|
1137
|
+
new_path: { type: "string", description: "New path for the documentation entry, e.g. 'backend/v2/authentication'. Can change folder, name, or both. Omitted: the entry keeps its current path (new_project is then required)." },
|
|
1138
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION + " This is the project the entry is moved from." },
|
|
1139
|
+
new_project: { type: "string", description: "Project the entry is moved to. Must already exist (see list_projects). Omitted: the entry stays in `project`." },
|
|
1139
1140
|
},
|
|
1140
|
-
required: ["path", "
|
|
1141
|
+
required: ["path", "project"],
|
|
1141
1142
|
},
|
|
1142
1143
|
},
|
|
1143
1144
|
{
|
|
@@ -1,29 +1,43 @@
|
|
|
1
1
|
import { MoveDocArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
2
|
-
import { resolveDocInProject, isScopeError, scopeErrorResult } from "../core/project-scope.js";
|
|
2
|
+
import { resolveDocInProject, assertProject, isScopeError, scopeErrorResult } from "../core/project-scope.js";
|
|
3
3
|
export async function moveDoc(storageEngine, embeddingEngine, args) {
|
|
4
4
|
try {
|
|
5
5
|
const validated = MoveDocArgsSchema.parse(args);
|
|
6
6
|
const resolved = await resolveDocInProject(storageEngine, validated.path, validated.project, "move");
|
|
7
7
|
if (isScopeError(resolved))
|
|
8
8
|
return scopeErrorResult(resolved);
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const project = validated.project;
|
|
10
|
+
const newProject = validated.new_project ?? project;
|
|
11
|
+
const newPath = validated.new_path ?? validated.path;
|
|
12
|
+
const targetError = assertProject(storageEngine, newProject);
|
|
13
|
+
if (targetError)
|
|
14
|
+
return scopeErrorResult(targetError);
|
|
15
|
+
if (newProject === project && newPath === validated.path) {
|
|
16
|
+
return {
|
|
17
|
+
isError: true,
|
|
18
|
+
content: [{ type: "text", text: `"${validated.path}" is already at this path in project "${project}" — nothing to move.` }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const moved = await storageEngine.moveDoc(validated.path, project, newPath, newProject);
|
|
11
22
|
if (!moved) {
|
|
12
23
|
return {
|
|
13
24
|
isError: true,
|
|
14
25
|
content: [{ type: "text", text: `Failed to move "${validated.path}".` }],
|
|
15
26
|
};
|
|
16
27
|
}
|
|
17
|
-
await embeddingEngine.moveEmbedding(validated.path,
|
|
18
|
-
const rewritten = await storageEngine.rewriteLinks(validated.path,
|
|
28
|
+
await embeddingEngine.moveEmbedding(validated.path, project, newPath, newProject);
|
|
29
|
+
const rewritten = await storageEngine.rewriteLinks(validated.path, project, newPath, newProject);
|
|
19
30
|
const rewriteNote = rewritten.length > 0
|
|
20
31
|
? `\n\nLiens mis à jour automatiquement dans ${rewritten.length} doc(s) :\n${rewritten.map((id) => ` - ${id}`).join("\n")}`
|
|
21
32
|
: "\n\nAucune référence existante à mettre à jour.";
|
|
33
|
+
const crossProjectNote = newProject !== project
|
|
34
|
+
? `\n\nLes fichiers suivis (tracked_files) sont désormais résolus depuis la source de code du projet "${newProject}" — lancer check_freshness pour vérifier qu'ils y existent.`
|
|
35
|
+
: "";
|
|
22
36
|
return {
|
|
23
37
|
content: [
|
|
24
38
|
{
|
|
25
39
|
type: "text",
|
|
26
|
-
text: `Documentation entry moved successfully.\nFrom: "${validated.path}"\nTo: "${
|
|
40
|
+
text: `Documentation entry moved successfully.\nFrom: "${project}:${validated.path}"\nTo: "${newProject}:${newPath}"${rewriteNote}${crossProjectNote}`,
|
|
27
41
|
},
|
|
28
42
|
],
|
|
29
43
|
};
|