@imenam/mcp-documentor 1.0.53 → 1.0.55
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/dist/shared/schemas/doc.schema.d.ts +7 -3
- package/dist/shared/schemas/doc.schema.js +13 -3
- package/dist/src/core/code-ref-resolver.d.ts +14 -0
- package/dist/src/core/code-ref-resolver.js +118 -0
- package/dist/src/core/draft-store.d.ts +2 -0
- package/dist/src/core/draft-store.js +14 -0
- package/dist/src/index.js +9 -5
- package/dist/src/tools/create-doc.js +24 -27
- package/dist/src/tools/patch-doc.js +34 -2
- package/dist/src/tools/update-doc.js +34 -29
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
|
@@ -90,7 +90,8 @@ export declare const FreshnessReportSchema: z.ZodObject<{
|
|
|
90
90
|
export type FreshnessReport = z.infer<typeof FreshnessReportSchema>;
|
|
91
91
|
export declare const CreateDocArgsSchema: z.ZodObject<{
|
|
92
92
|
title: z.ZodString;
|
|
93
|
-
content: z.ZodString
|
|
93
|
+
content: z.ZodOptional<z.ZodString>;
|
|
94
|
+
content_file: z.ZodOptional<z.ZodString>;
|
|
94
95
|
tracked_files: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
95
96
|
path: z.ZodString;
|
|
96
97
|
lines: z.ZodArray<z.ZodObject<{
|
|
@@ -106,6 +107,7 @@ export declare const UpdateDocArgsSchema: z.ZodObject<{
|
|
|
106
107
|
path: z.ZodString;
|
|
107
108
|
title: z.ZodOptional<z.ZodString>;
|
|
108
109
|
content: z.ZodOptional<z.ZodString>;
|
|
110
|
+
content_file: z.ZodOptional<z.ZodString>;
|
|
109
111
|
tracked_files: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
110
112
|
path: z.ZodString;
|
|
111
113
|
lines: z.ZodArray<z.ZodObject<{
|
|
@@ -140,8 +142,10 @@ export declare const GetDocMetadataArgsSchema: z.ZodObject<{
|
|
|
140
142
|
}, z.core.$strip>;
|
|
141
143
|
export declare const PatchDocArgsSchema: z.ZodObject<{
|
|
142
144
|
path: z.ZodString;
|
|
143
|
-
search: z.ZodString
|
|
144
|
-
|
|
145
|
+
search: z.ZodOptional<z.ZodString>;
|
|
146
|
+
search_file: z.ZodOptional<z.ZodString>;
|
|
147
|
+
replacement: z.ZodOptional<z.ZodString>;
|
|
148
|
+
replacement_file: z.ZodOptional<z.ZodString>;
|
|
145
149
|
}, z.core.$strip>;
|
|
146
150
|
export declare const CheckLinksArgsSchema: z.ZodObject<{
|
|
147
151
|
project: z.ZodOptional<z.ZodString>;
|
|
@@ -45,16 +45,20 @@ export const FreshnessReportSchema = z.object({
|
|
|
45
45
|
});
|
|
46
46
|
export const CreateDocArgsSchema = z.object({
|
|
47
47
|
title: z.string().describe("Title of the documentation entry"),
|
|
48
|
-
content: z.string().describe("Documentation content in markdown"),
|
|
48
|
+
content: z.string().optional().describe("Documentation content in markdown"),
|
|
49
|
+
content_file: z.string().optional().describe("Absolute path to a text file whose content will be used instead of the content parameter. Use this to reuse a previously persisted draft."),
|
|
49
50
|
tracked_files: z.array(TrackedFileInputSchema).optional().describe("Files to track for staleness detection. Each entry is either a plain path string (whole-file tracking) or an object { path, lines: [{ start, end }, ...] } for granular section tracking. Lines are 1-indexed and inclusive."),
|
|
50
51
|
tags: z.array(z.string()).optional().describe("Tags for categorization"),
|
|
51
52
|
folder: z.string().optional().describe("Folder path for organization, e.g. 'frontend/components'"),
|
|
52
53
|
related_docs: z.array(z.string()).optional().describe("IDs of related documentation entries"),
|
|
54
|
+
}).refine(d => d.content || d.content_file, {
|
|
55
|
+
message: "Either content or content_file is required",
|
|
53
56
|
});
|
|
54
57
|
export const UpdateDocArgsSchema = z.object({
|
|
55
58
|
path: z.string().describe("Path of the documentation entry to update, e.g. 'backend/auth/authentication' or 'authentication' for root docs"),
|
|
56
59
|
title: z.string().optional().describe("Title of the documentation entry"),
|
|
57
60
|
content: z.string().optional().describe("New content (replaces existing)"),
|
|
61
|
+
content_file: z.string().optional().describe("Absolute path to a text file whose content will be used instead of the content parameter. Use this to reuse a previously persisted draft."),
|
|
58
62
|
tracked_files: z.array(TrackedFileInputSchema).optional().describe("New list of tracked files (replaces existing). Each entry is either a plain path string (whole-file tracking) or an object { path, lines: [{ start, end }, ...] } for granular section tracking. Lines are 1-indexed and inclusive."),
|
|
59
63
|
tags: z.array(z.string()).optional().describe("New tags (replaces existing)"),
|
|
60
64
|
related_docs: z.array(z.string()).optional().describe("Paths of related documentation entries (replaces existing)"),
|
|
@@ -83,8 +87,14 @@ export const GetDocMetadataArgsSchema = z.object({
|
|
|
83
87
|
});
|
|
84
88
|
export const PatchDocArgsSchema = z.object({
|
|
85
89
|
path: z.string().describe("Path of the documentation entry to patch, e.g. 'backend/auth/authentication'"),
|
|
86
|
-
search: z.string().describe("Exact text to find in the current content"),
|
|
87
|
-
|
|
90
|
+
search: z.string().optional().describe("Exact text to find in the current content"),
|
|
91
|
+
search_file: z.string().optional().describe("Absolute path to a text file whose content will be used as the search text. Use this to reuse a previously persisted draft."),
|
|
92
|
+
replacement: z.string().optional().describe("Text to replace it with"),
|
|
93
|
+
replacement_file: z.string().optional().describe("Absolute path to a text file whose content will be used as the replacement. Use this to reuse a previously persisted draft."),
|
|
94
|
+
}).refine(d => d.search || d.search_file, {
|
|
95
|
+
message: "Either search or search_file is required",
|
|
96
|
+
}).refine(d => d.replacement || d.replacement_file, {
|
|
97
|
+
message: "Either replacement or replacement_file is required",
|
|
88
98
|
});
|
|
89
99
|
export const CheckLinksArgsSchema = z.object({
|
|
90
100
|
project: z.string().optional().describe("Project to check (defaults to current project)"),
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TrackedFile, TrackedFileInput } from "../../shared/schemas/doc.schema.js";
|
|
2
|
+
export interface CodeRefResult {
|
|
3
|
+
content: string;
|
|
4
|
+
additionalTrackedFiles: TrackedFileInput[];
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveCodeRefs(content: string): CodeRefResult;
|
|
7
|
+
export declare function snapshotTrackedFiles(inputs: TrackedFileInput[]): TrackedFile[];
|
|
8
|
+
/**
|
|
9
|
+
* Merges snapshotted tracked files into an existing list.
|
|
10
|
+
* - If a file is not yet tracked: appended.
|
|
11
|
+
* - If a file is already tracked with sections and the new entry also has sections: sections are merged.
|
|
12
|
+
* - Otherwise (whole-file tracking involved): the existing entry is replaced.
|
|
13
|
+
*/
|
|
14
|
+
export declare function mergeTrackedFiles(existing: TrackedFile[], additions: TrackedFileInput[]): TrackedFile[];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { snapshotSection } from "./section-tracker.js";
|
|
5
|
+
const EXT_TO_LANG = {
|
|
6
|
+
ts: "typescript",
|
|
7
|
+
tsx: "tsx",
|
|
8
|
+
js: "javascript",
|
|
9
|
+
jsx: "jsx",
|
|
10
|
+
py: "python",
|
|
11
|
+
css: "css",
|
|
12
|
+
scss: "scss",
|
|
13
|
+
json: "json",
|
|
14
|
+
md: "markdown",
|
|
15
|
+
html: "html",
|
|
16
|
+
sh: "bash",
|
|
17
|
+
yml: "yaml",
|
|
18
|
+
yaml: "yaml",
|
|
19
|
+
sql: "sql",
|
|
20
|
+
rs: "rust",
|
|
21
|
+
go: "go",
|
|
22
|
+
java: "java",
|
|
23
|
+
cs: "csharp",
|
|
24
|
+
};
|
|
25
|
+
// Syntax: [copy-from-codebase|/absolute/path/to/file.ts|10|50]
|
|
26
|
+
// Using | as separator to avoid conflicts with Windows drive letters (C:\...)
|
|
27
|
+
const CODE_REF_REGEX = /\[copy-from-codebase\|([^|]+)\|(\d+)\|(\d+)\]/g;
|
|
28
|
+
function computeFileHash(absolutePath) {
|
|
29
|
+
return crypto.createHash("sha256").update(fs.readFileSync(absolutePath)).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
export function resolveCodeRefs(content) {
|
|
32
|
+
const additionalTrackedFiles = [];
|
|
33
|
+
const resolved = content.replace(CODE_REF_REGEX, (match, filePath, startStr, endStr) => {
|
|
34
|
+
const start = parseInt(startStr, 10);
|
|
35
|
+
const end = parseInt(endStr, 10);
|
|
36
|
+
if (!fs.existsSync(filePath)) {
|
|
37
|
+
console.warn(`[copy-from-codebase] File not found: ${filePath}`);
|
|
38
|
+
return match;
|
|
39
|
+
}
|
|
40
|
+
const lines = fs.readFileSync(filePath, "utf-8").split("\n");
|
|
41
|
+
const extracted = lines.slice(start - 1, end).join("\n");
|
|
42
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
43
|
+
const lang = EXT_TO_LANG[ext] ?? "";
|
|
44
|
+
additionalTrackedFiles.push({ path: filePath, lines: [{ start, end }] });
|
|
45
|
+
return `\`\`\`${lang}\n${extracted}\n\`\`\``;
|
|
46
|
+
});
|
|
47
|
+
return { content: resolved, additionalTrackedFiles };
|
|
48
|
+
}
|
|
49
|
+
export function snapshotTrackedFiles(inputs) {
|
|
50
|
+
const byPath = new Map();
|
|
51
|
+
for (const input of inputs) {
|
|
52
|
+
const fp = typeof input === "string" ? input : input.path;
|
|
53
|
+
const absolutePath = path.resolve(process.cwd(), fp);
|
|
54
|
+
const mtime = fs.existsSync(absolutePath)
|
|
55
|
+
? fs.statSync(absolutePath).mtime.toISOString()
|
|
56
|
+
: new Date().toISOString();
|
|
57
|
+
const isWhole = typeof input === "string" || input.lines.length === 0;
|
|
58
|
+
const existing = byPath.get(absolutePath);
|
|
59
|
+
if (isWhole) {
|
|
60
|
+
byPath.set(absolutePath, { mtime, lineRanges: [], wholeFile: true });
|
|
61
|
+
}
|
|
62
|
+
else if (existing) {
|
|
63
|
+
if (!existing.wholeFile) {
|
|
64
|
+
existing.lineRanges.push(...input.lines);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
byPath.set(absolutePath, {
|
|
69
|
+
mtime,
|
|
70
|
+
lineRanges: [...input.lines],
|
|
71
|
+
wholeFile: false,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const tracked = [];
|
|
76
|
+
for (const [absolutePath, { mtime, lineRanges, wholeFile }] of byPath) {
|
|
77
|
+
if (!wholeFile && lineRanges.length > 0 && fs.existsSync(absolutePath)) {
|
|
78
|
+
const sections = lineRanges
|
|
79
|
+
.filter(({ start, end }) => start <= end)
|
|
80
|
+
.map(({ start, end }) => snapshotSection(absolutePath, start, end));
|
|
81
|
+
tracked.push({ path: absolutePath, lastModifiedAt: mtime, sections });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const contentHash = fs.existsSync(absolutePath) ? computeFileHash(absolutePath) : undefined;
|
|
85
|
+
tracked.push({ path: absolutePath, lastModifiedAt: mtime, ...(contentHash !== undefined && { contentHash }) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return tracked;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Merges snapshotted tracked files into an existing list.
|
|
92
|
+
* - If a file is not yet tracked: appended.
|
|
93
|
+
* - If a file is already tracked with sections and the new entry also has sections: sections are merged.
|
|
94
|
+
* - Otherwise (whole-file tracking involved): the existing entry is replaced.
|
|
95
|
+
*/
|
|
96
|
+
export function mergeTrackedFiles(existing, additions) {
|
|
97
|
+
if (additions.length === 0)
|
|
98
|
+
return existing;
|
|
99
|
+
const snapshotted = snapshotTrackedFiles(additions);
|
|
100
|
+
const result = [...existing];
|
|
101
|
+
for (const newTf of snapshotted) {
|
|
102
|
+
const idx = result.findIndex(tf => tf.path === newTf.path);
|
|
103
|
+
if (idx < 0) {
|
|
104
|
+
result.push(newTf);
|
|
105
|
+
}
|
|
106
|
+
else if (newTf.sections && result[idx].sections) {
|
|
107
|
+
result[idx] = {
|
|
108
|
+
...result[idx],
|
|
109
|
+
sections: [...result[idx].sections, ...newTf.sections],
|
|
110
|
+
lastModifiedAt: newTf.lastModifiedAt,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
result[idx] = newTf;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export function persistDraft(dataDir, label, content) {
|
|
4
|
+
const draftsDir = path.join(dataDir, "errors-tmp-files");
|
|
5
|
+
fs.mkdirSync(draftsDir, { recursive: true });
|
|
6
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
7
|
+
const filename = `${timestamp}-${label}.txt`;
|
|
8
|
+
const filePath = path.join(draftsDir, filename);
|
|
9
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
10
|
+
return filePath;
|
|
11
|
+
}
|
|
12
|
+
export function readDraft(filePath) {
|
|
13
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
14
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -620,7 +620,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
620
620
|
type: "object",
|
|
621
621
|
properties: {
|
|
622
622
|
title: { type: "string", description: "Title of the documentation entry" },
|
|
623
|
-
content: { type: "string", description: "Documentation content in markdown. To link to another document inline, use the syntax [text](doc://target-doc-id) — the viewer will navigate to that document on click. To link to a heading within the same document, use [text](#heading-slug). Use check_links to validate both types. For code blocks, always specify a language identifier for syntax highlighting, e.g. ```typescript, ```python, ```bash, ```json, ```yaml, ```sql." },
|
|
623
|
+
content: { type: "string", description: "Documentation content in markdown. To link to another document inline, use the syntax [text](doc://target-doc-id) — the viewer will navigate to that document on click. To link to a heading within the same document, use [text](#heading-slug). Use check_links to validate both types. For code blocks, always specify a language identifier for syntax highlighting, e.g. ```typescript, ```python, ```bash, ```json, ```yaml, ```sql. To embed a code excerpt directly from a source file, use [copy-from-codebase|/absolute/path/to/file.ts|startLine|endLine] — the tag is replaced at save time by a fenced code block with automatic language detection, and the file+range is automatically added to tracked_files. IMPORTANT: always read the source file first to identify the exact line numbers before writing the tag." },
|
|
624
|
+
content_file: { type: "string", description: "Absolute path to a text file whose content will be used instead of the content parameter. Use this to reuse a previously persisted draft (path is returned in the error message when a previous call failed)." },
|
|
624
625
|
tracked_files: {
|
|
625
626
|
type: "array",
|
|
626
627
|
items: {
|
|
@@ -664,7 +665,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
664
665
|
},
|
|
665
666
|
project: { type: "string", description: "Project to create the doc in (defaults to current project if omitted). If the project name does not exist yet, it will be created automatically." },
|
|
666
667
|
},
|
|
667
|
-
required: ["title"
|
|
668
|
+
required: ["title"],
|
|
668
669
|
},
|
|
669
670
|
},
|
|
670
671
|
{
|
|
@@ -699,7 +700,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
699
700
|
type: "string",
|
|
700
701
|
description: "New display title stored in frontmatter. Does not rename the file or change the document path — use move_doc to relocate or rename on disk.",
|
|
701
702
|
},
|
|
702
|
-
content: { type: "string", description: "New content (replaces existing). Supports [text](doc://target-doc-path) to link inline to other documents, and [text](#heading-slug) to link to headings within this document. For code blocks, always specify a language identifier for syntax highlighting, e.g. ```typescript, ```python, ```bash, ```json, ```yaml, ```sql." },
|
|
703
|
+
content: { type: "string", description: "New content (replaces existing). Supports [text](doc://target-doc-path) to link inline to other documents, and [text](#heading-slug) to link to headings within this document. For code blocks, always specify a language identifier for syntax highlighting, e.g. ```typescript, ```python, ```bash, ```json, ```yaml, ```sql. To embed a code excerpt directly from a source file, use [copy-from-codebase|/absolute/path/to/file.ts|startLine|endLine] — the tag is replaced at save time by a fenced code block with automatic language detection, and the file+range is automatically added to tracked_files. IMPORTANT: always read the source file first to identify the exact line numbers before writing the tag." },
|
|
704
|
+
content_file: { type: "string", description: "Absolute path to a text file whose content will be used instead of the content parameter. Use this to reuse a previously persisted draft (path is returned in the error message when a previous call failed)." },
|
|
703
705
|
tracked_files: {
|
|
704
706
|
type: "array",
|
|
705
707
|
items: {
|
|
@@ -838,9 +840,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
838
840
|
properties: {
|
|
839
841
|
path: { type: "string", description: "Path of the documentation entry to patch, e.g. 'backend/auth/authentication'" },
|
|
840
842
|
search: { type: "string", description: "Exact text to find in the current content" },
|
|
841
|
-
|
|
843
|
+
search_file: { type: "string", description: "Absolute path to a text file whose content will be used as the search text. Use this to reuse a previously persisted draft (path is returned in the error message when a previous call failed)." },
|
|
844
|
+
replacement: { type: "string", description: "Text to replace it with. Supports [copy-from-codebase|/absolute/path/to/file.ts|startLine|endLine] — the tag is replaced at save time by a fenced code block, and the file+range is automatically added to tracked_files. IMPORTANT: always read the source file first to identify the exact line numbers before writing the tag." },
|
|
845
|
+
replacement_file: { type: "string", description: "Absolute path to a text file whose content will be used as the replacement. Use this to reuse a previously persisted draft (path is returned in the error message when a previous call failed)." },
|
|
842
846
|
},
|
|
843
|
-
required: ["path", "search"
|
|
847
|
+
required: ["path", "search"],
|
|
844
848
|
},
|
|
845
849
|
},
|
|
846
850
|
{
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
2
|
import { CreateDocArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { persistDraft } from "../core/draft-store.js";
|
|
4
|
+
import { resolveCodeRefs, snapshotTrackedFiles } from "../core/code-ref-resolver.js";
|
|
6
5
|
export function slugify(title) {
|
|
7
6
|
return title
|
|
8
7
|
.normalize("NFD")
|
|
@@ -11,31 +10,14 @@ export function slugify(title) {
|
|
|
11
10
|
.replace(/[^a-z0-9]+/g, "-")
|
|
12
11
|
.replace(/^-|-$/g, "");
|
|
13
12
|
}
|
|
14
|
-
function snapshotTrackedFiles(inputs) {
|
|
15
|
-
const tracked = [];
|
|
16
|
-
for (const input of inputs) {
|
|
17
|
-
const fp = typeof input === "string" ? input : input.path;
|
|
18
|
-
const absolutePath = path.resolve(process.cwd(), fp);
|
|
19
|
-
const mtime = fs.existsSync(absolutePath)
|
|
20
|
-
? fs.statSync(absolutePath).mtime.toISOString()
|
|
21
|
-
: new Date().toISOString();
|
|
22
|
-
if (typeof input !== "string" && input.lines.length > 0 && fs.existsSync(absolutePath)) {
|
|
23
|
-
const sections = input.lines
|
|
24
|
-
.filter(({ start, end }) => start <= end)
|
|
25
|
-
.map(({ start, end }) => snapshotSection(absolutePath, start, end));
|
|
26
|
-
tracked.push({ path: absolutePath, lastModifiedAt: mtime, sections });
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
const contentHash = fs.existsSync(absolutePath) ? computeFileHash(absolutePath) : undefined;
|
|
30
|
-
tracked.push({ path: absolutePath, lastModifiedAt: mtime, ...(contentHash !== undefined && { contentHash }) });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return tracked;
|
|
34
|
-
}
|
|
35
13
|
export async function createDoc(storageEngine, embeddingEngine, args) {
|
|
36
14
|
try {
|
|
37
15
|
const validated = CreateDocArgsSchema.parse(args);
|
|
38
16
|
const { project } = (args ?? {});
|
|
17
|
+
const rawContent = validated.content_file
|
|
18
|
+
? fs.readFileSync(validated.content_file, "utf-8")
|
|
19
|
+
: validated.content;
|
|
20
|
+
const { content, additionalTrackedFiles } = resolveCodeRefs(rawContent);
|
|
39
21
|
const slug = slugify(validated.title);
|
|
40
22
|
const id = validated.folder ? `${validated.folder}/${slug}` : slug;
|
|
41
23
|
const now = new Date().toISOString();
|
|
@@ -49,12 +31,16 @@ export async function createDoc(storageEngine, embeddingEngine, args) {
|
|
|
49
31
|
if (project) {
|
|
50
32
|
storageEngine.ensureProject(project);
|
|
51
33
|
}
|
|
52
|
-
const
|
|
34
|
+
const allTrackedInputs = [
|
|
35
|
+
...(validated.tracked_files ?? []),
|
|
36
|
+
...additionalTrackedFiles,
|
|
37
|
+
];
|
|
38
|
+
const trackedFiles = allTrackedInputs.length > 0 ? snapshotTrackedFiles(allTrackedInputs) : [];
|
|
53
39
|
const folder = validated.folder;
|
|
54
40
|
const doc = {
|
|
55
41
|
id,
|
|
56
42
|
title: validated.title,
|
|
57
|
-
content
|
|
43
|
+
content,
|
|
58
44
|
tracked_files: trackedFiles,
|
|
59
45
|
tags: validated.tags ?? [],
|
|
60
46
|
folder,
|
|
@@ -65,11 +51,14 @@ export async function createDoc(storageEngine, embeddingEngine, args) {
|
|
|
65
51
|
const resolvedProject = project ?? storageEngine.getConfig().currentProject;
|
|
66
52
|
await storageEngine.saveDoc(doc, resolvedProject, "create");
|
|
67
53
|
if (embeddingEngine.isConfigured()) {
|
|
68
|
-
const embedding = await embeddingEngine.generateEmbedding(
|
|
54
|
+
const embedding = await embeddingEngine.generateEmbedding(content);
|
|
69
55
|
if (embedding) {
|
|
70
56
|
await embeddingEngine.saveEmbedding(id, embedding, resolvedProject);
|
|
71
57
|
}
|
|
72
58
|
}
|
|
59
|
+
if (validated.content_file) {
|
|
60
|
+
fs.unlinkSync(validated.content_file);
|
|
61
|
+
}
|
|
73
62
|
return {
|
|
74
63
|
content: [
|
|
75
64
|
{
|
|
@@ -81,6 +70,14 @@ export async function createDoc(storageEngine, embeddingEngine, args) {
|
|
|
81
70
|
}
|
|
82
71
|
catch (err) {
|
|
83
72
|
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
const rawContent = args?.content;
|
|
74
|
+
if (rawContent) {
|
|
75
|
+
const draftPath = persistDraft(storageEngine.getDataDir(), "create-doc", rawContent);
|
|
76
|
+
return {
|
|
77
|
+
isError: true,
|
|
78
|
+
content: [{ type: "text", text: `Error creating doc: ${message}\n\n⚠️ The content has been saved to: ${draftPath}\nYou can reuse it directly via the content_file parameter instead of rewriting it.` }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
84
81
|
return { isError: true, content: [{ type: "text", text: `Error creating doc: ${message}` }] };
|
|
85
82
|
}
|
|
86
83
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import fs from "fs";
|
|
1
2
|
import { PatchDocArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
3
|
+
import { persistDraft } from "../core/draft-store.js";
|
|
4
|
+
import { resolveCodeRefs, mergeTrackedFiles } from "../core/code-ref-resolver.js";
|
|
2
5
|
export async function patchDoc(storageEngine, embeddingEngine, args) {
|
|
3
6
|
try {
|
|
4
7
|
const validated = PatchDocArgsSchema.parse(args);
|
|
@@ -7,7 +10,14 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
|
|
|
7
10
|
return { isError: true, content: [{ type: "text", text: `Documentation entry "${validated.path}" not found in any project.` }] };
|
|
8
11
|
}
|
|
9
12
|
const { doc, project: resolvedProject } = resolved;
|
|
10
|
-
|
|
13
|
+
const search = validated.search_file
|
|
14
|
+
? fs.readFileSync(validated.search_file, "utf-8")
|
|
15
|
+
: validated.search;
|
|
16
|
+
const rawReplacement = validated.replacement_file
|
|
17
|
+
? fs.readFileSync(validated.replacement_file, "utf-8")
|
|
18
|
+
: validated.replacement;
|
|
19
|
+
const { content: replacement, additionalTrackedFiles } = resolveCodeRefs(rawReplacement);
|
|
20
|
+
if (!doc.content.includes(search)) {
|
|
11
21
|
const preview = doc.content.slice(0, 300).replace(/\n/g, "↵");
|
|
12
22
|
return {
|
|
13
23
|
isError: true,
|
|
@@ -17,7 +27,8 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
|
|
|
17
27
|
}],
|
|
18
28
|
};
|
|
19
29
|
}
|
|
20
|
-
doc.content = doc.content.replace(
|
|
30
|
+
doc.content = doc.content.replace(search, replacement);
|
|
31
|
+
doc.tracked_files = mergeTrackedFiles(doc.tracked_files, additionalTrackedFiles);
|
|
21
32
|
doc.updatedAt = new Date().toISOString();
|
|
22
33
|
await storageEngine.saveDoc(doc, resolvedProject, "patch");
|
|
23
34
|
if (embeddingEngine.isConfigured()) {
|
|
@@ -26,12 +37,33 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
|
|
|
26
37
|
await embeddingEngine.saveEmbedding(validated.path, embedding, resolvedProject);
|
|
27
38
|
}
|
|
28
39
|
}
|
|
40
|
+
if (validated.search_file)
|
|
41
|
+
fs.unlinkSync(validated.search_file);
|
|
42
|
+
if (validated.replacement_file)
|
|
43
|
+
fs.unlinkSync(validated.replacement_file);
|
|
29
44
|
return {
|
|
30
45
|
content: [{ type: "text", text: `Documentation entry "${validated.path}" patched successfully.` }],
|
|
31
46
|
};
|
|
32
47
|
}
|
|
33
48
|
catch (err) {
|
|
34
49
|
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
const rawSearch = args?.search;
|
|
51
|
+
const rawReplacement = args?.replacement;
|
|
52
|
+
if (rawSearch || rawReplacement) {
|
|
53
|
+
const parts = [];
|
|
54
|
+
if (rawSearch) {
|
|
55
|
+
const searchPath = persistDraft(storageEngine.getDataDir(), "patch-doc-search", rawSearch);
|
|
56
|
+
parts.push(`search → ${searchPath}`);
|
|
57
|
+
}
|
|
58
|
+
if (rawReplacement) {
|
|
59
|
+
const replacementPath = persistDraft(storageEngine.getDataDir(), "patch-doc-replacement", rawReplacement);
|
|
60
|
+
parts.push(`replacement → ${replacementPath}`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
isError: true,
|
|
64
|
+
content: [{ type: "text", text: `Error patching doc: ${message}\n\n⚠️ The following content has been saved:\n${parts.join("\n")}\nYou can reuse them directly via the search_file / replacement_file parameters instead of rewriting them.` }],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
35
67
|
return { isError: true, content: [{ type: "text", text: `Error patching doc: ${message}` }] };
|
|
36
68
|
}
|
|
37
69
|
}
|
|
@@ -1,29 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
2
|
import { UpdateDocArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
function snapshotTrackedFiles(inputs) {
|
|
7
|
-
const tracked = [];
|
|
8
|
-
for (const input of inputs) {
|
|
9
|
-
const fp = typeof input === "string" ? input : input.path;
|
|
10
|
-
const absolutePath = path.resolve(process.cwd(), fp);
|
|
11
|
-
const mtime = fs.existsSync(absolutePath)
|
|
12
|
-
? fs.statSync(absolutePath).mtime.toISOString()
|
|
13
|
-
: new Date().toISOString();
|
|
14
|
-
if (typeof input !== "string" && input.lines.length > 0 && fs.existsSync(absolutePath)) {
|
|
15
|
-
const sections = input.lines
|
|
16
|
-
.filter(({ start, end }) => start <= end)
|
|
17
|
-
.map(({ start, end }) => snapshotSection(absolutePath, start, end));
|
|
18
|
-
tracked.push({ path: absolutePath, lastModifiedAt: mtime, sections });
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
const contentHash = fs.existsSync(absolutePath) ? computeFileHash(absolutePath) : undefined;
|
|
22
|
-
tracked.push({ path: absolutePath, lastModifiedAt: mtime, ...(contentHash !== undefined && { contentHash }) });
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return tracked;
|
|
26
|
-
}
|
|
3
|
+
import { persistDraft } from "../core/draft-store.js";
|
|
4
|
+
import { resolveCodeRefs, snapshotTrackedFiles, mergeTrackedFiles } from "../core/code-ref-resolver.js";
|
|
27
5
|
export async function updateDoc(storageEngine, embeddingEngine, args) {
|
|
28
6
|
try {
|
|
29
7
|
const validated = UpdateDocArgsSchema.parse(args);
|
|
@@ -35,11 +13,25 @@ export async function updateDoc(storageEngine, embeddingEngine, args) {
|
|
|
35
13
|
if (validated.title !== undefined) {
|
|
36
14
|
doc.title = validated.title;
|
|
37
15
|
}
|
|
38
|
-
|
|
39
|
-
|
|
16
|
+
let additionalTrackedFiles = [];
|
|
17
|
+
if (validated.content_file !== undefined) {
|
|
18
|
+
const { content, additionalTrackedFiles: refs } = resolveCodeRefs(fs.readFileSync(validated.content_file, "utf-8"));
|
|
19
|
+
doc.content = content;
|
|
20
|
+
additionalTrackedFiles = refs;
|
|
21
|
+
}
|
|
22
|
+
else if (validated.content !== undefined) {
|
|
23
|
+
const { content, additionalTrackedFiles: refs } = resolveCodeRefs(validated.content);
|
|
24
|
+
doc.content = content;
|
|
25
|
+
additionalTrackedFiles = refs;
|
|
40
26
|
}
|
|
41
27
|
if (validated.tracked_files !== undefined) {
|
|
42
|
-
doc.tracked_files = snapshotTrackedFiles(
|
|
28
|
+
doc.tracked_files = snapshotTrackedFiles([
|
|
29
|
+
...validated.tracked_files,
|
|
30
|
+
...additionalTrackedFiles,
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
doc.tracked_files = mergeTrackedFiles(doc.tracked_files, additionalTrackedFiles);
|
|
43
35
|
}
|
|
44
36
|
if (validated.tags !== undefined) {
|
|
45
37
|
doc.tags = validated.tags;
|
|
@@ -49,17 +41,22 @@ export async function updateDoc(storageEngine, embeddingEngine, args) {
|
|
|
49
41
|
}
|
|
50
42
|
doc.updatedAt = new Date().toISOString();
|
|
51
43
|
await storageEngine.saveDoc(doc, resolvedProject, "update");
|
|
52
|
-
if (validated.content !== undefined && embeddingEngine.isConfigured()) {
|
|
53
|
-
const embedding = await embeddingEngine.generateEmbedding(
|
|
44
|
+
if ((validated.content !== undefined || validated.content_file !== undefined) && embeddingEngine.isConfigured()) {
|
|
45
|
+
const embedding = await embeddingEngine.generateEmbedding(doc.content);
|
|
54
46
|
if (embedding) {
|
|
55
47
|
await embeddingEngine.saveEmbedding(validated.path, embedding, resolvedProject);
|
|
56
48
|
}
|
|
57
49
|
}
|
|
50
|
+
if (validated.content_file !== undefined) {
|
|
51
|
+
fs.unlinkSync(validated.content_file);
|
|
52
|
+
}
|
|
58
53
|
const changes = [];
|
|
59
54
|
if (validated.title !== undefined)
|
|
60
55
|
changes.push(`title (${doc.title})`);
|
|
61
56
|
if (validated.content !== undefined)
|
|
62
57
|
changes.push("content");
|
|
58
|
+
if (validated.content_file !== undefined)
|
|
59
|
+
changes.push("content (from file)");
|
|
63
60
|
if (validated.tracked_files !== undefined)
|
|
64
61
|
changes.push(`tracked_files (${doc.tracked_files.length} files)`);
|
|
65
62
|
if (validated.tags !== undefined)
|
|
@@ -74,6 +71,14 @@ export async function updateDoc(storageEngine, embeddingEngine, args) {
|
|
|
74
71
|
}
|
|
75
72
|
catch (err) {
|
|
76
73
|
const message = err instanceof Error ? err.message : String(err);
|
|
74
|
+
const rawContent = args?.content;
|
|
75
|
+
if (rawContent) {
|
|
76
|
+
const draftPath = persistDraft(storageEngine.getDataDir(), "update-doc", rawContent);
|
|
77
|
+
return {
|
|
78
|
+
isError: true,
|
|
79
|
+
content: [{ type: "text", text: `Error updating doc: ${message}\n\n⚠️ The content has been saved to: ${draftPath}\nYou can reuse it directly via the content_file parameter instead of rewriting it.` }],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
77
82
|
return { isError: true, content: [{ type: "text", text: `Error updating doc: ${message}` }] };
|
|
78
83
|
}
|
|
79
84
|
}
|
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.55";
|
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.55';
|