@dtd-dev/agent-kb 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +35 -14
  2. package/bin/cli.js +110 -33
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,7 +36,9 @@ Không cài global, không cần clone. Zero-dependency nên `npx` chạy tức
36
36
  | `help` | Trợ giúp |
37
37
 
38
38
  Chạy tương tác sẽ hỏi: **tên dự án, backend, database, frontend, CI/CD, lệnh deploy, tool phụ**
39
- (Enter để giữ gợi ý). agent-kb **tự đọc `package.json`/`go.mod`/… để gợi ý stack & database sẵn**.
39
+ (Enter để giữ gợi ý). agent-kb **tự đọc `package.json`/`go.mod`/… để gợi ý stack & database sẵn**
40
+ — quét cả **thư mục con tới 3 cấp** nên hỗ trợ **monorepo** (vd `apps/web`, `services/api`). Nếu
41
+ không thấy manifest nào, nó **báo rõ** và để bạn nhập tay (hoặc dùng cờ `--backend/--frontend/--database`).
40
42
  Các phần còn lại (cache, state, UI lib, container, URL môi trường, rollback…) là placeholder
41
43
  `<!-- vd: ... -->` trong file để bạn điền tay sau.
42
44
 
@@ -86,20 +88,39 @@ tiêu đề đã điền tên feature — khỏi copy thư mục `feature-a/` (v
86
88
  - `.ai/core/*` là Tier 1 (luôn load, giữ nhỏ). Mọi thứ khác load theo task qua router trong `AGENTS.md`.
87
89
  - Không nhân bản rule sang nhiều file → tránh lệch và tốn token.
88
90
 
89
- ## Tự publish lên npm (tuỳ chọn)
90
- 1. `name` trong `package.json` đã đặt là `@dtd-dev/agent-kb` (đổi scope nếu cần).
91
- 2. `npm login`
92
- 3. `npm publish --access public`
93
- Sau đó cả team dùng: `npx @dtd-dev/agent-kb`.
91
+ ## Bản đồ file: cái nào core, cái nào cá nhân hoá, cái nào nhờ AI điền
94
92
 
95
- Hoặc dùng local không cần publish:
96
- ```bash
97
- npm pack # tạo file .tgz
98
- # rồi ở dự án khác: npx /duong/dan/dtd-dev-agent-kb-0.1.0.tgz init
99
- ```
93
+ Sau khi `init`, mỗi file rơi vào **một** trong 3 nhóm. Cột "Ai điền" cho biết bạn phải tự làm hay nhờ AI/CLI:
94
+
95
+ | File / Thư mục | Nhóm | Ai điền | Ghi chú |
96
+ |---|---|---|---|
97
+ | `AGENTS.md` (router + quy tắc), `CLAUDE.md`/`GEMINI.md`/`copilot-instructions.md` | 🟦 **Core** | CLI | Khung dùng lại mọi dự án. Phần trong block `BEGIN/END agent-kb` do CLI quản — đừng sửa tay (chạy lại `init` sẽ ghi đè block). |
98
+ | `.ai/README.md` (index Tier 0) | 🟦 **Core** | — | Bản đồ "task → file". Giữ nguyên. |
99
+ | `.ai/workflows/*` (create-feature, fix-bug, code-review, release, learn, incident-response) | 🟦 **Core** | — | Phương pháp luận, dùng lại như nhau giữa các dự án. |
100
+ | `.ai/agents/*` (reviewer, tester, bug-fixer, feature-builder) | 🟦 **Core** | — | Nguồn chân lý của sub-agent; CLI tự emit sang `.claude/agents/`. |
101
+ | `.ai/core/tech-stack.md` | 🟩 **AI điền** | CLI + AI | CLI điền `{{...}}` từ nhận diện stack; AI bổ sung các chỗ `<!-- vd: ... -->` (cache, state, UI lib…) sau khi đọc code. |
102
+ | `.ai/core/coding-standards.md`, `.ai/core/glossary.md` | 🟩 **AI điền** | AI | Nhờ AI đọc codebase rồi viết. |
103
+ | `.ai/architecture.md` | 🟩 **AI điền** | AI | AI suy ra từ cấu trúc dự án. |
104
+ | `.ai/examples/*` | 🟩 **AI điền** | AI | Sinh từ code thật của bạn (thay ví dụ mẫu). |
105
+ | `.ai/skills/devops/*` (deployment, docker, github-actions), `.ai/skills/frontend/react.md`, `ui-guideline.md` | 🟩 **AI điền** | AI | Skill generic — AI viết theo stack thực tế. `deployment.md` có `{{DEPLOY_CMD}}` do CLI điền. |
106
+ | `.ai/memory/*` (common-bugs, lessons-learned, troubleshooting) | 🟩 **AI điền** | AI (dần dần) | Tích luỹ qua workflow `learn.md` mỗi khi rút kinh nghiệm. |
107
+ | `.ai/product/*` (vision, business-rules, domain-model) | 🟥 **Cá nhân hoá** | Người (+AI hỗ trợ) | **Nội dung shipped là MẪU CarePay** — bắt buộc thay bằng nghiệp vụ công ty bạn. Giữ cách đánh số (BR-001…). |
108
+ | `.ai/skills/backend/carepay-*`, `.ai/skills/frontend/carepay-firebase.md` | 🟥 **Cá nhân hoá** | Người | Skill nghiệp vụ mẫu — xoá hoặc thay bằng skill dự án bạn. |
109
+ | `.ai/decisions/ADR-00x-*` | 🟥 **Cá nhân hoá** | Người | Mẫu — thay bằng quyết định thật; tạo mới: `agent-kb adr <tiêu đề>`. |
110
+ | `.ai/specs/feature-a/*` | 🟥 **Cá nhân hoá** | Người | Spec mẫu — tạo spec thật: `agent-kb feature <tên>`. |
111
+
112
+ > 🟦 **Core** = giữ nguyên, dùng lại mọi dự án. 🟩 **AI điền** = nhờ AI đọc code rồi viết. 🟥 **Cá nhân hoá** = nội dung nghiệp vụ riêng, bạn (hoặc AI có ngữ cảnh công ty) phải thay.
113
+
114
+ ### AI đọc file nào mỗi lần chạy?
115
+ - **Luôn (mọi session)**: `CLAUDE.md` → import `AGENTS.md` (router + Tier 1). Tier 1 = `.ai/core/tech-stack.md` + `coding-standards.md` + `glossary.md`. Giữ 3 file này thật ngắn (< ~500 token/file) vì đây là phần tốn token cố định.
116
+ - **Theo task (Tier 2, on-demand)**: AI tra bảng router trong `AGENTS.md` / `.ai/README.md` rồi chỉ mở file khớp task — vd fix bug → `workflows/fix-bug.md` + `memory/common-bugs.md`; hiểu nghiệp vụ → `product/business-rules.md` + `product/domain-model.md`. **Không** nuốt cả `.ai/`.
100
117
 
101
- ## Cập nhật template về sau
102
- Sửa các file trong `template/` của package này → bump version → publish lại.
103
- Mọi dự án init sau đó nhận bản mới. (Dự án chạy lại `agent-kb init` để phần thiếu.)
118
+ Gợi ý onboard dự án mới: điền 🟩 (nhờ AI) trước → thay 🟥 (nghiệp vụ) → chạy `agent-kb doctor` để chốt.
119
+
120
+ **Prompt 1-câu để Agent tự điền các file 🟩** (dán vào Claude Code/Cursor… thư mục gốc dự án):
121
+
122
+ ```
123
+ Đọc codebase của dự án này rồi điền chính xác CHỈ các file nhóm 🟩 trong .ai/ (core/tech-stack.md ở các chỗ <!-- vd: ... -->, core/coding-standards.md, core/glossary.md, architecture.md, examples/*, skills/devops/* và skills/frontend/react.md + ui-guideline.md) bằng dữ kiện có thật trong code — giữ mỗi file core/* dưới ~500 token, KHÔNG đụng file 🟥 nghiệp vụ (product/*, skills/backend/carepay-*, carepay-firebase, decisions/, specs/), KHÔNG sửa trong block BEGIN/END agent-kb — xong chạy `agent-kb doctor` và sửa tới khi sạch cảnh báo.
124
+ ```
104
125
 
105
126
  MIT
package/bin/cli.js CHANGED
@@ -167,38 +167,86 @@ function substituteTokens(values) {
167
167
  }
168
168
  }
