@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.
@@ -9,14 +9,53 @@ import { homedir } from "node:os";
9
9
  import { parse, stringify } from "yaml";
10
10
  import { execSync } from "node:child_process";
11
11
  import { basename } from "node:path";
12
+ import { loadConfigFromFile, saveValueToConfig } from "../utils/utils.mjs";
12
13
 
13
14
  const WELLKNOWN_SERVICE_PATH_PREFIX = "/.well-known/service";
15
+ const DEFAULT_APP_URL = "https://docsmith.aigne.io";
14
16
 
15
17
  /**
16
- * Get project name from git repository or current directory
17
- * @returns {string} - The project name
18
+ * Get GitHub repository information
19
+ * @param {string} repoUrl - The repository URL
20
+ * @returns {Promise<Object>} - Repository information
18
21
  */
19
- 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
+
20
59
  // Check if we're in a git repository
21
60
  try {
22
61
  const gitRemote = execSync("git remote get-url origin", {
@@ -26,11 +65,59 @@ function getProjectName() {
26
65
 
27
66
  // Extract repository name from git remote URL
28
67
  const repoName = gitRemote.split("/").pop().replace(".git", "");
29
- 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
+ }
30
78
  } catch (error) {
31
79
  // Not in git repository or no origin remote, use current directory name
32
- return basename(process.cwd());
80
+ console.warn("No git repository found, using current directory name");
33
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
+ };
34
121
  }
35
122
 
36
123
  /**
@@ -77,8 +164,9 @@ async function getAccessToken(appUrl) {
77
164
  const result = await createConnect({
78
165
  connectUrl: connectUrl,
79
166
  connectAction: "gen-simple-access-key",
80
- source: `@aigne/cli doc-smith connect to Discuss Kit`,
167
+ source: `AIGNE DocSmith connect to Discuss Kit`,
81
168
  closeOnSuccess: true,
169
+ appName: "AIGNE DocSmith",
82
170
  openPage: (pageUrl) => open(pageUrl),
83
171
  });
84
172
 
@@ -116,77 +204,102 @@ async function getAccessToken(appUrl) {
116
204
  return accessToken;
117
205
  }
118
206
 
119
- /**
120
- * Save boardId to config.yaml file if it was auto-created
121
- * @param {string} boardId - The original boardId (may be empty)
122
- * @param {string} newBoardId - The boardId returned from publishDocsFn
123
- */
124
- async function saveBoardIdToInput(boardId, newBoardId) {
125
- // Only save if boardId was auto-created
126
- if (!boardId && newBoardId) {
127
- try {
128
- const docSmithDir = join(process.cwd(), "doc-smith");
129
- if (!existsSync(docSmithDir)) {
130
- mkdirSync(docSmithDir, { recursive: true });
131
- }
207
+ export default async function publishDocs(
208
+ { docsDir, appUrl, boardId, boardName, boardDesc, boardCover },
209
+ options
210
+ ) {
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;
132
214
 
133
- const inputFilePath = join(docSmithDir, "config.yaml");
134
- let fileContent = "";
215
+ // Use environment variable if available, otherwise use the provided appUrl
216
+ if (useEnvAppUrl) {
217
+ appUrl = envAppUrl;
218
+ }
135
219
 
136
- // Read existing file content if it exists
137
- if (existsSync(inputFilePath)) {
138
- fileContent = await readFile(inputFilePath, "utf8");
139
- }
220
+ // Check if appUrl is default and not saved in config (only when not using env variable)
221
+ const config = await loadConfigFromFile();
222
+ const isDefaultAppUrl = appUrl === DEFAULT_APP_URL;
223
+ const hasAppUrlInConfig = config && config.appUrl;
140
224
 
141
- // Check if boardId already exists in the file
142
- const boardIdRegex = /^boardId:\s*.*$/m;
143
- const newBoardIdLine = `boardId: ${newBoardId}`;
144
-
145
- if (boardIdRegex.test(fileContent)) {
146
- // Replace existing boardId line
147
- fileContent = fileContent.replace(boardIdRegex, newBoardIdLine);
148
- } else {
149
- // Add boardId to the end of file
150
- if (fileContent && !fileContent.endsWith("\n")) {
151
- fileContent += "\n";
152
- }
153
- fileContent += newBoardIdLine + "\n";
154
- }
225
+ if (!useEnvAppUrl && isDefaultAppUrl && !hasAppUrlInConfig) {
226
+ const choice = await options.prompts.select({
227
+ message: "Select platform to publish your documents:",
228
+ choices: [
229
+ {
230
+ name: "Publish to docsmith.aigne.io - free, but your documents will be public accessible, recommended for open-source projects",
231
+ value: "default",
232
+ },
233
+ {
234
+ name: "Publish to your own website - you will need to run Discuss Kit by your self ",
235
+ value: "custom",
236
+ },
237
+ ],
238
+ });
155
239
 
156
- await writeFile(inputFilePath, fileContent);
157
- } catch (error) {
158
- console.warn("Failed to save board ID to config.yaml:", error.message);
240
+ if (choice === "custom") {
241
+ appUrl = await options.prompts.input({
242
+ message: "Please enter your Discuss Kit platform URL:",
243
+ validate: (input) => {
244
+ try {
245
+ new URL(input);
246
+ return true;
247
+ } catch {
248
+ return "Please enter a valid URL";
249
+ }
250
+ },
251
+ });
159
252
  }
160
253
  }
161
- }
162
254
 
163
- export default async function publishDocs({ docsDir, appUrl, boardId }) {
164
255
  const accessToken = await getAccessToken(appUrl);
165
256
 
166
257
  process.env.DOC_ROOT_DIR = docsDir;
167
258
 
168
259
  const sidebarPath = join(docsDir, "_sidebar.md");
169
260
 
170
- 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
+ }
171
276
 
172
277
  const { success, boardId: newBoardId } = await publishDocsFn({
173
278
  sidebarPath,
174
279
  accessToken,
175
280
  appUrl,
176
281
  boardId,
177
- // If boardId is empty, use project name as boardName and auto create board
178
- boardName,
179
- autoCreateBoard: !boardId,
282
+ autoCreateBoard: true,
283
+ // Pass additional project information if available
284
+ boardName: projectInfo.name,
285
+ boardDesc: projectInfo.description,
286
+ boardCover: projectInfo.icon,
180
287
  });
181
288
 
182
- // Save boardId to config.yaml if it was auto-created
183
- await saveBoardIdToInput(boardId, newBoardId);
289
+ // Save values to config.yaml if publish was successful
290
+ if (success) {
291
+ // Save appUrl to config only when not using environment variable
292
+ if (!useEnvAppUrl) {
293
+ await saveValueToConfig("appUrl", appUrl);
294
+ }
184
295
 
185
- return {
186
- publishResult: {
187
- success,
188
- },
189
- };
296
+ // Save boardId to config if it was auto-created
297
+ if (boardId !== newBoardId) {
298
+ await saveValueToConfig("boardId", newBoardId);
299
+ }
300
+ }
301
+
302
+ return {};
190
303
  }
191
304
 
192
305
  publishDocs.input_schema = {
@@ -199,9 +312,7 @@ publishDocs.input_schema = {
199
312
  appUrl: {
200
313
  type: "string",
201
314
  description: "The url of the app",
202
- default:
203
- // "https://bbqawfllzdt3pahkdsrsone6p3wpxcwp62vlabtawfu.did.abtnet.io",
204
- "https://www.staging.arcblock.io",
315
+ default: DEFAULT_APP_URL,
205
316
  },
206
317
  boardId: {
207
318
  type: "string",
@@ -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
@@ -1,5 +1,6 @@
1
1
  import { writeFile, readdir, unlink } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { getCurrentGitHead, saveGitHeadToConfig } from "../utils/utils.mjs";
3
4
 
4
5
  /**
5
6
  * @param {Object} params
@@ -15,31 +16,36 @@ export default async function saveDocs({
15
16
  locale,
16
17
  }) {
17
18
  const results = [];
19
+ // Save current git HEAD to config.yaml for change detection
20
+ try {
21
+ const gitHead = getCurrentGitHead();
22
+ await saveGitHeadToConfig(gitHead);
23
+ } catch (err) {
24
+ console.warn("Failed to save git HEAD:", err.message);
25
+ }
18
26
 
19
27
  // Generate _sidebar.md
20
28
  try {
21
29
  const sidebar = generateSidebar(structurePlan);
22
30
  const sidebarPath = join(docsDir, "_sidebar.md");
23
31
  await writeFile(sidebarPath, sidebar, "utf8");
24
- results.push({ path: sidebarPath, success: true });
25
32
  } catch (err) {
26
- results.push({ path: "_sidebar.md", success: false, error: err.message });
33
+ console.error("Failed to save _sidebar.md:", err.message);
27
34
  }
28
35
 
29
36
  // Clean up invalid .md files that are no longer in the structure plan
30
37
  try {
31
- const cleanupResults = await cleanupInvalidFiles(
38
+ await cleanupInvalidFiles(
32
39
  structurePlan,
33
40
  docsDir,
34
41
  translateLanguages,
35
42
  locale
36
43
  );
37
- results.push(...cleanupResults);
38
44
  } catch (err) {
39
- results.push({ path: "cleanup", success: false, error: err.message });
45
+ console.error("Failed to cleanup invalid .md files:", err.message);
40
46
  }
41
47
 
42
- return { saveDocsResult: results };
48
+ return {};
43
49
  }
44
50
 
45
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
@@ -5,6 +5,9 @@ alias:
5
5
  - p
6
6
  description: Publish the documentation to Discuss Kit
7
7
  skills:
8
+ - url: ./input-generator.mjs
9
+ default_input:
10
+ skipIfExists: true
8
11
  - load-config.mjs
9
12
  - publish-docs.mjs
10
13
  input_schema:
@@ -1,14 +1,28 @@
1
+ import { normalizePath, toRelativePath } from "../utils/utils.mjs";
2
+
1
3
  export default function transformDetailDatasources({
2
4
  sourceIds,
3
5
  datasourcesList,
4
6
  }) {
5
- // Build a map for fast lookup
7
+ // Build a map for fast lookup, with path normalization for compatibility
6
8
  const dsMap = Object.fromEntries(
7
- (datasourcesList || []).map((ds) => [ds.sourceId, ds.content])
9
+ (datasourcesList || []).map((ds) => {
10
+ const normalizedSourceId = normalizePath(ds.sourceId);
11
+ return [normalizedSourceId, ds.content];
12
+ })
8
13
  );
9
- // Collect formatted contents in order
14
+
15
+ // Collect formatted contents in order, with path normalization
10
16
  const contents = (sourceIds || [])
11
- .filter((id) => dsMap[id])
12
- .map((id) => `// sourceId: ${id}\n${dsMap[id]}\n`);
17
+ .filter((id) => {
18
+ const normalizedId = normalizePath(id);
19
+ return dsMap[normalizedId];
20
+ })
21
+ .map((id) => {
22
+ const normalizedId = normalizePath(id);
23
+ const relativeId = toRelativePath(id);
24
+ return `// sourceId: ${relativeId}\n${dsMap[normalizedId]}\n`;
25
+ });
26
+
13
27
  return { detailDataSources: contents.join("") };
14
28
  }
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.1.4",
3
+ "version": "0.2.1",
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",
15
+ "@aigne/anthropic": "^0.10.9",
16
+ "@aigne/cli": "^1.30.4",
17
+ "@aigne/core": "^1.45.0",
18
+ "@aigne/gemini": "^0.8.13",
19
+ "@aigne/openai": "^0.10.13",
20
20
  "@aigne/publish-docs": "^0.5.2",
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
  },
@@ -17,7 +17,7 @@
17
17
  ```
18
18
  {{ feedback }}
19
19
 
20
- 根据最新的 Data Sources 按需要更新节点的 sourceIds ,更新至状态。
20
+ 根据最新的 Data Sources 按需要更新节点的 sourceIds
21
21
  ```
22
22
  </context>
23
23
 
@@ -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>
@@ -0,0 +1,105 @@
1
+ // Default file patterns for inclusion and exclusion
2
+ export const DEFAULT_INCLUDE_PATTERNS = [
3
+ "*.py",
4
+ "*.js",
5
+ "*.jsx",
6
+ "*.ts",
7
+ "*.tsx",
8
+ "*.go",
9
+ "*.java",
10
+ "*.pyi",
11
+ "*.pyx",
12
+ "*.c",
13
+ "*.cc",
14
+ "*.cpp",
15
+ "*.h",
16
+ "*.md",
17
+ "*.rst",
18
+ "*.json",
19
+ "*Dockerfile",
20
+ "*Makefile",
21
+ "*.yaml",
22
+ "*.yml",
23
+ ];
24
+
25
+ export const DEFAULT_EXCLUDE_PATTERNS = [
26
+ "aigne-docs/**",
27
+ "doc-smith/**",
28
+ "assets/**",
29
+ "data/**",
30
+ "images/**",
31
+ "public/**",
32
+ "static/**",
33
+ "**/vendor/**",
34
+ "temp/**",
35
+ "**/*docs/**",
36
+ "**/*doc/**",
37
+ "**/*venv/**",
38
+ "*.venv/**",
39
+ "*test*",
40
+ "**/*test/**",
41
+ "**/*tests/**",
42
+ "**/*examples/**",
43
+ "**/playgrounds/**",
44
+ "v1/**",
45
+ "**/dist/**",
46
+ "**/*build/**",
47
+ "**/*experimental/**",
48
+ "**/*deprecated/**",
49
+ "**/*misc/**",
50
+ "**/*legacy/**",
51
+ ".git/**",
52
+ ".github/**",
53
+ ".next/**",
54
+ ".vscode/**",
55
+ "**/*obj/**",
56
+ "**/*bin/**",
57
+ "**/*node_modules/**",
58
+ "*.log",
59
+ "**/*test.*",
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
+ };