@aigne/doc-smith 0.2.5 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## [0.2.5](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.2.4...v0.2.5) (2025-08-08)
4
11
 
5
12
 
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,141 +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;
64
-
65
- for (let i = 0; i < lines.length; i++) {
66
- const line = lines[i];
67
- const lineNumber = i + 1;
26
+ // Run comprehensive markdown validation with all checks
27
+ try {
28
+ const markdownErrors = await checkMarkdown(reviewContent, "result", {
29
+ allowedLinks,
30
+ });
68
31
 
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
- // Check for edge descriptions with numbered list format
133
- // Pattern: -- "1. description" --> or similar variants
134
- const edgeDescriptionRegex = /--\s*"([^"]*)"\s*-->/g;
135
- let edgeMatch;
136
-
137
- while ((edgeMatch = edgeDescriptionRegex.exec(line)) !== null) {
138
- const description = edgeMatch[1];
139
- // Check if description starts with number followed by period
140
- if (/^\d+\.\s/.test(description)) {
141
- isApproved = false;
142
- detailFeedback.push(
143
- `Unsupported markdown: list - Found numbered list format in Mermaid edge description in ${source} at line ${lineNumber}: "${description}" - numbered lists in edge descriptions are not supported`
144
- );
145
- }
146
- }
147
- }
148
- }
149
-
150
- // Check single line content (this needs to be done after the loop)
151
- const newlineCount = (text.match(/\n/g) || []).length;
152
- if (newlineCount === 0 && text.trim().length > 0) {
32
+ if (markdownErrors.length > 0) {
153
33
  isApproved = false;
154
- detailFeedback.push(
155
- `Found single line content in ${source}: content appears to be on only one line, check for missing line breaks`
156
- );
34
+ detailFeedback.push(...markdownErrors);
157
35
  }
158
- };
159
-
160
- // Check content
161
- checkLinks(reviewContent, "result");
162
- performAllChecks(reviewContent, "result");
36
+ } catch (error) {
37
+ isApproved = false;
38
+ detailFeedback.push(
39
+ `Found markdown validation error in result: ${error.message}`
40
+ );
41
+ }
163
42
 
164
43
  return {
165
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,7 +27,9 @@ 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:
@@ -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
@@ -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
  }
@@ -1,13 +1,16 @@
1
1
  import { writeFile, mkdir, readFile } from "node:fs/promises";
2
2
  import { join, dirname } from "node:path";
3
3
  import chalk from "chalk";
4
- import { validatePath, getAvailablePaths } from "../utils/utils.mjs";
4
+ import {
5
+ validatePath,
6
+ getAvailablePaths,
7
+ getProjectInfo,
8
+ } from "../utils/utils.mjs";
5
9
  import {
6
10
  SUPPORTED_LANGUAGES,
7
11
  DOCUMENT_STYLES,
8
12
  TARGET_AUDIENCES,
9
13
  } from "../utils/constants.mjs";
10
-
11
14
  // UI constants
12
15
  const PRESS_ENTER_TO_FINISH = "Press Enter to finish";
13
16
 
