@aigne/doc-smith 0.1.4 → 0.2.1

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.
@@ -0,0 +1,28 @@
1
+ ## Related Issue
2
+
3
+ <!-- Use keywords like fixes, closes, resolves, relates to link the issue. In principle, all PRs should be associated with an issue -->
4
+
5
+ ### Major Changes
6
+
7
+ <!--
8
+ @example:
9
+ 1. Fixed xxx
10
+ 2. Improved xxx
11
+ 3. Adjusted xxx
12
+ -->
13
+
14
+ ### Screenshots
15
+
16
+ <!-- If the changes are related to the UI, whether CLI or WEB, screenshots should be included -->
17
+
18
+ ### Test Plan
19
+
20
+ <!-- If this change is not covered by automated tests, what is your test case collection? Please write it as a to-do list below -->
21
+
22
+ ### Checklist
23
+
24
+ - [ ] This change requires documentation updates, and I have updated the relevant documentation. If the documentation has not been updated, please create a documentation update issue and link it here
25
+ - [ ] The changes are already covered by tests, and I have adjusted the test coverage for the changed parts
26
+ - [ ] The newly added code logic is also covered by tests
27
+ - [ ] This change adds dependencies, and they are placed in dependencies and devDependencies
28
+ - [ ] This change includes adding or updating npm dependencies, and it does not result in multiple versions of the same dependency [check the diff of pnpm-lock.yaml]
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.2.0...v0.2.1) (2025-08-06)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * release 0.2.1 ([e3a39ae](https://github.com/AIGNE-io/aigne-doc-smith/commit/e3a39aedcee129deae424e96942f9798b9191663))
9
+
10
+ ## [0.2.0](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.1.4...v0.2.0) (2025-08-05)
11
+
12
+
13
+ ### Features
14
+
15
+ * support automatic init configuration when calling agents ([24d29db](https://github.com/AIGNE-io/aigne-doc-smith/commit/24d29db4dd86709750aa22ff649e7dacc4124126))
16
+ * update docs when sources changed ([#9](https://github.com/AIGNE-io/aigne-doc-smith/issues/9)) ([4adcecf](https://github.com/AIGNE-io/aigne-doc-smith/commit/4adcecfb32e72c9e88d0b0bd8ce0a91022847ca7))
17
+
3
18
  ## [0.1.4](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.1.3...v0.1.4) (2025-08-04)
4
19
 
5
20
 
package/README.md CHANGED
@@ -63,8 +63,10 @@ npx --no doc-smith run --entry-agent init
63
63
  # 生成命令
64
64
  npx --no doc-smith run --entry-agent generate --model gemini:gemini-2.5-flash
65
65
 
66
+ aigne run --path /Users/lban/arcblock/code/aigne-doc-smith/ --entry-agent generate --model gemini:gemini-2.5-flash --input-forceRegenerate=true
67
+
66
68
  # 重新生成单篇
67
- npx --no doc-smith run --entry-agent update --input-path bitnet-getting-started
69
+ npx --no doc-smith run --entry-agent update --input-doc-path bitnet-getting-started
68
70
 
69
71
  # 结构规划优化
70
72
  npx --no doc-smith run --entry-agent generate --input-feedback "补充节点的 sourceIds,确保所有节点 sourceIds 都有值" --model gemini:gemini-2.5-pro
@@ -1,8 +1,8 @@
1
1
  type: team
2
- name: batch-docs-detail-generator
2
+ name: batchDocsDetailGenerator
3
3
  description: 批量生成文档详情
4
4
  skills:
5
- - ./check-detail-generated.mjs
5
+ - ./check-detail.mjs
6
6
  input_schema:
7
7
  type: object
8
8
  properties:
@@ -10,5 +10,10 @@ input_schema:
10
10
  type: string
11
11
  description: 结构规划的上下文,用于辅助结构规划
12
12
  structurePlanResult: ./schema/structure-plan-result.yaml
13
+ modifiedFiles:
14
+ type: array
15
+ items: { type: string }
16
+ description: Array of modified files since last generation
13
17
  iterate_on: structurePlanResult
18
+ concurrency: 3
14
19
  mode: sequential
@@ -1,5 +1,5 @@
1
1
  type: team
2
- name: batch-translate
2
+ name: batchTranslate
3
3
  description: 批量翻译文档到多个语言
4
4
  skills:
5
5
  - type: team
@@ -51,49 +51,35 @@ export default async function checkDetailResult({
51
51
  }
52
52
  };
53
53
 
54
- const checkTableSeparators = (text, source) => {
55
- // Split text into lines and check each line
54
+ const performAllChecks = (text, source) => {
55
+ // Split text into lines once and perform all checks in a single pass
56
56
  const lines = text.split("\n");
57
- for (let i = 0; i < lines.length; i++) {
58
- const line = lines[i];
59
- if (tableSeparatorRegex.test(line)) {
60
- isApproved = false;
61
- detailFeedback.push(
62
- `Found an incorrect table separator in ${source} at line ${
63
- i + 1
64
- }: ${line.trim()}`
65
- );
66
- }
67
- }
68
- };
69
57
 
70
- const checkSingleLine = (text, source) => {
71
- // Count newline characters to check if content is only on one line
72
- const newlineCount = (text.match(/\n/g) || []).length;
73
- if (newlineCount === 0 && text.trim().length > 0) {
74
- isApproved = false;
75
- detailFeedback.push(
76
- `Found single line content in ${source}: content appears to be on only one line, check for missing line breaks`
77
- );
78
- }
79
- };
80
-
81
- const checkCodeBlockIndentation = (text, source) => {
82
- // Split text into lines and check each line
83
- const lines = text.split("\n");
58
+ // State variables for different checks
84
59
  let inCodeBlock = false;
85
60
  let codeBlockIndentLevel = 0;
86
61
  let codeBlockStartLine = 0;
62
+ let inMermaidBlock = false;
63
+ let mermaidStartLine = 0;
87
64
 
88
65
  for (let i = 0; i < lines.length; i++) {
89
66
  const line = lines[i];
67
+ const lineNumber = i + 1;
90
68
 
91
- // Check if this line is a code block marker
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
92
78
  if (codeBlockRegex.test(line)) {
93
79
  if (!inCodeBlock) {
94
80
  // Starting a new code block
95
81
  inCodeBlock = true;
96
- codeBlockStartLine = i + 1;
82
+ codeBlockStartLine = lineNumber;
97
83
  // Calculate indentation level of the code block marker
98
84
  const match = line.match(/^(\s*)(```)/);
99
85
  codeBlockIndentLevel = match ? match[1].length : 0;
@@ -102,11 +88,8 @@ export default async function checkDetailResult({
102
88
  inCodeBlock = false;
103
89
  codeBlockIndentLevel = 0;
104
90
  }
105
- continue;
106
- }
107
-
108
- // If we're inside a code block, check if content has proper indentation
109
- if (inCodeBlock) {
91
+ } else if (inCodeBlock) {
92
+ // If we're inside a code block, check if content has proper indentation
110
93
  const contentIndentLevel = line.match(/^(\s*)/)[1].length;
111
94
 
112
95
  // If code block marker has indentation, content should have at least the same indentation
@@ -116,23 +99,51 @@ export default async function checkDetailResult({
116
99
  ) {
117
100
  isApproved = false;
118
101
  detailFeedback.push(
119
- `Found code block with inconsistent indentation in ${source} at line ${codeBlockStartLine}: code block marker has ${codeBlockIndentLevel} spaces indentation but content at line ${
120
- i + 1
121
- } has only ${contentIndentLevel} spaces indentation`
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`
122
103
  );
123
104
  // Reset to avoid multiple errors for the same code block
124
105
  inCodeBlock = false;
125
106
  codeBlockIndentLevel = 0;
126
107
  }
127
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) {
137
+ 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
+ );
128
141
  }
129
142
  };
130
143
 
131
144
  // Check content
132
145
  checkLinks(reviewContent, "result");
133
- checkTableSeparators(reviewContent, "result");
134
- checkSingleLine(reviewContent, "result");
135
- checkCodeBlockIndentation(reviewContent, "result");
146
+ performAllChecks(reviewContent, "result");
136
147
 
137
148
  return {
138
149
  isApproved,
@@ -3,12 +3,23 @@ import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { TeamAgent } from "@aigne/core";
5
5
  import checkDetailResult from "./check-detail-result.mjs";
6
+ import { hasSourceFilesChanged } from "../utils/utils.mjs";
6
7
 
7
8
  // Get current script directory
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
10
 
10
- export default async function checkDetailGenerated(
11
- { path, docsDir, sourceIds, originalStructurePlan, structurePlan, ...rest },
11
+ export default async function checkDetail(
12
+ {
13
+ path,
14
+ docsDir,
15
+ sourceIds,
16
+ originalStructurePlan,
17
+ structurePlan,
18
+ modifiedFiles,
19
+ lastGitHead,
20
+ forceRegenerate,
21
+ ...rest
22
+ },
12
23
  options
13
24
  ) {
14
25
  // Check if the detail file already exists
@@ -61,6 +72,21 @@ export default async function checkDetailGenerated(
61
72
  }
62
73
  }
63
74
 
75
+ // Check if source files have changed since last generation
76
+ let sourceFilesChanged = false;
77
+ if (sourceIds && sourceIds.length > 0 && modifiedFiles) {
78
+ sourceFilesChanged = hasSourceFilesChanged(sourceIds, modifiedFiles);
79
+
80
+ if (sourceFilesChanged) {
81
+ console.log(`Source files changed for ${path}, will regenerate`);
82
+ }
83
+ }
84
+
85
+ // If lastGitHead is not set, regenerate
86
+ if (!lastGitHead) {
87
+ sourceFilesChanged = true;
88
+ }
89
+
64
90
  // If file exists, check content validation
65
91
  let contentValidationFailed = false;
66
92
  if (detailGenerated && fileContent && structurePlan) {
@@ -74,8 +100,14 @@ export default async function checkDetailGenerated(
74
100
  }
75
101
  }
76
102
 
77
- // If file exists, sourceIds haven't changed, and content validation passes, no need to regenerate
78
- if (detailGenerated && !sourceIdsChanged && !contentValidationFailed) {
103
+ // If file exists, sourceIds haven't changed, source files haven't changed, and content validation passes, no need to regenerate
104
+ if (
105
+ detailGenerated &&
106
+ !sourceIdsChanged &&
107
+ !sourceFilesChanged &&
108
+ !contentValidationFailed &&
109
+ forceRegenerate !== "true"
110
+ ) {
79
111
  return {
80
112
  path,
81
113
  docsDir,
@@ -85,8 +117,8 @@ export default async function checkDetailGenerated(
85
117
  }
86
118
 
87
119
  const teamAgent = TeamAgent.from({
88
- name: "generate-detail",
89
- skills: [options.context.agents["detail-generator-and-translate"]],
120
+ name: "generateDetail",
121
+ skills: [options.context.agents["detailGeneratorAndTranslate"]],
90
122
  });
91
123
 
92
124
  const result = await options.context.invoke(teamAgent, {
@@ -0,0 +1,72 @@
1
+ import {
2
+ getCurrentGitHead,
3
+ hasFileChangesBetweenCommits,
4
+ } from "../utils/utils.mjs";
5
+
6
+ export default async function checkStructurePlan(
7
+ { originalStructurePlan, feedback, lastGitHead, forceRegenerate, ...rest },
8
+ options
9
+ ) {
10
+ // Check if we need to regenerate structure plan
11
+ let shouldRegenerate = false;
12
+ let finalFeedback = feedback;
13
+
14
+ // If no feedback and originalStructurePlan exists, check for git changes
15
+ if (originalStructurePlan) {
16
+ // If no lastGitHead, regenerate by default
17
+ if (!lastGitHead) {
18
+ shouldRegenerate = true;
19
+ } else {
20
+ // Check if there are relevant file changes since last generation
21
+ const currentGitHead = getCurrentGitHead();
22
+ if (currentGitHead && currentGitHead !== lastGitHead) {
23
+ const hasChanges = hasFileChangesBetweenCommits(
24
+ lastGitHead,
25
+ currentGitHead
26
+ );
27
+ if (hasChanges) {
28
+ shouldRegenerate = true;
29
+ }
30
+ }
31
+ }
32
+
33
+ if (shouldRegenerate) {
34
+ finalFeedback = `
35
+ ${finalFeedback || ""}
36
+
37
+ 根据最新的 DataSources 更新结构规划:
38
+ 1. 对于新增的内容,可以根据需要新增节点,或补充到原有节点展示
39
+ 2. 谨慎删除节点,除非节点关联 sourceIds 都被删除了
40
+ 3. 不能修改原有节点的 path
41
+ `;
42
+ }
43
+ }
44
+
45
+ // If no regeneration needed, return original structure plan
46
+ if (
47
+ originalStructurePlan &&
48
+ !feedback &&
49
+ !shouldRegenerate &&
50
+ forceRegenerate !== "true"
51
+ ) {
52
+ return {
53
+ structurePlan: originalStructurePlan,
54
+ };
55
+ }
56
+
57
+ const panningAgent = options.context.agents["reflectiveStructurePlanner"];
58
+
59
+ const result = await options.context.invoke(panningAgent, {
60
+ feedback: finalFeedback || "",
61
+ originalStructurePlan,
62
+ ...rest,
63
+ });
64
+
65
+ return {
66
+ ...result,
67
+ feedback: "", // clear feedback
68
+ originalStructurePlan: originalStructurePlan
69
+ ? originalStructurePlan
70
+ : JSON.parse(JSON.stringify(result.structurePlan || [])),
71
+ };
72
+ }
@@ -1,4 +1,4 @@
1
- name: check-structure-planning-result
1
+ name: checkStructurePlanResult
2
2
  description: 对 structure-planning agent 生成的结果进行检查,确保其符合预期,特别是在有上一轮生成结果和用户反馈的场景下。
3
3
  instructions:
4
4
  url: ../prompts/check-structure-planning-result.md
@@ -1,4 +1,4 @@
1
- name: content-detail-generator
1
+ name: contentDetailGenerator
2
2
  description: 通用内容详情生成器,支持网站、文档、书籍、演示文稿等多种场景
3
3
  instructions:
4
4
  url: ../prompts/content-detail-generator.md
@@ -1,5 +1,5 @@
1
1
  type: team
2
- name: detail-generator-and-translate
2
+ name: detailGeneratorAndTranslate
3
3
  description: 生成文档详情并翻译
4
4
  skills:
5
5
  - ./transform-detail-datasources.mjs
@@ -4,6 +4,9 @@ alias:
4
4
  - up
5
5
  description: Optimize and regenerate individual document content and translations
6
6
  skills:
7
+ - url: ./input-generator.mjs
8
+ default_input:
9
+ skipIfExists: true
7
10
  - ./load-config.mjs
8
11
  - ./load-sources.mjs
9
12
  - type: transform
@@ -5,18 +5,20 @@ alias:
5
5
  - g
6
6
  description: Automatically generates comprehensive project documentation
7
7
  skills:
8
+ - url: ./input-generator.mjs
9
+ default_input:
10
+ skipIfExists: true
8
11
  - ./load-config.mjs
9
12
  - ./load-sources.mjs
10
- - ./check-structure-planning.mjs
11
- - type: transform
12
- jsonata: |
13
- $merge([$, {
14
- "saveKey": "structurePlan",
15
- "savePath": outputDir,
16
- "fileName": "structure-plan.json"
17
- }])
18
- - ./save-output.mjs
13
+ - ./check-structure-plan.mjs
14
+ - url: ./save-output.mjs
15
+ default_input:
16
+ saveKey: structurePlan
17
+ savePath:
18
+ $get: outputDir
19
+ fileName: structure-plan.json
19
20
  - type: transform
21
+ name: transformData
20
22
  jsonata: |
21
23
  $merge([
22
24
  $,
@@ -100,6 +102,9 @@ input_schema:
100
102
  feedback:
101
103
  type: string
102
104
  description: Feedback for structure planning adjustments
105
+ forceRegenerate:
106
+ type: string
107
+ description: Force regenerate the documentation
103
108
  # labels:
104
109
  # type: array
105
110
  # items:
@@ -1,10 +1,89 @@
1
- export default async function findItemByPath({
2
- "doc-path": docPath,
3
- structurePlanResult,
4
- boardId,
5
- }) {
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ export default async function findItemByPath(
5
+ { "doc-path": docPath, structurePlanResult, boardId, docsDir },
6
+ options
7
+ ) {
6
8
  let foundItem = null;
7
9
 
10
+ // If docPath is empty, let user select from available documents
11
+ if (!docPath) {
12
+ try {
13
+ // Get all .md files in docsDir
14
+ const files = await readdir(docsDir);
15
+
16
+ // Filter for main language .md files (exclude _sidebar.md and language-specific files)
17
+ const mainLanguageFiles = files.filter(
18
+ (file) =>
19
+ file.endsWith(".md") &&
20
+ file !== "_sidebar.md" &&
21
+ !file.match(/\.\w+(-\w+)?\.md$/) // Exclude language-specific files like .en.md, .zh-CN.md, etc.
22
+ );
23
+
24
+ if (mainLanguageFiles.length === 0) {
25
+ throw new Error(
26
+ "Please provide a doc-path parameter to specify which document to update"
27
+ );
28
+ }
29
+
30
+ // Let user select a file
31
+ const selectedFile = await options.prompts.search({
32
+ message: "Select a document to update:",
33
+ source: async (input, { signal }) => {
34
+ if (!input || input.trim() === "") {
35
+ return mainLanguageFiles.map((file) => ({
36
+ name: file,
37
+ value: file,
38
+ }));
39
+ }
40
+
41
+ const searchTerm = input.trim().toLowerCase();
42
+ const filteredFiles = mainLanguageFiles.filter((file) =>
43
+ file.toLowerCase().includes(searchTerm)
44
+ );
45
+
46
+ return filteredFiles.map((file) => ({
47
+ name: file,
48
+ value: file,
49
+ }));
50
+ },
51
+ });
52
+
53
+ if (!selectedFile) {
54
+ throw new Error(
55
+ "Please provide a doc-path parameter to specify which document to update"
56
+ );
57
+ }
58
+
59
+ // Convert filename back to path
60
+ // Remove .md extension
61
+ const flatName = selectedFile.replace(/\.md$/, "");
62
+
63
+ // Try to find matching item by comparing flattened paths
64
+ let foundItemByFile = null;
65
+
66
+ // First try without boardId prefix
67
+ foundItemByFile = structurePlanResult.find((item) => {
68
+ const itemFlattenedPath = item.path
69
+ .replace(/^\//, "")
70
+ .replace(/\//g, "-");
71
+ return itemFlattenedPath === flatName;
72
+ });
73
+ if (!foundItemByFile) {
74
+ throw new Error(
75
+ "Please provide a doc-path parameter to specify which document to update"
76
+ );
77
+ }
78
+
79
+ docPath = foundItemByFile.path;
80
+ } catch (error) {
81
+ throw new Error(
82
+ "Please provide a doc-path parameter to specify which document to update"
83
+ );
84
+ }
85
+ }
86
+
8
87
  // First try direct path matching
9
88
  foundItem = structurePlanResult.find((item) => item.path === docPath);
10
89