@aigne/doc-smith 0.2.4 → 0.2.6

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 (37) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -0
  3. package/agents/check-detail-result.mjs +15 -120
  4. package/agents/check-structure-plan.mjs +56 -7
  5. package/agents/detail-generator-and-translate.yaml +7 -1
  6. package/agents/detail-regenerator.yaml +6 -57
  7. package/agents/docs-generator.yaml +5 -61
  8. package/agents/find-item-by-path.mjs +63 -14
  9. package/agents/input-generator.mjs +31 -21
  10. package/agents/language-selector.mjs +101 -0
  11. package/agents/load-config.mjs +3 -3
  12. package/agents/load-sources.mjs +55 -40
  13. package/agents/publish-docs.mjs +44 -153
  14. package/agents/retranslate.yaml +74 -0
  15. package/agents/save-docs.mjs +12 -2
  16. package/agents/save-output.mjs +9 -3
  17. package/agents/save-single-doc.mjs +19 -0
  18. package/agents/structure-planning.yaml +6 -0
  19. package/agents/team-publish-docs.yaml +7 -7
  20. package/agents/translate.yaml +3 -0
  21. package/aigne.yaml +5 -1
  22. package/docs-mcp/docs-search.yaml +1 -1
  23. package/docs-mcp/get-docs-detail.mjs +1 -1
  24. package/docs-mcp/get-docs-structure.mjs +1 -1
  25. package/docs-mcp/read-doc-content.mjs +1 -1
  26. package/package.json +16 -7
  27. package/prompts/check-structure-planning-result.md +4 -7
  28. package/prompts/content-detail-generator.md +1 -2
  29. package/prompts/structure-planning.md +7 -2
  30. package/prompts/translator.md +4 -0
  31. package/tests/test-all-validation-cases.mjs +707 -0
  32. package/utils/constants.mjs +3 -2
  33. package/utils/markdown-checker.mjs +386 -0
  34. package/utils/mermaid-validator.mjs +158 -0
  35. package/utils/mermaid-worker-pool.mjs +254 -0
  36. package/utils/mermaid-worker.mjs +242 -0
  37. package/utils/utils.mjs +155 -44
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.6](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.2.5...v0.2.6) (2025-08-12)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * release 0.2.6 ([c5b5ea5](https://github.com/AIGNE-io/aigne-doc-smith/commit/c5b5ea5c404d44f3b0d420f0b57e4ae64ae5d624))
9
+
10
+ ## [0.2.5](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.2.4...v0.2.5) (2025-08-08)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * polish cli process ([#17](https://github.com/AIGNE-io/aigne-doc-smith/issues/17)) ([4c94263](https://github.com/AIGNE-io/aigne-doc-smith/commit/4c9426378dff9ca3270bd0e455aa6fb1045f6abb))
16
+
3
17
  ## [0.2.4](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.2.3...v0.2.4) (2025-08-07)
4
18
 
5
19
 
package/README.md CHANGED
@@ -206,6 +206,7 @@ aigne doc publish --appUrl https://your-discuss-kit-instance.com
206
206
  npx --no doc-smith run --entry-agent init
207
207
  npx --no doc-smith run --entry-agent generate
208
208
  npx --no doc-smith run --entry-agent update
209
+ npx --no doc-smith run --entry-agent retranslate
209
210
  npx --no doc-smith run --entry-agent publish
210
211
  ```
211
212
 
@@ -1,11 +1,9 @@
1
+ import { checkMarkdown } from "../utils/markdown-checker.mjs";
2
+
1
3
  export default async function checkDetailResult({
2
4
  structurePlan,
3
5
  reviewContent,
4
6
  }) {
5
- const linkRegex = /(?<!\!)\[([^\]]+)\]\(([^)]+)\)/g;
6
- const tableSeparatorRegex = /^\s*\|\s*-+\s*\|\s*$/;
7
- const codeBlockRegex = /^\s+```(?:\w+)?$/;
8
-
9
7
  let isApproved = true;
10
8
  const detailFeedback = [];
11
9
 
@@ -25,125 +23,22 @@ export default async function checkDetailResult({
25
23
  allowedLinks.add(flatPath);
26
24
  });
27
25
 
28
- const checkLinks = (text, source) => {
29
- let match;
30
- while ((match = linkRegex.exec(text)) !== null) {
31
- const link = match[2];
32
- const trimLink = link.trim();
33
-
34
- // Only check links that processContent would process
35
- // Exclude external links and mailto
36
- if (/^(https?:\/\/|mailto:)/.test(trimLink)) continue;
37
-
38
- // Preserve anchors
39
- const [path, hash] = trimLink.split("#");
40
-
41
- // Only process relative paths or paths starting with /
42
- if (!path) continue;
43
-
44
- // Check if this link is in the allowed links set
45
- if (!allowedLinks.has(trimLink)) {
46
- isApproved = false;
47
- detailFeedback.push(
48
- `Found a dead link in ${source}: [${match[1]}](${trimLink}), ensure the link exists in the structure plan path`
49
- );
50
- }
51
- }
52
- };
53
-
54
- const performAllChecks = (text, source) => {
55
- // Split text into lines once and perform all checks in a single pass
56
- const lines = text.split("\n");
57
-
58
- // State variables for different checks
59
- let inCodeBlock = false;
60
- let codeBlockIndentLevel = 0;
61
- let codeBlockStartLine = 0;
62
- let inMermaidBlock = false;
63
- let mermaidStartLine = 0;
26
+ // Run comprehensive markdown validation with all checks
27
+ try {
28
+ const markdownErrors = await checkMarkdown(reviewContent, "result", {
29
+ allowedLinks,
30
+ });
64
31
 
65
- for (let i = 0; i < lines.length; i++) {
66
- const line = lines[i];
67
- const lineNumber = i + 1;
68
-
69
- // Check table separators
70
- if (tableSeparatorRegex.test(line)) {
71
- isApproved = false;
72
- detailFeedback.push(
73
- `Found an incorrect table separator in ${source} at line ${lineNumber}: ${line.trim()}`
74
- );
75
- }
76
-
77
- // Check code block markers and indentation
78
- if (codeBlockRegex.test(line)) {
79
- if (!inCodeBlock) {
80
- // Starting a new code block
81
- inCodeBlock = true;
82
- codeBlockStartLine = lineNumber;
83
- // Calculate indentation level of the code block marker
84
- const match = line.match(/^(\s*)(```)/);
85
- codeBlockIndentLevel = match ? match[1].length : 0;
86
- } else {
87
- // Ending the code block
88
- inCodeBlock = false;
89
- codeBlockIndentLevel = 0;
90
- }
91
- } else if (inCodeBlock) {
92
- // If we're inside a code block, check if content has proper indentation
93
- const contentIndentLevel = line.match(/^(\s*)/)[1].length;
94
-
95
- // If code block marker has indentation, content should have at least the same indentation
96
- if (
97
- codeBlockIndentLevel > 0 &&
98
- contentIndentLevel < codeBlockIndentLevel
99
- ) {
100
- isApproved = false;
101
- detailFeedback.push(
102
- `Found code block with inconsistent indentation in ${source} at line ${codeBlockStartLine}: code block marker has ${codeBlockIndentLevel} spaces indentation but content at line ${lineNumber} has only ${contentIndentLevel} spaces indentation`
103
- );
104
- // Reset to avoid multiple errors for the same code block
105
- inCodeBlock = false;
106
- codeBlockIndentLevel = 0;
107
- }
108
- }
109
-
110
- // Check mermaid block markers
111
- if (/^\s*```mermaid\s*$/.test(line)) {
112
- inMermaidBlock = true;
113
- mermaidStartLine = lineNumber;
114
- } else if (inMermaidBlock && /^\s*```\s*$/.test(line)) {
115
- inMermaidBlock = false;
116
- } else if (inMermaidBlock) {
117
- // If we're inside a mermaid block, check for backticks in node labels
118
- // Check for node definitions with backticks in labels
119
- // Pattern: A["label with backticks"] or A{"label with backticks"}
120
- const nodeLabelRegex =
121
- /[A-Za-z0-9_]+\["([^"]*`[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*`[^}]*)"}/g;
122
- let match;
123
-
124
- while ((match = nodeLabelRegex.exec(line)) !== null) {
125
- const label = match[1] || match[2];
126
- isApproved = false;
127
- detailFeedback.push(
128
- `Found backticks in Mermaid node label in ${source} at line ${lineNumber}: "${label}" - backticks in node labels cause rendering issues in Mermaid diagrams`
129
- );
130
- }
131
- }
132
- }
133
-
134
- // Check single line content (this needs to be done after the loop)
135
- const newlineCount = (text.match(/\n/g) || []).length;
136
- if (newlineCount === 0 && text.trim().length > 0) {
32
+ if (markdownErrors.length > 0) {
137
33
  isApproved = false;
138
- detailFeedback.push(
139
- `Found single line content in ${source}: content appears to be on only one line, check for missing line breaks`
140
- );
34
+ detailFeedback.push(...markdownErrors);
141
35
  }
142
- };
143
-
144
- // Check content
145
- checkLinks(reviewContent, "result");
146
- performAllChecks(reviewContent, "result");
36
+ } catch (error) {
37
+ isApproved = false;
38
+ detailFeedback.push(
39
+ `Found markdown validation error in result: ${error.message}`
40
+ );
41
+ }
147
42
 
148
43
  return {
149
44
  isApproved,
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  getCurrentGitHead,
3
3
  hasFileChangesBetweenCommits,
4
+ loadConfigFromFile,
5
+ saveValueToConfig,
6
+ getProjectInfo,
4
7
  } from "../utils/utils.mjs";
5
8
 
6
9
  export default async function checkStructurePlan(
7
- { originalStructurePlan, feedback, lastGitHead, forceRegenerate, ...rest },
10
+ { originalStructurePlan, feedback, lastGitHead, ...rest },
8
11
  options
9
12
  ) {
10
13
  // Check if we need to regenerate structure plan
@@ -38,17 +41,13 @@ export default async function checkStructurePlan(
38
41
  1. 对于新增的内容,可以根据需要新增节点,或补充到原有节点展示
39
42
  2. 谨慎删除节点,除非节点关联 sourceIds 都被删除了
40
43
  3. 不能修改原有节点的 path
44
+ 4. 根据最新的 Data Sources 按需要更新节点的 sourceIds,如没有大的变化,可以不更新。
41
45
  `;
42
46
  }
43
47
  }
44
48
 
45
49
  // If no regeneration needed, return original structure plan
46
- if (
47
- originalStructurePlan &&
48
- !feedback &&
49
- !shouldRegenerate &&
50
- !forceRegenerate
51
- ) {
50
+ if (originalStructurePlan && !feedback && !shouldRegenerate) {
52
51
  return {
53
52
  structurePlan: originalStructurePlan,
54
53
  };
@@ -62,9 +61,59 @@ export default async function checkStructurePlan(
62
61
  ...rest,
63
62
  });
64
63
 
64
+ let message = "";
65
+
66
+ // Check and save project information if user hasn't modified it
67
+ if (result.projectName || result.projectDesc) {
68
+ try {
69
+ const currentConfig = await loadConfigFromFile();
70
+ const projectInfo = await getProjectInfo();
71
+
72
+ // Check if user has modified project information
73
+ const userModifiedProjectName =
74
+ currentConfig?.projectName &&
75
+ currentConfig.projectName !== projectInfo.name;
76
+ const userModifiedProjectDesc =
77
+ currentConfig?.projectDesc &&
78
+ currentConfig.projectDesc !== projectInfo.description;
79
+
80
+ // If user hasn't modified project info and it's not from GitHub, save AI output
81
+ if (!userModifiedProjectName && !userModifiedProjectDesc) {
82
+ let hasUpdated = false;
83
+ // Don't update if the current info is from GitHub (meaningful repository info)
84
+ if (
85
+ result.projectName &&
86
+ result.projectName !== projectInfo.name &&
87
+ !projectInfo.fromGitHub
88
+ ) {
89
+ await saveValueToConfig("projectName", result.projectName);
90
+ message += `Project name: \`${result.projectName}\``;
91
+ hasUpdated = true;
92
+ }
93
+
94
+ if (
95
+ result.projectDesc &&
96
+ result.projectDesc !== projectInfo.description &&
97
+ !projectInfo.fromGitHub
98
+ ) {
99
+ await saveValueToConfig("projectDesc", result.projectDesc);
100
+ message += `\nProject description: \`${result.projectDesc}\``;
101
+ hasUpdated = true;
102
+ }
103
+
104
+ if (hasUpdated) {
105
+ message = `\n### Auto-updated Project Info to \`.aigne/doc-smith/config.yaml\`\n\n${message}\n\n`;
106
+ }
107
+ }
108
+ } catch (error) {
109
+ console.warn("Failed to check/save project information:", error.message);
110
+ }
111
+ }
112
+
65
113
  return {
66
114
  ...result,
67
115
  feedback: "", // clear feedback
116
+ projectInfoMessage: message,
68
117
  originalStructurePlan: originalStructurePlan
69
118
  ? originalStructurePlan
70
119
  : JSON.parse(JSON.stringify(result.structurePlan || [])),
@@ -15,9 +15,15 @@ skills:
15
15
  reflection:
16
16
  reviewer: ./check-detail-result.mjs
17
17
  is_approved: isApproved
18
- max_iterations: 3
18
+ max_iterations: 5
19
19
  return_last_on_max_iterations: true
20
20
  task_title: Generate detail for '{{ title }}'
21
+ - type: transform
22
+ jsonata: |
23
+ $merge([
24
+ $,
25
+ { "feedback": "" }
26
+ ])
21
27
  - ./batch-translate.yaml
22
28
  - ./save-single-doc.mjs
23
29
  input_schema:
@@ -27,24 +27,16 @@ skills:
27
27
  ])
28
28
  - ./find-item-by-path.mjs
29
29
  - ./format-structure-plan.mjs
30
- - ./detail-generator-and-translate.yaml
30
+ - url: ./detail-generator-and-translate.yaml
31
+ default_input:
32
+ isShowMessage: true
31
33
  input_schema:
32
34
  type: object
33
35
  properties:
34
- config:
35
- type: string
36
- description: Path to the config file
37
- default: ./doc-smith/config.yaml
38
- # nodeName:
39
- # type: string
40
- # description: Name of the section to generate documentation for
41
- # default: Section
42
- # locale:
43
- # type: string
44
- # description: Primary language for documentation (e.g., zh, en)
45
- # targetAudience:
36
+ # config:
46
37
  # type: string
47
- # description: Target audience for the documentation
38
+ # description: Path to the config file
39
+ # default: ./.aigne/doc-smith/config.yaml
48
40
  glossary:
49
41
  type: string
50
42
  description: Glossary of terms for consistent terminology
@@ -54,49 +46,6 @@ input_schema:
54
46
  feedback:
55
47
  type: string
56
48
  description: Feedback for content improvement
57
- # sources:
58
- # type: array
59
- # items:
60
- # type: string
61
- # description: Source code files for documentation generation
62
- # sourcesPath:
63
- # type: array
64
- # description: Source code paths
65
- # items:
66
- # type: string
67
- # default:
68
- # - ./
69
- # includePatterns:
70
- # type: array
71
- # description: File patterns to include
72
- # items:
73
- # type: string
74
- # excludePatterns:
75
- # type: array
76
- # description: File patterns to exclude
77
- # items:
78
- # type: string
79
- # outputDir:
80
- # type: string
81
- # description: Output directory for intermediate files
82
- # default: ./doc-smith/output
83
- # docsDir:
84
- # type: string
85
- # description: Directory to save generated documentation
86
- # default: ./doc-smith/docs
87
- # additionalInformation:
88
- # type: string
89
- # description: Additional context or information for documentation
90
- # translateLanguages:
91
- # type: array
92
- # items:
93
- # type: string
94
- # description: Target languages for translation (e.g., zh, en)
95
- # labels:
96
- # type: array
97
- # items:
98
- # type: string
99
- # description: Tags or labels for categorization
100
49
  output_schema:
101
50
  type: object
102
51
  properties:
@@ -30,7 +30,8 @@ skills:
30
30
  'translates': [$map(translateLanguages, function($lang) { {"language": $lang} })]
31
31
  }
32
32
  ])
33
- })
33
+ }),
34
+ "datasources": ""
34
35
  }
