@khiem_enhance/ai-doc-agent 0.1.1 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getCacheKey = getCacheKey;
7
+ exports.readCache = readCache;
8
+ exports.writeCache = writeCache;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const crypto_1 = __importDefault(require("crypto"));
12
+ const CACHE_DIR = ".ai-doc-cache";
13
+ function sha1(input) {
14
+ return crypto_1.default.createHash("sha1").update(input).digest("hex");
15
+ }
16
+ function getCacheKey(parts) {
17
+ return sha1(`${parts.kind}:${parts.name}:${parts.fileList}:${parts.payload}`);
18
+ }
19
+ function readCache(key) {
20
+ const p = path_1.default.join(CACHE_DIR, `${key}.md`);
21
+ if (!fs_1.default.existsSync(p))
22
+ return null;
23
+ return fs_1.default.readFileSync(p, "utf-8");
24
+ }
25
+ function writeCache(key, content) {
26
+ fs_1.default.mkdirSync(CACHE_DIR, { recursive: true });
27
+ fs_1.default.writeFileSync(path_1.default.join(CACHE_DIR, `${key}.md`), content);
28
+ }
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ require("dotenv/config");
4
5
  const commander_1 = require("commander");
5
6
  const generate_1 = require("./commands/generate");
6
7
  const program = new commander_1.Command();
@@ -10,8 +11,27 @@ program
10
11
  .version("0.1.0");
11
12
  program
12
13
  .command("generate")
13
- .option("--only <part>", "architecture|modules", "architecture")
14
- .option("--output <dir>", "Docs output directory", "docs")
15
14
  .option("--since <commit>", "Only analyze changes since commit")
16
- .action(generate_1.generateDocs);
15
+ .option("--output <dir>", "Docs output directory", "docs")
16
+ .option("--only <part>", "architecture|modules|all", "all")
17
+ .option("--max-files <n>", "Max files included per LLM request", "8")
18
+ .option("--max-chars <n>", "Max characters included per LLM request", "60000")
19
+ .option("--module <name>", "Only generate docs for a specific module")
20
+ .option("--max-modules <n>", "Max modules to generate in one run", "3")
21
+ .action(async (opts) => {
22
+ // normalize options
23
+ const only = String(opts.only ?? "all");
24
+ const maxFiles = Number(opts.maxFiles ?? 8);
25
+ const maxChars = Number(opts.maxChars ?? 60000);
26
+ const maxModules = Number(opts.maxModules ?? 3);
27
+ await (0, generate_1.generateDocs)({
28
+ since: opts.since,
29
+ output: opts.output,
30
+ only: only ?? "all",
31
+ maxFiles: Number.isFinite(maxFiles) ? maxFiles : 8,
32
+ maxChars: Number.isFinite(maxChars) ? maxChars : 60000,
33
+ module: opts.module ? String(opts.module) : undefined,
34
+ maxModules: Number.isFinite(maxModules) ? maxModules : 3,
35
+ });
36
+ });
17
37
  program.parse();
@@ -12,35 +12,80 @@ const modules_1 = require("../analyzers/modules");
12
12
  const markdownWriter_1 = require("../writers/markdownWriter");
13
13
  const gitUtils_1 = require("../git/gitUtils");
14
14
  const moduleDetector_1 = require("../scanner/moduleDetector");
