@aigne/doc-smith 0.9.7 → 0.9.8-beta

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/agents/create/analyze-diagram-type-llm.yaml +160 -0
  3. package/agents/create/analyze-diagram-type.mjs +297 -0
  4. package/agents/create/generate-diagram-image.yaml +60 -0
  5. package/agents/create/replace-d2-with-image.mjs +624 -0
  6. package/agents/create/utils/init-current-content.mjs +5 -9
  7. package/agents/evaluate/document.yaml +6 -0
  8. package/agents/evaluate/index.yaml +1 -0
  9. package/agents/init/index.mjs +16 -0
  10. package/agents/media/batch-generate-media-description.yaml +2 -0
  11. package/agents/media/generate-media-description.yaml +3 -0
  12. package/agents/media/load-media-description.mjs +44 -15
  13. package/agents/publish/publish-docs.mjs +1 -4
  14. package/agents/update/check-diagram-flag.mjs +116 -0
  15. package/agents/update/check-document.mjs +0 -1
  16. package/agents/update/check-generate-diagram.mjs +48 -30
  17. package/agents/update/check-sync-image-flag.mjs +55 -0
  18. package/agents/update/check-update-is-single.mjs +11 -0
  19. package/agents/update/generate-diagram.yaml +43 -9
  20. package/agents/update/generate-document.yaml +9 -0
  21. package/agents/update/handle-document-update.yaml +10 -8
  22. package/agents/update/index.yaml +16 -1
  23. package/agents/update/sync-images-and-exit.mjs +148 -0
  24. package/agents/update/update-single/update-single-document-detail.mjs +131 -17
  25. package/agents/utils/analyze-feedback-intent.mjs +136 -0
  26. package/agents/utils/choose-docs.mjs +183 -40
  27. package/agents/utils/generate-document-or-skip.mjs +41 -0
  28. package/agents/utils/handle-diagram-operations.mjs +263 -0
  29. package/agents/utils/load-all-document-content.mjs +30 -0
  30. package/agents/utils/load-sources.mjs +2 -2
  31. package/agents/utils/read-current-document-content.mjs +46 -0
  32. package/agents/utils/save-doc.mjs +42 -0
  33. package/agents/utils/skip-if-content-exists.mjs +27 -0
  34. package/aigne.yaml +6 -1
  35. package/assets/report-template/report.html +17 -17
  36. package/docs-mcp/read-doc-content.mjs +30 -1
  37. package/package.json +4 -4
  38. package/prompts/detail/diagram/generate-image-system.md +135 -0
  39. package/prompts/detail/diagram/generate-image-user.md +32 -0
  40. package/prompts/detail/generate/user-prompt.md +27 -13
  41. package/prompts/evaluate/document.md +23 -10
  42. package/prompts/media/media-description/system-prompt.md +10 -2
  43. package/prompts/media/media-description/user-prompt.md +9 -0
  44. package/utils/check-document-has-diagram.mjs +97 -0
  45. package/utils/constants/index.mjs +46 -0
  46. package/utils/d2-utils.mjs +114 -181
  47. package/utils/delete-diagram-images.mjs +103 -0
  48. package/utils/docs-finder-utils.mjs +34 -1
  49. package/utils/image-compress.mjs +75 -0
  50. package/utils/kroki-utils.mjs +2 -3
  51. package/utils/sync-diagram-to-translations.mjs +258 -0
  52. package/utils/utils.mjs +24 -0
  53. package/agents/create/check-diagram.mjs +0 -40
  54. package/agents/create/draw-diagram.yaml +0 -27
  55. package/agents/create/merge-diagram.yaml +0 -39
  56. package/agents/create/wrap-diagram-code.mjs +0 -35