35
36
  ])
36
37
  - ./format-structure-plan.mjs
@@ -40,76 +41,19 @@ skills:
40
41
  input_schema:
41
42
  type: object
42
43
  properties:
43
- config:
44
- type: string
45
- description: Path to the config file
46
- default: ./doc-smith/config.yaml
47
- # nodeName:
48
- # type: string
49
- # description: Name of the section to generate documentation for
50
- # default: Section
51
- # rules:
52
- # type: string
53
- # description: Documentation generation requirements and rules
54
- # locale:
55
- # type: string
56
- # description: Primary language for documentation (e.g., zh, en)
57
- # sources:
58
- # type: array
59
- # items:
60
- # type: string
61
- # description: Source code files for documentation generation
62
- # sourcesPath:
63
- # type: array
64
- # description: Source code paths
65
- # items:
66
- # type: string
67
- # default:
68
- # - ./
69
- # includePatterns:
70
- # type: array
71
- # description: File patterns to include
72
- # items:
73
- # type: string
74
- # excludePatterns:
75
- # type: array
76
- # description: File patterns to exclude
77
- # items:
78
- # type: string
79
- # docsDir:
44
+ # config:
80
45
  # type: string
81
- # description: Directory to save generated documentation
82
- # default: ./doc-smith/docs
83
- # outputDir:
84
- # type: string
85
- # description: Output directory for intermediate files
86
- # default: ./doc-smith/output
87
- # translateLanguages:
88
- # type: array
89
- # items:
90
- # type: string
91
- # description: Target languages for translation (e.g., zh, en)
46
+ # description: Path to the config file
47
+ # default: ./.aigne/doc-smith/config.yaml
92
48
  glossary:
