@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,1389 @@
1
+ // lib/wizard/index.js
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { fileURLToPath } from 'node:url';
6
+ import pc from 'picocolors';
7
+ import { createWizardUI, sep, ensureBooklibGitignore } from './prompt.js';
8
+ import { detect as detectProject } from './project-detector.js';
9
+ import { SKILL_LIMIT } from './skill-recommender.js';
10
+ import { detectIntegrations } from './integration-detector.js';
11
+ import { SkillFetcher, countAllSlots, countInstalledSlots, listInstalledSkillNames, installSkill } from '../skill-fetcher.js';
12
+ import { BookLibIndexer } from '../engine/indexer.js';
13
+ import { BookLibSearcher } from '../engine/searcher.js';
14
+ import { AgentDetector } from '../agent-detector.js';
15
+ import { writeAgentLine } from '../project-initializer.js';
16
+ import { resolveBookLibPaths } from '../paths.js';
17
+ import { writeMCPConfig, MCP_CAPABLE } from '../mcp-config-writer.js';
18
+
19
+ /** Recursively count content files (.md, .yaml, .txt, etc.) in a directory. */
20
+ function countContentFiles(dirPath) {
21
+ let count = 0;
22
+ const walk = (dir) => {
23
+ try {
24
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
25
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
26
+ walk(path.join(dir, entry.name));
27
+ } else if (/\.(md|mdx|txt|yml|yaml|sh|json)$/i.test(entry.name)) {
28
+ count++;
29
+ }
30
+ }
31
+ } catch { /* permission error, skip */ }
32
+ };
33
+ walk(dirPath);
34
+ return count;
35
+ }
36
+
37
+ const AGENT_LABELS = {
38
+ claude: 'Claude Code', cursor: 'Cursor', copilot: 'Copilot',
39
+ gemini: 'Gemini CLI', codex: 'Codex', windsurf: 'Windsurf',
40
+ 'roo-code': 'Roo Code', openhands: 'OpenHands', junie: 'Junie',
41
+ goose: 'Goose', opencode: 'OpenCode', letta: 'Letta',
42
+ };
43
+
44
+ const AGENT_INSTRUCTION_FILES = {
45
+ claude: 'CLAUDE.md',
46
+ cursor: '.cursor/rules/booklib.mdc',
47
+ copilot: '.github/copilot-instructions.md',
48
+ gemini: '.gemini/context.md',
49
+ codex: 'AGENTS.md',
50
+ windsurf: '.windsurfrules',
51
+ 'roo-code': '.roo/rules/booklib.md',
52
+ openhands: '.openhands/instructions.md',
53
+ junie: '.junie/guidelines.md',
54
+ goose: '.goose/context.md',
55
+ opencode: '.opencode/instructions.md',
56
+ letta: '.letta/skills/booklib.md',
57
+ };
58
+ const ALL_AGENTS = Object.keys(AGENT_LABELS);
59
+
60
+ /**
61
+ * Merge BookLib hooks into the user's .claude/settings.json.
62
+ * Resolves ${BOOKLIB_ROOT} in hooks.json to the actual package path.
63
+ * @param {string} cwd - project directory (unused, hooks are global)
64
+ * @returns {string|null} path to written settings file, or null
65
+ */
66
+ function writeClaudeHooks(cwd) {
67
+ const packageRoot = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
68
+ const hooksJsonPath = path.join(packageRoot, 'hooks', 'hooks.json');
69
+
70
+ if (!fs.existsSync(hooksJsonPath)) return null;
71
+
72
+ const hooksConfig = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
73
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
74
+
75
+ let settings = {};
76
+ if (fs.existsSync(settingsPath)) {
77
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
78
+ }
79
+
80
+ if (!settings.hooks) settings.hooks = {};
81
+
82
+ // Resolve ${BOOKLIB_ROOT} to actual package path in all hook commands
83
+ const resolveCmd = (cmd) => cmd.replace(/\$\{BOOKLIB_ROOT\}/g, packageRoot);
84
+
85
+ for (const [event, entries] of Object.entries(hooksConfig)) {
86
+ if (!Array.isArray(settings.hooks[event])) {
87
+ settings.hooks[event] = [];
88
+ }
89
+
90
+ for (const entry of entries) {
91
+ const resolved = {
92
+ ...entry,
93
+ hooks: (entry.hooks ?? []).map(h => ({
94
+ ...h,
95
+ command: resolveCmd(h.command),
96
+ })),
97
+ };
98
+
99
+ // Skip if an identical hook command already exists for this event
100
+ const alreadyInstalled = settings.hooks[event].some(existing =>
101
+ existing.hooks?.some(eh =>
102
+ resolved.hooks.some(rh => rh.command === eh.command)
103
+ )
104
+ );
105
+ if (alreadyInstalled) continue;
106
+
107
+ settings.hooks[event].push(resolved);
108
+ }
109
+ }
110
+
111
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
112
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
113
+ return settingsPath;
114
+ }
115
+
116
+ export async function runWizard(cwd = process.cwd(), opts = {}) {
117
+ const markerPath = path.join(cwd, '.booklib', 'initialized');
118
+
119
+ if (opts.reset) {
120
+ if (fs.existsSync(markerPath)) fs.unlinkSync(markerPath);
121
+ // Offer clean slate — clear local index, sources, cache
122
+ const booklibDir = path.join(cwd, '.booklib');
123
+ if (fs.existsSync(booklibDir)) {
124
+ const ui = createWizardUI();
125
+ const clean = await ui.confirm('Clear local BookLib data? (index, sources, cache)', false);
126
+ if (clean) {
127
+ fs.rmSync(booklibDir, { recursive: true, force: true });
128
+ }
129
+ }
130
+ return runSetup(cwd);
131
+ }
132
+
133
+ if (fs.existsSync(markerPath)) {
134
+ console.log('\n Already initialized. Running relevance check...');
135
+ console.log(' (to re-run full setup: rm -rf .booklib && booklib init)\n');
136
+ return runRelevanceAudit(cwd);
137
+ }
138
+ return runSetup(cwd);
139
+ }
140
+
141
+ // ── Setup flow ───────────────────────────────────────────────────────────────
142
+
143
+ async function runSetup(cwd) {
144
+ const ui = createWizardUI();
145
+
146
+ // Banner
147
+ console.log('');
148
+ console.log(' \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2726');
149
+ console.log(' \u2502 \u2500\u2500\u2500 \u2502 \u2500\u2500\u2500 \u2502');
150
+ console.log(' \u2502 \u2500\u2500 \u2502 \u2500\u2500 \u2502 BookLib');
151
+ console.log(' \u2502 \u2500\u2500\u2500 \u2502 \u2500\u2500\u2500 \u2502');
152
+ console.log(' \u2502 \u2500\u2500 \u2502 \u2500\u2500 \u2502 AI-agent skills from');
153
+ console.log(' \u2502 \u2500\u2500\u2500 \u2502 \u2500\u2500\u2500 \u2502 expert knowledge');
154
+ console.log(' \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2518');
155
+ console.log('');
156
+
157
+ ui.intro('Setup Wizard');
158
+
159
+ // Step 1: Project detection
160
+ const project = await stepProjectDetection(ui, cwd);
161
+
162
+ // Step 2: Profile selection
163
+ const profile = await stepProfileSelection(ui);
164
+
165
+ // Step 3: Processing mode
166
+ const { mode: reasoningMode, ollamaModel } = await stepProcessingMode(ui, cwd);
167
+
168
+ // Save profile + reasoning to config (single write)
169
+ try {
170
+ const { configPath } = resolveBookLibPaths(cwd);
171
+ let savedConfig = {};
172
+ try { savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { /* no config */ }
173
+ savedConfig.profile = profile;
174
+ savedConfig.reasoning = reasoningMode;
175
+ if (reasoningMode === 'local') savedConfig.ollamaModel = ollamaModel;
176
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
177
+ fs.writeFileSync(configPath, JSON.stringify(savedConfig, null, 2));
178
+ } catch (err) { ui.log.warn(`Could not save config: ${err.message}`); }
179
+
180
+ // Step 4: Count installed skills (warning deferred to skill recommendation step)
181
+ const slotsUsed = countInstalledSlots();
182
+ const installedNames = listInstalledSkillNames();
183
+
184
+ // Step 5: Tool detection
185
+ const selectedAgents = await stepToolSelection(ui, cwd);
186
+
187
+ // Step 6: Index build with spinner
188
+ const resolvedIndexPath = await stepIndexBuild(ui);
189
+
190
+ // Step 7: Scan for knowledge gaps
191
+ const { gaps, connectSuggestions } = await stepGapDetection(ui, cwd);
192
+
193
+ // Step 8: Connect project docs
194
+ const indexedSources = await stepConnectDocs(ui, cwd, connectSuggestions);
195
+
196
+ // Step 8b: Demo decision checker on one source file
197
+ await stepDecisionDemo(ui, cwd, indexedSources);
198
+
199
+ // Step 9: (removed — stepResolveGaps handles Context7 + GitHub automatically)
200
+
201
+ // Step 10: Show project analysis — which APIs are affected by gaps
202
+ let analysisResult = null;
203
+ if (gaps?.postTraining?.length > 0) {
204
+ analysisResult = await stepShowAnalysis(ui, cwd);
205
+ }
206
+
207
+ // Step 11: Auto-resolve knowledge gaps via Context7 / GitHub / manual
208
+ let gapResults = { resolved: 0, unresolved: 0 };
209
+ if (gaps?.postTraining?.length > 0) {
210
+ gapResults = await stepResolveGaps(ui, cwd, gaps.postTraining);
211
+ }
212
+
213
+ // Step 11b: Build runtime context map from knowledge graph + gaps
214
+ await stepBuildContextMap(ui, cwd, gaps);
215
+
216
+ // Step 12: Recommend + install + cleanup
217
+ const selectedSkills = await stepRecommendAndInstall(ui, project, slotsUsed, installedNames, resolvedIndexPath);
218
+
219
+ // Step 13: Write config files
220
+ const skillsForConfig = selectedSkills.length > 0 ? selectedSkills : installedNames.slice(0, 10);
221
+ const stack = project.languages.join(', ');
222
+ await stepWriteConfigs(ui, cwd, selectedAgents, skillsForConfig, profile, stack);
223
+
224
+ // Step 14: Summary
225
+ ui.outro('Setup complete');
226
+
227
+ const finalSlots = countInstalledSlots();
228
+ const totalDocs = indexedSources.reduce((sum, s) => sum + (s.files || 0), 0);
229
+ const resolvedCount = gapResults.details?.filter(d => d.resolved).length ?? 0;
230
+ const unresolvedCount = gapResults.details?.filter(d => !d.resolved).length ?? 0;
231
+
232
+ console.log('');
233
+
234
+ const postCount = gaps?.postTraining?.length ?? 0;
235
+ const apiCount = analysisResult?.totalApis ?? 0;
236
+ const fileCount = analysisResult?.totalFiles ?? 0;
237
+
238
+ console.log(' What was found:');
239
+ if (postCount > 0) {
240
+ console.log(` ${postCount} packages released after your AI's training data`);
241
+ console.log(` ${fileCount} files affected, ${apiCount} APIs your AI may get wrong`);
242
+ }
243
+ if (indexedSources.length > 0) {
244
+ console.log(' Team decisions indexed from your docs and specs');
245
+ }
246
+ if (postCount === 0 && indexedSources.length === 0) {
247
+ console.log(' No knowledge gaps detected. Connect team docs for more value.');
248
+ }
249
+ console.log('');
250
+
251
+ if (selectedAgents.length > 0) {
252
+ console.log(' Your tools:');
253
+ console.log('');
254
+ for (const agent of selectedAgents) {
255
+ const label = (AGENT_LABELS[agent] ?? agent).padEnd(17);
256
+ if (agent === 'claude') {
257
+ console.log(` ${label}automatic — corrects before edits, catches`);
258
+ console.log(' contradictions after');
259
+ } else if (['cursor', 'copilot', 'gemini', 'windsurf', 'roo-code', 'goose', 'opencode'].includes(agent)) {
260
+ console.log(` ${label}on-demand — AI calls BookLib when it needs help`);
261
+ } else {
262
+ console.log(` ${label}guidelines — project standards in config file`);
263
+ }
264
+ }
265
+ console.log('');
266
+ }
267
+
268
+ console.log(' BookLib stays silent when everything is fine.');
269
+ console.log('');
270
+ console.log(` ${sep()}`);
271
+ console.log('');
272
+ console.log(' Try it now: booklib analyze');
273
+ console.log('');
274
+ console.log(' When you need it:');
275
+ console.log(' booklib analyze files and APIs your AI may get wrong');
276
+ console.log(' booklib gaps packages newer than your AI knows');
277
+ console.log(' booklib connect <source> add Notion, GitHub, or local docs');
278
+ console.log(' booklib search "query" search indexed knowledge');
279
+ console.log(' booklib doctor health check');
280
+ console.log('');
281
+
282
+ // Mark initialized
283
+ const markerPath = path.join(cwd, '.booklib', 'initialized');
284
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
285
+ fs.writeFileSync(markerPath, new Date().toISOString());
286
+ }
287
+
288
+ async function stepProjectDetection(ui, cwd) {
289
+ const project = detectProject(cwd);
290
+
291
+ if (project.languages.length > 0) {
292
+ const langs = project.languages.join(', ');
293
+ const fw = project.frameworks.length ? ` (${project.frameworks.join(', ')})` : '';
294
+ const ok = await ui.confirm(`Detected: ${langs}${fw}. Correct?`, true);
295
+ if (!ok) {
296
+ const answer = await ui.text('Describe your stack:', 'e.g. React + Node.js, Kotlin Android');
297
+ return { languages: [answer], frameworks: [], signals: [] };
298
+ }
299
+ } else {
300
+ const answer = await ui.text('What are you building?', 'e.g. React + Node.js, Kotlin Android');
301
+ return { languages: [answer], frameworks: [], signals: [] };
302
+ }
303
+
304
+ return project;
305
+ }
306
+
307
+ async function stepProfileSelection(ui) {
308
+ const profile = await ui.select('What kind of work is this project for?', [
309
+ { value: 'software-development', label: 'Software development', hint: 'recommended' },
310
+ { value: 'writing-content', label: 'Writing & content' },
311
+ { value: 'research-analysis', label: 'Research & analysis' },
312
+ { value: 'design', label: 'Design' },
313
+ { value: 'general', label: 'General / other' },
314
+ ]);
315
+ return profile;
316
+ }
317
+
318
+ async function stepProcessingMode(ui, cwd) {
319
+ const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
320
+ const hasOpenAIKey = !!process.env.OPENAI_API_KEY;
321
+ const hasApiKey = hasAnthropicKey || hasOpenAIKey;
322
+ const keyProvider = hasAnthropicKey ? 'Anthropic' : hasOpenAIKey ? 'OpenAI' : null;
323
+
324
+ // Check if Ollama is available
325
+ let ollamaAvailable = false;
326
+ let ollamaModels = [];
327
+ try {
328
+ const res = await fetch('http://localhost:11434/api/tags', { signal: AbortSignal.timeout(2000) });
329
+ if (res.ok) {
330
+ const data = await res.json();
331
+ ollamaModels = (data.models ?? []).map(m => m.name);
332
+ ollamaAvailable = true;
333
+ }
334
+ } catch { /* Ollama not running */ }
335
+
336
+ const options = [];
337
+
338
+ if (hasApiKey) {
339
+ options.push({ value: 'api', label: 'Cloud AI (recommended)', hint: `Uses your ${keyProvider} API key — best quality, ~1-2s per query` });
340
+ } else {
341
+ options.push({ value: 'api', label: 'Cloud AI (recommended)', hint: 'Uses API key (Anthropic or OpenAI) — best quality, ~1-2s per query' });
342
+ }
343
+
344
+ if (ollamaAvailable && ollamaModels.length > 0) {
345
+ options.push({ value: 'local', label: 'Local AI (Ollama)', hint: `${ollamaModels.length} model(s) available — free, private, ~2s per query` });
346
+ } else {
347
+ options.push({ value: 'local', label: 'Local AI (Ollama)', hint: ollamaAvailable ? 'Ollama running but no models pulled yet' : 'Requires Ollama — free, private, runs on your machine' });
348
+ }
349
+
350
+ options.push({ value: 'fast', label: 'Fast (no AI)', hint: 'Instant results, basic filtering — no AI reasoning' });
351
+
352
+ let mode = await ui.select('How should BookLib process search results?', options);
353
+ let ollamaModel = 'phi3';
354
+
355
+ // === Local mode setup ===
356
+ if (mode === 'local') {
357
+ if (!ollamaAvailable) {
358
+ // Help install Ollama
359
+ const platform = process.platform;
360
+ const installCmd = platform === 'darwin' ? 'brew install ollama' : platform === 'linux' ? 'curl -fsSL https://ollama.com/install.sh | sh' : 'See https://ollama.com/download';
361
+
362
+ ui.log.info(
363
+ 'Ollama is not running. To set up local AI:\n\n' +
364
+ ` 1. Install: ${installCmd}\n` +
365
+ ' 2. Start: ollama serve\n' +
366
+ ' 3. Pull a model: ollama pull phi3\n\n' +
367
+ 'Recommended models:\n' +
368
+ ' phi3 — 3.8B params, ~2GB, fast and capable\n' +
369
+ ' qwen2.5:1.5b — 1.5B params, ~1GB, very fast\n' +
370
+ ' gemma2:2b — 2B params, ~1.5GB, good quality'
371
+ );
372
+
373
+ const proceed = await ui.confirm('Continue with fast mode for now?', true);
374
+ if (proceed) {
375
+ mode = 'fast';
376
+ }
377
+ } else if (ollamaModels.length === 0) {
378
+ // Ollama running but no models
379
+ ui.log.info(
380
+ 'Ollama is running but has no models. Pull one:\n\n' +
381
+ ' ollama pull phi3 — 3.8B, ~2GB, recommended\n' +
382
+ ' ollama pull qwen2.5:1.5b — 1.5B, ~1GB, fastest\n' +
383
+ ' ollama pull gemma2:2b — 2B, ~1.5GB, good quality'
384
+ );
385
+ mode = 'fast';
386
+ } else {
387
+ // Ollama running with models — let user pick
388
+ if (ollamaModels.length === 1) {
389
+ ollamaModel = ollamaModels[0];
390
+ ui.log.success(`Using Ollama model: ${ollamaModel}`);
391
+ } else {
392
+ const modelOptions = ollamaModels.map(m => ({ value: m, label: m }));
393
+ ollamaModel = await ui.select('Which Ollama model should BookLib use?', modelOptions);
394
+ ui.log.success(`Selected: ${ollamaModel}`);
395
+ }
396
+ }
397
+ }
398
+
399
+ // === Cloud AI setup ===
400
+ if (mode === 'api' && !hasApiKey) {
401
+ const key = await ui.text(
402
+ 'Paste your API key (Anthropic or OpenAI):',
403
+ 'sk-...'
404
+ );
405
+
406
+ if (key && key.length > 10) {
407
+ const envVar = key.startsWith('sk-ant-') ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY';
408
+ const envPath = path.join(cwd, '.env');
409
+ const envLine = `${envVar}=${key}\n`;
410
+ if (fs.existsSync(envPath)) {
411
+ const existing = fs.readFileSync(envPath, 'utf8');
412
+ if (!existing.includes(envVar)) {
413
+ fs.appendFileSync(envPath, envLine);
414
+ }
415
+ } else {
416
+ fs.writeFileSync(envPath, envLine);
417
+ }
418
+ process.env[envVar] = key;
419
+
420
+ const gitignorePath = path.join(cwd, '.gitignore');
421
+ if (fs.existsSync(gitignorePath)) {
422
+ const gitignore = fs.readFileSync(gitignorePath, 'utf8');
423
+ if (!gitignore.includes('.env')) {
424
+ fs.appendFileSync(gitignorePath, '\n.env\n');
425
+ }
426
+ } else {
427
+ fs.writeFileSync(gitignorePath, '.env\n');
428
+ }
429
+
430
+ ui.log.success(`${envVar} saved to .env (added to .gitignore)`);
431
+ } else {
432
+ ui.log.info(
433
+ 'No key provided. BookLib will use fast mode for now.\n' +
434
+ 'To enable later, add your key to .env:\n' +
435
+ ' echo "ANTHROPIC_API_KEY=sk-ant-..." >> .env'
436
+ );
437
+ mode = 'fast';
438
+ }
439
+ } else if (mode === 'api' && hasApiKey) {
440
+ ui.log.success(`${keyProvider} API key detected. Cloud AI reasoning enabled.`);
441
+ }
442
+
443
+ return { mode, ollamaModel };
444
+ }
445
+
446
+ async function stepToolSelection(ui, cwd) {
447
+ const detector = new AgentDetector({ cwd });
448
+ const detected = detector.detect();
449
+ const detectedSet = new Set(detected);
450
+
451
+ const options = ALL_AGENTS.map(a => ({
452
+ value: a,
453
+ label: AGENT_LABELS[a],
454
+ hint: detectedSet.has(a) ? 'detected' : undefined,
455
+ }));
456
+
457
+ const selected = await ui.multiselect('Which AI tools do you use?', options, {
458
+ initialValues: detected,
459
+ });
460
+
461
+ // Save to config
462
+ try {
463
+ const { configPath } = resolveBookLibPaths(cwd);
464
+ let savedConfig = {};
465
+ try { savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { /* no config */ }
466
+ savedConfig.tools = selected;
467
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
468
+ fs.writeFileSync(configPath, JSON.stringify(savedConfig, null, 2));
469
+ } catch { /* best-effort */ }
470
+
471
+ const integrations = detectIntegrations({ cwd });
472
+ if (integrations.superpowers) {
473
+ ui.log.info('Detected: obra/superpowers plugin (skills auto-synced)');
474
+ }
475
+
476
+ return selected;
477
+ }
478
+
479
+ /**
480
+ * Cheaply checks whether the BM25 index contains at least one skill chunk.
481
+ * A skill chunk is any doc whose metadata has a truthy `name` field
482
+ * (as opposed to knowledge nodes which have `title` or connected sources
483
+ * which have `sourceName`).
484
+ *
485
+ * @param {string} bm25FilePath - Path to bm25.json
486
+ * @returns {boolean}
487
+ */
488
+ function bm25ContainsSkills(bm25FilePath) {
489
+ try {
490
+ const raw = fs.readFileSync(bm25FilePath, 'utf8');
491
+ const { docs } = JSON.parse(raw);
492
+ if (!Array.isArray(docs)) return false;
493
+ return docs.some(doc => doc.metadata?.name);
494
+ } catch {
495
+ return false;
496
+ }
497
+ }
498
+
499
+ async function stepIndexBuild(ui) {
500
+ const s = ui.spinner();
501
+ const { indexPath } = resolveBookLibPaths();
502
+ const indexDir = path.dirname(indexPath);
503
+ const indexFile = path.join(indexPath, 'index.json');
504
+ const bm25File = path.join(indexDir, 'bm25.json');
505
+
506
+ const vectraExists = fs.existsSync(indexFile);
507
+ const bm25Exists = fs.existsSync(bm25File) && fs.statSync(bm25File).size > 100;
508
+
509
+ // Fast path: both indexes exist and contain skill chunks
510
+ if (vectraExists && bm25Exists && bm25ContainsSkills(bm25File)) {
511
+ s.start('Found your index, all good');
512
+ s.stop('Index ready (existing)');
513
+ return indexPath;
514
+ }
515
+
516
+ // Backup restore: vectra exists but BM25 is missing or has no skills
517
+ if (vectraExists) {
518
+ const backupBm25 = path.join(indexDir, 'index-backup', 'bm25.json');
519
+ if (fs.existsSync(backupBm25) && fs.statSync(backupBm25).size > 100 && bm25ContainsSkills(backupBm25)) {
520
+ fs.copyFileSync(backupBm25, bm25File);
521
+ s.start('Found your index, patching it up');
522
+ s.stop('Index ready');
523
+ return indexPath;
524
+ }
525
+ }
526
+
527
+ // Try pre-built index package first — instant setup, no model download
528
+ try {
529
+ const { installIndex, hasPrebuiltIndex } = await import('@booklib/index');
530
+ if (hasPrebuiltIndex()) {
531
+ s.start('Grabbing pre-built index (~93 MB, one-time thing)...');
532
+ await installIndex(indexDir);
533
+ s.stop('Index ready (pre-built)');
534
+ return indexPath;
535
+ }
536
+ } catch {
537
+ // @booklib/index not installed — fall through to build
538
+ }
539
+
540
+ // Fall back to building index locally
541
+ const indexer = new BookLibIndexer();
542
+
543
+ if (!fs.existsSync(indexFile)) {
544
+ s.start('Downloading the brain (~25 MB, just this once)...');
545
+ try {
546
+ await indexer.loadModel({ quiet: true });
547
+ } catch (err) {
548
+ s.stop(`Model download failed: ${err.message}`);
549
+ const continueWithFast = await ui.confirm(
550
+ 'Continue without semantic search? (BM25 keyword search still works)',
551
+ true
552
+ );
553
+ if (!continueWithFast) {
554
+ ui.log.info('Re-run "booklib init" when network is available.');
555
+ return indexPath;
556
+ }
557
+ ui.log.info('Continuing with keyword search only.');
558
+ return indexPath;
559
+ }
560
+ s.message('Crunching the knowledge base...');
561
+ } else {
562
+ s.start('Crunching the knowledge base...');
563
+ }
564
+
565
+ try {
566
+ const { skillsPath } = resolveBookLibPaths();
567
+ await indexer.indexDirectory(skillsPath, false, {
568
+ quiet: true,
569
+ onFileProgress({ current, total, file }) {
570
+ const name = file.split('/')[0] ?? file;
571
+ const pct = Math.round((current / total) * 100);
572
+ const barWidth = 20;
573
+ const filled = Math.round((current / total) * barWidth);
574
+ const bar = '\x1b[36m' + '\u2588'.repeat(filled) + '\x1b[90m' + '\u2591'.repeat(barWidth - filled) + '\x1b[0m';
575
+ s.message(`Building index ${bar} ${pct}% [${current}/${total}] ${name}`);
576
+ },
577
+ });
578
+ s.stop('Index ready');
579
+ } catch (err) {
580
+ s.stop(`Index build failed: ${err.message}`);
581
+ }
582
+
583
+ return indexPath;
584
+ }
585
+
586
+ async function stepRecommendAndInstall(ui, project, slotsUsed, installedNames, indexPath) {
587
+ const s = ui.spinner();
588
+ s.start('Picking the best skills for your stack...');
589
+
590
+ const searcher = new BookLibSearcher(indexPath);
591
+ // Include frameworks in query for better relevance
592
+ const queryParts = [...project.languages, ...project.frameworks].filter(Boolean);
593
+ const queryText = queryParts.join(' ') + ' best practices';
594
+
595
+ // Search to find which skills are most relevant → these get pre-selected
596
+ const recommendedNames = new Set();
597
+ const scoreMap = new Map();
598
+ try {
599
+ const results = await searcher.search(queryText, 30);
600
+ for (const r of results) {
601
+ const name = r.metadata?.name;
602
+ if (!name) continue;
603
+ if (!scoreMap.has(name) || r.score > scoreMap.get(name).score) {
604
+ const snippet = (r.text ?? '').replace(/\n/g, ' ').slice(0, 60).trim();
605
+ scoreMap.set(name, { score: r.score, displayScore: r.displayScore, snippet });
606
+ }
607
+ }
608
+ // Top scoring skills are recommended
609
+ const sorted = [...scoreMap.entries()].sort((a, b) => b[1].score - a[1].score);
610
+ for (const [name] of sorted.slice(0, 8)) {
611
+ recommendedNames.add(name);
612
+ }
613
+ } catch (err) {
614
+ // Search failed — show all skills without recommendations
615
+ ui.log.warn(`Skill matching unavailable: ${err.message}`);
616
+ }
617
+
618
+ // Always merge language-based skills — search may miss obvious matches
619
+ const LANG_SKILLS = {
620
+ typescript: ['effective-typescript', 'clean-code-typescript', 'react-typescript-cheatsheet'],
621
+ javascript: ['airbnb-javascript', 'js-testing-best-practices', 'node-error-handling'],
622
+ python: ['effective-python', 'python-google-style', 'using-asyncio-python'],
623
+ java: ['effective-java', 'spring-boot-in-action', 'design-patterns'],
624
+ kotlin: ['effective-kotlin', 'kotlin-in-action'],
625
+ rust: ['programming-with-rust', 'rust-in-action'],
626
+ ruby: ['clean-code-reviewer'],
627
+ go: ['system-design-interview', 'api-design-rest'],
628
+ php: ['clean-code-reviewer', 'api-design-rest'],
629
+ };
630
+ for (const lang of project.languages) {
631
+ for (const skill of LANG_SKILLS[lang] ?? []) {
632
+ recommendedNames.add(skill);
633
+ }
634
+ }
635
+
636
+ // Collect ALL available skills: bundled (from package root) + community registry
637
+ // Use fileURLToPath for proper path resolution across platforms
638
+ const packageRoot = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
639
+ const bundledSkillsDir = path.join(packageRoot, 'skills');
640
+ const skillMeta = new Map(); // name → { source, description }
641
+
642
+ // Bundled skills (in package's skills/ directory)
643
+ try {
644
+ const bundled = fs.readdirSync(bundledSkillsDir)
645
+ .filter(d => {
646
+ try { return fs.statSync(path.join(bundledSkillsDir, d)).isDirectory(); } catch { return false; }
647
+ })
648
+ .filter(d => fs.existsSync(path.join(bundledSkillsDir, d, 'SKILL.md')))
649
+ .filter(d => d !== 'skill-router');
650
+ for (const name of bundled) {
651
+ skillMeta.set(name, { source: 'bundled', description: '' });
652
+ }
653
+ } catch { /* skills dir missing */ }
654
+
655
+ // Community registry skills
656
+ try {
657
+ const registryPath = path.join(packageRoot, 'community', 'registry.json');
658
+ if (fs.existsSync(registryPath)) {
659
+ const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
660
+ for (const skill of registry.skills ?? []) {
661
+ if (!skillMeta.has(skill.name)) {
662
+ const langs = skill.languages?.join(', ') ?? '';
663
+ skillMeta.set(skill.name, {
664
+ source: 'community',
665
+ description: skill.description?.slice(0, 60) ?? '',
666
+ languages: langs,
667
+ });
668
+ }
669
+ }
670
+ }
671
+ } catch { /* registry missing */ }
672
+
673
+ s.stop(`${skillMeta.size} skills available (${recommendedNames.size} recommended for your stack)`);
674
+
675
+ if (skillMeta.size === 0) return [];
676
+
677
+ // Overload warning — shown here (during skill selection) rather than before indexing
678
+ if (slotsUsed > SKILL_LIMIT) {
679
+ ui.log.warn(
680
+ `${slotsUsed} skills installed (limit: ${SKILL_LIMIT}).\n` +
681
+ 'Agent context is overloaded \u2014 most skills get truncated.\n' +
682
+ 'Select only the skills you need below, then clean up the rest.'
683
+ );
684
+ }
685
+
686
+ const installedSet = new Set(installedNames.map(n => n.toLowerCase()));
687
+ const allNames = [...skillMeta.keys()];
688
+
689
+ // Sort: recommended first, then installed, then bundled, then community — alphabetical within each
690
+ const sortedSkills = allNames.sort((a, b) => {
691
+ const aRec = recommendedNames.has(a) ? 0 : 1;
692
+ const bRec = recommendedNames.has(b) ? 0 : 1;
693
+ if (aRec !== bRec) return aRec - bRec;
694
+ const aInst = installedSet.has(a.toLowerCase()) ? 0 : 1;
695
+ const bInst = installedSet.has(b.toLowerCase()) ? 0 : 1;
696
+ if (aInst !== bInst) return aInst - bInst;
697
+ const aBundled = skillMeta.get(a)?.source === 'bundled' ? 0 : 1;
698
+ const bBundled = skillMeta.get(b)?.source === 'bundled' ? 0 : 1;
699
+ if (aBundled !== bBundled) return aBundled - bBundled;
700
+ return a.localeCompare(b);
701
+ });
702
+
703
+ const options = sortedSkills.map(name => {
704
+ const meta = scoreMap.get(name);
705
+ const info = skillMeta.get(name);
706
+ const isRec = recommendedNames.has(name);
707
+ const isInst = installedSet.has(name.toLowerCase());
708
+ // Simple labels: "recommended" or "installed" or description
709
+ let hint = isRec ? 'recommended' : (isInst ? 'installed' : '');
710
+ if (!hint && info?.description) hint = info.description;
711
+ if (!hint && meta?.snippet) hint = meta.snippet;
712
+ const score = meta ? ` [${meta.displayScore ?? Math.round(meta.score * 100)}%]` : '';
713
+ return { value: name, label: `${name}${score}`, hint: hint || undefined };
714
+ });
715
+
716
+ // Pre-select recommended only — don't auto-select all installed
717
+ // Installed-but-not-recommended skills stay in the list but unchecked
718
+ const initialValues = sortedSkills.filter(n => recommendedNames.has(n));
719
+
720
+ const selected = await ui.multiselect(
721
+ `Skills for your project (${initialValues.length} pre-selected for your stack, ${skillMeta.size} available):`,
722
+ options,
723
+ { initialValues },
724
+ );
725
+
726
+ if (selected.length === 0) return installedNames;
727
+
728
+ // Install selected skills
729
+ const toInstall = selected.filter(n => !installedSet.has(n.toLowerCase()));
730
+ const installed = [];
731
+
732
+ for (const name of selected) {
733
+ if (installedSet.has(name.toLowerCase())) {
734
+ ui.log.info(`${name} (already installed)`);
735
+ installed.push(name);
736
+ } else {
737
+ const result = installSkill(name);
738
+ if (result === 'installed') {
739
+ ui.log.success(`${name}`);
740
+ installed.push(name);
741
+ } else if (result === 'already-installed') {
742
+ ui.log.info(`${name} (already installed)`);
743
+ installed.push(name);
744
+ } else {
745
+ ui.log.warn(`${name}: not found in any catalog`);
746
+ }
747
+ }
748
+ }
749
+
750
+ // Cleanup offer
751
+ if (slotsUsed > SKILL_LIMIT && installed.length > 0) {
752
+ const toRemove = installedNames.filter(n => !selected.includes(n));
753
+ if (toRemove.length === 0) {
754
+ // Nothing to clean up — skip the question
755
+ } else {
756
+ const cleanup = await ui.select(`You have ${slotsUsed} skills but only need ~${installed.length}.`, [
757
+ { value: 'clean', label: `Clean up \u2014 keep only recommended (remove ${toRemove.length})` },
758
+ { value: 'keep', label: 'Keep all + add recommended' },
759
+ { value: 'skip', label: 'Skip \u2014 I\'ll handle it manually' },
760
+ ]);
761
+
762
+ if (cleanup === 'clean') {
763
+ const fetcher = new SkillFetcher();
764
+ let removed = 0;
765
+ for (const name of toRemove) {
766
+ fetcher.desyncFromClaudeSkills({ name });
767
+ removed++;
768
+ }
769
+ ui.log.success(`Removed ${removed} skills. Kept ${installed.length}.`);
770
+ }
771
+ } // end if toRemove.length > 0
772
+ }
773
+
774
+ return installed;
775
+ }
776
+
777
+ async function stepWriteConfigs(ui, cwd, selectedAgents, skillNames, profile, stack) {
778
+ if (selectedAgents.length === 0) return;
779
+
780
+ // Wire up: MCP registration + one-line agent config + hooks
781
+ {
782
+ const s = ui.spinner();
783
+ s.start('Wiring everything up...');
784
+
785
+ const wired = [];
786
+
787
+ for (const tool of selectedAgents) {
788
+ // 1. MCP registration
789
+ if (MCP_CAPABLE.has(tool)) {
790
+ try {
791
+ const mcpPath = writeMCPConfig(tool, cwd);
792
+ if (mcpPath) wired.push(`${tool}: MCP registered`);
793
+ } catch { /* best-effort */ }
794
+ }
795
+
796
+ // 2. One line in agent instruction file
797
+ const instrFile = AGENT_INSTRUCTION_FILES[tool];
798
+ if (instrFile) {
799
+ try {
800
+ const absPath = path.join(cwd, instrFile);
801
+ const isNew = !fs.existsSync(absPath);
802
+ writeAgentLine(absPath, { skeleton: isNew });
803
+ wired.push(instrFile);
804
+ } catch { /* best-effort */ }
805
+ }
806
+ }
807
+
808
+ // 3. Install booklib-mcp-guide skill into project
809
+ try {
810
+ const packageRoot = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
811
+ const srcSkill = path.join(packageRoot, 'skills', 'booklib-mcp-guide', 'SKILL.md');
812
+ if (fs.existsSync(srcSkill)) {
813
+ const destDir = path.join(cwd, 'skills', 'booklib-mcp-guide');
814
+ if (!fs.existsSync(destDir)) {
815
+ fs.mkdirSync(destDir, { recursive: true });
816
+ fs.copyFileSync(srcSkill, path.join(destDir, 'SKILL.md'));
817
+ wired.push('skills/booklib-mcp-guide (MCP tool guide)');
818
+ }
819
+ }
820
+ } catch { /* best-effort */ }
821
+
822
+ // 4. Hooks for Claude Code
823
+ if (selectedAgents.includes('claude')) {
824
+ try {
825
+ const hookResult = writeClaudeHooks(cwd);
826
+ if (hookResult) wired.push(`${path.relative(cwd, hookResult)} (hooks)`);
827
+ } catch { /* best-effort */ }
828
+ }
829
+
830
+ // 4. Gitignore
831
+ const gitignoreAdded = ensureBooklibGitignore(cwd);
832
+ if (gitignoreAdded.length > 0) wired.push(`.gitignore (${gitignoreAdded.length} entries)`);
833
+
834
+ if (wired.length > 0) {
835
+ s.stop('Everything wired up');
836
+ for (const w of wired) ui.log.success(w);
837
+ } else {
838
+ s.stop('Already configured');
839
+ }
840
+ }
841
+ }
842
+
843
+ // ── New wizard steps: gap detection, doc connection, GitHub releases ─────────
844
+
845
+ async function stepGapDetection(ui, cwd) {
846
+ const s = ui.spinner();
847
+ s.start('Checking what your AI might not know yet...');
848
+
849
+ try {
850
+ const { GapDetector } = await import('../engine/gap-detector.js');
851
+ const detector = new GapDetector();
852
+ const gaps = await detector.detect(cwd);
853
+
854
+ if (gaps.postTraining.length === 0 && gaps.uncapturedDocs.length === 0) {
855
+ s.stop('No knowledge gaps detected');
856
+ return { gaps, connectSuggestions: [] };
857
+ }
858
+
859
+ s.stop(`Found ${gaps.postTraining.length} post-training dep(s), ${gaps.uncapturedDocs.length} uncaptured doc(s)`);
860
+
861
+ if (gaps.postTraining.length > 0) {
862
+ ui.log.warn('Post-training dependencies (model may have outdated knowledge):');
863
+ for (const dep of gaps.postTraining.slice(0, 5)) {
864
+ const date = dep.publishDate.toISOString().split('T')[0];
865
+ ui.log.info(` ${dep.name}@${dep.version} (${dep.ecosystem}, published ${date})`);
866
+ }
867
+ if (gaps.postTraining.length > 5) {
868
+ ui.log.info(` ... and ${gaps.postTraining.length - 5} more`);
869
+ }
870
+ }
871
+
872
+ return { gaps, connectSuggestions: gaps.uncapturedDocs };
873
+ } catch (err) {
874
+ s.stop(`Gap scan skipped: ${err.message}`);
875
+ return { gaps: null, connectSuggestions: [] };
876
+ }
877
+ }
878
+
879
+ async function stepConnectDocs(ui, cwd, uncapturedDocs) {
880
+ const docSources = [...uncapturedDocs];
881
+
882
+ // Check for PKM vaults and SDD spec directories
883
+ const pkmDirs = ['.obsidian', '.logseq', '.foam'];
884
+ const sddDirs = ['.specify', '.planning', '.gsd', '.kiro'];
885
+
886
+ for (const dir of [...pkmDirs, ...sddDirs]) {
887
+ const full = path.join(cwd, dir);
888
+ if (fs.existsSync(full) && fs.statSync(full).isDirectory()) {
889
+ // For PKM vaults, scan the vault root (parent of .obsidian etc.)
890
+ const scanDir = pkmDirs.includes(dir) ? cwd : full;
891
+ const sourcePath = pkmDirs.includes(dir) ? '.' : dir;
892
+ const type = pkmDirs.includes(dir) ? 'pkm' : 'sdd-spec';
893
+ if (!docSources.some(d => d.path === sourcePath)) {
894
+ const fileCount = countContentFiles(scanDir);
895
+ if (fileCount > 0) {
896
+ docSources.push({ path: sourcePath, type, fileCount });
897
+ }
898
+ }
899
+ }
900
+ }
901
+
902
+ if (docSources.length === 0) return [];
903
+
904
+ ui.log.info('Found project documentation that could be indexed:');
905
+
906
+ const options = docSources.map(doc => ({
907
+ value: doc,
908
+ label: `${doc.path}/ (${doc.fileCount} file(s))`,
909
+ hint: doc.type ?? 'auto-detect',
910
+ }));
911
+
912
+ const selected = await ui.multiselect('Index these docs into BookLib?', options, {
913
+ initialValues: docSources,
914
+ });
915
+
916
+ if (selected.length === 0) return [];
917
+
918
+ const indexed = [];
919
+ for (const doc of selected) {
920
+ const s = ui.spinner();
921
+ const sourcePath = path.resolve(cwd, doc.path);
922
+ const sourceName = doc.path.replace(/[/\\]/g, '-').replace(/^\./, '');
923
+ s.start(`Reading through ${doc.path}...`);
924
+ const indexStart = Date.now();
925
+
926
+ try {
927
+ const { detectSourceType } = await import('../engine/source-detector.js');
928
+ const detected = detectSourceType(sourcePath);
929
+ const sourceType = doc.type ?? detected.type;
930
+
931
+ const { SourceManager } = await import('../engine/source-manager.js');
932
+ const booklibDir = path.join(cwd, '.booklib');
933
+ const mgr = new SourceManager(booklibDir);
934
+
935
+ if (mgr.getSource(sourceName)) {
936
+ s.stop(`${doc.path} (already indexed)`);
937
+ continue;
938
+ }
939
+
940
+ mgr.registerSource({ name: sourceName, sourcePath, type: sourceType });
941
+
942
+ const indexer = new BookLibIndexer();
943
+ let lastProgressUpdate = 0;
944
+ const result = await indexer.indexDirectory(sourcePath, false, {
945
+ sourceName,
946
+ quiet: true,
947
+ onProgress({ current, total }) {
948
+ // Throttle to every 500ms — prevents spinner line spam
949
+ const now = Date.now();
950
+ if (now - lastProgressUpdate < 500 && current < total) return;
951
+ lastProgressUpdate = now;
952
+
953
+ const pct = Math.round((current / total) * 100);
954
+ const barWidth = 20;
955
+ const filled = Math.round((current / total) * barWidth);
956
+ const bar = pc.cyan('\u2588'.repeat(filled)) + pc.gray('\u2591'.repeat(barWidth - filled));
957
+ const elapsed = ((now - indexStart) / 1000).toFixed(0);
958
+ s.message(`Learning ${doc.path} ${bar} ${pct}% (${current}/${total} chunks, ${elapsed}s)`);
959
+ },
960
+ onStatus(phase) {
961
+ if (phase === 'saving') {
962
+ s.message(`Almost there, saving ${doc.path}...`);
963
+ }
964
+ },
965
+ });
966
+
967
+ const chunkCount = result?.chunks ?? 0;
968
+ mgr.markIndexed(sourceName, chunkCount);
969
+ s.stop(`${doc.path} \u2014 ${result?.files ?? doc.fileCount} files, ${chunkCount} chunks indexed (${sourceType})`);
970
+ indexed.push({ name: sourceName, files: doc.fileCount });
971
+ } catch (err) {
972
+ s.stop(`${doc.path}: failed (${err.message})`);
973
+ }
974
+ }
975
+
976
+ return indexed;
977
+ }
978
+
979
+ async function stepDecisionDemo(ui, cwd, indexedSources) {
980
+ if (indexedSources.length === 0) return;
981
+
982
+ const demoFile = findDemoFile(cwd);
983
+ if (!demoFile) return;
984
+
985
+ const s = ui.spinner();
986
+ const relPath = path.relative(cwd, demoFile);
987
+ s.start(`Checking ${relPath} against your team rules...`);
988
+
989
+ try {
990
+ const { DecisionChecker } = await import('../engine/decision-checker.js');
991
+ const searcher = new BookLibSearcher();
992
+ const checker = new DecisionChecker({ searcher });
993
+ const result = await checker.checkFile(demoFile);
994
+
995
+ if (result.contradictions.length > 0) {
996
+ s.stop(`Found ${result.contradictions.length} potential contradiction(s) in ${relPath}`);
997
+ for (const c of result.contradictions.slice(0, 3)) {
998
+ ui.log.warn(`${c.identifier} -- contradicts: ${c.source}`);
999
+ ui.log.info(` "${c.decision.slice(0, 120)}..."`);
1000
+ }
1001
+ if (result.contradictions.length > 3) {
1002
+ ui.log.info(` ... and ${result.contradictions.length - 3} more`);
1003
+ }
1004
+ } else {
1005
+ s.stop(''); // silent when no contradictions — no noise
1006
+ }
1007
+
1008
+ ui.log.info('Run "booklib check-decisions <file>" to check any file against your team decisions.');
1009
+ } catch (err) {
1010
+ s.stop(`Decision check skipped: ${err.message}`);
1011
+ }
1012
+ }
1013
+
1014
+ /**
1015
+ * Find the first .js or .ts source file in the project for demo purposes.
1016
+ * Scans up to 3 levels deep, skips node_modules and .booklib.
1017
+ */
1018
+ function findDemoFile(cwd) {
1019
+ const SKIP = new Set(['node_modules', '.booklib', '.git', 'dist', 'build', 'coverage']);
1020
+ const EXTENSIONS = /\.(js|ts|jsx|tsx)$/;
1021
+
1022
+ function scan(dir, depth) {
1023
+ if (depth > 3) return null;
1024
+ try {
1025
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1026
+ for (const entry of entries) {
1027
+ if (entry.isFile() && EXTENSIONS.test(entry.name) && !entry.name.endsWith('.d.ts')) {
1028
+ return path.join(dir, entry.name);
1029
+ }
1030
+ }
1031
+ for (const entry of entries) {
1032
+ if (entry.isDirectory() && !entry.name.startsWith('.') && !SKIP.has(entry.name)) {
1033
+ const found = scan(path.join(dir, entry.name), depth + 1);
1034
+ if (found) return found;
1035
+ }
1036
+ }
1037
+ } catch { /* permission error */ }
1038
+ return null;
1039
+ }
1040
+
1041
+ return scan(cwd, 0);
1042
+ }
1043
+
1044
+ async function stepConnectGitHub(ui, gaps) {
1045
+ if (!gaps?.postTraining?.length) return;
1046
+
1047
+ try {
1048
+ const { GitHubConnector } = await import('../connectors/github.js');
1049
+ const gh = new GitHubConnector();
1050
+ if (!gh.checkAuth().ok) return;
1051
+ } catch {
1052
+ return;
1053
+ }
1054
+
1055
+ const npmDeps = gaps.postTraining.filter(d => d.ecosystem === 'npm').slice(0, 5);
1056
+ if (npmDeps.length === 0) return;
1057
+
1058
+ const connect = await ui.confirm(
1059
+ `${npmDeps.length} npm dep(s) are post-training. Check GitHub for release notes?`,
1060
+ true
1061
+ );
1062
+
1063
+ if (!connect) return;
1064
+
1065
+ ui.log.info(
1066
+ 'To index release notes for post-training packages:\n' +
1067
+ npmDeps.map(d => ` booklib connect github releases <owner>/${d.name}`).join('\n') +
1068
+ '\n\nRun these after setup \u2014 they require the package owner/repo names.'
1069
+ );
1070
+ }
1071
+
1072
+ async function stepShowAnalysis(ui, cwd) {
1073
+ const s = ui.spinner();
1074
+ s.start('Looking for APIs that might need fresh docs...');
1075
+
1076
+ try {
1077
+ const { ProjectAnalyzer } = await import('../engine/project-analyzer.js');
1078
+ const analyzer = new ProjectAnalyzer();
1079
+ const result = await analyzer.analyze(cwd);
1080
+
1081
+ if (result.affected.length === 0) {
1082
+ s.stop('No affected APIs found in your code');
1083
+ return;
1084
+ }
1085
+
1086
+ const apiSummary = result.iconApis > 0
1087
+ ? `${result.totalApis} APIs (${result.frameworkApis} framework, ${result.iconApis} icon imports)`
1088
+ : `${result.totalApis} post-training API(s)`;
1089
+ s.stop(`Found ${apiSummary} across ${result.totalFiles} file(s)`);
1090
+
1091
+ // Group by dep for clean display, skip icon libraries from headline
1092
+ const byDep = new Map();
1093
+ for (const entry of result.affected) {
1094
+ if (entry.isIconLibrary) continue;
1095
+ const key = entry.dep.name;
1096
+ if (!byDep.has(key)) byDep.set(key, { dep: entry.dep, files: [] });
1097
+ byDep.get(key).files.push({ file: entry.file, apis: entry.apis });
1098
+ }
1099
+
1100
+ for (const [, { dep, files }] of byDep) {
1101
+ const trained = dep.publishDate
1102
+ ? `model trained before ${dep.publishDate.toISOString().split('T')[0]}`
1103
+ : 'post-training';
1104
+ ui.log.warn(`${dep.name}@${dep.version} (${trained}):`);
1105
+ for (const { file, apis } of files.slice(0, 5)) {
1106
+ ui.log.info(` ${file} \u2192 ${apis.join(', ')}`);
1107
+ }
1108
+ if (files.length > 5) {
1109
+ ui.log.info(` ... and ${files.length - 5} more file(s)`);
1110
+ }
1111
+ }
1112
+
1113
+ ui.log.success('Your AI now has current docs for these APIs.');
1114
+ return result;
1115
+ } catch (err) {
1116
+ s.stop(`Analysis skipped: ${err.message}`);
1117
+ }
1118
+ }
1119
+
1120
+ async function stepResolveGaps(ui, cwd, postTrainingDeps) {
1121
+ const s = ui.spinner();
1122
+ try {
1123
+ const { GapResolver } = await import('../engine/gap-resolver.js');
1124
+ const resolver = new GapResolver({
1125
+ outputBase: path.join(cwd, '.booklib', 'sources'),
1126
+ });
1127
+
1128
+ s.start(`Filling in ${postTrainingDeps.length} knowledge gap(s)...`);
1129
+
1130
+ const results = await resolver.resolveAll(postTrainingDeps, ({ dep, result, index, total }) => {
1131
+ const status = result.resolved ? '\u2713' : '\u2717';
1132
+ s.message(`Hang tight... [${index + 1}/${total}] ${status} ${dep.name}`);
1133
+ });
1134
+
1135
+ const resolved = results.filter(r => r.result.resolved);
1136
+ const unresolved = results.filter(r => !r.result.resolved);
1137
+
1138
+ s.stop(`${resolved.length} of ${postTrainingDeps.length} gap(s) resolved`);
1139
+
1140
+ // Index resolved sources into BookLib
1141
+ if (resolved.length > 0) {
1142
+ for (const { dep, result } of resolved) {
1143
+ try {
1144
+ const { SourceManager } = await import('../engine/source-manager.js');
1145
+ const booklibDir = path.join(cwd, '.booklib');
1146
+ const mgr = new SourceManager(booklibDir);
1147
+ const { detectSourceType } = await import('../engine/source-detector.js');
1148
+ const detected = detectSourceType(result.outputDir);
1149
+ mgr.registerSource({ name: result.sourceName, sourcePath: result.outputDir, type: detected.type });
1150
+
1151
+ const indexer = new BookLibIndexer();
1152
+ await indexer.indexDirectory(result.outputDir, false, { sourceName: result.sourceName, quiet: true });
1153
+
1154
+ ui.log.success(`${dep.name}@${dep.version} \u2014 ${result.pageCount} pages from ${result.source}`);
1155
+ } catch (err) {
1156
+ ui.log.warn(`${dep.name}: indexed but failed to register: ${err.message}`);
1157
+ }
1158
+ }
1159
+ }
1160
+
1161
+ // Show suggestions for unresolved
1162
+ for (const { dep, result } of unresolved) {
1163
+ if (result.suggestion) {
1164
+ ui.log.info(`${dep.name}@${dep.version} \u2014 not found automatically\n \u2192 ${result.suggestion}`);
1165
+ }
1166
+ }
1167
+
1168
+ const details = results.map(({ dep, result }) => ({
1169
+ name: dep.name,
1170
+ version: dep.version,
1171
+ resolved: result.resolved,
1172
+ source: result.source,
1173
+ pageCount: result.pageCount ?? 0,
1174
+ }));
1175
+
1176
+ return { resolved: resolved.length, unresolved: unresolved.length, details };
1177
+ } catch (err) {
1178
+ s.stop(`Gap resolution skipped: ${err.message}`);
1179
+ return { resolved: 0, unresolved: postTrainingDeps.length, details: [] };
1180
+ }
1181
+ }
1182
+
1183
+ async function stepBuildContextMap(ui, cwd, gaps) {
1184
+ const s = ui.spinner();
1185
+ s.start('Preparing runtime injections for your project...');
1186
+ try {
1187
+ const { ContextMapBuilder } = await import('../engine/context-map.js');
1188
+ const { listNodes, loadNode, parseNodeFrontmatter, resolveKnowledgePaths } = await import('../engine/graph.js');
1189
+
1190
+ // Read config for processing mode
1191
+ const { configPath } = resolveBookLibPaths(cwd);
1192
+ let config = {};
1193
+ try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
1194
+
1195
+ const builder = new ContextMapBuilder({
1196
+ processingMode: config.reasoning ?? 'fast',
1197
+ apiKey: process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY,
1198
+ ollamaModel: config.ollamaModel,
1199
+ });
1200
+
1201
+ // Collect knowledge items from PROJECT-LOCAL graph nodes only
1202
+ // Global ~/.booklib/knowledge/ is for MCP search, not hook injection
1203
+ const projectNodesDir = path.join(cwd, '.booklib', 'knowledge', 'nodes');
1204
+ const nodeIds = fs.existsSync(projectNodesDir) ? listNodes({ nodesDir: projectNodesDir }) : [];
1205
+ const knowledgeItems = nodeIds.map(id => {
1206
+ const raw = loadNode(id, { nodesDir: projectNodesDir });
1207
+ if (!raw) return null;
1208
+ const parsed = parseNodeFrontmatter(raw);
1209
+ return { id, text: (parsed.title ?? '') + '\n' + (parsed.body ?? ''), source: 'knowledge-graph', type: parsed.type ?? 'note' };
1210
+ }).filter(Boolean);
1211
+
1212
+ // Also extract decisions/constraints from connected project sources
1213
+ const sourcesPath = path.join(cwd, '.booklib', 'sources.json');
1214
+ if (fs.existsSync(sourcesPath)) {
1215
+ try {
1216
+ const registry = JSON.parse(fs.readFileSync(sourcesPath, 'utf8'));
1217
+ for (const source of registry.sources ?? []) {
1218
+ if (!source.sourcePath || !fs.existsSync(source.sourcePath)) continue;
1219
+ const extracted = extractDecisionsFromSource(source.sourcePath, source.name);
1220
+ knowledgeItems.push(...extracted);
1221
+ }
1222
+ } catch { /* best effort */ }
1223
+ }
1224
+
1225
+ let map = await builder.buildFromKnowledge(knowledgeItems);
1226
+
1227
+ // Add gap detection items
1228
+ if (gaps?.postTraining?.length > 0) {
1229
+ const gapMap = await builder.buildFromGaps(gaps.postTraining, { booklibDir: path.join(cwd, '.booklib') });
1230
+ map = { ...map, items: [...map.items, ...gapMap.items] };
1231
+ }
1232
+
1233
+ const mapPath = path.join(cwd, '.booklib', 'context-map.json');
1234
+ builder.save(mapPath, map);
1235
+ s.stop(`Context map: ${map.items.length} items ready for runtime injection`);
1236
+ } catch (err) {
1237
+ s.stop(`Context map skipped: ${err.message}`);
1238
+ }
1239
+ }
1240
+
1241
+ /**
1242
+ * Extract decisions, constraints, and key rules from connected source files.
1243
+ * Scans markdown files for ADR-style sections, MUST/SHOULD statements,
1244
+ * constitution rules, and spec requirements.
1245
+ * @param {string} sourcePath - directory of the connected source
1246
+ * @param {string} sourceName - name for attribution
1247
+ * @returns {Array<{id, text, source, type}>}
1248
+ */
1249
+ function extractDecisionsFromSource(sourcePath, sourceName) {
1250
+ const items = [];
1251
+ let files;
1252
+ try {
1253
+ const stat = fs.statSync(sourcePath);
1254
+ if (stat.isFile()) {
1255
+ files = [sourcePath];
1256
+ } else {
1257
+ files = [];
1258
+ const walk = (dir) => {
1259
+ try {
1260
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1261
+ if (entry.isDirectory() && !entry.name.startsWith('.')) walk(path.join(dir, entry.name));
1262
+ else if (/\.(md|mdx|txt)$/i.test(entry.name)) files.push(path.join(dir, entry.name));
1263
+ }
1264
+ } catch { /* permission error */ }
1265
+ };
1266
+ walk(sourcePath);
1267
+ }
1268
+ } catch { return []; }
1269
+
1270
+ for (const file of files.slice(0, 20)) { // cap to avoid huge source dirs
1271
+ // Skip template files — they contain MUST/SHOULD but aren't actual decisions
1272
+ const basename = path.basename(file).toLowerCase();
1273
+ if (basename.includes('template') || basename.includes('example') || basename.includes('sample')) continue;
1274
+
1275
+ let content;
1276
+ try { content = fs.readFileSync(file, 'utf8'); } catch { continue; }
1277
+ const relPath = path.relative(sourcePath, file);
1278
+
1279
+ // Extract ADR-style decisions: sections with ## Decision or ## Context + ## Consequences
1280
+ const adrMatch = content.match(/##\s*Decision[\s\S]*?(?=\n##\s|$)/gi);
1281
+ if (adrMatch) {
1282
+ for (const section of adrMatch.slice(0, 3)) {
1283
+ items.push({
1284
+ id: `src:${sourceName}:${relPath}:decision`,
1285
+ text: section.trim().slice(0, 500),
1286
+ source: `${sourceName}/${relPath}`,
1287
+ type: 'decision',
1288
+ });
1289
+ }
1290
+ }
1291
+
1292
+ // Extract MUST/SHOULD/SHALL rules
1293
+ const rules = content.match(/^.*\b(?:MUST|SHOULD|SHALL|MUST NOT|SHOULD NOT)\b.*$/gm);
1294
+ if (rules) {
1295
+ // Group nearby rules into one item
1296
+ const grouped = rules.slice(0, 10).join('\n');
1297
+ items.push({
1298
+ id: `src:${sourceName}:${relPath}:rules`,
1299
+ text: grouped.slice(0, 500),
1300
+ source: `${sourceName}/${relPath}`,
1301
+ type: 'decision',
1302
+ });
1303
+ }
1304
+
1305
+ // Extract GOAL/DELIVERS/NOT DOING from SDD specs
1306
+ const goalMatch = content.match(/^GOAL:.*$/gm);
1307
+ const deliversMatch = content.match(/^DELIVERS:[\s\S]*?(?=\n(?:NOT DOING|ASSUMPTIONS|##|$))/gm);
1308
+ if (goalMatch) {
1309
+ items.push({
1310
+ id: `src:${sourceName}:${relPath}:spec`,
1311
+ text: (goalMatch[0] + '\n' + (deliversMatch?.[0] ?? '')).slice(0, 500),
1312
+ source: `${sourceName}/${relPath}`,
1313
+ type: 'note',
1314
+ });
1315
+ }
1316
+ }
1317
+
1318
+ return items;
1319
+ }
1320
+
1321
+ // ── Re-run flow (already initialized) ────────────────────────────────────────
1322
+
1323
+ async function runRelevanceAudit(cwd) {
1324
+ const { cosine } = await import('./skill-recommender.js');
1325
+ const { getEmbeddings } = await import('./registry-embeddings.js');
1326
+ const { detect: detectProj } = await import('./project-detector.js');
1327
+
1328
+ console.log('\n BookLib \u2014 Relevance Check\n');
1329
+
1330
+ const project = detectProj(cwd);
1331
+ const installedNames = listInstalledSkillNames();
1332
+
1333
+ if (installedNames.length === 0) {
1334
+ console.log(' No BookLib-managed skills installed. Run "booklib init" to set up.');
1335
+ return;
1336
+ }
1337
+
1338
+ process.stdout.write(`\u25ba Scoring ${installedNames.length} skill(s) against your project`);
1339
+ const dotInterval = setInterval(() => { process.stdout.write('.'); }, 300);
1340
+
1341
+ const embeddings = await getEmbeddings();
1342
+ const searcher = new BookLibSearcher();
1343
+ const queryText = project.languages.map(l => `${l} programming`).join('. ') || 'software engineering';
1344
+ const queryVec = await searcher.getEmbedding(queryText);
1345
+
1346
+ clearInterval(dotInterval);
1347
+ process.stdout.write('\n\n');
1348
+
1349
+ const RELEVANCE_THRESHOLD = 0.35;
1350
+ const scored = installedNames
1351
+ .map(name => ({ name, score: embeddings.has(name) ? cosine(queryVec, embeddings.get(name)) : null }))
1352
+ .filter(s => s.score !== null)
1353
+ .sort((a, b) => b.score - a.score);
1354
+
1355
+ const unindexedCount = installedNames.length - scored.length;
1356
+ if (unindexedCount > 0) {
1357
+ process.stdout.write(` ${unindexedCount} skill(s) not yet indexed \u2014 run "booklib index" to score them\n\n`);
1358
+ }
1359
+
1360
+ if (scored.length === 0) {
1361
+ process.stdout.write(' Nothing to score yet. Run "booklib index" first.\n\n');
1362
+ return;
1363
+ }
1364
+
1365
+ const relevant = scored.filter(s => s.score >= RELEVANCE_THRESHOLD);
1366
+ const lowRelevance = scored.filter(s => s.score < RELEVANCE_THRESHOLD);
1367
+
1368
+ for (const { name, score } of relevant.slice(0, 5)) {
1369
+ process.stdout.write(` \u2713 ${name.padEnd(30)} ${(score * 100).toFixed(0)}% match\n`);
1370
+ }
1371
+ if (relevant.length > 5) {
1372
+ process.stdout.write(` \u2026 and ${relevant.length - 5} more relevant skill(s)\n`);
1373
+ }
1374
+
1375
+ if (lowRelevance.length === 0) {
1376
+ process.stdout.write(`\n All ${scored.length} scored skill(s) are relevant to this project.\n\n`);
1377
+ return;
1378
+ }
1379
+
1380
+ process.stdout.write('\n Low relevance for this project:\n');
1381
+ for (const { name, score } of lowRelevance.slice(0, 10)) {
1382
+ process.stdout.write(` \u00b7 ${name.padEnd(30)} ${(score * 100).toFixed(0)}% match\n`);
1383
+ }
1384
+ if (lowRelevance.length > 10) {
1385
+ process.stdout.write(` \u2026 and ${lowRelevance.length - 10} more\n`);
1386
+ }
1387
+
1388
+ process.stdout.write('\n Tip: run "booklib uninstall <skill>" to free slots for more relevant skills.\n\n');
1389
+ }