@@ -0,0 +1,258 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { readFileContent } from "./docs-finder-utils.mjs";
3
+ import { debug } from "./debug.mjs";
4
+ import path from "node:path";
5
+ import fs from "fs-extra";
6
+ import { d2CodeBlockRegex } from "./d2-utils.mjs";
7
+ const diagramImageRegex =
8
+ /<!--\s*DIAGRAM_IMAGE_START:[^>]+-->\s*!\[[^\]]*\]\(([^)]+)\)\s*<!--\s*DIAGRAM_IMAGE_END\s*-->/g;
9
+
10
+ /**
11
+ * Find all translation files for a document
12
+ * @param {string} docPath - Document path (e.g., "/guides/getting-started")
13
+ * @param {string} docsDir - Documentation directory
14
+ * @param {string} locale - Main language locale (e.g., "en")
15
+ * @returns {Promise<Array<{language: string, fileName: string}>>} - Array of translation file info
16
+ */
17
+ async function findTranslationFiles(docPath, docsDir, locale) {
18
+ // Convert path to flat filename format
19
+ const flatName = docPath.replace(/^\//, "").replace(/\//g, "-");
20
+
21
+ try {
22
+ const files = readdirSync(docsDir);
23
+ const translationFiles = [];
24
+
25
+ // Filter files to find translation files matching the pattern
26
+ for (const file of files) {
27
+ if (
28
+ file.startsWith(`${flatName}.`) &&
29
+ file.endsWith(".md") &&
30
+ file !== `${flatName}.md` &&
31
+ file.match(/\.\w+(-\w+)?\.md$/)
32
+ ) {
33
+ const langMatch = file.match(/\.(\w+(-\w+)?)\.md$/);
34
+ if (langMatch && langMatch[1] !== locale) {
35
+ translationFiles.push({
36
+ language: langMatch[1],
37
+ fileName: file,
38
+ });
39
+ }
40
+ }
41
+ }
42
+
43
+ return translationFiles;
44
+ } catch (error) {
45
+ debug(`⚠️ Could not read translation files from ${docsDir}: ${error.message}`);
46
+ return [];
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Extract diagram image paths from content
52
+ * @param {string} content - Document content
53
+ * @returns {Array<{path: string, fullMatch: string, index: number}>} - Array of diagram image info
54
+ */
55
+ function extractDiagramImagePaths(content) {
56
+ const images = [];
57
+
58
+ // Reset regex lastIndex
59
+ diagramImageRegex.lastIndex = 0;
60
+
61
+ let match = diagramImageRegex.exec(content);
62
+ while (match !== null) {
63
+ images.push({
64
+ path: match[1],
65
+ fullMatch: match[0],
66
+ index: match.index,
67
+ });
68
+ match = diagramImageRegex.exec(content);
69
+ }
70
+
71
+ return images;
72
+ }
73
+
74
+ /**
75
+ * Replace diagram images in translation files
76
+ * @param {string} mainContent - Main document content (already updated)
77
+ * @param {string} docPath - Document path
78
+ * @param {string} docsDir - Documentation directory
79
+ * @param {string} locale - Main language locale
80
+ * @param {string} operationType - Operation type: "delete", "add", "update", or "sync" (default)
81
+ * @returns {Promise<{updated: number, skipped: number, errors: Array}>} - Sync result
82
+ */
83
+ export async function syncDiagramToTranslations(
84
+ mainContent,
85
+ docPath,
86
+ docsDir,
87
+ locale = "en",
88
+ operationType = "sync",
89
+ ) {
90
+ const result = {
91
+ updated: 0,
92
+ skipped: 0,
93
+ errors: [],
94
+ };
95
+
96
+ // Find all translation files
97
+ const translationFiles = await findTranslationFiles(docPath, docsDir, locale);
98
+
99
+ if (translationFiles.length === 0) {
100
+ debug("ℹ️ No translation files found, skipping sync");
101
+ return result;
102
+ }
103
+
104
+ // Extract diagram images from updated main content
105
+ const mainImages = extractDiagramImagePaths(mainContent);
106
+
107
+ // If no diagrams in main content and operation is not delete, skip sync
108
+ // For delete operations, we need to process translations even if main has 0 diagrams
109
+ // to remove diagrams from translation files
110
+ if (mainImages.length === 0 && operationType !== "delete") {
111
+ debug("ℹ️ No diagram images in main content, skipping sync");
112
+ return result;
113
+ }
114
+
115
+ // Process each translation file
116
+ for (const { fileName } of translationFiles) {
117
+ try {
118
+ const translationFilePath = path.join(docsDir, fileName);
119
+ const translationContent = await readFileContent(docsDir, fileName);
120
+
121
+ if (!translationContent) {
122
+ debug(`⚠️ Could not read translation file: ${fileName}`);
123
+ result.skipped++;
124
+ continue;
125
+ }
126
+
127
+ let hasChanges = false;
128
+ let updatedContent = translationContent;
129
+
130
+ // Strategy 1: Replace D2 code blocks with AI images (if main doc switched from D2 to AI)
131
+ const translationD2Blocks = Array.from(translationContent.matchAll(d2CodeBlockRegex));
132
+ if (translationD2Blocks.length > 0 && mainImages.length > 0) {
133
+ // If main doc has AI images and translation has D2 blocks, replace them by index
134
+ for (let i = 0; i < Math.min(translationD2Blocks.length, mainImages.length); i++) {
135
+ const d2Match = translationD2Blocks[i];
136
+ const mainImage = mainImages[i];
137
+
138
+ if (d2Match && mainImage) {
139
+ // Replace D2 block with AI image from main doc
140
+ updatedContent = updatedContent.replace(d2Match[0], mainImage.fullMatch);
141
+ hasChanges = true;
142
+ debug(`🔄 Replaced D2 block with AI image in ${fileName} (index ${i})`);
143
+ }
144
+ }
145
+ }
146
+
147
+ // Strategy 2: Replace old image paths with new ones (if paths changed)
148
+ const translationImages = extractDiagramImagePaths(updatedContent); // Re-extract after D2 replacement
149
+ if (mainImages.length > 0) {
150
+ for (let i = 0; i < Math.min(translationImages.length, mainImages.length); i++) {
151
+ const translationImage = translationImages[i];
152
+ const mainImage = mainImages[i];
153
+
154
+ // If image path changed, update it
155
+ if (translationImage && mainImage && translationImage.path !== mainImage.path) {
156
+ // Replace old image path with new one (escape special regex characters)
157
+ const escapedPath = translationImage.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
158
+ const oldImagePattern = new RegExp(
159
+ `<!--\\s*DIAGRAM_IMAGE_START:[^>]+-->\\s*!\\[[^\\]]*\\]\\(${escapedPath}\\)\\s*<!--\\s*DIAGRAM_IMAGE_END\\s*-->`,
160
+ "g",
161
+ );
162
+ updatedContent = updatedContent.replace(oldImagePattern, mainImage.fullMatch);
163
+ hasChanges = true;
164
+ debug(
165
+ `🔄 Updated image path in ${fileName} (index ${i}): ${translationImage.path} -> ${mainImage.path}`,
166
+ );
167
+ }
168
+ }
169
+ }
170
+
171
+ // Strategy 3: If translation has fewer images than main, add missing ones
172
+ // (This handles cases where new diagrams were added)
173
+ let finalTranslationImages = extractDiagramImagePaths(updatedContent); // Re-extract after all replacements
174
+ if (mainImages.length > 0 && finalTranslationImages.length < mainImages.length) {
175
+ // Find the last diagram position in updated content
176
+ const lastDiagramIndex =
177
+ finalTranslationImages.length > 0
178
+ ? finalTranslationImages[finalTranslationImages.length - 1].index +
179
+ finalTranslationImages[finalTranslationImages.length - 1].fullMatch.length
180
+ : updatedContent.length;
181
+
182
+ // Add missing images after the last diagram
183
+ const missingImages = mainImages.slice(finalTranslationImages.length);
184
+ const imagesToAdd = missingImages.map((img) => img.fullMatch).join("\n\n");
185
+
186
+ updatedContent =
187
+ updatedContent.slice(0, lastDiagramIndex) +
188
+ "\n\n" +
189
+ imagesToAdd +
190
+ "\n\n" +
191
+ updatedContent.slice(lastDiagramIndex);
192
+ hasChanges = true;
193
+ debug(`➕ Added ${missingImages.length} missing diagram(s) to ${fileName}`);
194
+ // Re-extract after adding images
195
+ finalTranslationImages = extractDiagramImagePaths(updatedContent);
196
+ }
197
+
198
+ // Strategy 4: If translation has more images than main, remove excess ones
199
+ // (This handles cases where diagrams were deleted from main document, including all diagrams)
200
+ if (finalTranslationImages.length > mainImages.length) {
201
+ // Remove excess images from translation (keep only the first N images matching main)
202
+ // Process from end to start to preserve indices
203
+ const excessCount = finalTranslationImages.length - mainImages.length;
204
+ for (let i = finalTranslationImages.length - 1; i >= mainImages.length; i--) {
205
+ const imageToRemove = finalTranslationImages[i];
206
+ const before = updatedContent.substring(0, imageToRemove.index);
207
+ const after = updatedContent.substring(
208
+ imageToRemove.index + imageToRemove.fullMatch.length,
209
+ );
210
+ // Remove the image and clean up extra newlines
211
+ updatedContent = `${before.replace(/\n+$/, "")}\n${after.replace(/^\n+/, "")}`;
212
+ hasChanges = true;
213
+ }
214
+ debug(`➖ Removed ${excessCount} excess diagram(s) from ${fileName}`);
215
+ }
216
+
217
+ // Strategy 5: Remove D2 code blocks from translation if main has no diagrams
218
+ // (This handles cases where all diagrams were deleted from main document)
219
+ if (mainImages.length === 0) {
220
+ // Re-extract D2 blocks from updated content (in case some were already replaced)
221
+ const remainingD2Blocks = Array.from(updatedContent.matchAll(d2CodeBlockRegex));
222
+ if (remainingD2Blocks.length > 0) {
223
+ // Remove all D2 code blocks from translation
224
+ // Process from end to start to preserve indices
225
+ for (let i = remainingD2Blocks.length - 1; i >= 0; i--) {
226
+ const d2Match = remainingD2Blocks[i];
227
+ const before = updatedContent.substring(0, d2Match.index);
228
+ const after = updatedContent.substring(d2Match.index + d2Match[0].length);
229
+ // Remove the D2 block and clean up extra newlines
230
+ updatedContent = `${before.replace(/\n+$/, "")}\n${after.replace(/^\n+/, "")}`;
231
+ hasChanges = true;
232
+ }
233
+ // Clean up extra newlines
234
+ updatedContent = updatedContent.replace(/\n{3,}/g, "\n\n");
235
+ debug(`➖ Removed ${remainingD2Blocks.length} D2 code block(s) from ${fileName}`);
236
+ }
237
+ }
238
+
239
+ // Save updated translation file if there were changes
240
+ if (hasChanges) {
241
+ await fs.writeFile(translationFilePath, updatedContent, "utf8");
242
+ result.updated++;
243
+ debug(`✅ Synced diagram images to ${fileName}`);
244
+ } else {
245
+ result.skipped++;
246
+ debug(`⏭️ No changes needed for ${fileName}`);
247
+ }
248
+ } catch (error) {
249
+ debug(`❌ Error syncing diagram to ${fileName}: ${error.message}`);
250
+ result.errors.push({
251
+ file: fileName,
252
+ error: error.message,
253
+ });
254
+ }
255
+ }
256
+
257
+ return result;
258
+ }
package/utils/utils.mjs CHANGED
@@ -302,10 +302,18 @@ export function hasFileChangesBetweenCommits(
302
302
  return addedOrDeletedFiles.some((filePath) => {
303
303
  // Check if file matches any include pattern
304
304
  const matchesInclude = includePatterns.some((pattern) => {
305
+ // Skip empty patterns
306
+ if (!pattern || !pattern.trim()) {
307
+ return false;
308
+ }
305
309
  // First escape all regex special characters except * and ?
306
310
  const escapedPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
307
311
  // Then convert glob wildcards to regex
308
312
  const regexPattern = escapedPattern.replace(/\*/g, ".*").replace(/\?/g, ".");
313
+ // Only create regex if pattern is not empty
314
+ if (!regexPattern) {
315
+ return false;
316
+ }
309
317
  const regex = new RegExp(regexPattern);
310
318
  return regex.test(filePath);
311
319
  });
@@ -316,10 +324,18 @@ export function hasFileChangesBetweenCommits(
316
324
 
317
325
  // Check if file matches any exclude pattern
318
326
  const matchesExclude = excludePatterns.some((pattern) => {
327
+ // Skip empty patterns
328
+ if (!pattern || !pattern.trim()) {
329
+ return false;
330
+ }
319
331
  // First escape all regex special characters except * and ?
320
332
  const escapedPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
321
333
  // Then convert glob wildcards to regex
322
334
  const regexPattern = escapedPattern.replace(/\*/g, ".*").replace(/\?/g, ".");
335
+ // Only create regex if pattern is not empty
336
+ if (!regexPattern) {
337
+ return false;
338
+ }
323
339
  const regex = new RegExp(regexPattern);
324
340
  return regex.test(filePath);
325
341
  });
@@ -370,6 +386,10 @@ export async function loadConfigFromFile() {
370
386
  * @returns {string} Updated file content
371
387
  */
372
388
  function handleArrayValueUpdate(key, value, comment, fileContent) {
389
+ // Skip if key is empty to avoid "Empty regular expressions are not allowed" error
390
+ if (!key || !key.trim()) {
391
+ return fileContent;
392
+ }
373
393
  // Use yaml library to safely serialize the key-value pair
374
394
  const yamlObject = { [key]: value };
375
395
  const yamlContent = yamlStringify(yamlObject).trim();
@@ -455,6 +475,10 @@ function handleArrayValueUpdate(key, value, comment, fileContent) {
455
475
  * @returns {string} Updated file content
456
476
  */
457
477
  function handleStringValueUpdate(key, value, comment, fileContent) {
478
+ // Skip if key is empty to avoid "Empty regular expressions are not allowed" error
479
+ if (!key || !key.trim()) {
480
+ return fileContent;
481
+ }
458
482
  // Use yaml library to safely serialize the key-value pair
459
483
  const yamlObject = { [key]: value };
460
484
  const yamlContent = yamlStringify(yamlObject).trim();
@@ -1,40 +0,0 @@
1
- import { checkContent } from "../../utils/d2-utils.mjs";
2
-
3
- export default async function checkD2DiagramIsValid({ diagramSourceCode }) {
4
- try {
5
- await checkContent({ content: diagramSourceCode });
6
- return {
7
- isValid: true,
8
- };
9
- } catch (err) {
10
- return {
11
- isValid: false,
12
- diagramError: err.message,
13
- };
14
- }
15
- }
16
-
17
- checkD2DiagramIsValid.input_schema = {
18
- type: "object",
19
- properties: {
20
- diagramSourceCode: {
21
- type: "string",
22
- description: "Source code of d2 diagram",
23
- },
24
- },
25
- required: ["diagramSourceCode"],
26
- };
27
- checkD2DiagramIsValid.output_schema = {
28
- type: "object",
29
- properties: {
30
- isValid: {
31
- type: "boolean",
32
- description: "Indicates whether the provided d2 diagram source passes validation",
33
- },
34
- error: {
35
- type: "string",
36
- description: "Validation error details when the diagram does not pass",
37
- },
38
- },
39
- required: ["isValid"],
40
- };
@@ -1,27 +0,0 @@
1
- name: drawDiagram
2
- description: Generate a D2 diagram from document content.
3
- instructions:
4
- - role: system
5
- url: ../../prompts/detail/diagram/system-prompt.md
6
- - role: user
7
- url: ../../prompts/detail/diagram/user-prompt.md
8
- input_schema:
9
- type: object
10
- properties:
11
- documentContent:
12
- type: string
13
- description: The **raw text content** of the current document. (**Note:** This is the original document and **does not include** any diagram source code.)
14
- locale:
15
- type: string
16
- description: Language for diagram labels and text
17
- default: en
18
- required:
19
- - documentContent
20
- output_schema:
21
- type: object
22
- properties:
23
- diagramSourceCode:
24
- type: string
25
- description: The **diagram source code** generated from the input text.
26
- required:
27
- - diagramSourceCode
@@ -1,39 +0,0 @@
1
- name: mergeDiagramToDocument
2
- description: Merge Diagram source code into document
3
- instructions: |
4
- You are an AI assistant that helps to merge d2 diagram into document.
5
-
6
- <detail_data_source>
7
- {{ content }}
8
- </detail_data_source>
9
-
10
- <diagram_source_code>
11
- {{ diagramSourceCode }}
12
- </diagram_source_code>
13
-
14
- Given the source content of a document and the D2 diagram source code, your task is to:
15
- - **Keep the original content as soon as possible.**
16
- - D2 diagram source code should wrap by ```d2 and ``` in markdown format.
17
- - Should find proper position to insert the D2 diagram in the document, usually after the first paragraph or after the section that describes the diagram.
18
- - If there is no suitable position, append it to the end of the document.
19
- - If there is no D2 diagram source code, return the original document content without any changes.
20
- input_schema:
21
- type: object
22
- properties:
23
- content:
24
- type: string
25
- description: Source content of the document
26
- diagramSourceCode:
27
- type: string
28
- description: Source content of D2 Diagram
29
- required:
30
- - content
31
- - diagramSourceCode
32
- output_schema:
33
- type: object
34
- properties:
35
- content:
36
- type: string
37
- description: Merged content of the document with D2 diagram
38
- required:
39
- - content
@@ -1,35 +0,0 @@
1
- import { wrapCode } from "../../utils/d2-utils.mjs";
2
-
3
- export default async function wrapDiagramCode({ diagramSourceCode }) {
4
- try {
5
- const result = await wrapCode({ content: diagramSourceCode });
6
- return {
7
- diagramSourceCode: result,
8
- };
9
- } catch {
10
- return {
11
- diagramSourceCode,
12
- };
13
- }
14
- }
15
-
16
- wrapDiagramCode.input_schema = {
17
- type: "object",
18
- properties: {
19
- diagramSourceCode: {
20
- type: "string",
21
- description: "Source code of d2 diagram",
22
- },
23
- },
24
- required: ["diagramSourceCode"],
25
- };
26
- wrapDiagramCode.output_schema = {
27
- type: "object",
28
- properties: {
29
- diagramSourceCode: {
30
- type: "string",
31
- description: "Source code of d2 diagram",
32
- },
33
- },
34
- required: ["diagramSourceCode"],
35
- };