@aigne/doc-smith 0.4.5 → 0.6.0

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 (36) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/agents/batch-translate.yaml +3 -0
  3. package/agents/check-detail-result.mjs +2 -1
  4. package/agents/check-detail.mjs +1 -0
  5. package/agents/check-feedback-refiner.mjs +79 -0
  6. package/agents/check-structure-plan.mjs +16 -0
  7. package/agents/detail-generator-and-translate.yaml +3 -0
  8. package/agents/detail-regenerator.yaml +3 -0
  9. package/agents/docs-generator.yaml +3 -0
  10. package/agents/feedback-refiner.yaml +48 -0
  11. package/agents/find-items-by-paths.mjs +5 -1
  12. package/agents/find-user-preferences-by-path.mjs +37 -0
  13. package/agents/input-generator.mjs +8 -9
  14. package/agents/load-sources.mjs +63 -9
  15. package/agents/manage-prefs.mjs +203 -0
  16. package/agents/publish-docs.mjs +3 -1
  17. package/agents/retranslate.yaml +3 -0
  18. package/agents/structure-planning.yaml +3 -0
  19. package/aigne.yaml +4 -0
  20. package/package.json +10 -9
  21. package/prompts/content-detail-generator.md +13 -1
  22. package/prompts/document/detail-generator.md +1 -0
  23. package/prompts/feedback-refiner.md +84 -0
  24. package/prompts/structure-planning.md +8 -0
  25. package/prompts/translator.md +8 -0
  26. package/tests/{test-all-validation-cases.mjs → all-validation-cases.test.mjs} +60 -137
  27. package/tests/check-detail-result.test.mjs +90 -77
  28. package/tests/load-sources.test.mjs +103 -291
  29. package/tests/preferences-utils.test.mjs +369 -0
  30. package/tests/{test-save-docs.mjs → save-docs.test.mjs} +29 -47
  31. package/tests/save-value-to-config.test.mjs +165 -288
  32. package/utils/auth-utils.mjs +1 -1
  33. package/utils/constants.mjs +22 -10
  34. package/utils/markdown-checker.mjs +89 -9
  35. package/utils/preferences-utils.mjs +175 -0
  36. package/utils/utils.mjs +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.5.1...v0.6.0) (2025-08-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * complete support for media processing before publish ([#63](https://github.com/AIGNE-io/aigne-doc-smith/issues/63)) ([5257ca1](https://github.com/AIGNE-io/aigne-doc-smith/commit/5257ca1756f47487b65a1813949e547b6fc51aca))
9
+
10
+ ## [0.5.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.5.0...v0.5.1) (2025-08-26)
11
+
12
+
13
+ ### Miscellaneous Chores
14
+
15
+ * release 0.5.1 ([892d96e](https://github.com/AIGNE-io/aigne-doc-smith/commit/892d96e939a6404a42e8d2521f95bb7acfeabe27))
16
+
17
+ ## [0.5.0](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.4.5...v0.5.0) (2025-08-26)
18
+
19
+
20
+ ### Features
21
+
22
+ * support persistent user feedback as preferences ([#57](https://github.com/AIGNE-io/aigne-doc-smith/issues/57)) ([761a583](https://github.com/AIGNE-io/aigne-doc-smith/commit/761a583297b397a12d848d10d26cd5b675f8a9e7))
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * polish init question copy ([#65](https://github.com/AIGNE-io/aigne-doc-smith/issues/65)) ([d4e8762](https://github.com/AIGNE-io/aigne-doc-smith/commit/d4e8762f26fd757bde43427860a0c1dade384269))
28
+
3
29
  ## [0.4.5](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.4.4...v0.4.5) (2025-08-25)
4
30
 
5
31
 
@@ -6,6 +6,9 @@ skills:
6
6
  task_render_mode: collapse
7
7
  name: translate
8
8
  skills:
9
+ - url: ./find-user-preferences-by-path.mjs
10
+ default_input:
11
+ scope: translation
9
12
  - ./translate.yaml
10
13
  - type: transform
11
14
  jsonata: |
@@ -1,6 +1,6 @@
1
1
  import { checkMarkdown } from "../utils/markdown-checker.mjs";
2
2
 
3
- export default async function checkDetailResult({ structurePlan, reviewContent }) {
3
+ export default async function checkDetailResult({ structurePlan, reviewContent, docsDir }) {
4
4
  let isApproved = true;
5
5
  const detailFeedback = [];
6
6
 
@@ -24,6 +24,7 @@ export default async function checkDetailResult({ structurePlan, reviewContent }
24
24
  try {
25
25
  const markdownErrors = await checkMarkdown(reviewContent, "result", {
26
26
  allowedLinks,
27
+ baseDir: docsDir,
27
28
  });
28
29
 
29
30
  if (markdownErrors.length > 0) {
@@ -85,6 +85,7 @@ export default async function checkDetail(
85
85
  const validationResult = await checkDetailResult({
86
86
  structurePlan,
87
87
  reviewContent: fileContent,
88
+ docsDir,
88
89
  });
89
90
 
90
91
  if (!validationResult.isApproved) {
@@ -0,0 +1,79 @@
1
+ import { stringify } from "yaml";
2
+ import { addPreferenceRule, readPreferences } from "../utils/preferences-utils.mjs";
3
+
4
+ export default async function checkFeedbackRefiner(
5
+ { feedback, stage, selectedPaths, structurePlanFeedback },
6
+ options,
7
+ ) {
8
+ // If feedback is empty, no need to save user preferences
9
+ if (!feedback && !structurePlanFeedback) {
10
+ return {};
11
+ }
12
+
13
+ // Read existing preferences as context for deduplication
14
+ const existingPreferences = readPreferences();
15
+ const activePreferences = existingPreferences.rules?.filter((rule) => rule.active) || [];
16
+
17
+ // Convert active preferences to YAML string format for passing
18
+ const activePreferencesYaml =
19
+ activePreferences.length > 0 ? stringify({ rules: activePreferences }, { indent: 2 }) : "";
20
+
21
+ const feedbackToUse = feedback || structurePlanFeedback;
22
+ const result = await options.context.invoke(options.context.agents["feedbackRefiner"], {
23
+ feedback: feedbackToUse,
24
+ stage,
25
+ paths: selectedPaths,
26
+ existingPreferences: activePreferencesYaml,
27
+ });
28
+
29
+ // If preferences need to be saved, save them to the preference file
30
+ if (result?.save) {
31
+ try {
32
+ const savedRule = addPreferenceRule(result, feedbackToUse, selectedPaths);
33
+
34
+ // Add saved preference information to the return result
35
+ result.savedPreference = {
36
+ id: savedRule.id,
37
+ saved: true,
38
+ };
39
+ } catch (error) {
40
+ console.error(
41
+ "Failed to save user preference rule:",
42
+ error.message,
43
+ "\nFeedback:",
44
+ feedbackToUse,
45
+ );
46
+ result.savedPreference = {
47
+ saved: false,
48
+ error: error.message,
49
+ };
50
+ }
51
+ }
52
+
53
+ return result;
54
+ }
55
+
56
+ checkFeedbackRefiner.input_schema = {
57
+ type: "object",
58
+ properties: {
59
+ feedback: {
60
+ type: "string",
61
+ description: "User feedback to refine",
62
+ },
63
+ structurePlanFeedback: {
64
+ type: "string",
65
+ description: "Feedback from structure planning stage",
66
+ },
67
+ stage: {
68
+ type: "string",
69
+ description: "Stage of the feedback",
70
+ },
71
+ selectedPaths: {
72
+ type: "array",
73
+ items: {
74
+ type: "string",
75
+ },
76
+ description: "Selected paths of documents",
77
+ },
78
+ },
79
+ };
@@ -1,5 +1,6 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { getActiveRulesForScope } from "../utils/preferences-utils.mjs";
3
4
  import {
4
5
  getCurrentGitHead,
5
6
  getProjectInfo,
@@ -15,6 +16,7 @@ export default async function checkStructurePlan(
15
16
  // Check if we need to regenerate structure plan
16
17
  let shouldRegenerate = false;
17
18
  let finalFeedback = feedback;
19
+ let submittedFeedback = feedback;
18
20
 
19
21
  // Prompt for feedback if originalStructurePlan exists and no feedback provided
20
22
  if (originalStructurePlan && !feedback) {
@@ -24,6 +26,7 @@ export default async function checkStructurePlan(
24
26
 
25
27
  if (userFeedback?.trim()) {
26
28
  finalFeedback = userFeedback.trim();
29
+ submittedFeedback = userFeedback.trim();
27
30
  }
28
31
  }
29
32
 
@@ -84,9 +87,21 @@ export default async function checkStructurePlan(
84
87
 
85
88
  const panningAgent = options.context.agents["structurePlanning"];
86
89
 
90
+ // Get user preferences for structure planning and global scope
91
+ const structureRules = getActiveRulesForScope("structure", []);
92
+ const globalRules = getActiveRulesForScope("global", []);
93
+
94
+ // Combine structure and global rules, extract only rule text
95
+ const allApplicableRules = [...structureRules, ...globalRules];
96
+ const ruleTexts = allApplicableRules.map((rule) => rule.rule);
97
+
98
+ // Convert rule texts to string format for passing to the agent
99
+ const userPreferences = ruleTexts.length > 0 ? ruleTexts.join("\n\n") : "";
100
+
87
101
  const result = await options.context.invoke(panningAgent, {
88
102
  feedback: finalFeedback || "",
89
103
  originalStructurePlan,
104
+ userPreferences,
90
105
  ...rest,
91
106
  });
92
107
 
@@ -140,6 +155,7 @@ export default async function checkStructurePlan(
140
155
  return {
141
156
  ...result,
142
157
  feedback: "", // clear feedback
158
+ structurePlanFeedback: submittedFeedback,
143
159
  projectInfoMessage: message,
144
160
  originalStructurePlan: originalStructurePlan
145
161
  ? originalStructurePlan
@@ -7,6 +7,9 @@ skills:
7
7
  task_render_mode: collapse
8
8
  name: detailGenerate
9
9
  skills:
10
+ - url: ./find-user-preferences-by-path.mjs
11
+ default_input:
12
+ scope: document
10
13
  - ./content-detail-generator.yaml
11
14
  - type: transform
12
15
  jsonata: |
@@ -34,6 +34,9 @@ skills:
34
34
  - ./detail-generator-and-translate.yaml
35
35
  iterate_on: selectedDocs
36
36
  concurrency: 3
37
+ - url: ./check-feedback-refiner.mjs
38
+ default_input:
39
+ stage: document_refine
37
40
  - url: ./action-success.mjs
38
41
  default_input:
39
42
  action: "Document updated"
@@ -37,6 +37,9 @@ skills:
37
37
  ])
38
38
  - ./format-structure-plan.mjs
39
39
  - ./batch-docs-detail-generator.yaml
40
+ - url: ./check-feedback-refiner.mjs
41
+ default_input:
42
+ stage: structure_planning
40
43
  - ./save-docs.mjs
41
44
 
42
45
  input_schema:
@@ -0,0 +1,48 @@
1
+ name: feedbackRefiner
2
+ description: Analyzes a user's specific feedback on generated documentation and refines it into a general, reusable preference rule for future use.
3
+
4
+ # The instructions file contains the core logic (the prompt) for this Agent.
5
+ instructions:
6
+ url: ../prompts/feedback-refiner.md
7
+
8
+ input_schema:
9
+ type: object
10
+ properties:
11
+ feedback:
12
+ type: string
13
+ description: User's original feedback
14
+ stage:
15
+ type: string
16
+ description: The command/scenario that generated the feedback (used to infer scope), possible values. structure_planning, document_refine, translation_refine
17
+ paths:
18
+ type: array
19
+ items:
20
+ type: string
21
+ description: Optional. Document paths specified by the user in the current command
22
+ existingPreferences:
23
+ type: string
24
+ description: Optional. YAML format string of currently saved user preference rules, used to avoid duplicate saving of similar rules
25
+ required:
26
+ - feedback
27
+ - stage
28
+
29
+ output_schema:
30
+ type: object
31
+ properties:
32
+ rule:
33
+ type: string
34
+ description: Refine and summarize user feedback into a general rule
35
+ scope:
36
+ type: string
37
+ description: Rule scope. global, structure, document, translation
38
+ save:
39
+ type: boolean
40
+ description: Whether to save as persistent preference (true=save; false=one-time, do not save)
41
+ limitToInputPaths:
42
+ type: boolean
43
+ description: Whether to limit to the "paths specified in current input" when used subsequently
44
+ required:
45
+ - rule
46
+ - scope
47
+ - save
48
+ - limitToInputPaths
@@ -95,5 +95,9 @@ export default async function selectedDocs(
95
95
  // Add feedback to all results if provided
96
96
  foundItems = addFeedbackToItems(foundItems, userFeedback);
97
97
 
98
- return { selectedDocs: foundItems };
98
+ return {
99
+ selectedDocs: foundItems,
100
+ feedback: userFeedback,
101
+ selectedPaths: foundItems.map((item) => item.path),
102
+ };
99
103
  }
@@ -0,0 +1,37 @@
1
+ import { getActiveRulesForScope } from "../utils/preferences-utils.mjs";
2
+
3
+ export default async function findUserPreferencesByPath({ path, scope }) {
4
+ // Get global rules (always applicable)
5
+ const globalRules = getActiveRulesForScope("global", []);
6
+
7
+ // Get scope-specific rules (document/translation based on scope parameter)
8
+ const scopeRules = getActiveRulesForScope(scope, path ? [path] : []);
9
+
10
+ // Combine all applicable rules
11
+ const allApplicableRules = [...globalRules, ...scopeRules];
12
+
13
+ // Extract only rule text and join with double newlines
14
+ const ruleTexts = allApplicableRules.map((rule) => rule.rule);
15
+ const userPreferences = ruleTexts.length > 0 ? ruleTexts.join("\n\n") : "";
16
+
17
+ return {
18
+ userPreferences,
19
+ };
20
+ }
21
+
22
+ findUserPreferencesByPath.input_schema = {
23
+ type: "object",
24
+ properties: {
25
+ path: {
26
+ type: "string",
27
+ description: "Document path to find preferences for",
28
+ },
29
+ scope: {
30
+ type: "string",
31
+ description:
32
+ "Preference scope: 'document' for update operations, 'translation' for translate operations",
33
+ enum: ["document", "translation"],
34
+ },
35
+ },
36
+ required: ["scope"],
37
+ };
@@ -47,8 +47,7 @@ export default async function init(
47
47
 
48
48
  // 1. Primary purpose - what's the main outcome you want readers to achieve?
49
49
  const purposeChoices = await options.prompts.checkbox({
50
- message:
51
- "📝 Step 1/8: What's the main outcome you want readers to achieve? (Select all that apply)",
50
+ message: "📝 [1/8]: What is the primary goal for your readers? (Select all that apply)",
52
51
  choices: Object.entries(DOCUMENT_STYLES)
53
52
  .filter(([key]) => key !== "custom") // Remove custom option for multiselect
54
53
  .map(([key, style]) => ({
@@ -96,7 +95,7 @@ export default async function init(
96
95
 
97
96
  // 2. Target audience - who will be reading this most often?
98
97
  const audienceChoices = await options.prompts.checkbox({
99
- message: "👥 Step 2/8: Who will be reading this most often? (Select all that apply)",
98
+ message: "👥 [2/8]: Who is the primary audience for this documentation?",
100
99
  choices: Object.entries(TARGET_AUDIENCES)
101
100
  .filter(([key]) => key !== "custom") // Remove custom option for multiselect
102
101
  .map(([key, audience]) => ({
@@ -130,7 +129,7 @@ export default async function init(
130
129
  );
131
130
 
132
131
  const knowledgeChoice = await options.prompts.select({
133
- message: "🧠 Step 3/8: What do readers typically know when they arrive?",
132
+ message: "🧠 [3/8]: What is your reader's typical starting knowledge level?",
134
133
  choices: Object.entries(filteredKnowledgeOptions).map(([key, level]) => ({
135
134
  name: `${level.name}`,
136
135
  description: level.description,
@@ -175,7 +174,7 @@ export default async function init(
175
174
  );
176
175
 
177
176
  const depthChoice = await options.prompts.select({
178
- message: "📊 Step 4/8: How comprehensive should the documentation be?",
177
+ message: "📊 [4/8]: How comprehensive should the documentation be?",
179
178
  choices: Object.entries(filteredDepthOptions).map(([key, depth]) => ({
180
179
  name: `${depth.name}`,
181
180
  description: depth.description,
@@ -193,7 +192,7 @@ export default async function init(
193
192
 
194
193
  // Let user select primary language from supported list
195
194
  const primaryLanguageChoice = await options.prompts.select({
196
- message: "🌐 Step 5/8: Choose primary documentation language:",
195
+ message: "🌐 [5/8]: Choose primary documentation language:",
197
196
  choices: SUPPORTED_LANGUAGES.map((lang) => ({
198
197
  name: `${lang.label} - ${lang.sample}`,
199
198
  value: lang.code,
@@ -210,7 +209,7 @@ export default async function init(
210
209
  );
211
210
 
212
211
  const translateLanguageChoices = await options.prompts.checkbox({
213
- message: "🔄 Step 6/8: Select translation languages:",
212
+ message: "🔄 [6/8]: Select translation languages:",
214
213
  choices: availableTranslationLanguages.map((lang) => ({
215
214
  name: `${lang.label} - ${lang.sample}`,
216
215
  value: lang.code,
@@ -221,13 +220,13 @@ export default async function init(
221
220
 
222
221
  // 7. Documentation directory
223
222
  const docsDirInput = await options.prompts.input({
224
- message: `📁 Step 7/8: Where to save generated docs:`,
223
+ message: `📁 [7/8]: Where to save generated docs:`,
225
224
  default: `${outputPath}/docs`,
226
225
  });
227
226
  input.docsDir = docsDirInput.trim() || `${outputPath}/docs`;
228
227
 
229
228
  // 8. Source code paths
230
- console.log("\n🔍 Step 8/8: Source Code Paths");
229
+ console.log("\n🔍 [8/8]: Source Code Paths");
231
230
  console.log("Enter paths to analyze for documentation (e.g., ./src, ./lib)");
232
231
  console.log("💡 If no paths are configured, './' will be used as default");
233
232
 
@@ -86,17 +86,55 @@ export default async function loadSources({
86
86
  }
87
87
 
88
88
  files = [...new Set(files)];
89
+
90
+ // Define media file extensions
91
+ const mediaExtensions = [
92
+ ".jpg",
93
+ ".jpeg",
94
+ ".png",
95
+ ".gif",
96
+ ".bmp",
97
+ ".webp",
98
+ ".svg",
99
+ ".mp4",
100
+ ".mov",
101
+ ".avi",
102
+ ".mkv",
103
+ ".webm",
104
+ ".m4v",
105
+ ];
106
+
107
+ // Separate source files from media files
108
+ const sourceFiles = [];
109
+ const mediaFiles = [];
89
110
  let allSources = "";
90
- const sourceFiles = await Promise.all(
111
+
112
+ await Promise.all(
91
113
  files.map(async (file) => {
92
- const content = await readFile(file, "utf8");
93
- // Convert absolute path to relative path from project root
94
- const relativePath = path.relative(process.cwd(), file);
95
- allSources += `// sourceId: ${relativePath}\n${content}\n`;
96
- return {
97
- sourceId: relativePath,
98
- content,
99
- };
114
+ const ext = path.extname(file).toLowerCase();
115
+
116
+ if (mediaExtensions.includes(ext)) {
117
+ // This is a media file
118
+ const relativePath = path.relative(docsDir, file);
119
+ const fileName = path.basename(file);
120
+ const description = path.parse(fileName).name;
121
+
122
+ mediaFiles.push({
123
+ name: fileName,
124
+ path: relativePath,
125
+ description,
126
+ });
127
+ } else {
128
+ // This is a source file
129
+ const content = await readFile(file, "utf8");
130
+ const relativePath = path.relative(process.cwd(), file);
131
+ allSources += `// sourceId: ${relativePath}\n${content}\n`;
132
+
133
+ sourceFiles.push({
134
+ sourceId: relativePath,
135
+ content,
136
+ });
137
+ }
100
138
  }),
101
139
  );
102
140
 
@@ -164,6 +202,17 @@ export default async function loadSources({
164
202
  }
165
203
  }
166
204
 
205
+ // Generate assets content from media files
206
+ let assetsContent = "# Available Media Assets for Documentation\n\n";
207
+
208
+ if (mediaFiles.length > 0) {
209
+ const mediaMarkdown = mediaFiles
210
+ .map((file) => `![${file.description}](${file.path})`)
211
+ .join("\n\n");
212
+
213
+ assetsContent += mediaMarkdown;
214
+ }
215
+
167
216
  // Count words and lines in allSources
168
217
  let totalWords = 0;
169
218
  let totalLines = 0;
@@ -188,6 +237,7 @@ export default async function loadSources({
188
237
  modifiedFiles,
189
238
  totalWords,
190
239
  totalLines,
240
+ assetsContent,
191
241
  };
192
242
  }
193
243
 
@@ -257,6 +307,10 @@ loadSources.output_schema = {
257
307
  items: { type: "string" },
258
308
  description: "Array of modified files since last generation",
259
309
  },
310
+ assetsContent: {
311
+ type: "string",
312
+ description: "Markdown content for available media assets",
313
+ },
260
314
  },
261
315
  };
262
316