@@ -180,6 +183,12 @@ export default async function init(
180
183
  // If no paths entered, use default
181
184
  input.sourcesPath = sourcePaths.length > 0 ? sourcePaths : ["./"];
182
185
 
186
+ // Save project info to config
187
+ const projectInfo = await getProjectInfo();
188
+ input.projectName = projectInfo.name;
189
+ input.projectDesc = projectInfo.description;
190
+ input.projectLogo = projectInfo.icon;
191
+
183
192
  // Generate YAML content
184
193
  const yamlContent = generateYAML(input, outputPath);
185
194
 
@@ -193,13 +202,17 @@ export default async function init(
193
202
 
194
203
  await writeFile(filePath, yamlContent, "utf8");
195
204
  console.log(`\nšŸŽ‰ Configuration saved to: ${chalk.cyan(filePath)}`);
205
+ // Print YAML content for user review
206
+ console.log(chalk.cyan("---"));
207
+ console.log(chalk.cyan(yamlContent));
208
+ console.log(chalk.cyan("---"));
196
209
  console.log(
197
- "šŸ’” You can edit the configuration file anytime to modify settings."
210
+ "šŸ’” You can edit the configuration file anytime to modify settings.\n"
198
211
  );
199
212
  console.log(
200
213
  `šŸš€ Run ${chalk.cyan(
201
214
  "'aigne doc generate'"
202
- )} to start documentation generation!`
215
+ )} to start documentation generation!\n`
203
216
  );
204
217
 
205
218
  return {};
@@ -215,12 +228,18 @@ export default async function init(
215
228
  /**
216
229
  * Generate YAML configuration content
217
230
  * @param {Object} input - Input object
218
- * @param {string} outputPath - Output path for directory configuration
219
231
  * @returns {string} YAML string
220
232
  */
221
- function generateYAML(input, outputPath) {
233
+ function generateYAML(input) {
222
234
  let yaml = "";
223
235
 
236
+ // Add project information at the beginning
237
+ yaml += `# Project information for documentation publishing\n`;
238
+ yaml += `projectName: ${input.projectName || ""}\n`;
239
+ yaml += `projectDesc: ${input.projectDesc || ""}\n`;
240
+ yaml += `projectLogo: ${input.projectLogo || ""}\n`;
241
+ yaml += `\n`;
242
+
224
243
  // Add rules (required field)
225
244
  yaml += `rules: |\n`;
226
245
  if (input.rules && input.rules.trim()) {
@@ -0,0 +1,101 @@
1
+ import { SUPPORTED_LANGUAGES } from "../utils/constants.mjs";
2
+
3
+ /**
4
+ * Interactive language selector for translation from configured languages
5
+ * @param {Object} params
6
+ * @param {Array<string>} [params.languages] - Pre-selected languages
7
+ * @param {Array<string>} params.translateLanguages - Available languages from config
8
+ * @param {Object} options - Options object with prompts
9
+ * @returns {Promise<Object>} Selected languages
10
+ */
11
+ export default async function languageSelector(
12
+ { languages, translateLanguages },
13
+ options
14
+ ) {
15
+ let selectedLanguages = [];
16
+
17
+ // Check if translateLanguages is available from config
18
+ if (
19
+ !translateLanguages ||
20
+ !Array.isArray(translateLanguages) ||
21
+ translateLanguages.length === 0
22
+ ) {
23
+ throw new Error(
24
+ "No translation languages configured in config.yaml. Please add translateLanguages to your configuration."
25
+ );
26
+ }
27
+
28
+ // If languages are provided as parameter, validate against configured languages
29
+ if (languages && Array.isArray(languages) && languages.length > 0) {
30
+ const validLanguages = languages.filter((lang) =>
31
+ translateLanguages.includes(lang)
32
+ );
33
+
34
+ if (validLanguages.length > 0) {
35
+ selectedLanguages = validLanguages;
36
+ } else {
37
+ console.log(`āš ļø Invalid languages provided: ${languages.join(", ")}`);
38
+ console.log(
39
+ "Available configured languages:",
40
+ translateLanguages.join(", ")
41
+ );
42
+ }
43
+ }
44
+
45
+ // If no valid languages were provided, let user select from configured languages
46
+ if (selectedLanguages.length === 0) {
47
+ // Create choices from configured languages with labels
48
+ const choices = translateLanguages.map((langCode) => {
49
+ const supportedLang = SUPPORTED_LANGUAGES.find(
50
+ (l) => l.code === langCode
51
+ );
52
+ return {
53
+ name: supportedLang
54
+ ? `${supportedLang.label} (${supportedLang.sample})`
55
+ : langCode,
56
+ value: langCode,
57
+ short: langCode,
58
+ };
59
+ });
60
+
61
+ selectedLanguages = await options.prompts.checkbox({
62
+ message: "Select languages to translate:",
63
+ choices: choices,
64
+ validate: (answer) => {
65
+ if (answer.length === 0) {
66
+ return "Please select at least one language";
67
+ }
68
+ return true;
69
+ },
70
+ });
71
+ }
72
+
73
+ if (selectedLanguages.length === 0) {
74
+ throw new Error("No languages selected for re-translation");
75
+ }
76
+
77
+ return {
78
+ selectedLanguages,
79
+ };
80
+ }
81
+
82
+ languageSelector.input_schema = {
83
+ type: "object",
84
+ properties: {
85
+ languages: {
86
+ type: "array",
87
+ items: {
88
+ type: "string",
89
+ },
90
+ description: "Pre-selected languages for translation",
91
+ },
92
+ translateLanguages: {
93
+ type: "array",
94
+ items: {
95
+ type: "string",
96
+ },
97
+ description: "Available translation languages from config",
98
+ },
99
+ },
100
+ required: ["translateLanguages"],
101
+ };