@booklib/core 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/booklib-standards.mdc +40 -0
- package/.gemini/context.md +372 -0
- package/AGENTS.md +166 -0
- package/CHANGELOG.md +226 -0
- package/CLAUDE.md +81 -0
- package/CODE_OF_CONDUCT.md +31 -0
- package/CONTRIBUTING.md +304 -0
- package/LICENSE +21 -0
- package/PLAN.md +28 -0
- package/README.ja.md +198 -0
- package/README.ko.md +198 -0
- package/README.md +503 -0
- package/README.pt-BR.md +198 -0
- package/README.uk.md +241 -0
- package/README.zh-CN.md +198 -0
- package/SECURITY.md +9 -0
- package/agents/architecture-reviewer.md +136 -0
- package/agents/booklib-reviewer.md +90 -0
- package/agents/data-reviewer.md +107 -0
- package/agents/jvm-reviewer.md +146 -0
- package/agents/python-reviewer.md +128 -0
- package/agents/rust-reviewer.md +115 -0
- package/agents/ts-reviewer.md +110 -0
- package/agents/ui-reviewer.md +117 -0
- package/assets/logo.svg +36 -0
- package/bin/booklib-mcp.js +304 -0
- package/bin/booklib.js +1705 -0
- package/bin/skills.cjs +1292 -0
- package/booklib-router.mdc +36 -0
- package/booklib.config.json +19 -0
- package/commands/animation-at-work.md +10 -0
- package/commands/clean-code-reviewer.md +10 -0
- package/commands/data-intensive-patterns.md +10 -0
- package/commands/data-pipelines.md +10 -0
- package/commands/design-patterns.md +10 -0
- package/commands/domain-driven-design.md +10 -0
- package/commands/effective-java.md +10 -0
- package/commands/effective-kotlin.md +10 -0
- package/commands/effective-python.md +10 -0
- package/commands/effective-typescript.md +10 -0
- package/commands/kotlin-in-action.md +10 -0
- package/commands/lean-startup.md +10 -0
- package/commands/microservices-patterns.md +10 -0
- package/commands/programming-with-rust.md +10 -0
- package/commands/refactoring-ui.md +10 -0
- package/commands/rust-in-action.md +10 -0
- package/commands/skill-router.md +10 -0
- package/commands/spring-boot-in-action.md +10 -0
- package/commands/storytelling-with-data.md +10 -0
- package/commands/system-design-interview.md +10 -0
- package/commands/using-asyncio-python.md +10 -0
- package/commands/web-scraping-python.md +10 -0
- package/community/registry.json +1616 -0
- package/hooks/hooks.json +23 -0
- package/hooks/posttooluse-capture.mjs +67 -0
- package/hooks/suggest.js +153 -0
- package/lib/agent-behaviors.js +40 -0
- package/lib/agent-detector.js +96 -0
- package/lib/config-loader.js +39 -0
- package/lib/conflict-resolver.js +148 -0
- package/lib/context-builder.js +574 -0
- package/lib/discovery-engine.js +298 -0
- package/lib/doctor/hook-installer.js +83 -0
- package/lib/doctor/usage-tracker.js +87 -0
- package/lib/engine/ai-features.js +253 -0
- package/lib/engine/auditor.js +103 -0
- package/lib/engine/bm25-index.js +178 -0
- package/lib/engine/capture.js +120 -0
- package/lib/engine/corrections.js +198 -0
- package/lib/engine/doctor.js +195 -0
- package/lib/engine/graph-injector.js +137 -0
- package/lib/engine/graph.js +161 -0
- package/lib/engine/handoff.js +405 -0
- package/lib/engine/indexer.js +242 -0
- package/lib/engine/parser.js +53 -0
- package/lib/engine/query-expander.js +42 -0
- package/lib/engine/reranker.js +40 -0
- package/lib/engine/rrf.js +59 -0
- package/lib/engine/scanner.js +151 -0
- package/lib/engine/searcher.js +139 -0
- package/lib/engine/session-coordinator.js +306 -0
- package/lib/engine/session-manager.js +429 -0
- package/lib/engine/synthesizer.js +70 -0
- package/lib/installer.js +70 -0
- package/lib/instinct-block.js +33 -0
- package/lib/mcp-config-writer.js +88 -0
- package/lib/paths.js +57 -0
- package/lib/profiles/design.md +19 -0
- package/lib/profiles/general.md +16 -0
- package/lib/profiles/research-analysis.md +22 -0
- package/lib/profiles/software-development.md +23 -0
- package/lib/profiles/writing-content.md +19 -0
- package/lib/project-initializer.js +916 -0
- package/lib/registry/skills.js +102 -0
- package/lib/registry-searcher.js +99 -0
- package/lib/rules/rules-manager.js +169 -0
- package/lib/skill-fetcher.js +333 -0
- package/lib/well-known-builder.js +70 -0
- package/lib/wizard/index.js +404 -0
- package/lib/wizard/integration-detector.js +41 -0
- package/lib/wizard/project-detector.js +100 -0
- package/lib/wizard/prompt.js +156 -0
- package/lib/wizard/registry-embeddings.js +107 -0
- package/lib/wizard/skill-recommender.js +69 -0
- package/llms-full.txt +254 -0
- package/llms.txt +70 -0
- package/package.json +45 -0
- package/research-reports/2026-04-01-current-architecture.md +160 -0
- package/research-reports/IDEAS.md +93 -0
- package/rules/common/clean-code.md +42 -0
- package/rules/java/effective-java.md +42 -0
- package/rules/kotlin/effective-kotlin.md +37 -0
- package/rules/python/effective-python.md +38 -0
- package/rules/rust/rust.md +37 -0
- package/rules/typescript/effective-typescript.md +42 -0
- package/scripts/gen-llms-full.mjs +36 -0
- package/scripts/gen-og.mjs +142 -0
- package/scripts/validate-frontmatter.js +25 -0
- package/skills/animation-at-work/SKILL.md +270 -0
- package/skills/animation-at-work/assets/example_asset.txt +1 -0
- package/skills/animation-at-work/evals/evals.json +44 -0
- package/skills/animation-at-work/evals/results.json +13 -0
- package/skills/animation-at-work/examples/after.md +64 -0
- package/skills/animation-at-work/examples/before.md +35 -0
- package/skills/animation-at-work/references/api_reference.md +369 -0
- package/skills/animation-at-work/references/review-checklist.md +79 -0
- package/skills/animation-at-work/scripts/audit_animations.py +295 -0
- package/skills/animation-at-work/scripts/example.py +1 -0
- package/skills/clean-code-reviewer/SKILL.md +444 -0
- package/skills/clean-code-reviewer/audit.json +35 -0
- package/skills/clean-code-reviewer/evals/evals.json +185 -0
- package/skills/clean-code-reviewer/evals/results.json +13 -0
- package/skills/clean-code-reviewer/examples/after.md +48 -0
- package/skills/clean-code-reviewer/examples/before.md +33 -0
- package/skills/clean-code-reviewer/references/api_reference.md +158 -0
- package/skills/clean-code-reviewer/references/practices-catalog.md +282 -0
- package/skills/clean-code-reviewer/references/review-checklist.md +254 -0
- package/skills/clean-code-reviewer/scripts/pre-review.py +206 -0
- package/skills/data-intensive-patterns/SKILL.md +267 -0
- package/skills/data-intensive-patterns/assets/example_asset.txt +1 -0
- package/skills/data-intensive-patterns/evals/evals.json +54 -0
- package/skills/data-intensive-patterns/evals/results.json +13 -0
- package/skills/data-intensive-patterns/examples/after.md +61 -0
- package/skills/data-intensive-patterns/examples/before.md +38 -0
- package/skills/data-intensive-patterns/references/api_reference.md +34 -0
- package/skills/data-intensive-patterns/references/patterns-catalog.md +551 -0
- package/skills/data-intensive-patterns/references/review-checklist.md +193 -0
- package/skills/data-intensive-patterns/scripts/adr.py +213 -0
- package/skills/data-intensive-patterns/scripts/example.py +1 -0
- package/skills/data-pipelines/SKILL.md +259 -0
- package/skills/data-pipelines/assets/example_asset.txt +1 -0
- package/skills/data-pipelines/evals/evals.json +45 -0
- package/skills/data-pipelines/evals/results.json +13 -0
- package/skills/data-pipelines/examples/after.md +97 -0
- package/skills/data-pipelines/examples/before.md +37 -0
- package/skills/data-pipelines/references/api_reference.md +301 -0
- package/skills/data-pipelines/references/review-checklist.md +181 -0
- package/skills/data-pipelines/scripts/example.py +1 -0
- package/skills/data-pipelines/scripts/new_pipeline.py +444 -0
- package/skills/design-patterns/SKILL.md +271 -0
- package/skills/design-patterns/assets/example_asset.txt +1 -0
- package/skills/design-patterns/evals/evals.json +46 -0
- package/skills/design-patterns/evals/results.json +13 -0
- package/skills/design-patterns/examples/after.md +52 -0
- package/skills/design-patterns/examples/before.md +29 -0
- package/skills/design-patterns/references/api_reference.md +1 -0
- package/skills/design-patterns/references/patterns-catalog.md +726 -0
- package/skills/design-patterns/references/review-checklist.md +173 -0
- package/skills/design-patterns/scripts/example.py +1 -0
- package/skills/design-patterns/scripts/scaffold.py +807 -0
- package/skills/domain-driven-design/SKILL.md +142 -0
- package/skills/domain-driven-design/assets/example_asset.txt +1 -0
- package/skills/domain-driven-design/evals/evals.json +48 -0
- package/skills/domain-driven-design/evals/results.json +13 -0
- package/skills/domain-driven-design/examples/after.md +80 -0
- package/skills/domain-driven-design/examples/before.md +43 -0
- package/skills/domain-driven-design/references/api_reference.md +1 -0
- package/skills/domain-driven-design/references/patterns-catalog.md +545 -0
- package/skills/domain-driven-design/references/review-checklist.md +158 -0
- package/skills/domain-driven-design/scripts/example.py +1 -0
- package/skills/domain-driven-design/scripts/scaffold.py +421 -0
- package/skills/effective-java/SKILL.md +227 -0
- package/skills/effective-java/assets/example_asset.txt +1 -0
- package/skills/effective-java/evals/evals.json +46 -0
- package/skills/effective-java/evals/results.json +13 -0
- package/skills/effective-java/examples/after.md +83 -0
- package/skills/effective-java/examples/before.md +37 -0
- package/skills/effective-java/references/api_reference.md +1 -0
- package/skills/effective-java/references/items-catalog.md +955 -0
- package/skills/effective-java/references/review-checklist.md +216 -0
- package/skills/effective-java/scripts/checkstyle_setup.py +211 -0
- package/skills/effective-java/scripts/example.py +1 -0
- package/skills/effective-kotlin/SKILL.md +271 -0
- package/skills/effective-kotlin/assets/example_asset.txt +1 -0
- package/skills/effective-kotlin/audit.json +29 -0
- package/skills/effective-kotlin/evals/evals.json +45 -0
- package/skills/effective-kotlin/evals/results.json +13 -0
- package/skills/effective-kotlin/examples/after.md +36 -0
- package/skills/effective-kotlin/examples/before.md +38 -0
- package/skills/effective-kotlin/references/api_reference.md +1 -0
- package/skills/effective-kotlin/references/practices-catalog.md +1228 -0
- package/skills/effective-kotlin/references/review-checklist.md +126 -0
- package/skills/effective-kotlin/scripts/example.py +1 -0
- package/skills/effective-python/SKILL.md +441 -0
- package/skills/effective-python/evals/evals.json +44 -0
- package/skills/effective-python/evals/results.json +13 -0
- package/skills/effective-python/examples/after.md +56 -0
- package/skills/effective-python/examples/before.md +40 -0
- package/skills/effective-python/ref-01-pythonic-thinking.md +202 -0
- package/skills/effective-python/ref-02-lists-and-dicts.md +146 -0
- package/skills/effective-python/ref-03-functions.md +186 -0
- package/skills/effective-python/ref-04-comprehensions-generators.md +211 -0
- package/skills/effective-python/ref-05-classes-interfaces.md +188 -0
- package/skills/effective-python/ref-06-metaclasses-attributes.md +209 -0
- package/skills/effective-python/ref-07-concurrency.md +213 -0
- package/skills/effective-python/ref-08-robustness-performance.md +248 -0
- package/skills/effective-python/ref-09-testing-debugging.md +253 -0
- package/skills/effective-python/ref-10-collaboration.md +175 -0
- package/skills/effective-python/references/api_reference.md +218 -0
- package/skills/effective-python/references/practices-catalog.md +483 -0
- package/skills/effective-python/references/review-checklist.md +190 -0
- package/skills/effective-python/scripts/lint.py +173 -0
- package/skills/effective-typescript/SKILL.md +262 -0
- package/skills/effective-typescript/audit.json +29 -0
- package/skills/effective-typescript/evals/evals.json +37 -0
- package/skills/effective-typescript/evals/results.json +13 -0
- package/skills/effective-typescript/examples/after.md +70 -0
- package/skills/effective-typescript/examples/before.md +47 -0
- package/skills/effective-typescript/references/api_reference.md +118 -0
- package/skills/effective-typescript/references/practices-catalog.md +371 -0
- package/skills/effective-typescript/scripts/review.py +169 -0
- package/skills/kotlin-in-action/SKILL.md +261 -0
- package/skills/kotlin-in-action/assets/example_asset.txt +1 -0
- package/skills/kotlin-in-action/evals/evals.json +43 -0
- package/skills/kotlin-in-action/evals/results.json +13 -0
- package/skills/kotlin-in-action/examples/after.md +53 -0
- package/skills/kotlin-in-action/examples/before.md +39 -0
- package/skills/kotlin-in-action/references/api_reference.md +1 -0
- package/skills/kotlin-in-action/references/practices-catalog.md +436 -0
- package/skills/kotlin-in-action/references/review-checklist.md +204 -0
- package/skills/kotlin-in-action/scripts/example.py +1 -0
- package/skills/kotlin-in-action/scripts/setup_detekt.py +224 -0
- package/skills/lean-startup/SKILL.md +160 -0
- package/skills/lean-startup/assets/example_asset.txt +1 -0
- package/skills/lean-startup/evals/evals.json +43 -0
- package/skills/lean-startup/evals/results.json +13 -0
- package/skills/lean-startup/examples/after.md +80 -0
- package/skills/lean-startup/examples/before.md +34 -0
- package/skills/lean-startup/references/api_reference.md +319 -0
- package/skills/lean-startup/references/review-checklist.md +137 -0
- package/skills/lean-startup/scripts/example.py +1 -0
- package/skills/lean-startup/scripts/new_experiment.py +286 -0
- package/skills/microservices-patterns/SKILL.md +384 -0
- package/skills/microservices-patterns/evals/evals.json +45 -0
- package/skills/microservices-patterns/evals/results.json +13 -0
- package/skills/microservices-patterns/examples/after.md +69 -0
- package/skills/microservices-patterns/examples/before.md +40 -0
- package/skills/microservices-patterns/references/patterns-catalog.md +391 -0
- package/skills/microservices-patterns/references/review-checklist.md +169 -0
- package/skills/microservices-patterns/scripts/new_service.py +583 -0
- package/skills/programming-with-rust/SKILL.md +209 -0
- package/skills/programming-with-rust/evals/evals.json +37 -0
- package/skills/programming-with-rust/evals/results.json +13 -0
- package/skills/programming-with-rust/examples/after.md +107 -0
- package/skills/programming-with-rust/examples/before.md +59 -0
- package/skills/programming-with-rust/references/api_reference.md +152 -0
- package/skills/programming-with-rust/references/practices-catalog.md +335 -0
- package/skills/programming-with-rust/scripts/review.py +142 -0
- package/skills/refactoring-ui/SKILL.md +362 -0
- package/skills/refactoring-ui/assets/example_asset.txt +1 -0
- package/skills/refactoring-ui/evals/evals.json +45 -0
- package/skills/refactoring-ui/evals/results.json +13 -0
- package/skills/refactoring-ui/examples/after.md +85 -0
- package/skills/refactoring-ui/examples/before.md +58 -0
- package/skills/refactoring-ui/references/api_reference.md +355 -0
- package/skills/refactoring-ui/references/review-checklist.md +114 -0
- package/skills/refactoring-ui/scripts/audit_css.py +250 -0
- package/skills/refactoring-ui/scripts/example.py +1 -0
- package/skills/rust-in-action/SKILL.md +350 -0
- package/skills/rust-in-action/evals/evals.json +38 -0
- package/skills/rust-in-action/evals/results.json +13 -0
- package/skills/rust-in-action/examples/after.md +156 -0
- package/skills/rust-in-action/examples/before.md +56 -0
- package/skills/rust-in-action/references/practices-catalog.md +346 -0
- package/skills/rust-in-action/scripts/review.py +147 -0
- package/skills/skill-router/SKILL.md +186 -0
- package/skills/skill-router/evals/evals.json +38 -0
- package/skills/skill-router/evals/results.json +13 -0
- package/skills/skill-router/examples/after.md +63 -0
- package/skills/skill-router/examples/before.md +39 -0
- package/skills/skill-router/references/api_reference.md +24 -0
- package/skills/skill-router/references/routing-heuristics.md +89 -0
- package/skills/skill-router/references/skill-catalog.md +174 -0
- package/skills/skill-router/scripts/route.py +266 -0
- package/skills/spring-boot-in-action/SKILL.md +340 -0
- package/skills/spring-boot-in-action/evals/evals.json +39 -0
- package/skills/spring-boot-in-action/evals/results.json +13 -0
- package/skills/spring-boot-in-action/examples/after.md +185 -0
- package/skills/spring-boot-in-action/examples/before.md +84 -0
- package/skills/spring-boot-in-action/references/practices-catalog.md +403 -0
- package/skills/spring-boot-in-action/scripts/review.py +184 -0
- package/skills/storytelling-with-data/SKILL.md +241 -0
- package/skills/storytelling-with-data/assets/example_asset.txt +1 -0
- package/skills/storytelling-with-data/evals/evals.json +47 -0
- package/skills/storytelling-with-data/evals/results.json +13 -0
- package/skills/storytelling-with-data/examples/after.md +50 -0
- package/skills/storytelling-with-data/examples/before.md +33 -0
- package/skills/storytelling-with-data/references/api_reference.md +379 -0
- package/skills/storytelling-with-data/references/review-checklist.md +111 -0
- package/skills/storytelling-with-data/scripts/chart_review.py +301 -0
- package/skills/storytelling-with-data/scripts/example.py +1 -0
- package/skills/system-design-interview/SKILL.md +233 -0
- package/skills/system-design-interview/assets/example_asset.txt +1 -0
- package/skills/system-design-interview/evals/evals.json +46 -0
- package/skills/system-design-interview/evals/results.json +13 -0
- package/skills/system-design-interview/examples/after.md +94 -0
- package/skills/system-design-interview/examples/before.md +27 -0
- package/skills/system-design-interview/references/api_reference.md +582 -0
- package/skills/system-design-interview/references/review-checklist.md +201 -0
- package/skills/system-design-interview/scripts/example.py +1 -0
- package/skills/system-design-interview/scripts/new_design.py +421 -0
- package/skills/using-asyncio-python/SKILL.md +290 -0
- package/skills/using-asyncio-python/assets/example_asset.txt +1 -0
- package/skills/using-asyncio-python/evals/evals.json +43 -0
- package/skills/using-asyncio-python/evals/results.json +13 -0
- package/skills/using-asyncio-python/examples/after.md +68 -0
- package/skills/using-asyncio-python/examples/before.md +39 -0
- package/skills/using-asyncio-python/references/api_reference.md +267 -0
- package/skills/using-asyncio-python/references/review-checklist.md +149 -0
- package/skills/using-asyncio-python/scripts/check_blocking.py +270 -0
- package/skills/using-asyncio-python/scripts/example.py +1 -0
- package/skills/web-scraping-python/SKILL.md +280 -0
- package/skills/web-scraping-python/assets/example_asset.txt +1 -0
- package/skills/web-scraping-python/evals/evals.json +46 -0
- package/skills/web-scraping-python/evals/results.json +13 -0
- package/skills/web-scraping-python/examples/after.md +109 -0
- package/skills/web-scraping-python/examples/before.md +40 -0
- package/skills/web-scraping-python/references/api_reference.md +393 -0
- package/skills/web-scraping-python/references/review-checklist.md +163 -0
- package/skills/web-scraping-python/scripts/example.py +1 -0
- package/skills/web-scraping-python/scripts/new_scraper.py +231 -0
- package/skills/writing-plans/audit.json +34 -0
- package/tests/agent-detector.test.js +83 -0
- package/tests/corrections.test.js +245 -0
- package/tests/doctor/hook-installer.test.js +72 -0
- package/tests/doctor/usage-tracker.test.js +140 -0
- package/tests/engine/benchmark-eval.test.js +31 -0
- package/tests/engine/bm25-index.test.js +85 -0
- package/tests/engine/capture-command.test.js +35 -0
- package/tests/engine/capture.test.js +17 -0
- package/tests/engine/graph-augmented-search.test.js +107 -0
- package/tests/engine/graph-injector.test.js +44 -0
- package/tests/engine/graph.test.js +216 -0
- package/tests/engine/hybrid-searcher.test.js +74 -0
- package/tests/engine/indexer-bm25.test.js +37 -0
- package/tests/engine/mcp-tools.test.js +73 -0
- package/tests/engine/project-initializer-mcp.test.js +99 -0
- package/tests/engine/query-expander.test.js +36 -0
- package/tests/engine/reranker.test.js +51 -0
- package/tests/engine/rrf.test.js +49 -0
- package/tests/engine/srag-prefix.test.js +47 -0
- package/tests/instinct-block.test.js +23 -0
- package/tests/mcp-config-writer.test.js +60 -0
- package/tests/project-initializer-new-agents.test.js +48 -0
- package/tests/rules/rules-manager.test.js +230 -0
- package/tests/well-known-builder.test.js +40 -0
- package/tests/wizard/integration-detector.test.js +31 -0
- package/tests/wizard/project-detector.test.js +51 -0
- package/tests/wizard/prompt-session.test.js +61 -0
- package/tests/wizard/prompt.test.js +16 -0
- package/tests/wizard/registry-embeddings.test.js +35 -0
- package/tests/wizard/skill-recommender.test.js +34 -0
- package/tests/wizard/slot-count.test.js +25 -0
- package/vercel.json +21 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import https from 'https';
|
|
4
|
+
import { resolveBookLibPaths } from './paths.js';
|
|
5
|
+
import { loadConfig } from './config-loader.js';
|
|
6
|
+
|
|
7
|
+
const DISCOVERED_FILENAME = 'discovered.json';
|
|
8
|
+
const GITHUB_AUTH_HEADER = process.env.GITHUB_TOKEN
|
|
9
|
+
? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }
|
|
10
|
+
: {};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Scans approved sources (GitHub orgs, npm scopes) for available skills.
|
|
14
|
+
* Results are cached with a TTL to avoid hammering external APIs.
|
|
15
|
+
*/
|
|
16
|
+
export class DiscoveryEngine {
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.paths = resolveBookLibPaths(options.projectCwd);
|
|
19
|
+
this.config = loadConfig(options.projectCwd);
|
|
20
|
+
this.cacheFile = path.join(this.paths.cachePath, DISCOVERED_FILENAME);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns all discoverable skills from approved sources.
|
|
25
|
+
* Uses cache if fresh; re-scans if stale or missing.
|
|
26
|
+
*
|
|
27
|
+
* @returns {Array<object>} List of discovered skill descriptors
|
|
28
|
+
*/
|
|
29
|
+
async discover() {
|
|
30
|
+
const cached = this._loadCache();
|
|
31
|
+
if (cached) return cached;
|
|
32
|
+
|
|
33
|
+
const results = [];
|
|
34
|
+
|
|
35
|
+
for (const source of this.config.sources) {
|
|
36
|
+
if (source.type === 'registry') continue; // bundled registry handled separately
|
|
37
|
+
try {
|
|
38
|
+
const found = await this._scanSource(source);
|
|
39
|
+
results.push(...found);
|
|
40
|
+
} catch {
|
|
41
|
+
// Non-fatal: skip unreachable sources
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this._saveCache(results);
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Forces a re-scan, ignoring any cached results.
|
|
51
|
+
*/
|
|
52
|
+
async refresh() {
|
|
53
|
+
this._clearCache();
|
|
54
|
+
return this.discover();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
async _scanSource(source) {
|
|
60
|
+
if (source.type === 'github-org') {
|
|
61
|
+
return this._scanGitHubOrg(source);
|
|
62
|
+
}
|
|
63
|
+
if (source.type === 'npm-scope') {
|
|
64
|
+
return this._scanNpmScope(source);
|
|
65
|
+
}
|
|
66
|
+
if (source.type === 'manifest') {
|
|
67
|
+
return this._scanManifest(source);
|
|
68
|
+
}
|
|
69
|
+
if (source.type === 'github-skills-dir') {
|
|
70
|
+
return this._scanGitHubSkillsDir(source);
|
|
71
|
+
}
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Scans a specific directory in a GitHub repo for subdirectories containing SKILL.md.
|
|
77
|
+
* Fetches the frontmatter from each SKILL.md to populate name/description.
|
|
78
|
+
*
|
|
79
|
+
* Source format:
|
|
80
|
+
* { type: 'github-skills-dir', repo: 'owner/repo', dir: 'skills', branch: 'main', trusted: false }
|
|
81
|
+
*/
|
|
82
|
+
async _scanGitHubSkillsDir(source) {
|
|
83
|
+
const { repo, dir = 'skills', branch = 'main', trusted = false } = source;
|
|
84
|
+
const apiUrl = `https://api.github.com/repos/${repo}/contents/${dir}`;
|
|
85
|
+
|
|
86
|
+
let entries;
|
|
87
|
+
try {
|
|
88
|
+
entries = await this._fetchJson(apiUrl);
|
|
89
|
+
} catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!Array.isArray(entries)) return [];
|
|
94
|
+
|
|
95
|
+
const skills = [];
|
|
96
|
+
const subdirs = entries.filter(e => e.type === 'dir');
|
|
97
|
+
|
|
98
|
+
for (const subdir of subdirs) {
|
|
99
|
+
const skillUrl = `https://raw.githubusercontent.com/${repo}/${branch}/${dir}/${subdir.name}/SKILL.md`;
|
|
100
|
+
const exists = await this._urlExists(skillUrl);
|
|
101
|
+
if (!exists) continue;
|
|
102
|
+
|
|
103
|
+
// Fetch first 25 lines to extract frontmatter name/description without loading full file
|
|
104
|
+
let name = subdir.name;
|
|
105
|
+
let description = '';
|
|
106
|
+
try {
|
|
107
|
+
const head = await this._fetchHead(skillUrl, 25);
|
|
108
|
+
const nameMatch = head.match(/^name:\s*(.+)$/m);
|
|
109
|
+
if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
110
|
+
description = this._parseFrontmatterDescription(head);
|
|
111
|
+
} catch { /* use defaults */ }
|
|
112
|
+
|
|
113
|
+
skills.push({
|
|
114
|
+
name,
|
|
115
|
+
description,
|
|
116
|
+
source: { type: 'github', url: skillUrl, repo, dir: subdir.name },
|
|
117
|
+
triggers: { extensions: [], keywords: name.split('-') },
|
|
118
|
+
trusted,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return skills;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Fetches just the first N lines of a text file (avoids downloading large files). */
|
|
126
|
+
_fetchHead(url, lines = 20) {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const options = { headers: { 'User-Agent': 'booklib-discovery/1.0', 'Range': 'bytes=0-2000', ...GITHUB_AUTH_HEADER } };
|
|
129
|
+
https.get(url, options, res => {
|
|
130
|
+
let data = '';
|
|
131
|
+
res.on('data', chunk => (data += chunk));
|
|
132
|
+
res.on('end', () => resolve(data.split('\n').slice(0, lines).join('\n')));
|
|
133
|
+
}).on('error', reject);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fetches a JSON manifest file (local path or HTTPS URL) and returns its skill list.
|
|
139
|
+
* Manifest format: { version: 1, skills: [{ name, description, stars, source, trusted, ... }] }
|
|
140
|
+
*/
|
|
141
|
+
async _scanManifest(source) {
|
|
142
|
+
let data;
|
|
143
|
+
if (source.url.startsWith('http')) {
|
|
144
|
+
data = await this._fetchJson(source.url);
|
|
145
|
+
} else {
|
|
146
|
+
// Local file path (absolute or relative to cwd)
|
|
147
|
+
const filePath = path.resolve(source.url);
|
|
148
|
+
if (!fs.existsSync(filePath)) return [];
|
|
149
|
+
try { data = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return []; }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const skills = data?.skills ?? [];
|
|
153
|
+
// Allow the source entry to override trusted for the whole manifest
|
|
154
|
+
return skills.map(skill => ({
|
|
155
|
+
...skill,
|
|
156
|
+
trusted: source.trusted ?? skill.trusted ?? false,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Lists repositories in a GitHub org and checks each for a SKILL.md at the root.
|
|
162
|
+
*/
|
|
163
|
+
async _scanGitHubOrg(source) {
|
|
164
|
+
const { org, trusted = false } = source;
|
|
165
|
+
const reposUrl = `https://api.github.com/orgs/${org}/repos?per_page=100&type=public`;
|
|
166
|
+
|
|
167
|
+
let repos;
|
|
168
|
+
try {
|
|
169
|
+
const body = await this._fetchJson(reposUrl);
|
|
170
|
+
repos = Array.isArray(body) ? body : [];
|
|
171
|
+
} catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const skills = [];
|
|
176
|
+
for (const repo of repos) {
|
|
177
|
+
const skillUrl = `https://raw.githubusercontent.com/${org}/${repo.name}/main/SKILL.md`;
|
|
178
|
+
const exists = await this._urlExists(skillUrl);
|
|
179
|
+
if (exists) {
|
|
180
|
+
skills.push({
|
|
181
|
+
name: repo.name,
|
|
182
|
+
description: repo.description ?? '',
|
|
183
|
+
source: { type: 'github', org, repo: repo.name, url: skillUrl },
|
|
184
|
+
trusted,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return skills;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Searches npm for packages in a given scope and checks each for a SKILL.md in their dist.
|
|
193
|
+
*/
|
|
194
|
+
async _scanNpmScope(source) {
|
|
195
|
+
const { scope, trusted = false } = source;
|
|
196
|
+
const searchUrl = `https://registry.npmjs.org/-/v1/search?text=scope:${scope.replace('@', '')}&size=50`;
|
|
197
|
+
|
|
198
|
+
let body;
|
|
199
|
+
try {
|
|
200
|
+
body = await this._fetchJson(searchUrl);
|
|
201
|
+
} catch {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const packages = body?.objects ?? [];
|
|
206
|
+
return packages.map(pkg => ({
|
|
207
|
+
name: pkg.package.name,
|
|
208
|
+
description: pkg.package.description ?? '',
|
|
209
|
+
source: { type: 'npm', package: pkg.package.name, version: pkg.package.version },
|
|
210
|
+
trusted,
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Cache helpers ──────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
_loadCache() {
|
|
217
|
+
if (!fs.existsSync(this.cacheFile)) return null;
|
|
218
|
+
try {
|
|
219
|
+
const data = JSON.parse(fs.readFileSync(this.cacheFile, 'utf8'));
|
|
220
|
+
const ttlMs = (this.config.discovery.ttlHours ?? 24) * 60 * 60 * 1000;
|
|
221
|
+
if (Date.now() - data.timestamp < ttlMs) {
|
|
222
|
+
return data.skills;
|
|
223
|
+
}
|
|
224
|
+
} catch { /* corrupt cache */ }
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_saveCache(skills) {
|
|
229
|
+
fs.mkdirSync(path.dirname(this.cacheFile), { recursive: true });
|
|
230
|
+
fs.writeFileSync(this.cacheFile, JSON.stringify({ timestamp: Date.now(), skills }, null, 2));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_clearCache() {
|
|
234
|
+
if (fs.existsSync(this.cacheFile)) fs.unlinkSync(this.cacheFile);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Frontmatter helpers ────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Extracts description from YAML frontmatter, handling all common formats:
|
|
241
|
+
* description: single line value
|
|
242
|
+
* description: "quoted value"
|
|
243
|
+
* description: |
|
|
244
|
+
* block scalar (literal, preserves newlines)
|
|
245
|
+
* description: >
|
|
246
|
+
* folded scalar (folds newlines to spaces)
|
|
247
|
+
*/
|
|
248
|
+
_parseFrontmatterDescription(text) {
|
|
249
|
+
const lines = text.split('\n');
|
|
250
|
+
for (let i = 0; i < lines.length; i++) {
|
|
251
|
+
const m = lines[i].match(/^description:\s*(.*)/);
|
|
252
|
+
if (!m) continue;
|
|
253
|
+
const inline = m[1].trim();
|
|
254
|
+
// Block/folded scalar — collect indented continuation lines
|
|
255
|
+
if (inline === '|' || inline === '>') {
|
|
256
|
+
const parts = [];
|
|
257
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
258
|
+
if (lines[j].match(/^\s+/) || lines[j].trim() === '') {
|
|
259
|
+
const part = lines[j].trim();
|
|
260
|
+
if (part) parts.push(part);
|
|
261
|
+
else break;
|
|
262
|
+
} else break;
|
|
263
|
+
}
|
|
264
|
+
return parts.join(inline === '>' ? ' ' : ' ').trim();
|
|
265
|
+
}
|
|
266
|
+
// Strip surrounding quotes
|
|
267
|
+
return inline.replace(/^["']|["']$/g, '');
|
|
268
|
+
}
|
|
269
|
+
return '';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── HTTP helpers ───────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
_fetchJson(url) {
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
const options = { headers: { 'User-Agent': 'booklib-discovery/1.0', ...GITHUB_AUTH_HEADER } };
|
|
277
|
+
https.get(url, options, res => {
|
|
278
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
279
|
+
return resolve(this._fetchJson(res.headers.location));
|
|
280
|
+
}
|
|
281
|
+
let data = '';
|
|
282
|
+
res.on('data', chunk => (data += chunk));
|
|
283
|
+
res.on('end', () => {
|
|
284
|
+
try { resolve(JSON.parse(data)); } catch { reject(new Error('Invalid JSON')); }
|
|
285
|
+
});
|
|
286
|
+
}).on('error', reject);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_urlExists(url) {
|
|
291
|
+
return new Promise(resolve => {
|
|
292
|
+
const options = { method: 'HEAD', headers: { 'User-Agent': 'booklib-discovery/1.0', ...GITHUB_AUTH_HEADER } };
|
|
293
|
+
https.request(url, options, res => resolve(res.statusCode === 200))
|
|
294
|
+
.on('error', () => resolve(false))
|
|
295
|
+
.end();
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// lib/doctor/hook-installer.js
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const TRACK_USAGE_CONTENT = `#!/usr/bin/env node
|
|
7
|
+
// track-usage.mjs — installed by: booklib doctor --install-hook
|
|
8
|
+
// Appends { skill, timestamp } to ~/.booklib/usage.json on every Skill tool invocation.
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
const raw = process.argv[2] ?? '{}';
|
|
14
|
+
let input;
|
|
15
|
+
try { input = JSON.parse(raw); } catch { process.exit(0); }
|
|
16
|
+
|
|
17
|
+
const skill = input?.skill ?? input?.name ?? input?.args?.skill;
|
|
18
|
+
if (!skill || typeof skill !== 'string') process.exit(0);
|
|
19
|
+
|
|
20
|
+
const dir = path.join(os.homedir(), '.booklib');
|
|
21
|
+
const file = path.join(dir, 'usage.json');
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
+
let entries = [];
|
|
26
|
+
try {
|
|
27
|
+
entries = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err.code !== 'ENOENT') process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
entries.push({ skill: skill.trim(), timestamp: new Date().toISOString() });
|
|
32
|
+
fs.writeFileSync(file, JSON.stringify(entries, null, 2));
|
|
33
|
+
} catch {
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const HOOK_COMMAND = `node ~/.booklib/track-usage.mjs "$TOOL_INPUT"`;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Installs the usage-tracking hook into Claude Code settings.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} [home]
|
|
44
|
+
* @returns {{ scriptPath: string, settingsPath: string, alreadyInstalled: boolean }}
|
|
45
|
+
*/
|
|
46
|
+
export function installTrackingHook(home = os.homedir()) {
|
|
47
|
+
const booklibDir = path.join(home, '.booklib');
|
|
48
|
+
const claudeDir = path.join(home, '.claude');
|
|
49
|
+
const scriptPath = path.join(booklibDir, 'track-usage.mjs');
|
|
50
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
51
|
+
|
|
52
|
+
fs.mkdirSync(booklibDir, { recursive: true });
|
|
53
|
+
fs.writeFileSync(scriptPath, TRACK_USAGE_CONTENT);
|
|
54
|
+
|
|
55
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
56
|
+
let settings = {};
|
|
57
|
+
try {
|
|
58
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err.code !== 'ENOENT') throw err;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!settings.hooks) settings.hooks = {};
|
|
64
|
+
if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
|
|
65
|
+
|
|
66
|
+
const newEntry = {
|
|
67
|
+
matcher: 'Skill',
|
|
68
|
+
hooks: [{ type: 'command', command: HOOK_COMMAND }],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const existingIdx = settings.hooks.PreToolUse.findIndex(e => e.matcher === 'Skill');
|
|
72
|
+
const alreadyInstalled = existingIdx !== -1 &&
|
|
73
|
+
settings.hooks.PreToolUse[existingIdx]?.hooks?.[0]?.command === HOOK_COMMAND;
|
|
74
|
+
|
|
75
|
+
if (existingIdx !== -1) {
|
|
76
|
+
settings.hooks.PreToolUse[existingIdx] = newEntry;
|
|
77
|
+
} else {
|
|
78
|
+
settings.hooks.PreToolUse.push(newEntry);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
82
|
+
return { scriptPath, settingsPath, alreadyInstalled };
|
|
83
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// lib/doctor/usage-tracker.js
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_USAGE_PATH = path.join(os.homedir(), '.booklib', 'usage.json');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Appends a single usage entry to the usage log.
|
|
10
|
+
* Creates the file (and parent dirs) if absent.
|
|
11
|
+
*/
|
|
12
|
+
export function appendUsage(skillName, usagePath = DEFAULT_USAGE_PATH) {
|
|
13
|
+
fs.mkdirSync(path.dirname(usagePath), { recursive: true });
|
|
14
|
+
let entries = [];
|
|
15
|
+
try {
|
|
16
|
+
entries = JSON.parse(fs.readFileSync(usagePath, 'utf8'));
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (err.code !== 'ENOENT') throw err;
|
|
19
|
+
}
|
|
20
|
+
entries.push({ skill: skillName, timestamp: new Date().toISOString() });
|
|
21
|
+
fs.writeFileSync(usagePath, JSON.stringify(entries, null, 2));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reads and parses the usage log. Returns [] if file does not exist.
|
|
26
|
+
*/
|
|
27
|
+
export function readUsage(usagePath = DEFAULT_USAGE_PATH) {
|
|
28
|
+
if (!fs.existsSync(usagePath)) return [];
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(usagePath, 'utf8'));
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Derives per-skill health summaries from raw usage data.
|
|
38
|
+
*
|
|
39
|
+
* @param {{ skill: string, timestamp: string }[]} usageData
|
|
40
|
+
* @param {string[]} installedNames
|
|
41
|
+
* @param {Object.<string, Date>} [installDates]
|
|
42
|
+
*/
|
|
43
|
+
export function summarize(usageData, installedNames, installDates = {}) {
|
|
44
|
+
const NOW = Date.now();
|
|
45
|
+
const MS_1D = 24 * 60 * 60 * 1000;
|
|
46
|
+
const MS_60D = 60 * MS_1D;
|
|
47
|
+
const MS_30D = 30 * MS_1D;
|
|
48
|
+
|
|
49
|
+
const countMap = new Map();
|
|
50
|
+
const lastMap = new Map();
|
|
51
|
+
const recent60Map = new Map();
|
|
52
|
+
const cutoff60 = new Date(NOW - MS_60D);
|
|
53
|
+
|
|
54
|
+
for (const { skill, timestamp } of usageData) {
|
|
55
|
+
countMap.set(skill, (countMap.get(skill) ?? 0) + 1);
|
|
56
|
+
const ts = new Date(timestamp);
|
|
57
|
+
if (!lastMap.has(skill) || ts > lastMap.get(skill)) lastMap.set(skill, ts);
|
|
58
|
+
if (ts >= cutoff60) recent60Map.set(skill, (recent60Map.get(skill) ?? 0) + 1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const results = installedNames.map(name => {
|
|
62
|
+
const uses = countMap.get(name) ?? 0;
|
|
63
|
+
const lastUsed = lastMap.get(name) ?? null;
|
|
64
|
+
const daysSince = lastUsed ? Math.floor((NOW - lastUsed.getTime()) / MS_1D) : null;
|
|
65
|
+
const recent60 = recent60Map.get(name) ?? 0;
|
|
66
|
+
const installDate = installDates[name] ?? null;
|
|
67
|
+
|
|
68
|
+
const isEstablished = uses > 0 || (installDate && (NOW - installDate.getTime()) > 14 * MS_1D);
|
|
69
|
+
|
|
70
|
+
let suggestion = null;
|
|
71
|
+
if (uses === 0 && installDate && (NOW - installDate.getTime()) > MS_30D) {
|
|
72
|
+
suggestion = 'remove';
|
|
73
|
+
} else if (isEstablished && recent60 < 2) {
|
|
74
|
+
suggestion = 'low-activity';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { name, uses, lastUsed, daysSinceLastUse: daysSince, suggestion };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
results.sort((a, b) => {
|
|
81
|
+
if (a.suggestion === null && b.suggestion !== null) return -1;
|
|
82
|
+
if (a.suggestion !== null && b.suggestion === null) return 1;
|
|
83
|
+
return b.uses - a.uses;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return results;
|
|
87
|
+
}
|