@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
package/bin/booklib.js ADDED
@@ -0,0 +1,2394 @@
1
+ #!/usr/bin/env node
2
+ // Suppress noisy ML model initialisation warnings from @huggingface/transformers
3
+ // The library uses console.warn for dtype/device messages — filter them here.
4
+ const _origWarn = console.warn.bind(console);
5
+ console.warn = (...args) => {
6
+ const msg = typeof args[0] === 'string' ? args[0] : '';
7
+ if (msg.includes('dtype not specified') || msg.includes('Using the default dtype')) return;
8
+ _origWarn(...args);
9
+ };
10
+
11
+ import path from 'path';
12
+ import fs from 'fs';
13
+ import os from 'os';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { createInterface } from 'node:readline';
16
+
17
+ const PACKAGE_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..');
18
+ const BUNDLED_SKILLS_DIR = path.join(PACKAGE_ROOT, 'skills');
19
+ import { BookLibIndexer } from '../lib/engine/indexer.js';
20
+ import { BookLibSearcher } from '../lib/engine/searcher.js';
21
+ import { BookLibHandoff } from '../lib/engine/handoff.js';
22
+ import { BookLibAuditor } from '../lib/engine/auditor.js';
23
+ import { BookLibRegistrySearcher } from '../lib/registry-searcher.js';
24
+ import { BookLibInstaller } from '../lib/installer.js';
25
+ import { BookLibScanner } from '../lib/engine/scanner.js';
26
+ import { BookLibSessionCoordinator } from '../lib/engine/session-coordinator.js';
27
+ import { BookLibSessionManager } from '../lib/engine/session-manager.js';
28
+ import { resolveBookLibPaths } from '../lib/paths.js';
29
+ import { SkillFetcher, RequiresConfirmationError, listInstalledSkillNames, countInstalledSlots } from '../lib/skill-fetcher.js';
30
+ import { runWizard } from '../lib/wizard/index.js';
31
+ import { SKILL_LIMIT } from '../lib/wizard/skill-recommender.js';
32
+ import {
33
+ generateNodeId, serializeNode, saveNode, loadNode,
34
+ listNodes, appendEdge, parseNodeFrontmatter, resolveKnowledgePaths,
35
+ resolveNodeRef, EDGE_TYPES, parseCaptureLinkArgs,
36
+ } from '../lib/engine/graph.js';
37
+ import { autoLink, autoLinkReverse } from '../lib/engine/auto-linker.js';
38
+ import { DiscoveryEngine } from '../lib/discovery-engine.js';
39
+ import { ProjectInitializer } from '../lib/project-initializer.js';
40
+ import { ContextBuilder } from '../lib/context-builder.js';
41
+ import {
42
+ buildDictatePrompt, buildSummarizePrompt, callAnthropicAPI,
43
+ openEditor, readStdin, readInteractive,
44
+ } from '../lib/engine/capture.js';
45
+ import { readUsage, summarize } from '../lib/doctor/usage-tracker.js';
46
+ import { installTrackingHook } from '../lib/doctor/hook-installer.js';
47
+ import { listAvailable as listAvailableRules, installRule as installRuleFn, status as rulesStatus } from '../lib/rules/rules-manager.js';
48
+ import { addCorrection, listCorrections, removeCorrection, levelFromMentions } from '../lib/engine/corrections.js';
49
+ import { WellKnownBuilder } from '../lib/well-known-builder.js';
50
+
51
+ /**
52
+ * Remove all BM25 and Vectra chunks belonging to a named source.
53
+ * Shared by the disconnect and refresh commands to avoid duplication.
54
+ */
55
+ async function removeSourceChunks(sourceName, bm25Path, indexer) {
56
+ // Remove BM25 chunks and rebuild index stats
57
+ if (fs.existsSync(bm25Path)) {
58
+ const { BM25Index: BM25 } = await import('../lib/engine/bm25-index.js');
59
+ const idx = BM25.load(bm25Path);
60
+ const before = idx._docs.length;
61
+ idx._docs = idx._docs.filter(d => d.metadata?.sourceName !== sourceName);
62
+ idx._df = {};
63
+ idx._totalLen = 0;
64
+ for (const doc of idx._docs) {
65
+ for (const term of Object.keys(doc.freq)) {
66
+ idx._df[term] = (idx._df[term] ?? 0) + 1;
67
+ }
68
+ idx._totalLen += doc.len;
69
+ }
70
+ idx._avgLen = idx._docs.length > 0 ? idx._totalLen / idx._docs.length : 0;
71
+ idx.save(bm25Path);
72
+ const removed = before - idx._docs.length;
73
+ if (removed > 0) console.log(`Removed ${removed} BM25 chunk(s).`);
74
+ }
75
+
76
+ // Remove matching Vectra vectors
77
+ try {
78
+ const items = await indexer.index.listItems();
79
+ let vectraRemoved = 0;
80
+ for (const item of items) {
81
+ if (item.metadata?.sourceName === sourceName) {
82
+ await indexer.index.deleteItem(item.id);
83
+ vectraRemoved++;
84
+ }
85
+ }
86
+ if (vectraRemoved > 0) console.log(`Removed ${vectraRemoved} vector chunk(s).`);
87
+ } catch {
88
+ // Vectra index may not exist yet
89
+ }
90
+ }
91
+
92
+ const args = process.argv.slice(2);
93
+ const command = args[0];
94
+
95
+ // Handle --version / -v before anything else
96
+ if (command === '--version' || command === '-v' || args.includes('--version')) {
97
+ const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
98
+ console.log(`booklib v${pkg.version}`);
99
+ process.exit(0);
100
+ }
101
+
102
+ function parseFlag(args, flag) {
103
+ const long = args.find(a => a.startsWith(`--${flag}=`))?.replace(`--${flag}=`, '');
104
+ if (long !== undefined) return long;
105
+ const idx = args.indexOf(`--${flag}`);
106
+ return idx !== -1 ? args[idx + 1] : null;
107
+ }
108
+
109
+ function parseInterval(str) {
110
+ const match = str.match(/^(\d+)(s|m|h)$/);
111
+ if (!match) throw new Error(`Invalid interval: "${str}". Use: 30s, 5m, 1h`);
112
+ const [, num, unit] = match;
113
+ const multipliers = { s: 1000, m: 60000, h: 3600000 };
114
+ return parseInt(num) * multipliers[unit];
115
+ }
116
+
117
+ function formatBytes(bytes) {
118
+ if (bytes < 1024) return `${bytes} B`;
119
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
120
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
121
+ }
122
+
123
+ /** Auto-index a freshly saved node so it's immediately searchable. Silently skips on error. */
124
+ async function autoIndexNode(filePath) {
125
+ const { nodesDir } = resolveKnowledgePaths();
126
+ try {
127
+ const indexer = new BookLibIndexer();
128
+ await indexer.indexNodeFile(filePath, nodesDir);
129
+ } catch {
130
+ // Index may not exist yet — user can run `booklib index` to build it
131
+ }
132
+ }
133
+
134
+
135
+ const TOOL_MENU = [
136
+ { num: 1, name: 'Claude Code', target: 'claude', file: 'CLAUDE.md' },
137
+ { num: 2, name: 'Cursor', target: 'cursor', file: '.cursor/rules/' },
138
+ { num: 3, name: 'Copilot', target: 'copilot', file: '.github/copilot-instructions.md' },
139
+ { num: 4, name: 'Gemini CLI', target: 'gemini', file: '.gemini/context.md' },
140
+ { num: 5, name: 'Codex', target: 'codex', file: 'AGENTS.md' },
141
+ { num: 6, name: 'Windsurf', target: 'windsurf', file: '.windsurfrules' },
142
+ { num: 7, name: 'Roo Code', target: 'roo-code', file: '.roo/rules/' },
143
+ { num: 8, name: 'OpenHands', target: 'openhands', file: '.openhands/instructions.md' },
144
+ { num: 9, name: 'Junie', target: 'junie', file: '.junie/guidelines.md' },
145
+ { num: 10, name: 'Goose', target: 'goose', file: '.goose/context.md' },
146
+ { num: 11, name: 'OpenCode', target: 'opencode', file: 'opencode.toml' },
147
+ { num: 12, name: 'Letta', target: 'letta', file: '.letta/instructions.md' },
148
+ { num: 13, name: 'All', target: 'all', file: null },
149
+ ];
150
+
151
+ const MCP_TOOL_MENU = [
152
+ { num: 1, name: 'Claude Code', target: 'claude', file: '.claude/settings.json' },
153
+ { num: 2, name: 'Cursor', target: 'cursor', file: '.cursor/mcp.json' },
154
+ { num: 3, name: 'Copilot', target: 'copilot', file: '.vscode/mcp.json' },
155
+ { num: 4, name: 'Gemini CLI', target: 'gemini', file: '.gemini/settings.json' },
156
+ { num: 5, name: 'Codex', target: 'codex', file: '.codex/config.toml' },
157
+ { num: 6, name: 'Roo Code', target: 'roo-code', file: '.roo/mcp.json' },
158
+ { num: 7, name: 'Windsurf', target: 'windsurf', file: '~/.codeium/windsurf/mcp_config.json' },
159
+ { num: 8, name: 'Goose', target: 'goose', file: '.goose/config.yaml' },
160
+ { num: 9, name: 'Zed', target: 'zed', file: '.zed/settings.json' },
161
+ { num: 10, name: 'Continue', target: 'continue', file: '.continue/mcpServers/booklib.yaml' },
162
+ { num: 11, name: 'All of the above', target: 'all', file: null },
163
+ ];
164
+
165
+ async function promptToolSelection() {
166
+ process.stdout.write('\nWhich AI tool do you use?\n\n');
167
+ for (const t of TOOL_MENU) {
168
+ const fileInfo = t.file ? ` → ${t.file}` : '';
169
+ process.stdout.write(` ${t.num}) ${t.name.padEnd(12)}${fileInfo}\n`);
170
+ }
171
+ process.stdout.write('\nSelect [1-13, or comma-separated for multiple]: ');
172
+
173
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
174
+ const answer = await new Promise(resolve => {
175
+ rl.once('line', line => { rl.close(); resolve(line.trim()); });
176
+ });
177
+
178
+ if (!answer) return 'all';
179
+ const nums = answer.split(',').map(n => parseInt(n.trim(), 10)).filter(n => !isNaN(n));
180
+ if (nums.length === 0 || nums.includes(13)) return 'all';
181
+ const selected = nums.map(n => TOOL_MENU.find(t => t.num === n)?.target).filter(Boolean);
182
+ return selected.length > 0 ? selected.join(',') : 'all';
183
+ }
184
+
185
+ async function promptMcpToolSelection() {
186
+ const SEP = '━'.repeat(51);
187
+ process.stdout.write(`\n${SEP}\n MCP Server Setup\n${SEP}\n\n`);
188
+ process.stdout.write(' BookLib has an MCP server — your AI tools can call it\n');
189
+ process.stdout.write(' directly to search knowledge, fetch context, and create\n');
190
+ process.stdout.write(' notes without leaving the conversation.\n\n');
191
+ process.stdout.write(' Wire up the MCP server? (Y/n): ');
192
+
193
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
194
+ const yn = await new Promise(resolve => {
195
+ rl.once('line', line => { rl.close(); resolve(line.trim().toLowerCase()); });
196
+ });
197
+ if (yn === 'n' || yn === 'no') return null;
198
+
199
+ process.stdout.write('\n Which tools should I configure? (select all that apply)\n\n');
200
+ for (const t of MCP_TOOL_MENU) {
201
+ const fileInfo = t.file ? ` → ${t.file}` : '';
202
+ process.stdout.write(` ${t.num}. ${t.name.padEnd(18)}${fileInfo}\n`);
203
+ }
204
+ process.stdout.write('\n Enter numbers separated by commas (1,2,5) or 11 for all: ');
205
+
206
+ const rl2 = createInterface({ input: process.stdin, output: process.stdout });
207
+ const answer = await new Promise(resolve => {
208
+ rl2.once('line', line => { rl2.close(); resolve(line.trim()); });
209
+ });
210
+
211
+ if (!answer) return 'all';
212
+ const nums = answer.split(',').map(n => parseInt(n.trim(), 10)).filter(n => !isNaN(n));
213
+ if (nums.length === 0 || nums.includes(11)) return 'all';
214
+ const selected = nums.map(n => MCP_TOOL_MENU.find(t => t.num === n)?.target).filter(Boolean);
215
+ return selected.length > 0 ? selected.join(',') : 'all';
216
+ }
217
+
218
+ async function main() {
219
+ switch (command) {
220
+ case 'index': {
221
+ const { skillsPath, cachePath } = resolveBookLibPaths();
222
+ const explicitDir = args[1] && !args[1].startsWith('--') ? args[1] : null;
223
+ const indexer = new BookLibIndexer();
224
+ const verboseIndex = args.includes('--verbose');
225
+ const indexStart = Date.now();
226
+
227
+ process.stdout.write('► Building index...\n');
228
+ if (explicitDir) {
229
+ // Explicit directory: just index that one
230
+ await indexer.indexDirectory(explicitDir, true, { quiet: !verboseIndex });
231
+ } else {
232
+ // Always index bundled skills first (clear on first pass)
233
+ await indexer.indexDirectory(BUNDLED_SKILLS_DIR, true, { quiet: !verboseIndex });
234
+ // Add community/user skills — deduplicate to avoid double-indexing same dir
235
+ const communitySkillsDir = path.join(cachePath, 'skills');
236
+ const dirsToAdd = new Set();
237
+ if (skillsPath !== BUNDLED_SKILLS_DIR) dirsToAdd.add(skillsPath);
238
+ if (communitySkillsDir !== skillsPath) dirsToAdd.add(communitySkillsDir);
239
+ for (const dir of dirsToAdd) {
240
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length > 0) {
241
+ const count = fs.readdirSync(dir).length;
242
+ if (verboseIndex) console.log(`Indexing ${count} community skill(s) from ${dir}...`);
243
+ await indexer.indexDirectory(dir, false, { quiet: !verboseIndex });
244
+ }
245
+ }
246
+ }
247
+ // Index knowledge nodes from .booklib/knowledge/nodes/
248
+ const { resolveKnowledgePaths } = await import('../lib/engine/graph.js');
249
+ const { nodesDir } = resolveKnowledgePaths();
250
+ await indexer.indexKnowledgeNodes(nodesDir);
251
+ const elapsed = ((Date.now() - indexStart) / 1000).toFixed(0);
252
+ console.log(`✅ Index built in ${elapsed}s`);
253
+ console.log(`\n → Now try: booklib search "your query"\n`);
254
+ break;
255
+ }
256
+
257
+ case 'lookup':
258
+ case 'search': {
259
+ const autoFetch = args.includes('--auto-fetch');
260
+ const useGraph = args.includes('--graph');
261
+ const roleFilter = (args.find(a => a.startsWith('--role=')) ?? '').replace('--role=', '') || null;
262
+ const query = args.slice(1).filter(a => !a.startsWith('--')).join(' ');
263
+ if (!query) { console.error('Usage: booklib search "<query>" [--auto-fetch] [--role=<role>] [--graph]'); process.exit(1); }
264
+
265
+ const regSearcher = new BookLibRegistrySearcher();
266
+ let { local, suggested, conflicts } = await regSearcher.searchHybrid(query, { useGraph });
267
+
268
+ // Role filter — narrow results to skills tagged for the requested agent role
269
+ if (roleFilter) {
270
+ const roleMatch = s => !s.roles || s.roles.includes(roleFilter);
271
+ local = local.filter(r => roleMatch(r.metadata ?? r));
272
+ suggested = suggested.filter(roleMatch);
273
+ if (local.length === 0 && suggested.length === 0) {
274
+ console.log(`No results for "${query}" in role "${roleFilter}". Try without --role to see all matches.`);
275
+ break;
276
+ }
277
+ }
278
+
279
+ // Auto-fetch: silently fetch trusted suggestions and re-search
280
+ if (autoFetch && suggested.length > 0) {
281
+ const trustedSuggestions = suggested.filter(s => s.trusted);
282
+ if (trustedSuggestions.length > 0) {
283
+ const fetcher = new SkillFetcher();
284
+ for (const skill of trustedSuggestions) {
285
+ if (!fetcher.isCached(skill)) {
286
+ process.stderr.write(`[booklib] Fetching ${skill.name}...\n`);
287
+ try { await fetcher.fetch(skill); } catch { /* non-fatal */ }
288
+ }
289
+ }
290
+ // Re-index and re-search with newly fetched skills
291
+ const { cachePath } = resolveBookLibPaths();
292
+ const indexer = new BookLibIndexer();
293
+ await indexer.indexDirectory(path.join(cachePath, 'skills'));
294
+ ({ local, suggested, conflicts } = await regSearcher.searchHybrid(query, { useGraph }));
295
+ }
296
+ }
297
+
298
+ if (local.length > 0) {
299
+ console.log('\n📚 Local results:\n');
300
+ local.forEach(r => {
301
+ const rationale = r._rationale ? ` ↳ ${r._rationale}` : '';
302
+ const isNode = r.metadata?.nodeKind === 'knowledge';
303
+ const label = isNode
304
+ ? `📝 ${r.metadata?.title ?? r.metadata?.filePath ?? '?'} [${r.metadata?.type ?? 'note'}]`
305
+ : `📚 ${r.metadata?.name ?? r.metadata?.filePath ?? '?'} (${r.metadata?.type ?? 'chunk'})`;
306
+ const s = r.score ?? 0;
307
+ const bar = s >= 0.7 ? '████' : s >= 0.5 ? '███░' : s >= 0.35 ? '██░░' : '█░░░';
308
+ console.log(` ${bar} ${label}`);
309
+ if (rationale) console.log(rationale);
310
+ });
311
+ }
312
+ if (suggested.length > 0) {
313
+ console.log('\n💡 Community skills available (not yet indexed):');
314
+ suggested.forEach(s => {
315
+ const stars = s.stars ? ` ★${s.stars.toLocaleString()}` : '';
316
+ const rationale = s._rationale ? `\n ↳ ${s._rationale}` : '';
317
+ console.log(` • ${s.name}${stars} — ${s.description}${rationale}`);
318
+ });
319
+ if (!autoFetch) console.log('\nTip: run with --auto-fetch to install and search in one step');
320
+ }
321
+ if (local.length === 0 && suggested.length === 0) {
322
+ console.log('No results found.');
323
+ }
324
+ break;
325
+ }
326
+
327
+ case 'review':
328
+ case 'audit': {
329
+ const auditor = new BookLibAuditor();
330
+ const skillName = args[1];
331
+ const filePath = args[2];
332
+ if (!skillName || !filePath) { console.error('Usage: booklib audit <skill-name> <file-path>'); process.exit(1); }
333
+ const { skillsPath } = resolveBookLibPaths();
334
+ const candidates = [
335
+ path.join(skillsPath, skillName),
336
+ path.join(BUNDLED_SKILLS_DIR, skillName),
337
+ ];
338
+ const skillPath = candidates.find(p => fs.existsSync(p)) ?? candidates[0];
339
+ if (!fs.existsSync(skillPath)) {
340
+ const available = fs.readdirSync(BUNDLED_SKILLS_DIR)
341
+ .filter(d => fs.statSync(path.join(BUNDLED_SKILLS_DIR, d)).isDirectory())
342
+ .sort();
343
+ console.error(` Unknown skill: '${skillName}'`);
344
+ console.error(` Available: ${available.join(', ')}`);
345
+ process.exit(1);
346
+ }
347
+ const report = await auditor.audit(skillPath, filePath);
348
+ const divider = '─'.repeat(60);
349
+ console.log(`\n► Audit prompt — paste into Claude, ChatGPT, or your AI assistant:\n${divider}\n`);
350
+ console.log(report);
351
+ console.log(`\n${divider}`);
352
+ console.log(`Tip: pipe to clipboard → booklib audit ${skillName} ${filePath} | pbcopy (mac)`);
353
+ console.log(` → booklib audit ${skillName} ${filePath} | xclip (linux)\n`);
354
+ break;
355
+ }
356
+
357
+ case 'scan': {
358
+ const scanner = new BookLibScanner();
359
+ const docsMode = args.includes('--docs');
360
+ const scanDir = args.filter(a => !a.startsWith('--'))[1] || process.cwd();
361
+ const report = await scanner.scan(scanDir, { mode: docsMode ? 'docs' : 'code' });
362
+ console.log(report);
363
+ break;
364
+ }
365
+
366
+ case 'context': {
367
+ const promptOnly = args.includes('--prompt-only');
368
+ const task = args.slice(1).filter(a => !a.startsWith('--')).join(' ');
369
+ if (!task) {
370
+ console.error('Usage: booklib context "<task description>" [--prompt-only] [--file=<path>] [--no-graph]');
371
+ console.error('Example: booklib context "implement a payment service in Kotlin with async error handling"');
372
+ process.exit(1);
373
+ }
374
+ const builder = new ContextBuilder();
375
+ const useGraph = !args.includes('--no-graph') && !promptOnly;
376
+ const fileArg = parseFlag(args, 'file');
377
+ const result = useGraph
378
+ ? await builder.buildWithGraph(task, fileArg)
379
+ : await builder.build(task, { promptOnly });
380
+ console.log(result);
381
+
382
+ if (fileArg && useGraph && !result.includes('## Knowledge Graph Context')) {
383
+ process.stderr.write(
384
+ `\nTip: no component is mapped to "${fileArg}".\n` +
385
+ ` To enable graph context injection: booklib component add <name> "<glob>"\n` +
386
+ ` Example: booklib component add auth "src/auth/**"\n`
387
+ );
388
+ }
389
+ break;
390
+ }
391
+
392
+ case 'save-state': {
393
+ const handoff = new BookLibHandoff();
394
+ const parsed = {};
395
+ const stateArgs = args.slice(1);
396
+ for (let i = 0; i < stateArgs.length; i++) {
397
+ const a = stateArgs[i];
398
+ if (!a.startsWith('--')) continue;
399
+ const stripped = a.replace(/^--/, '');
400
+ if (stripped.includes('=')) {
401
+ const [k, ...v] = stripped.split('=');
402
+ parsed[k] = v.join('=');
403
+ } else if (i + 1 < stateArgs.length && !stateArgs[i + 1].startsWith('--')) {
404
+ parsed[stripped] = stateArgs[++i];
405
+ }
406
+ }
407
+ handoff.saveState(parsed);
408
+ break;
409
+ }
410
+
411
+ case 'resume': {
412
+ const handoff = new BookLibHandoff();
413
+ console.log(handoff.resume(args[1]));
414
+ break;
415
+ }
416
+
417
+ case 'recover-auto': {
418
+ const handoff = new BookLibHandoff();
419
+ console.log(handoff.recoverFromSessionOrGit());
420
+ break;
421
+ }
422
+
423
+ case 'sessions': {
424
+ const mgr = new BookLibSessionManager(process.cwd());
425
+ const subCmd = args[1];
426
+
427
+ if (subCmd === 'cleanup') {
428
+ const beforeDays = parseInt(args[2]?.split('=')[1]) || 90;
429
+ const result = mgr.cleanupSessions({ beforeDays, archive: true });
430
+ console.log(`✅ Archived ${result.archived} sessions, deleted ${result.deleted}`);
431
+ console.log(`Preview: ${JSON.stringify(result.preview.slice(0, 3), null, 2)}`);
432
+ } else if (subCmd === 'diff') {
433
+ const diff = mgr.diffSessions(args[2], args[3]);
434
+ if (diff.error) console.error(diff.error);
435
+ else {
436
+ console.log(`\n📊 Comparing: ${diff.session1} vs ${diff.session2}`);
437
+ console.log(`\nGoal Changed: ${diff.goal.changed}`);
438
+ console.log(` ${diff.session1}: ${diff.goal.s1}`);
439
+ console.log(` ${diff.session2}: ${diff.goal.s2}`);
440
+ console.log(`\nConflicting Tasks: ${diff.tasks.conflicts.length}`);
441
+ diff.tasks.conflicts.forEach(t => console.log(` ⚠️ ${t}`));
442
+ console.log(`\nNew Skills: ${diff.skills.added.join(', ') || 'none'}`);
443
+ }
444
+ } else if (subCmd === 'find') {
445
+ const result = mgr.findSession(args[2], { searchGlobal: true });
446
+ if (result) {
447
+ console.log(`✅ Found: ${result.path} (${result.scope})`);
448
+ } else {
449
+ console.log(`❌ Session not found: ${args[2]}`);
450
+ }
451
+ } else if (subCmd === 'search') {
452
+ const results = mgr.searchSessions(args[2]);
453
+ if (results.length === 0) {
454
+ console.log(`No sessions found matching: ${args[2]}`);
455
+ } else {
456
+ console.log(`\n🔍 Found ${results.length} session(s):`);
457
+ results.forEach(r => {
458
+ console.log(`\n 📝 ${r.name}`);
459
+ console.log(` Goal: ${r.goal}`);
460
+ console.log(` Tags: ${r.tags.join(', ') || 'none'}`);
461
+ });
462
+ }
463
+ } else if (subCmd === 'tag') {
464
+ const sessionId = args[2];
465
+ const tagArg = args.find(a => a.startsWith('--add='));
466
+ if (!tagArg) {
467
+ console.error('Usage: booklib sessions tag <id> --add=tag1,tag2');
468
+ process.exit(1);
469
+ }
470
+ const tags = tagArg.split('=')[1].split(',');
471
+ const result = mgr.tagSession(sessionId, tags, 'add');
472
+ console.log(`✅ Tagged: ${result.session}`);
473
+ console.log(` Tags: ${result.tags.join(', ')}`);
474
+ } else if (subCmd === 'validate') {
475
+ const result = mgr.validateSession(args[2]);
476
+ console.log(`\n${result.valid ? '✅' : '⚠️'} Validation Result:`);
477
+ if (result.errors.length > 0) {
478
+ console.log('Errors:');
479
+ result.errors.forEach(e => console.log(` ❌ ${e}`));
480
+ }
481
+ if (result.warnings.length > 0) {
482
+ console.log('Warnings:');
483
+ result.warnings.forEach(w => console.log(` ⚠️ ${w}`));
484
+ }
485
+ console.log(`Score: ${result.score}/100`);
486
+ } else if (subCmd === 'create') {
487
+ const templateArg = args.find(a => a.startsWith('--template='));
488
+ const template = templateArg?.split('=')[1];
489
+ const sessionName = args[3];
490
+ if (!template || !sessionName) {
491
+ console.error('Usage: booklib sessions create --template=<type> <name>');
492
+ process.exit(1);
493
+ }
494
+ const result = mgr.createFromTemplate(template, sessionName);
495
+ if (result.error) console.error(result.error);
496
+ else console.log(`✅ Created session from template: ${result.created}`);
497
+ } else if (subCmd === 'report') {
498
+ const sinceArg = args.find(a => a.startsWith('--since='));
499
+ const since = sinceArg?.split('=')[1];
500
+ const stats = mgr.generateReport({ since });
501
+ console.log(`\n📊 Session Report`);
502
+ console.log(`Total sessions: ${stats.total_sessions}`);
503
+ console.log(`Pending tasks: ${stats.total_tasks}`);
504
+ console.log(`Active skills: ${stats.unique_skills}`);
505
+ console.log(`\nTop Skills: ${stats.unique_skills.slice(0, 3).join(', ')}`);
506
+ console.log(`\nRecent Activity:`);
507
+ stats.recent_activity.forEach(a => {
508
+ console.log(` 📝 ${a.name}: ${a.goal} (${new Date(a.timestamp).toLocaleDateString()})`);
509
+ });
510
+ } else if (subCmd === 'history') {
511
+ const history = mgr.getVersionHistory(args[2]);
512
+ console.log(`\n📜 Version History: ${args[2]}`);
513
+ console.log(`Total versions: ${history.length}`);
514
+ history.slice(0, 5).forEach(v => {
515
+ console.log(` Version ${v.version}: ${v.timestamp}`);
516
+ });
517
+ }
518
+ break;
519
+ }
520
+
521
+ case 'sessions-list': {
522
+ const coord = new BookLibSessionCoordinator();
523
+ const sessions = coord.listAllSessions();
524
+ if (sessions.length === 0) { console.log('No sessions found.'); break; }
525
+ sessions.forEach(s => console.log(` 📝 ${s.id} — ${s.goal} [${s.branch}]`));
526
+ break;
527
+ }
528
+
529
+ case 'sessions-merge': {
530
+ const coord = new BookLibSessionCoordinator();
531
+ const ids = args[1]?.split(',');
532
+ const output = args[2];
533
+ if (!ids || !output) { console.error('Usage: booklib sessions-merge <id1,id2,...> <output-name>'); process.exit(1); }
534
+ const result = coord.mergeSessions(ids, output);
535
+ console.log(result.message || `✅ Merged into: ${output}`);
536
+ break;
537
+ }
538
+
539
+ case 'sessions-lineage': {
540
+ const coord = new BookLibSessionCoordinator();
541
+ if (args[1] && args[2]) {
542
+ coord.trackLineage(args[1], args[2], args[3] || '');
543
+ console.log(`✅ Lineage tracked: ${args[1]} → ${args[2]}`);
544
+ } else {
545
+ console.log(coord.displayLineageTree());
546
+ }
547
+ break;
548
+ }
549
+
550
+ case 'sessions-compare': {
551
+ const coord = new BookLibSessionCoordinator();
552
+ const ids = args[1]?.split(',');
553
+ const targetFile = args[2];
554
+ const output = args[3];
555
+ if (!ids || !targetFile || !output) { console.error('Usage: booklib sessions-compare <id1,id2,...> <file> <output-name>'); process.exit(1); }
556
+ const result = coord.compareAudits(ids, targetFile, output);
557
+ console.log(result.message || `✅ Comparison saved: ${output}`);
558
+ break;
559
+ }
560
+
561
+
562
+
563
+ case 'init': {
564
+ // Backwards-compat: if legacy flags are passed, run old init flow
565
+ const hasLegacyFlags = args.some(a =>
566
+ a.startsWith('--tool=') || a.startsWith('--skills=') ||
567
+ a.includes('--dry-run') || a.includes('--ecc')
568
+ );
569
+
570
+ if (hasLegacyFlags) {
571
+ // ── Legacy init path ─────────────────────────────────────────────────
572
+ const orchestratorArg = args.find(a => a.startsWith('--orchestrator='))?.split('=')[1] ?? null;
573
+ const dryRun = args.includes('--dry-run');
574
+ const hasToolFlag = args.some(a => a.startsWith('--tool='));
575
+ const targetFlag = args.find(a => a.startsWith('--target='))?.split('=')[1] ?? null;
576
+ let targetArg;
577
+ if (hasToolFlag) {
578
+ targetArg = args.find(a => a.startsWith('--tool='))?.split('=')[1];
579
+ } else if (targetFlag) {
580
+ targetArg = targetFlag;
581
+ } else if (!dryRun) {
582
+ const { configPath } = resolveBookLibPaths();
583
+ let savedConfig = {};
584
+ try { savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { /* no config yet */ }
585
+ if (savedConfig.tools?.length) {
586
+ targetArg = savedConfig.tools.join(',');
587
+ console.log(`Using saved tool selection: ${targetArg} (pass --tool=X to override)\n`);
588
+ } else {
589
+ targetArg = await promptToolSelection();
590
+ const updatedConfig = { ...savedConfig, tools: targetArg === 'all'
591
+ ? ['claude', 'cursor', 'copilot', 'gemini', 'codex', 'windsurf', 'roo-code', 'openhands', 'junie', 'goose', 'opencode', 'letta']
592
+ : targetArg.split(',') };
593
+ try { fs.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2)); } catch { /* best-effort */ }
594
+ }
595
+ } else {
596
+ targetArg = 'auto';
597
+ }
598
+ const skillsArg = args.find(a => a.startsWith('--skills='))?.split('=')[1];
599
+ const rulesArg = args.find(a => a.startsWith('--rules='))?.split('=')[1];
600
+ const pullEcc = args.includes('--ecc');
601
+ const includeAgents = pullEcc || args.includes('--agents');
602
+ const includeCommands = pullEcc || args.includes('--commands');
603
+ const includeRules = pullEcc || args.includes('--rules') || rulesArg != null;
604
+ const skillList = skillsArg?.split(',').map(s => s.trim());
605
+ const langList = rulesArg ? rulesArg.split(',').map(s => s.trim()) : (includeRules ? null : false);
606
+ const initializer = new ProjectInitializer();
607
+
608
+ if (!skillList) {
609
+ const detected = initializer.detectRelevantSkills();
610
+ if (detected.length === 0 && !includeAgents && !includeCommands && !includeRules) {
611
+ console.log('No skills auto-detected. Specify with --skills=skill1,skill2 or use --ecc to pull agents/commands/rules.');
612
+ process.exit(1);
613
+ }
614
+ if (detected.length > 0) console.log(`Auto-detected skills: ${detected.join(', ')}\n`);
615
+ }
616
+
617
+ if (hasToolFlag && !dryRun) {
618
+ const { configPath } = resolveBookLibPaths();
619
+ let savedConfig = {};
620
+ try { savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { /* no config yet */ }
621
+ const toolList = targetArg === 'all'
622
+ ? ['claude', 'cursor', 'copilot', 'gemini', 'codex', 'windsurf', 'roo-code', 'openhands', 'junie', 'goose', 'opencode', 'letta']
623
+ : targetArg.split(',');
624
+ try { fs.writeFileSync(configPath, JSON.stringify({ ...savedConfig, tools: toolList }, null, 2)); } catch { /* best-effort */ }
625
+ }
626
+
627
+ if (skillList || initializer.detectRelevantSkills().length > 0) {
628
+ console.log(`Generating context files for: ${targetArg === 'all' ? 'claude, cursor, copilot, gemini, codex, windsurf, roo-code, openhands, junie, goose, opencode, letta' : targetArg}\n`);
629
+ const effectiveSkills = skillList ?? initializer.detectRelevantSkills();
630
+ // Load saved config for profile-based rendering (Spec ⑨)
631
+ const { configPath: legacyConfigPath } = resolveBookLibPaths();
632
+ let legacySavedConfig = {};
633
+ try { legacySavedConfig = JSON.parse(fs.readFileSync(legacyConfigPath, 'utf8')); } catch { /* no config yet */ }
634
+ const written = await initializer.init({
635
+ skills: effectiveSkills,
636
+ target: targetArg,
637
+ dryRun,
638
+ profile: legacySavedConfig.profile ?? 'software-development',
639
+ stack: legacySavedConfig.stack ?? effectiveSkills.join(', '),
640
+ });
641
+ if (!dryRun && written.length > 0) console.log('');
642
+
643
+ // Discovery hint: suggest related skills
644
+ const related = initializer.suggestRelatedSkills(effectiveSkills, process.cwd());
645
+ if (related.length > 0) {
646
+ console.log(' \u{1F4A1} Also consider for your stack:');
647
+ related.forEach(s => console.log(` booklib init --skills=${effectiveSkills.join(',')},${s}`));
648
+ }
649
+ }
650
+
651
+ if (includeAgents || includeCommands || includeRules) {
652
+ const pulling = [];
653
+ if (includeRules) pulling.push(langList ? `rules (${langList.join(',')})` : 'rules (all languages)');
654
+ if (includeAgents) pulling.push('agents → .claude/agents/');
655
+ if (includeCommands) pulling.push('commands → .claude/commands/');
656
+ console.log(`Pulling ECC artifacts: ${pulling.join(', ')}\n`);
657
+ try {
658
+ const eccWritten = await initializer.fetchEccArtifacts({ languages: langList, includeAgents, includeCommands, dryRun });
659
+ if (!dryRun && eccWritten.length > 0) console.log(`\nPulled ${eccWritten.length} artifact(s) from ECC.`);
660
+ } catch (err) {
661
+ console.error(`ECC fetch failed: ${err.message}`);
662
+ }
663
+ }
664
+ break;
665
+ }
666
+
667
+ // ── New guided wizard ─────────────────────────────────────────────────
668
+ const reset = args.includes('--reset');
669
+ await runWizard(process.cwd(), { reset });
670
+ break;
671
+ }
672
+
673
+ case 'setup': {
674
+ const engine = new DiscoveryEngine();
675
+ const fetcher = new SkillFetcher();
676
+ console.log('Discovering skills...');
677
+ const skills = await engine.refresh();
678
+ const trusted = skills.filter(s => s.trusted);
679
+ const untrusted = skills.filter(s => !s.trusted);
680
+ if (trusted.length === 0) {
681
+ console.log('No trusted skills found. Check your booklib.config.json sources.');
682
+ break;
683
+ }
684
+ console.log(`Found ${trusted.length} trusted skill(s) to install, ${untrusted.length} require confirmation.\n`);
685
+ let installed = 0;
686
+ for (const skill of trusted) {
687
+ if (fetcher.isCached(skill)) {
688
+ console.log(` ✓ ${skill.name} (already installed)`);
689
+ continue;
690
+ }
691
+ process.stdout.write(` ↓ Fetching ${skill.name}...`);
692
+ try {
693
+ await fetcher.fetch(skill);
694
+ console.log(' done');
695
+ installed++;
696
+ } catch (err) {
697
+ console.log(` failed: ${err.message}`);
698
+ }
699
+ }
700
+ if (installed > 0) {
701
+ console.log(`\nRe-indexing...`);
702
+ const { skillsPath, cachePath } = resolveBookLibPaths();
703
+ const indexer = new BookLibIndexer();
704
+ await indexer.indexDirectory(skillsPath);
705
+ await indexer.indexDirectory(path.join(cachePath, 'skills'));
706
+ }
707
+ console.log('\n✅ Setup complete. Run: booklib search "<query>"');
708
+ console.log(' Skills synced to ~/.claude/skills/ — pair with an orchestrator if needed:');
709
+ console.log(' obra/superpowers: /plugin install superpowers ruflo: npm install -g ruflo');
710
+ if (untrusted.length > 0) {
711
+ console.log(`\nTo install remaining skills, run: booklib install <skill-name>`);
712
+ untrusted.forEach(s => console.log(` • ${s.name}`));
713
+ }
714
+ break;
715
+ }
716
+
717
+ case 'add': {
718
+ console.error('⚠ "booklib add" is deprecated. Use: booklib install <name>');
719
+ const installer = new BookLibInstaller();
720
+ const skillId = args[1];
721
+ if (!skillId) { console.error('Usage: booklib add <skill-id-or-url>'); process.exit(1); }
722
+ await installer.add(skillId);
723
+ break;
724
+ }
725
+
726
+ case 'install': {
727
+ const names = args.slice(1).filter(a => !a.startsWith('--'));
728
+ if (names.length === 0) {
729
+ console.error('Usage: booklib install <skill-name> [skill-name...]');
730
+ process.exit(1);
731
+ }
732
+ const { installSkill } = await import('../lib/skill-fetcher.js');
733
+ for (const name of names) {
734
+ const result = installSkill(name);
735
+ if (result === 'installed') console.log(` ✓ ${name}`);
736
+ else if (result === 'already-installed') console.log(` · ${name} (already installed)`);
737
+ else console.log(` ✗ ${name}: not found in any catalog`);
738
+ }
739
+ break;
740
+ }
741
+
742
+ case 'fetch': {
743
+ console.error('⚠ "booklib fetch" is deprecated. Use: booklib install <name>');
744
+ const { SKILL_REGISTRY } = await import('../lib/registry/skills.js');
745
+ const skillName = args[1];
746
+ if (!skillName) { console.error('Usage: booklib fetch <skill-name>'); process.exit(1); }
747
+ const skill = SKILL_REGISTRY.find(s => s.name === skillName || s.name.endsWith(`/${skillName}`));
748
+ if (!skill) { console.error(`Skill not found in registry: ${skillName}`); process.exit(1); }
749
+ const fetcher = new SkillFetcher();
750
+ try {
751
+ await fetcher.fetch(skill, {
752
+ onPrompt: async (s) => {
753
+ process.stdout.write(`Index "${s.name}" from ${s.source.type} (untrusted)? [y/N] `);
754
+ const answer = await new Promise(r => {
755
+ process.stdin.once('data', d => r(d.toString().trim().toLowerCase()));
756
+ });
757
+ return answer === 'y' || answer === 'yes';
758
+ },
759
+ });
760
+ } catch (err) {
761
+ if (err instanceof RequiresConfirmationError) {
762
+ console.error(err.message);
763
+ process.exit(1);
764
+ }
765
+ throw err;
766
+ }
767
+ // Slot limit warning
768
+ const slotCount = countInstalledSlots();
769
+ if (slotCount >= SKILL_LIMIT) {
770
+ console.log(`\n ⚠ You now have ${slotCount}/${SKILL_LIMIT} skill slots used.`);
771
+ console.log(' Claude may truncate skill descriptions. Run "booklib doctor" to clean up.');
772
+ } else if (slotCount >= SKILL_LIMIT - 4) {
773
+ console.log(`\n ⚠ ${slotCount}/${SKILL_LIMIT} slots used — approaching limit.`);
774
+ console.log(' Run "booklib doctor" to review installed skills.');
775
+ }
776
+ break;
777
+ }
778
+
779
+ case 'sync': {
780
+ // Retroactively sync all already-fetched BookLib skills to ~/.claude/skills/
781
+ const { cachePath } = resolveBookLibPaths();
782
+ const skillsDir = path.join(cachePath, 'skills');
783
+ if (!fs.existsSync(skillsDir)) { console.log('No fetched skills found. Run: booklib setup'); break; }
784
+ const fetcher = new SkillFetcher();
785
+ const dirs = fs.readdirSync(skillsDir).filter(d => fs.existsSync(path.join(skillsDir, d, 'SKILL.md')));
786
+ let synced = 0;
787
+ for (const d of dirs) {
788
+ const skillFile = path.join(skillsDir, d, 'SKILL.md');
789
+ const head = fs.readFileSync(skillFile, 'utf8').split('\n').slice(0, 15).join('\n');
790
+ const nameMatch = head.match(/^name:\s*["']?(.+?)["']?\s*$/m);
791
+ const descMatch = head.match(/^description:\s*(.+)$/m);
792
+ const name = nameMatch ? nameMatch[1].trim() : d;
793
+ const description = descMatch ? descMatch[1].trim().replace(/^["']|["']$/g, '') : '';
794
+ fetcher._syncToClaudeSkills({ name, description }, path.join(skillsDir, d));
795
+ synced++;
796
+ }
797
+ console.log(`Synced ${synced} skills to ~/.claude/skills/ — available via Claude Code Skill tool`);
798
+ console.log(` Pair with an orchestrator: /plugin install superpowers (obra) · npm install -g ruflo (ruflo)`);
799
+ break;
800
+ }
801
+
802
+ case 'discover': {
803
+ const engine = new DiscoveryEngine();
804
+ const flag = args[1];
805
+ if (flag === '--refresh') {
806
+ console.log('Refreshing discovery cache...');
807
+ const skills = await engine.refresh();
808
+ console.log(`Found ${skills.length} skills from external sources.`);
809
+ skills.forEach(s => {
810
+ const stars = s.stars ? ` ★${s.stars.toLocaleString()}` : '';
811
+ const trust = s.trusted ? '' : ' (requires confirmation)';
812
+ console.log(` • ${s.name}${stars} [${s.source.type}]${trust} — ${s.description}`);
813
+ });
814
+ } else {
815
+ const skills = await engine.discover();
816
+ if (skills.length === 0) {
817
+ console.log('No external sources configured. Add sources to booklib.config.json.');
818
+ } else {
819
+ console.log(`Discovered ${skills.length} skills:`);
820
+ skills.forEach(s => {
821
+ const stars = s.stars ? ` ★${s.stars.toLocaleString()}` : '';
822
+ const trust = s.trusted ? '' : ' (requires confirmation)';
823
+ console.log(` • ${s.name}${stars} [${s.source.type}]${trust} — ${s.description}`);
824
+ });
825
+ }
826
+ }
827
+ break;
828
+ }
829
+
830
+ case 'profile': {
831
+ const role = args[1];
832
+ const ALL_ROLES = ['architect', 'coder', 'reviewer', 'tester', 'security', 'frontend', 'optimizer', 'devops', 'ai-engineer', 'manager', 'product', 'legal', 'writer', 'strategist', 'designer'];
833
+ if (!role || role === '--list') {
834
+ console.log('\nAvailable agent roles:\n');
835
+ ALL_ROLES.forEach(r => console.log(` • ${r}`));
836
+ console.log('\nUsage: booklib profile <role>\n');
837
+ break;
838
+ }
839
+
840
+ const engine = new DiscoveryEngine();
841
+ const all = await engine.discover();
842
+ // Merge with registry for role metadata
843
+ const { skills: regSkills } = JSON.parse(
844
+ (await import('fs')).default.readFileSync(
845
+ (await import('path')).default.join(
846
+ (await import('url')).default.fileURLToPath(new URL('.', import.meta.url)),
847
+ '../community/registry.json'
848
+ ), 'utf8'
849
+ )
850
+ );
851
+ const roleMap = new Map(regSkills.map(s => [s.name, s.roles ?? []]));
852
+
853
+ const matches = all.filter(s => {
854
+ const roles = roleMap.get(s.name) ?? s.roles ?? [];
855
+ return roles.includes(role);
856
+ });
857
+
858
+ if (matches.length === 0) {
859
+ console.log(`No skills found for role "${role}". Try: booklib profile --list`);
860
+ break;
861
+ }
862
+
863
+ console.log(`\n🤖 Skill profile for agent role: ${role}\n`);
864
+ console.log(` ${matches.length} skills pre-selected from ${all.length} available\n`);
865
+ matches.forEach(s => {
866
+ const stars = s.stars ? ` ★${s.stars.toLocaleString()}` : '';
867
+ console.log(` • ${s.name}${stars}`);
868
+ if (s.description) console.log(` ${s.description.slice(0, 100)}`);
869
+ });
870
+ console.log(`\nTo load all: booklib setup (then each skill is available to inject)`);
871
+ console.log(`To search within role: booklib search "<query>" --role=${role}\n`);
872
+ break;
873
+ }
874
+
875
+ case 'swarm-config': {
876
+ const trigger = args[1];
877
+
878
+ // Trigger → roles → skill domains mapping (extends ruflo's worker-integration concept)
879
+ const SWARM_TRIGGERS = {
880
+ audit: { roles: ['security', 'tester'], phases: ['security-scan', 'coverage', 'vulnerability-check'] },
881
+ refactor: { roles: ['coder', 'reviewer'], phases: ['complexity', 'naming', 'patterns', 'solid'] },
882
+ architect: { roles: ['architect'], phases: ['system-design', 'ddd', 'api-design'] },
883
+ frontend: { roles: ['frontend', 'tester'], phases: ['components', 'state', 'performance', 'a11y'] },
884
+ release: { roles: ['devops', 'security'], phases: ['docker', 'secrets', 'headers', 'changelog'] },
885
+ research: { roles: ['ai-engineer', 'architect'], phases: ['prompt-design', 'rag', 'reliability'] },
886
+ manage: { roles: ['manager'], phases: ['leadership', 'retro', 'process'] },
887
+ product: { roles: ['product', 'writer'], phases: ['requirements', 'user-stories', 'prioritization'] },
888
+ legal: { roles: ['legal'], phases: ['contract-review', 'risk-assessment', 'compliance'] },
889
+ write: { roles: ['writer'], phases: ['outline', 'draft', 'edit', 'review'] },
890
+ strategy: { roles: ['strategist', 'product'], phases: ['discovery', 'positioning', 'roadmap'] },
891
+ design: { roles: ['designer', 'frontend'], phases: ['visual-hierarchy', 'typography', 'brand'] },
892
+ };
893
+
894
+ if (!trigger || trigger === '--list') {
895
+ console.log('\n🐝 BookLib Swarm Trigger Config\n');
896
+ console.log(' Maps swarm triggers → agent roles → skill domains\n');
897
+ console.log(' Usage: booklib swarm-config <trigger>\n');
898
+ Object.entries(SWARM_TRIGGERS).forEach(([t, cfg]) => {
899
+ console.log(` ${t.padEnd(12)} → roles: ${cfg.roles.join(', ')}`);
900
+ });
901
+ console.log('\n booklib swarm-config <trigger> Show skills for a trigger');
902
+ console.log(' booklib profile <role> Show skills for a role\n');
903
+ break;
904
+ }
905
+
906
+ const cfg = SWARM_TRIGGERS[trigger];
907
+ if (!cfg) {
908
+ console.log(`Unknown trigger "${trigger}". Available: ${Object.keys(SWARM_TRIGGERS).join(', ')}`);
909
+ break;
910
+ }
911
+
912
+ const engine = new DiscoveryEngine();
913
+ const all = await engine.discover();
914
+ const { skills: regSkills } = JSON.parse(
915
+ (await import('fs')).default.readFileSync(
916
+ (await import('path')).default.join(
917
+ (await import('url')).default.fileURLToPath(new URL('.', import.meta.url)),
918
+ '../community/registry.json'
919
+ ), 'utf8'
920
+ )
921
+ );
922
+ const roleMap = new Map(regSkills.map(s => [s.name, s.roles ?? []]));
923
+
924
+ console.log(`\n🐝 Swarm config for trigger: ${trigger}\n`);
925
+ console.log(` Phases: ${cfg.phases.join(' → ')}\n`);
926
+
927
+ for (const role of cfg.roles) {
928
+ const roleSkills = all.filter(s => (roleMap.get(s.name) ?? s.roles ?? []).includes(role));
929
+ console.log(` Agent role: ${role} (${roleSkills.length} skills)`);
930
+ roleSkills.slice(0, 5).forEach(s => {
931
+ const stars = s.stars ? ` ★${s.stars.toLocaleString()}` : '';
932
+ console.log(` • ${s.name}${stars} — ${(s.description ?? '').slice(0, 75)}`);
933
+ });
934
+ if (roleSkills.length > 5) console.log(` … and ${roleSkills.length - 5} more (booklib profile ${role})`);
935
+ console.log();
936
+ }
937
+ break;
938
+ }
939
+
940
+ case 'note': {
941
+ const title = args.slice(1).join(' ');
942
+ if (!title) { console.error('Usage: booklib note "<title>"'); process.exit(1); }
943
+ const id = generateNodeId('node');
944
+ let body = await readStdin();
945
+ if (!body) body = openEditor('') ?? '';
946
+ if (!body) body = await readInteractive('Enter note content (Ctrl+D to finish):\n');
947
+ const noteContent = serializeNode({ id, type: 'note', title, content: body ?? '' });
948
+ const filePath = saveNode(noteContent, id);
949
+ await autoIndexNode(filePath);
950
+ try {
951
+ const autoLinked = await autoLink({ nodeId: id, title, content: body ?? '' });
952
+ if (autoLinked.length > 0) {
953
+ console.log(` Auto-linked: ${autoLinked.map(l => `${l.to} (${l.type})`).join(', ')}`);
954
+ }
955
+ } catch { /* best-effort */ }
956
+ console.log(`✅ Note created: ${filePath}`);
957
+ console.log(` ID: ${id}`);
958
+ break;
959
+ }
960
+
961
+ case 'component': {
962
+ const subcommand = args[1];
963
+ const name = args[2];
964
+ const glob = args[3];
965
+ if (subcommand !== 'add' || !name || !glob) {
966
+ console.error('Usage: booklib component add <name> "<glob>"');
967
+ process.exit(1);
968
+ }
969
+ const id = `comp_${name.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
970
+ const content = serializeNode({
971
+ id,
972
+ type: 'component',
973
+ title: name,
974
+ nodePaths: [glob],
975
+ content: '',
976
+ });
977
+ const filePath = saveNode(content, id);
978
+ try {
979
+ const reverseLinks = await autoLinkReverse({ componentId: id, componentTitle: name });
980
+ if (reverseLinks.length > 0) {
981
+ console.log(` Auto-linked ${reverseLinks.length} existing note(s) to this component`);
982
+ }
983
+ } catch { /* best-effort */ }
984
+ console.log(`✅ Component created: ${filePath}`);
985
+ console.log(` ID: ${id} paths: ${glob}`);
986
+ break;
987
+ }
988
+
989
+ case 'link': {
990
+ const [, fromRef, toRef] = args;
991
+ const typeArg = parseFlag(args, 'type');
992
+ const weightArg = parseFlag(args, 'weight');
993
+ if (!fromRef || !toRef || !typeArg) {
994
+ console.error('Usage: booklib link "<title-or-id>" "<title-or-id>" --type <edge-type> [--weight 0.9]');
995
+ process.exit(1);
996
+ }
997
+ if (!EDGE_TYPES.includes(typeArg)) {
998
+ console.error(`Invalid edge type "${typeArg}". Valid: ${EDGE_TYPES.join(', ')}`);
999
+ process.exit(1);
1000
+ }
1001
+ let from, to;
1002
+ try {
1003
+ from = resolveNodeRef(fromRef);
1004
+ to = resolveNodeRef(toRef);
1005
+ } catch (err) {
1006
+ console.error(err.message);
1007
+ process.exit(1);
1008
+ }
1009
+ const edge = {
1010
+ from,
1011
+ to,
1012
+ type: typeArg,
1013
+ weight: weightArg ? parseFloat(weightArg) : 1.0,
1014
+ created: new Date().toISOString().split('T')[0],
1015
+ };
1016
+ appendEdge(edge);
1017
+ console.log(`✅ Edge added: ${from} --[${typeArg}]--> ${to} (weight: ${edge.weight})`);
1018
+ break;
1019
+ }
1020
+
1021
+ case 'nodes': {
1022
+ const subcommand = args[1];
1023
+ if (!subcommand || subcommand === 'list') {
1024
+ const ids = listNodes();
1025
+ if (ids.length === 0) { console.log('No knowledge nodes yet. Try: booklib note "title"'); break; }
1026
+ console.log(`\n📝 Knowledge nodes (${ids.length}):\n`);
1027
+ for (const id of ids) {
1028
+ const raw = loadNode(id);
1029
+ const parsed = raw ? parseNodeFrontmatter(raw) : {};
1030
+ const tags = Array.isArray(parsed.tags) ? parsed.tags.join(', ') : '';
1031
+ console.log(` ${id} [${parsed.type ?? '?'}] ${parsed.title ?? '?'}${tags ? ` (${tags})` : ''}`);
1032
+ }
1033
+ break;
1034
+ }
1035
+ if (subcommand === 'show') {
1036
+ const id = args[2];
1037
+ if (!id) { console.error('Usage: booklib nodes show <id>'); process.exit(1); }
1038
+ const raw = loadNode(id);
1039
+ if (!raw) { console.error(`Node "${id}" not found.`); process.exit(1); }
1040
+ console.log(raw);
1041
+ break;
1042
+ }
1043
+ console.error('Usage: booklib nodes list | booklib nodes show <id>');
1044
+ process.exit(1);
1045
+ }
1046
+
1047
+ case 'dictate': {
1048
+ const isRaw = args.includes('--raw');
1049
+ const titleArg = parseFlag(args, 'title');
1050
+
1051
+ const stdinText = await readStdin();
1052
+ const rawText = stdinText || await readInteractive();
1053
+
1054
+ if (!rawText) { console.error('No input provided.'); process.exit(1); }
1055
+
1056
+ const id = generateNodeId('node');
1057
+ let nodeContent;
1058
+
1059
+ if (isRaw) {
1060
+ const title = titleArg ?? rawText.split('\n')[0].slice(0, 60);
1061
+ nodeContent = serializeNode({ id, type: 'note', title, content: rawText });
1062
+ } else {
1063
+ console.log('Structuring with AI...');
1064
+ let structured;
1065
+ try {
1066
+ structured = await callAnthropicAPI(buildDictatePrompt(rawText));
1067
+ } catch (err) {
1068
+ console.error(`AI structuring failed: ${err.message}`);
1069
+ console.error('Tip: use --raw to save without AI processing.');
1070
+ const title = titleArg ?? rawText.split('\n')[0].slice(0, 60);
1071
+ nodeContent = serializeNode({ id, type: 'note', title, content: rawText });
1072
+ }
1073
+ if (structured) {
1074
+ nodeContent = `---\nid: "${id}"\n` + structured.replace(/^---\n?/, '');
1075
+ }
1076
+ }
1077
+
1078
+ const filePath = saveNode(nodeContent, id);
1079
+ await autoIndexNode(filePath);
1080
+ try {
1081
+ const savedRaw = loadNode(id);
1082
+ const savedParsed = savedRaw ? parseNodeFrontmatter(savedRaw) : {};
1083
+ const autoLinked = await autoLink({
1084
+ nodeId: id,
1085
+ title: savedParsed.title ?? titleArg ?? '',
1086
+ content: savedParsed.body ?? rawText ?? '',
1087
+ });
1088
+ if (autoLinked.length > 0) {
1089
+ console.log(` Auto-linked: ${autoLinked.map(l => `${l.to} (${l.type})`).join(', ')}`);
1090
+ }
1091
+ } catch { /* best-effort */ }
1092
+ console.log(`✅ Note saved: ${filePath}`);
1093
+ console.log(` ID: ${id}`);
1094
+ break;
1095
+ }
1096
+
1097
+ case 'save-chat': {
1098
+ const doSummarize = args.includes('--summarize');
1099
+ const titleArg = parseFlag(args, 'title');
1100
+ // Skip flag values consumed by --flag value pairs so they aren't mistaken for a file path
1101
+ const titleIdx = args.indexOf('--title');
1102
+ const consumedIndices = new Set(titleIdx !== -1 ? [titleIdx, titleIdx + 1] : []);
1103
+ const fileArg = args.slice(1).find((a, i) => !a.startsWith('--') && !consumedIndices.has(i + 1));
1104
+
1105
+ let transcript;
1106
+ if (fileArg) {
1107
+ transcript = fs.readFileSync(fileArg, 'utf8').trim();
1108
+ } else {
1109
+ transcript = await readStdin();
1110
+ }
1111
+ if (!transcript) {
1112
+ transcript = openEditor('# Paste or type the conversation here\n\n');
1113
+ }
1114
+ if (!transcript) { console.error('No conversation content provided.'); process.exit(1); }
1115
+
1116
+ const id = generateNodeId('node');
1117
+ let nodeContent;
1118
+
1119
+ if (doSummarize) {
1120
+ console.log('Summarizing conversation with AI...');
1121
+ try {
1122
+ const summary = await callAnthropicAPI(buildSummarizePrompt(transcript, titleArg ?? ''));
1123
+ nodeContent = `---\nid: "${id}"\n` + summary.replace(/^---\n?/, '');
1124
+ } catch (err) {
1125
+ console.error(`AI summarization failed: ${err.message}`);
1126
+ nodeContent = serializeNode({
1127
+ id, type: 'note',
1128
+ title: titleArg ?? 'Conversation transcript',
1129
+ content: transcript,
1130
+ sources: ['conversation'],
1131
+ });
1132
+ }
1133
+ } else {
1134
+ nodeContent = serializeNode({
1135
+ id, type: 'note',
1136
+ title: titleArg ?? 'Conversation transcript',
1137
+ content: transcript,
1138
+ sources: ['conversation'],
1139
+ });
1140
+ }
1141
+
1142
+ const filePath = saveNode(nodeContent, id);
1143
+ await autoIndexNode(filePath);
1144
+ try {
1145
+ const savedRaw = loadNode(id);
1146
+ const savedParsed = savedRaw ? parseNodeFrontmatter(savedRaw) : {};
1147
+ const autoLinked = await autoLink({
1148
+ nodeId: id,
1149
+ title: savedParsed.title ?? titleArg ?? 'Conversation transcript',
1150
+ content: savedParsed.body ?? transcript ?? '',
1151
+ });
1152
+ if (autoLinked.length > 0) {
1153
+ console.log(` Auto-linked: ${autoLinked.map(l => `${l.to} (${l.type})`).join(', ')}`);
1154
+ }
1155
+ } catch { /* best-effort */ }
1156
+ console.log(`✅ Conversation saved: ${filePath}`);
1157
+ console.log(` ID: ${id}`);
1158
+ break;
1159
+ }
1160
+
1161
+ case 'research': {
1162
+ const topic = args.slice(1).join(' ');
1163
+ if (!topic) { console.error('Usage: booklib research "<topic>"'); process.exit(1); }
1164
+ const id = generateNodeId('node');
1165
+ const template = `## Sources\n\n<!-- Add URLs, papers, docs -->\n\n## Key Findings\n\n<!-- Fill in after researching -->\n\n## Summary\n\n<!-- 2-3 sentence summary -->\n`;
1166
+ const nodeContent = serializeNode({
1167
+ id,
1168
+ type: 'research',
1169
+ title: topic,
1170
+ content: template,
1171
+ confidence: 'low',
1172
+ });
1173
+ const filePath = saveNode(nodeContent, id);
1174
+ await autoIndexNode(filePath);
1175
+ try {
1176
+ const autoLinked = await autoLink({ nodeId: id, title: topic, content: template });
1177
+ if (autoLinked.length > 0) {
1178
+ console.log(` Auto-linked: ${autoLinked.map(l => `${l.to} (${l.type})`).join(', ')}`);
1179
+ }
1180
+ } catch { /* best-effort */ }
1181
+ console.log(`✅ Research template created: ${filePath}`);
1182
+ console.log(` ID: ${id}`);
1183
+ console.log(` Fill in the findings — this node is already indexed and searchable.`);
1184
+ break;
1185
+ }
1186
+
1187
+ case 'uninstall': {
1188
+ const skillName = args[1];
1189
+ if (!skillName) {
1190
+ console.error('Usage: booklib uninstall <skill-name>');
1191
+ process.exit(1);
1192
+ }
1193
+ const fetcher = new SkillFetcher();
1194
+ fetcher.desyncFromClaudeSkills({ name: skillName });
1195
+ const remaining = countInstalledSlots();
1196
+ console.log(`✓ Removed ${skillName} from ~/.claude/skills/`);
1197
+ console.log(` ${remaining}/${SKILL_LIMIT} slots now used`);
1198
+ break;
1199
+ }
1200
+
1201
+ case 'list': {
1202
+ const names = listInstalledSkillNames();
1203
+ const slots = countInstalledSlots();
1204
+ if (names.length === 0) {
1205
+ console.log('No BookLib-managed skills installed. Run "booklib init" to get started.');
1206
+ break;
1207
+ }
1208
+ console.log(`\nInstalled skills (${slots}/${SKILL_LIMIT} slots):\n`);
1209
+ for (const name of names) console.log(` · ${name}`);
1210
+ console.log('');
1211
+ if (slots > SKILL_LIMIT - 4) console.log(' ⚠ Approaching slot limit. Run "booklib doctor" to review.');
1212
+ break;
1213
+ }
1214
+
1215
+ case 'doctor': {
1216
+ const installHook = args.includes('--install-hook');
1217
+ const showUsage = args.includes('--usage');
1218
+ const cure = args.includes('--cure');
1219
+
1220
+ if (installHook) {
1221
+ try {
1222
+ const result = installTrackingHook();
1223
+ if (result.alreadyInstalled) {
1224
+ console.log(' Hook already installed — nothing changed.');
1225
+ } else {
1226
+ console.log('✓ Tracking hook installed');
1227
+ console.log(` Script: ${result.scriptPath}`);
1228
+ console.log(` Hook: ${result.settingsPath} → PreToolUse[Skill]`);
1229
+ console.log('');
1230
+ console.log(' Skill usage will be tracked from now on.');
1231
+ console.log(' Run `booklib doctor` after a few sessions to see your report.');
1232
+ }
1233
+ } catch (err) {
1234
+ console.error(`Failed to install hook: ${err.message}`);
1235
+ process.exit(1);
1236
+ }
1237
+ break;
1238
+ }
1239
+
1240
+ if (showUsage) {
1241
+ // Legacy usage report (moved behind --usage flag)
1242
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
1243
+ const SKILL_NAME_PAD = 24;
1244
+ const USE_LABEL_PAD = 9;
1245
+
1246
+ const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
1247
+ const usagePath = path.join(os.homedir(), '.booklib', 'usage.json');
1248
+ const installedNames = listInstalledSkillNames();
1249
+ const usageData = readUsage(usagePath);
1250
+
1251
+ if (installedNames.length === 0) {
1252
+ console.log('\n No BookLib-managed skills installed. Run "booklib init" to get started.\n');
1253
+ break;
1254
+ }
1255
+
1256
+ const installDates = {};
1257
+ for (const name of installedNames) {
1258
+ try {
1259
+ const stat = fs.statSync(path.join(claudeSkillsDir, name, '.booklib'));
1260
+ installDates[name] = stat.mtime;
1261
+ } catch { /* unknown install date */ }
1262
+ }
1263
+
1264
+ const summary = summarize(usageData, installedNames, installDates);
1265
+ const suggestions = summary.filter(s => s.suggestion !== null);
1266
+
1267
+ console.log('\n► Skill usage report\n');
1268
+
1269
+ const active = summary.filter(s => s.uses > 0 || s.suggestion !== null);
1270
+ const silentCount = summary.length - active.length;
1271
+
1272
+ const noUsageFile = !fs.existsSync(usagePath);
1273
+ if (active.length === 0 && noUsageFile) {
1274
+ console.log(` ${installedNames.length} community skill${installedNames.length === 1 ? '' : 's'} in ~/.booklib/skills/. No usage data yet.\n`);
1275
+ console.log(' Tip: run `booklib doctor --install-hook` to start tracking usage automatically.');
1276
+ } else if (active.length === 0) {
1277
+ console.log(` ${installedNames.length} community skill${installedNames.length === 1 ? '' : 's'} in ~/.booklib/skills/. No usage data yet.\n`);
1278
+ } else {
1279
+ for (const item of active) {
1280
+ const icon = item.suggestion ? '⚠' : '✓';
1281
+ const useLabel = item.uses === 1 ? '1 use ' : `${item.uses} uses`;
1282
+ let whenLabel;
1283
+ if (item.lastUsed === null) {
1284
+ const days = installDates[item.name]
1285
+ ? Math.floor((Date.now() - installDates[item.name].getTime()) / MS_PER_DAY)
1286
+ : null;
1287
+ whenLabel = days !== null ? `never — installed ${days} days ago` : 'never';
1288
+ } else {
1289
+ whenLabel = `${item.daysSinceLastUse} day${item.daysSinceLastUse === 1 ? '' : 's'} ago`;
1290
+ }
1291
+ console.log(` ${icon} ${item.name.padEnd(SKILL_NAME_PAD)} ${useLabel.padEnd(USE_LABEL_PAD)} (${whenLabel})`);
1292
+ }
1293
+ if (silentCount > 0) {
1294
+ console.log(`\n ${silentCount} other skill${silentCount === 1 ? '' : 's'} — no usage recorded`);
1295
+ }
1296
+ }
1297
+
1298
+ if (suggestions.length > 0) {
1299
+ console.log('\n Suggestions:');
1300
+ for (const item of suggestions) {
1301
+ if (item.suggestion === 'remove') {
1302
+ console.log(` · ${item.name}: never used — consider removing (booklib uninstall ${item.name})`);
1303
+ } else {
1304
+ const days = item.daysSinceLastUse ?? 60;
1305
+ console.log(` · ${item.name}: ${item.uses} use${item.uses === 1 ? '' : 's'} in ${days} days — low activity`);
1306
+ }
1307
+ }
1308
+ console.log('\n Run `booklib uninstall <skill>` to free up slots.');
1309
+ }
1310
+
1311
+ if (noUsageFile && active.length > 0) {
1312
+ console.log('\n Tip: run `booklib doctor --install-hook` to start tracking usage.');
1313
+ }
1314
+
1315
+ console.log('');
1316
+ break;
1317
+ }
1318
+
1319
+ // Default: run diagnostics
1320
+ const { runDiagnostics, printDiagnostics } = await import('../lib/engine/doctor.js');
1321
+
1322
+ console.log('\n BookLib Health Check\n');
1323
+ const findings = runDiagnostics(process.cwd());
1324
+ printDiagnostics(findings);
1325
+
1326
+ if (cure && findings.some(f => f.fixable)) {
1327
+ console.log(' Applying fixes...\n');
1328
+
1329
+ for (const f of findings) {
1330
+ if (!f.fixable) continue;
1331
+
1332
+ if (f.check === 'missing-index') {
1333
+ console.log(' Building search index...');
1334
+ const indexer = new BookLibIndexer();
1335
+ const { skillsPath } = resolveBookLibPaths();
1336
+ await indexer.indexDirectory(skillsPath, false, { quiet: true });
1337
+ console.log(' Index built.\n');
1338
+ }
1339
+ }
1340
+
1341
+ // Re-run diagnostics to show updated state
1342
+ const updated = runDiagnostics(process.cwd());
1343
+ const remaining = updated.filter(f => f.fixable).length;
1344
+ if (remaining === 0) {
1345
+ console.log(' All fixable issues resolved.\n');
1346
+ } else {
1347
+ console.log(` ${remaining} issue(s) remain that require manual intervention.\n`);
1348
+ }
1349
+ }
1350
+
1351
+ break;
1352
+ }
1353
+
1354
+ case 'correction': {
1355
+ const sub = args[1];
1356
+
1357
+ if (!sub || sub === 'help') {
1358
+ console.log('\nUsage:');
1359
+ console.log(' booklib correction add "<text>" — record a correction');
1360
+ console.log(' booklib correction list — show all corrections');
1361
+ console.log(' booklib correction remove <id> — delete a correction\n');
1362
+ break;
1363
+ }
1364
+
1365
+ if (sub === 'add') {
1366
+ const text = args.slice(2).join(' ').replace(/^["']|["']$/g, '');
1367
+ if (!text) {
1368
+ console.error(' Usage: booklib correction add "text of the correction"');
1369
+ process.exit(1);
1370
+ }
1371
+ process.stdout.write(' Recording correction (loading embedding model)...\n');
1372
+ const result = await addCorrection(text);
1373
+ const levelUp = result.wasExisting && result.level > levelFromMentions(result.mentions - 1);
1374
+ const action = result.wasExisting ? 'Updated' : 'Recorded';
1375
+ const arrow = levelUp ? ' ↑' : '';
1376
+ console.log(`✓ ${action}: "${result.text}" (mentions: ${result.mentions}, level: ${result.level}${arrow})`);
1377
+ if (levelUp && result.level >= 3) {
1378
+ console.log(` → ~/.claude/CLAUDE.md updated`);
1379
+ }
1380
+ break;
1381
+ }
1382
+
1383
+ if (sub === 'list') {
1384
+ const all = listCorrections();
1385
+ if (all.length === 0) {
1386
+ console.log('\n No corrections recorded yet.\n');
1387
+ break;
1388
+ }
1389
+ console.log(`\n► Learned corrections (${all.length} total)\n`);
1390
+ console.log(` ${'ID'.padEnd(8)} ${'Mentions'.padEnd(10)} ${'Level'.padEnd(7)} Text`);
1391
+ for (const c of all) {
1392
+ const marker = c.level >= 3 ? '●' : ' ';
1393
+ const lvl = `${c.level} ${marker}`;
1394
+ console.log(` ${c.id.padEnd(8)} ${String(c.mentions).padEnd(10)} ${lvl.padEnd(7)} ${c.text.slice(0, 60)}`);
1395
+ }
1396
+ console.log('\n ● = injected into ~/.claude/CLAUDE.md\n');
1397
+ break;
1398
+ }
1399
+
1400
+ if (sub === 'remove') {
1401
+ const id = args[2];
1402
+ if (!id) {
1403
+ console.error(' Usage: booklib correction remove <id>');
1404
+ process.exit(1);
1405
+ }
1406
+ const removed = removeCorrection(id);
1407
+ if (!removed) {
1408
+ console.error(` Not found: ${id}`);
1409
+ process.exit(1);
1410
+ }
1411
+ console.log(`✓ Removed "${removed.text}"`);
1412
+ console.log(` → ~/.claude/CLAUDE.md updated`);
1413
+ break;
1414
+ }
1415
+
1416
+ console.error(` Unknown subcommand: ${sub}`);
1417
+ console.error(' Use: booklib correction add|list|remove');
1418
+ process.exit(1);
1419
+ }
1420
+
1421
+ case 'rules': {
1422
+ const subcommand = args[1];
1423
+
1424
+ switch (subcommand) {
1425
+ case 'list': {
1426
+ const available = listAvailableRules();
1427
+ console.log('\n► Available rule sets\n');
1428
+ console.log(` ${'Bundled:'.padEnd(22)} ${'project'.padEnd(12)} global`);
1429
+ for (const item of available) {
1430
+ const icon = (item.installedProject || item.installedGlobal) ? '✓' : '·';
1431
+ const proj = item.installedProject ? 'installed' : '—';
1432
+ const glob = item.installedGlobal ? 'installed' : '—';
1433
+ console.log(` ${icon} ${item.lang.padEnd(22)} ${proj.padEnd(12)} ${glob}`);
1434
+ }
1435
+ console.log('');
1436
+ console.log(' booklib rules install <lang> → add to .cursor/rules/');
1437
+ console.log(' booklib rules install <lang> --global → add to global agent config');
1438
+ console.log('');
1439
+ break;
1440
+ }
1441
+
1442
+ case 'install': {
1443
+ const lang = args[2];
1444
+ if (!lang || lang.startsWith('--')) {
1445
+ console.error(' Usage: booklib rules install <lang> [--global]');
1446
+ process.exit(1);
1447
+ }
1448
+ const isGlobal = args.includes('--global');
1449
+ try {
1450
+ const written = installRuleFn(lang, { global: isGlobal });
1451
+ if (written.length === 0) {
1452
+ console.log(`\n No rule files found for '${lang}'.\n`);
1453
+ break;
1454
+ }
1455
+ if (isGlobal) {
1456
+ const st = rulesStatus();
1457
+ const entry = st.global.find(g => g.lang === lang);
1458
+ const sizeLabel = entry ? formatBytes(entry.sizeBytes) : '';
1459
+ console.log(`\n✓ Installed ${lang} rules globally`);
1460
+ console.log(` ~/.claude/CLAUDE.md → added ${lang} section (${sizeLabel})\n`);
1461
+ } else {
1462
+ console.log(`\n✓ Installed ${lang} rules`);
1463
+ for (const p of written) {
1464
+ console.log(` ${path.relative(process.cwd(), p)} (${formatBytes(fs.statSync(p).size)})`);
1465
+ }
1466
+ console.log('');
1467
+ }
1468
+ } catch (err) {
1469
+ const msg = err.message;
1470
+ const availIdx = msg.indexOf('. Available:');
1471
+ if (availIdx !== -1) {
1472
+ console.error(` ${msg.slice(0, availIdx)}`);
1473
+ console.error(` ${msg.slice(availIdx + 2)}`);
1474
+ } else {
1475
+ console.error(` ${msg}`);
1476
+ }
1477
+ process.exit(1);
1478
+ }
1479
+ break;
1480
+ }
1481
+
1482
+ case 'status': {
1483
+ const st = rulesStatus();
1484
+ console.log('\n► Rules status\n');
1485
+
1486
+ if (st.cursor.length === 0 && st.global.length === 0) {
1487
+ console.log(' No rules installed in current project.\n');
1488
+ console.log(' Tip: booklib rules install <lang> to add standards.\n');
1489
+ break;
1490
+ }
1491
+
1492
+ if (st.cursor.length > 0) {
1493
+ console.log(' .cursor/rules/ (project)');
1494
+ for (const item of st.cursor) {
1495
+ console.log(` ${path.basename(item.path).padEnd(42)} ${formatBytes(item.sizeBytes)}`);
1496
+ }
1497
+ console.log('');
1498
+ }
1499
+
1500
+ if (st.global.length > 0) {
1501
+ console.log(' ~/.claude/CLAUDE.md (global)');
1502
+ for (const item of st.global) {
1503
+ console.log(` ${item.lang.padEnd(42)} ${formatBytes(item.sizeBytes)}`);
1504
+ }
1505
+ console.log('');
1506
+ }
1507
+
1508
+ const projCount = st.cursor.length;
1509
+ const globCount = st.global.length;
1510
+ console.log(` Total: ${formatBytes(st.totalBytes)} across ${projCount} project + ${globCount} global rule(s)\n`);
1511
+ break;
1512
+ }
1513
+
1514
+ default:
1515
+ console.log('\n booklib rules list — show available rule sets');
1516
+ console.log(' booklib rules install <lang> — install to .cursor/rules/');
1517
+ console.log(' booklib rules install <lang> --global — install to global agent config');
1518
+ console.log(' booklib rules status — show installed rules + sizes\n');
1519
+ }
1520
+ break;
1521
+ }
1522
+
1523
+ case 'remember':
1524
+ case 'capture': {
1525
+ const title = parseFlag(args, 'title');
1526
+ const type = parseFlag(args, 'type') ?? 'insight';
1527
+ const tagsArg = parseFlag(args, 'tags') ?? '';
1528
+ const linksArg = parseFlag(args, 'links') ?? '';
1529
+
1530
+ if (!title) {
1531
+ console.error('Usage: booklib capture --title "<title>" [--type insight] [--tags tag1,tag2] [--links "skill:edge-type,...]"');
1532
+ process.exit(1);
1533
+ }
1534
+
1535
+ const tags = tagsArg ? tagsArg.split(',').map(t => t.trim()).filter(Boolean) : [];
1536
+ const links = parseCaptureLinkArgs(linksArg);
1537
+
1538
+ for (const link of links) {
1539
+ if (!EDGE_TYPES.includes(link.type)) {
1540
+ console.error(`Invalid edge type "${link.type}". Valid: ${EDGE_TYPES.join(', ')}`);
1541
+ process.exit(1);
1542
+ }
1543
+ }
1544
+
1545
+ const id = generateNodeId(type);
1546
+ const nodeContent = serializeNode({ id, type, title, tags });
1547
+
1548
+ const globalBookLibDir = path.join(os.homedir(), '.booklib');
1549
+ const globalNodesDir = path.join(globalBookLibDir, 'knowledge', 'nodes');
1550
+ const globalGraphFile = path.join(globalBookLibDir, 'knowledge', 'graph.jsonl');
1551
+
1552
+ const filePath = saveNode(nodeContent, id, { nodesDir: globalNodesDir });
1553
+ await autoIndexNode(filePath);
1554
+
1555
+ const today = new Date().toISOString().split('T')[0];
1556
+ for (const link of links) {
1557
+ appendEdge({ from: id, to: link.to, type: link.type, weight: 1.0, created: today }, { graphFile: globalGraphFile });
1558
+ }
1559
+
1560
+ try {
1561
+ const autoLinked = await autoLink({
1562
+ nodeId: id,
1563
+ title,
1564
+ content: '',
1565
+ tags,
1566
+ nodesDir: globalNodesDir,
1567
+ graphFile: globalGraphFile,
1568
+ });
1569
+ if (autoLinked.length > 0) {
1570
+ console.log(` Auto-linked: ${autoLinked.map(l => `${l.to} (${l.type})`).join(', ')}`);
1571
+ }
1572
+ } catch { /* best-effort */ }
1573
+
1574
+ console.log(`✅ Knowledge node created: ${filePath}`);
1575
+ console.log(` ID: ${id}`);
1576
+ if (links.length > 0) {
1577
+ console.log(` Linked: ${links.map(l => `${l.to} (${l.type})`).join(', ')}`);
1578
+ }
1579
+ break;
1580
+ }
1581
+
1582
+ case 'benchmark': {
1583
+ const { run } = await import('../benchmark/run-eval.js');
1584
+ await run();
1585
+ break;
1586
+ }
1587
+
1588
+ case 'build-wellknown': {
1589
+ const builder = new WellKnownBuilder();
1590
+ const outPath = await builder.build();
1591
+ console.log(`Generated: ${outPath}`);
1592
+ process.exit(0);
1593
+ }
1594
+
1595
+ case 'connect': {
1596
+ const target = args[1];
1597
+ if (!target) {
1598
+ console.error('Usage: booklib connect <url-or-path> [--type=<type>] [--name=<name>] [--depth=N] [--include=ext1,ext2] [--exclude=dir1,dir2] [--watch]');
1599
+ console.error(' booklib connect github <releases|wiki|discussions> <owner/repo>');
1600
+ console.error(' booklib connect notion <page|database|search> <id-or-query>');
1601
+ process.exit(1);
1602
+ }
1603
+
1604
+ // Notion subcommand — fetch pages, databases, or search results via Notion API
1605
+ if (target === 'notion') {
1606
+ const subcommand = args[2];
1607
+ const targetId = args[3];
1608
+
1609
+ if (!subcommand || !targetId) {
1610
+ console.error('Usage: booklib connect notion <page|database|search> <id-or-query>');
1611
+ process.exit(1);
1612
+ }
1613
+
1614
+ const { NotionConnector } = await import('../lib/connectors/notion.js');
1615
+ const notion = new NotionConnector();
1616
+
1617
+ const auth = notion.checkAuth();
1618
+ if (!auth.ok) {
1619
+ console.error(auth.error);
1620
+ process.exit(1);
1621
+ }
1622
+
1623
+ const sourceName = parseFlag(args, 'name') ?? `notion-${subcommand}-${targetId.slice(0, 12)}`;
1624
+ const outputDir = path.join('.booklib', 'sources', sourceName);
1625
+
1626
+ console.log(`Fetching from Notion (${subcommand})...`);
1627
+
1628
+ let result;
1629
+ try {
1630
+ switch (subcommand) {
1631
+ case 'page':
1632
+ result = await notion.fetchPage(targetId, outputDir);
1633
+ break;
1634
+ case 'database':
1635
+ result = await notion.fetchDatabase(targetId, outputDir);
1636
+ break;
1637
+ case 'search':
1638
+ result = await notion.fetchSearch(targetId, outputDir);
1639
+ break;
1640
+ default:
1641
+ console.error(`Unknown subcommand: ${subcommand}. Use: page, database, search`);
1642
+ process.exit(1);
1643
+ }
1644
+ } catch (err) {
1645
+ console.error(`Notion fetch failed: ${err.message}`);
1646
+ process.exit(1);
1647
+ }
1648
+
1649
+ if (result.pageCount === 0) {
1650
+ console.log('No pages found.');
1651
+ break;
1652
+ }
1653
+
1654
+ console.log(`Fetched ${result.pageCount} page(s).`);
1655
+
1656
+ const { detectSourceType } = await import('../lib/engine/source-detector.js');
1657
+ const detected = detectSourceType(outputDir);
1658
+ const sourceType = parseFlag(args, 'type') ?? detected.type;
1659
+
1660
+ const { SourceManager } = await import('../lib/engine/source-manager.js');
1661
+ const mgr = new SourceManager(path.join(process.cwd(), '.booklib'));
1662
+ mgr.registerSource({ name: sourceName, sourcePath: outputDir, type: sourceType });
1663
+
1664
+ const indexer = new BookLibIndexer();
1665
+ await indexer.indexDirectory(outputDir, false, { sourceName });
1666
+
1667
+ console.log(`Indexed as "${sourceName}" (type: ${sourceType}).`);
1668
+ break;
1669
+ }
1670
+
1671
+ // GitHub subcommand — fetch releases, wiki, or discussions via gh CLI
1672
+ if (target === 'github') {
1673
+ const subcommand = args[2];
1674
+ const repo = args[3];
1675
+
1676
+ if (!subcommand || !repo) {
1677
+ console.error('Usage: booklib connect github <releases|wiki|discussions> <owner/repo>');
1678
+ process.exit(1);
1679
+ }
1680
+
1681
+ const { GitHubConnector } = await import('../lib/connectors/github.js');
1682
+ const gh = new GitHubConnector();
1683
+
1684
+ const auth = gh.checkAuth();
1685
+ if (!auth.ok) {
1686
+ console.error(auth.error);
1687
+ process.exit(1);
1688
+ }
1689
+
1690
+ const sourceName = parseFlag(args, 'name') ?? `github-${repo.replace('/', '-')}-${subcommand}`;
1691
+ const outputDir = path.join('.booklib', 'sources', sourceName);
1692
+
1693
+ console.log(`Fetching ${subcommand} from ${repo}...`);
1694
+
1695
+ let result;
1696
+ try {
1697
+ switch (subcommand) {
1698
+ case 'releases':
1699
+ result = await gh.fetchReleases(repo, outputDir);
1700
+ break;
1701
+ case 'wiki':
1702
+ result = await gh.fetchWiki(repo, outputDir);
1703
+ break;
1704
+ case 'discussions':
1705
+ result = await gh.fetchDiscussions(repo, outputDir);
1706
+ break;
1707
+ default:
1708
+ console.error(`Unknown subcommand: ${subcommand}. Use: releases, wiki, discussions`);
1709
+ process.exit(1);
1710
+ }
1711
+ } catch (err) {
1712
+ console.error(`GitHub fetch failed: ${err.message}`);
1713
+ process.exit(1);
1714
+ }
1715
+
1716
+ if (result.pageCount === 0) {
1717
+ console.log(`No ${subcommand} found for ${repo}.`);
1718
+ break;
1719
+ }
1720
+
1721
+ console.log(`Fetched ${result.pageCount} ${subcommand}.`);
1722
+
1723
+ const { detectSourceType } = await import('../lib/engine/source-detector.js');
1724
+ const detected = detectSourceType(outputDir);
1725
+ const sourceType = parseFlag(args, 'type') ?? detected.type;
1726
+
1727
+ const { SourceManager } = await import('../lib/engine/source-manager.js');
1728
+ const mgr = new SourceManager(path.join(process.cwd(), '.booklib'));
1729
+ mgr.registerSource({ name: sourceName, sourcePath: outputDir, type: sourceType, url: `https://github.com/${repo}` });
1730
+
1731
+ const indexer = new BookLibIndexer();
1732
+ await indexer.indexDirectory(outputDir, false, { sourceName });
1733
+
1734
+ console.log(`Indexed ${result.pageCount} ${subcommand} from ${repo} as "${sourceName}" (type: ${sourceType}).`);
1735
+ break;
1736
+ }
1737
+
1738
+ const isUrl = target.startsWith('http://') || target.startsWith('https://');
1739
+ const typeFlag = parseFlag(args, 'type');
1740
+ const name = parseFlag(args, 'name') ?? undefined;
1741
+
1742
+ const { SourceManager } = await import('../lib/engine/source-manager.js');
1743
+ const booklibDir = path.join(process.cwd(), '.booklib');
1744
+ const mgr = new SourceManager(booklibDir);
1745
+
1746
+ if (isUrl) {
1747
+ // Web connector: scrape URL into local markdown, then index
1748
+ const depth = parseInt(parseFlag(args, 'depth') ?? '1', 10);
1749
+ const { WebConnector } = await import('../lib/connectors/web.js');
1750
+ const wc = new WebConnector({ depth });
1751
+
1752
+ const sourceName = name ?? new URL(target).hostname.replace(/\./g, '-');
1753
+ const outputDir = path.join(booklibDir, 'sources', sourceName);
1754
+
1755
+ console.log(`Scraping ${target} (depth=${depth})...`);
1756
+ const { pageCount } = await wc.scrape(target, outputDir);
1757
+ console.log(`Scraped ${pageCount} page(s) to ${outputDir}`);
1758
+
1759
+ // Auto-detect source type from scraped content when --type not provided
1760
+ let type;
1761
+ if (typeFlag) {
1762
+ type = typeFlag;
1763
+ } else {
1764
+ const { detectSourceType } = await import('../lib/engine/source-detector.js');
1765
+ const detection = detectSourceType(outputDir);
1766
+ type = detection.type;
1767
+ console.log(` Detected source type: ${detection.type} (confidence: ${detection.confidence})`);
1768
+ }
1769
+
1770
+ let source;
1771
+ try {
1772
+ source = mgr.registerSource({ name: sourceName, sourcePath: outputDir, type, url: target });
1773
+ } catch (err) { console.error(err.message); process.exit(1); }
1774
+ console.log(`Registered source "${source.name}" (${outputDir})`);
1775
+
1776
+ // Index the scraped markdown — rollback registration on failure
1777
+ try {
1778
+ const indexer = new BookLibIndexer();
1779
+ console.log('Indexing source...');
1780
+ await indexer.indexDirectory(outputDir, false, { quiet: false, sourceName: source.name });
1781
+
1782
+ const { BM25Index: BM25 } = await import('../lib/engine/bm25-index.js');
1783
+ const bm25File = indexer.bm25Path;
1784
+ let chunkCount = 0;
1785
+ if (fs.existsSync(bm25File)) {
1786
+ const idx = BM25.load(bm25File);
1787
+ chunkCount = idx._docs.filter(d => d.metadata?.sourceName === source.name).length;
1788
+ }
1789
+ mgr.markIndexed(source.name, chunkCount);
1790
+ console.log(`Source "${source.name}" connected and indexed (${chunkCount} chunks).`);
1791
+ } catch (indexErr) {
1792
+ try { mgr.removeSource(source.name); } catch { /* best effort */ }
1793
+ console.error(`Indexing failed for "${source.name}": ${indexErr.message}`);
1794
+ console.error('Source registration rolled back.');
1795
+ process.exit(1);
1796
+ }
1797
+ } else {
1798
+ // Local path connector — with filtering, mtime tracking, and optional watch
1799
+ const resolvedPath = path.resolve(target);
1800
+ if (!fs.existsSync(resolvedPath)) {
1801
+ console.error(`Path does not exist: ${resolvedPath}`);
1802
+ process.exit(1);
1803
+ }
1804
+
1805
+ const includeArg = parseFlag(args, 'include');
1806
+ const excludeArg = parseFlag(args, 'exclude');
1807
+ const include = includeArg ? includeArg.split(',').map(s => s.trim()) : undefined;
1808
+ const exclude = excludeArg ? excludeArg.split(',').map(s => s.trim()) : undefined;
1809
+
1810
+ const { LocalConnector } = await import('../lib/connectors/local.js');
1811
+ const lc = new LocalConnector({ include, exclude });
1812
+ const matchingFiles = lc.listFiles(resolvedPath);
1813
+ console.log(`Found ${matchingFiles.length} file(s) matching filters.`);
1814
+
1815
+ // Auto-detect source type from directory content when --type not provided
1816
+ let type;
1817
+ if (typeFlag) {
1818
+ type = typeFlag;
1819
+ } else {
1820
+ const { detectSourceType } = await import('../lib/engine/source-detector.js');
1821
+ const detection = detectSourceType(resolvedPath);
1822
+ type = detection.type;
1823
+ console.log(` Detected source type: ${detection.type} (confidence: ${detection.confidence})`);
1824
+ }
1825
+
1826
+ let source;
1827
+ try {
1828
+ source = mgr.registerSource({ name, sourcePath: resolvedPath, type });
1829
+ } catch (err) {
1830
+ console.error(err.message);
1831
+ process.exit(1);
1832
+ }
1833
+ console.log(`Registered source "${source.name}" (${resolvedPath})`);
1834
+
1835
+ // Index the source directory — rollback registration on failure
1836
+ try {
1837
+ const indexer = new BookLibIndexer();
1838
+ console.log('Indexing source...');
1839
+ await indexer.indexDirectory(resolvedPath, false, { quiet: false, sourceName: source.name });
1840
+
1841
+ const { BM25Index: BM25 } = await import('../lib/engine/bm25-index.js');
1842
+ const bm25File = indexer.bm25Path;
1843
+ let chunkCount = 0;
1844
+ if (fs.existsSync(bm25File)) {
1845
+ const idx = BM25.load(bm25File);
1846
+ chunkCount = idx._docs.filter(d => d.metadata?.sourceName === source.name).length;
1847
+ }
1848
+ mgr.markIndexed(source.name, chunkCount);
1849
+
1850
+ // Store file mtimes for incremental refresh
1851
+ const mtimes = lc.getFileMtimes(resolvedPath);
1852
+ mgr.updateMtimes(source.name, mtimes);
1853
+
1854
+ console.log(`Source "${source.name}" connected and indexed (${chunkCount} chunks).`);
1855
+ } catch (indexErr) {
1856
+ try { mgr.removeSource(source.name); } catch { /* best effort */ }
1857
+ console.error(`Indexing failed for "${source.name}": ${indexErr.message}`);
1858
+ console.error('Source registration rolled back.');
1859
+ process.exit(1);
1860
+ }
1861
+
1862
+ // Optional: watch for changes and re-index
1863
+ if (args.includes('--watch')) {
1864
+ const indexer = new BookLibIndexer();
1865
+ const watcher = lc.watch(resolvedPath, async (eventType, filename) => {
1866
+ console.log(` ${eventType}: ${filename}`);
1867
+ await indexer.indexDirectory(resolvedPath, false, { quiet: true, sourceName: source.name });
1868
+ const updatedMtimes = lc.getFileMtimes(resolvedPath);
1869
+ mgr.updateMtimes(source.name, updatedMtimes);
1870
+ console.log(` Re-indexed ${source.name}`);
1871
+ });
1872
+ process.on('SIGINT', () => { watcher.close(); process.exit(0); });
1873
+ }
1874
+ }
1875
+ break;
1876
+ }
1877
+
1878
+ case 'disconnect': {
1879
+ const disconnectName = args[1];
1880
+ if (!disconnectName) {
1881
+ console.error('Usage: booklib disconnect <name>');
1882
+ process.exit(1);
1883
+ }
1884
+ const { SourceManager } = await import('../lib/engine/source-manager.js');
1885
+ const booklibDir = path.join(process.cwd(), '.booklib');
1886
+ const mgr = new SourceManager(booklibDir);
1887
+
1888
+ const source = mgr.getSource(disconnectName);
1889
+ if (!source) {
1890
+ console.error(`Source not found: "${disconnectName}". Run 'booklib sources' to see registered sources.`);
1891
+ process.exit(1);
1892
+ }
1893
+
1894
+ const indexer = new BookLibIndexer();
1895
+ await removeSourceChunks(disconnectName, indexer.bm25Path, indexer);
1896
+
1897
+ mgr.removeSource(disconnectName);
1898
+ console.log(`Source "${disconnectName}" disconnected.`);
1899
+ break;
1900
+ }
1901
+
1902
+ case 'sources': {
1903
+ const { SourceManager } = await import('../lib/engine/source-manager.js');
1904
+ const booklibDir = path.join(process.cwd(), '.booklib');
1905
+ const mgr = new SourceManager(booklibDir);
1906
+ const sources = mgr.listSources();
1907
+
1908
+ if (sources.length === 0) {
1909
+ console.log('No sources connected. Use `booklib connect <path>` to add one.');
1910
+ break;
1911
+ }
1912
+
1913
+ console.log(`\n ${'Name'.padEnd(20)} ${'Type'.padEnd(8)} ${'Chunks'.padEnd(8)} Path`);
1914
+ console.log(` ${'─'.repeat(20)} ${'─'.repeat(8)} ${'─'.repeat(8)} ${'─'.repeat(30)}`);
1915
+ for (const s of sources) {
1916
+ const chunks = s.chunk_count != null ? String(s.chunk_count) : '-';
1917
+ console.log(` ${s.name.padEnd(20)} ${s.type.padEnd(8)} ${chunks.padEnd(8)} ${s.sourcePath}`);
1918
+ }
1919
+ console.log();
1920
+ break;
1921
+ }
1922
+
1923
+ case 'refresh': {
1924
+ const refreshName = args[1];
1925
+ if (!refreshName) {
1926
+ console.error('Usage: booklib refresh <name> [--every 5m]');
1927
+ process.exit(1);
1928
+ }
1929
+ const { SourceManager } = await import('../lib/engine/source-manager.js');
1930
+ const booklibDir = path.join(process.cwd(), '.booklib');
1931
+ const mgr = new SourceManager(booklibDir);
1932
+
1933
+ const source = mgr.getSource(refreshName);
1934
+ if (!source) {
1935
+ console.error(`Source not found: "${refreshName}". Run 'booklib sources' to see registered sources.`);
1936
+ process.exit(1);
1937
+ }
1938
+
1939
+ if (!fs.existsSync(source.sourcePath)) {
1940
+ console.error(`Source path no longer exists: ${source.sourcePath}`);
1941
+ process.exit(1);
1942
+ }
1943
+
1944
+ /** Run a single refresh cycle for the source. */
1945
+ const runRefresh = async () => {
1946
+ console.log(`Refreshing source "${refreshName}" from ${source.sourcePath}...`);
1947
+
1948
+ const isLocalSource = source.type === 'local' || (!source.url && fs.statSync(source.sourcePath).isDirectory());
1949
+ const indexer = new BookLibIndexer();
1950
+
1951
+ if (isLocalSource) {
1952
+ const { LocalConnector } = await import('../lib/connectors/local.js');
1953
+ const lc = new LocalConnector();
1954
+ const previousMtimes = mgr.getMtimes(refreshName);
1955
+ const { changed, removed, currentMtimes } = lc.findChanges(source.sourcePath, previousMtimes);
1956
+
1957
+ if (changed.length === 0 && removed.length === 0) {
1958
+ console.log(`Source "${refreshName}" is up to date — no changes detected.`);
1959
+ return;
1960
+ }
1961
+
1962
+ console.log(` ${changed.length} changed, ${removed.length} removed`);
1963
+
1964
+ await removeSourceChunks(refreshName, indexer.bm25Path, indexer);
1965
+ await indexer.indexDirectory(source.sourcePath, false, { quiet: false, sourceName: refreshName });
1966
+
1967
+ mgr.updateMtimes(refreshName, currentMtimes);
1968
+ } else {
1969
+ await removeSourceChunks(refreshName, indexer.bm25Path, indexer);
1970
+ await indexer.indexDirectory(source.sourcePath, false, { quiet: false, sourceName: refreshName });
1971
+ }
1972
+
1973
+ let chunkCount = 0;
1974
+ if (fs.existsSync(indexer.bm25Path)) {
1975
+ const { BM25Index: BM25 } = await import('../lib/engine/bm25-index.js');
1976
+ const idx = BM25.load(indexer.bm25Path);
1977
+ chunkCount = idx._docs.filter(d => d.metadata?.sourceName === refreshName).length;
1978
+ }
1979
+ mgr.markIndexed(refreshName, chunkCount);
1980
+ console.log(`Source "${refreshName}" refreshed (${chunkCount} chunks).`);
1981
+ };
1982
+
1983
+ const every = parseFlag(args, 'every');
1984
+ if (every) {
1985
+ const ms = parseInterval(every);
1986
+ console.log(`Watching "${refreshName}" — refreshing every ${every} (Ctrl+C to stop)`);
1987
+ // Sequential loop: wait for completion before scheduling next run
1988
+ // Prevents overlapping refreshes and handles errors gracefully
1989
+ const loop = async () => {
1990
+ while (true) {
1991
+ try {
1992
+ await runRefresh();
1993
+ console.log(`Refreshed at ${new Date().toLocaleTimeString()}`);
1994
+ } catch (err) {
1995
+ console.error(`Refresh failed: ${err.message} — retrying in ${every}`);
1996
+ }
1997
+ await new Promise(r => setTimeout(r, ms));
1998
+ }
1999
+ };
2000
+ await loop();
2001
+ } else {
2002
+ await runRefresh();
2003
+ }
2004
+ break;
2005
+ }
2006
+
2007
+ case 'gaps': {
2008
+ const { GapDetector } = await import('../lib/engine/gap-detector.js');
2009
+ const detector = new GapDetector();
2010
+ console.log('Scanning project for knowledge gaps...\n');
2011
+ try {
2012
+ const gaps = await detector.detect(process.cwd());
2013
+
2014
+ if (gaps.postTraining.length > 0) {
2015
+ console.log('Post-training dependencies (model may have outdated knowledge):');
2016
+ for (const dep of gaps.postTraining) {
2017
+ const date = dep.publishDate.toISOString().split('T')[0];
2018
+ console.log(` ${dep.name}@${dep.version} (${dep.ecosystem}, published ${date})`);
2019
+ }
2020
+ } else {
2021
+ console.log('No post-training dependencies detected.');
2022
+ }
2023
+
2024
+ if (gaps.uncapturedDocs.length > 0) {
2025
+ console.log('\nUncaptured project docs:');
2026
+ for (const doc of gaps.uncapturedDocs) {
2027
+ const suffix = doc.type === 'directory' ? '/' : '';
2028
+ console.log(` ${doc.path} (${doc.fileCount} file(s))`);
2029
+ console.log(` → booklib connect ./${doc.path}${suffix} --type=team-decision`);
2030
+ }
2031
+ }
2032
+
2033
+ if (gaps.ecosystems.length > 0) {
2034
+ console.log(`\nScanned: ${gaps.totalDeps} dependencies across ${gaps.ecosystems.join(', ')}`);
2035
+ } else {
2036
+ console.log('\nNo dependency ecosystems detected.');
2037
+ }
2038
+ } catch (err) {
2039
+ console.error(`Gap detection failed: ${err.message}`);
2040
+ process.exit(1);
2041
+ }
2042
+ break;
2043
+ }
2044
+
2045
+ case 'fix':
2046
+ case 'resolve-gaps': {
2047
+ const { GapDetector } = await import('../lib/engine/gap-detector.js');
2048
+ const { GapResolver } = await import('../lib/engine/gap-resolver.js');
2049
+
2050
+ console.log('Scanning for gaps...');
2051
+ const detector = new GapDetector();
2052
+ const gaps = await detector.detect(process.cwd());
2053
+
2054
+ if (gaps.postTraining.length === 0) {
2055
+ console.log('No post-training dependencies detected.');
2056
+ break;
2057
+ }
2058
+
2059
+ console.log(`Found ${gaps.postTraining.length} post-training dep(s). Resolving...\n`);
2060
+
2061
+ const resolver = new GapResolver();
2062
+ const results = await resolver.resolveAll(gaps.postTraining, ({ dep, result }) => {
2063
+ const icon = result.resolved ? '\u2713' : '\u2717';
2064
+ console.log(` ${icon} ${dep.name}@${dep.version} \u2014 ${result.source} (${result.pageCount} pages)`);
2065
+ });
2066
+
2067
+ // Index resolved sources
2068
+ const resolvedResults = results.filter(r => r.result.resolved);
2069
+ for (const { result } of resolvedResults) {
2070
+ try {
2071
+ const { SourceManager } = await import('../lib/engine/source-manager.js');
2072
+ const booklibDir = path.join(process.cwd(), '.booklib');
2073
+ const mgr = new SourceManager(booklibDir);
2074
+ const { detectSourceType } = await import('../lib/engine/source-detector.js');
2075
+ const detected = detectSourceType(result.outputDir);
2076
+ mgr.registerSource({ name: result.sourceName, sourcePath: result.outputDir, type: detected.type });
2077
+
2078
+ const indexer = new BookLibIndexer();
2079
+ await indexer.indexDirectory(result.outputDir, false, { sourceName: result.sourceName, quiet: true });
2080
+ } catch (err) {
2081
+ console.warn(` Warning: ${result.sourceName}: ${err.message}`);
2082
+ }
2083
+ }
2084
+
2085
+ // Show suggestions for unresolved
2086
+ const unresolved = results.filter(r => !r.result.resolved);
2087
+ for (const { dep, result } of unresolved) {
2088
+ if (result.suggestion) {
2089
+ console.log(`\n ${dep.name}@${dep.version} \u2014 not resolved\n \u2192 ${result.suggestion}`);
2090
+ }
2091
+ }
2092
+
2093
+ console.log(`\nResolved: ${resolvedResults.length}/${gaps.postTraining.length}`);
2094
+ break;
2095
+ }
2096
+
2097
+ case 'analyze': {
2098
+ const { ProjectAnalyzer } = await import('../lib/engine/project-analyzer.js');
2099
+ console.log('Analyzing project...\n');
2100
+ try {
2101
+ const analyzer = new ProjectAnalyzer();
2102
+ const result = await analyzer.analyze(process.cwd());
2103
+
2104
+ if (result.affected.length === 0) {
2105
+ console.log('No post-training APIs detected in your code.');
2106
+ break;
2107
+ }
2108
+
2109
+ // Group by dep
2110
+ const byDep = new Map();
2111
+ for (const entry of result.affected) {
2112
+ const key = entry.dep.name;
2113
+ if (!byDep.has(key)) byDep.set(key, { dep: entry.dep, files: [] });
2114
+ byDep.get(key).files.push({ file: entry.file, apis: entry.apis });
2115
+ }
2116
+
2117
+ for (const [, { dep, files }] of byDep) {
2118
+ console.log(`\n${dep.name}@${dep.version} (post-training):`);
2119
+ for (const { file, apis } of files) {
2120
+ console.log(` ${file} \u2192 ${apis.join(', ')}`);
2121
+ }
2122
+ }
2123
+
2124
+ console.log(`\n${result.totalFiles} file(s), ${result.totalApis} post-training API(s).`);
2125
+ } catch (err) {
2126
+ console.error(`Analysis failed: ${err.message}`);
2127
+ process.exit(1);
2128
+ }
2129
+ break;
2130
+ }
2131
+
2132
+ case 'guard':
2133
+ case 'check-decisions': {
2134
+ const filePath = args[1];
2135
+ if (!filePath) {
2136
+ console.error('Usage: booklib check-decisions <file>');
2137
+ process.exit(1);
2138
+ }
2139
+ try {
2140
+ const { DecisionChecker } = await import('../lib/engine/decision-checker.js');
2141
+ const { BookLibSearcher } = await import('../lib/engine/searcher.js');
2142
+ const searcher = new BookLibSearcher();
2143
+ const checker = new DecisionChecker({ searcher });
2144
+ const result = await checker.checkFile(filePath);
2145
+
2146
+ if (result.contradictions.length > 0) {
2147
+ console.log(`\u26a0 ${result.contradictions.length} potential contradiction(s):\n`);
2148
+ for (const c of result.contradictions) {
2149
+ console.log(` ${c.identifier} \u2014 contradicts: ${c.source}`);
2150
+ console.log(` "${c.decision.slice(0, 120)}..."`);
2151
+ console.log('');
2152
+ }
2153
+ } else {
2154
+ console.log('No contradictions found.');
2155
+ }
2156
+ console.log(`Checked ${result.checked} identifier(s).`);
2157
+ } catch (err) {
2158
+ console.error(`Decision check failed: ${err.message}`);
2159
+ process.exit(1);
2160
+ }
2161
+ break;
2162
+ }
2163
+
2164
+ case 'verify':
2165
+ case 'check-imports': {
2166
+ const filePath = args[1];
2167
+ if (!filePath) {
2168
+ console.error('Usage: booklib check-imports <file>');
2169
+ process.exit(1);
2170
+ }
2171
+ const { ImportChecker } = await import('../lib/engine/import-checker.js');
2172
+
2173
+ let indexMode = 'manual';
2174
+ try {
2175
+ const cfgPath = path.join(process.cwd(), 'booklib.config.json');
2176
+ if (fs.existsSync(cfgPath)) {
2177
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
2178
+ indexMode = cfg.importChecking ?? 'manual';
2179
+ }
2180
+ } catch { /* use default */ }
2181
+
2182
+ const checker = new ImportChecker({
2183
+ searcher: new BookLibSearcher(),
2184
+ indexMode,
2185
+ });
2186
+
2187
+ const resolved = path.resolve(filePath);
2188
+ console.log(`Checking imports in ${filePath}...\n`);
2189
+ try {
2190
+ const result = await checker.checkFile(resolved, process.cwd());
2191
+
2192
+ if (result.unknown.length > 0) {
2193
+ console.log('Unknown APIs (not in BookLib index):');
2194
+ for (const imp of result.unknown) {
2195
+ const eco = imp.language === 'js' ? 'npm' : imp.language;
2196
+ console.log(` ${imp.module} (${eco})`);
2197
+ const docs = await checker.resolveDocsUrl(imp);
2198
+ if (docs.url) {
2199
+ console.log(` \u2192 booklib connect ${docs.url} --type=framework-docs`);
2200
+ }
2201
+ }
2202
+ }
2203
+
2204
+ const knownCount = result.known.length;
2205
+ const unknownCount = result.unknown.length;
2206
+ const skippedCount = result.skipped.length;
2207
+ console.log(`\nKnown: ${knownCount} imports | Unknown: ${unknownCount} imports | Skipped: ${skippedCount} stdlib`);
2208
+ } catch (err) {
2209
+ console.error(`Import check failed: ${err.message}`);
2210
+ process.exit(1);
2211
+ }
2212
+ break;
2213
+ }
2214
+
2215
+ default: {
2216
+ const showAll = args.includes('--all');
2217
+ if (showAll) {
2218
+ console.log(`
2219
+ BookLib — AI Agent Skill Library (full reference)
2220
+
2221
+ CORE:
2222
+ booklib index [dir] [--clear] Build semantic index (skills + knowledge nodes)
2223
+ booklib search "<query>" Search skills and your knowledge nodes
2224
+ booklib audit <skill> <file> Deep-audit a file against a skill
2225
+ booklib scan [dir] [--docs] Project-wide heatmap
2226
+ booklib gaps Detect post-training deps & uncaptured docs
2227
+ booklib resolve-gaps Auto-resolve gaps via Context7/GitHub/manual
2228
+ booklib analyze Show which APIs in your code have post-training gaps
2229
+ booklib check-imports <file> Check if file imports are covered by BookLib
2230
+ booklib check-decisions <file> Check if code contradicts captured team decisions
2231
+ booklib capture --title "<title>" [--type insight] [--tags t1,t2] [--links "skill:edge-type,...]"
2232
+ booklib benchmark Run retrieval quality benchmark (MRR/Recall/NDCG)
2233
+ booklib context "<task>" [--prompt-only] Cross-skill context + conflict resolution
2234
+ booklib context "<task>" --file <path> Also injects graph context for the file's component
2235
+
2236
+ KNOWLEDGE GRAPH:
2237
+ booklib note "<title>" Create a note (pipe content via stdin, or opens editor)
2238
+ booklib dictate [--raw] [--title "<t>"] Type/speak rough thoughts → AI structures → note
2239
+ booklib research "<topic>" Create a research template node to fill in later
2240
+ booklib save-chat [--summarize] [--title "<t>"] Save current conversation as a knowledge node
2241
+ booklib component add <name> "<glob>" Define a project component (e.g. "auth" "src/auth/**")
2242
+ booklib link "<title-or-id>" "<title-or-id>" --type <edge-type> Connect two nodes
2243
+ booklib nodes list List all knowledge nodes
2244
+ booklib nodes show <id> View a specific node
2245
+
2246
+ Edge types: implements · contradicts · extends · applies-to · see-also · inspired-by · supersedes · depends-on
2247
+
2248
+ SKILLS:
2249
+ booklib init [--reset] [--tool=claude|cursor|copilot|gemini|codex|windsurf|roo-code|openhands|junie|goose|opencode|letta|all|auto] [--skills=s1,s2]
2250
+ [--ecc] [--agents] [--commands] [--rules[=kotlin,python]]
2251
+ [--orchestrator=obra|ruflo] [--dry-run]
2252
+ booklib setup Fetch & index all trusted community skills
2253
+ booklib discover [--refresh] List available community skills
2254
+ booklib install <skill-name> Install a skill
2255
+ booklib fetch <skill-name> (deprecated) Use: booklib install
2256
+ booklib add <skill-id-or-url> (deprecated) Use: booklib install
2257
+ booklib rules list|install <lang>|status Manage always-on language rules
2258
+
2259
+ SESSION HANDOFF:
2260
+ booklib save-state --goal=".." --next=".." Save agent context
2261
+ booklib resume [session-name] Resume last session
2262
+ booklib recover-auto Auto-recover from session or git
2263
+
2264
+ SESSION MANAGEMENT:
2265
+ booklib sessions cleanup --before 90days Archive old sessions
2266
+ booklib sessions diff <id1> <id2> Compare two sessions
2267
+ booklib sessions find <name> Find session (local+global)
2268
+ booklib sessions search <query> Search by content
2269
+ booklib sessions tag <id> --add=tag1,tag2 Tag sessions
2270
+ booklib sessions validate [id] Check quality
2271
+ booklib sessions report [--since "2 weeks"] Team report
2272
+ booklib sessions create --template=<t> <n> Create from template
2273
+ booklib sessions history <id> Version history
2274
+
2275
+ SOURCES:
2276
+ booklib connect <path> [--type=<type>] [--name=<name>] Connect a doc source
2277
+ booklib connect github releases <owner/repo> Index GitHub releases
2278
+ booklib connect github wiki <owner/repo> Index GitHub wiki pages
2279
+ booklib connect github discussions <owner/repo> Index GitHub discussions
2280
+ booklib connect notion page <page-id> Index a Notion page
2281
+ booklib connect notion database <database-id> Index a Notion database
2282
+ booklib connect notion search <query> Index Notion search results
2283
+ booklib disconnect <name> Disconnect a source + remove chunks
2284
+ booklib sources List connected sources
2285
+ booklib refresh <name> [--every 5m] Re-index a source (with optional polling)
2286
+
2287
+ ORCHESTRATOR COMPATIBILITY:
2288
+ booklib sync Sync all fetched skills → ~/.claude/skills/
2289
+
2290
+ SWARM / MULTI-AGENT:
2291
+ booklib profile <role>|--list Skill bundle for an agent role
2292
+ booklib swarm-config [trigger] Trigger → roles → skills pipeline
2293
+ booklib sessions-list|merge|lineage|compare Multi-agent session coordination
2294
+
2295
+ `);
2296
+ } else {
2297
+ console.log(`
2298
+ BookLib v3.0.0 — Context engineering for AI coding assistants
2299
+
2300
+ QUICK START:
2301
+ booklib init Guided setup — detects stack, registers MCP, builds index
2302
+ booklib analyze Show which APIs your AI doesn't know about
2303
+
2304
+ EVERYDAY USE:
2305
+ booklib gaps Detect post-training dependencies
2306
+ booklib fix Auto-resolve gaps via Context7/GitHub
2307
+ booklib analyze Show affected files and post-training APIs
2308
+ booklib search "<query>" Search skills and knowledge
2309
+ booklib verify <file> Flag unknown APIs (11 languages)
2310
+ booklib guard <file> Check code against team decisions
2311
+ booklib doctor Health check for skills and config
2312
+
2313
+ KNOWLEDGE:
2314
+ booklib remember --title "<t>" Save a team decision or insight
2315
+ booklib note "<title>" Create a note (pipe or editor)
2316
+ booklib connect <path> Index local documentation
2317
+ booklib connect github releases <repo> Index GitHub changelogs
2318
+ booklib connect notion database <id> Index Notion pages
2319
+ booklib sources List connected sources
2320
+
2321
+ SKILLS:
2322
+ booklib install <skill-name> Install a skill
2323
+ booklib discover Browse the community skill catalog
2324
+ booklib index Rebuild the search index
2325
+
2326
+ booklib --help --all Show all commands including advanced
2327
+
2328
+ `);
2329
+ }
2330
+ break;
2331
+ }
2332
+ }
2333
+ }
2334
+
2335
+ const NO_NUDGE_COMMANDS = new Set(['help', 'search', 'context', 'audit', 'scan', 'nodes', 'sessions', 'sessions-list']);
2336
+ const BOOKLIB_DIR = path.join(os.homedir(), '.booklib');
2337
+
2338
+ function readCounter(file) {
2339
+ try { return parseInt(fs.readFileSync(file, 'utf8'), 10) || 0; } catch { return 0; }
2340
+ }
2341
+ function writeCounter(file, value) {
2342
+ fs.mkdirSync(BOOKLIB_DIR, { recursive: true });
2343
+ fs.writeFileSync(file, String(value));
2344
+ }
2345
+
2346
+ async function maybeAskFeedback() {
2347
+ // Only in interactive terminals, only on action commands
2348
+ if (!process.stderr.isTTY || !command || NO_NUDGE_COMMANDS.has(command) || args.includes('--help')) return;
2349
+
2350
+ const FEEDBACK_EVERY = 25;
2351
+ const counterFile = path.join(BOOKLIB_DIR, 'feedback-count');
2352
+ const count = readCounter(counterFile);
2353
+ const next = count + 1;
2354
+ writeCounter(counterFile, next);
2355
+ if (next % FEEDBACK_EVERY !== 0) return;
2356
+
2357
+ return new Promise(resolve => {
2358
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
2359
+ rl.question('\n Quick question: is BookLib useful to you? [y/n/skip] ', answer => {
2360
+ rl.close();
2361
+ const a = answer.trim().toLowerCase();
2362
+ if (a === 'y' || a === 'yes') {
2363
+ console.error(' Glad to hear it! A ⭐ helps others find it: https://github.com/booklib-ai/booklib\n');
2364
+ } else if (a === 'n' || a === 'no') {
2365
+ console.error(' Thanks for the honesty. Tell us what\'s missing: https://github.com/booklib-ai/booklib/issues\n');
2366
+ }
2367
+ resolve();
2368
+ });
2369
+ });
2370
+ }
2371
+
2372
+ function maybeNudgeStar() {
2373
+ if (!command || NO_NUDGE_COMMANDS.has(command) || args.includes('--help')) return;
2374
+ const NUDGE_EVERY = 50;
2375
+ const counterFile = path.join(BOOKLIB_DIR, 'nudge-count');
2376
+ try {
2377
+ const next = readCounter(counterFile) + 1;
2378
+ writeCounter(counterFile, next);
2379
+ if (next % NUDGE_EVERY === 0) {
2380
+ console.error('\n ⭐ If BookLib is useful, a star helps: https://github.com/booklib-ai/booklib\n');
2381
+ }
2382
+ } catch {
2383
+ // never block the CLI for a nudge
2384
+ }
2385
+ }
2386
+
2387
+ main()
2388
+ .then(() => maybeAskFeedback())
2389
+ .then(() => maybeNudgeStar())
2390
+ .catch(err => {
2391
+ console.error(err.message);
2392
+ console.error('\n If this looks like a bug, please report it: https://github.com/booklib-ai/booklib/issues\n');
2393
+ process.exit(1);
2394
+ });