@imenam/mcp-documentor 1.0.82 → 1.0.83

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.
@@ -99,11 +99,18 @@ export declare const FreshnessReportSchema: z.ZodObject<{
99
99
  sourceError: z.ZodOptional<z.ZodString>;
100
100
  }, z.core.$strip>;
101
101
  export type FreshnessReport = z.infer<typeof FreshnessReportSchema>;
102
+ /**
103
+ * Descriptions des formes alternatives d'un texte (`<x>_file_path`, `<x>_file_content`,
104
+ * `<x>_draft`). `<x>_file_path` / `<x>_file_content` forment la paire que le relais de
105
+ * mcp-http-gateway reconnaît : il lit le fichier sur la machine de l'agent et en envoie
106
+ * le contenu.
107
+ */
108
+ export declare function textSourceDescriptions(prefix: string, what: string): {
109
+ file_path: string;
110
+ file_content: string;
111
+ draft: string;
112
+ };
102
113
  export declare const CreateDocArgsSchema: z.ZodObject<{
103
- project: z.ZodString;
104
- title: z.ZodString;
105
- content: z.ZodOptional<z.ZodString>;
106
- content_file: z.ZodOptional<z.ZodString>;
107
114
  tracked_files: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
108
115
  path: z.ZodString;
109
116
  lines: z.ZodArray<z.ZodObject<{
@@ -114,13 +121,14 @@ export declare const CreateDocArgsSchema: z.ZodObject<{
114
121
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
115
122
  folder: z.ZodOptional<z.ZodString>;
116
123
  related_docs: z.ZodOptional<z.ZodArray<z.ZodString>>;
117
- }, z.core.$strip>;
118
- export declare const UpdateDocArgsSchema: z.ZodObject<{
119
- path: z.ZodString;
124
+ content_file_path: z.ZodOptional<z.ZodString>;
125
+ content_file_content: z.ZodOptional<z.ZodString>;
126
+ content_draft: z.ZodOptional<z.ZodString>;
120
127
  project: z.ZodString;
121
- title: z.ZodOptional<z.ZodString>;
128
+ title: z.ZodString;
122
129
  content: z.ZodOptional<z.ZodString>;
123
- content_file: z.ZodOptional<z.ZodString>;
130
+ }, z.core.$strip>;
131
+ export declare const UpdateDocArgsSchema: z.ZodObject<{
124
132
  tracked_files: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
125
133
  path: z.ZodString;
126
134
  lines: z.ZodArray<z.ZodObject<{
@@ -130,6 +138,13 @@ export declare const UpdateDocArgsSchema: z.ZodObject<{
130
138
  }, z.core.$strip>]>>>;
131
139
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
132
140
  related_docs: z.ZodOptional<z.ZodArray<z.ZodString>>;
141
+ content_file_path: z.ZodOptional<z.ZodString>;
142
+ content_file_content: z.ZodOptional<z.ZodString>;
143
+ content_draft: z.ZodOptional<z.ZodString>;
144
+ path: z.ZodString;
145
+ project: z.ZodString;
146
+ title: z.ZodOptional<z.ZodString>;
147
+ content: z.ZodOptional<z.ZodString>;
133
148
  }, z.core.$strip>;
134
149
  export declare const GetDocArgsSchema: z.ZodObject<{
135
150
  path: z.ZodString;
@@ -162,12 +177,16 @@ export declare const GetDocMetadataArgsSchema: z.ZodObject<{
162
177
  project: z.ZodString;
163
178
  }, z.core.$strip>;
164
179
  export declare const PatchDocArgsSchema: z.ZodObject<{
180
+ replacement_file_path: z.ZodOptional<z.ZodString>;
181
+ replacement_file_content: z.ZodOptional<z.ZodString>;
182
+ replacement_draft: z.ZodOptional<z.ZodString>;
183
+ replacement: z.ZodOptional<z.ZodString>;
184
+ search_file_path: z.ZodOptional<z.ZodString>;
185
+ search_file_content: z.ZodOptional<z.ZodString>;
186
+ search_draft: z.ZodOptional<z.ZodString>;
165
187
  path: z.ZodString;
166
188
  project: z.ZodString;
167
189
  search: z.ZodOptional<z.ZodString>;
168
- search_file: z.ZodOptional<z.ZodString>;
169
- replacement: z.ZodOptional<z.ZodString>;
170
- replacement_file: z.ZodOptional<z.ZodString>;
171
190
  }, z.core.$strip>;
172
191
  export declare const CheckLinksArgsSchema: z.ZodObject<{
173
192
  project: z.ZodString;
@@ -70,24 +70,45 @@ const requiredProject = (action) => z
70
70
  .string()
71
71
  .min(1)
72
72
  .describe(`Project the documentation entry belongs to. Mandatory — ${action} is never resolved across projects, and never falls back to the current project.`);
73
+ /**
74
+ * Descriptions des formes alternatives d'un texte (`<x>_file_path`, `<x>_file_content`,
75
+ * `<x>_draft`). `<x>_file_path` / `<x>_file_content` forment la paire que le relais de
76
+ * mcp-http-gateway reconnaît : il lit le fichier sur la machine de l'agent et en envoie
77
+ * le contenu.
78
+ */
79
+ export function textSourceDescriptions(prefix, what) {
80
+ return {
81
+ file_path: `Absolute path to a UTF-8 text file on the agent machine whose content is used as ${what}, instead of ${prefix}. The file is left untouched.`,
82
+ file_content: "Filled in by the mcp-http-gateway relay, never by the agent: the base64 content of the file read on the agent machine.",
83
+ draft: `Name of a draft saved by the server after a failed call (the name is given in the error message), used as ${what} instead of ${prefix}. The draft is deleted once the call succeeds.`,
84
+ };
85
+ }
86
+ function textSourceFields(prefix, what) {
87
+ const d = textSourceDescriptions(prefix, what);
88
+ return {
89
+ [`${prefix}_file_path`]: z.string().optional().describe(d.file_path),
90
+ [`${prefix}_file_content`]: z.string().optional().describe(d.file_content),
91
+ [`${prefix}_draft`]: z.string().optional().describe(d.draft),
92
+ };
93
+ }
73
94
  export const CreateDocArgsSchema = z.object({
74
95
  project: requiredProject("creation").describe("Project to create the documentation entry in. Mandatory — never defaulted to the current project. The project is created automatically if it does not exist yet."),
75
96
  title: z.string().describe("Title of the documentation entry"),
76
97
  content: z.string().optional().describe("Documentation content in markdown"),
77
- 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."),
98
+ ...textSourceFields("content", "the content"),
78
99
  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. Provide absolute paths, or paths relative to the project's code root; they are stored relative to that root so the docs stay portable across machines. Tracked files must exist at the project's code source — which is the local working copy by default, or a branch of a GitHub repository when one is configured for the project (check_freshness reports which)."),
79
100
  tags: z.array(z.string()).optional().describe("Tags for categorization"),
80
101
  folder: z.string().optional().describe("Folder path for organization, e.g. 'frontend/components'"),
81
102
  related_docs: z.array(z.string()).optional().describe("IDs of related documentation entries"),
82
- }).refine(d => d.content || d.content_file, {
83
- message: "Either content or content_file is required",
103
+ }).refine(d => d.content !== undefined || d.content_file_path !== undefined || d.content_file_content !== undefined || d.content_draft !== undefined, {
104
+ message: "One of content, content_file_path or content_draft is required",
84
105
  });
85
106
  export const UpdateDocArgsSchema = z.object({
86
107
  path: z.string().describe("Path of the documentation entry to update, e.g. 'backend/auth/authentication' or 'authentication' for root docs"),
87
108
  project: requiredProject("the update"),
88
109
  title: z.string().optional().describe("Title of the documentation entry"),
89
110
  content: z.string().optional().describe("New content (replaces existing)"),
90
- 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."),
111
+ ...textSourceFields("content", "the new content"),
91
112
  tracked_files: z.array(TrackedFileInputSchema).optional().describe("New list of tracked files (replaces existing). Files already tracked with the same scope keep their stored snapshot, so this never changes staleness — only refresh_file_snapshot acknowledges file changes. 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. Provide absolute paths, or paths relative to the project's code root."),
92
113
  tags: z.array(z.string()).optional().describe("New tags (replaces existing)"),
93
114
  related_docs: z.array(z.string()).optional().describe("Paths of related documentation entries (replaces existing)"),
@@ -131,13 +152,13 @@ export const PatchDocArgsSchema = z.object({
131
152
  path: z.string().describe("Path of the documentation entry to patch, e.g. 'backend/auth/authentication'"),
132
153
  project: requiredProject("the patch"),
133
154
  search: z.string().optional().describe("Exact text to find in the current content"),
134
- 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."),
155
+ ...textSourceFields("search", "the search text"),
135
156
  replacement: z.string().optional().describe("Text to replace it with"),
136
- 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."),
137
- }).refine(d => d.search || d.search_file, {
138
- message: "Either search or search_file is required",
139
- }).refine(d => d.replacement || d.replacement_file, {
140
- message: "Either replacement or replacement_file is required",
157
+ ...textSourceFields("replacement", "the replacement"),
158
+ }).refine(d => d.search !== undefined || d.search_file_path !== undefined || d.search_file_content !== undefined || d.search_draft !== undefined, {
159
+ message: "One of search, search_file_path or search_draft is required",
160
+ }).refine(d => d.replacement !== undefined || d.replacement_file_path !== undefined || d.replacement_file_content !== undefined || d.replacement_draft !== undefined, {
161
+ message: "One of replacement, replacement_file_path or replacement_draft is required",
141
162
  });
142
163
  export const CheckLinksArgsSchema = z.object({
143
164
  project: requiredProject("the link check").describe("Project whose entries are checked. Mandatory — links are never checked across projects, and there is no current-project fallback."),
@@ -1,2 +1,4 @@
1
+ /** Enregistre un brouillon et renvoie son nom. */
1
2
  export declare function persistDraft(dataDir: string, label: string, content: string): string;
2
- export declare function readDraft(filePath: string): string;
3
+ export declare function readDraft(dataDir: string, name: string): string;
4
+ export declare function deleteDraft(dataDir: string, name: string): void;
@@ -1,14 +1,34 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ /**
4
+ * Brouillons : le texte envoyé par un appel qui a échoué, conservé côté serveur pour être
5
+ * réutilisé par nom. Le nom seul circule, jamais le chemin : il désigne un fichier de la
6
+ * machine du serveur, que l'agent n'a pas forcément sous la main.
7
+ */
8
+ function draftsDir(dataDir) {
9
+ return path.join(dataDir, "errors-tmp-files");
10
+ }
11
+ function draftFile(dataDir, name) {
12
+ if (path.basename(name) !== name || name === "." || name === "..") {
13
+ throw new Error(`Invalid draft name: "${name}". Pass the name returned in the error message, without any path.`);
14
+ }
15
+ return path.join(draftsDir(dataDir), name);
16
+ }
17
+ /** Enregistre un brouillon et renvoie son nom. */
3
18
  export function persistDraft(dataDir, label, content) {
4
- const draftsDir = path.join(dataDir, "errors-tmp-files");
5
- fs.mkdirSync(draftsDir, { recursive: true });
19
+ fs.mkdirSync(draftsDir(dataDir), { recursive: true });
6
20
  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;
21
+ const name = `${timestamp}-${label}.txt`;
22
+ fs.writeFileSync(path.join(draftsDir(dataDir), name), content, "utf-8");
23
+ return name;
24
+ }
25
+ export function readDraft(dataDir, name) {
26
+ const file = draftFile(dataDir, name);
27
+ if (!fs.existsSync(file)) {
28
+ throw new Error(`Draft not found: "${name}".`);
29
+ }
30
+ return fs.readFileSync(file, "utf-8");
11
31
  }
12
- export function readDraft(filePath) {
13
- return fs.readFileSync(filePath, "utf-8");
32
+ export function deleteDraft(dataDir, name) {
33
+ fs.unlinkSync(draftFile(dataDir, name));
14
34
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Un texte fourni à un outil, sous l'une de trois formes exclusives :
3
+ * - en ligne (`inline`) ;
4
+ * - par un fichier de la machine de l'agent (`filePath`). Derrière mcp-http-gateway, le
5
+ * relais lit ce fichier et envoie son contenu en base64 dans `fileContent` ;
6
+ * - par le nom d'un brouillon conservé côté serveur (`draft`).
7
+ */
8
+ export interface TextInput {
9
+ inline?: string;
10
+ filePath?: string;
11
+ fileContent?: string;
12
+ draft?: string;
13
+ }
14
+ /** Noms des paramètres de l'outil, pour les messages d'erreur. */
15
+ export interface TextInputParams {
16
+ inline: string;
17
+ file: string;
18
+ draft: string;
19
+ }
20
+ export declare const CONTENT_PARAMS: TextInputParams;
21
+ export declare const SEARCH_PARAMS: TextInputParams;
22
+ export declare const REPLACEMENT_PARAMS: TextInputParams;
23
+ export interface ResolvedText {
24
+ text: string;
25
+ /** Nom du brouillon utilisé, à supprimer une fois l'opération réussie. */
26
+ draft?: string;
27
+ }
28
+ /** Renvoie le texte, ou `undefined` quand aucune forme n'est fournie. Le fichier de l'agent n'est jamais modifié. */
29
+ export declare function resolveTextInput(dataDir: string, input: TextInput, params: TextInputParams): ResolvedText | undefined;
@@ -0,0 +1,29 @@
1
+ import fs from "fs";
2
+ import { readDraft } from "./draft-store.js";
3
+ export const CONTENT_PARAMS = { inline: "content", file: "content_file_path", draft: "content_draft" };
4
+ export const SEARCH_PARAMS = { inline: "search", file: "search_file_path", draft: "search_draft" };
5
+ export const REPLACEMENT_PARAMS = { inline: "replacement", file: "replacement_file_path", draft: "replacement_draft" };
6
+ function countSources(input) {
7
+ const file = input.filePath !== undefined || input.fileContent !== undefined;
8
+ return [input.inline !== undefined, file, input.draft !== undefined].filter(Boolean).length;
9
+ }
10
+ /** Renvoie le texte, ou `undefined` quand aucune forme n'est fournie. Le fichier de l'agent n'est jamais modifié. */
11
+ export function resolveTextInput(dataDir, input, params) {
12
+ const count = countSources(input);
13
+ if (count === 0)
14
+ return undefined;
15
+ if (count > 1) {
16
+ throw new Error(`Provide only one of ${params.inline}, ${params.file} or ${params.draft}.`);
17
+ }
18
+ if (input.inline !== undefined)
19
+ return { text: input.inline };
20
+ if (input.draft !== undefined)
21
+ return { text: readDraft(dataDir, input.draft), draft: input.draft };
22
+ if (input.fileContent !== undefined) {
23
+ return { text: Buffer.from(input.fileContent.replace(/^data:[^;,]*;base64,/, ""), "base64").toString("utf-8") };
24
+ }
25
+ if (!fs.existsSync(input.filePath)) {
26
+ throw new Error(`File not found: ${input.filePath} (${params.file}).`);
27
+ }
28
+ return { text: fs.readFileSync(input.filePath, "utf-8") };
29
+ }
package/dist/src/index.js CHANGED
@@ -45,6 +45,7 @@ import { getImage } from "./tools/get-image.js";
45
45
  import { SyncEngine } from "./core/git-sync.js";
46
46
  import { syncStatus, pullDocs, pushDocs, syncDocs, resolveSyncConflict } from "./tools/sync.js";
47
47
  import { listConflicts, getConflictHunks, resolveConflictHunks } from "./tools/conflicts.js";
48
+ import { textSourceDescriptions, } from "../shared/schemas/doc.schema.js";
48
49
  import { isReadOnly, WRITE_TOOLS } from "../shared/read-only.js";
49
50
  const __filename = fileURLToPath(import.meta.url);
50
51
  const __dirname = path.dirname(__filename);
@@ -883,6 +884,15 @@ async function reconnectGui(args) {
883
884
  }
884
885
  server.setRequestHandler(ListToolsRequestSchema, async () => {
885
886
  const PROJECT_PARAM_DESCRIPTION = "Project the documentation entry belongs to. REQUIRED — never guessed, and never defaulted to the current project. Use list_projects or get_doc_tree if you are unsure which project holds the entry.";
887
+ /** Propriétés `<x>_file_path` / `<x>_file_content` / `<x>_draft` d'un texte, pour les schémas publiés. */
888
+ const textSourceProperties = (prefix, what) => {
889
+ const d = textSourceDescriptions(prefix, what);
890
+ return {
891
+ [`${prefix}_file_path`]: { type: "string", description: d.file_path },
892
+ [`${prefix}_file_content`]: { type: "string", description: d.file_content },
893
+ [`${prefix}_draft`]: { type: "string", description: d.draft },
894
+ };
895
+ };
886
896
  const tools = [
887
897
  {
888
898
  name: "create_doc",
@@ -892,7 +902,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
892
902
  properties: {
893
903
  title: { type: "string", description: "Title of the documentation entry" },
894
904
  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." },
895
- 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)." },
905
+ ...textSourceProperties("content", "the content"),
896
906
  tracked_files: {
897
907
  type: "array",
898
908
  items: {
@@ -973,7 +983,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
973
983
  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.",
974
984
  },
975
985
  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." },
976
- 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)." },
986
+ ...textSourceProperties("content", "the new content"),
977
987
  tracked_files: {
978
988
  type: "array",
979
989
  items: {
@@ -1105,12 +1115,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1105
1115
  properties: {
1106
1116
  path: { type: "string", description: "Path of the documentation entry to patch, e.g. 'backend/auth/authentication'" },
1107
1117
  search: { type: "string", description: "Exact text to find in the current content" },
1108
- 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)." },
1118
+ ...textSourceProperties("search", "the search text"),
1109
1119
  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." },
1110
- 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)." },
1120
+ ...textSourceProperties("replacement", "the replacement"),
1111
1121
  project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
1112
1122
  },
1113
- required: ["path", "search", "project"],
1123
+ required: ["path", "project"],
1114
1124
  },
1115
1125
  },
1116
1126
  {
@@ -1,6 +1,6 @@
1
- import fs from "fs";
2
1
  import { CreateDocArgsSchema } from "../../shared/schemas/doc.schema.js";
3
- import { persistDraft } from "../core/draft-store.js";
2
+ import { persistDraft, deleteDraft } from "../core/draft-store.js";
3
+ import { resolveTextInput, CONTENT_PARAMS } from "../core/text-input.js";
4
4
  import { resolveCodeRefs, snapshotTrackedFiles } from "../core/code-ref-resolver.js";
5
5
  export function slugify(title) {
6
6
  return title
@@ -14,10 +14,13 @@ export async function createDoc(storageEngine, embeddingEngine, args) {
14
14
  try {
15
15
  const validated = CreateDocArgsSchema.parse(args);
16
16
  const { project } = validated;
17
- const rawContent = validated.content_file
18
- ? fs.readFileSync(validated.content_file, "utf-8")
19
- : validated.content;
20
- const { content, additionalTrackedFiles } = await resolveCodeRefs(rawContent, project);
17
+ const source = resolveTextInput(storageEngine.getDataDir(), {
18
+ inline: validated.content,
19
+ filePath: validated.content_file_path,
20
+ fileContent: validated.content_file_content,
21
+ draft: validated.content_draft,
22
+ }, CONTENT_PARAMS);
23
+ const { content, additionalTrackedFiles } = await resolveCodeRefs(source.text, project);
21
24
  const slug = slugify(validated.title);
22
25
  const id = validated.folder ? `${validated.folder}/${slug}` : slug;
23
26
  const now = new Date().toISOString();
@@ -54,8 +57,8 @@ export async function createDoc(storageEngine, embeddingEngine, args) {
54
57
  await embeddingEngine.saveEmbedding(id, embedding, resolvedProject);
55
58
  }
56
59
  }
57
- if (validated.content_file) {
58
- fs.unlinkSync(validated.content_file);
60
+ if (source.draft) {
61
+ deleteDraft(storageEngine.getDataDir(), source.draft);
59
62
  }
60
63
  return {
61
64
  content: [
@@ -70,10 +73,10 @@ export async function createDoc(storageEngine, embeddingEngine, args) {
70
73
  const message = err instanceof Error ? err.message : String(err);
71
74
  const rawContent = args?.content;
72
75
  if (rawContent) {
73
- const draftPath = persistDraft(storageEngine.getDataDir(), "create-doc", rawContent);
76
+ const draft = persistDraft(storageEngine.getDataDir(), "create-doc", rawContent);
74
77
  return {
75
78
  isError: true,
76
- 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
+ content: [{ type: "text", text: `Error creating doc: ${message}\n\n⚠️ The content has been saved as draft "${draft}".\nYou can reuse it directly via the content_draft parameter instead of rewriting it.` }],
77
80
  };
78
81
  }
79
82
  return { isError: true, content: [{ type: "text", text: `Error creating doc: ${message}` }] };
@@ -1,6 +1,6 @@
1
- import fs from "fs";
2
1
  import { PatchDocArgsSchema } from "../../shared/schemas/doc.schema.js";
3
- import { persistDraft } from "../core/draft-store.js";
2
+ import { persistDraft, deleteDraft } from "../core/draft-store.js";
3
+ import { resolveTextInput, SEARCH_PARAMS, REPLACEMENT_PARAMS } from "../core/text-input.js";
4
4
  import { resolveCodeRefs, mergeTrackedFiles } from "../core/code-ref-resolver.js";
5
5
  import { resolveDocInProject, isScopeError, scopeErrorResult } from "../core/project-scope.js";
6
6
  export async function patchDoc(storageEngine, embeddingEngine, args) {
@@ -11,13 +11,21 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
11
11
  return scopeErrorResult(resolved);
12
12
  const { doc } = resolved;
13
13
  const resolvedProject = validated.project;
14
- const search = validated.search_file
15
- ? fs.readFileSync(validated.search_file, "utf-8")
16
- : validated.search;
17
- const rawReplacement = validated.replacement_file
18
- ? fs.readFileSync(validated.replacement_file, "utf-8")
19
- : validated.replacement;
20
- const { content: replacement, additionalTrackedFiles } = await resolveCodeRefs(rawReplacement, resolvedProject);
14
+ const dataDir = storageEngine.getDataDir();
15
+ const searchSource = resolveTextInput(dataDir, {
16
+ inline: validated.search,
17
+ filePath: validated.search_file_path,
18
+ fileContent: validated.search_file_content,
19
+ draft: validated.search_draft,
20
+ }, SEARCH_PARAMS);
21
+ const replacementSource = resolveTextInput(dataDir, {
22
+ inline: validated.replacement,
23
+ filePath: validated.replacement_file_path,
24
+ fileContent: validated.replacement_file_content,
25
+ draft: validated.replacement_draft,
26
+ }, REPLACEMENT_PARAMS);
27
+ const search = searchSource.text;
28
+ const { content: replacement, additionalTrackedFiles } = await resolveCodeRefs(replacementSource.text, resolvedProject);
21
29
  if (!doc.content.includes(search)) {
22
30
  const preview = doc.content.slice(0, 300).replace(/\n/g, "↵");
23
31
  return {
@@ -38,10 +46,10 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
38
46
  await embeddingEngine.saveEmbedding(validated.path, embedding, resolvedProject);
39
47
  }
40
48
  }
41
- if (validated.search_file)
42
- fs.unlinkSync(validated.search_file);
43
- if (validated.replacement_file)
44
- fs.unlinkSync(validated.replacement_file);
49
+ if (searchSource.draft)
50
+ deleteDraft(dataDir, searchSource.draft);
51
+ if (replacementSource.draft)
52
+ deleteDraft(dataDir, replacementSource.draft);
45
53
  return {
46
54
  content: [{ type: "text", text: `Documentation entry "${validated.path}" patched successfully.` }],
47
55
  };
@@ -53,16 +61,16 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
53
61
  if (rawSearch || rawReplacement) {
54
62
  const parts = [];
55
63
  if (rawSearch) {
56
- const searchPath = persistDraft(storageEngine.getDataDir(), "patch-doc-search", rawSearch);
57
- parts.push(`search → ${searchPath}`);
64
+ const searchDraft = persistDraft(storageEngine.getDataDir(), "patch-doc-search", rawSearch);
65
+ parts.push(`search → draft "${searchDraft}"`);
58
66
  }
59
67
  if (rawReplacement) {
60
- const replacementPath = persistDraft(storageEngine.getDataDir(), "patch-doc-replacement", rawReplacement);
61
- parts.push(`replacement → ${replacementPath}`);
68
+ const replacementDraft = persistDraft(storageEngine.getDataDir(), "patch-doc-replacement", rawReplacement);
69
+ parts.push(`replacement → draft "${replacementDraft}"`);
62
70
  }
63
71
  return {
64
72
  isError: true,
65
- 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.` }],
73
+ content: [{ type: "text", text: `Error patching doc: ${message}\n\n⚠️ The following content has been saved as drafts:\n${parts.join("\n")}\nYou can reuse them directly via the search_draft / replacement_draft parameters instead of rewriting them.` }],
66
74
  };
67
75
  }
68
76
  return { isError: true, content: [{ type: "text", text: `Error patching doc: ${message}` }] };
@@ -1,6 +1,6 @@
1
- import fs from "fs";
2
1
  import { UpdateDocArgsSchema } from "../../shared/schemas/doc.schema.js";
3
- import { persistDraft } from "../core/draft-store.js";
2
+ import { persistDraft, deleteDraft } from "../core/draft-store.js";
3
+ import { resolveTextInput, CONTENT_PARAMS } from "../core/text-input.js";
4
4
  import { resolveCodeRefs, resnapshotPreserving, mergeTrackedFiles } from "../core/code-ref-resolver.js";
5
5
  import { resolveDocInProject, isScopeError, scopeErrorResult } from "../core/project-scope.js";
6
6
  export async function updateDoc(storageEngine, embeddingEngine, args) {
@@ -15,13 +15,14 @@ export async function updateDoc(storageEngine, embeddingEngine, args) {
15
15
  doc.title = validated.title;
16
16
  }
17
17
  let additionalTrackedFiles = [];
18
- if (validated.content_file !== undefined) {
19
- const { content, additionalTrackedFiles: refs } = await resolveCodeRefs(fs.readFileSync(validated.content_file, "utf-8"), resolvedProject);
20
- doc.content = content;
21
- additionalTrackedFiles = refs;
22
- }
23
- else if (validated.content !== undefined) {
24
- const { content, additionalTrackedFiles: refs } = await resolveCodeRefs(validated.content, resolvedProject);
18
+ const source = resolveTextInput(storageEngine.getDataDir(), {
19
+ inline: validated.content,
20
+ filePath: validated.content_file_path,
21
+ fileContent: validated.content_file_content,
22
+ draft: validated.content_draft,
23
+ }, CONTENT_PARAMS);
24
+ if (source) {
25
+ const { content, additionalTrackedFiles: refs } = await resolveCodeRefs(source.text, resolvedProject);
25
26
  doc.content = content;
26
27
  additionalTrackedFiles = refs;
27
28
  }
@@ -47,22 +48,24 @@ export async function updateDoc(storageEngine, embeddingEngine, args) {
47
48
  }
48
49
  doc.updatedAt = new Date().toISOString();
49
50
  await storageEngine.saveDoc(doc, resolvedProject, "update");
50
- if ((validated.content !== undefined || validated.content_file !== undefined) && embeddingEngine.isConfigured()) {
51
+ if (source && embeddingEngine.isConfigured()) {
51
52
  const embedding = await embeddingEngine.generateEmbedding(doc.content);
52
53
  if (embedding) {
53
54
  await embeddingEngine.saveEmbedding(validated.path, embedding, resolvedProject);
54
55
  }
55
56
  }
56
- if (validated.content_file !== undefined) {
57
- fs.unlinkSync(validated.content_file);
57
+ if (source?.draft) {
58
+ deleteDraft(storageEngine.getDataDir(), source.draft);
58
59
  }
59
60
  const changes = [];
60
61
  if (validated.title !== undefined)
61
62
  changes.push(`title (${doc.title})`);
62
63
  if (validated.content !== undefined)
63
64
  changes.push("content");
64
- if (validated.content_file !== undefined)
65
+ if (validated.content_file_path !== undefined || validated.content_file_content !== undefined)
65
66
  changes.push("content (from file)");
67
+ if (validated.content_draft !== undefined)
68
+ changes.push(`content (from draft "${validated.content_draft}")`);
66
69
  if (trackedResult !== undefined) {
67
70
  const parts = [`${doc.tracked_files.length} files`];
68
71
  if (trackedResult.preserved.length > 0)
@@ -87,10 +90,10 @@ export async function updateDoc(storageEngine, embeddingEngine, args) {
87
90
  const message = err instanceof Error ? err.message : String(err);
88
91
  const rawContent = args?.content;
89
92
  if (rawContent) {
90
- const draftPath = persistDraft(storageEngine.getDataDir(), "update-doc", rawContent);
93
+ const draft = persistDraft(storageEngine.getDataDir(), "update-doc", rawContent);
91
94
  return {
92
95
  isError: true,
93
- 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.` }],
96
+ content: [{ type: "text", text: `Error updating doc: ${message}\n\n⚠️ The content has been saved as draft "${draft}".\nYou can reuse it directly via the content_draft parameter instead of rewriting it.` }],
94
97
  };
95
98
  }
96
99
  return { isError: true, content: [{ type: "text", text: `Error updating doc: ${message}` }] };
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.82";
1
+ export declare const APP_VERSION = "1.0.83";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated version file - do not edit manually
2
- export const APP_VERSION = '1.0.82';
2
+ export const APP_VERSION = '1.0.83';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-documentor",
3
- "version": "1.0.82",
3
+ "version": "1.0.83",
4
4
  "main": "dist/src/index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",