@booklib/core 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/.cursor/rules/booklib-standards.mdc +40 -0
  2. package/.gemini/context.md +372 -0
  3. package/AGENTS.md +166 -0
  4. package/CHANGELOG.md +226 -0
  5. package/CLAUDE.md +81 -0
  6. package/CODE_OF_CONDUCT.md +31 -0
  7. package/CONTRIBUTING.md +304 -0
  8. package/LICENSE +21 -0
  9. package/PLAN.md +28 -0
  10. package/README.ja.md +198 -0
  11. package/README.ko.md +198 -0
  12. package/README.md +503 -0
  13. package/README.pt-BR.md +198 -0
  14. package/README.uk.md +241 -0
  15. package/README.zh-CN.md +198 -0
  16. package/SECURITY.md +9 -0
  17. package/agents/architecture-reviewer.md +136 -0
  18. package/agents/booklib-reviewer.md +90 -0
  19. package/agents/data-reviewer.md +107 -0
  20. package/agents/jvm-reviewer.md +146 -0
  21. package/agents/python-reviewer.md +128 -0
  22. package/agents/rust-reviewer.md +115 -0
  23. package/agents/ts-reviewer.md +110 -0
  24. package/agents/ui-reviewer.md +117 -0
  25. package/assets/logo.svg +36 -0
  26. package/bin/booklib-mcp.js +304 -0
  27. package/bin/booklib.js +1705 -0
  28. package/bin/skills.cjs +1292 -0
  29. package/booklib-router.mdc +36 -0
  30. package/booklib.config.json +19 -0
  31. package/commands/animation-at-work.md +10 -0
  32. package/commands/clean-code-reviewer.md +10 -0
  33. package/commands/data-intensive-patterns.md +10 -0
  34. package/commands/data-pipelines.md +10 -0
  35. package/commands/design-patterns.md +10 -0
  36. package/commands/domain-driven-design.md +10 -0
  37. package/commands/effective-java.md +10 -0
  38. package/commands/effective-kotlin.md +10 -0
  39. package/commands/effective-python.md +10 -0
  40. package/commands/effective-typescript.md +10 -0
  41. package/commands/kotlin-in-action.md +10 -0
  42. package/commands/lean-startup.md +10 -0
  43. package/commands/microservices-patterns.md +10 -0
  44. package/commands/programming-with-rust.md +10 -0
  45. package/commands/refactoring-ui.md +10 -0
  46. package/commands/rust-in-action.md +10 -0
  47. package/commands/skill-router.md +10 -0
  48. package/commands/spring-boot-in-action.md +10 -0
  49. package/commands/storytelling-with-data.md +10 -0
  50. package/commands/system-design-interview.md +10 -0
  51. package/commands/using-asyncio-python.md +10 -0
  52. package/commands/web-scraping-python.md +10 -0
  53. package/community/registry.json +1616 -0
  54. package/hooks/hooks.json +23 -0
  55. package/hooks/posttooluse-capture.mjs +67 -0
  56. package/hooks/suggest.js +153 -0
  57. package/lib/agent-behaviors.js +40 -0
  58. package/lib/agent-detector.js +96 -0
  59. package/lib/config-loader.js +39 -0
  60. package/lib/conflict-resolver.js +148 -0
  61. package/lib/context-builder.js +574 -0
  62. package/lib/discovery-engine.js +298 -0
  63. package/lib/doctor/hook-installer.js +83 -0
  64. package/lib/doctor/usage-tracker.js +87 -0
  65. package/lib/engine/ai-features.js +253 -0
  66. package/lib/engine/auditor.js +103 -0
  67. package/lib/engine/bm25-index.js +178 -0
  68. package/lib/engine/capture.js +120 -0
  69. package/lib/engine/corrections.js +198 -0
  70. package/lib/engine/doctor.js +195 -0
  71. package/lib/engine/graph-injector.js +137 -0
  72. package/lib/engine/graph.js +161 -0
  73. package/lib/engine/handoff.js +405 -0
  74. package/lib/engine/indexer.js +242 -0
  75. package/lib/engine/parser.js +53 -0
  76. package/lib/engine/query-expander.js +42 -0
  77. package/lib/engine/reranker.js +40 -0
  78. package/lib/engine/rrf.js +59 -0
  79. package/lib/engine/scanner.js +151 -0
  80. package/lib/engine/searcher.js +139 -0
  81. package/lib/engine/session-coordinator.js +306 -0
  82. package/lib/engine/session-manager.js +429 -0
  83. package/lib/engine/synthesizer.js +70 -0
  84. package/lib/installer.js +70 -0
  85. package/lib/instinct-block.js +33 -0
  86. package/lib/mcp-config-writer.js +88 -0
  87. package/lib/paths.js +57 -0
  88. package/lib/profiles/design.md +19 -0
  89. package/lib/profiles/general.md +16 -0
  90. package/lib/profiles/research-analysis.md +22 -0
  91. package/lib/profiles/software-development.md +23 -0
  92. package/lib/profiles/writing-content.md +19 -0
  93. package/lib/project-initializer.js +916 -0
  94. package/lib/registry/skills.js +102 -0
  95. package/lib/registry-searcher.js +99 -0
  96. package/lib/rules/rules-manager.js +169 -0
  97. package/lib/skill-fetcher.js +333 -0
  98. package/lib/well-known-builder.js +70 -0
  99. package/lib/wizard/index.js +404 -0
  100. package/lib/wizard/integration-detector.js +41 -0
  101. package/lib/wizard/project-detector.js +100 -0
  102. package/lib/wizard/prompt.js +156 -0
  103. package/lib/wizard/registry-embeddings.js +107 -0
  104. package/lib/wizard/skill-recommender.js +69 -0
  105. package/llms-full.txt +254 -0
  106. package/llms.txt +70 -0
  107. package/package.json +45 -0
  108. package/research-reports/2026-04-01-current-architecture.md +160 -0
  109. package/research-reports/IDEAS.md +93 -0
  110. package/rules/common/clean-code.md +42 -0
  111. package/rules/java/effective-java.md +42 -0
  112. package/rules/kotlin/effective-kotlin.md +37 -0
  113. package/rules/python/effective-python.md +38 -0
  114. package/rules/rust/rust.md +37 -0
  115. package/rules/typescript/effective-typescript.md +42 -0
  116. package/scripts/gen-llms-full.mjs +36 -0
  117. package/scripts/gen-og.mjs +142 -0
  118. package/scripts/validate-frontmatter.js +25 -0
  119. package/skills/animation-at-work/SKILL.md +270 -0
  120. package/skills/animation-at-work/assets/example_asset.txt +1 -0
  121. package/skills/animation-at-work/evals/evals.json +44 -0
  122. package/skills/animation-at-work/evals/results.json +13 -0
  123. package/skills/animation-at-work/examples/after.md +64 -0
  124. package/skills/animation-at-work/examples/before.md +35 -0
  125. package/skills/animation-at-work/references/api_reference.md +369 -0
  126. package/skills/animation-at-work/references/review-checklist.md +79 -0
  127. package/skills/animation-at-work/scripts/audit_animations.py +295 -0
  128. package/skills/animation-at-work/scripts/example.py +1 -0
  129. package/skills/clean-code-reviewer/SKILL.md +444 -0
  130. package/skills/clean-code-reviewer/audit.json +35 -0
  131. package/skills/clean-code-reviewer/evals/evals.json +185 -0
  132. package/skills/clean-code-reviewer/evals/results.json +13 -0
  133. package/skills/clean-code-reviewer/examples/after.md +48 -0
  134. package/skills/clean-code-reviewer/examples/before.md +33 -0
  135. package/skills/clean-code-reviewer/references/api_reference.md +158 -0
  136. package/skills/clean-code-reviewer/references/practices-catalog.md +282 -0
  137. package/skills/clean-code-reviewer/references/review-checklist.md +254 -0
  138. package/skills/clean-code-reviewer/scripts/pre-review.py +206 -0
  139. package/skills/data-intensive-patterns/SKILL.md +267 -0
  140. package/skills/data-intensive-patterns/assets/example_asset.txt +1 -0
  141. package/skills/data-intensive-patterns/evals/evals.json +54 -0
  142. package/skills/data-intensive-patterns/evals/results.json +13 -0
  143. package/skills/data-intensive-patterns/examples/after.md +61 -0
  144. package/skills/data-intensive-patterns/examples/before.md +38 -0
  145. package/skills/data-intensive-patterns/references/api_reference.md +34 -0
  146. package/skills/data-intensive-patterns/references/patterns-catalog.md +551 -0
  147. package/skills/data-intensive-patterns/references/review-checklist.md +193 -0
  148. package/skills/data-intensive-patterns/scripts/adr.py +213 -0
  149. package/skills/data-intensive-patterns/scripts/example.py +1 -0
  150. package/skills/data-pipelines/SKILL.md +259 -0
  151. package/skills/data-pipelines/assets/example_asset.txt +1 -0
  152. package/skills/data-pipelines/evals/evals.json +45 -0
  153. package/skills/data-pipelines/evals/results.json +13 -0
  154. package/skills/data-pipelines/examples/after.md +97 -0
  155. package/skills/data-pipelines/examples/before.md +37 -0
  156. package/skills/data-pipelines/references/api_reference.md +301 -0
  157. package/skills/data-pipelines/references/review-checklist.md +181 -0
  158. package/skills/data-pipelines/scripts/example.py +1 -0
  159. package/skills/data-pipelines/scripts/new_pipeline.py +444 -0
  160. package/skills/design-patterns/SKILL.md +271 -0
  161. package/skills/design-patterns/assets/example_asset.txt +1 -0
  162. package/skills/design-patterns/evals/evals.json +46 -0
  163. package/skills/design-patterns/evals/results.json +13 -0
  164. package/skills/design-patterns/examples/after.md +52 -0
  165. package/skills/design-patterns/examples/before.md +29 -0
  166. package/skills/design-patterns/references/api_reference.md +1 -0
  167. package/skills/design-patterns/references/patterns-catalog.md +726 -0
  168. package/skills/design-patterns/references/review-checklist.md +173 -0
  169. package/skills/design-patterns/scripts/example.py +1 -0
  170. package/skills/design-patterns/scripts/scaffold.py +807 -0
  171. package/skills/domain-driven-design/SKILL.md +142 -0
  172. package/skills/domain-driven-design/assets/example_asset.txt +1 -0
  173. package/skills/domain-driven-design/evals/evals.json +48 -0
  174. package/skills/domain-driven-design/evals/results.json +13 -0
  175. package/skills/domain-driven-design/examples/after.md +80 -0
  176. package/skills/domain-driven-design/examples/before.md +43 -0
  177. package/skills/domain-driven-design/references/api_reference.md +1 -0
  178. package/skills/domain-driven-design/references/patterns-catalog.md +545 -0
  179. package/skills/domain-driven-design/references/review-checklist.md +158 -0
  180. package/skills/domain-driven-design/scripts/example.py +1 -0
  181. package/skills/domain-driven-design/scripts/scaffold.py +421 -0
  182. package/skills/effective-java/SKILL.md +227 -0
  183. package/skills/effective-java/assets/example_asset.txt +1 -0
  184. package/skills/effective-java/evals/evals.json +46 -0
  185. package/skills/effective-java/evals/results.json +13 -0
  186. package/skills/effective-java/examples/after.md +83 -0
  187. package/skills/effective-java/examples/before.md +37 -0
  188. package/skills/effective-java/references/api_reference.md +1 -0
  189. package/skills/effective-java/references/items-catalog.md +955 -0
  190. package/skills/effective-java/references/review-checklist.md +216 -0
  191. package/skills/effective-java/scripts/checkstyle_setup.py +211 -0
  192. package/skills/effective-java/scripts/example.py +1 -0
  193. package/skills/effective-kotlin/SKILL.md +271 -0
  194. package/skills/effective-kotlin/assets/example_asset.txt +1 -0
  195. package/skills/effective-kotlin/audit.json +29 -0
  196. package/skills/effective-kotlin/evals/evals.json +45 -0
  197. package/skills/effective-kotlin/evals/results.json +13 -0
  198. package/skills/effective-kotlin/examples/after.md +36 -0
  199. package/skills/effective-kotlin/examples/before.md +38 -0
  200. package/skills/effective-kotlin/references/api_reference.md +1 -0
  201. package/skills/effective-kotlin/references/practices-catalog.md +1228 -0
  202. package/skills/effective-kotlin/references/review-checklist.md +126 -0
  203. package/skills/effective-kotlin/scripts/example.py +1 -0
  204. package/skills/effective-python/SKILL.md +441 -0
  205. package/skills/effective-python/evals/evals.json +44 -0
  206. package/skills/effective-python/evals/results.json +13 -0
  207. package/skills/effective-python/examples/after.md +56 -0
  208. package/skills/effective-python/examples/before.md +40 -0
  209. package/skills/effective-python/ref-01-pythonic-thinking.md +202 -0
  210. package/skills/effective-python/ref-02-lists-and-dicts.md +146 -0
  211. package/skills/effective-python/ref-03-functions.md +186 -0
  212. package/skills/effective-python/ref-04-comprehensions-generators.md +211 -0
  213. package/skills/effective-python/ref-05-classes-interfaces.md +188 -0
  214. package/skills/effective-python/ref-06-metaclasses-attributes.md +209 -0
  215. package/skills/effective-python/ref-07-concurrency.md +213 -0
  216. package/skills/effective-python/ref-08-robustness-performance.md +248 -0
  217. package/skills/effective-python/ref-09-testing-debugging.md +253 -0
  218. package/skills/effective-python/ref-10-collaboration.md +175 -0
  219. package/skills/effective-python/references/api_reference.md +218 -0
  220. package/skills/effective-python/references/practices-catalog.md +483 -0
  221. package/skills/effective-python/references/review-checklist.md +190 -0
  222. package/skills/effective-python/scripts/lint.py +173 -0
  223. package/skills/effective-typescript/SKILL.md +262 -0
  224. package/skills/effective-typescript/audit.json +29 -0
  225. package/skills/effective-typescript/evals/evals.json +37 -0
  226. package/skills/effective-typescript/evals/results.json +13 -0
  227. package/skills/effective-typescript/examples/after.md +70 -0
  228. package/skills/effective-typescript/examples/before.md +47 -0
  229. package/skills/effective-typescript/references/api_reference.md +118 -0
  230. package/skills/effective-typescript/references/practices-catalog.md +371 -0
  231. package/skills/effective-typescript/scripts/review.py +169 -0
  232. package/skills/kotlin-in-action/SKILL.md +261 -0
  233. package/skills/kotlin-in-action/assets/example_asset.txt +1 -0
  234. package/skills/kotlin-in-action/evals/evals.json +43 -0
  235. package/skills/kotlin-in-action/evals/results.json +13 -0
  236. package/skills/kotlin-in-action/examples/after.md +53 -0
  237. package/skills/kotlin-in-action/examples/before.md +39 -0
  238. package/skills/kotlin-in-action/references/api_reference.md +1 -0
  239. package/skills/kotlin-in-action/references/practices-catalog.md +436 -0
  240. package/skills/kotlin-in-action/references/review-checklist.md +204 -0
  241. package/skills/kotlin-in-action/scripts/example.py +1 -0
  242. package/skills/kotlin-in-action/scripts/setup_detekt.py +224 -0
  243. package/skills/lean-startup/SKILL.md +160 -0
  244. package/skills/lean-startup/assets/example_asset.txt +1 -0
  245. package/skills/lean-startup/evals/evals.json +43 -0
  246. package/skills/lean-startup/evals/results.json +13 -0
  247. package/skills/lean-startup/examples/after.md +80 -0
  248. package/skills/lean-startup/examples/before.md +34 -0
  249. package/skills/lean-startup/references/api_reference.md +319 -0
  250. package/skills/lean-startup/references/review-checklist.md +137 -0
  251. package/skills/lean-startup/scripts/example.py +1 -0
  252. package/skills/lean-startup/scripts/new_experiment.py +286 -0
  253. package/skills/microservices-patterns/SKILL.md +384 -0
  254. package/skills/microservices-patterns/evals/evals.json +45 -0
  255. package/skills/microservices-patterns/evals/results.json +13 -0
  256. package/skills/microservices-patterns/examples/after.md +69 -0
  257. package/skills/microservices-patterns/examples/before.md +40 -0
  258. package/skills/microservices-patterns/references/patterns-catalog.md +391 -0
  259. package/skills/microservices-patterns/references/review-checklist.md +169 -0
  260. package/skills/microservices-patterns/scripts/new_service.py +583 -0
  261. package/skills/programming-with-rust/SKILL.md +209 -0
  262. package/skills/programming-with-rust/evals/evals.json +37 -0
  263. package/skills/programming-with-rust/evals/results.json +13 -0
  264. package/skills/programming-with-rust/examples/after.md +107 -0
  265. package/skills/programming-with-rust/examples/before.md +59 -0
  266. package/skills/programming-with-rust/references/api_reference.md +152 -0
  267. package/skills/programming-with-rust/references/practices-catalog.md +335 -0
  268. package/skills/programming-with-rust/scripts/review.py +142 -0
  269. package/skills/refactoring-ui/SKILL.md +362 -0
  270. package/skills/refactoring-ui/assets/example_asset.txt +1 -0
  271. package/skills/refactoring-ui/evals/evals.json +45 -0
  272. package/skills/refactoring-ui/evals/results.json +13 -0
  273. package/skills/refactoring-ui/examples/after.md +85 -0
  274. package/skills/refactoring-ui/examples/before.md +58 -0
  275. package/skills/refactoring-ui/references/api_reference.md +355 -0
  276. package/skills/refactoring-ui/references/review-checklist.md +114 -0
  277. package/skills/refactoring-ui/scripts/audit_css.py +250 -0
  278. package/skills/refactoring-ui/scripts/example.py +1 -0
  279. package/skills/rust-in-action/SKILL.md +350 -0
  280. package/skills/rust-in-action/evals/evals.json +38 -0
  281. package/skills/rust-in-action/evals/results.json +13 -0
  282. package/skills/rust-in-action/examples/after.md +156 -0
  283. package/skills/rust-in-action/examples/before.md +56 -0
  284. package/skills/rust-in-action/references/practices-catalog.md +346 -0
  285. package/skills/rust-in-action/scripts/review.py +147 -0
  286. package/skills/skill-router/SKILL.md +186 -0
  287. package/skills/skill-router/evals/evals.json +38 -0
  288. package/skills/skill-router/evals/results.json +13 -0
  289. package/skills/skill-router/examples/after.md +63 -0
  290. package/skills/skill-router/examples/before.md +39 -0
  291. package/skills/skill-router/references/api_reference.md +24 -0
  292. package/skills/skill-router/references/routing-heuristics.md +89 -0
  293. package/skills/skill-router/references/skill-catalog.md +174 -0
  294. package/skills/skill-router/scripts/route.py +266 -0
  295. package/skills/spring-boot-in-action/SKILL.md +340 -0
  296. package/skills/spring-boot-in-action/evals/evals.json +39 -0
  297. package/skills/spring-boot-in-action/evals/results.json +13 -0
  298. package/skills/spring-boot-in-action/examples/after.md +185 -0
  299. package/skills/spring-boot-in-action/examples/before.md +84 -0
  300. package/skills/spring-boot-in-action/references/practices-catalog.md +403 -0
  301. package/skills/spring-boot-in-action/scripts/review.py +184 -0
  302. package/skills/storytelling-with-data/SKILL.md +241 -0
  303. package/skills/storytelling-with-data/assets/example_asset.txt +1 -0
  304. package/skills/storytelling-with-data/evals/evals.json +47 -0
  305. package/skills/storytelling-with-data/evals/results.json +13 -0
  306. package/skills/storytelling-with-data/examples/after.md +50 -0
  307. package/skills/storytelling-with-data/examples/before.md +33 -0
  308. package/skills/storytelling-with-data/references/api_reference.md +379 -0
  309. package/skills/storytelling-with-data/references/review-checklist.md +111 -0
  310. package/skills/storytelling-with-data/scripts/chart_review.py +301 -0
  311. package/skills/storytelling-with-data/scripts/example.py +1 -0
  312. package/skills/system-design-interview/SKILL.md +233 -0
  313. package/skills/system-design-interview/assets/example_asset.txt +1 -0
  314. package/skills/system-design-interview/evals/evals.json +46 -0
  315. package/skills/system-design-interview/evals/results.json +13 -0
  316. package/skills/system-design-interview/examples/after.md +94 -0
  317. package/skills/system-design-interview/examples/before.md +27 -0
  318. package/skills/system-design-interview/references/api_reference.md +582 -0
  319. package/skills/system-design-interview/references/review-checklist.md +201 -0
  320. package/skills/system-design-interview/scripts/example.py +1 -0
  321. package/skills/system-design-interview/scripts/new_design.py +421 -0
  322. package/skills/using-asyncio-python/SKILL.md +290 -0
  323. package/skills/using-asyncio-python/assets/example_asset.txt +1 -0
  324. package/skills/using-asyncio-python/evals/evals.json +43 -0
  325. package/skills/using-asyncio-python/evals/results.json +13 -0
  326. package/skills/using-asyncio-python/examples/after.md +68 -0
  327. package/skills/using-asyncio-python/examples/before.md +39 -0
  328. package/skills/using-asyncio-python/references/api_reference.md +267 -0
  329. package/skills/using-asyncio-python/references/review-checklist.md +149 -0
  330. package/skills/using-asyncio-python/scripts/check_blocking.py +270 -0
  331. package/skills/using-asyncio-python/scripts/example.py +1 -0
  332. package/skills/web-scraping-python/SKILL.md +280 -0
  333. package/skills/web-scraping-python/assets/example_asset.txt +1 -0
  334. package/skills/web-scraping-python/evals/evals.json +46 -0
  335. package/skills/web-scraping-python/evals/results.json +13 -0
  336. package/skills/web-scraping-python/examples/after.md +109 -0
  337. package/skills/web-scraping-python/examples/before.md +40 -0
  338. package/skills/web-scraping-python/references/api_reference.md +393 -0
  339. package/skills/web-scraping-python/references/review-checklist.md +163 -0
  340. package/skills/web-scraping-python/scripts/example.py +1 -0
  341. package/skills/web-scraping-python/scripts/new_scraper.py +231 -0
  342. package/skills/writing-plans/audit.json +34 -0
  343. package/tests/agent-detector.test.js +83 -0
  344. package/tests/corrections.test.js +245 -0
  345. package/tests/doctor/hook-installer.test.js +72 -0
  346. package/tests/doctor/usage-tracker.test.js +140 -0
  347. package/tests/engine/benchmark-eval.test.js +31 -0
  348. package/tests/engine/bm25-index.test.js +85 -0
  349. package/tests/engine/capture-command.test.js +35 -0
  350. package/tests/engine/capture.test.js +17 -0
  351. package/tests/engine/graph-augmented-search.test.js +107 -0
  352. package/tests/engine/graph-injector.test.js +44 -0
  353. package/tests/engine/graph.test.js +216 -0
  354. package/tests/engine/hybrid-searcher.test.js +74 -0
  355. package/tests/engine/indexer-bm25.test.js +37 -0
  356. package/tests/engine/mcp-tools.test.js +73 -0
  357. package/tests/engine/project-initializer-mcp.test.js +99 -0
  358. package/tests/engine/query-expander.test.js +36 -0
  359. package/tests/engine/reranker.test.js +51 -0
  360. package/tests/engine/rrf.test.js +49 -0
  361. package/tests/engine/srag-prefix.test.js +47 -0
  362. package/tests/instinct-block.test.js +23 -0
  363. package/tests/mcp-config-writer.test.js +60 -0
  364. package/tests/project-initializer-new-agents.test.js +48 -0
  365. package/tests/rules/rules-manager.test.js +230 -0
  366. package/tests/well-known-builder.test.js +40 -0
  367. package/tests/wizard/integration-detector.test.js +31 -0
  368. package/tests/wizard/project-detector.test.js +51 -0
  369. package/tests/wizard/prompt-session.test.js +61 -0
  370. package/tests/wizard/prompt.test.js +16 -0
  371. package/tests/wizard/registry-embeddings.test.js +35 -0
  372. package/tests/wizard/skill-recommender.test.js +34 -0
  373. package/tests/wizard/slot-count.test.js +25 -0
  374. package/vercel.json +21 -0
