@booklib/core 2.0.0

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 (374) hide show
  1. package/.cursor/rules/booklib-standards.mdc +40 -0
  2. package/.gemini/context.md +372 -0
  3. package/AGENTS.md +166 -0
  4. package/CHANGELOG.md +226 -0
  5. package/CLAUDE.md +81 -0
  6. package/CODE_OF_CONDUCT.md +31 -0
  7. package/CONTRIBUTING.md +304 -0
  8. package/LICENSE +21 -0
  9. package/PLAN.md +28 -0
  10. package/README.ja.md +198 -0
  11. package/README.ko.md +198 -0
  12. package/README.md +503 -0
  13. package/README.pt-BR.md +198 -0
  14. package/README.uk.md +241 -0
  15. package/README.zh-CN.md +198 -0
  16. package/SECURITY.md +9 -0
  17. package/agents/architecture-reviewer.md +136 -0
  18. package/agents/booklib-reviewer.md +90 -0
  19. package/agents/data-reviewer.md +107 -0
  20. package/agents/jvm-reviewer.md +146 -0
  21. package/agents/python-reviewer.md +128 -0
  22. package/agents/rust-reviewer.md +115 -0
  23. package/agents/ts-reviewer.md +110 -0
  24. package/agents/ui-reviewer.md +117 -0
  25. package/assets/logo.svg +36 -0
  26. package/bin/booklib-mcp.js +304 -0
  27. package/bin/booklib.js +1705 -0
  28. package/bin/skills.cjs +1292 -0
  29. package/booklib-router.mdc +36 -0
  30. package/booklib.config.json +19 -0
  31. package/commands/animation-at-work.md +10 -0
  32. package/commands/clean-code-reviewer.md +10 -0
  33. package/commands/data-intensive-patterns.md +10 -0
  34. package/commands/data-pipelines.md +10 -0
  35. package/commands/design-patterns.md +10 -0
  36. package/commands/domain-driven-design.md +10 -0
  37. package/commands/effective-java.md +10 -0
  38. package/commands/effective-kotlin.md +10 -0
  39. package/commands/effective-python.md +10 -0
  40. package/commands/effective-typescript.md +10 -0
  41. package/commands/kotlin-in-action.md +10 -0
  42. package/commands/lean-startup.md +10 -0
  43. package/commands/microservices-patterns.md +10 -0
  44. package/commands/programming-with-rust.md +10 -0
  45. package/commands/refactoring-ui.md +10 -0
  46. package/commands/rust-in-action.md +10 -0
  47. package/commands/skill-router.md +10 -0
  48. package/commands/spring-boot-in-action.md +10 -0
  49. package/commands/storytelling-with-data.md +10 -0
  50. package/commands/system-design-interview.md +10 -0
  51. package/commands/using-asyncio-python.md +10 -0
  52. package/commands/web-scraping-python.md +10 -0
  53. package/community/registry.json +1616 -0
  54. package/hooks/hooks.json +23 -0
  55. package/hooks/posttooluse-capture.mjs +67 -0
  56. package/hooks/suggest.js +153 -0
  57. package/lib/agent-behaviors.js +40 -0
  58. package/lib/agent-detector.js +96 -0
  59. package/lib/config-loader.js +39 -0
  60. package/lib/conflict-resolver.js +148 -0
  61. package/lib/context-builder.js +574 -0
  62. package/lib/discovery-engine.js +298 -0
  63. package/lib/doctor/hook-installer.js +83 -0
  64. package/lib/doctor/usage-tracker.js +87 -0
  65. package/lib/engine/ai-features.js +253 -0
  66. package/lib/engine/auditor.js +103 -0
  67. package/lib/engine/bm25-index.js +178 -0
  68. package/lib/engine/capture.js +120 -0
  69. package/lib/engine/corrections.js +198 -0
  70. package/lib/engine/doctor.js +195 -0
  71. package/lib/engine/graph-injector.js +137 -0
  72. package/lib/engine/graph.js +161 -0
  73. package/lib/engine/handoff.js +405 -0
  74. package/lib/engine/indexer.js +242 -0
  75. package/lib/engine/parser.js +53 -0
  76. package/lib/engine/query-expander.js +42 -0
  77. package/lib/engine/reranker.js +40 -0
  78. package/lib/engine/rrf.js +59 -0
  79. package/lib/engine/scanner.js +151 -0
  80. package/lib/engine/searcher.js +139 -0
  81. package/lib/engine/session-coordinator.js +306 -0
  82. package/lib/engine/session-manager.js +429 -0
  83. package/lib/engine/synthesizer.js +70 -0
  84. package/lib/installer.js +70 -0
  85. package/lib/instinct-block.js +33 -0
  86. package/lib/mcp-config-writer.js +88 -0
  87. package/lib/paths.js +57 -0
  88. package/lib/profiles/design.md +19 -0
  89. package/lib/profiles/general.md +16 -0
  90. package/lib/profiles/research-analysis.md +22 -0
  91. package/lib/profiles/software-development.md +23 -0
  92. package/lib/profiles/writing-content.md +19 -0
  93. package/lib/project-initializer.js +916 -0
  94. package/lib/registry/skills.js +102 -0
  95. package/lib/registry-searcher.js +99 -0
  96. package/lib/rules/rules-manager.js +169 -0
  97. package/lib/skill-fetcher.js +333 -0
  98. package/lib/well-known-builder.js +70 -0
  99. package/lib/wizard/index.js +404 -0
  100. package/lib/wizard/integration-detector.js +41 -0
  101. package/lib/wizard/project-detector.js +100 -0
  102. package/lib/wizard/prompt.js +156 -0
  103. package/lib/wizard/registry-embeddings.js +107 -0
  104. package/lib/wizard/skill-recommender.js +69 -0
  105. package/llms-full.txt +254 -0
  106. package/llms.txt +70 -0
  107. package/package.json +45 -0
  108. package/research-reports/2026-04-01-current-architecture.md +160 -0
  109. package/research-reports/IDEAS.md +93 -0
  110. package/rules/common/clean-code.md +42 -0
  111. package/rules/java/effective-java.md +42 -0
  112. package/rules/kotlin/effective-kotlin.md +37 -0
  113. package/rules/python/effective-python.md +38 -0
  114. package/rules/rust/rust.md +37 -0
  115. package/rules/typescript/effective-typescript.md +42 -0
  116. package/scripts/gen-llms-full.mjs +36 -0
  117. package/scripts/gen-og.mjs +142 -0
  118. package/scripts/validate-frontmatter.js +25 -0
  119. package/skills/animation-at-work/SKILL.md +270 -0
  120. package/skills/animation-at-work/assets/example_asset.txt +1 -0
  121. package/skills/animation-at-work/evals/evals.json +44 -0
  122. package/skills/animation-at-work/evals/results.json +13 -0
  123. package/skills/animation-at-work/examples/after.md +64 -0
  124. package/skills/animation-at-work/examples/before.md +35 -0
  125. package/skills/animation-at-work/references/api_reference.md +369 -0
  126. package/skills/animation-at-work/references/review-checklist.md +79 -0
  127. package/skills/animation-at-work/scripts/audit_animations.py +295 -0
  128. package/skills/animation-at-work/scripts/example.py +1 -0
  129. package/skills/clean-code-reviewer/SKILL.md +444 -0
  130. package/skills/clean-code-reviewer/audit.json +35 -0
  131. package/skills/clean-code-reviewer/evals/evals.json +185 -0
  132. package/skills/clean-code-reviewer/evals/results.json +13 -0
  133. package/skills/clean-code-reviewer/examples/after.md +48 -0
  134. package/skills/clean-code-reviewer/examples/before.md +33 -0
  135. package/skills/clean-code-reviewer/references/api_reference.md +158 -0
  136. package/skills/clean-code-reviewer/references/practices-catalog.md +282 -0
  137. package/skills/clean-code-reviewer/references/review-checklist.md +254 -0
  138. package/skills/clean-code-reviewer/scripts/pre-review.py +206 -0
  139. package/skills/data-intensive-patterns/SKILL.md +267 -0
  140. package/skills/data-intensive-patterns/assets/example_asset.txt +1 -0
  141. package/skills/data-intensive-patterns/evals/evals.json +54 -0
  142. package/skills/data-intensive-patterns/evals/results.json +13 -0
  143. package/skills/data-intensive-patterns/examples/after.md +61 -0
  144. package/skills/data-intensive-patterns/examples/before.md +38 -0
  145. package/skills/data-intensive-patterns/references/api_reference.md +34 -0
  146. package/skills/data-intensive-patterns/references/patterns-catalog.md +551 -0
  147. package/skills/data-intensive-patterns/references/review-checklist.md +193 -0
  148. package/skills/data-intensive-patterns/scripts/adr.py +213 -0
  149. package/skills/data-intensive-patterns/scripts/example.py +1 -0
  150. package/skills/data-pipelines/SKILL.md +259 -0
  151. package/skills/data-pipelines/assets/example_asset.txt +1 -0
  152. package/skills/data-pipelines/evals/evals.json +45 -0
  153. package/skills/data-pipelines/evals/results.json +13 -0
  154. package/skills/data-pipelines/examples/after.md +97 -0
  155. package/skills/data-pipelines/examples/before.md +37 -0
  156. package/skills/data-pipelines/references/api_reference.md +301 -0
  157. package/skills/data-pipelines/references/review-checklist.md +181 -0
  158. package/skills/data-pipelines/scripts/example.py +1 -0
  159. package/skills/data-pipelines/scripts/new_pipeline.py +444 -0
  160. package/skills/design-patterns/SKILL.md +271 -0
  161. package/skills/design-patterns/assets/example_asset.txt +1 -0
  162. package/skills/design-patterns/evals/evals.json +46 -0
  163. package/skills/design-patterns/evals/results.json +13 -0
  164. package/skills/design-patterns/examples/after.md +52 -0
  165. package/skills/design-patterns/examples/before.md +29 -0
  166. package/skills/design-patterns/references/api_reference.md +1 -0
  167. package/skills/design-patterns/references/patterns-catalog.md +726 -0
  168. package/skills/design-patterns/references/review-checklist.md +173 -0
  169. package/skills/design-patterns/scripts/example.py +1 -0
  170. package/skills/design-patterns/scripts/scaffold.py +807 -0
  171. package/skills/domain-driven-design/SKILL.md +142 -0
  172. package/skills/domain-driven-design/assets/example_asset.txt +1 -0
  173. package/skills/domain-driven-design/evals/evals.json +48 -0
  174. package/skills/domain-driven-design/evals/results.json +13 -0
  175. package/skills/domain-driven-design/examples/after.md +80 -0
  176. package/skills/domain-driven-design/examples/before.md +43 -0
  177. package/skills/domain-driven-design/references/api_reference.md +1 -0
  178. package/skills/domain-driven-design/references/patterns-catalog.md +545 -0
  179. package/skills/domain-driven-design/references/review-checklist.md +158 -0
  180. package/skills/domain-driven-design/scripts/example.py +1 -0
  181. package/skills/domain-driven-design/scripts/scaffold.py +421 -0
  182. package/skills/effective-java/SKILL.md +227 -0
  183. package/skills/effective-java/assets/example_asset.txt +1 -0
  184. package/skills/effective-java/evals/evals.json +46 -0
  185. package/skills/effective-java/evals/results.json +13 -0
  186. package/skills/effective-java/examples/after.md +83 -0
  187. package/skills/effective-java/examples/before.md +37 -0
  188. package/skills/effective-java/references/api_reference.md +1 -0
  189. package/skills/effective-java/references/items-catalog.md +955 -0
  190. package/skills/effective-java/references/review-checklist.md +216 -0
  191. package/skills/effective-java/scripts/checkstyle_setup.py +211 -0
  192. package/skills/effective-java/scripts/example.py +1 -0
  193. package/skills/effective-kotlin/SKILL.md +271 -0
  194. package/skills/effective-kotlin/assets/example_asset.txt +1 -0
  195. package/skills/effective-kotlin/audit.json +29 -0
  196. package/skills/effective-kotlin/evals/evals.json +45 -0
  197. package/skills/effective-kotlin/evals/results.json +13 -0
  198. package/skills/effective-kotlin/examples/after.md +36 -0
  199. package/skills/effective-kotlin/examples/before.md +38 -0
  200. package/skills/effective-kotlin/references/api_reference.md +1 -0
  201. package/skills/effective-kotlin/references/practices-catalog.md +1228 -0
  202. package/skills/effective-kotlin/references/review-checklist.md +126 -0
  203. package/skills/effective-kotlin/scripts/example.py +1 -0
  204. package/skills/effective-python/SKILL.md +441 -0
  205. package/skills/effective-python/evals/evals.json +44 -0
  206. package/skills/effective-python/evals/results.json +13 -0
  207. package/skills/effective-python/examples/after.md +56 -0
  208. package/skills/effective-python/examples/before.md +40 -0
  209. package/skills/effective-python/ref-01-pythonic-thinking.md +202 -0
  210. package/skills/effective-python/ref-02-lists-and-dicts.md +146 -0
  211. package/skills/effective-python/ref-03-functions.md +186 -0
  212. package/skills/effective-python/ref-04-comprehensions-generators.md +211 -0
  213. package/skills/effective-python/ref-05-classes-interfaces.md +188 -0
  214. package/skills/effective-python/ref-06-metaclasses-attributes.md +209 -0
  215. package/skills/effective-python/ref-07-concurrency.md +213 -0
  216. package/skills/effective-python/ref-08-robustness-performance.md +248 -0
  217. package/skills/effective-python/ref-09-testing-debugging.md +253 -0
  218. package/skills/effective-python/ref-10-collaboration.md +175 -0
  219. package/skills/effective-python/references/api_reference.md +218 -0
  220. package/skills/effective-python/references/practices-catalog.md +483 -0
  221. package/skills/effective-python/references/review-checklist.md +190 -0
  222. package/skills/effective-python/scripts/lint.py +173 -0
  223. package/skills/effective-typescript/SKILL.md +262 -0
  224. package/skills/effective-typescript/audit.json +29 -0
  225. package/skills/effective-typescript/evals/evals.json +37 -0
  226. package/skills/effective-typescript/evals/results.json +13 -0
  227. package/skills/effective-typescript/examples/after.md +70 -0
  228. package/skills/effective-typescript/examples/before.md +47 -0
  229. package/skills/effective-typescript/references/api_reference.md +118 -0
  230. package/skills/effective-typescript/references/practices-catalog.md +371 -0
  231. package/skills/effective-typescript/scripts/review.py +169 -0
  232. package/skills/kotlin-in-action/SKILL.md +261 -0
  233. package/skills/kotlin-in-action/assets/example_asset.txt +1 -0
  234. package/skills/kotlin-in-action/evals/evals.json +43 -0
  235. package/skills/kotlin-in-action/evals/results.json +13 -0
  236. package/skills/kotlin-in-action/examples/after.md +53 -0
  237. package/skills/kotlin-in-action/examples/before.md +39 -0
  238. package/skills/kotlin-in-action/references/api_reference.md +1 -0
  239. package/skills/kotlin-in-action/references/practices-catalog.md +436 -0
  240. package/skills/kotlin-in-action/references/review-checklist.md +204 -0
  241. package/skills/kotlin-in-action/scripts/example.py +1 -0
  242. package/skills/kotlin-in-action/scripts/setup_detekt.py +224 -0
  243. package/skills/lean-startup/SKILL.md +160 -0
  244. package/skills/lean-startup/assets/example_asset.txt +1 -0
  245. package/skills/lean-startup/evals/evals.json +43 -0
  246. package/skills/lean-startup/evals/results.json +13 -0
  247. package/skills/lean-startup/examples/after.md +80 -0
  248. package/skills/lean-startup/examples/before.md +34 -0
  249. package/skills/lean-startup/references/api_reference.md +319 -0
  250. package/skills/lean-startup/references/review-checklist.md +137 -0
  251. package/skills/lean-startup/scripts/example.py +1 -0
  252. package/skills/lean-startup/scripts/new_experiment.py +286 -0
  253. package/skills/microservices-patterns/SKILL.md +384 -0
  254. package/skills/microservices-patterns/evals/evals.json +45 -0
  255. package/skills/microservices-patterns/evals/results.json +13 -0
  256. package/skills/microservices-patterns/examples/after.md +69 -0
  257. package/skills/microservices-patterns/examples/before.md +40 -0
  258. package/skills/microservices-patterns/references/patterns-catalog.md +391 -0
  259. package/skills/microservices-patterns/references/review-checklist.md +169 -0
  260. package/skills/microservices-patterns/scripts/new_service.py +583 -0
  261. package/skills/programming-with-rust/SKILL.md +209 -0
  262. package/skills/programming-with-rust/evals/evals.json +37 -0
  263. package/skills/programming-with-rust/evals/results.json +13 -0
  264. package/skills/programming-with-rust/examples/after.md +107 -0
  265. package/skills/programming-with-rust/examples/before.md +59 -0
  266. package/skills/programming-with-rust/references/api_reference.md +152 -0
  267. package/skills/programming-with-rust/references/practices-catalog.md +335 -0
  268. package/skills/programming-with-rust/scripts/review.py +142 -0
  269. package/skills/refactoring-ui/SKILL.md +362 -0
  270. package/skills/refactoring-ui/assets/example_asset.txt +1 -0
  271. package/skills/refactoring-ui/evals/evals.json +45 -0
  272. package/skills/refactoring-ui/evals/results.json +13 -0
  273. package/skills/refactoring-ui/examples/after.md +85 -0
  274. package/skills/refactoring-ui/examples/before.md +58 -0
  275. package/skills/refactoring-ui/references/api_reference.md +355 -0
  276. package/skills/refactoring-ui/references/review-checklist.md +114 -0
  277. package/skills/refactoring-ui/scripts/audit_css.py +250 -0
  278. package/skills/refactoring-ui/scripts/example.py +1 -0
  279. package/skills/rust-in-action/SKILL.md +350 -0
  280. package/skills/rust-in-action/evals/evals.json +38 -0
  281. package/skills/rust-in-action/evals/results.json +13 -0
  282. package/skills/rust-in-action/examples/after.md +156 -0
  283. package/skills/rust-in-action/examples/before.md +56 -0
  284. package/skills/rust-in-action/references/practices-catalog.md +346 -0
  285. package/skills/rust-in-action/scripts/review.py +147 -0
  286. package/skills/skill-router/SKILL.md +186 -0
  287. package/skills/skill-router/evals/evals.json +38 -0
  288. package/skills/skill-router/evals/results.json +13 -0
  289. package/skills/skill-router/examples/after.md +63 -0
  290. package/skills/skill-router/examples/before.md +39 -0
  291. package/skills/skill-router/references/api_reference.md +24 -0
  292. package/skills/skill-router/references/routing-heuristics.md +89 -0
  293. package/skills/skill-router/references/skill-catalog.md +174 -0
  294. package/skills/skill-router/scripts/route.py +266 -0
  295. package/skills/spring-boot-in-action/SKILL.md +340 -0
  296. package/skills/spring-boot-in-action/evals/evals.json +39 -0
  297. package/skills/spring-boot-in-action/evals/results.json +13 -0
  298. package/skills/spring-boot-in-action/examples/after.md +185 -0
  299. package/skills/spring-boot-in-action/examples/before.md +84 -0
  300. package/skills/spring-boot-in-action/references/practices-catalog.md +403 -0
  301. package/skills/spring-boot-in-action/scripts/review.py +184 -0
  302. package/skills/storytelling-with-data/SKILL.md +241 -0
  303. package/skills/storytelling-with-data/assets/example_asset.txt +1 -0
  304. package/skills/storytelling-with-data/evals/evals.json +47 -0
  305. package/skills/storytelling-with-data/evals/results.json +13 -0
  306. package/skills/storytelling-with-data/examples/after.md +50 -0
  307. package/skills/storytelling-with-data/examples/before.md +33 -0
  308. package/skills/storytelling-with-data/references/api_reference.md +379 -0
  309. package/skills/storytelling-with-data/references/review-checklist.md +111 -0
  310. package/skills/storytelling-with-data/scripts/chart_review.py +301 -0
  311. package/skills/storytelling-with-data/scripts/example.py +1 -0
  312. package/skills/system-design-interview/SKILL.md +233 -0
  313. package/skills/system-design-interview/assets/example_asset.txt +1 -0
  314. package/skills/system-design-interview/evals/evals.json +46 -0
  315. package/skills/system-design-interview/evals/results.json +13 -0
  316. package/skills/system-design-interview/examples/after.md +94 -0
  317. package/skills/system-design-interview/examples/before.md +27 -0
  318. package/skills/system-design-interview/references/api_reference.md +582 -0
  319. package/skills/system-design-interview/references/review-checklist.md +201 -0
  320. package/skills/system-design-interview/scripts/example.py +1 -0
  321. package/skills/system-design-interview/scripts/new_design.py +421 -0
  322. package/skills/using-asyncio-python/SKILL.md +290 -0
  323. package/skills/using-asyncio-python/assets/example_asset.txt +1 -0
  324. package/skills/using-asyncio-python/evals/evals.json +43 -0
  325. package/skills/using-asyncio-python/evals/results.json +13 -0
  326. package/skills/using-asyncio-python/examples/after.md +68 -0
  327. package/skills/using-asyncio-python/examples/before.md +39 -0
  328. package/skills/using-asyncio-python/references/api_reference.md +267 -0
  329. package/skills/using-asyncio-python/references/review-checklist.md +149 -0
  330. package/skills/using-asyncio-python/scripts/check_blocking.py +270 -0
  331. package/skills/using-asyncio-python/scripts/example.py +1 -0
  332. package/skills/web-scraping-python/SKILL.md +280 -0
  333. package/skills/web-scraping-python/assets/example_asset.txt +1 -0
  334. package/skills/web-scraping-python/evals/evals.json +46 -0
  335. package/skills/web-scraping-python/evals/results.json +13 -0
  336. package/skills/web-scraping-python/examples/after.md +109 -0
  337. package/skills/web-scraping-python/examples/before.md +40 -0
  338. package/skills/web-scraping-python/references/api_reference.md +393 -0
  339. package/skills/web-scraping-python/references/review-checklist.md +163 -0
  340. package/skills/web-scraping-python/scripts/example.py +1 -0
  341. package/skills/web-scraping-python/scripts/new_scraper.py +231 -0
  342. package/skills/writing-plans/audit.json +34 -0
  343. package/tests/agent-detector.test.js +83 -0
  344. package/tests/corrections.test.js +245 -0
  345. package/tests/doctor/hook-installer.test.js +72 -0
  346. package/tests/doctor/usage-tracker.test.js +140 -0
  347. package/tests/engine/benchmark-eval.test.js +31 -0
  348. package/tests/engine/bm25-index.test.js +85 -0
  349. package/tests/engine/capture-command.test.js +35 -0
  350. package/tests/engine/capture.test.js +17 -0
  351. package/tests/engine/graph-augmented-search.test.js +107 -0
  352. package/tests/engine/graph-injector.test.js +44 -0
  353. package/tests/engine/graph.test.js +216 -0
  354. package/tests/engine/hybrid-searcher.test.js +74 -0
  355. package/tests/engine/indexer-bm25.test.js +37 -0
  356. package/tests/engine/mcp-tools.test.js +73 -0
  357. package/tests/engine/project-initializer-mcp.test.js +99 -0
  358. package/tests/engine/query-expander.test.js +36 -0
  359. package/tests/engine/reranker.test.js +51 -0
  360. package/tests/engine/rrf.test.js +49 -0
  361. package/tests/engine/srag-prefix.test.js +47 -0
  362. package/tests/instinct-block.test.js +23 -0
  363. package/tests/mcp-config-writer.test.js +60 -0
  364. package/tests/project-initializer-new-agents.test.js +48 -0
  365. package/tests/rules/rules-manager.test.js +230 -0
  366. package/tests/well-known-builder.test.js +40 -0
  367. package/tests/wizard/integration-detector.test.js +31 -0
  368. package/tests/wizard/project-detector.test.js +51 -0
  369. package/tests/wizard/prompt-session.test.js +61 -0
  370. package/tests/wizard/prompt.test.js +16 -0
  371. package/tests/wizard/registry-embeddings.test.js +35 -0
  372. package/tests/wizard/skill-recommender.test.js +34 -0
  373. package/tests/wizard/slot-count.test.js +25 -0
  374. package/vercel.json +21 -0
