@imenam/mcp-documentor 1.0.56 → 1.0.58

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.
@@ -2,6 +2,7 @@ import fs from "fs";
2
2
  import { RefreshFileSnapshotArgsSchema } from "../../shared/schemas/doc.schema.js";
3
3
  import { computeFileHash } from "./check-freshness.js";
4
4
  import { relocateAndRefreshSection } from "../core/section-tracker.js";
5
+ import { toAbsolutePath, toStoredPath } from "../core/path-resolver.js";
5
6
  export async function refreshFileSnapshot(storageEngine, historyStore, args) {
6
7
  try {
7
8
  const validated = RefreshFileSnapshotArgsSchema.parse(args);
@@ -17,24 +18,26 @@ export async function refreshFileSnapshot(storageEngine, historyStore, args) {
17
18
  const missing = [];
18
19
  const skipped = [];
19
20
  const anchorsLost = [];
20
- // Normalize to forward slashes and lowercase for case-insensitive comparison on Windows
21
- // (e.g. stored 'c:\...' vs provided 'C:\...'). file_paths must be absolute paths.
21
+ // Normalize to forward slashes and lowercase for case-insensitive comparison
22
+ // on Windows. Agent-supplied file_paths are absolute; map them to the stored
23
+ // (portable) form so they match tracked.path regardless of representation.
22
24
  const norm = (p) => p.replace(/\\/g, "/").toLowerCase();
23
- const resolvedFilePaths = validated.file_paths?.map(norm);
25
+ const resolvedFilePaths = validated.file_paths?.map((p) => norm(toStoredPath(p, resolvedProject)));
24
26
  for (const tracked of doc.tracked_files) {
25
27
  if (resolvedFilePaths && !resolvedFilePaths.includes(norm(tracked.path))) {
26
28
  skipped.push(tracked.path);
27
29
  continue;
28
30
  }
29
- if (!fs.existsSync(tracked.path)) {
31
+ const absolutePath = toAbsolutePath(tracked.path, resolvedProject);
32
+ if (!absolutePath || !fs.existsSync(absolutePath)) {
30
33
  missing.push(tracked.path);
31
34
  continue;
32
35
  }
33
- tracked.lastModifiedAt = fs.statSync(tracked.path).mtime.toISOString();
36
+ tracked.lastModifiedAt = fs.statSync(absolutePath).mtime.toISOString();
34
37
  if (tracked.sections && tracked.sections.length > 0) {
35
38
  let hasLostAnchors = false;
36
39
  const refreshedSections = tracked.sections.map((section) => {
37
- const result = relocateAndRefreshSection(tracked.path, section);
40
+ const result = relocateAndRefreshSection(absolutePath, section);
38
41
  if (!result.found)
39
42
  hasLostAnchors = true;
40
43
  return result.found ? result.refreshed : section;
@@ -48,7 +51,7 @@ export async function refreshFileSnapshot(storageEngine, historyStore, args) {
48
51
  }
49
52
  }
50
53
  else {
51
- tracked.contentHash = computeFileHash(tracked.path);
54
+ tracked.contentHash = computeFileHash(absolutePath);
52
55
  refreshed.push(tracked.path);
53
56
  }
54
57
  }
@@ -0,0 +1,17 @@
1
+ import type { SyncEngine } from "../core/git-sync.js";
2
+ import type { StorageEngine } from "../core/storage-engine.js";
3
+ import type { EmbeddingEngine } from "../core/embedding-engine.js";
4
+ import type { EmbeddingStore } from "../core/embedding-store.js";
5
+ type ToolResult = {
6
+ content: {
7
+ type: "text";
8
+ text: string;
9
+ }[];
10
+ isError?: boolean;
11
+ };
12
+ export declare function syncStatus(syncEngine: SyncEngine): Promise<ToolResult>;
13
+ export declare function pullDocs(syncEngine: SyncEngine, storageEngine: StorageEngine, embeddingEngine: EmbeddingEngine, embeddingStore: EmbeddingStore): Promise<ToolResult>;
14
+ export declare function pushDocs(syncEngine: SyncEngine, args: unknown): Promise<ToolResult>;
15
+ export declare function syncDocs(syncEngine: SyncEngine, storageEngine: StorageEngine, embeddingEngine: EmbeddingEngine, embeddingStore: EmbeddingStore, args: unknown): Promise<ToolResult>;
16
+ export declare function resolveSyncConflict(syncEngine: SyncEngine, args: unknown): Promise<ToolResult>;
17
+ export {};
@@ -0,0 +1,113 @@
1
+ import { z } from "zod";
2
+ import { reindexEmbeddings } from "./reindex-embeddings.js";
3
+ function ok(text) {
4
+ return { content: [{ type: "text", text }] };
5
+ }
6
+ function err(text) {
7
+ return { content: [{ type: "text", text }], isError: true };
8
+ }
9
+ const NOT_CONFIGURED = "Sync is not configured. Set MCP_DOCUMENTOR_SYNC_REMOTE (and MCP_DOCUMENTOR_SYNC_TOKEN) in the server environment.";
10
+ export async function syncStatus(syncEngine) {
11
+ try {
12
+ if (!syncEngine.isConfigured())
13
+ return ok(NOT_CONFIGURED);
14
+ const s = await syncEngine.status();
15
+ const lines = [
16
+ `Branch: ${s.branch}`,
17
+ `Ahead: ${s.ahead} commit(s) · Behind: ${s.behind} commit(s)`,
18
+ s.conflicted.length > 0 ? `Conflicted files (${s.conflicted.length}):\n - ${s.conflicted.join("\n - ")}` : "No conflicts.",
19
+ s.dirty.length > 0 ? `Uncommitted changes (${s.dirty.length}):\n - ${s.dirty.join("\n - ")}` : "Working tree clean.",
20
+ ];
21
+ return ok(lines.join("\n"));
22
+ }
23
+ catch (e) {
24
+ return err(`Error getting sync status: ${e instanceof Error ? e.message : String(e)}`);
25
+ }
26
+ }
27
+ function describePull(res) {
28
+ switch (res.status) {
29
+ case "up-to-date":
30
+ return "Already up to date — nothing to pull.";
31
+ case "updated":
32
+ return `Pulled ${res.changed.length} changed file(s):\n - ${res.changed.join("\n - ")}`;
33
+ case "diverged":
34
+ return `You have uncommitted local changes while the remote has ${res.behind} new commit(s). No changes applied — your local data is intact. Run push_docs to commit and publish your changes, then pull again to merge.`;
35
+ case "conflict":
36
+ return `Merge conflict in ${res.conflicted.length} file(s):\n - ${res.conflicted.join("\n - ")}\nEdit these markdown files to resolve, or use resolve_sync_conflict, then push_docs.`;
37
+ }
38
+ }
39
+ /** Reindex embeddings for new docs after a successful merge. Best-effort. */
40
+ async function reindexAfterPull(res, storageEngine, embeddingEngine, embeddingStore) {
41
+ if (res.status !== "updated" || res.changed.length === 0)
42
+ return "";
43
+ if (!embeddingEngine.isConfigured())
44
+ return "\n(Embeddings not reindexed: OpenAI key not configured.)";
45
+ try {
46
+ await reindexEmbeddings(storageEngine, embeddingEngine, embeddingStore, {});
47
+ return "\nEmbeddings reindexed for new documents.";
48
+ }
49
+ catch (e) {
50
+ return `\n(Embedding reindex failed: ${e instanceof Error ? e.message : String(e)})`;
51
+ }
52
+ }
53
+ export async function pullDocs(syncEngine, storageEngine, embeddingEngine, embeddingStore) {
54
+ try {
55
+ if (!syncEngine.isConfigured())
56
+ return err(NOT_CONFIGURED);
57
+ const res = await syncEngine.pull();
58
+ const reindexNote = await reindexAfterPull(res, storageEngine, embeddingEngine, embeddingStore);
59
+ return ok(describePull(res) + reindexNote);
60
+ }
61
+ catch (e) {
62
+ return err(`Error pulling docs: ${e instanceof Error ? e.message : String(e)}`);
63
+ }
64
+ }
65
+ const PushDocsArgs = z.object({ message: z.string().optional() });
66
+ export async function pushDocs(syncEngine, args) {
67
+ try {
68
+ if (!syncEngine.isConfigured())
69
+ return err(NOT_CONFIGURED);
70
+ const v = PushDocsArgs.parse(args);
71
+ const res = await syncEngine.push(v.message);
72
+ return ok(res.message);
73
+ }
74
+ catch (e) {
75
+ return err(`Error pushing docs: ${e instanceof Error ? e.message : String(e)}`);
76
+ }
77
+ }
78
+ export async function syncDocs(syncEngine, storageEngine, embeddingEngine, embeddingStore, args) {
79
+ try {
80
+ if (!syncEngine.isConfigured())
81
+ return err(NOT_CONFIGURED);
82
+ const v = PushDocsArgs.parse(args);
83
+ const pull = await syncEngine.pull();
84
+ const reindexNote = await reindexAfterPull(pull, storageEngine, embeddingEngine, embeddingStore);
85
+ // Don't push over a divergence/conflict — surface it so the user can resolve first.
86
+ if (pull.status === "diverged" || pull.status === "conflict") {
87
+ return ok(`Pull step:\n${describePull(pull)}${reindexNote}\n\nPush skipped until the above is resolved.`);
88
+ }
89
+ const push = await syncEngine.push(v.message);
90
+ return ok(`Pull step: ${describePull(pull)}${reindexNote}\n\nPush step: ${push.message}`);
91
+ }
92
+ catch (e) {
93
+ return err(`Error syncing docs: ${e instanceof Error ? e.message : String(e)}`);
94
+ }
95
+ }
96
+ const ResolveConflictArgs = z.object({
97
+ strategy: z.enum(["mine", "theirs"]),
98
+ paths: z.array(z.string()).optional(),
99
+ });
100
+ export async function resolveSyncConflict(syncEngine, args) {
101
+ try {
102
+ if (!syncEngine.isConfigured())
103
+ return err(NOT_CONFIGURED);
104
+ const v = ResolveConflictArgs.parse(args);
105
+ const resolved = await syncEngine.resolveConflict(v.strategy, v.paths);
106
+ if (resolved.length === 0)
107
+ return ok("No conflicted files to resolve.");
108
+ return ok(`Resolved ${resolved.length} file(s) keeping "${v.strategy}":\n - ${resolved.join("\n - ")}\nReview and run push_docs to finalize.`);
109
+ }
110
+ catch (e) {
111
+ return err(`Error resolving conflict: ${e instanceof Error ? e.message : String(e)}`);
112
+ }
113
+ }
@@ -28,10 +28,10 @@ export async function updateDoc(storageEngine, embeddingEngine, args) {
28
28
  doc.tracked_files = snapshotTrackedFiles([
29
29
  ...validated.tracked_files,
30
30
  ...additionalTrackedFiles,
31
- ]);
31
+ ], resolvedProject);
32
32
  }
33
33
  else {
34
- doc.tracked_files = mergeTrackedFiles(doc.tracked_files, additionalTrackedFiles);
34
+ doc.tracked_files = mergeTrackedFiles(doc.tracked_files, additionalTrackedFiles, resolvedProject);
35
35
  }
36
36
  if (validated.tags !== undefined) {
37
37
  doc.tags = validated.tags;
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.56";
1
+ export declare const APP_VERSION = "1.0.58";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated version file - do not edit manually
2
- export const APP_VERSION = '1.0.56';
2
+ export const APP_VERSION = '1.0.58';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-documentor",
3
- "version": "1.0.56",
3
+ "version": "1.0.58",
4
4
  "main": "dist/src/index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -35,12 +35,14 @@
35
35
  "jszip": "^3.10.1",
36
36
  "openai": "^6.25.0",
37
37
  "puppeteer-core": "^25.0.4",
38
+ "simple-git": "^3.36.0",
38
39
  "zod": "^4.3.6"
39
40
  },
40
41
  "type": "module",
41
42
  "bin": {
42
43
  "mcp-documentor": "dist/src/index.js",
43
- "mcp-documentor-migrate": "scripts/migrate-links.js"
44
+ "mcp-documentor-migrate": "scripts/migrate-links.js",
45
+ "mcp-documentor-migrate-paths": "scripts/migrate-paths-to-relative.js"
44
46
  },
45
47
  "files": [
46
48
  "dist",
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * migrate-paths-to-relative.js
4
+ *
5
+ * One-shot migration that rewrites the absolute paths stored in each doc's
6
+ * `tracked_files[].path` into the portable, code-root-relative form introduced
7
+ * alongside GitHub documentation sync. Portable paths let an agent running on a
8
+ * different machine (where the codebase lives at a different absolute location)
9
+ * resolve tracked files correctly.
10
+ *
11
+ * Resolution reuses the compiled path-resolver (dist/) so it behaves exactly
12
+ * like the running server — RUN `npm run build` FIRST.
13
+ *
14
+ * The conversion is driven by the same environment variables as the server:
15
+ * MCP_DOCUMENTOR_CODE_ROOT default code root
16
+ * MCP_DOCUMENTOR_CODE_ROOTS JSON map of per-project overrides
17
+ *
18
+ * A tracked file whose absolute path is NOT under the project's code root is
19
+ * left absolute and reported, so nothing is silently broken.
20
+ *
21
+ * The data dir is resolved like the server: --data-dir flag, else MCP_DATA_DIR,
22
+ * else <package>/data. The MCP_DATA_DIR fallback is what makes npx usable.
23
+ *
24
+ * Usage (from a clone):
25
+ * node scripts/migrate-paths-to-relative.js [--dry-run] [--data-dir <dir>]
26
+ *
27
+ * Usage (npx install — no clone):
28
+ * MCP_DOCUMENTOR_CODE_ROOT="<repos root>" MCP_DATA_DIR="<your data dir>" \
29
+ * npx -p @imenam/mcp-documentor mcp-documentor-migrate-paths --dry-run
30
+ *
31
+ * This migration is a ONE-TIME operation: run it once on the machine where the
32
+ * stored absolute paths are valid, publish the result (sync_docs / push_docs),
33
+ * and every other machine just pulls and sets its own MCP_DOCUMENTOR_CODE_ROOT.
34
+ */
35
+
36
+ import fs from "fs";
37
+ import path from "path";
38
+ import { fileURLToPath, pathToFileURL } from "url";
39
+ import matter from "gray-matter";
40
+
41
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
42
+ const rootDir = path.join(__dirname, "..");
43
+
44
+ const args = process.argv.slice(2);
45
+ const dryRun = args.includes("--dry-run");
46
+
47
+ // Data dir resolution mirrors the server (StorageEngine.getDataDir):
48
+ // --data-dir flag > MCP_DATA_DIR env > <package>/data fallback.
49
+ // The env fallback is what makes `npx` work: the data lives in the user's
50
+ // MCP_DATA_DIR, not inside the npx package cache.
51
+ const dataDirArgIdx = args.indexOf("--data-dir");
52
+ const dataDir =
53
+ dataDirArgIdx !== -1 && args[dataDirArgIdx + 1]
54
+ ? args[dataDirArgIdx + 1]
55
+ : process.env.MCP_DATA_DIR && process.env.MCP_DATA_DIR.trim() !== ""
56
+ ? process.env.MCP_DATA_DIR.trim()
57
+ : path.join(rootDir, "data");
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Reuse the compiled resolver so migration matches runtime behaviour exactly.
61
+ // ---------------------------------------------------------------------------
62
+
63
+ const resolverPath = path.join(rootDir, "dist", "src", "core", "path-resolver.js");
64
+ if (!fs.existsSync(resolverPath)) {
65
+ console.error(`[ERROR] Compiled resolver not found at: ${resolverPath}`);
66
+ console.error(" Run `npm run build` before this migration.");
67
+ process.exit(1);
68
+ }
69
+ const { getCodeRoot, toStoredPath } = await import(pathToFileURL(resolverPath).href);
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Helpers
73
+ // ---------------------------------------------------------------------------
74
+
75
+ function walkMdFiles(dir) {
76
+ if (!fs.existsSync(dir)) return [];
77
+ const results = [];
78
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
79
+ const fullPath = path.join(dir, entry.name);
80
+ if (entry.isDirectory()) {
81
+ results.push(...walkMdFiles(fullPath));
82
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
83
+ results.push(fullPath);
84
+ }
85
+ }
86
+ return results;
87
+ }
88
+
89
+ function filePathToDocId(filePath, projectDir) {
90
+ return path.relative(projectDir, filePath).replace(/\.md$/, "").split(path.sep).join("/");
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Load config
95
+ // ---------------------------------------------------------------------------
96
+
97
+ const configPath = path.join(dataDir, "config.json");
98
+ if (!fs.existsSync(configPath)) {
99
+ console.error(`[ERROR] config.json not found at: ${configPath}`);
100
+ console.error(" Make sure --data-dir points to the right directory.");
101
+ process.exit(1);
102
+ }
103
+
104
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
105
+ const projects = config.projects ?? ["default"];
106
+
107
+ console.log(`${dryRun ? "[DRY RUN] " : ""}migrate-paths-to-relative`);
108
+ console.log(`Data dir : ${dataDir}`);
109
+ console.log(`Projects : ${projects.length}`);
110
+ console.log("");
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Migrate
114
+ // ---------------------------------------------------------------------------
115
+
116
+ let totalDocsChanged = 0;
117
+ let totalConverted = 0;
118
+ let totalLeftAbsolute = 0;
119
+ const projectsWithoutRoot = [];
120
+
121
+ for (const project of projects) {
122
+ const root = getCodeRoot(project);
123
+ if (!root) {
124
+ projectsWithoutRoot.push(project);
125
+ continue;
126
+ }
127
+
128
+ const projectDir = path.join(dataDir, "projects", project);
129
+ const files = walkMdFiles(projectDir);
130
+ if (files.length === 0) continue;
131
+
132
+ const docChanges = [];
133
+
134
+ for (const filePath of files) {
135
+ const fullId = filePathToDocId(filePath, projectDir);
136
+ const parsed = matter(fs.readFileSync(filePath, "utf-8"));
137
+ const tracked = parsed.data.tracked_files;
138
+ if (!Array.isArray(tracked) || tracked.length === 0) continue;
139
+
140
+ let docChanged = false;
141
+ for (const tf of tracked) {
142
+ if (!tf || typeof tf.path !== "string") continue;
143
+ if (!path.isAbsolute(tf.path)) continue; // already relative
144
+
145
+ const stored = toStoredPath(tf.path, project);
146
+ if (path.isAbsolute(stored)) {
147
+ // Outside the root (or different drive) — resolver kept it absolute.
148
+ totalLeftAbsolute++;
149
+ docChanges.push(` [${fullId}] KEPT ABSOLUTE (outside root): ${tf.path}`);
150
+ continue;
151
+ }
152
+
153
+ docChanges.push(` [${fullId}] "${tf.path}" → "${stored}"`);
154
+ tf.path = stored;
155
+ docChanged = true;
156
+ totalConverted++;
157
+ }
158
+
159
+ if (docChanged) {
160
+ totalDocsChanged++;
161
+ if (!dryRun) {
162
+ fs.writeFileSync(filePath, matter.stringify(parsed.content, parsed.data), "utf-8");
163
+ }
164
+ }
165
+ }
166
+
167
+ if (docChanges.length > 0) {
168
+ console.log(`### Project: ${project} (root: ${root})`);
169
+ docChanges.forEach((c) => console.log(c));
170
+ console.log("");
171
+ }
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Summary
176
+ // ---------------------------------------------------------------------------
177
+
178
+ console.log("─".repeat(60));
179
+ console.log(`Paths converted to relative : ${totalConverted}`);
180
+ console.log(`Paths kept absolute : ${totalLeftAbsolute} ← outside the code root`);
181
+ console.log(`Docs modified : ${totalDocsChanged}`);
182
+ if (projectsWithoutRoot.length > 0) {
183
+ console.log("");
184
+ console.log(`Projects skipped (no code root configured): ${projectsWithoutRoot.length}`);
185
+ console.log(` ${projectsWithoutRoot.join(", ")}`);
186
+ console.log(" → set MCP_DOCUMENTOR_CODE_ROOT or add them to MCP_DOCUMENTOR_CODE_ROOTS to migrate them.");
187
+ }
188
+
189
+ if (dryRun) {
190
+ console.log("");
191
+ console.log("[DRY RUN] No files were modified. Run without --dry-run to apply.");
192
+ }