@aigne/doc-smith 0.8.15-beta.2 → 0.8.15-beta.4

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.15-beta.4](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.15-beta.3...v0.8.15-beta.4) (2025-10-28)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * fix update translate not working ([#221](https://github.com/AIGNE-io/aigne-doc-smith/issues/221)) ([95bc49e](https://github.com/AIGNE-io/aigne-doc-smith/commit/95bc49ec8b1e18fe20dd1360d9707afdd6629bad))
9
+
10
+ ## [0.8.15-beta.3](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.15-beta.2...v0.8.15-beta.3) (2025-10-28)
11
+
12
+
13
+ ### Features
14
+
15
+ * glossary support load from remote-url ([#220](https://github.com/AIGNE-io/aigne-doc-smith/issues/220)) ([5d52746](https://github.com/AIGNE-io/aigne-doc-smith/commit/5d527469819a4dc08f17338b1d1fb2cd7ccf07c0))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * polish generate/update/translate save doc logic ([#218](https://github.com/AIGNE-io/aigne-doc-smith/issues/218)) ([dada1a4](https://github.com/AIGNE-io/aigne-doc-smith/commit/dada1a422d589512f2a0f6e5c69df4845eca44ae))
21
+
3
22
  ## [0.8.15-beta.2](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.15-beta.1...v0.8.15-beta.2) (2025-10-24)
4
23
 
5
24
 
@@ -23,6 +23,7 @@ skills:
23
23
  savePath:
24
24
  $get: outputDir
25
25
  fileName: structure-plan.json
26
+ - ../utils/save-sidebar.mjs
26
27
  - type: transform
27
28
  name: transformData
28
29
  task_render_mode: hide
@@ -47,7 +48,7 @@ skills:
47
48
  - url: ../utils/check-feedback-refiner.mjs
48
49
  default_input:
49
50
  stage: document_structure
50
- - ../utils/save-docs.mjs
51
+ - ../utils/post-generate.mjs
51
52
 
52
53
  input_schema:
53
54
  type: object
@@ -10,5 +10,7 @@ export default async function checkNeedGenerate({ docsDir, locale, documentExecu
10
10
  );
11
11
  process.exit(0);
12
12
  }
13
- return {};
13
+ return {
14
+ message: 'Documents found in the docs directory, skipping "generate" step',
15
+ };
14
16
  }
@@ -20,6 +20,7 @@ import {
20
20
  isGlobPattern,
21
21
  validatePath,
22
22
  } from "../../utils/utils.mjs";
23
+ import { isRemoteFile } from "../../utils/file-utils.mjs";
23
24
 
24
25
  const _PRESS_ENTER_TO_FINISH = "Press Enter to finish";
25
26
 
@@ -246,17 +247,23 @@ export default async function init(
246
247
  // 8. Content sources
247
248
  console.log("\nšŸ” [8/9]: Content Sources");
248
249
  console.log(
249
- "Please specify the folders and files we should analyze to generate your documentation (e.g., ./src, ./docs, ./README.md).",
250
+ "Please specify the folders and files we should analyze to generate your documentation.",
250
251
  );
251
252
  console.log(
252
- "šŸ’” You can also use glob patterns like src/**/*.js or docs/**/*.md for more specific file matching.",
253
+ ` 1. You can use local file paths like ${chalk.green("./src")}, ${chalk.green("./docs")}, ${chalk.green("./README.md")} (prefix with '!' to ignore a file or folder like ${chalk.green("!./src/private")}).`,
254
+ );
255
+ console.log(
256
+ ` 2. You can also use glob patterns like ${chalk.green("src/**/*.js")} or ${chalk.green("docs/**/*.md")} for more specific file matching. (prefix with '!' to ignore a file or folder like ${chalk.green("!private/**/*.js")}).`,
257
+ );
258
+ console.log(
259
+ ` 3. You can also use remote url like ${chalk.green("https://example.com/openapi.yaml")}.`,
253
260
  );
254
261
  console.log("šŸ’” If you leave this empty, we will scan the entire directory.");
255
262
 
256
263
  const sourcePaths = [];
257
264
  while (true) {
258
265
  const selectedPath = await options.prompts.search({
259
- message: "Please enter a file or folder path, or a glob pattern:",
266
+ message: "Please enter a file or folder path, or a glob pattern or remote url:",
260
267
  source: async (input) => {
261
268
  if (!input || input.trim() === "") {
262
269
  return [
@@ -268,13 +275,23 @@ export default async function init(
268
275
  ];
269
276
  }
270
277
 
278
+ let isIgnore = false;
271
279
  const searchTerm = input.trim();
280
+ let cleanSearchTerm = searchTerm;
281
+ if (cleanSearchTerm.startsWith("!")) {
282
+ isIgnore = true;
283
+ cleanSearchTerm = searchTerm.slice(1);
284
+ }
272
285
 
273
286
  // Search for matching files and folders in current directory
274
- const availablePaths = getAvailablePaths(searchTerm);
287
+ const availablePaths = getAvailablePaths(cleanSearchTerm);
275
288
 
276
289
  // Also add option to use as glob pattern
277
- const options = [...availablePaths];
290
+ const options = [...availablePaths].map((x) => ({
291
+ ...x,
292
+ name: isIgnore ? `!${x.name}` : x.name,
293
+ value: isIgnore ? `!${x.value}` : x.value,
294
+ }));
278
295
 
279
296
  // Check if input looks like a glob pattern
280
297
  const isGlobPatternResult = isGlobPattern(searchTerm);
@@ -287,6 +304,14 @@ export default async function init(
287
304
  });
288
305
  }
289
306
 
307
+ if (!isIgnore && isRemoteFile(searchTerm)) {
308
+ options.push({
309
+ name: searchTerm,
310
+ value: searchTerm,
311
+ description: "Use this remote url for content source.",
312
+ });
313
+ }
314
+
290
315
  return options;
291
316
  },
292
317
  });
@@ -301,6 +326,14 @@ export default async function init(
301
326
  // Check if it's a glob pattern
302
327
  const isGlobPatternResult = isGlobPattern(trimmedPath);
303
328
 
329
+ if (isRemoteFile(trimmedPath)) {
330
+ // For remote urls, just add them without validation
331
+ if (sourcePaths.includes(trimmedPath)) {
332
+ console.log(`āš ļø URL already exists: ${trimmedPath}`);
333
+ continue;
334
+ }
335
+ sourcePaths.push(trimmedPath);
336
+ }
304
337
  if (isGlobPatternResult) {
305
338
  // For glob patterns, just add them without validation
306
339
  if (sourcePaths.includes(trimmedPath)) {
@@ -309,8 +342,9 @@ export default async function init(
309
342
  }
310
343
  sourcePaths.push(trimmedPath);
311
344
  } else {
345
+ const cleanTrimmedPath = trimmedPath.startsWith("!") ? trimmedPath.slice(1) : trimmedPath;
312
346
  // Use validatePath to check if path is valid for regular paths
313
- const validation = validatePath(trimmedPath);
347
+ const validation = validatePath(cleanTrimmedPath);
314
348
 
315
349
  if (!validation.isValid) {
316
350
  console.log(`āš ļø ${validation.error}`);
@@ -14,14 +14,9 @@ import {
14
14
  } from "../../utils/constants/index.mjs";
15
15
  import { beforePublishHook, ensureTmpDir } from "../../utils/d2-utils.mjs";
16
16
  import { deploy } from "../../utils/deploy.mjs";
17
- import {
18
- getGithubRepoUrl,
19
- isHttp,
20
- loadConfigFromFile,
21
- saveValueToConfig,
22
- } from "../../utils/utils.mjs";
17
+ import { getGithubRepoUrl, loadConfigFromFile, saveValueToConfig } from "../../utils/utils.mjs";
23
18
  import updateBranding from "../utils/update-branding.mjs";
24
- import { downloadAndUploadImage } from "../../utils/file-utils.mjs";
19
+ import { isRemoteFile, downloadAndUploadImage } from "../../utils/file-utils.mjs";
25
20
 
26
21
  const BASE_URL = process.env.DOC_SMITH_BASE_URL || CLOUD_SERVICE_URL_PROD;
27
22
 
@@ -205,7 +200,7 @@ export default async function publishDocs(
205
200
  let finalPath = null;
206
201
 
207
202
  // Handle project logo download if it's a URL
208
- if (projectInfo.icon && isHttp(projectInfo.icon)) {
203
+ if (projectInfo.icon && isRemoteFile(projectInfo.icon)) {
209
204
  const { url: uploadedImageUrl, downloadFinalPath } = await downloadAndUploadImage(
210
205
  projectInfo.icon,
211
206
  docsDir,
@@ -34,9 +34,6 @@ skills:
34
34
  skills:
35
35
  - ../translate/translate-multilingual.yaml
36
36
  - url: ./record-translation-history.mjs
37
- - url: ../utils/save-single-doc.mjs
38
- default_input:
39
- isTranslate: true
40
37
  iterate_on: selectedDocs
41
38
  concurrency: 10
42
39
  - url: ../utils/check-feedback-refiner.mjs
@@ -22,6 +22,7 @@ skills:
22
22
  max_iterations: 5
23
23
  return_last_on_max_iterations: true
24
24
  task_title: Translate '{{ title }}' to '{{ language }}'
25
+ - ../utils/save-doc-translation.mjs
25
26
  input_schema:
26
27
  type: object
27
28
  properties:
@@ -48,4 +49,3 @@ output_schema:
48
49
  type: string
49
50
  iterate_on: translates
50
51
  concurrency: 3
51
-
@@ -4,4 +4,4 @@ name: batchUpdateDocument
4
4
  skills:
5
5
  - ../update/handle-document-update.yaml
6
6
  iterate_on: selectedDocs
7
- concurrency: 10
7
+ concurrency: 10
@@ -2,7 +2,10 @@ import { access, readFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { TeamAgent } from "@aigne/core";
5
+ import fs from "fs-extra";
6
+ import pMap from "p-map";
5
7
 
8
+ import { getFileName } from "../../utils/utils.mjs";
6
9
  import checkDetailResult from "../utils/check-detail-result.mjs";
7
10
 
8
11
  // Get current script directory
@@ -18,13 +21,13 @@ export default async function checkDocument(
18
21
  modifiedFiles,
19
22
  forceRegenerate,
20
23
  locale,
24
+ translates,
21
25
  ...rest
22
26
  },
23
27
  options,
24
28
  ) {
25
29
  // Check if the detail file already exists
26
- const flatName = path.replace(/^\//, "").replace(/\//g, "-");
27
- const fileFullName = locale === "en" ? `${flatName}.md` : `${flatName}.${locale}.md`;
30
+ const fileFullName = getFileName(path, locale);
28
31
  const filePath = join(docsDir, fileFullName);
29
32
  let detailGenerated = true;
30
33
  let fileContent = null;
@@ -84,24 +87,42 @@ export default async function checkDocument(
84
87
  contentValidationFailed = true;
85
88
  }
86
89
  }
90
+ const languages = translates.map((x) => x.language);
91
+ const lackLanguages = new Set(languages);
92
+ const skills = [];
87
93
 
88
94
  // If file exists, sourceIds haven't changed, source files haven't changed, and content validation passes, no need to regenerate
89
95
  if (detailGenerated && !sourceIdsChanged && !contentValidationFailed && !forceRegenerate) {
90
- return {
91
- path,
92
- docsDir,
93
- ...rest,
94
- detailGenerated: true,
95
- };
96
+ await pMap(
97
+ languages,
98
+ async (x) => {
99
+ const languageFileName = getFileName(path, x);
100
+ const languageFilePath = join(docsDir, languageFileName);
101
+ if (await fs.exists(languageFilePath)) {
102
+ lackLanguages.delete(x);
103
+ }
104
+ },
105
+ { concurrency: 10 },
106
+ );
107
+ if (lackLanguages.size === 0) {
108
+ return {
109
+ path,
110
+ docsDir,
111
+ ...rest,
112
+ detailGenerated: true,
113
+ };
114
+ }
115
+ // translations during generation don't need feedback, content is satisfactory
116
+ rest.content = fileContent;
117
+ } else {
118
+ skills.push(options.context.agents["handleDocumentUpdate"]);
96
119
  }
97
120
 
121
+ skills.push(options.context.agents["translateMultilingual"]);
122
+
98
123
  const teamAgent = TeamAgent.from({
99
124
  name: "generateDocument",
100
- skills: [
101
- options.context.agents["handleDocumentUpdate"],
102
- options.context.agents["translateMultilingual"],
103
- options.context.agents["saveSingleDoc"],
104
- ],
125
+ skills,
105
126
  });
106
127
  let openAPISpec = null;
107
128
 
@@ -119,6 +140,7 @@ export default async function checkDocument(
119
140
 
120
141
  const result = await options.context.invoke(teamAgent, {
121
142
  ...rest,
143
+ translates: translates.filter((x) => lackLanguages.has(x.language)),
122
144
  locale,
123
145
  docsDir,
124
146
  path,
@@ -0,0 +1,30 @@
1
+ type: team
2
+ task_render_mode: collapse
3
+ name: generateDiagram
4
+ skills:
5
+ - ../generate/draw-diagram.yaml
6
+ - ../generate/wrap-diagram-code.mjs
7
+ reflection:
8
+ reviewer: ../generate/check-diagram.mjs
9
+ is_approved: isValid
10
+ max_iterations: 5
11
+ return_last_on_max_iterations: false
12
+ custom_error_message: "MUST NOT generate any diagram: validation failed after max iterations."
13
+ input_schema:
14
+ type: object
15
+ properties:
16
+ documentContent:
17
+ type: string
18
+ description: The **raw text content** of the current document. (**Note:** This is the original document and **does not include** any diagram source code.)
19
+ locale:
20
+ type: string
21
+ description: Language for diagram labels and text
22
+ default: en
23
+ required:
24
+ - documentContent
25
+ output_schema:
26
+ type: object
27
+ properties:
28
+ diagramSourceCode:
29
+ type: string
30
+ description: The **diagram source code** generated from the input text.
@@ -58,33 +58,4 @@ afs:
58
58
  should search and read as needed while generating document content
59
59
  keep_text_in_tool_uses: false
60
60
  skills:
61
- - type: team
62
- task_render_mode: collapse
63
- name: generateDiagram
64
- skills:
65
- - ../generate/draw-diagram.yaml
66
- - ../generate/wrap-diagram-code.mjs
67
- reflection:
68
- reviewer: ../generate/check-diagram.mjs
69
- is_approved: isValid
70
- max_iterations: 5
71
- return_last_on_max_iterations: false
72
- custom_error_message: "MUST NOT generate any diagram: validation failed after max iterations."
73
- input_schema:
74
- type: object
75
- properties:
76
- documentContent:
77
- type: string
78
- description: The **raw text content** of the current document. (**Note:** This is the original document and **does not include** any diagram source code.)
79
- locale:
80
- type: string
81
- description: Language for diagram labels and text
82
- default: en
83
- required:
84
- - documentContent
85
- output_schema:
86
- type: object
87
- properties:
88
- diagramSourceCode:
89
- type: string
90
- description: The **diagram source code** generated from the input text.
61
+ - ./generate-diagram.yaml
@@ -23,6 +23,7 @@ skills:
23
23
  max_iterations: 5
24
24
  return_last_on_max_iterations: true
25
25
  task_title: Generate document for '{{ title }}'
26
+ - ../utils/save-doc.mjs
26
27
  input_schema:
27
28
  type: object
28
29
  properties:
@@ -1,4 +1,4 @@
1
- import { recordUpdate } from "../../utils/history-utils.mjs";
1
+ import pMap from "p-map";
2
2
 
3
3
  export default async function saveAndTranslateDocument(input, options) {
4
4
  const { selectedDocs, docsDir, translateLanguages, locale } = input;
@@ -7,21 +7,6 @@ export default async function saveAndTranslateDocument(input, options) {
7
7
  return {};
8
8
  }
9
9
 
10
- // Saves a document with optional translation data
11
- const saveDocument = async (doc, translates = null, isTranslate = false) => {
12
- const saveAgent = options.context.agents["saveSingleDoc"];
13
-
14
- return await options.context.invoke(saveAgent, {
15
- path: doc.path,
16
- content: doc.content,
17
- docsDir: docsDir,
18
- locale: locale,
19
- translates: translates || doc.translates,
20
- labels: doc.labels,
21
- isTranslate: isTranslate,
22
- });
23
- };
24
-
25
10
  // Only prompt user if translation is actually needed
26
11
  let shouldTranslate = false;
27
12
  if (
@@ -46,28 +31,6 @@ export default async function saveAndTranslateDocument(input, options) {
46
31
 
47
32
  // Save documents in batches
48
33
  const batchSize = 3;
49
- for (let i = 0; i < selectedDocs.length; i += batchSize) {
50
- const batch = selectedDocs.slice(i, i + batchSize);
51
-
52
- const savePromises = batch.map(async (doc) => {
53
- try {
54
- await saveDocument(doc);
55
-
56
- // Record history for each document if feedback is provided
57
- if (doc.feedback?.trim()) {
58
- recordUpdate({
59
- operation: "document_update",
60
- feedback: doc.feedback.trim(),
61
- documentPath: doc.path,
62
- });
63
- }
64
- } catch (error) {
65
- console.error(`āŒ Failed to save document ${doc.path}:`, error.message);
66
- }
67
- });
68
-
69
- await Promise.all(savePromises);
70
- }
71
34
 
72
35
  // Return results if user chose to skip translation
73
36
  if (!shouldTranslate) {
@@ -77,30 +40,27 @@ export default async function saveAndTranslateDocument(input, options) {
77
40
  // Translate documents in batches
78
41
  const translateAgent = options.context.agents["translateMultilingual"];
79
42
 
80
- for (let i = 0; i < selectedDocs.length; i += batchSize) {
81
- const batch = selectedDocs.slice(i, i + batchSize);
82
-
83
- const translatePromises = batch.map(async (doc) => {
43
+ await pMap(
44
+ selectedDocs,
45
+ async (doc) => {
84
46
  try {
85
47
  // Clear feedback to ensure translation is not affected by update feedback
86
48
  doc.feedback = "";
87
49
 
88
- const result = await options.context.invoke(translateAgent, {
50
+ await options.context.invoke(translateAgent, {
89
51
  ...input, // context is required
90
52
  content: doc.content,
91
53
  translates: doc.translates,
92
54
  title: doc.title,
55
+ path: doc.path,
56
+ docsDir,
93
57
  });
94
-
95
- // Save the translated content
96
- await saveDocument(doc, result.translates, true);
97
58
  } catch (error) {
98
59
  console.error(`āŒ Failed to translate document ${doc.path}:`, error.message);
99
60
  }
100
- });
101
-
102
- await Promise.all(translatePromises);
103
- }
61
+ },
62
+ { concurrency: batchSize },
63
+ );
104
64
 
105
65
  return {};
106
66
  }
@@ -57,4 +57,5 @@ afs:
57
57
  keep_text_in_tool_uses: false
58
58
  skills:
59
59
  - ./document-tools/update-document-content.mjs
60
+ - ./generate-diagram.yaml
60
61
  task_render_mode: collapse
@@ -3,5 +3,6 @@ name: updateSingleDocument
3
3
  skills:
4
4
  - ../utils/transform-detail-datasources.mjs
5
5
  - ../update/user-review-document.mjs
6
+ - ../utils/save-doc.mjs
6
7
  iterate_on: selectedDocs
7
8
  concurrency: 1
@@ -8,7 +8,7 @@ import {
8
8
  loadFilesFromPaths,
9
9
  readFileContents,
10
10
  getMimeType,
11
- checkIsRemoteFile,
11
+ isRemoteFile,
12
12
  } from "../../utils/file-utils.mjs";
13
13
  import {
14
14
  getCurrentGitHead,
@@ -144,7 +144,7 @@ export default async function loadSources(
144
144
  files.map(async (file) => {
145
145
  const ext = path.extname(file).toLowerCase();
146
146
 
147
- if (mediaExtensions.includes(ext) && !checkIsRemoteFile(file)) {
147
+ if (mediaExtensions.includes(ext) && !isRemoteFile(file)) {
148
148
  // This is a media file
149
149
  const relativePath = path.relative(docsDir, file);
150
150
  const fileName = path.basename(file);
@@ -214,15 +214,15 @@ export default async function loadSources(
214
214
  return !isOpenAPI;
215
215
  });
216
216
 
217
- const httpFileList = [];
217
+ const remoteFileList = [];
218
218
 
219
219
  sourceFiles.forEach((file) => {
220
- if (checkIsRemoteFile(file.sourceId)) {
221
- httpFileList.push(file);
220
+ if (isRemoteFile(file.sourceId)) {
221
+ remoteFileList.push(file);
222
222
  }
223
223
  });
224
224
  if (options?.context?.userContext) {
225
- options.context.userContext.httpFileList = httpFileList;
225
+ options.context.userContext.remoteFileList = remoteFileList;
226
226
  }
227
227
 
228
228
  // Build allSources string using utility function
@@ -1,4 +1,4 @@
1
- import { readdir, unlink, writeFile } from "node:fs/promises";
1
+ import { readdir, unlink } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { shutdownMermaidWorkerPool } from "../../utils/mermaid-worker-pool.mjs";
4
4
  import { getCurrentGitHead, saveGitHeadToConfig } from "../../utils/utils.mjs";
@@ -10,7 +10,7 @@ import { getCurrentGitHead, saveGitHeadToConfig } from "../../utils/utils.mjs";
10
10
  * @param {Array<string>} [params.translateLanguages] - Translation languages
11
11
  * @returns {Promise<Array<{ path: string, success: boolean, error?: string }>>}
12
12
  */
13
- export default async function saveDocs({
13
+ export default async function postGenerate({
14
14
  documentExecutionStructure: documentStructure,
15
15
  docsDir,
16
16
  translateLanguages = [],
@@ -26,15 +26,6 @@ export default async function saveDocs({
26
26
  console.warn("Failed to save git HEAD:", err.message);
27
27
  }
28
28
 
29
- // Generate _sidebar.md
30
- try {
31
- const sidebar = generateSidebar(documentStructure);
32
- const sidebarPath = join(docsDir, "_sidebar.md");
33
- await writeFile(sidebarPath, sidebar, "utf8");
34
- } catch (err) {
35
- console.error("Failed to save _sidebar.md:", err.message);
36
- }
37
-
38
29
  // Clean up invalid .md files that are no longer in the documentation structure
39
30
  try {
40
31
  await cleanupInvalidFiles(documentStructure, docsDir, translateLanguages, locale);
@@ -140,43 +131,3 @@ async function cleanupInvalidFiles(documentStructure, docsDir, translateLanguage
140
131
  }
141
132
 
142
133
  // Generate sidebar content, support nested structure, and the order is consistent with documentStructure
143
- function generateSidebar(documentStructure) {
144
- // Build tree structure
145
- const root = {};
146
- for (const { path, title, parentId } of documentStructure) {
147
- const relPath = path.replace(/^\//, "");
148
- const segments = relPath.split("/");
149
- let node = root;
150
- for (let i = 0; i < segments.length; i++) {
151
- const seg = segments[i];
152
- if (!node[seg])
153
- node[seg] = {
154
- __children: {},
155
- __title: null,
156
- __fullPath: segments.slice(0, i + 1).join("/"),
157
- __parentId: parentId,
158
- };
159
- if (i === segments.length - 1) node[seg].__title = title;
160
- node = node[seg].__children;
161
- }
162
- }
163
- // Recursively generate sidebar text, the link path is the flattened file name
164
- function walk(node, parentSegments = [], indent = "") {
165
- let out = "";
166
- for (const key of Object.keys(node)) {
167
- const item = node[key];
168
- const fullSegments = [...parentSegments, key];
169
- const flatFile = `${fullSegments.join("-")}.md`;
170
- if (item.__title) {
171
- const realIndent = item.__parentId === null ? "" : indent;
172
- out += `${realIndent}* [${item.__title}](/${flatFile})\n`;
173
- }
174
- const children = item.__children;
175
- if (Object.keys(children).length > 0) {
176
- out += walk(children, fullSegments, `${indent} `);
177
- }
178
- }
179
- return out;
180
- }
181
- return walk(root).replace(/\n+$/, "");
182
- }
@@ -0,0 +1,27 @@
1
+ import { saveDocTranslation as _saveDocTranslation } from "../../utils/utils.mjs";
2
+
3
+ export default async function saveDocTranslation({
4
+ path,
5
+ docsDir,
6
+ translation,
7
+ language,
8
+ labels,
9
+ isShowMessage = false,
10
+ }) {
11
+ await _saveDocTranslation({
12
+ path,
13
+ docsDir,
14
+ language,
15
+ translation,
16
+ labels,
17
+ });
18
+
19
+ if (isShowMessage) {
20
+ const message = `āœ… Translation completed successfully.`;
21
+ return { message };
22
+ }
23
+
24
+ return {};
25
+ }
26
+
27
+ saveDocTranslation.task_render_mode = "hide";
@@ -0,0 +1,55 @@
1
+ import { recordUpdate } from "../../utils/history-utils.mjs";
2
+ import { shutdownMermaidWorkerPool } from "../../utils/mermaid-worker-pool.mjs";
3
+ import { saveDoc as _saveDoc } from "../../utils/utils.mjs";
4
+
5
+ export default async function saveDoc({
6
+ path,
7
+ content,
8
+ docsDir,
9
+ labels,
10
+ locale,
11
+ feedback,
12
+ isShowMessage = false,
13
+ ...rest
14
+ }) {
15
+ await _saveDoc({
16
+ path,
17
+ content,
18
+ docsDir,
19
+ labels,
20
+ locale,
21
+ });
22
+
23
+ if (feedback?.trim()) {
24
+ recordUpdate({
25
+ operation: "document_update",
26
+ feedback: feedback.trim(),
27
+ documentPath: path,
28
+ });
29
+ }
30
+
31
+ if (isShowMessage) {
32
+ // Shutdown mermaid worker pool to ensure clean exit
33
+ try {
34
+ await shutdownMermaidWorkerPool();
35
+ } catch (error) {
36
+ console.warn("Failed to shutdown mermaid worker pool:", error.message);
37
+ }
38
+
39
+ const message = `āœ… Document updated successfully.`;
40
+ return { message };
41
+ }
42
+
43
+ return {
44
+ path,
45
+ content,
46
+ docsDir,
47
+ labels,
48
+ locale,
49
+ feedback,
50
+ isShowMessage,
51
+ ...rest,
52
+ };
53
+ }
54
+
55
+ saveDoc.task_render_mode = "hide";
@@ -0,0 +1,59 @@
1
+ import { join } from "node:path";
2
+ import fs from "fs-extra";
3
+
4
+ export default async function saveSidebar({ documentStructure, docsDir }) {
5
+ // Generate _sidebar.md
6
+ try {
7
+ const sidebar = generateSidebar(documentStructure);
8
+ const sidebarPath = join(docsDir, "_sidebar.md");
9
+
10
+ await fs.ensureDir(docsDir);
11
+ await fs.writeFile(sidebarPath, sidebar, "utf8");
12
+ } catch (err) {
13
+ console.error("Failed to save _sidebar.md:", err.message);
14
+ }
15
+ return {};
16
+ }
17
+
18
+ // Recursively generate sidebar text, the link path is the flattened file name
19
+ function walk(node, parentSegments = [], indent = "") {
20
+ let out = "";
21
+ for (const key of Object.keys(node)) {
22
+ const item = node[key];
23
+ const fullSegments = [...parentSegments, key];
24
+ const flatFile = `${fullSegments.join("-")}.md`;
25
+ if (item.__title) {
26
+ const realIndent = item.__parentId === null ? "" : indent;
27
+ out += `${realIndent}* [${item.__title}](/${flatFile})\n`;
28
+ }
29
+ const children = item.__children;
30
+ if (Object.keys(children).length > 0) {
31
+ out += walk(children, fullSegments, `${indent} `);
32
+ }
33
+ }
34
+ return out;
35
+ }
36
+
37
+ function generateSidebar(documentStructure) {
38
+ // Build tree structure
39
+ const root = {};
40
+ for (const { path, title, parentId } of documentStructure) {
41
+ const relPath = path.replace(/^\//, "");
42
+ const segments = relPath.split("/");
43
+ let node = root;
44
+ for (let i = 0; i < segments.length; i++) {
45
+ const seg = segments[i];
46
+ if (!node[seg])
47
+ node[seg] = {
48
+ __children: {},
49
+ __title: null,
50
+ __fullPath: segments.slice(0, i + 1).join("/"),
51
+ __parentId: parentId,
52
+ };
53
+ if (i === segments.length - 1) node[seg].__title = title;
54
+ node = node[seg].__children;
55
+ }
56
+ }
57
+
58
+ return walk(root).replace(/\n+$/, "");
59
+ }
@@ -1,11 +1,11 @@
1
1
  import fs from "node:fs";
2
+ import { isRemoteFile } from "../../utils/file-utils.mjs";
2
3
  import { normalizePath, toRelativePath } from "../../utils/utils.mjs";
3
- import { checkIsRemoteFile } from "../../utils/file-utils.mjs";
4
4
 
5
5
  export default function transformDetailDatasources({ sourceIds }, options = {}) {
6
6
  // Read file content for each sourceId, ignoring failures
7
7
  let openAPISpec;
8
- const httpFileList = options?.context?.userContext?.httpFileList || [];
8
+ const remoteFileList = options?.context?.userContext?.remoteFileList || [];
9
9
  const contents = (sourceIds || [])
10
10
  .filter((id) => {
11
11
  const openApiSourceId = options?.context?.userContext?.openAPISpec?.sourceId;
@@ -17,8 +17,8 @@ export default function transformDetailDatasources({ sourceIds }, options = {})
17
17
  })
18
18
  .map((id) => {
19
19
  try {
20
- if (checkIsRemoteFile(id)) {
21
- const findFile = httpFileList.find((f) => f.sourceId === id);
20
+ if (isRemoteFile(id)) {
21
+ const findFile = remoteFileList.find((f) => f.sourceId === id);
22
22
  if (findFile) {
23
23
  return `// sourceId: ${id}\n${findFile.content}\n`;
24
24
  }
package/aigne.yaml CHANGED
@@ -18,7 +18,7 @@ agents:
18
18
  - ./agents/generate/check-document-structure.yaml
19
19
  - ./agents/generate/user-review-document-structure.mjs
20
20
  - ./agents/generate/index.yaml
21
-
21
+
22
22
  # Documentation Structure Tools
23
23
  - ./agents/generate/document-structure-tools/add-document.mjs
24
24
  - ./agents/generate/document-structure-tools/delete-document.mjs
@@ -47,6 +47,7 @@ agents:
47
47
 
48
48
  # Publishing
49
49
  - ./agents/publish/publish-docs.mjs
50
+ - ./agents/publish/translate-meta.mjs
50
51
  - ./agents/publish/index.yaml
51
52
 
52
53
  # Media
@@ -65,9 +66,11 @@ agents:
65
66
 
66
67
  # Utilities
67
68
  - ./agents/utils/load-sources.mjs
68
- - ./agents/utils/save-docs.mjs
69
+ - ./agents/utils/post-generate.mjs
70
+ - ./agents/utils/save-sidebar.mjs
69
71
  - ./agents/utils/transform-detail-datasources.mjs
70
- - ./agents/utils/save-single-doc.mjs
72
+ - ./agents/utils/save-doc.mjs
73
+ - ./agents/utils/save-doc-translation.mjs
71
74
  - ./agents/utils/save-output.mjs
72
75
  - ./agents/utils/format-document-structure.mjs
73
76
  - ./agents/utils/find-item-by-path.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.8.15-beta.2",
3
+ "version": "0.8.15-beta.4",
4
4
  "description": "AI-driven documentation generation tool built on the AIGNE Framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -97,6 +97,7 @@ Generate detailed and well-structured document for the current {{nodeName}} base
97
97
  YOU SHOULD:
98
98
  - Use AFS tools `afs_list` `afs_search` or `afs_read` to gather relevant and accurate information to enhance the content.
99
99
  - Follow rules in `<diagram_generation_guide>`: use `generateDiagram` tool to create and embed a diagram when appropriate, following the diagram generation guidelines.
100
+ - If the `generateDiagram` tool is not called, do not attempt to add any diagrams.
100
101
 
101
102
  <steps>
102
103
  1. Analyze the provided document structure and user requirements to plan the content.
@@ -286,7 +286,7 @@ export async function loadFilesFromPaths(sourcesPath, options = {}) {
286
286
  continue;
287
287
  }
288
288
 
289
- if (checkIsRemoteFile(dir)) {
289
+ if (isRemoteFile(dir)) {
290
290
  allFiles.push(dir);
291
291
  continue;
292
292
  }
@@ -387,8 +387,8 @@ export async function loadFilesFromPaths(sourcesPath, options = {}) {
387
387
  * @returns {Promise<boolean>} True if file appears to be a text file
388
388
  */
389
389
  async function isTextFile(filePath) {
390
- if (checkIsRemoteFile(filePath)) {
391
- return checkIsHttpTextFile(filePath);
390
+ if (isRemoteFile(filePath)) {
391
+ return isRemoteTextFile(filePath);
392
392
  }
393
393
 
394
394
  try {
@@ -400,14 +400,21 @@ async function isTextFile(filePath) {
400
400
  }
401
401
  }
402
402
 
403
- export function checkIsRemoteFile(filepath) {
404
- if (filepath.startsWith("http://") || filepath.startsWith("https://")) {
403
+ /**
404
+ * Check if a string is an HTTP/HTTPS URL
405
+ * @param {string} fileUrl - The string to check
406
+ * @returns {boolean} - True if the string starts with http:// or https://
407
+ */
408
+ export function isRemoteFile(fileUrl) {
409
+ if (typeof fileUrl !== "string") return false;
410
+
411
+ if (fileUrl.startsWith("http://") || fileUrl.startsWith("https://")) {
405
412
  return true;
406
413
  }
407
414
  return false;
408
415
  }
409
416
 
410
- export async function checkIsHttpTextFile(fileUrl) {
417
+ export async function isRemoteTextFile(fileUrl) {
411
418
  try {
412
419
  const res = await fetch(fileUrl, {
413
420
  method: "HEAD",
@@ -435,14 +442,14 @@ export async function checkIsHttpTextFile(fileUrl) {
435
442
  }
436
443
  }
437
444
 
438
- export async function getHttpFileContent(file) {
439
- if (!file) return null;
445
+ export async function getRemoteFileContent(fileUrl) {
446
+ if (!fileUrl) return null;
440
447
  try {
441
- const res = await fetch(file);
448
+ const res = await fetch(fileUrl);
442
449
  const text = await res.text();
443
450
  return text;
444
451
  } catch (error) {
445
- debug(`Failed to fetch HTTP file content: ${file} - ${error.message}`);
452
+ debug(`Failed to fetch HTTP file content: ${fileUrl} - ${error.message}`);
446
453
  return null;
447
454
  }
448
455
  }
@@ -469,8 +476,8 @@ export async function readFileContents(files, baseDir = process.cwd(), options =
469
476
  }
470
477
 
471
478
  try {
472
- if (checkIsRemoteFile(file)) {
473
- const content = await getHttpFileContent(file);
479
+ if (isRemoteFile(file)) {
480
+ const content = await getRemoteFileContent(file);
474
481
  if (content) {
475
482
  return {
476
483
  sourceId: file,
@@ -28,7 +28,7 @@ export default async function loadConfig({ config, appUrl }) {
28
28
  }
29
29
 
30
30
  // Parse new configuration fields and convert keys to actual content
31
- const processedConfig = processConfigFields(parsedConfig);
31
+ const processedConfig = await processConfigFields(parsedConfig);
32
32
 
33
33
  return {
34
34
  lastGitHead: parsedConfig.lastGitHead || "",
package/utils/utils.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  SUPPORTED_LANGUAGES,
19
19
  TARGET_AUDIENCES,
20
20
  } from "./constants/index.mjs";
21
+ import { isRemoteFile, getRemoteFileContent } from "./file-utils.mjs";
21
22
 
22
23
  /**
23
24
  * Normalize path to absolute path for consistent comparison
@@ -47,16 +48,6 @@ export function isGlobPattern(pattern) {
47
48
  return /[*?[\]]|(\*\*)/.test(pattern);
48
49
  }
49
50
 
50
- /**
51
- * Check if a string is an HTTP/HTTPS URL
52
- * @param {string} url - The string to check
53
- * @returns {boolean} - True if the string starts with http:// or https://
54
- */
55
- export function isHttp(url) {
56
- if (typeof url !== "string") return false;
57
- return url.startsWith("http://") || url.startsWith("https://");
58
- }
59
-
60
51
  export function processContent({ content }) {
61
52
  // Match markdown regular links [text](link), exclude images ![text](link)
62
53
  return content.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, (match, text, link) => {
@@ -81,77 +72,72 @@ export function processContent({ content }) {
81
72
  });
82
73
  }
83
74
 
75
+ // Helper function to generate filename based on language
76
+ export function getFileName(docPath, language) {
77
+ // Flatten path: remove leading /, replace all / with -
78
+ const flatName = docPath.replace(/^\//, "").replace(/\//g, "-");
79
+ const isEnglish = language === "en";
80
+ return isEnglish ? `${flatName}.md` : `${flatName}.${language}.md`;
81
+ }
82
+
84
83
  /**
85
- * Save a single document and its translations to files
84
+ * Save a single document to files
86
85
  * @param {Object} params
87
86
  * @param {string} params.path - Relative path (without extension)
88
87
  * @param {string} params.content - Main document content
89
88
  * @param {string} params.docsDir - Root directory
90
89
  * @param {string} params.locale - Main content language (e.g., 'en', 'zh', 'fr')
91
- * @param {Array<{language: string, translation: string}>} [params.translates] - Translation content
92
90
  * @param {Array<string>} [params.labels] - Document labels for front matter
93
- * @returns {Promise<Array<{ path: string, success: boolean, error?: string }>>}
91
+ * @returns {Promise<{ path: string, success: boolean, error?: string }>}
94
92
  */
95
- export async function saveDocWithTranslations({
96
- path: docPath,
97
- content,
98
- docsDir,
99
- locale,
100
- translates = [],
101
- labels,
102
- isTranslate = false,
103
- }) {
104
- const results = [];
93
+ export async function saveDoc({ path: docPath, content, docsDir, locale, labels }) {
105
94
  try {
106
- // Flatten path: remove leading /, replace all / with -
107
- const flatName = docPath.replace(/^\//, "").replace(/\//g, "-");
108
95
  await fs.mkdir(docsDir, { recursive: true });
96
+ const mainFileName = getFileName(docPath, locale);
97
+ const mainFilePath = path.join(docsDir, mainFileName);
109
98
 
110
- // Helper function to generate filename based on language
111
- const getFileName = (language) => {
112
- const isEnglish = language === "en";
113
- return isEnglish ? `${flatName}.md` : `${flatName}.${language}.md`;
114
- };
115
-
116
- // Save main content with appropriate filename based on locale (skip if isTranslate is true)
117
- if (!isTranslate) {
118
- const mainFileName = getFileName(locale);
119
- const mainFilePath = path.join(docsDir, mainFileName);
120
-
121
- // Add labels front matter if labels are provided
122
- let finalContent = processContent({ content });
99
+ // Add labels front matter if labels are provided
100
+ let finalContent = processContent({ content });
123
101
 
124
- if (labels && labels.length > 0) {
125
- const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
126
- finalContent = frontMatter + finalContent;
127
- }
128
-
129
- await fs.writeFile(mainFilePath, finalContent, "utf8");
130
- results.push({ path: mainFilePath, success: true });
102
+ if (labels && labels.length > 0) {
103
+ const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
104
+ finalContent = frontMatter + finalContent;
131
105
  }
132
106
 
133
- // Process all translations
134
- for (const translate of translates) {
135
- const translateFileName = getFileName(translate.language);
136
- const translatePath = path.join(docsDir, translateFileName);
107
+ await fs.writeFile(mainFilePath, finalContent, "utf8");
108
+ return { path: mainFilePath, success: true };
109
+ } catch (err) {
110
+ return { path: docPath, success: false, error: err.message };
111
+ }
112
+ }
137
113
 
138
- // Add labels front matter to translation content if labels are provided
139
- let finalTranslationContent = processContent({
140
- content: translate.translation,
141
- });
114
+ export async function saveDocTranslation({
115
+ path: docPath,
116
+ docsDir,
117
+ translation,
118
+ language,
119
+ labels,
120
+ }) {
121
+ try {
122
+ await fs.mkdir(docsDir, { recursive: true });
123
+ const translateFileName = getFileName(docPath, language);
124
+ const translatePath = path.join(docsDir, translateFileName);
142
125
 
143
- if (labels && labels.length > 0) {
144
- const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
145
- finalTranslationContent = frontMatter + finalTranslationContent;
146
- }
126
+ // Add labels front matter to translation content if labels are provided
127
+ let finalTranslationContent = processContent({
128
+ content: translation,
129
+ });
147
130
 
148
- await fs.writeFile(translatePath, finalTranslationContent, "utf8");
149
- results.push({ path: translatePath, success: true });
131
+ if (labels && labels.length > 0) {
132
+ const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
133
+ finalTranslationContent = frontMatter + finalTranslationContent;
150
134
  }
135
+
136
+ await fs.writeFile(translatePath, finalTranslationContent, "utf8");
137
+ return { path: translatePath, success: true };
151
138
  } catch (err) {
152
- results.push({ path: docPath, success: false, error: err.message });
139
+ return { path: docPath, success: false, error: err.message };
153
140
  }
154
- return results;
155
141
  }
156
142
 
157
143
  /**
@@ -963,7 +949,7 @@ export function processTargetAudience(targetAudienceTypes, existingTargetAudienc
963
949
  * @param {Object} config - Parsed configuration
964
950
  * @returns {Object} Processed configuration with content fields
965
951
  */
966
- export function processConfigFields(config) {
952
+ export async function processConfigFields(config) {
967
953
  const processed = {};
968
954
  const allRulesContent = [];
969
955
 
@@ -995,7 +981,15 @@ export function processConfigFields(config) {
995
981
  if (typeof config.rules === "string") {
996
982
  const existingRules = config.rules.trim();
997
983
  if (existingRules) {
998
- allRulesContent.push(existingRules);
984
+ // load rules from remote url
985
+ if (isRemoteFile(existingRules)) {
986
+ const remoteFileContent = await getRemoteFileContent(existingRules);
987
+ if (remoteFileContent) {
988
+ allRulesContent.push(remoteFileContent);
989
+ }
990
+ } else {
991
+ allRulesContent.push(existingRules);
992
+ }
999
993
  }
1000
994
  } else if (Array.isArray(config.rules)) {
1001
995
  // Handle array of rules - join them with newlines
@@ -1054,6 +1048,12 @@ export function processConfigFields(config) {
1054
1048
  }
1055
1049
  }
1056
1050
 
1051
+ if (config.glossary) {
1052
+ if (isRemoteFile(config.glossary)) {
1053
+ processed.glossary = await getRemoteFileContent(config.glossary);
1054
+ }
1055
+ }
1056
+
1057
1057
  // Detect and handle conflicts in user selections
1058
1058
  const conflicts = detectResolvableConflicts(config);
1059
1059
  if (conflicts.length > 0) {
@@ -1097,8 +1097,10 @@ export function processConfigFields(config) {
1097
1097
  * @returns {Promise<any>} - The processed configuration with file content loaded in place of references.
1098
1098
  */
1099
1099
  export async function resolveFileReferences(obj, basePath = process.cwd()) {
1100
- if (typeof obj === "string" && obj.startsWith("@")) {
1101
- return await loadFileContent(obj.slice(1), basePath);
1100
+ if (typeof obj === "string") {
1101
+ if (obj.startsWith("@")) {
1102
+ return await loadFileContent(obj.slice(1), basePath);
1103
+ }
1102
1104
  }
1103
1105
 
1104
1106
  if (Array.isArray(obj)) {
@@ -1,41 +0,0 @@
1
- import { shutdownMermaidWorkerPool } from "../../utils/mermaid-worker-pool.mjs";
2
- import { saveDocWithTranslations } from "../../utils/utils.mjs";
3
-
4
- export default async function saveSingleDoc({
5
- path,
6
- content,
7
- docsDir,
8
- translates,
9
- labels,
10
- locale,
11
- isTranslate = false,
12
- isShowMessage = false,
13
- }) {
14
- const _results = await saveDocWithTranslations({
15
- path,
16
- content,
17
- docsDir,
18
- translates,
19
- labels,
20
- locale,
21
- isTranslate,
22
- });
23
-
24
- if (isShowMessage) {
25
- // Shutdown mermaid worker pool to ensure clean exit
26
- try {
27
- await shutdownMermaidWorkerPool();
28
- } catch (error) {
29
- console.warn("Failed to shutdown mermaid worker pool:", error.message);
30
- }
31
-
32
- const message = isTranslate
33
- ? `āœ… Translation completed successfully`
34
- : `āœ… Document updated successfully`;
35
- return { message };
36
- }
37
-
38
- return {};
39
- }
40
-
41
- saveSingleDoc.task_render_mode = "hide";