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