93
49
  type: string
94
50
  description: Glossary of terms for consistent terminology
95
- # additionalInformation:
96
- # type: string
97
- # description: Additional context or information for documentation
98
- # docsType:
99
- # type: string
100
- # description: Type of documentation (general, getting-started, reference, faq)
101
- # default: general
102
51
  feedback:
103
52
  type: string
104
53
  description: Feedback for structure planning adjustments
105
54
  forceRegenerate:
106
55
  type: boolean
107
56
  description: Force regenerate all documentation
108
- # labels:
109
- # type: array
110
- # items:
111
- # type: string
112
- # description: Tags or labels for categorization
113
57
  required:
114
58
  - config
115
59
  mode: sequential
@@ -1,11 +1,25 @@
1
- import { readdir } from "node:fs/promises";
1
+ import { readdir, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
+ // Helper function to get action-specific text based on isTranslate flag
5
+ function getActionText(isTranslate, baseText) {
6
+ const action = isTranslate ? "retranslate" : "update";
7
+ return baseText.replace("{action}", action);
8
+ }
9
+
4
10
  export default async function findItemByPath(
5
- { "doc-path": docPath, structurePlanResult, boardId, docsDir },
11
+ {
12
+ "doc-path": docPath,
13
+ structurePlanResult,
14
+ boardId,
15
+ docsDir,
16
+ isTranslate,
17
+ feedback,
18
+ },
6
19
  options
7
20
  ) {
8
21
  let foundItem = null;
22
+ let selectedFileContent = null;
9
23
 
10
24
  // If docPath is empty, let user select from available documents
11
25
  if (!docPath) {
@@ -22,14 +36,12 @@ export default async function findItemByPath(
22
36
  );
23
37
 
24
38
  if (mainLanguageFiles.length === 0) {
25
- throw new Error(
26
- "Please provide a doc-path parameter to specify which document to update"
27
- );
39
+ throw new Error("No documents found in the docs directory");
28
40
  }
29
41
 
30
42
  // Let user select a file
31
43
  const selectedFile = await options.prompts.search({
32
- message: "Select a document to update:",
44
+ message: getActionText(isTranslate, "Select a document to {action}:"),
33
45
  source: async (input, { signal }) => {
34
46
  if (!input || input.trim() === "") {
35
47
  return mainLanguageFiles.map((file) => ({
@@ -51,9 +63,19 @@ export default async function findItemByPath(
51
63
  });
52
64
 
53
65
  if (!selectedFile) {
54
- throw new Error(
55
- "Please provide a doc-path parameter to specify which document to update"
66
+ throw new Error("No document selected");
67
+ }
68
+
69
+ // Read the selected .md file content
70
+ try {
71
+ const selectedFilePath = join(docsDir, selectedFile);
72
+ selectedFileContent = await readFile(selectedFilePath, "utf-8");
73
+ } catch (readError) {
74
+ console.warn(
75
+ `⚠️ Could not read content from ${selectedFile}:`,
76
+ readError.message
56
77
  );
78
+ selectedFileContent = null;
57
79
  }
58
80
 
59
81
  // Convert filename back to path
@@ -71,15 +93,17 @@ export default async function findItemByPath(
71
93
  return itemFlattenedPath === flatName;
72
94
  });
73
95
  if (!foundItemByFile) {
74
- throw new Error(
75
- "Please provide a doc-path parameter to specify which document to update"
76
- );
96
+ throw new Error("No document found");
77
97
  }
78
98
 
79
99
  docPath = foundItemByFile.path;
80
100
  } catch (error) {
101
+ console.error(error);
81
102
  throw new Error(
82
- "Please provide a doc-path parameter to specify which document to update"
103
+ getActionText(
104
+ isTranslate,
105
+ "Please provide a doc-path parameter to specify which document to {action}"
106
+ )
83
107
  );
84
108
  }
85
109
  }
@@ -111,8 +135,33 @@ export default async function findItemByPath(
111
135
  );
112
136
  }
113
137
 
114
- // Merge the found item with originalStructurePlan
115
- return {
138
+ // Prompt for feedback if not provided
139
+ let userFeedback = feedback;
140
+ if (!userFeedback) {
141
+ const feedbackMessage = getActionText(
142
+ isTranslate,
143
+ "Please provide feedback for the {action} (press Enter to skip):"
144
+ );
145
+
146
+ userFeedback = await options.prompts.input({
147
+ message: feedbackMessage,
148
+ });
149
+ }
150
+
151
+ // Merge the found item with originalStructurePlan and add content if available
152
+ const result = {
116
153
  ...foundItem,
117
154
  };
155
+
156
+ // Add content if we read it from user selection
157
+ if (selectedFileContent !== null) {
158
+ result.content = selectedFileContent;
159
+ }
160
+
161
+ // Add feedback to result if provided
162
+ if (userFeedback && userFeedback.trim()) {
163
+ result.feedback = userFeedback.trim();
164
+ }
165
+
166
+ return result;
118
167
  }