@@ -0,0 +1,405 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+
5
+ /**
6
+ * Manages agent session snapshots for multi-agent handoffs.
7
+ */
8
+ export class BookLibHandoff {
9
+ constructor(handoffDir = path.join(process.cwd(), '.booklib', 'sessions')) {
10
+ this.handoffDir = handoffDir;
11
+ }
12
+
13
+ /**
14
+ * Automatically detects a session ID based on Git branch or folder name + timestamp.
15
+ */
16
+ getAutoSessionId() {
17
+ try {
18
+ // 1. Try Git Branch
19
+ return execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim();
20
+ } catch {
21
+ // 2. Fallback to Folder Name + Date (for non-git or unrelated chats)
22
+ const folder = path.basename(process.cwd());
23
+ const date = new Date().toISOString().split('T')[0];
24
+ return `${folder}-${date}`;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Resolves the file path for a session.
30
+ */
31
+ getSessionPath(name) {
32
+ const sessionName = name || this.getAutoSessionId();
33
+ return path.join(this.handoffDir, `${sessionName}.md`);
34
+ }
35
+
36
+ /**
37
+ * Saves the current agent session state to a file.
38
+ * Enhanced with git state, uncommitted changes, and recent commits tracking.
39
+ * For long-running chats: includes recent commit history as implicit memory.
40
+ */
41
+ saveState({ name, goal, next, progress, skills }) {
42
+ if (!fs.existsSync(this.handoffDir)) {
43
+ fs.mkdirSync(this.handoffDir, { recursive: true });
44
+ }
45
+
46
+ // Capture git state
47
+ let gitInfo = '';
48
+ let recentCommits = '';
49
+ try {
50
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim();
51
+ const lastCommit = execSync('git log -1 --format=%H%n%s', { stdio: 'pipe' }).toString().trim().split('\n');
52
+ const stagedFiles = execSync('git diff --name-only --cached', { stdio: 'pipe' }).toString().trim().split('\n').filter(Boolean);
53
+ const unstagedFiles = execSync('git diff --name-only', { stdio: 'pipe' }).toString().trim().split('\n').filter(Boolean);
54
+
55
+ // Capture last 10 commits (implicit chat memory via commit messages)
56
+ const commits = execSync('git log --oneline -10', { stdio: 'pipe' }).toString().trim().split('\n');
57
+ recentCommits = commits.map(c => ` ${c}`).join('\n');
58
+
59
+ gitInfo = `
60
+ <git_state>
61
+ <branch>${branch}</branch>
62
+ <last_commit_sha>${lastCommit[0]}</last_commit_sha>
63
+ <last_commit_msg>${lastCommit[1] || 'N/A'}</last_commit_msg>
64
+ <uncommitted_changes>
65
+ <staged_files>${stagedFiles.length > 0 ? stagedFiles.join(', ') : 'None'}</staged_files>
66
+ <modified_files>${unstagedFiles.length > 0 ? unstagedFiles.join(', ') : 'None'}</modified_files>
67
+ </uncommitted_changes>
68
+ <recent_commit_history>
69
+ <note>Use this as implicit context: each commit message documents decisions made</note>
70
+ ${recentCommits}
71
+ </recent_commit_history>
72
+ </git_state>`;
73
+ } catch (err) {
74
+ gitInfo = `
75
+ <git_state>
76
+ <warning>Could not capture git state. Verify changes are committed before resuming.</warning>
77
+ </git_state>`;
78
+ }
79
+
80
+ const sessionPath = this.getSessionPath(name);
81
+ const content = `
82
+ <session_handoff>
83
+ <metadata>
84
+ <timestamp>${new Date().toISOString()}</timestamp>
85
+ <session_id>${name || this.getAutoSessionId()}</session_id>
86
+ <working_directory>${process.cwd()}</working_directory>
87
+ </metadata>
88
+
89
+ <context>
90
+ <goal>${goal || 'Not specified'}</goal>
91
+ <progress>${progress || 'Just started'}</progress>
92
+ <pending_tasks>${next || 'Determine next steps'}</pending_tasks>
93
+ <note>For long chats: review recent_commit_history below for detailed reasoning and decisions</note>
94
+ </context>
95
+
96
+ <active_knowledge>
97
+ ${(skills || []).map(s => `<skill id="${s}" />`).join('\n ')}
98
+ </active_knowledge>
99
+ ${gitInfo}
100
+
101
+ <recovery_instructions>
102
+ <step1>Run: \`node bin/booklib.js resume ${name || this.getAutoSessionId()}\` to load this context</step1>
103
+ <step2>Review the pending_tasks above</step2>
104
+ <step3>CHECK RECENT COMMIT HISTORY (git_state/recent_commit_history) for chat reasoning</step3>
105
+ <step4>If resuming in a different session, ensure you're in the correct working_directory</step4>
106
+ <step5>Run: \`git log --oneline -20\` to see full history if needed</step5>
107
+ </recovery_instructions>
108
+
109
+ <long_chat_recovery_guide>
110
+ <context_source>Since conversation transcripts aren't saved, use these sources:</context_source>
111
+ <source1>Recent commit messages (above) document each decision</source1>
112
+ <source2>Run: \`git show\` on recent commits to see code changes + reasoning</source2>
113
+ <source3>Run: \`git log -p --follow -- &lt;file&gt;\` to see file evolution</source3>
114
+ <source4>Pending_tasks above shows immediate next steps</source4>
115
+ <source5>Active skills tell you which frameworks were being applied</source5>
116
+ </long_chat_recovery_guide>
117
+ </session_handoff>
118
+ `;
119
+
120
+ fs.writeFileSync(sessionPath, content.trim());
121
+ console.log(`✅ Session snapshot saved to ${sessionPath}`);
122
+ console.log(`📝 Git state captured: branch, commits, uncommitted changes`);
123
+ console.log(`📚 Recent 10 commits saved for implicit chat memory`);
124
+ }
125
+
126
+ /**
127
+ * Resumes the session by reading the handoff file.
128
+ */
129
+ resume(name) {
130
+ const sessionPath = this.getSessionPath(name);
131
+
132
+ if (!fs.existsSync(sessionPath)) {
133
+ const sessions = this.listSessions();
134
+ if (sessions.length > 0) {
135
+ return `No snapshot found for "${name || this.getAutoSessionId()}". \nAvailable sessions: ${sessions.join(', ')}`;
136
+ }
137
+ return 'No handoff files found in .booklib/sessions/. Starting a fresh session.';
138
+ }
139
+
140
+ const content = fs.readFileSync(sessionPath, 'utf8');
141
+ return `
142
+ === RESUMING SESSION [${name || this.getAutoSessionId()}] ===
143
+ An previous agent has left a context snapshot for you.
144
+ Please read the following handoff details and continue the work:
145
+
146
+ ${content}
147
+ `;
148
+ }
149
+
150
+ /**
151
+ * Lists all available handoff sessions.
152
+ */
153
+ listSessions() {
154
+ if (!fs.existsSync(this.handoffDir)) return [];
155
+ return fs.readdirSync(this.handoffDir)
156
+ .filter(f => f.endsWith('.md'))
157
+ .map(f => f.replace('.md', ''));
158
+ }
159
+
160
+ /**
161
+ * Sets up automatic handoff saving on process exit (SIGINT, SIGTERM, SIGHUP).
162
+ * Catches the case where user forgets to explicitly call save-state.
163
+ *
164
+ * Usage:
165
+ * const handoff = new BookLibHandoff();
166
+ * handoff.setupAutoSave({
167
+ * goal: 'Build payment processor',
168
+ * progress: 'Phase 2 in progress',
169
+ * next: 'Implement webhook handler',
170
+ * skills: ['effective-typescript', 'clean-code-reviewer']
171
+ * });
172
+ */
173
+ setupAutoSave(options = {}) {
174
+ const { goal, progress, next, skills } = options;
175
+ const sessionId = this.getAutoSessionId();
176
+
177
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
178
+
179
+ signals.forEach(signal => {
180
+ process.on(signal, () => {
181
+ try {
182
+ console.log('\n⚠️ Session interrupted. Auto-saving handoff...');
183
+ this.saveState({
184
+ name: sessionId,
185
+ goal: goal || 'Session interrupted - see git log for context',
186
+ progress: progress || 'In progress - check recent commits',
187
+ next: next || 'Resume from last commit',
188
+ skills: skills || []
189
+ });
190
+ console.log(`✅ Auto-saved to ~/.booklib/sessions/${sessionId}.md`);
191
+ console.log(`💡 Next agent can resume with: booklib resume`);
192
+ process.exit(0);
193
+ } catch (err) {
194
+ console.error('⚠️ Auto-save failed:', err.message);
195
+ process.exit(1);
196
+ }
197
+ });
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Recovers handoff state from session files OR git (100% coverage).
203
+ *
204
+ * Priority order:
205
+ * 1. Explicit session file for current branch
206
+ * 2. Parent session (via lineage)
207
+ * 3. Most recent session on same branch
208
+ * 4. Git-based recovery (fallback)
209
+ *
210
+ * Usage:
211
+ * const recovered = handoff.recoverFromSessionOrGit();
212
+ * console.log(recovered);
213
+ */
214
+ recoverFromSessionOrGit() {
215
+ const branch = this._getCurrentBranch();
216
+ const sessionPath = this.getSessionPath(branch);
217
+
218
+ // Try 1: Explicit session file for current branch
219
+ if (fs.existsSync(sessionPath)) {
220
+ const content = fs.readFileSync(sessionPath, 'utf8');
221
+ return `
222
+ SESSION-BASED RECOVERY (found matching session)
223
+ ═══════════════════════════════════════════════════
224
+
225
+ Branch: ${branch}
226
+ Status: Session file found for this branch ✅
227
+
228
+ ${content}
229
+
230
+ NEXT STEPS:
231
+ 1. Review the context above from the previous agent
232
+ 2. Check git status for any uncommitted work
233
+ 3. Run: git log --oneline -5 to see recent commits
234
+ 4. Continue work as indicated in pending_tasks above
235
+ `;
236
+ }
237
+
238
+ // Try 2: Check lineage for parent session
239
+ const parentSession = this._getParentSession(branch);
240
+ if (parentSession) {
241
+ const parentPath = this.getSessionPath(parentSession);
242
+ if (fs.existsSync(parentPath)) {
243
+ const content = fs.readFileSync(parentPath, 'utf8');
244
+ return `
245
+ SESSION-BASED RECOVERY (found parent session in lineage)
246
+ ═════════════════════════════════════════════════════════
247
+
248
+ Branch: ${branch}
249
+ Status: No explicit session for this branch, but parent "${parentSession}" found ✅
250
+
251
+ PARENT SESSION CONTEXT:
252
+ ${content}
253
+
254
+ RECOVERY INTERPRETATION:
255
+ - This branch was created from: ${parentSession}
256
+ - Parent agent's work provides the base context
257
+ - Any commits since parent session show your additional work
258
+ - Run: git log ${parentSession}..HEAD to see your new commits
259
+
260
+ NEXT STEPS:
261
+ 1. Review parent session context above
262
+ 2. Run: git diff ${parentSession}..HEAD to see your changes
263
+ 3. Review pending_tasks from parent session
264
+ 4. Continue or adjust based on what you've added
265
+ `;
266
+ }
267
+ }
268
+
269
+ // Try 3: Most recent session on same branch
270
+ const recentSession = this._getMostRecentSessionOnBranch(branch);
271
+ if (recentSession) {
272
+ const recentPath = this.getSessionPath(recentSession);
273
+ if (fs.existsSync(recentPath)) {
274
+ const content = fs.readFileSync(recentPath, 'utf8');
275
+ return `
276
+ SESSION-BASED RECOVERY (found recent session on branch)
277
+ ═══════════════════════════════════════════════════════
278
+
279
+ Branch: ${branch}
280
+ Status: Using most recent session on this branch: "${recentSession}" ✅
281
+
282
+ ${content}
283
+
284
+ RECOVERY INTERPRETATION:
285
+ - This is the most recent session saved on this branch
286
+ - Any new commits since this session represent your continued work
287
+ - Run: git log ${recentSession}.. to see what you did after this session
288
+
289
+ NEXT STEPS:
290
+ 1. Review the session context above
291
+ 2. Check your recent commits for what you accomplished
292
+ 3. Review pending_tasks from the session
293
+ 4. Continue from where you left off
294
+ `;
295
+ }
296
+ }
297
+
298
+ // Fallback: Git-based recovery
299
+ return this.recoverFromGit();
300
+ }
301
+
302
+ // ─── HELPER METHODS FOR ENHANCED RECOVERY ───
303
+
304
+ /**
305
+ * Gets current git branch.
306
+ */
307
+ _getCurrentBranch() {
308
+ try {
309
+ return execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim();
310
+ } catch {
311
+ return 'unknown';
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Gets parent session from lineage file.
317
+ */
318
+ _getParentSession(branch) {
319
+ const lineageFile = path.join(this.handoffDir, '_lineage.json');
320
+ if (!fs.existsSync(lineageFile)) return null;
321
+
322
+ try {
323
+ const lineage = JSON.parse(fs.readFileSync(lineageFile, 'utf8'));
324
+ return lineage[branch]?.parent || null;
325
+ } catch {
326
+ return null;
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Finds most recent session saved on current branch.
332
+ */
333
+ _getMostRecentSessionOnBranch(branch) {
334
+ if (!fs.existsSync(this.handoffDir)) return null;
335
+
336
+ const sessions = fs.readdirSync(this.handoffDir)
337
+ .filter(f => f.endsWith('.md') && f !== '_lineage.json')
338
+ .map(f => {
339
+ const filePath = path.join(this.handoffDir, f);
340
+ const content = fs.readFileSync(filePath, 'utf8');
341
+ const branchMatch = content.match(/<branch>(.*?)<\/branch>/);
342
+ const sessionBranch = branchMatch ? branchMatch[1] : null;
343
+
344
+ return {
345
+ name: f.replace('.md', ''),
346
+ branch: sessionBranch,
347
+ mtime: fs.statSync(filePath).mtime
348
+ };
349
+ })
350
+ .filter(s => s.branch === branch)
351
+ .sort((a, b) => b.mtime - a.mtime);
352
+
353
+ return sessions.length > 0 ? sessions[0].name : null;
354
+ }
355
+
356
+ /**
357
+ * Recovers handoff state from git when explicit save file is missing.
358
+ * Useful if user forgets to save-state before quota hit.
359
+ *
360
+ * Usage:
361
+ * const recovered = handoff.recoverFromGit();
362
+ * console.log(recovered);
363
+ */
364
+ recoverFromGit() {
365
+ try {
366
+ const branch = this._getCurrentBranch();
367
+ const lastCommit = execSync('git log -1 --format=%H%n%s%n%b', { stdio: 'pipe' }).toString().trim().split('\n');
368
+ const commits = execSync('git log --oneline -10', { stdio: 'pipe' }).toString().trim();
369
+ const status = execSync('git status --short', { stdio: 'pipe' }).toString().trim();
370
+
371
+ return `
372
+ GIT-BASED RECOVERY (no session files found)
373
+ ═══════════════════════════════════════════
374
+
375
+ Branch: ${branch}
376
+
377
+ Last Commit: ${lastCommit[0]}
378
+ Message: ${lastCommit[1] || 'N/A'}
379
+
380
+ Recent 10 Commits:
381
+ ${commits}
382
+
383
+ Uncommitted Changes:
384
+ ${status || 'None'}
385
+
386
+ RECOVERY STEPS:
387
+ 1. Review recent commits above to understand what was done
388
+ 2. Run: git show <commit-sha> to see code + reasoning
389
+ 3. Run: git log -p -- <file> to trace file evolution
390
+ 4. Run: git status to see current work in progress
391
+ 5. Create explicit handoff for next agent: booklib save-state
392
+
393
+ SAVE FOR NEXT TIME:
394
+ Call "booklib save-state" before quota exhaustion to preserve:
395
+ - Goal statement
396
+ - Progress summary
397
+ - Next tasks
398
+ - Active skills
399
+ - Recovery instructions
400
+ `;
401
+ } catch (err) {
402
+ return `Could not recover from git: ${err.message}. Check manual git history.`;
403
+ }
404
+ }
405
+ }
@@ -0,0 +1,242 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { pipeline } from '@huggingface/transformers';
4
+ import { LocalIndex } from 'vectra';
5
+ import { parseSkillFile } from './parser.js';
6
+ import { resolveBookLibPaths } from '../paths.js';
7
+ import { BM25Index } from './bm25-index.js';
8
+
9
+ /**
10
+ * Builds a structured metadata prefix for SRAG-style embeddings.
11
+ * Prepended to chunk text before vector embedding so the model encodes domain context.
12
+ * @param {object} metadata - Chunk metadata (name/title, type, tags).
13
+ * @returns {string} Prefix string like "[skill:X] [type:Y] [tags:a,b] " or "".
14
+ */
15
+ export function buildMetadataPrefix(metadata) {
16
+ const parts = [];
17
+ const label = metadata.name ?? metadata.title;
18
+ if (label) parts.push(`[skill:${label}]`);
19
+ if (metadata.type) parts.push(`[type:${metadata.type}]`);
20
+ if (Array.isArray(metadata.tags) && metadata.tags.length > 0) {
21
+ parts.push(`[tags:${metadata.tags.join(',')}]`);
22
+ }
23
+ return parts.length > 0 ? parts.join(' ') + ' ' : '';
24
+ }
25
+
26
+ /**
27
+ * Handles the creation and updating of the semantic index for the BookLib library.
28
+ */
29
+ export class BookLibIndexer {
30
+ constructor(indexPath) {
31
+ this.indexPath = indexPath ?? resolveBookLibPaths().indexPath;
32
+ fs.mkdirSync(this.indexPath, { recursive: true });
33
+ this.index = new LocalIndex(this.indexPath);
34
+ this.extractor = null;
35
+ }
36
+
37
+ get bm25Path() {
38
+ return path.join(path.dirname(this.indexPath), 'bm25.json');
39
+ }
40
+
41
+ _loadOrCreateBM25() {
42
+ return fs.existsSync(this.bm25Path) ? BM25Index.load(this.bm25Path) : new BM25Index();
43
+ }
44
+
45
+ /**
46
+ * Loads the embedding model (lazy-loaded).
47
+ * @param {Object} opts
48
+ * @param {boolean} [opts.quiet=false] - Suppress the "Loading local embedding model..." message.
49
+ */
50
+ async loadModel(opts = {}) {
51
+ const { quiet = false } = opts;
52
+ if (!this.extractor) {
53
+ const indexExists = await this.index.isIndexCreated().catch(() => false);
54
+ if (!indexExists) {
55
+ console.log('First run: downloading embedding model (~25 MB, ~1 min)...');
56
+ } else if (!quiet) {
57
+ console.log('Loading local embedding model...');
58
+ }
59
+ this.extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Generates a vector embedding for a string.
65
+ */
66
+ async getEmbedding(text) {
67
+ await this.loadModel();
68
+ const output = await this.extractor(text, { pooling: 'mean', normalize: true });
69
+ return Array.from(output.data);
70
+ }
71
+
72
+ /**
73
+ * Indexes a directory of skills.
74
+ *
75
+ * @param {string} dirPath - The root directory of the skills library.
76
+ * @param {boolean} clearFirst - Whether to clear the index before starting.
77
+ * @param {Object} opts
78
+ * @param {boolean} [opts.quiet=false] - Suppress per-file output; print a single summary instead.
79
+ */
80
+ async indexDirectory(dirPath, clearFirst = false, opts = {}) {
81
+ const { quiet = false, onProgress } = opts;
82
+
83
+ if (clearFirst && fs.existsSync(this.indexPath)) {
84
+ fs.rmSync(this.indexPath, { recursive: true, force: true });
85
+ }
86
+
87
+ if (!(await this.index.isIndexCreated())) {
88
+ await this.index.createIndex();
89
+ }
90
+
91
+ const files = this.getFiles(dirPath, ['.md', '.mdc']);
92
+ if (!quiet) console.log(`Found ${files.length} skill files to index in ${dirPath}.`);
93
+
94
+ // Pre-warm the model so the load message respects the quiet flag.
95
+ await this.loadModel({ quiet });
96
+ const bm25Chunks = [];
97
+
98
+ let totalFiles = 0;
99
+ let totalChunks = 0;
100
+ let skipped = 0;
101
+
102
+ for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
103
+ const file = files[fileIndex];
104
+ const content = fs.readFileSync(file, 'utf8');
105
+ const relativePath = path.relative(dirPath, file);
106
+ let chunks;
107
+ try {
108
+ chunks = parseSkillFile(content, relativePath);
109
+ } catch (err) {
110
+ if (quiet) {
111
+ skipped++;
112
+ } else {
113
+ process.stderr.write(`⚠ Skipping ${relativePath}: ${err.message}\n`);
114
+ }
115
+ continue;
116
+ }
117
+
118
+ onProgress?.({ current: fileIndex + 1, total: files.length, file: relativePath });
119
+
120
+ if (quiet) {
121
+ totalFiles++;
122
+ totalChunks += chunks.length;
123
+ } else {
124
+ console.log(`Indexing ${relativePath} (${chunks.length} chunks)...`);
125
+ }
126
+
127
+ bm25Chunks.push(...chunks);
128
+
129
+ for (const chunk of chunks) {
130
+ const prefixedText = buildMetadataPrefix(chunk.metadata) + chunk.text;
131
+ const vector = await this.getEmbedding(prefixedText);
132
+ await this.index.insertItem({
133
+ vector,
134
+ metadata: { ...chunk.metadata, text: chunk.text }
135
+ });
136
+ }
137
+ }
138
+
139
+ const bm25 = new BM25Index();
140
+ bm25.build(bm25Chunks);
141
+ bm25.save(this.bm25Path);
142
+
143
+ if (quiet) {
144
+ console.log(` Indexed ${totalFiles} files (${totalChunks} chunks)`);
145
+ if (skipped > 0) {
146
+ console.log(` ⚠ ${skipped} file(s) skipped (malformed frontmatter)`);
147
+ }
148
+ } else {
149
+ console.log('Indexing complete.');
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Indexes a single knowledge node file into the existing index.
155
+ * Safe to call after each capture command — only adds the new node.
156
+ * @param {string} filePath - Absolute path to the node .md file.
157
+ * @param {string} nodesDir - Root nodes directory (used for relative path in metadata).
158
+ */
159
+ async indexNodeFile(filePath, nodesDir) {
160
+ if (!(await this.index.isIndexCreated())) {
161
+ await this.index.createIndex();
162
+ }
163
+
164
+ const content = fs.readFileSync(filePath, 'utf8');
165
+ const relativePath = path.relative(nodesDir, filePath);
166
+ let chunks;
167
+ try {
168
+ chunks = parseSkillFile(content, relativePath);
169
+ } catch (err) {
170
+ process.stderr.write(`⚠ Skipping node ${relativePath}: ${err.message}\n`);
171
+ return;
172
+ }
173
+
174
+ // If body is empty, index the frontmatter fields so the node is still findable by title/tags
175
+ if (chunks.length === 0) {
176
+ const matter = (await import('gray-matter')).default;
177
+ const { data } = matter.read(filePath);
178
+ const fallbackText = [data.title, data.type, ...(data.tags ?? [])].filter(Boolean).join(' ');
179
+ if (!fallbackText) return;
180
+ const prefixedFallback = buildMetadataPrefix(data) + fallbackText;
181
+ const vector = await this.getEmbedding(prefixedFallback);
182
+ await this.index.insertItem({
183
+ vector,
184
+ metadata: { text: fallbackText, id: data.id, title: data.title, type: data.type, nodeKind: 'knowledge', nodeFile: filePath }
185
+ });
186
+ const bm25 = this._loadOrCreateBM25();
187
+ bm25.add({ text: fallbackText, metadata: { id: data.id, title: data.title, type: data.type, nodeKind: 'knowledge', nodeFile: filePath } });
188
+ bm25.save(this.bm25Path);
189
+ return;
190
+ }
191
+
192
+ for (const chunk of chunks) {
193
+ const prefixedText = buildMetadataPrefix(chunk.metadata) + chunk.text;
194
+ const vector = await this.getEmbedding(prefixedText);
195
+ await this.index.insertItem({
196
+ vector,
197
+ metadata: { ...chunk.metadata, text: chunk.text, nodeKind: 'knowledge', nodeFile: filePath }
198
+ });
199
+ }
200
+
201
+ const bm25 = this._loadOrCreateBM25();
202
+ for (const chunk of chunks) bm25.add(chunk);
203
+ bm25.save(this.bm25Path);
204
+ }
205
+
206
+ /**
207
+ * Indexes all knowledge nodes from .booklib/knowledge/nodes/.
208
+ * Used by `booklib index` to rebuild the full knowledge portion of the index.
209
+ * @param {string} nodesDir - Path to the nodes directory.
210
+ */
211
+ async indexKnowledgeNodes(nodesDir) {
212
+ if (!fs.existsSync(nodesDir)) return;
213
+
214
+ const files = this.getFiles(nodesDir, ['.md']);
215
+ if (files.length === 0) return;
216
+ console.log(`Indexing ${files.length} knowledge node(s)...`);
217
+
218
+ for (const file of files) {
219
+ await this.indexNodeFile(file, nodesDir);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Helper to recursively list files with specific extensions.
225
+ */
226
+ getFiles(dir, extensions) {
227
+ let results = [];
228
+ const list = fs.readdirSync(dir);
229
+ list.forEach(file => {
230
+ file = path.join(dir, file);
231
+ const stat = fs.statSync(file);
232
+ if (stat && stat.isDirectory()) {
233
+ results = results.concat(this.getFiles(file, extensions));
234
+ } else {
235
+ if (extensions.some(ext => file.endsWith(ext))) {
236
+ results.push(file);
237
+ }
238
+ }
239
+ });
240
+ return results;
241
+ }
242
+ }
@@ -0,0 +1,53 @@
1
+ import matter from 'gray-matter';
2
+
3
+ /**
4
+ * Parses a markdown/mdc file and extracts semantic chunks based on XML tags.
5
+ * Each chunk includes the file's frontmatter as metadata.
6
+ *
7
+ * @param {string} content - The raw content of the file.
8
+ * @param {string} filePath - The path to the file (for metadata).
9
+ * @returns {Array<{text: string, metadata: object}>} - Array of semantic chunks.
10
+ */
11
+ export function parseSkillFile(content, filePath) {
12
+ const { data: frontmatter, content: body } = matter(content);
13
+ const chunks = [];
14
+
15
+ // 1. Extract the full summary/intro (everything before the first XML tag)
16
+ const introMatch = body.split(/<[a-z_]+>/)[0].trim();
17
+ if (introMatch) {
18
+ chunks.push({
19
+ text: introMatch,
20
+ metadata: { ...frontmatter, filePath, type: 'summary' }
21
+ });
22
+ }
23
+
24
+ // 2. Extract content from XML tags (Universal & Programming)
25
+ const tagRegex = /<([a-z_]+)>([\s\S]*?)<\/\1>/g;
26
+ let match;
27
+ while ((match = tagRegex.exec(body)) !== null) {
28
+ const tagName = match[1];
29
+ const tagContent = match[2].trim();
30
+ if (tagContent) {
31
+ // Map domain-specific tags to universal categories
32
+ let category = tagName;
33
+ if (tagName === 'core_principles') category = 'framework';
34
+ if (tagName === 'anti_patterns') category = 'pitfalls';
35
+ if (tagName === 'examples') category = 'case_studies';
36
+
37
+ chunks.push({
38
+ text: tagContent,
39
+ metadata: { ...frontmatter, filePath, type: category, originalTag: tagName }
40
+ });
41
+ }
42
+ }
43
+
44
+ // 3. If no XML tags were found but there is a body, treat the whole body as a chunk
45
+ if (chunks.length <= 1 && body.trim() && body.trim() !== introMatch) {
46
+ chunks.push({
47
+ text: body.trim(),
48
+ metadata: { ...frontmatter, filePath, type: 'content' }
49
+ });
50
+ }
51
+
52
+ return chunks;
53
+ }