@aigne/doc-smith 0.2.0 → 0.2.2

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.2](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.2.1...v0.2.2) (2025-08-07)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * release 0.2.2 ([c3fb52a](https://github.com/AIGNE-io/aigne-doc-smith/commit/c3fb52a78b95676e1c13361b30ebec2914a89fa8))
9
+
10
+ ## [0.2.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.2.0...v0.2.1) (2025-08-06)
11
+
12
+
13
+ ### Miscellaneous Chores
14
+
15
+ * release 0.2.1 ([e3a39ae](https://github.com/AIGNE-io/aigne-doc-smith/commit/e3a39aedcee129deae424e96942f9798b9191663))
16
+
3
17
  ## [0.2.0](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.1.4...v0.2.0) (2025-08-05)
4
18
 
5
19
 
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:
@@ -15,4 +15,5 @@ input_schema:
15
15
  items: { type: string }
16
16
  description: Array of modified files since last generation
17
17
  iterate_on: structurePlanResult
18
+ concurrency: 3
18
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
@@ -8,7 +8,7 @@ import { hasSourceFilesChanged } from "../utils/utils.mjs";
8
8
  // Get current script directory
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
 
11
- export default async function checkDetailGenerated(
11
+ export default async function checkDetail(
12
12
  {
13
13
  path,
14
14
  docsDir,
@@ -17,6 +17,7 @@ export default async function checkDetailGenerated(
17
17
  structurePlan,
18
18
  modifiedFiles,
19
19
  lastGitHead,
20
+ forceRegenerate,
20
21
  ...rest
21
22
  },
22
23
  options
@@ -104,7 +105,8 @@ export default async function checkDetailGenerated(
104
105
  detailGenerated &&
105
106
  !sourceIdsChanged &&
106
107
  !sourceFilesChanged &&
107
- !contentValidationFailed
108
+ !contentValidationFailed &&
109
+ forceRegenerate !== "true"
108
110
  ) {
109
111
  return {
110
112
  path,
@@ -115,8 +117,8 @@ export default async function checkDetailGenerated(
115
117
  }
116
118
 
117
119
  const teamAgent = TeamAgent.from({
118
- name: "generate-detail",
119
- skills: [options.context.agents["detail-generator-and-translate"]],
120
+ name: "generateDetail",
121
+ skills: [options.context.agents["detailGeneratorAndTranslate"]],
120
122
  });
121
123
 
122
124
  const result = await options.context.invoke(teamAgent, {
@@ -3,8 +3,8 @@ import {
3
3
  hasFileChangesBetweenCommits,
4
4
  } from "../utils/utils.mjs";
5
5
 
6
- export default async function checkStructurePlanning(
7
- { originalStructurePlan, feedback, lastGitHead, ...rest },
6
+ export default async function checkStructurePlan(
7
+ { originalStructurePlan, feedback, lastGitHead, forceRegenerate, ...rest },
8
8
  options
9
9
  ) {
10
10
  // Check if we need to regenerate structure plan
@@ -43,13 +43,18 @@ export default async function checkStructurePlanning(
43
43
  }
44
44
 
45
45
  // If no regeneration needed, return original structure plan
46
- if (originalStructurePlan && !feedback && !shouldRegenerate) {
46
+ if (
47
+ originalStructurePlan &&
48
+ !feedback &&
49
+ !shouldRegenerate &&
50
+ forceRegenerate !== "true"
51
+ ) {
47
52
  return {
48
53
  structurePlan: originalStructurePlan,
49
54
  };
50
55
  }
51
56
 
52
- const panningAgent = options.context.agents["reflective-structure-planner"];
57
+ const panningAgent = options.context.agents["reflectiveStructurePlanner"];
53
58
 
54
59
  const result = await options.context.invoke(panningAgent, {
55
60
  feedback: finalFeedback || "",
@@ -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
@@ -10,16 +10,15 @@ skills:
10
10
  skipIfExists: true
11
11
  - ./load-config.mjs
12
12
  - ./load-sources.mjs
13
- - ./check-structure-planning.mjs
14
- - type: transform
15
- jsonata: |
16
- $merge([$, {
17
- "saveKey": "structurePlan",
18
- "savePath": outputDir,
19
- "fileName": "structure-plan.json"
20
- }])
21
- - ./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
22
20
  - type: transform
21
+ name: transformData
23
22
  jsonata: |
24
23
  $merge([
25
24
  $,
@@ -103,6 +102,9 @@ input_schema:
103
102
  feedback:
104
103
  type: string
105
104
  description: Feedback for structure planning adjustments
105
+ forceRegenerate:
106
+ type: string
107
+ description: Force regenerate the documentation
106
108
  # labels:
107
109
  # type: array
108
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
 
@@ -1,37 +1,15 @@
1
1
  import { writeFile, mkdir, readFile } from "node:fs/promises";
2
2
  import { join, dirname } from "node:path";
3
+ import chalk from "chalk";
4
+ import { validatePath, getAvailablePaths } from "../utils/utils.mjs";
5
+ import {
6
+ SUPPORTED_LANGUAGES,
7
+ DOCUMENT_STYLES,
8
+ TARGET_AUDIENCES,
9
+ } from "../utils/constants.mjs";
3
10
 
4
- // Predefined document generation styles
5
- const DOCUMENT_STYLES = {
6
- actionFirst: {
7
- name: "Action-First Style",
8
- rules:
9
- "Action-first and task-oriented; steps first, copyable examples, minimal context; second person, active voice, short sentences",
10
- },
11
- conceptFirst: {
12
- name: "Concept-First Style",
13
- rules:
14
- "Why/What before How; precise and restrained, provide trade-offs and comparisons; support with architecture/flow/sequence diagrams",
15
- },
16
- specReference: {
17
- name: "Spec-Reference Style",
18
- rules:
19
- "Objective and precise, no rhetoric; tables/Schema focused, authoritative fields and defaults; clear error codes and multi-language examples",
20
- },
21
- custom: {
22
- name: "Custom Rules",
23
- rules: "Enter your own documentation generation rules",
24
- },
25
- };
26
-
27
- // Predefined target audiences
28
- const TARGET_AUDIENCES = {
29
- actionFirst: "Developers, Implementation Engineers, DevOps",
30
- conceptFirst:
31
- "Architects, Technical Leads, Developers interested in principles",
32
- generalUsers: "General Users",
33
- custom: "Enter your own target audience",
34
- };
11
+ // UI constants
12
+ const PRESS_ENTER_TO_FINISH = "Press Enter to finish";
35
13
 
36
14
  /**
37
15
  * Guide users through multi-turn dialogue to collect information and generate YAML configuration
@@ -41,17 +19,21 @@ const TARGET_AUDIENCES = {
41
19
  * @returns {Promise<Object>}
42
20
  */
43
21
  export default async function init(
44
- { outputPath = "./doc-smith", fileName = "config.yaml", skipIfExists = false },
22
+ {
23
+ outputPath = "./doc-smith",
24
+ fileName = "config.yaml",
25
+ skipIfExists = false,
26
+ },
45
27
  options
46
28
  ) {
47
29
  if (skipIfExists) {
48
30
  const filePath = join(outputPath, fileName);
49
31
  if (await readFile(filePath, "utf8").catch(() => null)) {
50
- return {}
32
+ return {};
51
33
  }
52
34
  }
53
35
 
54
- console.log("🚀 Welcome to AIGNE Doc Smith!");
36
+ console.log("🚀 Welcome to AIGNE DocSmith!");
55
37
  console.log("Let's create your documentation configuration.\n");
56
38
 
57
39
  // Collect user information
@@ -78,7 +60,6 @@ export default async function init(
78
60
  } else {
79
61
  // Use predefined style directly
80
62
  rules = DOCUMENT_STYLES[styleChoice].rules;
81
- console.log(`✅ Selected: ${DOCUMENT_STYLES[styleChoice].name}`);
82
63
  }
83
64
 
84
65
  input.rules = rules.trim();
@@ -104,58 +85,105 @@ export default async function init(
104
85
  } else {
105
86
  // Use predefined audience directly
106
87
  targetAudience = TARGET_AUDIENCES[audienceChoice];
107
- console.log(`✅ Selected: ${TARGET_AUDIENCES[audienceChoice]}`);
108
88
  }
109
89
 
110
90
  input.targetAudience = targetAudience.trim();
111
91
 
112
92
  // 3. Language settings
113
93
  console.log("\n🌐 Step 3/6: Primary Language");
114
- const localeInput = await options.prompts.input({
115
- message:
116
- "Primary documentation language (e.g., en, zh, press Enter for 'en'):",
94
+
95
+ // Let user select primary language from supported list
96
+ const primaryLanguageChoice = await options.prompts.select({
97
+ message: "Choose primary documentation language:",
98
+ choices: SUPPORTED_LANGUAGES.map((lang) => ({
99
+ name: `${lang.label} - ${lang.sample}`,
100
+ value: lang.code,
101
+ })),
117
102
  });
118
- input.locale = localeInput.trim() || "en";
103
+
104
+ input.locale = primaryLanguageChoice;
119
105
 
120
106
  // 4. Translation languages
121
107
  console.log("\n🔄 Step 4/6: Translation Languages");
122
- console.log(
123
- "Enter additional languages for translation (press Enter to skip):"
108
+
109
+ // Filter out the primary language from available choices
110
+ const availableTranslationLanguages = SUPPORTED_LANGUAGES.filter(
111
+ (lang) => lang.code !== primaryLanguageChoice
124
112
  );
125
- const translateLanguages = [];
126
- while (true) {
127
- const langInput = await options.prompts.input({
128
- message: `Language ${translateLanguages.length + 1} (e.g., zh, ja, fr):`,
129
- });
130
- if (!langInput.trim()) {
131
- break;
132
- }
133
- translateLanguages.push(langInput.trim());
134
- }
135
- input.translateLanguages = translateLanguages;
113
+
114
+ const translateLanguageChoices = await options.prompts.checkbox({
115
+ message: "Select translation languages:",
116
+ choices: availableTranslationLanguages.map((lang) => ({
117
+ name: `${lang.label} - ${lang.sample}`,
118
+ value: lang.code,
119
+ })),
120
+ });
121
+
122
+ input.translateLanguages = translateLanguageChoices;
136
123
 
137
124
  // 5. Documentation directory
138
125
  console.log("\n📁 Step 5/6: Output Directory");
139
126
  const docsDirInput = await options.prompts.input({
140
- message: `Where to save generated docs (press Enter for '${outputPath}/docs'):`,
127
+ message: `Where to save generated docs:`,
128
+ default: `${outputPath}/docs`,
141
129
  });
142
130
  input.docsDir = docsDirInput.trim() || `${outputPath}/docs`;
143
131
 
144
132
  // 6. Source code paths
145
133
  console.log("\n🔍 Step 6/6: Source Code Paths");
146
- console.log(
147
- "Enter paths to analyze for documentation (press Enter to use './'):"
148
- );
134
+ console.log("Enter paths to analyze for documentation (e.g., ./src, ./lib)");
135
+ console.log("💡 If no paths are configured, './' will be used as default");
149
136
 
150
137
  const sourcePaths = [];
151
138
  while (true) {
152
- const pathInput = await options.prompts.input({
153
- message: `Path ${sourcePaths.length + 1} (e.g., ./src, ./lib):`,
139
+ const selectedPath = await options.prompts.search({
140
+ message: "Path:",
141
+ source: async (input, { signal }) => {
142
+ if (!input || input.trim() === "") {
143
+ return [
144
+ {
145
+ name: "Press Enter to finish",
146
+ value: "",
147
+ description: "",
148
+ },
149
+ ];
150
+ }
151
+
152
+ const searchTerm = input.trim();
153
+
154
+ // Search for matching files and folders in current directory
155
+ const availablePaths = getAvailablePaths(searchTerm);
156
+
157
+ return [...availablePaths];
158
+ },
154
159
  });
155
- if (!pathInput.trim()) {
160
+
161
+ // Check if user chose to exit
162
+ if (
163
+ !selectedPath ||
164
+ selectedPath.trim() === "" ||
165
+ selectedPath === "Press Enter to finish"
166
+ ) {
156
167
  break;
157
168
  }
158
- sourcePaths.push(pathInput.trim());
169
+
170
+ const trimmedPath = selectedPath.trim();
171
+
172
+ // Use validatePath to check if path is valid
173
+ const validation = validatePath(trimmedPath);
174
+
175
+ if (!validation.isValid) {
176
+ console.log(`⚠️ ${validation.error}`);
177
+ continue;
178
+ }
179
+
180
+ // Avoid duplicate paths
181
+ if (sourcePaths.includes(trimmedPath)) {
182
+ console.log(`⚠️ Path already exists: ${trimmedPath}`);
183
+ continue;
184
+ }
185
+
186
+ sourcePaths.push(trimmedPath);
159
187
  }
160
188
 
161
189
  // If no paths entered, use default
@@ -173,18 +201,17 @@ export default async function init(
173
201
  await mkdir(dirPath, { recursive: true });
174
202
 
175
203
  await writeFile(filePath, yamlContent, "utf8");
176
- console.log(`\n🎉 Configuration saved to: ${filePath}`);
204
+ console.log(`\n🎉 Configuration saved to: ${chalk.cyan(filePath)}`);
177
205
  console.log(
178
206
  "💡 You can edit the configuration file anytime to modify settings."
179
207
  );
180
208
  console.log(
181
- "🚀 Run 'aigne doc generate' to start documentation generation!"
209
+ `🚀 Run ${chalk.cyan(
210
+ "'aigne doc generate'"
211
+ )} to start documentation generation!`
182
212
  );
183
213
 
184
- return {
185
- inputGeneratorStatus: true,
186
- inputGeneratorPath: filePath,
187
- };
214
+ return {};
188
215
  } catch (error) {
189
216
  console.error(`❌ Failed to save configuration file: ${error.message}`);
190
217
  return {
@@ -15,10 +15,47 @@ const WELLKNOWN_SERVICE_PATH_PREFIX = "/.well-known/service";
15
15
  const DEFAULT_APP_URL = "https://docsmith.aigne.io";
16
16
 
17
17
  /**
18
- * Get project name from git repository or current directory
19
- * @returns {string} - The project name
18
+ * Get GitHub repository information
19
+ * @param {string} repoUrl - The repository URL
20
+ * @returns {Promise<Object>} - Repository information
20
21
  */
21
- function getProjectName() {
22
+ async function getGitHubRepoInfo(repoUrl) {
23
+ try {
24
+ // Extract owner and repo from GitHub URL
25
+ const match = repoUrl.match(
26
+ /github\.com[\/:]([^\/]+)\/([^\/]+?)(?:\.git)?$/
27
+ );
28
+ if (!match) return null;
29
+
30
+ const [, owner, repo] = match;
31
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
32
+
33
+ const response = await fetch(apiUrl);
34
+ if (!response.ok) return null;
35
+
36
+ const data = await response.json();
37
+ return {
38
+ name: data.name,
39
+ description: data.description || "",
40
+ icon: data.owner?.avatar_url || "",
41
+ };
42
+ } catch (error) {
43
+ console.warn("Failed to fetch GitHub repository info:", error.message);
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get project information with user confirmation
50
+ * @param {Object} options - Options object containing prompts
51
+ * @returns {Promise<Object>} - Project information including name, description, and icon
52
+ */
53
+ async function getProjectInfo(options) {
54
+ let repoInfo = null;
55
+ let defaultName = basename(process.cwd());
56
+ let defaultDescription = "";
57
+ let defaultIcon = "";
58
+
22
59
  // Check if we're in a git repository
23
60
  try {
24
61
  const gitRemote = execSync("git remote get-url origin", {
@@ -28,11 +65,59 @@ function getProjectName() {
28
65
 
29
66
  // Extract repository name from git remote URL
30
67
  const repoName = gitRemote.split("/").pop().replace(".git", "");
31
- return repoName;
68
+ defaultName = repoName;
69
+
70
+ // If it's a GitHub repository, try to get additional info
71
+ if (gitRemote.includes("github.com")) {
72
+ repoInfo = await getGitHubRepoInfo(gitRemote);
73
+ if (repoInfo) {
74
+ defaultDescription = repoInfo.description;
75
+ defaultIcon = repoInfo.icon;
76
+ }
77
+ }
32
78
  } catch (error) {
33
79
  // Not in git repository or no origin remote, use current directory name
34
- return basename(process.cwd());
80
+ console.warn("No git repository found, using current directory name");
35
81
  }
82
+
83
+ // Prompt user for project information
84
+ console.log("\n📋 Project Information for Documentation Platform");
85
+
86
+ const projectName = await options.prompts.input({
87
+ message: "Project name:",
88
+ default: defaultName,
89
+ validate: (input) => {
90
+ if (!input || input.trim() === "") {
91
+ return "Project name cannot be empty";
92
+ }
93
+ return true;
94
+ },
95
+ });
96
+
97
+ const projectDescription = await options.prompts.input({
98
+ message: "Project description (optional):",
99
+ default: defaultDescription,
100
+ });
101
+
102
+ const projectIcon = await options.prompts.input({
103
+ message: "Project icon URL (optional):",
104
+ default: defaultIcon,
105
+ validate: (input) => {
106
+ if (!input || input.trim() === "") return true;
107
+ try {
108
+ new URL(input);
109
+ return true;
110
+ } catch {
111
+ return "Please enter a valid URL";
112
+ }
113
+ },
114
+ });
115
+
116
+ return {
117
+ name: projectName.trim(),
118
+ description: projectDescription.trim(),
119
+ icon: projectIcon.trim(),
120
+ };
36
121
  }
37
122
 
38
123
  /**
@@ -79,8 +164,9 @@ async function getAccessToken(appUrl) {
79
164
  const result = await createConnect({
80
165
  connectUrl: connectUrl,
81
166
  connectAction: "gen-simple-access-key",
82
- source: `@aigne/cli doc-smith connect to Discuss Kit`,
167
+ source: `AIGNE DocSmith connect to Discuss Kit`,
83
168
  closeOnSuccess: true,
169
+ appName: "AIGNE DocSmith",
84
170
  openPage: (pageUrl) => open(pageUrl),
85
171
  });
86
172
 
@@ -119,29 +205,33 @@ async function getAccessToken(appUrl) {
119
205
  }
120
206
 
121
207
  export default async function publishDocs(
122
- { docsDir, appUrl, boardId },
208
+ { docsDir, appUrl, boardId, boardName, boardDesc, boardCover },
123
209
  options
124
210
  ) {
125
- // Check if appUrl is default and not saved in config
211
+ // Check if DOC_DISCUSS_KIT_URL is set in environment variables
212
+ const envAppUrl = process.env.DOC_DISCUSS_KIT_URL;
213
+ const useEnvAppUrl = !!envAppUrl;
214
+
215
+ // Use environment variable if available, otherwise use the provided appUrl
216
+ if (useEnvAppUrl) {
217
+ appUrl = envAppUrl;
218
+ }
219
+
220
+ // Check if appUrl is default and not saved in config (only when not using env variable)
126
221
  const config = await loadConfigFromFile();
127
222
  const isDefaultAppUrl = appUrl === DEFAULT_APP_URL;
128
223
  const hasAppUrlInConfig = config && config.appUrl;
129
224
 
130
- if (isDefaultAppUrl && !hasAppUrlInConfig) {
131
- console.log("\n=== Document Publishing Platform Selection ===");
132
- console.log(
133
- "Please select the platform where you want to publish your documents:"
134
- );
135
-
225
+ if (!useEnvAppUrl && isDefaultAppUrl && !hasAppUrlInConfig) {
136
226
  const choice = await options.prompts.select({
137
- message: "Select publishing platform:",
227
+ message: "Select platform to publish your documents:",
138
228
  choices: [
139
229
  {
140
- name: "Use official platform (docsmith.aigne.io) - Documents will be publicly accessible, suitable for open source projects",
230
+ name: "Publish to docsmith.aigne.io - free, but your documents will be public accessible, recommended for open-source projects",
141
231
  value: "default",
142
232
  },
143
233
  {
144
- name: "Use private platform - Deploy your own Discuss Kit instance, suitable for internal documentation",
234
+ name: "Publish to your own website - you will need to run Discuss Kit by your self ",
145
235
  value: "custom",
146
236
  },
147
237
  ],
@@ -168,34 +258,48 @@ export default async function publishDocs(
168
258
 
169
259
  const sidebarPath = join(docsDir, "_sidebar.md");
170
260
 
171
- const boardName = boardId ? "" : getProjectName();
261
+ let projectInfo = {
262
+ name: boardName,
263
+ description: boardDesc,
264
+ icon: boardCover,
265
+ };
266
+
267
+ // Only get project info if we need to create a new board
268
+ if (!boardName) {
269
+ projectInfo = await getProjectInfo(options);
270
+
271
+ // save project info to config
272
+ await saveValueToConfig("boardName", projectInfo.name);
273
+ await saveValueToConfig("boardDesc", projectInfo.description);
274
+ await saveValueToConfig("boardCover", projectInfo.icon);
275
+ }
172
276
 
173
277
  const { success, boardId: newBoardId } = await publishDocsFn({
174
278
  sidebarPath,
175
279
  accessToken,
176
280
  appUrl,
177
281
  boardId,
178
- // If boardId is empty, use project name as boardName and auto create board
179
- boardName,
180
- autoCreateBoard: !boardId,
282
+ autoCreateBoard: true,
283
+ // Pass additional project information if available
284
+ boardName: projectInfo.name,
285
+ boardDesc: projectInfo.description,
286
+ boardCover: projectInfo.icon,
181
287
  });
182
288
 
183
289
  // Save values to config.yaml if publish was successful
184
290
  if (success) {
185
- // Save appUrl to config
186
- await saveValueToConfig("appUrl", appUrl);
291
+ // Save appUrl to config only when not using environment variable
292
+ if (!useEnvAppUrl) {
293
+ await saveValueToConfig("appUrl", appUrl);
294
+ }
187
295
 
188
296
  // Save boardId to config if it was auto-created
189
- if (!boardId && newBoardId) {
297
+ if (boardId !== newBoardId) {
190
298
  await saveValueToConfig("boardId", newBoardId);
191
299
  }
192
300
  }
193
301
 
194
- return {
195
- publishResult: {
196
- success,
197
- },
198
- };
302
+ return {};
199
303
  }
200
304
 
201
305
  publishDocs.input_schema = {
@@ -1,5 +1,5 @@
1
1
  type: team
2
- name: reflective-structure-planner
2
+ name: reflectiveStructurePlanner
3
3
  description: A team of agents that plan the structure of the documentation.
4
4
  skills:
5
5
  - structure-planning.yaml
@@ -16,7 +16,6 @@ export default async function saveDocs({
16
16
  locale,
17
17
  }) {
18
18
  const results = [];
19
-
20
19
  // Save current git HEAD to config.yaml for change detection
21
20
  try {
22
21
  const gitHead = getCurrentGitHead();
@@ -30,25 +29,23 @@ export default async function saveDocs({
30
29
  const sidebar = generateSidebar(structurePlan);
31
30
  const sidebarPath = join(docsDir, "_sidebar.md");
32
31
  await writeFile(sidebarPath, sidebar, "utf8");
33
- results.push({ path: sidebarPath, success: true });
34
32
  } catch (err) {
35
- results.push({ path: "_sidebar.md", success: false, error: err.message });
33
+ console.error("Failed to save _sidebar.md:", err.message);
36
34
  }
37
35
 
38
36
  // Clean up invalid .md files that are no longer in the structure plan
39
37
  try {
40
- const cleanupResults = await cleanupInvalidFiles(
38
+ await cleanupInvalidFiles(
41
39
  structurePlan,
42
40
  docsDir,
43
41
  translateLanguages,
44
42
  locale
45
43
  );
46
- results.push(...cleanupResults);
47
44
  } catch (err) {
48
- results.push({ path: "cleanup", success: false, error: err.message });
45
+ console.error("Failed to cleanup invalid .md files:", err.message);
49
46
  }
50
47
 
51
- return { saveDocsResult: results };
48
+ return {};
52
49
  }
53
50
 
54
51
  /**
@@ -16,5 +16,5 @@ export default async function saveSingleDoc({
16
16
  labels,
17
17
  locale,
18
18
  });
19
- return { saveSingleDocResult: results };
19
+ return {};
20
20
  }
@@ -1,4 +1,4 @@
1
- name: structure-planning
1
+ name: structurePlanning
2
2
  description: 通用结构规划生成器,支持网站、文档、书籍、演示文稿等多种场景
3
3
  instructions:
4
4
  url: ../prompts/structure-planning.md
package/aigne.yaml CHANGED
@@ -12,12 +12,12 @@ agents:
12
12
  - ./agents/save-docs.mjs
13
13
  - ./agents/translate.yaml
14
14
  - ./agents/detail-generator-and-translate.yaml
15
- - ./agents/check-detail-generated.mjs
15
+ - ./agents/check-detail.mjs
16
16
  - ./agents/transform-detail-datasources.mjs
17
17
  - ./agents/batch-translate.yaml
18
18
  - ./agents/save-single-doc.mjs
19
19
  - ./agents/save-output.mjs
20
- - ./agents/check-structure-planning.mjs
20
+ - ./agents/check-structure-plan.mjs
21
21
  - ./agents/content-detail-generator.yaml
22
22
  - ./agents/reflective-structure-planner.yaml
23
23
  - ./agents/check-structure-planning-result.yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -12,14 +12,16 @@
12
12
  "author": "Arcblock <blocklet@arcblock.io> https://github.com/blocklet",
13
13
  "license": "MIT",
14
14
  "dependencies": {
15
- "@aigne/anthropic": "^0.10.6",
16
- "@aigne/cli": "^1.30.1",
17
- "@aigne/core": "^1.43.0",
18
- "@aigne/gemini": "^0.8.10",
19
- "@aigne/openai": "^0.10.10",
20
- "@aigne/publish-docs": "^0.5.2",
15
+ "@aigne/anthropic": "^0.10.10",
16
+ "@aigne/cli": "^1.31.0",
17
+ "@aigne/core": "^1.46.0",
18
+ "@aigne/gemini": "^0.8.14",
19
+ "@aigne/openai": "^0.10.14",
20
+ "@aigne/publish-docs": "^0.5.3",
21
+ "chalk": "^5.5.0",
21
22
  "glob": "^11.0.3",
22
23
  "open": "^10.2.0",
24
+ "terminal-link": "^4.0.0",
23
25
  "ufo": "^1.6.1",
24
26
  "yaml": "^2.8.0"
25
27
  },
@@ -30,7 +30,7 @@
30
30
  <structure_plan_feedback>
31
31
  {{ feedback }}
32
32
 
33
- 根据最新的 Data Sources 按需要更新节点的 sourceIds
33
+ 根据最新的 Data Sources 按需要更新节点的 sourceIds,如没有大的变化,可以不更新。
34
34
  </structure_plan_feedback>
35
35
 
36
36
  <review_structure_plan>
@@ -58,3 +58,48 @@ export const DEFAULT_EXCLUDE_PATTERNS = [
58
58
  "*.log",
59
59
  "**/*test.*",
60
60
  ];
61
+
62
+ // Supported languages for documentation
63
+ export const SUPPORTED_LANGUAGES = [
64
+ { code: "en", label: "English (en)", sample: "Hello" },
65
+ { code: "zh-CN", label: "简体中文 (zh-CN)", sample: "你好" },
66
+ { code: "zh-TW", label: "繁體中文 (zh-TW)", sample: "你好" },
67
+ { code: "ja", label: "日本語 (ja)", sample: "こんにちは" },
68
+ { code: "ko", label: "한국어 (ko)", sample: "안녕하세요" },
69
+ { code: "es", label: "Español (es)", sample: "Hola" },
70
+ { code: "fr", label: "Français (fr)", sample: "Bonjour" },
71
+ { code: "de", label: "Deutsch (de)", sample: "Hallo" },
72
+ { code: "pt-BR", label: "Português (pt-BR)", sample: "Olá" },
73
+ { code: "ru", label: "Русский (ru)", sample: "Привет" },
74
+ { code: "it", label: "Italiano (it)", sample: "Ciao" },
75
+ { code: "ar", label: "العربية (ar)", sample: "مرحبا" },
76
+ ];
77
+
78
+ // Predefined document generation styles
79
+ export const DOCUMENT_STYLES = {
80
+ developerDocs: {
81
+ name: "Developer Docs",
82
+ rules: "Steps-first; copy-paste examples; minimal context; active 'you'.",
83
+ },
84
+ userGuide: {
85
+ name: "User Guide",
86
+ rules: "Scenario-based; step-by-step; plain language; outcomes & cautions.",
87
+ },
88
+ apiReference: {
89
+ name: "API Reference",
90
+ rules: "Exact & skimmable; schema-first; clear params/errors/examples.",
91
+ },
92
+ custom: {
93
+ name: "Custom Rules",
94
+ rules: "Enter your own documentation generation rules",
95
+ },
96
+ };
97
+
98
+ // Predefined target audiences
99
+ export const TARGET_AUDIENCES = {
100
+ actionFirst: "Developers, Implementation Engineers, DevOps",
101
+ conceptFirst:
102
+ "Architects, Technical Leads, Developers interested in principles",
103
+ generalUsers: "General Users",
104
+ custom: "Enter your own target audience",
105
+ };
package/utils/utils.mjs CHANGED
@@ -1,8 +1,16 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { execSync } from "node:child_process";
4
- import { existsSync, mkdirSync } from "node:fs";
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ readdirSync,
8
+ accessSync,
9
+ constants,
10
+ statSync,
11
+ } from "node:fs";
5
12
  import { parse } from "yaml";
13
+ import chalk from "chalk";
6
14
  import {
7
15
  DEFAULT_INCLUDE_PATTERNS,
8
16
  DEFAULT_EXCLUDE_PATTERNS,
@@ -101,6 +109,7 @@ export async function saveDocWithTranslations({
101
109
 
102
110
  await fs.writeFile(mainFilePath, finalContent, "utf8");
103
111
  results.push({ path: mainFilePath, success: true });
112
+ console.log(chalk.green(`Saved: ${chalk.cyan(mainFilePath)}`));
104
113
 
105
114
  // Process all translations
106
115
  for (const translate of translates) {
@@ -118,6 +127,7 @@ export async function saveDocWithTranslations({
118
127
 
119
128
  await fs.writeFile(translatePath, finalTranslationContent, "utf8");
120
129
  results.push({ path: translatePath, success: true });
130
+ console.log(chalk.green(`Saved: ${chalk.cyan(translatePath)}`));
121
131
  }
122
132
  } catch (err) {
123
133
  results.push({ path: docPath, success: false, error: err.message });
@@ -394,3 +404,238 @@ export async function saveValueToConfig(key, value) {
394
404
  console.warn(`Failed to save ${key} to config.yaml:`, error.message);
395
405
  }
396
406
  }
407
+
408
+ /**
409
+ * Validate if a path exists and is accessible
410
+ * @param {string} filePath - The path to validate (can be absolute or relative)
411
+ * @returns {Object} - Validation result with isValid boolean and error message
412
+ */
413
+ export function validatePath(filePath) {
414
+ try {
415
+ const absolutePath = normalizePath(filePath);
416
+
417
+ // Check if path exists
418
+ if (!existsSync(absolutePath)) {
419
+ return {
420
+ isValid: false,
421
+ error: `Path does not exist: ${filePath}`,
422
+ };
423
+ }
424
+
425
+ // Check if path is accessible (readable)
426
+ try {
427
+ accessSync(absolutePath, constants.R_OK);
428
+ } catch (accessError) {
429
+ return {
430
+ isValid: false,
431
+ error: `Path is not accessible: ${filePath}`,
432
+ };
433
+ }
434
+
435
+ return {
436
+ isValid: true,
437
+ error: null,
438
+ };
439
+ } catch (error) {
440
+ return {
441
+ isValid: false,
442
+ error: `Invalid path format: ${filePath}`,
443
+ };
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Validate multiple paths and return validation results
449
+ * @param {Array<string>} paths - Array of paths to validate
450
+ * @returns {Object} - Validation results with validPaths array and errors array
451
+ */
452
+ export function validatePaths(paths) {
453
+ const validPaths = [];
454
+ const errors = [];
455
+
456
+ for (const path of paths) {
457
+ const validation = validatePath(path);
458
+ if (validation.isValid) {
459
+ validPaths.push(path);
460
+ } else {
461
+ errors.push({
462
+ path: path,
463
+ error: validation.error,
464
+ });
465
+ }
466
+ }
467
+
468
+ return {
469
+ validPaths,
470
+ errors,
471
+ };
472
+ }
473
+
474
+ /**
475
+ * Check if input is a valid directory and add it to results if so
476
+ * @param {string} searchTerm - The search term to check
477
+ * @param {Array} results - The results array to modify
478
+ */
479
+ function addExactDirectoryMatch(searchTerm, results) {
480
+ const inputValidation = validatePath(searchTerm);
481
+ if (inputValidation.isValid) {
482
+ const stats = statSync(normalizePath(searchTerm));
483
+ if (stats.isDirectory()) {
484
+ results.unshift({
485
+ name: searchTerm,
486
+ value: searchTerm,
487
+ });
488
+ }
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Get available paths for search suggestions based on user input
494
+ * @param {string} userInput - User's input string
495
+ * @returns {Array<Object>} - Array of path objects with name, value, and description
496
+ */
497
+ export function getAvailablePaths(userInput = "") {
498
+ try {
499
+ const searchTerm = userInput.trim();
500
+
501
+ // If no input, return current directory contents
502
+ if (!searchTerm) {
503
+ return getDirectoryContents("./");
504
+ }
505
+
506
+ let results = [];
507
+
508
+ // Handle absolute paths
509
+ if (searchTerm.startsWith("/")) {
510
+ const dirPath = path.dirname(searchTerm);
511
+ const fileName = path.basename(searchTerm);
512
+ results = getDirectoryContents(dirPath, fileName);
513
+ addExactDirectoryMatch(searchTerm, results);
514
+ }
515
+ // Handle relative paths
516
+ else if (searchTerm.startsWith("./") || searchTerm.startsWith("../")) {
517
+ // Extract directory path and search term
518
+ const lastSlashIndex = searchTerm.lastIndexOf("/");
519
+ if (lastSlashIndex === -1) {
520
+ // No slash found, treat as current directory search
521
+ results = getDirectoryContents("./", searchTerm);
522
+ addExactDirectoryMatch(searchTerm, results);
523
+ } else {
524
+ const dirPath = searchTerm.substring(0, lastSlashIndex + 1);
525
+ const fileName = searchTerm.substring(lastSlashIndex + 1);
526
+
527
+ // Validate directory path
528
+ const validation = validatePath(dirPath);
529
+ if (!validation.isValid) {
530
+ return [
531
+ {
532
+ name: dirPath,
533
+ value: dirPath,
534
+ description: validation.error,
535
+ },
536
+ ];
537
+ }
538
+
539
+ results = getDirectoryContents(dirPath, fileName);
540
+ addExactDirectoryMatch(searchTerm, results);
541
+ }
542
+ }
543
+ // Handle simple file/directory names (search in current directory)
544
+ else {
545
+ results = getDirectoryContents("./", searchTerm);
546
+ addExactDirectoryMatch(searchTerm, results);
547
+ }
548
+
549
+ // Remove duplicates based on value (path)
550
+ const uniqueResults = [];
551
+ const seenPaths = new Set();
552
+
553
+ for (const item of results) {
554
+ if (!seenPaths.has(item.value)) {
555
+ seenPaths.add(item.value);
556
+ uniqueResults.push(item);
557
+ }
558
+ }
559
+
560
+ return uniqueResults;
561
+ } catch (error) {
562
+ console.warn(
563
+ `Failed to get available paths for "${userInput}":`,
564
+ error.message
565
+ );
566
+ return [];
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Get directory contents for a specific path
572
+ * @param {string} dirPath - Directory path to search in
573
+ * @param {string} searchTerm - Optional search term to filter results
574
+ * @returns {Array<Object>} - Array of path objects
575
+ */
576
+ function getDirectoryContents(dirPath, searchTerm = "") {
577
+ try {
578
+ const absoluteDirPath = normalizePath(dirPath);
579
+
580
+ // Check if directory exists
581
+ if (!existsSync(absoluteDirPath)) {
582
+ return [
583
+ {
584
+ name: dirPath,
585
+ value: dirPath,
586
+ description: "Directory does not exist",
587
+ },
588
+ ];
589
+ }
590
+
591
+ const items = [];
592
+
593
+ // Read directory contents
594
+ const entries = readdirSync(absoluteDirPath, { withFileTypes: true });
595
+
596
+ for (const entry of entries) {
597
+ const entryName = entry.name;
598
+ const relativePath = path.join(dirPath, entryName);
599
+
600
+ // Filter by search term if provided
601
+ if (
602
+ searchTerm &&
603
+ !entryName.toLowerCase().includes(searchTerm.toLowerCase())
604
+ ) {
605
+ continue;
606
+ }
607
+
608
+ // Skip hidden files and common ignore patterns
609
+ if (
610
+ entryName.startsWith(".") ||
611
+ entryName === "node_modules" ||
612
+ entryName === ".git" ||
613
+ entryName === "dist" ||
614
+ entryName === "build"
615
+ ) {
616
+ continue;
617
+ }
618
+
619
+ const isDirectory = entry.isDirectory();
620
+
621
+ // Only include directories, skip files
622
+ if (isDirectory) {
623
+ items.push({
624
+ name: relativePath,
625
+ value: relativePath,
626
+ });
627
+ }
628
+ }
629
+
630
+ // Sort alphabetically (all items are directories now)
631
+ items.sort((a, b) => a.name.localeCompare(b.name));
632
+
633
+ return items;
634
+ } catch (error) {
635
+ console.warn(
636
+ `Failed to get directory contents from ${dirPath}:`,
637
+ error.message
638
+ );
639
+ return [];
640
+ }
641
+ }