169
169
 
170
- // Đọc dự án để gợi ý stack chỉ đọc file, không đổi gì.
171
- function detectStack() {
172
- const out = { backend: null, frontend: null, database: null, cicd: null, stacks: [] };
173
- const addStack = (s) => {
174
- if (!out.stacks.includes(s)) out.stacks.push(s);
170
+ // Thư mục rác/sinh ra không quét để khỏi nhận diện nhầm & cho nhanh.
171
+ const IGNORE_DIRS = new Set([
172
+ "node_modules", ".git", ".ai", ".claude", "dist", "build", "out",
173
+ ".next", ".nuxt", "coverage", "vendor", "target", ".gradle", ".venv", "venv", "__pycache__",
174
+ ]);
175
+ const MANIFEST_NAMES = [
176
+ "package.json", "go.mod", "requirements.txt", "pyproject.toml", "Pipfile",
177
+ "pom.xml", "build.gradle", "build.gradle.kts", "Cargo.toml", "composer.json",
178
+ ];
179
+
180
+ // Tìm các thư mục có manifest, quét tối đa maxDepth cấp từ root (bỏ thư mục rác/dotdir).
181
+ function findProjectRoots(root, maxDepth) {
182
+ const roots = [];
183
+ const walk = (dir, depth) => {
184
+ let entries;
185
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
186
+ if (entries.some((e) => e.isFile() && MANIFEST_NAMES.includes(e.name))) roots.push(dir);
187
+ if (depth >= maxDepth) return;
188
+ for (const e of entries) {
189
+ if (e.isDirectory() && !e.name.startsWith(".") && !IGNORE_DIRS.has(e.name)) {
190
+ walk(path.join(dir, e.name), depth + 1);
191
+ }
192
+ }
175
193
  };
194
+ walk(root, 0);
195
+ return roots;
196
+ }
176
197
 
177
- const pkg = readJSONSafe(path.join(CWD, "package.json"));
198
+ // Nhận diện stack trong MỘT thư mục, gộp vào `out` (chỉ điền chỗ còn trống → root thắng subfolder).
199
+ function detectInDir(dir, out, addStack) {
200
+ const exists = (f) => fs.existsSync(path.join(dir, f));
201
+ const pkg = readJSONSafe(path.join(dir, "package.json"));
178
202
  if (pkg) {
179
203
  const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
180
204
  const has = (n) => Object.prototype.hasOwnProperty.call(deps, n);
181
- if (has("next")) { out.frontend = "Next.js + React"; addStack("react"); }
182
- else if (has("react")) { out.frontend = "React" + (has("vite") ? " + Vite" : ""); addStack("react"); }
183
- else if (has("vue")) out.frontend = "Vue";
184
- else if (has("svelte") || has("@sveltejs/kit")) out.frontend = "Svelte";
185
-
186
- if (has("@nestjs/core")) { out.backend = "Node.js + NestJS"; addStack("node"); }
187
- else if (has("express")) { out.backend = "Node.js + Express"; addStack("node"); }
188
- else if (has("fastify")) { out.backend = "Node.js + Fastify"; addStack("node"); }
189
- else if (!out.frontend) { out.backend = "Node.js"; addStack("node"); }
190
-
191
- if (has("pg") || has("postgres") || has("postgresql")) out.database = "PostgreSQL";
192
- else if (has("mysql") || has("mysql2")) out.database = "MySQL";
193
- else if (has("mongoose") || has("mongodb")) out.database = "MongoDB";
194
- else if (has("firebase") || has("firebase-admin")) out.database = "Firebase/Firestore";
195
- if (has("redis") || has("ioredis")) out.database = out.database ? out.database + " + Redis" : "Redis";
196
- }
197
- if (hasFile("go.mod")) { out.backend = out.backend || "Go"; addStack("go"); }
198
- if (hasFile("requirements.txt") || hasFile("pyproject.toml") || hasFile("Pipfile")) { out.backend = out.backend || "Python"; addStack("python"); }
199
- if (hasFile("pom.xml") || hasFile("build.gradle") || hasFile("build.gradle.kts")) { out.backend = out.backend || "Java"; addStack("java"); }
200
- if (hasFile("Cargo.toml")) { out.backend = out.backend || "Rust"; addStack("rust"); }
201
- if (hasFile("composer.json")) { out.backend = out.backend || "PHP"; addStack("php"); }
205
+ let fe = null, be = null;
206
+ if (has("next")) { fe = "Next.js + React"; addStack("react"); }
207
+ else if (has("react")) { fe = "React" + (has("vite") ? " + Vite" : ""); addStack("react"); }
208
+ else if (has("vue")) fe = "Vue";
209
+ else if (has("svelte") || has("@sveltejs/kit")) fe = "Svelte";
210
+
211
+ if (has("@nestjs/core")) { be = "Node.js + NestJS"; addStack("node"); }
212
+ else if (has("express")) { be = "Node.js + Express"; addStack("node"); }
213
+ else if (has("fastify")) { be = "Node.js + Fastify"; addStack("node"); }
214
+ else if (!fe) { out._node = true; addStack("node"); } // Node "trơn": ưu tiên thấp nhất, quyết ở cuối
215
+
216
+ if (fe && !out.frontend) out.frontend = fe;
217
+ if (be && !out.backend) out.backend = be;
218
+
219
+ if (!out.database) {
220
+ if (has("pg") || has("postgres") || has("postgresql")) out.database = "PostgreSQL";
221
+ else if (has("mysql") || has("mysql2")) out.database = "MySQL";
222
+ else if (has("mongoose") || has("mongodb")) out.database = "MongoDB";
223
+ else if (has("firebase") || has("firebase-admin")) out.database = "Firebase/Firestore";
224
+ }
225
+ if ((has("redis") || has("ioredis")) && !(out.database || "").includes("Redis")) {
226
+ out.database = out.database ? out.database + " + Redis" : "Redis";
227
+ }
228
+ }
229
+ if (exists("go.mod")) { out.backend = out.backend || "Go"; addStack("go"); }
230
+ if (exists("requirements.txt") || exists("pyproject.toml") || exists("Pipfile")) { out.backend = out.backend || "Python"; addStack("python"); }
231
+ if (exists("pom.xml") || exists("build.gradle") || exists("build.gradle.kts")) { out.backend = out.backend || "Java"; addStack("java"); }
232
+ if (exists("Cargo.toml")) { out.backend = out.backend || "Rust"; addStack("rust"); }
233
+ if (exists("composer.json")) { out.backend = out.backend || "PHP"; addStack("php"); }
234
+ }
235
+
236
+ // Đọc dự án để gợi ý stack — chỉ đọc file, không đổi gì. Quét cả thư mục con (monorepo).
237
+ function detectStack() {
238
+ const out = { backend: null, frontend: null, database: null, cicd: null, stacks: [], roots: [], _node: false };
239
+ const addStack = (s) => {
240
+ if (!out.stacks.includes(s)) out.stacks.push(s);
241
+ };
242
+
243
+ // Root (CWD) trước, rồi tới subfolder nông→sâu, để stack của thư mục gốc được ưu tiên.
244
+ const roots = findProjectRoots(CWD, 3).sort((a, b) => a.length - b.length);
245
+ for (const dir of roots) {
246
+ detectInDir(dir, out, addStack);
247
+ out.roots.push(path.relative(CWD, dir) || ".");
248
+ }
249
+ if (!out.backend && out._node) out.backend = "Node.js"; // fallback Node sau khi đã xét hết
202
250
 
203
251
  if (fs.existsSync(path.join(CWD, ".github", "workflows"))) out.cicd = "GitHub Actions";
204
252
  else if (hasFile(".gitlab-ci.yml")) out.cicd = "GitLab CI";
@@ -223,6 +271,8 @@ function availableStacks() {
223
271
  }
224
272
 
225
273
  // Emit sub-agent: copy .ai/agents/*.md (nguồn chân lý) -> .claude/agents/ cho Claude Code đọc.
274
+ // .claude/agents/ là bản emit PHÁI SINH — luôn ghi đè theo nguồn (không tôn trọng --force/skip),
275
+ // nếu không sửa .ai/agents/ rồi chạy lại init sẽ không đồng bộ.
226
276
  function emitAgents(stats) {
227
277
  const src = path.join(CWD, ".ai", "agents");
228
278
  if (!fs.existsSync(src)) return 0;
@@ -230,7 +280,17 @@ function emitAgents(stats) {
230
280
  let n = 0;
231
281
  for (const f of fs.readdirSync(src)) {
232
282
  if (!f.endsWith(".md")) continue;
233
- copyRecursive(path.join(src, f), path.join(dest, f), stats);
283
+ const to = path.join(dest, f);
284
+ const rel = path.relative(CWD, to);
285
+ const content = fs.readFileSync(path.join(src, f), "utf8");
286
+ const existed = fs.existsSync(to);
287
+ if (existed && fs.readFileSync(to, "utf8") === content) {
288
+ n++;
289
+ continue; // đã trùng nguồn → không ghi, không báo
290
+ }
291
+ fs.mkdirSync(dest, { recursive: true });
292
+ fs.writeFileSync(to, content);
293
+ (existed ? stats.synced : stats.written).push(rel);
234
294
  n++;
235
295
  }
236
296
  return n;
@@ -259,6 +319,14 @@ async function init() {
259
319
 
260
320
  if (det.backend || det.frontend || det.cicd) {
261
321
  console.log(`\nℹ Nhận diện stack: ${[det.backend, det.frontend, det.database, det.cicd].filter(Boolean).join(" · ") || "—"}`);
322
+ if (det.roots.length > 1 || (det.roots.length === 1 && det.roots[0] !== ".")) {
323
+ console.log(` (manifest tại: ${det.roots.join(", ")})`);
324
+ }
325
+ } else {
326
+ console.log("\n⚠ Không tự nhận diện được stack — không thấy manifest");
327
+ console.log(" (package.json / go.mod / requirements.txt / pyproject.toml / pom.xml / Cargo.toml / composer.json)");
328
+ console.log(" trong thư mục này hay các thư mục con (quét tối đa 3 cấp).");
329
+ console.log(" → Chạy ở đúng thư mục gốc dự án, hoặc nhập tay / dùng cờ --backend --frontend --database.");
262
330
  }
263
331
 
264
332
  if (!YES) {
@@ -279,7 +347,7 @@ async function init() {
279
347
  rl.close();
280
348
  }
281
349
 
282
- const stats = { written: [], skipped: [], merged: [] };
350
+ const stats = { written: [], skipped: [], merged: [], synced: [] };
283
351
  for (const [from, to] of MAP) {
284
352
  copyRecursive(path.join(TEMPLATE_DIR, from), path.join(CWD, to), stats);
285
353
  }
@@ -309,7 +377,10 @@ async function init() {
309
377
  }
310
378
  if (tools.length) console.log(`🔌 Tool đã bật: ${tools.join(", ")}`);
311
379
  if (stacks.length) console.log(`📦 Gói stack: ${stacks.join(", ")} → .ai/skills/stacks/`);
312
- if (agentsCount) console.log(`🤖 Sub-agents: ${agentsCount} → .claude/agents/ (nguồn: .ai/agents/)`);
380
+ if (agentsCount) {
381
+ const note = stats.synced.length ? ` (${stats.synced.length} đồng bộ lại từ nguồn)` : "";
382
+ console.log(`🤖 Sub-agents: ${agentsCount} → .claude/agents/ (nguồn: .ai/agents/)${note}`);
383
+ }
313
384
  if (stats.skipped.length) {
314
385
  console.log(`↷ Bỏ qua ${stats.skipped.length} file đã tồn tại (dùng --force để ghi đè).`);
315
386
  }
@@ -496,13 +567,19 @@ function doctor() {
496
567
  if (fs.existsSync(path.join(CWD, dest))) oks.push(`${dest} (${t})`);
497
568
  }
498
569
 
499
- // Sub-agents: mỗi .ai/agents/*.md cần có bản emit ở .claude/agents/
570
+ // Sub-agents: mỗi .ai/agents/*.md cần có bản emit ở .claude/agents/ KHỚP nội dung nguồn.
500
571
  const aiAgents = path.join(CWD, ".ai", "agents");
501
572
  if (fs.existsSync(aiAgents)) {
502
573
  for (const f of fs.readdirSync(aiAgents)) {
503
574
  if (!f.endsWith(".md")) continue;
504
- if (fs.existsSync(path.join(CWD, ".claude", "agents", f))) oks.push(`.claude/agents/${f}`);
505
- else warnings.push(`Agent .ai/agents/${f} chưa emit sang .claude/agents/ (chạy lại agent-kb init)`);
575
+ const emit = path.join(CWD, ".claude", "agents", f);
576
+ if (!fs.existsSync(emit)) {
577
+ warnings.push(`Agent .ai/agents/${f} chưa emit sang .claude/agents/ (chạy lại agent-kb init)`);
578
+ } else if (fs.readFileSync(path.join(aiAgents, f), "utf8") !== fs.readFileSync(emit, "utf8")) {
579
+ warnings.push(`.claude/agents/${f} lệch với nguồn .ai/agents/${f} (chạy lại agent-kb init để đồng bộ)`);
580
+ } else {
581
+ oks.push(`.claude/agents/${f}`);
582
+ }
506
583
  }
507
584
  }
508
585
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtd-dev/agent-kb",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Bộ agent skill chuẩn hoá cho doanh nghiệp: scaffold knowledge base .ai/ tiết kiệm token, tăng độ chính xác khi viết code, và cá nhân hoá theo nghiệp vụ công ty để tái dùng cho nhiều dự án cùng nghiệp vụ (AGENTS.md, CLAUDE.md, GEMINI.md, Copilot).",
5
5
  "bin": {
6
6
  "agent-kb": "bin/cli.js"