@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 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 (folder/name) within its project (`project` required) |
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.string().describe("New path for the documentation entry, e.g. 'backend/v2/authentication'. Changes folder and/or name."),
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, project: string): Promise<void>;
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, project) {
70
- this.store.move(project, oldDocId, newDocId);
71
- logger.debug(`Embedding moved: "${oldDocId}" → "${newDocId}" in project "${project}"`);
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
- moveDoc(oldId: string, newId: string, project: string): Promise<boolean>;
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
- rewriteLinks(oldId: string, newId: string, project: string): Promise<string[]>;
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
- async moveDoc(oldId, newId, project) {
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 newFilePath = this.docIdToFilePath(newId, projectDir);
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, project, "update");
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
- async rewriteLinks(oldId, newId, project) {
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 same project: rewrite same-project refs and explicit self-cross-project refs
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://${newId}`);
282
- // Also rewrite explicit self cross-project refs (doc://proj:oldId → doc://proj:newId)
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 newId;
311
+ return bareTarget;
292
312
  }
293
313
  if (ref === `${proj}:${oldId}`) {
294
314
  changed = true;
295
- return `${proj}:${newId}`;
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 this project
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://${proj}:${newId}`);
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 `${proj}:${newId}`;
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 path are automatically rewritten across the project. This is the only way to relocate a document — update_doc does not change the location.",
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 + " The doc stays in this project — move_doc never moves an entry across projects." },
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", "new_path", "project"],
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 resolvedProject = validated.project;
10
- const moved = await storageEngine.moveDoc(validated.path, validated.new_path, resolvedProject);
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, validated.new_path, resolvedProject);
18
- const rewritten = await storageEngine.rewriteLinks(validated.path, validated.new_path, resolvedProject);
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: "${validated.new_path}"${rewriteNote}`,
40
+ text: `Documentation entry moved successfully.\nFrom: "${project}:${validated.path}"\nTo: "${newProject}:${newPath}"${rewriteNote}${crossProjectNote}`,
27
41
  },
28
42
  ],
29
43
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-documentor",
3
- "version": "1.0.86",
3
+ "version": "1.0.87",
4
4
  "main": "dist/src/index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",