@aigne/doc-smith 0.8.15-beta → 0.8.15-beta.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.15-beta.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.15-beta...v0.8.15-beta.1) (2025-10-23)
4
+
5
+
6
+ ### Features
7
+
8
+ * support openapi datasource, generate better api docs ([#212](https://github.com/AIGNE-io/aigne-doc-smith/issues/212)) ([6a683f1](https://github.com/AIGNE-io/aigne-doc-smith/commit/6a683f117aa94f265383be6e1b3b957803004032))
9
+
3
10
  ## [0.8.15-beta](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.14...v0.8.15-beta) (2025-10-21)
4
11
 
5
12
 
@@ -2,6 +2,7 @@ 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
+
5
6
  import checkDetailResult from "../utils/check-detail-result.mjs";
6
7
 
7
8
  // Get current script directory
@@ -102,6 +103,19 @@ export default async function checkDocument(
102
103
  options.context.agents["saveSingleDoc"],
103
104
  ],
104
105
  });
106
+ let openAPISpec = null;
107
+
108
+ if (options.context?.userContext?.openAPISpec?.sourceId) {
109
+ const matchingDocument = originalDocumentStructure.find((item) => {
110
+ if (item.path === path) {
111
+ return item.sourceIds.find((x) => x === options.context.userContext.openAPISpec.sourceId);
112
+ }
113
+ return false;
114
+ });
115
+ if (matchingDocument) {
116
+ openAPISpec = options.context.userContext.openAPISpec;
117
+ }
118
+ }
105
119
 
