@imenam/mcp-documentor 1.0.77 → 1.0.78
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 +58 -0
- package/dist/shared/schemas/doc.schema.d.ts +6 -0
- package/dist/shared/schemas/doc.schema.js +18 -2
- package/dist/src/cli/setup-mcp.js +3 -0
- package/dist/src/core/code-operation.d.ts +10 -0
- package/dist/src/core/code-operation.js +42 -0
- package/dist/src/core/code-ref-resolver.d.ts +11 -4
- package/dist/src/core/code-ref-resolver.js +76 -51
- package/dist/src/core/code-source.d.ts +79 -0
- package/dist/src/core/code-source.js +248 -0
- package/dist/src/core/github-code-api.d.ts +72 -0
- package/dist/src/core/github-code-api.js +182 -0
- package/dist/src/core/section-tracker.d.ts +38 -13
- package/dist/src/core/section-tracker.js +75 -72
- package/dist/src/index.js +19 -11
- package/dist/src/tools/check-freshness.d.ts +1 -2
- package/dist/src/tools/check-freshness.js +82 -34
- package/dist/src/tools/create-doc.js +2 -2
- package/dist/src/tools/get-doc-metadata.js +14 -13
- package/dist/src/tools/get-doc.js +23 -20
- package/dist/src/tools/git-diff-doc.js +69 -13
- package/dist/src/tools/list-docs.js +2 -2
- package/dist/src/tools/patch-doc.js +2 -2
- package/dist/src/tools/refresh-file-snapshot.js +19 -17
- package/dist/src/tools/update-doc.js +4 -4
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
|
@@ -2,9 +2,11 @@ import { execSync } from "child_process";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { GitDiffDocArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
4
4
|
import { computeFreshness } from "./check-freshness.js";
|
|
5
|
-
import { toAbsolutePath
|
|
5
|
+
import { toAbsolutePath } from "../core/path-resolver.js";
|
|
6
|
+
import { getCodeSource, getGitHubApi, toStoredPathForMatching } from "../core/code-source.js";
|
|
6
7
|
import { assertProject, scopeErrorResult } from "../core/project-scope.js";
|
|
7
|
-
|
|
8
|
+
/** Diff of one tracked file against the local working copy, via the git CLI. */
|
|
9
|
+
function getLocalGitDiff(tracked, project) {
|
|
8
10
|
const filePath = toAbsolutePath(tracked.path, project);
|
|
9
11
|
if (!filePath) {
|
|
10
12
|
return ` [INFO] Cannot resolve "${tracked.path}" on this machine (no code root configured) — source absent.`;
|
|
@@ -40,6 +42,55 @@ function getGitDiffForFile(tracked, project) {
|
|
|
40
42
|
}
|
|
41
43
|
return diff;
|
|
42
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Diffs tracked files against the branch they are tracked on, using the commit each snapshot
|
|
47
|
+
* was taken at as the base.
|
|
48
|
+
*
|
|
49
|
+
* Files are grouped by that base commit so one comparison covers every file snapshotted at the
|
|
50
|
+
* same revision — which, for a doc refreshed in one go, means a single API call.
|
|
51
|
+
*/
|
|
52
|
+
async function getGitHubDiffs(api, trackedFiles) {
|
|
53
|
+
const diffs = new Map();
|
|
54
|
+
const byBase = new Map();
|
|
55
|
+
for (const tracked of trackedFiles) {
|
|
56
|
+
if (!tracked.sourceRevision) {
|
|
57
|
+
diffs.set(tracked.path, ` [INFO] No source commit recorded for "${tracked.path}" — this snapshot predates branch tracking, or was taken from a local working copy.\n` +
|
|
58
|
+
` → Call refresh_file_snapshot to re-anchor it on ${api.label}, after which diffs become available.`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const group = byBase.get(tracked.sourceRevision);
|
|
62
|
+
if (group)
|
|
63
|
+
group.push(tracked);
|
|
64
|
+
else
|
|
65
|
+
byBase.set(tracked.sourceRevision, [tracked]);
|
|
66
|
+
}
|
|
67
|
+
if (byBase.size === 0)
|
|
68
|
+
return diffs;
|
|
69
|
+
const { commitSha: head } = await api.resolveRef();
|
|
70
|
+
for (const [base, files] of byBase) {
|
|
71
|
+
if (base === head) {
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
diffs.set(file.path, ` [INFO] ${api.label} is still at the snapshot commit ${base.slice(0, 7)} — no changes.`);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
let patches;
|
|
78
|
+
try {
|
|
79
|
+
patches = await api.comparePatches(base, head);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
83
|
+
for (const file of files)
|
|
84
|
+
diffs.set(file.path, ` [ERROR] Could not compare ${base.slice(0, 7)}...${head.slice(0, 7)}: ${message}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
const patch = patches.get(file.path);
|
|
89
|
+
diffs.set(file.path, patch ?? ` [INFO] No changes to this file between ${base.slice(0, 7)} and ${head.slice(0, 7)}.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return diffs;
|
|
93
|
+
}
|
|
43
94
|
export async function gitDiffDoc(storageEngine, args) {
|
|
44
95
|
try {
|
|
45
96
|
const validated = GitDiffDocArgsSchema.parse(args);
|
|
@@ -47,6 +98,11 @@ export async function gitDiffDoc(storageEngine, args) {
|
|
|
47
98
|
const projectError = assertProject(storageEngine, project);
|
|
48
99
|
if (projectError)
|
|
49
100
|
return scopeErrorResult(projectError);
|
|
101
|
+
const source = getCodeSource(project);
|
|
102
|
+
const githubApi = getGitHubApi(project);
|
|
103
|
+
const wanted = validated.file_paths
|
|
104
|
+
? new Set(await Promise.all(validated.file_paths.map((p) => toStoredPathForMatching(project, p))))
|
|
105
|
+
: undefined;
|
|
50
106
|
const docSections = [];
|
|
51
107
|
const seenFilePaths = new Set();
|
|
52
108
|
for (const docPath of validated.paths) {
|
|
@@ -60,12 +116,11 @@ export async function gitDiffDoc(storageEngine, args) {
|
|
|
60
116
|
continue;
|
|
61
117
|
}
|
|
62
118
|
let filesToDiff;
|
|
63
|
-
if (
|
|
64
|
-
const wanted = new Set(validated.file_paths.map((p) => toStoredPath(p, project)));
|
|
119
|
+
if (wanted) {
|
|
65
120
|
filesToDiff = doc.tracked_files.filter((tf) => wanted.has(tf.path));
|
|
66
121
|
}
|
|
67
122
|
else {
|
|
68
|
-
const freshness = computeFreshness(doc.tracked_files, project);
|
|
123
|
+
const freshness = await computeFreshness(doc.tracked_files, project);
|
|
69
124
|
// Broken entries (no snapshot) can't be hash-compared, but the git diff
|
|
70
125
|
// is still the best signal the agent has to re-assess them — include them.
|
|
71
126
|
const changedPaths = new Set([...freshness.modified, ...freshness.missing, ...freshness.broken]);
|
|
@@ -78,22 +133,23 @@ export async function gitDiffDoc(storageEngine, args) {
|
|
|
78
133
|
docSections.push(`## ${doc.title}\n\n${msg}`);
|
|
79
134
|
continue;
|
|
80
135
|
}
|
|
81
|
-
const
|
|
82
|
-
.filter((tf) => {
|
|
136
|
+
const fresh = filesToDiff.filter((tf) => {
|
|
83
137
|
if (seenFilePaths.has(tf.path))
|
|
84
138
|
return false;
|
|
85
139
|
seenFilePaths.add(tf.path);
|
|
86
140
|
return true;
|
|
87
|
-
})
|
|
88
|
-
.map((tf) => {
|
|
89
|
-
const diff = getGitDiffForFile(tf, project);
|
|
90
|
-
return `### ${tf.path}\nSnapshot date: ${tf.lastModifiedAt}\n\n${diff}`;
|
|
91
141
|
});
|
|
92
|
-
if (
|
|
142
|
+
if (fresh.length === 0) {
|
|
93
143
|
docSections.push(`## ${doc.title} [${docPath}]\n\n[INFO] All tracked files already shown in a previous documentation section.`);
|
|
94
144
|
continue;
|
|
95
145
|
}
|
|
96
|
-
|
|
146
|
+
const diffs = githubApi ? await getGitHubDiffs(githubApi, fresh) : undefined;
|
|
147
|
+
const fileSections = fresh.map((tf) => {
|
|
148
|
+
const diff = diffs ? diffs.get(tf.path) : getLocalGitDiff(tf, project);
|
|
149
|
+
const base = tf.sourceRevision ? `\nSnapshot commit: ${tf.sourceRevision.slice(0, 7)}` : "";
|
|
150
|
+
return `### ${tf.path}\nSnapshot date: ${tf.lastModifiedAt}${base}\n\n${diff}`;
|
|
151
|
+
});
|
|
152
|
+
docSections.push(`## ${doc.title} [${docPath}]\n\nCode source: ${source.label}\n\n${fileSections.join("\n\n---\n\n")}`);
|
|
97
153
|
}
|
|
98
154
|
return { content: [{ type: "text", text: docSections.join("\n\n═══════════════════════════════════\n\n") }] };
|
|
99
155
|
}
|
|
@@ -41,7 +41,7 @@ export async function listDocs(storageEngine, args) {
|
|
|
41
41
|
for (const summary of summaries) {
|
|
42
42
|
const doc = await storageEngine.getDoc(summary.id, proj);
|
|
43
43
|
if (doc && doc.tracked_files.length > 0) {
|
|
44
|
-
const report = computeFreshness(doc.tracked_files, proj);
|
|
44
|
+
const report = await computeFreshness(doc.tracked_files, proj);
|
|
45
45
|
summary.isStale = report.status === "stale";
|
|
46
46
|
summary.isBroken = report.status === "broken";
|
|
47
47
|
}
|
|
@@ -60,7 +60,7 @@ export async function listDocs(storageEngine, args) {
|
|
|
60
60
|
for (const summary of summaries) {
|
|
61
61
|
const doc = await storageEngine.getDoc(summary.id, proj);
|
|
62
62
|
if (doc && doc.tracked_files.length > 0) {
|
|
63
|
-
const report = computeFreshness(doc.tracked_files, proj);
|
|
63
|
+
const report = await computeFreshness(doc.tracked_files, proj);
|
|
64
64
|
summary.isStale = report.status === "stale";
|
|
65
65
|
summary.isBroken = report.status === "broken";
|
|
66
66
|
}
|
|
@@ -17,7 +17,7 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
|
|
|
17
17
|
const rawReplacement = validated.replacement_file
|
|
18
18
|
? fs.readFileSync(validated.replacement_file, "utf-8")
|
|
19
19
|
: validated.replacement;
|
|
20
|
-
const { content: replacement, additionalTrackedFiles } = resolveCodeRefs(rawReplacement);
|
|
20
|
+
const { content: replacement, additionalTrackedFiles } = await resolveCodeRefs(rawReplacement, resolvedProject);
|
|
21
21
|
if (!doc.content.includes(search)) {
|
|
22
22
|
const preview = doc.content.slice(0, 300).replace(/\n/g, "↵");
|
|
23
23
|
return {
|
|
@@ -29,7 +29,7 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
doc.content = doc.content.replace(search, replacement);
|
|
32
|
-
doc.tracked_files = mergeTrackedFiles(doc.tracked_files, additionalTrackedFiles, resolvedProject);
|
|
32
|
+
doc.tracked_files = await mergeTrackedFiles(doc.tracked_files, additionalTrackedFiles, resolvedProject);
|
|
33
33
|
doc.updatedAt = new Date().toISOString();
|
|
34
34
|
await storageEngine.saveDoc(doc, resolvedProject, "patch");
|
|
35
35
|
if (embeddingEngine.isConfigured()) {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
1
|
import { RefreshFileSnapshotArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { toAbsolutePath, toStoredPath } from "../core/path-resolver.js";
|
|
2
|
+
import { relocateAndRefreshSection, hashFileContent } from "../core/section-tracker.js";
|
|
3
|
+
import { getCodeSource, toStoredPathForMatching } from "../core/code-source.js";
|
|
6
4
|
import { assertProject, scopeErrorResult } from "../core/project-scope.js";
|
|
7
5
|
export async function refreshFileSnapshot(storageEngine, historyStore, args) {
|
|
8
6
|
try {
|
|
@@ -11,7 +9,15 @@ export async function refreshFileSnapshot(storageEngine, historyStore, args) {
|
|
|
11
9
|
const projectError = assertProject(storageEngine, resolvedProject);
|
|
12
10
|
if (projectError)
|
|
13
11
|
return scopeErrorResult(projectError);
|
|
12
|
+
const source = getCodeSource(resolvedProject);
|
|
14
13
|
const docSections = [];
|
|
14
|
+
// Normalize to forward slashes and lowercase for case-insensitive comparison
|
|
15
|
+
// on Windows. Agent-supplied file_paths are absolute; map them to the stored
|
|
16
|
+
// (portable) form so they match tracked.path regardless of representation.
|
|
17
|
+
const norm = (p) => p.replace(/\\/g, "/").toLowerCase();
|
|
18
|
+
const resolvedFilePaths = validated.file_paths
|
|
19
|
+
? await Promise.all(validated.file_paths.map(async (p) => norm(await toStoredPathForMatching(resolvedProject, p))))
|
|
20
|
+
: undefined;
|
|
15
21
|
for (const docPath of validated.paths) {
|
|
16
22
|
const doc = await storageEngine.getDoc(docPath, resolvedProject);
|
|
17
23
|
if (!doc) {
|
|
@@ -22,31 +28,27 @@ export async function refreshFileSnapshot(storageEngine, historyStore, args) {
|
|
|
22
28
|
const missing = [];
|
|
23
29
|
const skipped = [];
|
|
24
30
|
const anchorsLost = [];
|
|
25
|
-
// Normalize to forward slashes and lowercase for case-insensitive comparison
|
|
26
|
-
// on Windows. Agent-supplied file_paths are absolute; map them to the stored
|
|
27
|
-
// (portable) form so they match tracked.path regardless of representation.
|
|
28
|
-
const norm = (p) => p.replace(/\\/g, "/").toLowerCase();
|
|
29
|
-
const resolvedFilePaths = validated.file_paths?.map((p) => norm(toStoredPath(p, resolvedProject)));
|
|
30
31
|
for (const tracked of doc.tracked_files) {
|
|
31
32
|
if (resolvedFilePaths && !resolvedFilePaths.includes(norm(tracked.path))) {
|
|
32
33
|
skipped.push(tracked.path);
|
|
33
34
|
continue;
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
-
if (!
|
|
36
|
+
const file = await source.read(tracked.path);
|
|
37
|
+
if (!file) {
|
|
37
38
|
missing.push(tracked.path);
|
|
38
39
|
continue;
|
|
39
40
|
}
|
|
40
|
-
tracked.lastModifiedAt =
|
|
41
|
+
tracked.lastModifiedAt = file.lastModifiedAt;
|
|
42
|
+
tracked.sourceRevision = file.revision;
|
|
43
|
+
tracked.sourceBlobId = file.blobId;
|
|
41
44
|
if (tracked.sections && tracked.sections.length > 0) {
|
|
42
45
|
let hasLostAnchors = false;
|
|
43
|
-
|
|
44
|
-
const result = relocateAndRefreshSection(
|
|
46
|
+
tracked.sections = tracked.sections.map((section) => {
|
|
47
|
+
const result = relocateAndRefreshSection(file.content, section);
|
|
45
48
|
if (!result.found)
|
|
46
49
|
hasLostAnchors = true;
|
|
47
50
|
return result.found ? result.refreshed : section;
|
|
48
51
|
});
|
|
49
|
-
tracked.sections = refreshedSections;
|
|
50
52
|
if (hasLostAnchors) {
|
|
51
53
|
anchorsLost.push(tracked.path);
|
|
52
54
|
}
|
|
@@ -55,13 +57,13 @@ export async function refreshFileSnapshot(storageEngine, historyStore, args) {
|
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
else {
|
|
58
|
-
tracked.contentHash =
|
|
60
|
+
tracked.contentHash = hashFileContent(file.content);
|
|
59
61
|
refreshed.push(tracked.path);
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
await storageEngine.saveDoc(doc, resolvedProject);
|
|
63
65
|
historyStore.record(resolvedProject, doc, "snapshot");
|
|
64
|
-
let section = `**${doc.title}** [${docPath}]: refreshed ${refreshed.length}, anchors lost ${anchorsLost.length}, missing ${missing.length}, skipped ${skipped.length}`;
|
|
66
|
+
let section = `**${doc.title}** [${docPath}]: refreshed ${refreshed.length}, anchors lost ${anchorsLost.length}, missing ${missing.length}, skipped ${skipped.length} (source: ${source.label})`;
|
|
65
67
|
if (refreshed.length > 0)
|
|
66
68
|
section += `\n${refreshed.map((f) => ` ✓ ${f}`).join("\n")}`;
|
|
67
69
|
if (missing.length > 0)
|
|
@@ -16,12 +16,12 @@ export async function updateDoc(storageEngine, embeddingEngine, args) {
|
|
|
16
16
|
}
|
|
17
17
|
let additionalTrackedFiles = [];
|
|
18
18
|
if (validated.content_file !== undefined) {
|
|
19
|
-
const { content, additionalTrackedFiles: refs } = resolveCodeRefs(fs.readFileSync(validated.content_file, "utf-8"));
|
|
19
|
+
const { content, additionalTrackedFiles: refs } = await resolveCodeRefs(fs.readFileSync(validated.content_file, "utf-8"), resolvedProject);
|
|
20
20
|
doc.content = content;
|
|
21
21
|
additionalTrackedFiles = refs;
|
|
22
22
|
}
|
|
23
23
|
else if (validated.content !== undefined) {
|
|
24
|
-
const { content, additionalTrackedFiles: refs } = resolveCodeRefs(validated.content);
|
|
24
|
+
const { content, additionalTrackedFiles: refs } = await resolveCodeRefs(validated.content, resolvedProject);
|
|
25
25
|
doc.content = content;
|
|
26
26
|
additionalTrackedFiles = refs;
|
|
27
27
|
}
|
|
@@ -30,14 +30,14 @@ export async function updateDoc(storageEngine, embeddingEngine, args) {
|
|
|
30
30
|
// the same scope are carried over as-is; only genuinely new entries get one.
|
|
31
31
|
let trackedResult;
|
|
32
32
|
if (validated.tracked_files !== undefined) {
|
|
33
|
-
trackedResult = resnapshotPreserving(doc.tracked_files, [
|
|
33
|
+
trackedResult = await resnapshotPreserving(doc.tracked_files, [
|
|
34
34
|
...validated.tracked_files,
|
|
35
35
|
...additionalTrackedFiles,
|
|
36
36
|
], resolvedProject);
|
|
37
37
|
doc.tracked_files = trackedResult.tracked;
|
|
38
38
|
}
|
|
39
39
|
else {
|
|
40
|
-
doc.tracked_files = mergeTrackedFiles(doc.tracked_files, additionalTrackedFiles, resolvedProject);
|
|
40
|
+
doc.tracked_files = await mergeTrackedFiles(doc.tracked_files, additionalTrackedFiles, resolvedProject);
|
|
41
41
|
}
|
|
42
42
|
if (validated.tags !== undefined) {
|
|
43
43
|
doc.tags = validated.tags;
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const APP_VERSION = "1.0.
|
|
1
|
+
export declare const APP_VERSION = "1.0.78";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated version file - do not edit manually
|
|
2
|
-
export const APP_VERSION = '1.0.
|
|
2
|
+
export const APP_VERSION = '1.0.78';
|