@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,148 @@
1
+ /**
2
+ * Resolves conflicts between skill suggestions or indexed chunks.
3
+ *
4
+ * Resolution rules (applied per topic group):
5
+ * specificity delta >= 2 OR score delta >= 0.2 → auto-resolve (higher wins, rationale attached)
6
+ * otherwise → escalate (add to conflicts list)
7
+ *
8
+ * Usage:
9
+ * const resolver = new ConflictResolver(registryArray);
10
+ * const { winners, suppressed, conflicts } = resolver.resolveSkills(suggestions);
11
+ * const { winners, suppressed, conflicts } = resolver.resolveChunks(searchResults);
12
+ */
13
+ export class ConflictResolver {
14
+ /**
15
+ * @param {Array<{name: string, specificity?: number, topic?: string, stars?: number}>} registry
16
+ */
17
+ constructor(registry = []) {
18
+ this._byName = new Map(registry.map(s => [s.name, s]));
19
+ }
20
+
21
+ /**
22
+ * Resolves conflicts between registry-level skill suggestions.
23
+ * @param {object[]} skills — each must have .name
24
+ */
25
+ resolveSkills(skills) {
26
+ return this._resolve(
27
+ skills,
28
+ s => s.name,
29
+ s => this._meta(s.name, s)
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Resolves conflicts between semantic-search chunks.
35
+ * @param {Array<{score: number, text: string, metadata: object}>} chunks
36
+ */
37
+ resolveChunks(chunks) {
38
+ // Deduplicate by skill name — keep highest-scored chunk per skill
39
+ const bySkill = new Map();
40
+ for (const c of chunks) {
41
+ const raw = c.metadata?.name ?? c.metadata?.filePath ?? 'unknown';
42
+ // Normalise to skill name: take first path segment (handles reference file paths like
43
+ // "clean-code-reviewer/references/review-checklist.md" → "clean-code-reviewer")
44
+ const skillName = raw.includes('/') ? raw.split('/')[0] : raw;
45
+ const existing = bySkill.get(skillName);
46
+ if (!existing || (c.score ?? 0) > (existing.score ?? 0)) {
47
+ bySkill.set(skillName, c);
48
+ }
49
+ }
50
+ const deduped = [...bySkill.values()];
51
+ return this._resolve(
52
+ deduped,
53
+ c => c.metadata?.name ?? c.metadata?.filePath ?? 'unknown',
54
+ c => this._meta(c.metadata?.name, c)
55
+ );
56
+ }
57
+
58
+ // ── Private ────────────────────────────────────────────────────────────────
59
+
60
+ /** Look up registry metadata, falling back to inline values or defaults. */
61
+ _meta(name, item = {}) {
62
+ const reg = this._byName.get(name);
63
+ return {
64
+ specificity: reg?.specificity ?? item?.specificity ?? 5,
65
+ topic: reg?.topic ?? item?.topic ?? name ?? 'unknown',
66
+ stars: reg?.stars ?? item?.stars ?? 0,
67
+ };
68
+ }
69
+
70
+ _resolve(items, getName, getMeta) {
71
+ // Group by topic
72
+ const byTopic = new Map();
73
+ for (const item of items) {
74
+ const { topic } = getMeta(item);
75
+ if (!byTopic.has(topic)) byTopic.set(topic, []);
76
+ byTopic.get(topic).push(item);
77
+ }
78
+
79
+ const winners = [];
80
+ const suppressed = [];
81
+ const conflicts = [];
82
+
83
+ for (const [, candidates] of byTopic) {
84
+ if (candidates.length === 1) {
85
+ winners.push({ ...candidates[0], _decision: 'auto', _rationale: null });
86
+ continue;
87
+ }
88
+
89
+ // Sort: specificity desc → score desc → stars desc
90
+ const sorted = [...candidates].sort((a, b) => {
91
+ const ma = getMeta(a), mb = getMeta(b);
92
+ if (mb.specificity !== ma.specificity) return mb.specificity - ma.specificity;
93
+ if ((b.score ?? 0) !== (a.score ?? 0)) return (b.score ?? 0) - (a.score ?? 0);
94
+ return (mb.stars ?? 0) - (ma.stars ?? 0);
95
+ });
96
+
97
+ const best = sorted[0];
98
+ const bestMeta = getMeta(best);
99
+ let bestAdded = false;
100
+
101
+ for (const runner of sorted.slice(1)) {
102
+ const runnerMeta = getMeta(runner);
103
+ const dSpec = bestMeta.specificity - runnerMeta.specificity;
104
+ const dScore = (best.score ?? 0) - (runner.score ?? 0);
105
+
106
+ if (dSpec >= 2 || dScore >= 0.2) {
107
+ // Clear winner — auto-resolve silently
108
+ const reason = dSpec >= 2
109
+ ? `more specific (${bestMeta.specificity} vs ${runnerMeta.specificity})`
110
+ : `higher relevance (${(best.score ?? 0).toFixed(2)} vs ${(runner.score ?? 0).toFixed(2)})`;
111
+
112
+ if (!bestAdded) {
113
+ winners.push({
114
+ ...best,
115
+ _decision: 'auto',
116
+ _rationale: `chosen over \`${getName(runner)}\` — ${reason}`,
117
+ });
118
+ bestAdded = true;
119
+ }
120
+ suppressed.push({ ...runner, _decision: 'suppressed', _rationale: `\`${getName(best)}\` preferred — ${reason}` });
121
+ } else {
122
+ // Genuine conflict — escalate to human
123
+ if (!conflicts.find(c => c.options.some(o => o.name === getName(best)))) {
124
+ conflicts.push({
125
+ topic: bestMeta.topic,
126
+ options: sorted.map(s => ({
127
+ name: getName(s),
128
+ specificity: getMeta(s).specificity,
129
+ score: s.score ?? null,
130
+ })),
131
+ message:
132
+ `\`${getName(best)}\` vs \`${getName(runner)}\` — both equally applicable` +
133
+ ` (specificity ${bestMeta.specificity} vs ${runnerMeta.specificity}).` +
134
+ ` Which should guide this decision?`,
135
+ });
136
+ }
137
+ }
138
+ }
139
+
140
+ // Add best as winner if no conflict was raised for it
141
+ if (!bestAdded && !conflicts.some(c => c.options.some(o => o.name === getName(best)))) {
142
+ winners.push({ ...best, _decision: 'auto', _rationale: null });
143
+ }
144
+ }
145
+
146
+ return { winners, suppressed, conflicts };
147
+ }
148
+ }
@@ -0,0 +1,167 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const CONTEXT7_API = 'https://context7.com/api';
5
+ const TIMEOUT_MS = 10000;
6
+
7
+ export class Context7Connector {
8
+ constructor(opts = {}) {
9
+ this.apiKey = opts.apiKey ?? process.env.CONTEXT7_API_KEY;
10
+ }
11
+
12
+ /**
13
+ * Context7 works without an API key (rate-limited).
14
+ * With a key, higher rate limits apply.
15
+ * @returns {{ ok: boolean }}
16
+ */
17
+ checkAuth() {
18
+ return { ok: true };
19
+ }
20
+
21
+ /**
22
+ * Search for a library by name.
23
+ * @param {string} libraryName - e.g., "next", "react", "stripe"
24
+ * @param {string} [query] - optional query for relevance ranking
25
+ * @returns {Promise<Array<{ id: string, name: string, description: string, versions?: string[] }> | null>}
26
+ */
27
+ async searchLibrary(libraryName, query) {
28
+ const params = new URLSearchParams({
29
+ libraryName,
30
+ query: query ?? libraryName,
31
+ });
32
+ const data = await this._apiGet(`/v2/libs/search?${params}`);
33
+ if (!data?.results) return null;
34
+ return data.results.map(r => ({
35
+ id: r.id ?? r.libraryId,
36
+ name: r.name ?? r.title ?? libraryName,
37
+ description: r.description ?? '',
38
+ versions: r.versions ?? [],
39
+ totalSnippets: r.totalSnippets ?? r.snippetCount ?? 0,
40
+ }));
41
+ }
42
+
43
+ /**
44
+ * Fetch documentation for a library and save as markdown files.
45
+ * @param {string} libraryId - Context7 library ID (e.g., "/vercel/next.js")
46
+ * @param {string} query - what to search for in the docs
47
+ * @param {string} outputDir - where to save markdown
48
+ * @returns {Promise<{ pageCount: number }>}
49
+ */
50
+ async fetchDocs(libraryId, query, outputDir) {
51
+ fs.mkdirSync(outputDir, { recursive: true });
52
+
53
+ // Request text format — returns rich markdown with code examples.
54
+ // JSON format returns a thin parsed version with less content.
55
+ const params = new URLSearchParams({ libraryId, query });
56
+ const text = await this._apiGetText(`/v2/context?${params}`);
57
+
58
+ if (!text || text.trim().length < 50) return { pageCount: 0 };
59
+
60
+ // Split by Context7's section separator into individual docs
61
+ const sections = text.split(/^-{20,}$/m).filter(s => s.trim());
62
+
63
+ if (sections.length === 0) {
64
+ // Single doc — save as-is
65
+ fs.writeFileSync(path.join(outputDir, 'docs.md'), text.trim() + '\n');
66
+ return { pageCount: 1 };
67
+ }
68
+
69
+ try {
70
+ let count = 0;
71
+ for (const section of sections) {
72
+ const trimmed = section.trim();
73
+ if (!trimmed) continue;
74
+ // Extract title from first heading or first line
75
+ const titleMatch = trimmed.match(/^###?\s+(.+)/m);
76
+ const title = titleMatch ? titleMatch[1].slice(0, 80) : `section-${count}`;
77
+ const filename = this._sanitize(title) + '.md';
78
+ fs.writeFileSync(path.join(outputDir, filename), trimmed + '\n');
79
+ count++;
80
+ }
81
+ return { pageCount: count };
82
+ } catch (err) {
83
+ fs.rmSync(outputDir, { recursive: true, force: true });
84
+ throw err;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Resolve a package name to a Context7 library and fetch its docs.
90
+ * Convenience method combining searchLibrary + fetchDocs.
91
+ * @param {string} packageName - e.g., "next", "react"
92
+ * @param {string} outputDir
93
+ * @param {string} [query] - what aspect to fetch docs for
94
+ * @returns {Promise<{ resolved: boolean, libraryId?: string, pageCount: number }>}
95
+ */
96
+ async resolveAndFetch(packageName, outputDir, query) {
97
+ const libraries = await this.searchLibrary(
98
+ packageName,
99
+ query ?? `${packageName} API documentation`,
100
+ );
101
+ if (!libraries || libraries.length === 0) {
102
+ return { resolved: false, pageCount: 0 };
103
+ }
104
+
105
+ const best = libraries[0];
106
+ const result = await this.fetchDocs(
107
+ best.id,
108
+ query ?? `${packageName} usage guide API`,
109
+ outputDir,
110
+ );
111
+
112
+ return { resolved: true, libraryId: best.id, pageCount: result.pageCount };
113
+ }
114
+
115
+ /** GET request to Context7 API. */
116
+ async _apiGet(endpoint) {
117
+ const controller = new AbortController();
118
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
119
+ try {
120
+ const res = await fetch(`${CONTEXT7_API}${endpoint}`, {
121
+ signal: controller.signal,
122
+ headers: {
123
+ ...(this.apiKey ? { 'Authorization': `Bearer ${this.apiKey}` } : {}),
124
+ 'X-Context7-Source': 'booklib',
125
+ 'User-Agent': 'BookLib/1.0',
126
+ },
127
+ });
128
+ if (!res.ok) return null;
129
+ return await res.json();
130
+ } catch {
131
+ return null;
132
+ } finally {
133
+ clearTimeout(timeout);
134
+ }
135
+ }
136
+
137
+ /** GET request returning raw text (for /v2/context text format). */
138
+ async _apiGetText(endpoint) {
139
+ const controller = new AbortController();
140
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
141
+ try {
142
+ const res = await fetch(`${CONTEXT7_API}${endpoint}`, {
143
+ signal: controller.signal,
144
+ headers: {
145
+ ...(this.apiKey ? { 'Authorization': `Bearer ${this.apiKey}` } : {}),
146
+ 'X-Context7-Source': 'booklib',
147
+ 'User-Agent': 'BookLib/3.0',
148
+ },
149
+ });
150
+ if (!res.ok) return null;
151
+ return await res.text();
152
+ } catch {
153
+ return null;
154
+ } finally {
155
+ clearTimeout(timeout);
156
+ }
157
+ }
158
+
159
+ /** Sanitize a string for use as a filename. */
160
+ _sanitize(str) {
161
+ return str
162
+ .replace(/[/\\:*?"<>|]/g, '_')
163
+ .replace(/\s+/g, '-')
164
+ .slice(0, 80)
165
+ .toLowerCase();
166
+ }
167
+ }
@@ -0,0 +1,223 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const RATE_MS = 1000;
6
+
7
+ export class GitHubConnector {
8
+ constructor(opts = {}) {
9
+ this.rateMs = opts.rateMs ?? RATE_MS;
10
+ }
11
+
12
+ /**
13
+ * Check if gh CLI is installed and authenticated.
14
+ * @returns {{ ok: boolean, error?: string }}
15
+ */
16
+ checkAuth() {
17
+ try {
18
+ execFileSync('gh', ['auth', 'status'], { stdio: 'pipe' });
19
+ return { ok: true };
20
+ } catch {
21
+ return {
22
+ ok: false,
23
+ error: 'gh CLI not authenticated. Install: https://cli.github.com then run: gh auth login',
24
+ };
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Fetch releases from a GitHub repo and save as markdown.
30
+ * @param {string} repo - owner/repo format
31
+ * @param {string} outputDir - where to save markdown files
32
+ * @param {object} [opts]
33
+ * @param {number} [opts.limit=30] - max releases to fetch
34
+ * @param {string} [opts.since] - ISO date, only fetch releases after this
35
+ * @returns {Promise<{ pageCount: number, releases: string[] }>}
36
+ */
37
+ async fetchReleases(repo, outputDir, opts = {}) {
38
+ const { limit = 30, since } = opts;
39
+ this._validateRepo(repo);
40
+ fs.mkdirSync(outputDir, { recursive: true });
41
+
42
+ const data = this._ghApi(`/repos/${repo}/releases?per_page=${Math.min(limit, 100)}`);
43
+ let releases;
44
+ try {
45
+ releases = JSON.parse(data);
46
+ } catch {
47
+ return { pageCount: 0, releases: [] };
48
+ }
49
+
50
+ if (!Array.isArray(releases)) return { pageCount: 0, releases: [] };
51
+
52
+ const saved = [];
53
+ for (const release of releases) {
54
+ if (since && new Date(release.published_at) <= new Date(since)) continue;
55
+
56
+ const tag = release.tag_name.replace(/[/\\:*?"<>|]/g, '_');
57
+ const filename = `release-${tag}.md`;
58
+ const content = this._formatRelease(release);
59
+ fs.writeFileSync(path.join(outputDir, filename), content);
60
+ saved.push(filename);
61
+ }
62
+
63
+ return { pageCount: saved.length, releases: saved };
64
+ }
65
+
66
+ /**
67
+ * Fetch wiki pages by cloning the wiki git repo.
68
+ * GitHub wikis live at <repo>.wiki.git — no REST API endpoint exists.
69
+ * @param {string} repo - owner/repo format
70
+ * @param {string} outputDir
71
+ * @returns {Promise<{ pageCount: number }>}
72
+ */
73
+ async fetchWiki(repo, outputDir) {
74
+ this._validateRepo(repo);
75
+ fs.mkdirSync(outputDir, { recursive: true });
76
+
77
+ const wikiUrl = `https://github.com/${repo}.wiki.git`;
78
+ try {
79
+ // Array form prevents shell injection via outputDir
80
+ execFileSync('git', ['clone', '--depth', '1', wikiUrl, outputDir], {
81
+ stdio: 'pipe',
82
+ timeout: 30000,
83
+ });
84
+ } catch (err) {
85
+ const msg = err.stderr?.toString() ?? '';
86
+ if (msg.includes('not found') || msg.includes('not exist')) {
87
+ return { pageCount: 0 };
88
+ }
89
+ throw new Error(`Wiki clone failed: ${msg.slice(0, 200)}`);
90
+ }
91
+
92
+ // Remove .git directory — we only want the content files
93
+ const gitDir = path.join(outputDir, '.git');
94
+ if (fs.existsSync(gitDir)) {
95
+ fs.rmSync(gitDir, { recursive: true, force: true });
96
+ }
97
+
98
+ const files = fs.readdirSync(outputDir).filter(f => f.endsWith('.md'));
99
+ return { pageCount: files.length };
100
+ }
101
+
102
+ /**
103
+ * Fetch discussion threads via GraphQL API.
104
+ * @param {string} repo - owner/repo format
105
+ * @param {string} outputDir
106
+ * @param {object} [opts]
107
+ * @param {number} [opts.limit=20] - max discussions
108
+ * @param {string} [opts.category] - filter by category name
109
+ * @returns {Promise<{ pageCount: number }>}
110
+ */
111
+ async fetchDiscussions(repo, outputDir, opts = {}) {
112
+ const { limit = 20, category } = opts;
113
+ this._validateRepo(repo);
114
+ fs.mkdirSync(outputDir, { recursive: true });
115
+
116
+ const [owner, name] = repo.split('/');
117
+ const count = Math.min(limit, 100);
118
+
119
+ // Parameterized GraphQL query — variables passed via -f to prevent injection
120
+ const query = `query($owner: String!, $name: String!, $count: Int!) {
121
+ repository(owner: $owner, name: $name) {
122
+ discussions(first: $count, orderBy: {field: UPDATED_AT, direction: DESC}) {
123
+ nodes {
124
+ number title body createdAt updatedAt
125
+ category { name }
126
+ answer { body author { login } }
127
+ comments(first: 10) { nodes { body author { login } } }
128
+ }
129
+ }
130
+ }
131
+ }`;
132
+
133
+ let data;
134
+ try {
135
+ const raw = execFileSync('gh', [
136
+ 'api', 'graphql',
137
+ '-f', `query=${query}`,
138
+ '-f', `owner=${owner}`,
139
+ '-f', `name=${name}`,
140
+ '-F', `count=${count}`,
141
+ ], { stdio: 'pipe', timeout: 30000 });
142
+ data = JSON.parse(raw.toString());
143
+ } catch (err) {
144
+ const msg = err.stderr?.toString().slice(0, 200) ?? err.message;
145
+ console.error(`GitHub discussions fetch failed: ${msg}`);
146
+ return { pageCount: 0 };
147
+ }
148
+
149
+ const discussions = data?.data?.repository?.discussions?.nodes ?? [];
150
+ let saved = 0;
151
+
152
+ for (const disc of discussions) {
153
+ if (category && disc.category?.name !== category) continue;
154
+
155
+ const filename = `discussion-${disc.number}.md`;
156
+ const content = this._formatDiscussion(disc);
157
+ fs.writeFileSync(path.join(outputDir, filename), content);
158
+ saved++;
159
+ }
160
+
161
+ return { pageCount: saved };
162
+ }
163
+
164
+ /** Format a release object as markdown. */
165
+ _formatRelease(release) {
166
+ const lines = [
167
+ `# ${release.name || release.tag_name}`,
168
+ '',
169
+ `**Tag:** ${release.tag_name}`,
170
+ `**Published:** ${release.published_at?.split('T')[0] ?? 'unknown'}`,
171
+ release.prerelease ? '**Pre-release**' : '',
172
+ '',
173
+ release.body ?? '_No release notes._',
174
+ ];
175
+ return lines.filter(l => l !== '').join('\n') + '\n';
176
+ }
177
+
178
+ /** Format a discussion as markdown with comments. */
179
+ _formatDiscussion(disc) {
180
+ const lines = [
181
+ `# ${disc.title}`,
182
+ '',
183
+ `**Discussion #${disc.number}** — ${disc.category?.name ?? 'General'}`,
184
+ `**Created:** ${disc.createdAt?.split('T')[0] ?? 'unknown'}`,
185
+ '',
186
+ disc.body ?? '',
187
+ ];
188
+
189
+ if (disc.answer) {
190
+ lines.push('', '---', `## Accepted Answer (by ${disc.answer.author?.login ?? 'unknown'})`, '', disc.answer.body);
191
+ }
192
+
193
+ const comments = disc.comments?.nodes ?? [];
194
+ if (comments.length > 0) {
195
+ lines.push('', '---', '## Comments', '');
196
+ for (const c of comments) {
197
+ lines.push(`**${c.author?.login ?? 'unknown'}:**`, c.body, '');
198
+ }
199
+ }
200
+
201
+ return lines.join('\n') + '\n';
202
+ }
203
+
204
+ /** Validate repo format is owner/name. */
205
+ _validateRepo(repo) {
206
+ if (!repo || !/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(repo)) {
207
+ throw new Error(`Invalid repo format: "${repo}". Use owner/repo (e.g., facebook/react)`);
208
+ }
209
+ }
210
+
211
+ /** Call gh api and return stdout — array form prevents shell injection. */
212
+ _ghApi(endpoint) {
213
+ try {
214
+ return execFileSync('gh', ['api', endpoint], {
215
+ stdio: 'pipe',
216
+ timeout: 15000,
217
+ maxBuffer: 10 * 1024 * 1024,
218
+ }).toString();
219
+ } catch (err) {
220
+ throw new Error(`GitHub API error: ${err.stderr?.toString().slice(0, 200) ?? err.message}`);
221
+ }
222
+ }
223
+ }
@@ -0,0 +1,120 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Local filesystem connector for BookLib.
6
+ * Walks directories, applies include/exclude filters, tracks mtimes for
7
+ * incremental re-indexing, and optionally watches for changes.
8
+ */
9
+ export class LocalConnector {
10
+ constructor(opts = {}) {
11
+ this.include = opts.include ?? ['*.md', '*.mdx', '*.txt', '*.rst', '*.adoc'];
12
+ this.exclude = opts.exclude ?? ['node_modules', '.git', '.booklib'];
13
+ }
14
+
15
+ /**
16
+ * List files matching include/exclude filters.
17
+ * @param {string} dirPath - Absolute directory path to walk.
18
+ * @returns {string[]} Sorted absolute file paths.
19
+ */
20
+ listFiles(dirPath) {
21
+ const results = [];
22
+ this._walk(dirPath, dirPath, results);
23
+ return results.sort();
24
+ }
25
+
26
+ /**
27
+ * Get mtimes for all matching files, keyed by relative path.
28
+ * @param {string} dirPath
29
+ * @returns {Object} { [relativePath]: mtimeMs }
30
+ */
31
+ getFileMtimes(dirPath) {
32
+ const files = this.listFiles(dirPath);
33
+ const mtimes = {};
34
+ for (const f of files) {
35
+ mtimes[path.relative(dirPath, f)] = fs.statSync(f).mtimeMs;
36
+ }
37
+ return mtimes;
38
+ }
39
+
40
+ /**
41
+ * Find files that changed since last index.
42
+ * @param {string} dirPath
43
+ * @param {Object} previousMtimes - { [relativePath]: mtimeMs } from last index.
44
+ * @returns {{ changed: string[], removed: string[], currentMtimes: Object }}
45
+ */
46
+ findChanges(dirPath, previousMtimes = {}) {
47
+ const currentMtimes = this.getFileMtimes(dirPath);
48
+ const changed = [];
49
+ const removed = [];
50
+
51
+ for (const [rel, mtime] of Object.entries(currentMtimes)) {
52
+ if (!previousMtimes[rel] || previousMtimes[rel] < mtime) {
53
+ changed.push(path.join(dirPath, rel));
54
+ }
55
+ }
56
+
57
+ for (const rel of Object.keys(previousMtimes)) {
58
+ if (!currentMtimes[rel]) {
59
+ removed.push(rel);
60
+ }
61
+ }
62
+
63
+ return { changed, removed, currentMtimes };
64
+ }
65
+
66
+ /**
67
+ * Watch directory for changes, calling callback on each matching file event.
68
+ * @param {string} dirPath
69
+ * @param {function} onChange - Called with (eventType, filename).
70
+ * @returns {fs.FSWatcher}
71
+ */
72
+ watch(dirPath, onChange) {
73
+ console.log(`Watching ${dirPath} for changes (Ctrl+C to stop)...`);
74
+ let debounceTimer = null;
75
+ const pending = new Set();
76
+
77
+ const watcher = fs.watch(dirPath, { recursive: true }, (eventType, filename) => {
78
+ if (!filename) return;
79
+ if (!this._matchesFilters(filename)) return;
80
+ pending.add(filename);
81
+
82
+ // Debounce: wait 500ms after last event before calling onChange
83
+ clearTimeout(debounceTimer);
84
+ debounceTimer = setTimeout(() => {
85
+ const files = [...pending];
86
+ pending.clear();
87
+ onChange(eventType, files);
88
+ }, 500);
89
+ });
90
+ return watcher;
91
+ }
92
+
93
+ /**
94
+ * Check if a filename matches include patterns and does not match exclude.
95
+ * @param {string} filename
96
+ * @returns {boolean}
97
+ */
98
+ _matchesFilters(filename) {
99
+ for (const excl of this.exclude) {
100
+ if (filename.includes(excl)) return false;
101
+ }
102
+ const ext = path.extname(filename).toLowerCase();
103
+ const includeExts = this.include.map(p => p.startsWith('*.') ? p.slice(1) : p);
104
+ return includeExts.some(e => ext === e);
105
+ }
106
+
107
+ /** Recursively walk a directory, collecting matching files. */
108
+ _walk(currentDir, rootDir, results) {
109
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
110
+ for (const entry of entries) {
111
+ const fullPath = path.join(currentDir, entry.name);
112
+ if (entry.isDirectory()) {
113
+ if (this.exclude.some(excl => entry.name === excl)) continue;
114
+ this._walk(fullPath, rootDir, results);
115
+ } else if (entry.isFile() && this._matchesFilters(entry.name)) {
116
+ results.push(fullPath);
117
+ }
118
+ }
119
+ }
120
+ }