106
120
  const result = await options.context.invoke(teamAgent, {
107
121
  ...rest,
@@ -112,6 +126,7 @@ export default async function checkDocument(
112
126
  originalDocumentStructure,
113
127
  documentStructure,
114
128
  detailFeedback: contentValidationFailed ? validationResult.detailFeedback : "",
129
+ openAPISpec,
115
130
  });
116
131
 
117
132
  return {
@@ -50,7 +50,12 @@ export default async function chooseDocs(
50
50
  });
51
51
 
52
52
  // Use title if available, otherwise fall back to filename
53
- const displayName = docItem?.title || file;
53
+ let displayName = docItem?.title;
54
+ if (displayName) {
55
+ displayName = `${displayName} (${file})`;
56
+ } else {
57
+ displayName = file;
58
+ }
54
59
 
55
60
  return {
56
61
  name: displayName,
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { statSync } from "node:fs";
2
3
  import path from "node:path";
3
4
  import imageSize from "image-size";
4
5
  import {
@@ -7,6 +8,7 @@ import {
7
8
  loadFilesFromPaths,
8
9
  readFileContents,
9
10
  getMimeType,
11
+ checkIsRemoteFile,
10
12
  } from "../../utils/file-utils.mjs";
11
13
  import {
12
14
  getCurrentGitHead,
@@ -18,28 +20,63 @@ import {
18
20
  DEFAULT_EXCLUDE_PATTERNS,
19
21
  DEFAULT_INCLUDE_PATTERNS,
20
22
  } from "../../utils/constants/index.mjs";
21
-
22
- export default async function loadSources({
23
- sources = [],
24
- sourcesPath = [],
25
- includePatterns,
26
- excludePatterns,
27
- outputDir,
28
- docsDir,
29
- "doc-path": docPath,
30
- boardId,
31
- useDefaultPatterns = true,
32
- lastGitHead,
33
- reset = false,
34
- media,
35
- } = {}) {
23
+ import { isOpenAPISpecFile } from "../../utils/openapi/index.mjs";
24
+
25
+ export default async function loadSources(
26
+ {
27
+ sources = [],
28
+ sourcesPath = [],
29
+ includePatterns,
30
+ excludePatterns,
31
+ outputDir,
32
+ docsDir,
33
+ "doc-path": docPath,
34
+ boardId,
35
+ useDefaultPatterns = true,
36
+ lastGitHead,
37
+ reset = false,
38
+ media,
39
+ } = {},
40
+ options,
41
+ ) {
36
42
  let files = Array.isArray(sources) ? [...sources] : [];
37
43
  const { minImageWidth } = media || { minImageWidth: 800 };
38
44
 
39
45
  if (sourcesPath) {
40
- const allFiles = await loadFilesFromPaths(sourcesPath, {
46
+ const sourcesPathList = Array.isArray(sourcesPath) ? sourcesPath : [sourcesPath];
47
+ const pickSourcesPath = [];
48
+ const omitSourcesPath = [];
49
+ sourcesPathList.forEach((x) => {
50
+ if (typeof x !== "string" || !x) {
51
+ return;
52
+ }
53
+ if (x.startsWith("!")) {
54
+ omitSourcesPath.push(x.substring(1));
55
+ } else {
56
+ pickSourcesPath.push(x);
57
+ }
58
+ });
59
+
60
+ const customExcludePatterns = omitSourcesPath
61
+ .map((x) => {
62
+ try {
63
+ const stats = statSync(x);
64
+ if (stats.isFile()) {
65
+ return x;
66
+ }
67
+ if (stats.isDirectory()) {
68
+ return `${x}/**`;
69
+ }
70
+ return null;
71
+ } catch (error) {
72
+ console.warn(`Failed to stat path ${x}: ${error.message}`);
73
+ return null;
74
+ }
75
+ })
76
+ .filter(Boolean);
77
+ const allFiles = await loadFilesFromPaths(pickSourcesPath, {
41
78
  includePatterns,
42
- excludePatterns,
79
+ excludePatterns: [...new Set([...(excludePatterns || []), ...customExcludePatterns])],
43
80
  useDefaultPatterns,
44
81
  defaultIncludePatterns: DEFAULT_INCLUDE_PATTERNS,
45
82
  defaultExcludePatterns: DEFAULT_EXCLUDE_PATTERNS,
@@ -50,9 +87,6 @@ export default async function loadSources({
50
87
 
51
88
  files = [...new Set(files)];
52
89
 
53
- // all files path
54
- const allFilesPaths = files.map((file) => `- ${toRelativePath(file)}`).join("\n");
55
-
56
90
  // Define media file extensions
57
91
  const mediaExtensions = [
58
92
  ".jpg",
@@ -110,7 +144,7 @@ export default async function loadSources({
110
144
  files.map(async (file) => {
111
145
  const ext = path.extname(file).toLowerCase();
112
146
 
113
- if (mediaExtensions.includes(ext)) {
147
+ if (mediaExtensions.includes(ext) && !checkIsRemoteFile(file)) {
114
148
  // This is a media file
115
149
  const relativePath = path.relative(docsDir, file);
116
150
  const fileName = path.basename(file);
@@ -161,7 +195,7 @@ export default async function loadSources({
161
195
  }
162
196
 
163
197
  // Read all source files using the utility function
164
- const sourceFiles = await readFileContents(sourceFilesPaths, process.cwd());
198
+ let sourceFiles = await readFileContents(sourceFilesPaths, process.cwd());
165
199
 
166
200
  // Count tokens and lines using utility function
167
201
  const { totalTokens, totalLines } = calculateFileStats(sourceFiles);
@@ -169,8 +203,32 @@ export default async function loadSources({
169
203
  // check if totalTokens is too large
170
204
  const isLargeContext = totalTokens > INTELLIGENT_SUGGESTION_TOKEN_THRESHOLD;
171
205
 
206
+ // filter OpenAPI doc should after check isLargeContext
207
+ sourceFiles = sourceFiles.filter((file) => {
208
+ if (options?.context?.userContext.openAPISpec) return true;
209
+
210
+ const isOpenAPI = isOpenAPISpecFile(file.content);
211
+ if (isOpenAPI && options?.context?.userContext) {
212
+ options.context.userContext.openAPISpec = file;
213
+ }
214
+ return !isOpenAPI;
215
+ });
216
+
217
+ const httpFileList = [];
218
+
219
+ sourceFiles.forEach((file) => {
220
+ if (checkIsRemoteFile(file.sourceId)) {
221
+ httpFileList.push(file);
222
+ }
223
+ });
224
+ if (options?.context?.userContext) {
225
+ options.context.userContext.httpFileList = httpFileList;
226
+ }
227
+
172
228
  // Build allSources string using utility function
173
229
  const allSources = buildSourcesContent(sourceFiles, isLargeContext);
230
+ // all files path
231
+ const allFilesPaths = sourceFiles.map((x) => `- ${toRelativePath(x.sourceId)}`).join("\n");
174
232
 
175
233
  // Get the last documentation structure
176
234
  let originalDocumentStructure;
@@ -1,11 +1,30 @@
1
1
  import fs from "node:fs";
2
2
  import { normalizePath, toRelativePath } from "../../utils/utils.mjs";
3
+ import { checkIsRemoteFile } from "../../utils/file-utils.mjs";
3
4
 
4
- export default function transformDetailDatasources({ sourceIds }) {
5
+ export default function transformDetailDatasources({ sourceIds }, options = {}) {
5
6
  // Read file content for each sourceId, ignoring failures
7
+ let openAPISpec;
8
+ const httpFileList = options?.context?.userContext?.httpFileList || [];
6
9
  const contents = (sourceIds || [])
10
+ .filter((id) => {
11
+ const openApiSourceId = options?.context?.userContext?.openAPISpec?.sourceId;
12
+ if (openApiSourceId !== undefined && openApiSourceId === id) {
13
+ openAPISpec = options.context.userContext.openAPISpec;
14
+ return false;
15
+ }
16
+ return true;
17
+ })
7
18
  .map((id) => {
8
19
  try {
20
+ if (checkIsRemoteFile(id)) {
21
+ const findFile = httpFileList.find((f) => f.sourceId === id);
22
+ if (findFile) {
23
+ return `// sourceId: ${id}\n${findFile.content}\n`;
24
+ }
25
+ return null;
26
+ }
27
+
9
28
  const normalizedId = normalizePath(id);
10
29
  const content = fs.readFileSync(normalizedId, "utf8");
11
30
  const relativeId = toRelativePath(id);
@@ -17,7 +36,10 @@ export default function transformDetailDatasources({ sourceIds }) {
17
36
  })
18
37
  .filter(Boolean);
19
38
 
20
- return { detailDataSources: contents.join("") };
39
+ return {
40
+ detailDataSources: contents.join(""),
41
+ openAPISpec,
42
+ };
21
43
  }
22
44
 
23
45
  transformDetailDatasources.task_render_mode = "hide";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.8.15-beta",
3
+ "version": "0.8.15-beta.1",
4
4
  "description": "AI-driven documentation generation tool built on the AIGNE Framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -913,6 +913,28 @@ Ensure that the shape names used in connections are accurate and match the actua
913
913
  - **Good Practice:**
914
914
  ```d2
915
915
  shape: sequence_diagram
916
+ User: {
917
+ shape: c4-person
918
+ }
919
+
920
+ App: {
921
+ label: "Your Application"
922
+ shape: rectangle
923
+
924
+ ResumeSubscription: {
925
+ label: "ResumeSubscription Component"
926
+ }
927
+ }
928
+
929
+ Payment-API: {
930
+ label: "Payment Backend API"
931
+ shape: rectangle
932
+ }
933
+
934
+ DID-Wallet: {
935
+ label: "DID Wallet"
936
+ icon: "https://www.arcblock.io/image-bin/uploads/37198ddc4a0b9e91e5c1c821ab895a34.svg"
937
+ }
916
938
 
917
939
  User -> App.ResumeSubscription: "1. Triggers resume action"
918
940
 
@@ -922,12 +944,16 @@ Ensure that the shape names used in connections are accurate and match the actua
922
944
  App.ResumeSubscription.t1 -> User: "4. Display confirmation dialog"
923
945
  User -> App.ResumeSubscription.t1: "5. Clicks 'Confirm'"
924
946
 
925
- App.ResumeSubscription.t1 -> DID-Wallet: "6a. Open 're-stake' session"
926
- User -> DID-Wallet: "7a. Approve in wallet"
927
- DID-Wallet -> App.ResumeSubscription.t1: "8a. Send success callback"
947
+ "If Re-Staking is Required": {
948
+ App.ResumeSubscription.t1 -> DID-Wallet: "6a. Open 're-stake' session"
949
+ User -> DID-Wallet: "7a. Approve in wallet"
950
+ DID-Wallet -> App.ResumeSubscription.t1: "8a. Send success callback"
951
+ }
928
952
 
929
- App.ResumeSubscription.t1 -> Payment-API: "6b. Call recover endpoint\n(PUT /recover)"
930
- Payment-API -> App.ResumeSubscription.t1: "7b. Return success"
953
+ "If No Staking is Required": {
954
+ App.ResumeSubscription.t1 -> Payment-API: "6b. Call recover endpoint\n(PUT /recover)"
955
+ Payment-API -> App.ResumeSubscription.t1: "7b. Return success"
956
+ }
931
957
 
932
958
  App.ResumeSubscription.t1 -> Payment-API: "9. Fetch updated subscription details"
933
959
  Payment-API -> App.ResumeSubscription.t1: "10. Return latest subscription"
@@ -1086,12 +1112,12 @@ Ensure that the shape names used in connections are accurate and match the actua
1086
1112
  Blocklet-Service -> Application.Auth-Middleware: "4. Return permissions"
1087
1113
  Application.Auth-Middleware -> Application.Auth-Middleware: "5. Evaluate all rules"
1088
1114
 
1089
- "If Authorized" {
1115
+ "If Authorized": {
1090
1116
  Application.Auth-Middleware -> Application.Protected-Route: "6a. next()"
1091
1117
  Application.Protected-Route -> Client: "7a. 200 OK Response"
1092
1118
  }
1093
1119
 
1094
- "If Forbidden" {
1120
+ "If Forbidden": {
1095
1121
  Application.Auth-Middleware -> Client: "6b. 403 Forbidden Response"
1096
1122
  }
1097
1123
  ```
@@ -27,6 +27,43 @@
27
27
 
28
28
  </datasources>
29
29
 
30
+ {% if openAPISpec %}
31
+ <openapi>
32
+
33
+ **Goal:** Use the provided OpenAPI (Swagger) specification, align it with the current page objective, and leverage it to refine this document.
34
+
35
+ **OpenAPI File Content:**
36
+ <openapi_doc>
37
+
38
+ {{ openAPISpec }}
39
+
40
+ </openapi_doc>
41
+
42
+ ---
43
+
44
+ ### **Documentation Requirements and Constraints**
45
+
46
+ 1. **Extract the core content:**
47
+ * Organize the document by functional modules.
48
+ * For each path item, include the following elements:
49
+ * HTTP method and path.
50
+ * Concise summary.
51
+ * Detailed description.
52
+ * Request parameters: name, location (`in`), type, required flag, description.
53
+ * Request body: describe its structure when present.
54
+ * Responses: at least the key status codes (e.g., 200, 201, 400, 500) and their schemas.
55
+
56
+ 2. **Mandatory API description constraints (deduplication rule):**
57
+ * **Ensure that throughout the document (including preface, overview, etc.), any introduction to the project APIs appears only within this OpenAPI-generated "API reference" section.**
58
+ * **Never** repeat or expand the interface list elsewhere in the document (for example, "Quick Start" or "Architecture Overview" sections).
59
+
60
+ ---
61
+
62
+ **Expected output format:** A concise, clear, and easy-to-scan Markdown document.
63
+
64
+ </openapi>
65
+ {% endif %}
66
+
30
67
 
31
68
  {% include "./detail-example.md" %}
32
69
 
@@ -12,6 +12,40 @@
12
12
  {{ datasources }}
13
13
  </datasources>
14
14
 
15
+ {% if userContext.openAPISpec %}
16
+ <openapi>
17
+
18
+ **Goal:** Use the provided OpenAPI (Swagger) specification to design how the OpenAPI content and the overall document should be structured together.
19
+
20
+ **OpenAPI File Content:**
21
+ <openapi_doc>
22
+
23
+ {{ userContext.openAPISpec }}
24
+
25
+ </openapi_doc>
26
+
27
+ ---
28
+
29
+ ### **Documentation Requirements and Constraints**
30
+
31
+ 1. **Section structure and titles:**
32
+ * Create a dedicated top-level section for the OpenAPI content.
33
+ * The section title must be professional and user friendly; **never** include terms such as OpenAPI, Swagger, or file formats. Recommended titles include **"API Interface Reference"** or **"Interface Reference"**.
34
+
35
+ 2. **Content hierarchy and presentation:**
36
+ * **Ideal state (single-level page):** Prefer to present all API endpoints within **one Markdown file (one page)**.
37
+ * **Split criteria (two-level pages):** Only when the number of endpoints is too large for a single file should you split by OpenAPI tags or logical modules, creating individual Markdown files per module.
38
+ * **Forced file hierarchy constraint:** Whether using one or two levels, the generated API reference files (Markdown) may contain **no more than two levels.**
39
+ * **Example (two-level structure):** `/api-reference.md` (index) -> `/api/user.md`, `/api/order.md` (module pages)
40
+ * **Disallow any third level or deeper structure:** for example, `/api/v1/user/get.md`.
41
+
42
+ 3. **Mandatory API description constraints (deduplication rule):**
43
+ * **Ensure that for the entire document (including preface, overview, etc.), any introduction to the project APIs appears only within this OpenAPI-generated "API reference" section.**
44
+ * **Never** repeat or extend the API list elsewhere in the document (for example, "Quick Start" or "Architecture Overview" sections).
45
+
46
+ </openapi>
47
+ {% endif %}
48
+
15
49
 
16
50
  {% if originalDocumentStructure %}
17
51
  <last_document_structure>
@@ -8,6 +8,8 @@ import { isBinaryFile } from "isbinaryfile";
8
8
  import { encode } from "gpt-tokenizer";
9
9
  import { fileTypeFromBuffer } from "file-type";
10
10
  import { gunzipSync } from "node:zlib";
11
+
12
+ import { debug } from "./debug.mjs";
11
13
  import { isGlobPattern } from "./utils.mjs";
12
14
  import { INTELLIGENT_SUGGESTION_TOKEN_THRESHOLD } from "./constants/index.mjs";
13
15
  import { uploadFiles } from "./upload-files.mjs";
@@ -284,6 +286,11 @@ export async function loadFilesFromPaths(sourcesPath, options = {}) {
284
286
  continue;
285
287
  }
286
288
 
289
+ if (checkIsRemoteFile(dir)) {
290
+ allFiles.push(dir);
291
+ continue;
292
+ }
293
+
287
294
  // First try to access as a file or directory
288
295
  const stats = await stat(dir);
289
296
 
@@ -313,7 +320,13 @@ export async function loadFilesFromPaths(sourcesPath, options = {}) {
313
320
  : [];
314
321
 
315
322
  finalIncludePatterns = [...defaultIncludePatterns, ...userInclude];
316
- finalExcludePatterns = [...defaultExcludePatterns, ...userExclude];
323
+ finalExcludePatterns = [
324
+ ...defaultExcludePatterns,
325
+ ...userExclude.map((x) => {
326
+ const prefix = `${dir}/`;
327
+ return x.startsWith(prefix) ? x.slice(prefix.length) : x;
328
+ }),
329
+ ];
317
330
  } else {
318
331
  // Use only user patterns
319
332
  if (includePatterns) {
@@ -374,6 +387,10 @@ export async function loadFilesFromPaths(sourcesPath, options = {}) {
374
387
  * @returns {Promise<boolean>} True if file appears to be a text file
375
388
  */
376
389
  async function isTextFile(filePath) {
390
+ if (checkIsRemoteFile(filePath)) {
391
+ return checkIsHttpTextFile(filePath);
392
+ }
393
+
377
394
  try {
378
395
  const isBinary = await isBinaryFile(filePath);
379
396
  return !isBinary;
@@ -383,6 +400,53 @@ async function isTextFile(filePath) {
383
400
  }
384
401
  }
385
402
 
403
+ export function checkIsRemoteFile(filepath) {
404
+ if (filepath.startsWith("http://") || filepath.startsWith("https://")) {
405
+ return true;
406
+ }
407
+ return false;
408
+ }
409
+
410
+ export async function checkIsHttpTextFile(fileUrl) {
411
+ try {
412
+ const res = await fetch(fileUrl, {
413
+ method: "HEAD",
414
+ });
415
+ const contentType = res.headers.get("content-type") || "";
416
+ const textMimeTypes = [
417
+ "application/json",
418
+ "application/ld+json",
419
+ "application/graphql+json",
420
+ "application/xml",
421
+ "application/xhtml+xml",
422
+ "application/javascript",
423
+ "application/ecmascript",
424
+ "application/x-www-form-urlencoded",
425
+ "application/rss+xml",
426
+ "application/atom+xml",
427
+ ];
428
+ if (contentType.startsWith("text/") || textMimeTypes.includes(contentType)) {
429
+ return true;
430
+ }
431
+ return false;
432
+ } catch (error) {
433
+ debug(`Failed to check HTTP file content type: ${fileUrl} - ${error.message}`);
434
+ return null;
435
+ }
436
+ }
437
+
438
+ export async function getHttpFileContent(file) {
439
+ if (!file) return null;
440
+ try {
441
+ const res = await fetch(file);
442
+ const text = await res.text();
443
+ return text;
444
+ } catch (error) {
445
+ debug(`Failed to fetch HTTP file content: ${file} - ${error.message}`);
446
+ return null;
447
+ }
448
+ }
449
+
386
450
  /**
387
451
  * Read and parse file contents from an array of file paths
388
452
  * @param {string[]} files - Array of file paths to read
@@ -405,12 +469,24 @@ export async function readFileContents(files, baseDir = process.cwd(), options =
405
469
  }
406
470
 
407
471
  try {
408
- const content = await readFile(file, "utf8");
409
- const relativePath = path.relative(baseDir, file);
410
- return {
411
- sourceId: relativePath,
412
- content,
413
- };
472
+ if (checkIsRemoteFile(file)) {
473
+ const content = await getHttpFileContent(file);
474
+ if (content) {
475
+ return {
476
+ sourceId: file,
477
+ content,
478
+ };
479
+ }
480
+
481
+ return null;
482
+ } else {
483
+ const content = await readFile(file, "utf8");
484
+ const relativePath = path.relative(baseDir, file);
485
+ return {
486
+ sourceId: relativePath,
487
+ content,
488
+ };
489
+ }
414
490
  } catch (error) {
415
491
  // If reading as text fails (e.g., binary file), skip it
416
492
  console.warn(`Failed to read file as text: ${file} - ${error.message}`);
@@ -0,0 +1,24 @@
1
+ import { parse } from "yaml";
2
+
3
+ export function isOpenAPISpecFile(content) {
4
+ const trimmedContent = content.trim();
5
+ try {
6
+ const parsed = parse(trimmedContent, {
7
+ logLevel: "silent",
8
+ });
9
+ if (parsed.openapi || parsed.swagger) {
10
+ return true;
11
+ }
12
+ } catch {
13
+ //
14
+ }
15
+ try {
16
+ const parsed = JSON.parse(trimmedContent);
17
+ if (parsed.openapi || parsed.swagger) {
18
+ return true;
19
+ }
20
+ } catch {
21
+ //
22
+ }
23
+ return false;
24
+ }