@@ -0,0 +1,916 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import https from 'https';
5
+ import matter from 'gray-matter';
6
+ import { fileURLToPath } from 'url';
7
+ import { parseSkillFile } from './engine/parser.js';
8
+ import { BookLibScanner } from './engine/scanner.js';
9
+ import { resolveBookLibPaths } from './paths.js';
10
+ import { loadConfig } from './config-loader.js';
11
+ import { AgentDetector } from './agent-detector.js';
12
+ import { renderBehaviors } from './agent-behaviors.js';
13
+ import { renderInstinctBlock } from './instinct-block.js';
14
+
15
+ const PACKAGE_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..');
16
+
17
+ const TOOL_FILE_MAP = {
18
+ claude: { filePath: 'CLAUDE.md', fileHeader: '' },
19
+ cursor: { filePath: '.cursor/rules/booklib-standards.mdc', fileHeader: null },
20
+ copilot: { filePath: '.github/copilot-instructions.md', fileHeader: '# Copilot Instructions\n\n' },
21
+ gemini: { filePath: '.gemini/context.md', fileHeader: '# Project Context\n\n' },
22
+ codex: { filePath: 'AGENTS.md', fileHeader: '# Agent Instructions\n\n' },
23
+ windsurf: { filePath: '.windsurfrules', fileHeader: '' },
24
+ 'roo-code': { filePath: '.roo/rules/booklib-standards.md', fileHeader: null },
25
+ openhands: { filePath: '.openhands/instructions.md', fileHeader: '# OpenHands Instructions\n\n' },
26
+ junie: { filePath: '.junie/guidelines.md', fileHeader: '# Junie Guidelines\n\n' },
27
+ goose: { filePath: '.goose/context.md', fileHeader: '# Goose Context\n\n' },
28
+ opencode: { filePath: '.opencode/instructions.md', fileHeader: '# OpenCode Instructions\n\n' },
29
+ letta: { filePath: '.letta/skills/booklib.md', fileHeader: null },
30
+ };
31
+
32
+ const TOOL_DOCS = {
33
+ claude: 'https://docs.anthropic.com/en/docs/claude-code/claude-md',
34
+ cursor: 'https://docs.cursor.com/context/rules-for-ai',
35
+ copilot: 'https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions',
36
+ gemini: 'https://github.com/google-gemini/gemini-cli#configuration',
37
+ codex: 'https://github.com/openai/codex#agents-md',
38
+ windsurf: 'https://docs.windsurf.com/windsurf/customize',
39
+ 'roo-code': 'https://docs.roocode.com/features/custom-rules',
40
+ openhands: 'https://docs.all-hands.dev/usage/configuration',
41
+ junie: 'https://www.jetbrains.com/help/junie/guidelines',
42
+ goose: 'https://block.github.io/goose/docs/configuration',
43
+ opencode: 'https://github.com/opencode-ai/opencode#configuration',
44
+ letta: 'https://docs.letta.com/agents/custom-instructions',
45
+ };
46
+
47
+ /**
48
+ * Generates tool-specific context files from BookLib skills.
49
+ *
50
+ * Supported targets:
51
+ * cursor → .cursor/rules/booklib-standards.mdc
52
+ * claude → CLAUDE.md (appends a standards section)
53
+ * copilot → .github/copilot-instructions.md
54
+ * gemini → .gemini/context.md
55
+ * all → all of the above
56
+ */
57
+ export class ProjectInitializer {
58
+ constructor(options = {}) {
59
+ this.paths = resolveBookLibPaths(options.projectCwd);
60
+ this.projectCwd = options.projectCwd ?? process.cwd();
61
+ this.config = loadConfig(options.projectCwd);
62
+ this.scanner = new BookLibScanner();
63
+ }
64
+
65
+ /**
66
+ * Detects which skills are relevant to the project via scan, returns skill names.
67
+ */
68
+ detectRelevantSkills() {
69
+ const files = this.scanner.getFiles(this.projectCwd);
70
+ const seen = new Set();
71
+ for (const file of files) {
72
+ const skill = this.scanner.detectSkill(file);
73
+ if (skill) seen.add(skill);
74
+ }
75
+ return [...seen];
76
+ }
77
+
78
+ /**
79
+ * Main entry point. Detects or uses provided skills, then writes context files.
80
+ *
81
+ * @param {object} opts
82
+ * @param {string[]} [opts.skills] - explicit skill names; auto-detected if omitted
83
+ * @param {string} opts.target - 'cursor' | 'claude' | 'copilot' | 'gemini' | 'all' | 'auto'
84
+ * @param {boolean} [opts.dryRun] - print what would be written, don't write
85
+ * @param {boolean} [opts.quiet] - suppress informational output (e.g. skipped files)
86
+ * @param {function} [opts.onFileConflict] - async callback invoked when a target file already
87
+ * exists. Receives { filePath, lineCount, hasMarkers } and should return 'skip' to leave the
88
+ * file untouched, or any other value ('append'|'update') to proceed with the default behavior.
89
+ * When omitted, existing files are always appended/updated without prompting.
90
+ * @param {string} [opts.profile] - activity-based profile name (e.g. 'software-development')
91
+ * @param {string} [opts.stack] - stack description to fill into profile template
92
+ * @param {boolean} [opts.legacy] - use old _render() with full block extraction (default: false)
93
+ * @returns {string[]} list of files written
94
+ */
95
+ async init({ skills, target = 'all', dryRun = false, quiet = false, onFileConflict, profile, stack, legacy = false } = {}) {
96
+ const skillNames = skills?.length ? skills : this.detectRelevantSkills();
97
+ if (skillNames.length === 0) {
98
+ throw new Error('No relevant skills detected. Pass --skills explicitly or run booklib index first.');
99
+ }
100
+
101
+ const ALL_TARGETS = [
102
+ 'claude', 'cursor', 'copilot', 'gemini', 'codex', 'windsurf',
103
+ 'roo-code', 'openhands', 'junie', 'goose', 'opencode', 'letta',
104
+ ];
105
+ const targets = target === 'all'
106
+ ? ALL_TARGETS
107
+ : target === 'auto'
108
+ ? new AgentDetector({ cwd: this.projectCwd }).detect()
109
+ : target.split(',').map(t => t.trim());
110
+
111
+ const MARKER_START = '<!-- booklib-standards-start -->';
112
+ const MARKER_RE = /<!-- booklib-standards-start -->[\s\S]*?<!-- booklib-standards-end -->/;
113
+ const MARKER_END = '<!-- booklib-standards-end -->';
114
+
115
+ // Only extract blocks if legacy mode is requested
116
+ const blocks = legacy ? this._extractBlocks(skillNames) : [];
117
+
118
+ const written = [];
119
+ for (const t of targets) {
120
+ let filePath, content, fileHeader;
121
+
122
+ if (legacy) {
123
+ ({ filePath, content, fileHeader } = this._render(t, blocks, skillNames));
124
+ } else {
125
+ const mapping = TOOL_FILE_MAP[t];
126
+ if (!mapping) continue;
127
+ filePath = mapping.filePath;
128
+ fileHeader = mapping.fileHeader;
129
+ const effectiveProfile = profile || 'software-development';
130
+ const profileContent = this._renderFromProfile(t, effectiveProfile, skillNames, stack);
131
+ content = `${MARKER_START}\n${profileContent}\n${MARKER_END}`;
132
+ }
133
+
134
+ const absPath = path.join(this.projectCwd, filePath);
135
+ if (dryRun) {
136
+ console.log(`\n[dry-run] Would write: ${filePath}\n${'─'.repeat(60)}\n${content.slice(0, 400)}…`);
137
+ } else {
138
+ // Conflict detection: when file exists and a callback is provided, let the caller decide
139
+ if (fs.existsSync(absPath) && onFileConflict) {
140
+ const existing = fs.readFileSync(absPath, 'utf8');
141
+ const hasMarkers = existing.includes(MARKER_START);
142
+ const lineCount = existing.split('\n').length;
143
+ const action = await onFileConflict({ filePath, lineCount, hasMarkers });
144
+ if (action === 'skip') {
145
+ if (!quiet) console.log(` · ${filePath} (skipped)`);
146
+ continue;
147
+ }
148
+ // 'append' or 'update' — fall through to existing write logic
149
+ }
150
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
151
+ if (fileHeader === null) {
152
+ // booklib owns this file entirely (cursor) — always overwrite
153
+ fs.writeFileSync(absPath, content);
154
+ } else if (fs.existsSync(absPath)) {
155
+ const existing = fs.readFileSync(absPath, 'utf8');
156
+ if (existing.includes(MARKER_START)) {
157
+ // Update only the booklib section, preserve everything else
158
+ fs.writeFileSync(absPath, existing.replace(MARKER_RE, content));
159
+ } else {
160
+ // File exists with no booklib section — append it
161
+ fs.appendFileSync(absPath, `\n\n${content}`);
162
+ }
163
+ } else {
164
+ // New file — write header + booklib section
165
+ fs.writeFileSync(absPath, `${fileHeader}${content}`);
166
+ }
167
+ written.push(filePath);
168
+ if (!quiet) console.log(` ✅ ${filePath}`);
169
+ }
170
+ }
171
+ return written;
172
+ }
173
+
174
+ // ── Private helpers ────────────────────────────────────────────────────────
175
+
176
+ /**
177
+ * Reads each skill's SKILL.md and extracts framework + pitfall blocks.
178
+ */
179
+ _extractBlocks(skillNames) {
180
+ const blocks = [];
181
+ for (const name of skillNames) {
182
+ const skillPath = path.join(this.paths.skillsPath, name, 'SKILL.md');
183
+ const cachePath = path.join(this.paths.cachePath, 'skills', name, 'SKILL.md');
184
+ const bundledPath = path.join(PACKAGE_ROOT, 'skills', name, 'SKILL.md');
185
+ const mdPath = [skillPath, cachePath, bundledPath].find(p => fs.existsSync(p)) ?? null;
186
+ if (!mdPath) continue;
187
+
188
+ const content = fs.readFileSync(mdPath, 'utf8');
189
+ const chunks = parseSkillFile(content, mdPath);
190
+
191
+ const framework = chunks.find(c =>
192
+ c.metadata.type === 'framework' || c.metadata.type === 'core_principles'
193
+ )?.text ?? null;
194
+
195
+ const pitfalls = chunks.find(c =>
196
+ c.metadata.type === 'pitfalls' || c.metadata.type === 'anti_patterns'
197
+ )?.text ?? null;
198
+
199
+ if (framework || pitfalls) {
200
+ blocks.push({ skill: name, framework, pitfalls });
201
+ }
202
+ }
203
+ return blocks;
204
+ }
205
+
206
+ /**
207
+ * Resolves the path to a skill's SKILL.md file by checking four locations
208
+ * in priority order: project-local, cached community, bundled, and Claude Code.
209
+ *
210
+ * @param {string} name - skill name (e.g. 'effective-kotlin')
211
+ * @returns {string|null} absolute path to SKILL.md, or null if not found
212
+ */
213
+ _findSkillFile(name) {
214
+ const candidates = [
215
+ path.join(this.paths.skillsPath, name, 'SKILL.md'),
216
+ path.join(this.paths.cachePath, 'skills', name, 'SKILL.md'),
217
+ path.join(PACKAGE_ROOT, 'skills', name, 'SKILL.md'),
218
+ path.join(os.homedir(), '.claude', 'skills', name, 'SKILL.md'),
219
+ ];
220
+ return candidates.find(p => fs.existsSync(p)) ?? null;
221
+ }
222
+
223
+ /**
224
+ * Reads ONLY frontmatter from each skill's SKILL.md and returns a concise
225
+ * markdown table. Used by Config Profiles (Spec A) to replace the verbose
226
+ * raw-block output of _extractBlocks().
227
+ *
228
+ * @param {string[]} skillNames - skill names to include
229
+ * @returns {string} markdown table or fallback message
230
+ */
231
+ _buildSkillTable(skillNames) {
232
+ const rows = [];
233
+ for (const name of skillNames) {
234
+ const skillPath = this._findSkillFile(name);
235
+ if (!skillPath) continue;
236
+ const content = fs.readFileSync(skillPath, 'utf8');
237
+ const { data } = matter(content);
238
+ const desc = (data.description ?? '').replace(/\n/g, ' ').slice(0, 60);
239
+ const tags = Array.isArray(data.tags) ? data.tags.join(', ') : '';
240
+ rows.push({ name: data.name ?? name, description: desc, tags });
241
+ }
242
+
243
+ if (rows.length === 0) return '_No skills matched._';
244
+
245
+ let table = '| Skill | Focus | Tags |\n|-------|-------|------|\n';
246
+ for (const r of rows) {
247
+ table += `| ${r.name} | ${r.description} | ${r.tags} |\n`;
248
+ }
249
+ return table;
250
+ }
251
+
252
+ /**
253
+ * Loads a profile template from lib/profiles/<name>.md.
254
+ * Falls back to 'general' if the requested profile does not exist.
255
+ *
256
+ * @param {string} profileName - profile identifier
257
+ * @returns {string} raw template content
258
+ */
259
+ _loadProfile(profileName) {
260
+ const profilePath = path.join(PACKAGE_ROOT, 'lib', 'profiles', `${profileName}.md`);
261
+ if (!fs.existsSync(profilePath)) {
262
+ return this._loadProfile('general'); // fallback
263
+ }
264
+ return fs.readFileSync(profilePath, 'utf8');
265
+ }
266
+
267
+ /**
268
+ * Renders a profile template by substituting {{variables}} with generated content.
269
+ *
270
+ * @param {string} target - tool target (e.g. 'claude', 'cursor')
271
+ * @param {string} profileName - profile identifier
272
+ * @param {string[]} skillNames - active skill names
273
+ * @param {string} [stackDescription] - user-provided stack description
274
+ * @returns {string} rendered markdown content
275
+ */
276
+ _renderFromProfile(target, profileName, skillNames, stackDescription) {
277
+ let template = this._loadProfile(profileName);
278
+
279
+ template = template.replace('{{stack}}', stackDescription || '<!-- Not specified -->');
280
+ template = template.replace('{{skills_table}}', this._buildSkillTable(skillNames));
281
+ template = template.replace('{{agent_behaviors}}', this._getAgentBehaviors(target));
282
+ template = template.replace('{{references}}', this._getReferences(target));
283
+
284
+ return template;
285
+ }
286
+
287
+ /**
288
+ * Returns the agent behavior instructions block for a given target tool.
289
+ * Uses renderInstinctBlock() for compact, action-oriented triggers.
290
+ *
291
+ * @param {string} target - tool target (e.g. 'claude', 'cursor', 'copilot')
292
+ * @returns {string} markdown block with agent behavior instructions
293
+ */
294
+ _getAgentBehaviors(target) {
295
+ return renderInstinctBlock(target);
296
+ }
297
+
298
+ /**
299
+ * Returns a References section with tool-specific documentation links.
300
+ *
301
+ * @param {string} target - tool target
302
+ * @returns {string} markdown references section
303
+ */
304
+ _getReferences(target) {
305
+ const url = TOOL_DOCS[target] ?? '';
306
+ const lines = [];
307
+ if (url) lines.push(`- [How to customize this file](${url})`);
308
+ lines.push('- [BookLib documentation](https://booklib-ai.github.io/booklib/)');
309
+ lines.push('- [BookLib skills catalog](https://github.com/booklib-ai/booklib)');
310
+ return '## References\n\n' + lines.join('\n');
311
+ }
312
+
313
+ /**
314
+ * Renders extracted blocks into a tool-specific file.
315
+ */
316
+ _render(target, blocks, skillNames) {
317
+ const principles = blocks
318
+ .filter(b => b.framework)
319
+ .map(b => `### From ${b.skill}\n\n${b.framework.trim()}`)
320
+ .join('\n\n---\n\n');
321
+
322
+ const antiPatterns = blocks
323
+ .filter(b => b.pitfalls)
324
+ .map(b => `### From ${b.skill}\n\n${b.pitfalls.trim()}`)
325
+ .join('\n\n---\n\n');
326
+
327
+ const sources = skillNames.join(', ');
328
+ const generated = `Generated by BookLib from: ${sources}`;
329
+
330
+ const toolDocsUrl = TOOL_DOCS[target] ?? '';
331
+ const referencesSection = `
332
+ ### References
333
+
334
+ - ${toolDocsUrl ? `[How to customize this file](${toolDocsUrl})` : 'Customize this file for your project'}
335
+ - [BookLib documentation](https://booklib-ai.github.io/booklib/)
336
+ - [BookLib skills catalog](https://github.com/booklib-ai/booklib)
337
+ `;
338
+
339
+ switch (target) {
340
+ case 'cursor':
341
+ // booklib owns .cursor/rules/booklib-standards.mdc — full overwrite, fileHeader: null signals that
342
+ return {
343
+ filePath: '.cursor/rules/booklib-standards.mdc',
344
+ fileHeader: null,
345
+ content: `---
346
+ description: Coding standards synthesized from ${sources}
347
+ alwaysApply: true
348
+ ---
349
+
350
+ <!-- ${generated} -->
351
+
352
+ ## Core Principles
353
+
354
+ ${principles || '_No structured principles found in selected skills._'}
355
+
356
+ ## Anti-Patterns to Avoid
357
+
358
+ ${antiPatterns || '_No anti-patterns found in selected skills._'}
359
+ ${referencesSection}`,
360
+ };
361
+
362
+ case 'claude':
363
+ return {
364
+ filePath: 'CLAUDE.md',
365
+ fileHeader: '',
366
+ content: `<!-- booklib-standards-start -->
367
+ ## Coding Standards
368
+
369
+ > ${generated}
370
+
371
+ ### Principles
372
+
373
+ ${principles || '_No structured principles found._'}
374
+
375
+ ### Anti-Patterns
376
+
377
+ ${antiPatterns || '_No anti-patterns found._'}
378
+ ${referencesSection}<!-- booklib-standards-end -->`,
379
+ };
380
+
381
+ case 'copilot':
382
+ return {
383
+ filePath: '.github/copilot-instructions.md',
384
+ fileHeader: '# Copilot Instructions\n\n',
385
+ content: `<!-- booklib-standards-start -->
386
+ <!-- ${generated} -->
387
+
388
+ ## What to follow
389
+
390
+ ${principles || '_No structured principles found._'}
391
+
392
+ ## What to avoid
393
+
394
+ ${antiPatterns || '_No anti-patterns found._'}
395
+ ${referencesSection}<!-- booklib-standards-end -->
396
+ `,
397
+ };
398
+
399
+ case 'gemini':
400
+ return {
401
+ filePath: '.gemini/context.md',
402
+ fileHeader: '# Project Context\n\n',
403
+ content: `<!-- booklib-standards-start -->
404
+ <!-- ${generated} -->
405
+
406
+ ## Coding Standards
407
+
408
+ ${principles || '_No structured principles found._'}
409
+
410
+ ## Anti-Patterns
411
+
412
+ ${antiPatterns || '_No anti-patterns found._'}
413
+ ${referencesSection}<!-- booklib-standards-end -->
414
+ `,
415
+ };
416
+
417
+ case 'codex':
418
+ return {
419
+ filePath: 'AGENTS.md',
420
+ fileHeader: '# Agent Instructions\n\n',
421
+ content: `<!-- booklib-standards-start -->
422
+ <!-- ${generated} -->
423
+
424
+ ## Coding Standards
425
+
426
+ ${principles || '_No structured principles found in selected skills._'}
427
+
428
+ ## Anti-Patterns to Avoid
429
+
430
+ ${antiPatterns || '_No anti-patterns found in selected skills._'}
431
+ ${referencesSection}<!-- booklib-standards-end -->
432
+ `,
433
+ };
434
+
435
+ case 'windsurf':
436
+ return {
437
+ filePath: '.windsurfrules',
438
+ fileHeader: '',
439
+ content: `<!-- booklib-standards-start -->
440
+ <!-- ${generated} -->
441
+
442
+ ## Core Principles
443
+
444
+ ${principles || '_No structured principles found in selected skills._'}
445
+
446
+ ## Anti-Patterns to Avoid
447
+
448
+ ${antiPatterns || '_No anti-patterns found in selected skills._'}
449
+ ${referencesSection}<!-- booklib-standards-end -->
450
+ `,
451
+ };
452
+
453
+ case 'roo-code':
454
+ return {
455
+ filePath: '.roo/rules/booklib-standards.md',
456
+ fileHeader: null,
457
+ content: `<!-- booklib-standards-start -->
458
+ <!-- ${generated} -->
459
+
460
+ ## Core Principles
461
+
462
+ ${principles || '_No structured principles found in selected skills._'}
463
+
464
+ ## Anti-Patterns
465
+
466
+ ${antiPatterns || '_No anti-patterns found._'}
467
+ ${referencesSection}<!-- booklib-standards-end -->
468
+ `,
469
+ };
470
+
471
+ case 'openhands':
472
+ return {
473
+ filePath: '.openhands/instructions.md',
474
+ fileHeader: '# OpenHands Instructions\n\n',
475
+ content: `<!-- booklib-standards-start -->
476
+ <!-- ${generated} -->
477
+
478
+ ## Coding Standards
479
+
480
+ ${principles || '_No structured principles found._'}
481
+
482
+ ## What to Avoid
483
+
484
+ ${antiPatterns || '_No anti-patterns found._'}
485
+ ${referencesSection}<!-- booklib-standards-end -->
486
+ `,
487
+ };
488
+
489
+ case 'junie':
490
+ return {
491
+ filePath: '.junie/guidelines.md',
492
+ fileHeader: '# Junie Guidelines\n\n',
493
+ content: `<!-- booklib-standards-start -->
494
+ <!-- ${generated} -->
495
+
496
+ ## Principles
497
+
498
+ ${principles || '_No structured principles found._'}
499
+
500
+ ## Anti-Patterns
501
+
502
+ ${antiPatterns || '_No anti-patterns found._'}
503
+ ${referencesSection}<!-- booklib-standards-end -->
504
+ `,
505
+ };
506
+
507
+ case 'goose':
508
+ return {
509
+ filePath: '.goose/context.md',
510
+ fileHeader: '# Goose Context\n\n',
511
+ content: `<!-- booklib-standards-start -->
512
+ <!-- ${generated} -->
513
+
514
+ ## Standards
515
+
516
+ ${principles || '_No structured principles found._'}
517
+
518
+ ## Avoid
519
+
520
+ ${antiPatterns || '_No anti-patterns found._'}
521
+ ${referencesSection}<!-- booklib-standards-end -->
522
+ `,
523
+ };
524
+
525
+ case 'opencode':
526
+ return {
527
+ filePath: '.opencode/instructions.md',
528
+ fileHeader: '# OpenCode Instructions\n\n',
529
+ content: `<!-- booklib-standards-start -->
530
+ <!-- ${generated} -->
531
+
532
+ ## Coding Standards
533
+
534
+ ${principles || '_No structured principles found._'}
535
+
536
+ ## Anti-Patterns
537
+
538
+ ${antiPatterns || '_No anti-patterns found._'}
539
+ ${referencesSection}<!-- booklib-standards-end -->
540
+ `,
541
+ };
542
+
543
+ case 'letta':
544
+ return {
545
+ filePath: '.letta/skills/booklib.md',
546
+ fileHeader: null,
547
+ content: `<!-- booklib-standards-start -->
548
+ <!-- ${generated} -->
549
+
550
+ # BookLib Knowledge
551
+
552
+ ## Principles
553
+
554
+ ${principles || '_No structured principles found._'}
555
+
556
+ ## Anti-Patterns
557
+
558
+ ${antiPatterns || '_No anti-patterns found._'}
559
+ ${referencesSection}<!-- booklib-standards-end -->
560
+ `,
561
+ };
562
+
563
+ default:
564
+ throw new Error(
565
+ `Unknown target: ${target}. Valid values: claude, cursor, copilot, gemini, codex, windsurf, roo-code, openhands, junie, goose, opencode, letta, all, auto`
566
+ );
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Returns additional skill names worth knowing about, based on detected skills
572
+ * and project characteristics. Used for discovery hints — NOT injected into CLAUDE.md.
573
+ */
574
+ suggestRelatedSkills(detectedSkills, projectCwd) {
575
+ const suggestions = new Set();
576
+ const packageJsonPath = path.join(projectCwd, 'package.json');
577
+ let pkg = {};
578
+ try { pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); } catch { /* no package.json */ }
579
+ const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
580
+
581
+ for (const skill of detectedSkills) {
582
+ if (skill === 'clean-code-reviewer') {
583
+ suggestions.add('node-error-handling');
584
+ if (deps.some(d => ['express', 'fastify', 'koa', 'hono'].includes(d))) {
585
+ suggestions.add('owasp-input-validation');
586
+ suggestions.add('node-security-validation');
587
+ }
588
+ }
589
+ if (skill === 'effective-typescript') {
590
+ suggestions.add('clean-code-reviewer');
591
+ suggestions.add('node-error-handling');
592
+ }
593
+ if (skill === 'effective-python') {
594
+ suggestions.add('django-security');
595
+ }
596
+ if (skill === 'effective-java') {
597
+ suggestions.add('springboot-patterns');
598
+ }
599
+ if (skill === 'effective-kotlin') {
600
+ suggestions.add('kotlin-testing');
601
+ }
602
+ }
603
+
604
+ // Remove already-detected skills from suggestions
605
+ for (const s of detectedSkills) suggestions.delete(s);
606
+ return [...suggestions].slice(0, 3);
607
+ }
608
+
609
+ // ── ECC Artifact fetching ──────────────────────────────────────────────────
610
+
611
+ /**
612
+ * Pulls rules/, agents/, and commands/ from all configured github-skills-dir sources.
613
+ *
614
+ * Sources opt in by including an "artifacts" array in booklib.config.json:
615
+ * { "type": "github-skills-dir", "repo": "...", "artifacts": ["rules", "agents", "commands"] }
616
+ *
617
+ * When multiple sources export the same artifact type, files are prefixed with the repo
618
+ * slug (last path segment of owner/repo) to avoid collisions.
619
+ *
620
+ * @param {object} opts
621
+ * @param {string[]|null} [opts.languages] - language folders for rules/ (null = all)
622
+ * @param {boolean} [opts.includeAgents] - pull agents/ → .claude/agents/
623
+ * @param {boolean} [opts.includeCommands] - pull commands/ → .claude/commands/
624
+ * @param {boolean} [opts.dryRun] - print what would be written without writing
625
+ * @returns {string[]} list of files written
626
+ */
627
+ async fetchEccArtifacts({ languages = null, includeAgents = true, includeCommands = true, dryRun = false } = {}) {
628
+ const artifactSources = this.config.sources.filter(
629
+ s => s.type === 'github-skills-dir' && Array.isArray(s.artifacts) && s.artifacts.length > 0
630
+ );
631
+
632
+ if (artifactSources.length === 0) {
633
+ throw new Error(
634
+ 'No artifact-capable sources found. Add "artifacts": ["rules","agents","commands"] to a ' +
635
+ 'github-skills-dir entry in booklib.config.json.'
636
+ );
637
+ }
638
+
639
+ // Determine which artifact types appear in more than one source — those need a prefix.
640
+ const typeCounts = { rules: 0, agents: 0, commands: 0 };
641
+ for (const src of artifactSources) {
642
+ if (src.artifacts.includes('rules')) typeCounts.rules++;
643
+ if (src.artifacts.includes('agents')) typeCounts.agents++;
644
+ if (src.artifacts.includes('commands')) typeCounts.commands++;
645
+ }
646
+
647
+ const written = [];
648
+
649
+ for (const source of artifactSources) {
650
+ const { repo, branch = 'main', artifacts: artifactList } = source;
651
+ // Derive a short slug from the repo name (owner/repo → repo segment)
652
+ const slug = repo.split('/').pop().replace(/[^a-z0-9]/gi, '-').toLowerCase();
653
+
654
+ if (languages !== false && artifactList.includes('rules')) {
655
+ const prefix = typeCounts.rules > 1 ? `${slug}-` : '';
656
+ written.push(...await this._pullRules(repo, branch, languages, dryRun, prefix));
657
+ }
658
+ if (includeAgents && artifactList.includes('agents')) {
659
+ const prefix = typeCounts.agents > 1 ? `${slug}-` : '';
660
+ written.push(...await this._pullDir(repo, branch, 'agents', '.claude/agents', dryRun, prefix));
661
+ }
662
+ if (includeCommands && artifactList.includes('commands')) {
663
+ const prefix = typeCounts.commands > 1 ? `${slug}-` : '';
664
+ written.push(...await this._pullDir(repo, branch, 'commands', '.claude/commands', dryRun, prefix));
665
+ }
666
+ }
667
+
668
+ return written;
669
+ }
670
+
671
+ /** Pulls rules/<language>/*.md → .cursor/rules/[prefix]<language>-<file>.mdc */
672
+ async _pullRules(repo, branch, languages, dryRun, prefix = '') {
673
+ const written = [];
674
+ let langDirs;
675
+ try {
676
+ const entries = await this._fetchJson(`https://api.github.com/repos/${repo}/contents/rules`);
677
+ if (!Array.isArray(entries)) return [];
678
+ langDirs = entries.filter(e => e.type === 'dir').map(e => e.name);
679
+ } catch {
680
+ return [];
681
+ }
682
+
683
+ if (languages && languages.length > 0) {
684
+ langDirs = langDirs.filter(d => languages.includes(d));
685
+ }
686
+
687
+ for (const lang of langDirs) {
688
+ let files;
689
+ try {
690
+ const entries = await this._fetchJson(`https://api.github.com/repos/${repo}/contents/rules/${lang}`);
691
+ files = Array.isArray(entries) ? entries.filter(e => e.type === 'file' && e.name.endsWith('.md')) : [];
692
+ } catch {
693
+ continue;
694
+ }
695
+
696
+ for (const file of files) {
697
+ const rawUrl = `https://raw.githubusercontent.com/${repo}/${branch}/rules/${lang}/${file.name}`;
698
+ const destName = `${prefix}${lang}-${file.name.replace(/\.md$/, '.mdc')}`;
699
+ const destPath = `.cursor/rules/${destName}`;
700
+ const absPath = path.join(this.projectCwd, destPath);
701
+
702
+ if (dryRun) {
703
+ console.log(`[dry-run] Would write: ${destPath} (from ${rawUrl})`);
704
+ written.push(destPath);
705
+ continue;
706
+ }
707
+
708
+ let content;
709
+ try { content = await this._fetchText(rawUrl); } catch { continue; }
710
+
711
+ if (!content.trimStart().startsWith('---')) {
712
+ content = `---\ndescription: ${lang} ${file.name.replace('.md', '')} rules\nalwaysApply: false\n---\n\n${content}`;
713
+ }
714
+
715
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
716
+ fs.writeFileSync(absPath, content);
717
+ console.log(` ✅ ${destPath}`);
718
+ written.push(destPath);
719
+ }
720
+ }
721
+
722
+ return written;
723
+ }
724
+
725
+ /** Pulls <srcDir>/*.md → <destDir>/[prefix]<file>.md */
726
+ async _pullDir(repo, branch, srcDir, destDir, dryRun, prefix = '') {
727
+ const written = [];
728
+ let files;
729
+ try {
730
+ const entries = await this._fetchJson(`https://api.github.com/repos/${repo}/contents/${srcDir}`);
731
+ files = Array.isArray(entries) ? entries.filter(e => e.type === 'file' && e.name.endsWith('.md')) : [];
732
+ } catch {
733
+ return [];
734
+ }
735
+
736
+ for (const file of files) {
737
+ const rawUrl = `https://raw.githubusercontent.com/${repo}/${branch}/${srcDir}/${file.name}`;
738
+ const destPath = `${destDir}/${prefix}${file.name}`;
739
+ const absPath = path.join(this.projectCwd, destPath);
740
+
741
+ if (dryRun) {
742
+ console.log(`[dry-run] Would write: ${destPath} (from ${rawUrl})`);
743
+ written.push(destPath);
744
+ continue;
745
+ }
746
+
747
+ let content;
748
+ try { content = await this._fetchText(rawUrl); } catch { continue; }
749
+
750
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
751
+ fs.writeFileSync(absPath, content);
752
+ console.log(` ✅ ${destPath}`);
753
+ written.push(destPath);
754
+ }
755
+
756
+ return written;
757
+ }
758
+
759
+ /**
760
+ * Writes MCP server config files for the selected tools.
761
+ *
762
+ * @param {object} opts
763
+ * @param {string[]} opts.tools - tool names: 'claude'|'cursor'|'copilot'|'gemini'|'codex'|'roo-code'|'windsurf'|'goose'|'zed'|'continue'
764
+ * @param {boolean} [opts.dryRun]
765
+ * @returns {string[]} list of files written
766
+ */
767
+ async generateMcpConfigs({ tools = [], dryRun = false } = {}) {
768
+ const written = [];
769
+ for (const tool of tools) {
770
+ const config = this._renderMcpConfig(tool);
771
+ if (!config) continue;
772
+ const { filePath, mode } = config;
773
+ const absPath = config.global ? filePath : path.join(this.projectCwd, filePath);
774
+ if (dryRun) {
775
+ console.log(`[dry-run] Would write MCP config: ${filePath}`);
776
+ written.push(filePath);
777
+ continue;
778
+ }
779
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
780
+ if (mode === 'json-merge') {
781
+ this._mergeJsonMcpServer(absPath, filePath, config.mcpKey, config.mcpValue);
782
+ } else if (mode === 'toml-merge') {
783
+ this._mergeTomlMcpSection(absPath);
784
+ } else if (mode === 'yaml-merge') {
785
+ this._mergeGooseYaml(absPath);
786
+ } else {
787
+ fs.writeFileSync(absPath, config.content);
788
+ }
789
+ console.log(` ✅ ${filePath}`);
790
+ written.push(filePath);
791
+ }
792
+ return written;
793
+ }
794
+
795
+ /** Returns a descriptor for writing the MCP config for a given tool. */
796
+ _renderMcpConfig(tool) {
797
+ const BOOKLIB_ENTRY = { command: 'booklib-mcp', args: [] };
798
+ switch (tool) {
799
+ case 'claude':
800
+ return { filePath: '.claude/settings.json', mode: 'json-merge', mcpKey: ['mcpServers', 'booklib'], mcpValue: BOOKLIB_ENTRY };
801
+ case 'cursor':
802
+ return { filePath: '.cursor/mcp.json', mode: 'json-merge', mcpKey: ['mcpServers', 'booklib'], mcpValue: BOOKLIB_ENTRY };
803
+ case 'copilot':
804
+ return { filePath: '.vscode/mcp.json', mode: 'json-merge', mcpKey: ['servers', 'booklib'], mcpValue: BOOKLIB_ENTRY };
805
+ case 'gemini':
806
+ return { filePath: '.gemini/settings.json', mode: 'json-merge', mcpKey: ['mcpServers', 'booklib'], mcpValue: BOOKLIB_ENTRY };
807
+ case 'codex':
808
+ return { filePath: '.codex/config.toml', mode: 'toml-merge' };
809
+ case 'roo-code':
810
+ return { filePath: '.roo/mcp.json', mode: 'json-merge', mcpKey: ['mcpServers', 'booklib'], mcpValue: BOOKLIB_ENTRY };
811
+ case 'windsurf': {
812
+ const windsurfPath = path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
813
+ return { filePath: windsurfPath, mode: 'json-merge', mcpKey: ['mcpServers', 'booklib'], mcpValue: BOOKLIB_ENTRY, global: true };
814
+ }
815
+ case 'goose':
816
+ return { filePath: '.goose/config.yaml', mode: 'yaml-merge' };
817
+ case 'zed':
818
+ return { filePath: '.zed/settings.json', mode: 'json-merge', mcpKey: ['context_servers', 'booklib-mcp'], mcpValue: { command: { path: 'booklib-mcp', args: [] } } };
819
+ case 'continue':
820
+ return { filePath: '.continue/mcpServers/booklib.yaml', mode: 'overwrite', content: 'name: booklib\ncommand: booklib-mcp\nargs: []\n' };
821
+ default:
822
+ return null;
823
+ }
824
+ }
825
+
826
+ /** Reads an existing JSON config (if any), sets keyPath to value, writes back. */
827
+ _mergeJsonMcpServer(absPath, filePath, keyPath, value) {
828
+ let root = {};
829
+ if (fs.existsSync(absPath)) {
830
+ try {
831
+ root = JSON.parse(fs.readFileSync(absPath, 'utf8'));
832
+ } catch {
833
+ console.warn(` ⚠️ Could not parse ${filePath} — writing fresh`);
834
+ root = {};
835
+ }
836
+ }
837
+ let node = root;
838
+ for (let i = 0; i < keyPath.length - 1; i++) {
839
+ if (!node[keyPath[i]] || typeof node[keyPath[i]] !== 'object') node[keyPath[i]] = {};
840
+ node = node[keyPath[i]];
841
+ }
842
+ node[keyPath[keyPath.length - 1]] = value;
843
+ fs.writeFileSync(absPath, JSON.stringify(root, null, 2) + '\n');
844
+ }
845
+
846
+ /** Appends or replaces the [mcp_servers.booklib] section in a TOML file. */
847
+ _mergeTomlMcpSection(absPath) {
848
+ const BOOKLIB_BLOCK = '[mcp_servers.booklib]\ncommand = "booklib-mcp"\nargs = []\n';
849
+ // Match from the section header to the next section header or end of string
850
+ const SECTION_RE = /\[mcp_servers\.booklib\][\s\S]*?(?=\n\[|$)/;
851
+
852
+ let existing = '';
853
+ if (fs.existsSync(absPath)) {
854
+ existing = fs.readFileSync(absPath, 'utf8');
855
+ }
856
+
857
+ if (SECTION_RE.test(existing)) {
858
+ fs.writeFileSync(absPath, existing.replace(SECTION_RE, BOOKLIB_BLOCK.trimEnd()));
859
+ } else {
860
+ fs.writeFileSync(absPath, existing + (existing.endsWith('\n') ? '' : '\n') + '\n' + BOOKLIB_BLOCK);
861
+ }
862
+ }
863
+
864
+ /** Appends or merges the booklib entry in a Goose YAML config. */
865
+ _mergeGooseYaml(absPath) {
866
+ const entry = '\nmcp_servers:\n booklib:\n command: booklib-mcp\n args: []\n';
867
+ if (fs.existsSync(absPath)) {
868
+ const content = fs.readFileSync(absPath, 'utf8');
869
+ if (content.includes('booklib:')) return; // already exists
870
+ if (content.includes('mcp_servers:')) {
871
+ // Append under existing mcp_servers section
872
+ const updated = content.replace('mcp_servers:', 'mcp_servers:\n booklib:\n command: booklib-mcp\n args: []');
873
+ fs.writeFileSync(absPath, updated);
874
+ } else {
875
+ fs.appendFileSync(absPath, entry);
876
+ }
877
+ } else {
878
+ fs.writeFileSync(absPath, entry.trim() + '\n');
879
+ }
880
+ }
881
+
882
+ // ── HTTP helpers ───────────────────────────────────────────────────────────
883
+
884
+ _fetchJson(url) {
885
+ return new Promise((resolve, reject) => {
886
+ https.get(url, { headers: { 'User-Agent': 'booklib-init/1.0' } }, res => {
887
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
888
+ return resolve(this._fetchJson(res.headers.location));
889
+ }
890
+ let data = '';
891
+ res.on('data', c => (data += c));
892
+ res.on('end', () => {
893
+ try { resolve(JSON.parse(data)); } catch { reject(new Error('Invalid JSON')); }
894
+ });
895
+ }).on('error', reject);
896
+ });
897
+ }
898
+
899
+ _fetchText(url) {
900
+ return new Promise((resolve, reject) => {
901
+ https.get(url, { headers: { 'User-Agent': 'booklib-init/1.0' } }, res => {
902
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
903
+ return resolve(this._fetchText(res.headers.location));
904
+ }
905
+ if (res.statusCode !== 200) {
906
+ reject(new Error(`HTTP ${res.statusCode} for ${url}`));
907
+ res.resume();
908
+ return;
909
+ }
910
+ let data = '';
911
+ res.on('data', c => (data += c));
912
+ res.on('end', () => resolve(data));
913
+ }).on('error', reject);
914
+ });
915
+ }
916
+ }