@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,177 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ listNodes, loadNode, parseNodeFrontmatter,
5
+ appendEdge, loadEdges,
6
+ } from './graph.js';
7
+
8
+ /**
9
+ * Auto-links a newly created knowledge node to:
10
+ * 1. Matching project components (by keyword in title/content)
11
+ * 2. Related existing knowledge nodes (by semantic similarity via search)
12
+ *
13
+ * @param {object} opts
14
+ * @param {string} opts.nodeId - the new node's ID
15
+ * @param {string} opts.title - node title
16
+ * @param {string} [opts.content] - node content
17
+ * @param {string[]} [opts.tags] - node tags
18
+ * @param {string} [opts.nodesDir] - knowledge nodes directory
19
+ * @param {string} [opts.graphFile] - graph.jsonl path
20
+ * @returns {Promise<Array<{from: string, to: string, type: string, reason: string}>>}
21
+ */
22
+ export async function autoLink({ nodeId, title, content = '', tags = [], nodesDir, graphFile }) {
23
+ const links = [];
24
+ const existingEdges = loadEdges({ graphFile });
25
+ const allNodeIds = listNodes({ nodesDir });
26
+ const today = new Date().toISOString().split('T')[0];
27
+
28
+ // Step 1: Component matching — find components whose name appears in title/content
29
+ const searchText = `${title} ${content} ${tags.join(' ')}`.toLowerCase();
30
+
31
+ for (const id of allNodeIds) {
32
+ if (id === nodeId) continue;
33
+ const raw = loadNode(id, { nodesDir });
34
+ if (!raw) continue;
35
+ const parsed = parseNodeFrontmatter(raw);
36
+
37
+ // Only match against components
38
+ if (parsed.type !== 'component') continue;
39
+
40
+ const componentName = (parsed.title ?? '').toLowerCase();
41
+ if (!componentName) continue;
42
+
43
+ // Check if component name (or significant words from it) appears in the new node's text
44
+ const nameWords = componentName.split(/[\s\-_]+/).filter(w => w.length > 2);
45
+ const matches = nameWords.some(word => searchText.includes(word));
46
+
47
+ if (matches && !edgeExists(existingEdges, nodeId, id)) {
48
+ const edge = { from: nodeId, to: id, type: 'applies-to', weight: 1.0, created: today };
49
+ appendEdge(edge, { graphFile });
50
+ links.push({ from: nodeId, to: id, type: 'applies-to', reason: `component name match: "${parsed.title}"` });
51
+ }
52
+ }
53
+
54
+ // Step 2: Knowledge matching — find related existing notes by title similarity
55
+ // Simple approach: check if significant words from the new title appear in existing node titles
56
+ const titleWords = title.toLowerCase().split(/[\s\-_]+/).filter(w => w.length > 3);
57
+
58
+ for (const id of allNodeIds) {
59
+ if (id === nodeId) continue;
60
+ if (links.length >= 3) break; // cap at 3 auto-links
61
+
62
+ const raw = loadNode(id, { nodesDir });
63
+ if (!raw) continue;
64
+ const parsed = parseNodeFrontmatter(raw);
65
+
66
+ // Skip components (already handled above)
67
+ if (parsed.type === 'component') continue;
68
+
69
+ const existingTitle = (parsed.title ?? '').toLowerCase();
70
+ if (!existingTitle) continue;
71
+
72
+ // Check for word overlap between titles
73
+ const existingWords = existingTitle.split(/[\s\-_]+/).filter(w => w.length > 3);
74
+ const overlap = titleWords.filter(w => existingWords.includes(w));
75
+
76
+ if (overlap.length >= 1 && !edgeExists(existingEdges, nodeId, id)) {
77
+ const edge = { from: nodeId, to: id, type: 'see-also', weight: 1.0, created: today };
78
+ appendEdge(edge, { graphFile });
79
+ links.push({ from: nodeId, to: id, type: 'see-also', reason: `related knowledge: "${parsed.title}"` });
80
+ }
81
+ }
82
+
83
+ return links;
84
+ }
85
+
86
+ /**
87
+ * Reverse auto-link: when a new component is created, find existing notes
88
+ * that mention the component name and link them.
89
+ */
90
+ export async function autoLinkReverse({ componentId, componentTitle, nodesDir, graphFile }) {
91
+ const links = [];
92
+ const existingEdges = loadEdges({ graphFile });
93
+ const allNodeIds = listNodes({ nodesDir });
94
+ const today = new Date().toISOString().split('T')[0];
95
+ const nameWords = componentTitle.toLowerCase().split(/[\s\-_]+/).filter(w => w.length > 2);
96
+
97
+ for (const id of allNodeIds) {
98
+ if (id === componentId) continue;
99
+ const raw = loadNode(id, { nodesDir });
100
+ if (!raw) continue;
101
+ const parsed = parseNodeFrontmatter(raw);
102
+ if (parsed.type === 'component') continue;
103
+
104
+ const nodeText = `${parsed.title ?? ''} ${parsed.body ?? ''}`.toLowerCase();
105
+ const matches = nameWords.some(word => nodeText.includes(word));
106
+
107
+ if (matches && !edgeExists(existingEdges, id, componentId)) {
108
+ const edge = { from: id, to: componentId, type: 'applies-to', weight: 1.0, created: today };
109
+ appendEdge(edge, { graphFile });
110
+ links.push({ from: id, to: componentId, type: 'applies-to', reason: `mentions component: "${componentTitle}"` });
111
+ }
112
+ }
113
+
114
+ return links;
115
+ }
116
+
117
+ /**
118
+ * Creates see-also edges between knowledge nodes and skills when their tags overlap.
119
+ * Called during `booklib index` after indexing knowledge nodes.
120
+ *
121
+ * @param {object} opts
122
+ * @param {Array<{id: string, title: string, tags: string[]}>} opts.knowledgeNodes
123
+ * @param {Array<{name: string, tags: string[]}>} opts.skillTags
124
+ * @param {string} [opts.graphFile]
125
+ * @returns {Promise<Array<{from: string, to: string, type: string, reason: string}>>}
126
+ */
127
+ export async function autoLinkSkills({ knowledgeNodes, skillTags, graphFile }) {
128
+ const links = [];
129
+ const existingEdges = loadEdges({ graphFile });
130
+ const today = new Date().toISOString().split('T')[0];
131
+
132
+ // For each knowledge node, check tag overlap with each skill
133
+ for (const node of knowledgeNodes) {
134
+ const nodeTagSet = new Set(node.tags || []);
135
+
136
+ // Extract significant words (>3 chars) from title
137
+ const titleWords = node.title.toLowerCase().split(/[\s\-_]+/).filter(w => w.length > 3);
138
+ for (const word of titleWords) {
139
+ nodeTagSet.add(word);
140
+ }
141
+
142
+ // Check overlap with each skill
143
+ for (const skill of skillTags) {
144
+ const skillTagSet = new Set(skill.tags || []);
145
+
146
+ // Count overlapping tags
147
+ const overlap = Array.from(nodeTagSet).filter(tag => skillTagSet.has(tag));
148
+
149
+ // Create edge if ≥2 tags overlap and edge doesn't already exist
150
+ if (overlap.length >= 2 && !edgeExists(existingEdges, node.id, skill.name)) {
151
+ const edge = {
152
+ from: node.id,
153
+ to: skill.name,
154
+ type: 'see-also',
155
+ weight: 1.0,
156
+ created: today,
157
+ };
158
+ appendEdge(edge, { graphFile });
159
+ links.push({
160
+ from: node.id,
161
+ to: skill.name,
162
+ type: 'see-also',
163
+ reason: `tag overlap: [${overlap.join(', ')}]`,
164
+ });
165
+ }
166
+ }
167
+ }
168
+
169
+ return links;
170
+ }
171
+
172
+ function edgeExists(edges, from, to) {
173
+ return edges.some(e =>
174
+ (e.from === from && e.to === to) ||
175
+ (e.from === to && e.to === from)
176
+ );
177
+ }
@@ -0,0 +1,178 @@
1
+ import fs from 'node:fs';
2
+
3
+ const K1 = 1.5;
4
+ const B = 0.75;
5
+ const MIN_TOKEN_LENGTH = 2;
6
+
7
+ /**
8
+ * Tokenizes text into lowercase alphanumeric terms of at least 2 characters.
9
+ * @param {string} text
10
+ * @returns {string[]}
11
+ */
12
+ function tokenize(text) {
13
+ return text
14
+ .toLowerCase()
15
+ .split(/[^a-z0-9]+/)
16
+ .filter(token => token.length >= MIN_TOKEN_LENGTH);
17
+ }
18
+
19
+ /**
20
+ * Counts term frequencies in a token array.
21
+ * @param {string[]} tokens
22
+ * @returns {Map<string, number>}
23
+ */
24
+ function countTermFrequencies(tokens) {
25
+ const freq = new Map();
26
+ for (const token of tokens) {
27
+ freq.set(token, (freq.get(token) ?? 0) + 1);
28
+ }
29
+ return freq;
30
+ }
31
+
32
+ /**
33
+ * Computes BM25 IDF for a term.
34
+ * Formula: log((N - df + 0.5) / (df + 0.5) + 1)
35
+ * @param {number} docCount - Total number of documents
36
+ * @param {number} docFrequency - Number of documents containing the term
37
+ * @returns {number}
38
+ */
39
+ function computeIdf(docCount, docFrequency) {
40
+ return Math.log((docCount - docFrequency + 0.5) / (docFrequency + 0.5) + 1);
41
+ }
42
+
43
+ /**
44
+ * Scores a single document against a set of query terms using BM25.
45
+ * @param {Object} doc - Document with freq map and len
46
+ * @param {string[]} queryTerms
47
+ * @param {Map<string, number>} df - Document frequency map
48
+ * @param {number} docCount
49
+ * @param {number} avgDocLen
50
+ * @returns {number}
51
+ */
52
+ function scoreBm25(doc, queryTerms, df, docCount, avgDocLen) {
53
+ let score = 0;
54
+ for (const term of queryTerms) {
55
+ const termFreq = doc.freq[term] ?? 0;
56
+ if (termFreq === 0) continue;
57
+
58
+ const docFrequency = df[term] ?? 0;
59
+ const idf = computeIdf(docCount, docFrequency);
60
+ const normalizedTf =
61
+ (termFreq * (K1 + 1)) /
62
+ (termFreq + K1 * (1 - B + B * (doc.len / avgDocLen)));
63
+
64
+ score += idf * normalizedTf;
65
+ }
66
+ return score;
67
+ }
68
+
69
+ /**
70
+ * BM25 full-text search index.
71
+ *
72
+ * Supports incremental document addition, JSON persistence, and
73
+ * Robertson BM25 ranking.
74
+ */
75
+ export class BM25Index {
76
+ constructor() {
77
+ this._docs = [];
78
+ this._df = {};
79
+ this._avgLen = 0;
80
+ this._totalLen = 0;
81
+ }
82
+
83
+ /**
84
+ * Builds the index from scratch from an array of chunks.
85
+ * @param {{ text: string, metadata: object }[]} chunks
86
+ */
87
+ build(chunks) {
88
+ this._docs = [];
89
+ this._df = {};
90
+ this._avgLen = 0;
91
+ this._totalLen = 0;
92
+
93
+ for (const chunk of chunks) {
94
+ this._appendDoc(chunk);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Incrementally adds one document to the index, updating avgLen and df.
100
+ * @param {{ text: string, metadata: object }} chunk
101
+ */
102
+ add(chunk) {
103
+ this._appendDoc(chunk);
104
+ }
105
+
106
+ /**
107
+ * Searches the index and returns top-K results sorted by score descending.
108
+ * @param {string} query
109
+ * @param {number} topK
110
+ * @returns {{ score: number, text: string, metadata: object }[]}
111
+ */
112
+ search(query, topK = 20) {
113
+ if (this._docs.length === 0) return [];
114
+
115
+ const queryTerms = tokenize(query);
116
+ if (queryTerms.length === 0) return [];
117
+
118
+ const scoredDocs = this._docs.map(doc => ({
119
+ score: scoreBm25(doc, queryTerms, this._df, this._docs.length, this._avgLen),
120
+ text: doc.text,
121
+ metadata: doc.metadata,
122
+ }));
123
+
124
+ const matchingDocs = scoredDocs.filter(r => r.score > 0);
125
+ return matchingDocs
126
+ .sort((a, b) => b.score - a.score)
127
+ .slice(0, topK);
128
+ }
129
+
130
+ /**
131
+ * Persists the index to a JSON file.
132
+ * @param {string} filePath
133
+ */
134
+ save(filePath) {
135
+ const serialized = JSON.stringify({
136
+ docs: this._docs,
137
+ df: this._df,
138
+ avgLen: this._avgLen,
139
+ });
140
+ fs.writeFileSync(filePath, serialized, 'utf8');
141
+ }
142
+
143
+ /**
144
+ * Restores a BM25Index instance from a JSON file.
145
+ * @param {string} filePath
146
+ * @returns {BM25Index}
147
+ */
148
+ static load(filePath) {
149
+ const raw = fs.readFileSync(filePath, 'utf8');
150
+ const { docs, df, avgLen } = JSON.parse(raw);
151
+ const idx = new BM25Index();
152
+ idx._docs = docs;
153
+ idx._df = df;
154
+ idx._avgLen = avgLen;
155
+ idx._totalLen = docs.reduce((sum, d) => sum + d.len, 0);
156
+ return idx;
157
+ }
158
+
159
+ /**
160
+ * Internal: adds one document and updates df and avgLen.
161
+ * @param {{ text: string, metadata: object }} chunk
162
+ */
163
+ _appendDoc(chunk) {
164
+ const tokens = tokenize(chunk.text);
165
+ const freqMap = countTermFrequencies(tokens);
166
+ const freqObj = Object.fromEntries(freqMap);
167
+
168
+ for (const term of freqMap.keys()) {
169
+ this._df[term] = (this._df[term] ?? 0) + 1;
170
+ }
171
+
172
+ const len = tokens.length;
173
+ this._docs.push({ text: chunk.text, metadata: chunk.metadata, freq: freqObj, len });
174
+
175
+ this._totalLen += len;
176
+ this._avgLen = this._totalLen / this._docs.length;
177
+ }
178
+ }
@@ -0,0 +1,120 @@
1
+ // lib/engine/capture.js
2
+ import { spawnSync } from 'node:child_process';
3
+ import { writeFileSync, readFileSync, unlinkSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ // ── AI prompt builders (exported for testing) ─────────────────────────────────
8
+
9
+ /** Returns the prompt used to ask Claude to structure raw dictation into a clean note. */
10
+ export function buildDictatePrompt(rawText) {
11
+ return `You are a knowledge management assistant. Structure the following raw notes into a clean markdown knowledge note.
12
+
13
+ Raw input:
14
+ ${rawText}
15
+
16
+ Return ONLY valid YAML frontmatter + markdown. The frontmatter must include:
17
+ - title: (concise, descriptive title extracted or inferred from the content)
18
+ - tags: (2-5 relevant tags as a YAML list)
19
+ - type: note
20
+
21
+ Fix grammar and typos. Preserve all meaning. Do not add information not present in the input.
22
+ Return nothing except the markdown document starting with ---.`;
23
+ }
24
+
25
+ /** Returns the prompt used to summarize a conversation transcript into a structured note. */
26
+ export function buildSummarizePrompt(transcript, title = '') {
27
+ const titleLine = title ? `Title: ${title}\n\n` : '';
28
+ return `You are a knowledge management assistant. Summarize this AI conversation into a structured knowledge note.
29
+
30
+ ${titleLine}Conversation:
31
+ ${transcript}
32
+
33
+ Return ONLY a markdown document starting with ---. Include this frontmatter:
34
+ - title: (descriptive title${title ? ` — use "${title}" as basis` : ''})
35
+ - tags: (2-5 relevant tags)
36
+ - type: note
37
+
38
+ And these sections in the body:
39
+ ## Key Decisions
40
+ (bullet list of decisions made)
41
+
42
+ ## Findings
43
+ (bullet list of key findings or conclusions)
44
+
45
+ ## Context
46
+ (1-2 sentences of background)
47
+
48
+ Then append the full transcript inside a details element:
49
+ <details>
50
+ <summary>Full conversation transcript</summary>
51
+
52
+ ${transcript}
53
+
54
+ </details>`;
55
+ }
56
+
57
+ // ── AI call (requires ANTHROPIC_API_KEY env var) ──────────────────────────────
58
+
59
+ /** Calls claude-haiku via the Anthropic Messages API. Requires ANTHROPIC_API_KEY. */
60
+ export async function callAnthropicAPI(prompt) {
61
+ const apiKey = process.env.ANTHROPIC_API_KEY;
62
+ if (!apiKey) {
63
+ throw new Error('ANTHROPIC_API_KEY not set. Export it or add to .env.local');
64
+ }
65
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ 'x-api-key': apiKey,
70
+ 'anthropic-version': '2023-06-01',
71
+ },
72
+ body: JSON.stringify({
73
+ model: 'claude-haiku-4-5-20251001',
74
+ max_tokens: 2048,
75
+ messages: [{ role: 'user', content: prompt }],
76
+ }),
77
+ });
78
+ if (!response.ok) {
79
+ const body = await response.text();
80
+ throw new Error(`Anthropic API ${response.status}: ${body}`);
81
+ }
82
+ const data = await response.json();
83
+ return data.content[0].text;
84
+ }
85
+
86
+ // ── Input helpers ─────────────────────────────────────────────────────────────
87
+
88
+ /** Opens $EDITOR (or vi) with optional initial content. Returns edited content. */
89
+ export function openEditor(initialContent = '') {
90
+ const tmpFile = join(tmpdir(), `booklib-edit-${Date.now()}.md`);
91
+ writeFileSync(tmpFile, initialContent, 'utf8');
92
+ const editor = process.env.EDITOR ?? 'vi';
93
+ spawnSync(editor, [tmpFile], { stdio: 'inherit' });
94
+ const content = readFileSync(tmpFile, 'utf8');
95
+ unlinkSync(tmpFile);
96
+ return content.trim();
97
+ }
98
+
99
+ /** Reads all of stdin when piped. Returns empty string when stdin is a TTY. */
100
+ export async function readStdin() {
101
+ if (process.stdin.isTTY) return '';
102
+ return new Promise(resolve => {
103
+ let data = '';
104
+ process.stdin.setEncoding('utf8');
105
+ process.stdin.on('data', chunk => { data += chunk; });
106
+ process.stdin.on('end', () => resolve(data.trim()));
107
+ });
108
+ }
109
+
110
+ /** Prompts user to type input interactively until Ctrl+D. */
111
+ export async function readInteractive(prompt = 'Type your note (Ctrl+D when done):') {
112
+ process.stdout.write(prompt + '\n');
113
+ return new Promise(resolve => {
114
+ let data = '';
115
+ process.stdin.setEncoding('utf8');
116
+ process.stdin.resume();
117
+ process.stdin.on('data', chunk => { data += chunk; });
118
+ process.stdin.on('end', () => resolve(data.trim()));
119
+ });
120
+ }