15
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
15
+ const fileRanker_1 = require("../scanner/fileRanker");
16
+ const docCache_1 = require("../cache/docCache");
17
+ const truncate = (s, maxChars) => s.length > maxChars ? s.slice(0, maxChars) + "\n\n...<truncated>" : s;
18
+ function buildPayload(files, maxFiles, maxChars) {
19
+ const raw = files
20
+ .slice(0, maxFiles)
21
+ .map((f) => `FILE: ${f}\n${(0, contentReader_1.readFile)(f)}`)
22
+ .join("\n\n");
23
+ return truncate(raw, maxChars);
24
+ }
16
25
  async function generateDocs(options) {
17
26
  const root = process.cwd();
18
- const files = options.since
27
+ const allFiles = options.since
19
28
  ? (0, gitUtils_1.getChangedFiles)(options.since).map((f) => path_1.default.join(root, f))
20
29
  : await (0, fileScanner_1.scanProject)(root);
30
+ const only = options.only ?? "all";
31
+ const maxFiles = Math.max(1, options.maxFiles ?? 8);
32
+ const maxChars = Math.max(5000, options.maxChars ?? 60000);
33
+ const maxModules = Math.max(1, options.maxModules ?? 3);
34
+ // Rank files globally for architecture (better signal, lower token)
35
+ const rankedAll = (0, fileRanker_1.rankFiles)(allFiles, root);
21
36
  // ---------- Architecture ----------
22
- const tree = files.map((f) => path_1.default.relative(root, f)).join("\n");
23
- const architectureSource = files
24
- .slice(0, 25)
25
- .map((f) => `FILE: ${f}\n${(0, contentReader_1.readFile)(f)}`)
26
- .join("\n\n");
27
- const architecture = await (0, architecture_1.generateArchitectureDoc)(tree, architectureSource);
28
- (0, markdownWriter_1.writeDoc)(options.output, "architecture.md", architecture);
29
- // ✅ Throttle để không chạm RPM ngay sau architecture
30
- await sleep(22000);
37
+ if (only === "architecture" || only === "all") {
38
+ const tree = allFiles.map((f) => path_1.default.relative(root, f)).join("\n");
39
+ const architecturePayload = buildPayload(rankedAll, maxFiles, maxChars);
40
+ const archCacheKey = (0, docCache_1.getCacheKey)({
41
+ kind: "architecture",
42
+ name: "architecture",
43
+ fileList: tree,
44
+ payload: architecturePayload,
45
+ });
46
+ const cachedArch = (0, docCache_1.readCache)(archCacheKey);
47
+ const architecture = cachedArch ?? (await (0, architecture_1.generateArchitectureDoc)(tree, architecturePayload));
48
+ if (cachedArch) {
49
+ console.log("🧠 Cache hit: architecture");
50
+ }
51
+ else {
52
+ (0, docCache_1.writeCache)(archCacheKey, architecture);
53
+ console.log("🧠 Cache miss: architecture (generated)");
54
+ }
55
+ (0, markdownWriter_1.writeDoc)(options.output, "architecture.md", architecture);
56
+ console.log("📄 Architecture doc generated");
57
+ }
31
58
  // ---------- Modules ----------
32
- const modules = (0, moduleDetector_1.detectModules)(files, root);
33
- for (const [moduleName, moduleFiles] of Object.entries(modules)) {
34
- const fileList = moduleFiles.map((f) => path_1.default.relative(root, f)).join("\n");
35
- const source = moduleFiles
36
- .slice(0, 20)
37
- .map((f) => `FILE: ${f}\n${(0, contentReader_1.readFile)(f)}`)
38
- .join("\n\n");
39
- const doc = await (0, modules_1.generateModuleDocs)(moduleName, fileList, source);
40
- (0, markdownWriter_1.writeDoc)(path_1.default.join(options.output, "modules"), `${moduleName}.md`, doc);
41
- console.log(`📄 Module doc generated: ${moduleName}`);
42
- // ✅ Throttle giữa các module để giữ < 3 request/min
43
- await sleep(22000);
59
+ if (only === "modules" || only === "all") {
60
+ const modules = (0, moduleDetector_1.detectModules)(allFiles, root);
61
+ const entries = Object.entries(modules).filter(([name]) => options.module ? name === options.module : true);
62
+ const limitedEntries = entries.slice(0, maxModules);
63
+ for (const [moduleName, moduleFiles] of limitedEntries) {
64
+ const fileList = moduleFiles.map((f) => path_1.default.relative(root, f)).join("\n");
65
+ const rankedModuleFiles = (0, fileRanker_1.rankFiles)(moduleFiles, root);
66
+ const modulePayload = buildPayload(rankedModuleFiles, maxFiles, maxChars);
67
+ const moduleCacheKey = (0, docCache_1.getCacheKey)({
68
+ kind: "module",
69
+ name: moduleName,
70
+ fileList,
71
+ payload: modulePayload,
72
+ });
73
+ const cachedModule = (0, docCache_1.readCache)(moduleCacheKey);
74
+ const doc = cachedModule ?? (await (0, modules_1.generateModuleDocs)(moduleName, fileList, modulePayload));
75
+ if (cachedModule) {
76
+ console.log(`🧠 Cache hit: module ${moduleName}`);
77
+ }
78
+ else {
79
+ (0, docCache_1.writeCache)(moduleCacheKey, doc);
80
+ console.log(`🧠 Cache miss: module ${moduleName} (generated)`);
81
+ }
82
+ (0, markdownWriter_1.writeDoc)(path_1.default.join(options.output, "modules"), `${moduleName}.md`, doc);
83
+ console.log(`📄 Module doc generated: ${moduleName}`);
84
+ }
85
+ if (entries.length > limitedEntries.length) {
86
+ console.log(`ℹ️ Skipped ${entries.length - limitedEntries.length} modules due to --max-modules=${maxModules}. ` +
87
+ `Re-run with higher limit or specify --module <name>.`);
88
+ }
44
89
  }
45
90
  console.log("✅ Docs generation completed");
46
91
  }
@@ -6,14 +6,60 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.openai = void 0;
7
7
  exports.askLLM = askLLM;
8
8
  const openai_1 = __importDefault(require("openai"));
9
- const env_1 = require("../config/env");
10
- exports.openai = new openai_1.default({
11
- apiKey: env_1.env.openaiKey
12
- });
13
- async function askLLM(prompt) {
14
- const res = await exports.openai.chat.completions.create({
15
- model: env_1.env.model,
16
- messages: [{ role: "user", content: prompt }]
17
- });
18
- return res.choices[0].message.content;
9
+ const apiKey = process.env.OPENAI_API_KEY;
10
+ if (!apiKey) {
11
+ throw new Error("Missing OPENAI_API_KEY.\n" +
12
+ "Set it before running:\n" +
13
+ ' export OPENAI_API_KEY="sk-xxxx"\n');
14
+ }
15
+ exports.openai = new openai_1.default({ apiKey });
16
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
17
+ /**
18
+ * askLLM with:
19
+ * - retry on 429 using retry-after headers
20
+ * - fail gracefully if retry-after is extremely long (token budget exhausted)
21
+ */
22
+ async function askLLM(prompt, maxRetries = 5) {
23
+ const model = process.env.MODEL || "gpt-4.1-mini";
24
+ let attempt = 0;
25
+ while (true) {
26
+ try {
27
+ const res = await exports.openai.chat.completions.create({
28
+ model,
29
+ messages: [{ role: "user", content: prompt }],
30
+ });
31
+ return res.choices[0].message.content ?? "";
32
+ }
33
+ catch (err) {
34
+ attempt++;
35
+ const status = err?.status;
36
+ const headers = err?.headers || {};
37
+ const retryAfterMs = Number(headers["retry-after-ms"]) ||
38
+ (Number(headers["retry-after"]) || 0) * 1000;
39
+ if (status === 429) {
40
+ // If retry-after is huge => org quota/token budget exhausted for a long window.
41
+ if (retryAfterMs && retryAfterMs > 120000) {
42
+ throw new Error([
43
+ "Rate limited (429) due to token/request limits.",
44
+ `Retry-After is very long: ~${Math.ceil(retryAfterMs / 1000)}s.`,
45
+ "",
46
+ "Fix suggestions:",
47
+ "- Reduce input size:",
48
+ " ai-doc-agent generate --only architecture --max-files 5 --max-chars 30000",
49
+ "- Or run modules separately:",
50
+ " ai-doc-agent generate --only modules --max-files 4 --max-chars 25000",
51
+ "- Or increase your OpenAI limits / add billing on your OpenAI account.",
52
+ ].join("\n"));
53
+ }
54
+ if (attempt <= maxRetries) {
55
+ const wait = retryAfterMs || 20000;
56
+ console.warn(`⚠️ Rate limited (429). Retrying in ${Math.ceil(wait / 1000)}s... (${attempt}/${maxRetries})`);
57
+ await sleep(wait);
58
+ continue;
59
+ }
60
+ }
61
+ // Non-429 or exceeded retries
62
+ throw err;
63
+ }
64
+ }
19
65
  }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.rankFiles = rankFiles;
7
+ const path_1 = __importDefault(require("path"));
8
+ const IMPORTANT_PATTERNS = [
9
+ /route/i,
10
+ /router/i,
11
+ /controller/i,
12
+ /service/i,
13
+ /api/i,
14
+ /hook/i,
15
+ /store/i,
16
+ /slice/i,
17
+ /middleware/i,
18
+ /schema/i,
19
+ /model/i,
20
+ /dto/i,
21
+ /validator/i,
22
+ /utils/i,
23
+ ];
24
+ function rankFiles(files, root) {
25
+ return [...files].sort((a, b) => score(b, root) - score(a, root));
26
+ }
27
+ function score(filePath, root) {
28
+ const rel = path_1.default.relative(root, filePath);
29
+ let s = 0;
30
+ // Prefer shorter paths (usually closer to feature root)
31
+ s += Math.max(0, 30 - rel.split(path_1.default.sep).length * 3);
32
+ // Prefer key filenames/patterns
33
+ for (const re of IMPORTANT_PATTERNS) {
34
+ if (re.test(rel))
35
+ s += 25;
36
+ }
37
+ // Prefer index/entrypoints
38
+ if (/index\.(ts|tsx|js|jsx)$/.test(rel))
39
+ s += 20;
40
+ if (/main\.(ts|js)$/.test(rel))
41
+ s += 15;
42
+ if (/app\.(ts|tsx|js|jsx)$/.test(rel))
43
+ s += 15;
44
+ // Prefer config
45
+ if (/config/i.test(rel))
46
+ s += 8;
47
+ return s;
48
+ }
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "@khiem_enhance/ai-doc-agent",
3
- "version": "0.1.1",
3
+ "version": "0.1.5",
4
4
  "description": "AI-powered documentation generator from source code",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "ai-doc-agent": "dist/cli.js"
8
8
  },
9
9
  "main": "dist/cli.js",
10
- "files": ["dist", "README.md", "LICENSE"],
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
11
15
  "scripts": {
12
16
  "build": "tsc",
13
17
  "prepublishOnly": "npm run build"