@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
package/bin/skills.cjs
ADDED
|
@@ -0,0 +1,1292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const command = args[0];
|
|
12
|
+
const skillsRoot = path.join(__dirname, '..', 'skills');
|
|
13
|
+
const commandsRoot = path.join(__dirname, '..', 'commands');
|
|
14
|
+
const agentsRoot = path.join(__dirname, '..', 'agents');
|
|
15
|
+
const rulesRoot = path.join(__dirname, '..', 'rules');
|
|
16
|
+
|
|
17
|
+
// ─── Installation profiles ────────────────────────────────────────────────────
|
|
18
|
+
const PROFILES = {
|
|
19
|
+
core: {
|
|
20
|
+
description: 'Routing + general code quality — a good starting point for any project',
|
|
21
|
+
skills: ['skill-router', 'clean-code-reviewer'],
|
|
22
|
+
agents: ['booklib-reviewer'],
|
|
23
|
+
},
|
|
24
|
+
python: {
|
|
25
|
+
description: 'Python best practices, async patterns, and web scraping',
|
|
26
|
+
skills: ['effective-python', 'using-asyncio-python', 'web-scraping-python'],
|
|
27
|
+
agents: ['python-reviewer'],
|
|
28
|
+
},
|
|
29
|
+
jvm: {
|
|
30
|
+
description: 'Java, Kotlin, and Spring Boot best practices',
|
|
31
|
+
skills: ['effective-java', 'effective-kotlin', 'kotlin-in-action', 'spring-boot-in-action'],
|
|
32
|
+
agents: ['jvm-reviewer'],
|
|
33
|
+
},
|
|
34
|
+
rust: {
|
|
35
|
+
description: 'Rust ownership, systems programming, and idiomatic patterns',
|
|
36
|
+
skills: ['programming-with-rust', 'rust-in-action'],
|
|
37
|
+
agents: ['rust-reviewer'],
|
|
38
|
+
},
|
|
39
|
+
ts: {
|
|
40
|
+
description: 'TypeScript type system and clean code for JS/TS projects',
|
|
41
|
+
skills: ['effective-typescript', 'clean-code-reviewer'],
|
|
42
|
+
agents: ['ts-reviewer'],
|
|
43
|
+
},
|
|
44
|
+
architecture: {
|
|
45
|
+
description: 'DDD, microservices, system design, data-intensive patterns, and GoF design patterns',
|
|
46
|
+
skills: ['domain-driven-design', 'microservices-patterns', 'system-design-interview', 'data-intensive-patterns', 'design-patterns'],
|
|
47
|
+
agents: ['architecture-reviewer'],
|
|
48
|
+
},
|
|
49
|
+
data: {
|
|
50
|
+
description: 'Data pipelines, ETL, and storage system patterns',
|
|
51
|
+
skills: ['data-intensive-patterns', 'data-pipelines'],
|
|
52
|
+
agents: ['data-reviewer'],
|
|
53
|
+
},
|
|
54
|
+
ui: {
|
|
55
|
+
description: 'UI design, data visualization, and web animations',
|
|
56
|
+
skills: ['refactoring-ui', 'storytelling-with-data', 'animation-at-work'],
|
|
57
|
+
agents: ['ui-reviewer'],
|
|
58
|
+
},
|
|
59
|
+
lean: {
|
|
60
|
+
description: 'Lean Startup methodology for product and feature decisions',
|
|
61
|
+
skills: ['lean-startup'],
|
|
62
|
+
agents: [],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ─── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
67
|
+
const c = {
|
|
68
|
+
bold: s => `\x1b[1m${s}\x1b[0m`,
|
|
69
|
+
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
70
|
+
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
71
|
+
cyan: s => `\x1b[36m${s}\x1b[0m`,
|
|
72
|
+
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
73
|
+
red: s => `\x1b[31m${s}\x1b[0m`,
|
|
74
|
+
blue: s => `\x1b[34m${s}\x1b[0m`,
|
|
75
|
+
line: (len = 60) => `\x1b[2m${'─'.repeat(len)}\x1b[0m`,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ─── SKILL.md helpers ─────────────────────────────────────────────────────────
|
|
79
|
+
function parseSkillFrontmatter(skillName) {
|
|
80
|
+
const skillMdPath = path.join(skillsRoot, skillName, 'SKILL.md');
|
|
81
|
+
try {
|
|
82
|
+
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
83
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
84
|
+
if (!fmMatch) return { name: skillName, description: '' };
|
|
85
|
+
const fm = fmMatch[1];
|
|
86
|
+
|
|
87
|
+
const blockMatch = fm.match(/^description:\s*>\s*\n((?:[ \t]+.+\n?)+)/m);
|
|
88
|
+
const quotedMatch = fm.match(/^description:\s*["'](.+?)["']\s*$/m);
|
|
89
|
+
const plainMatch = fm.match(/^description:\s*(?!>)(.+)$/m);
|
|
90
|
+
|
|
91
|
+
let description = '';
|
|
92
|
+
if (blockMatch) description = blockMatch[1].split('\n').map(l => l.trim()).filter(Boolean).join(' ');
|
|
93
|
+
else if (quotedMatch) description = quotedMatch[1];
|
|
94
|
+
else if (plainMatch) description = plainMatch[1].trim();
|
|
95
|
+
|
|
96
|
+
return { name: skillName, description };
|
|
97
|
+
} catch {
|
|
98
|
+
return { name: skillName, description: '' };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getSkillMdContent(skillName) {
|
|
103
|
+
return fs.readFileSync(path.join(skillsRoot, skillName, 'SKILL.md'), 'utf8');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getAvailableSkills() {
|
|
107
|
+
return fs.readdirSync(skillsRoot)
|
|
108
|
+
.filter(name => {
|
|
109
|
+
const p = path.join(skillsRoot, name);
|
|
110
|
+
return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md'));
|
|
111
|
+
})
|
|
112
|
+
.sort();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function firstSentence(text, maxLen = 65) {
|
|
116
|
+
const end = text.search(/[.!?](\s|$)/);
|
|
117
|
+
const s = end >= 0 ? text.slice(0, end + 1) : text;
|
|
118
|
+
return s.length <= maxLen ? s : s.slice(0, maxLen - 1) + '…';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── File copy ────────────────────────────────────────────────────────────────
|
|
122
|
+
function copyDir(src, dest) {
|
|
123
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
124
|
+
for (const entry of fs.readdirSync(src)) {
|
|
125
|
+
const srcPath = path.join(src, entry);
|
|
126
|
+
const destPath = path.join(dest, entry);
|
|
127
|
+
if (fs.statSync(srcPath).isDirectory()) copyDir(srcPath, destPath);
|
|
128
|
+
else fs.copyFileSync(srcPath, destPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function copySkill(skillName, targetDir) {
|
|
133
|
+
const src = path.join(skillsRoot, skillName);
|
|
134
|
+
if (!fs.existsSync(src)) {
|
|
135
|
+
console.error(c.red(`✗ Skill "${skillName}" not found.`) + ' Run ' + c.cyan('skills list') + ' to see available skills.');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
const dest = path.join(targetDir, skillName);
|
|
139
|
+
copyDir(src, dest);
|
|
140
|
+
console.log(c.green('✓') + ` ${c.bold(skillName)} → ${c.dim(dest)}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const isGlobal = args.includes('--global');
|
|
144
|
+
const targetDir = isGlobal
|
|
145
|
+
? path.join(os.homedir(), '.claude', 'skills')
|
|
146
|
+
: path.join(process.cwd(), '.claude', 'skills');
|
|
147
|
+
const commandsTargetDir = isGlobal
|
|
148
|
+
? path.join(os.homedir(), '.claude', 'commands')
|
|
149
|
+
: path.join(process.cwd(), '.claude', 'commands');
|
|
150
|
+
const agentsTargetDir = isGlobal
|
|
151
|
+
? path.join(os.homedir(), '.claude', 'agents')
|
|
152
|
+
: path.join(process.cwd(), '.claude', 'agents');
|
|
153
|
+
const rulesTargetDir = isGlobal
|
|
154
|
+
? path.join(os.homedir(), '.claude', 'rules')
|
|
155
|
+
: path.join(process.cwd(), '.claude', 'rules');
|
|
156
|
+
|
|
157
|
+
function copyCommand(skillName) {
|
|
158
|
+
const src = path.join(commandsRoot, `${skillName}.md`);
|
|
159
|
+
if (!fs.existsSync(src)) return;
|
|
160
|
+
fs.mkdirSync(commandsTargetDir, { recursive: true });
|
|
161
|
+
const dest = path.join(commandsTargetDir, `${skillName}.md`);
|
|
162
|
+
fs.copyFileSync(src, dest);
|
|
163
|
+
console.log(c.green('✓') + ` /${skillName} command → ${c.dim(dest)}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getAvailableAgents() {
|
|
167
|
+
if (!fs.existsSync(agentsRoot)) return [];
|
|
168
|
+
return fs.readdirSync(agentsRoot)
|
|
169
|
+
.filter(f => f.endsWith('.md'))
|
|
170
|
+
.map(f => f.replace(/\.md$/, ''))
|
|
171
|
+
.sort();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseAgentFrontmatter(agentName) {
|
|
175
|
+
const agentMdPath = path.join(agentsRoot, `${agentName}.md`);
|
|
176
|
+
try {
|
|
177
|
+
const content = fs.readFileSync(agentMdPath, 'utf8');
|
|
178
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
179
|
+
if (!fmMatch) return { name: agentName, description: '', model: '' };
|
|
180
|
+
const fm = fmMatch[1];
|
|
181
|
+
|
|
182
|
+
const blockMatch = fm.match(/^description:\s*>\s*\n((?:[ \t]+.+\n?)+)/m);
|
|
183
|
+
const quotedMatch = fm.match(/^description:\s*["'](.+?)["']\s*$/m);
|
|
184
|
+
const plainMatch = fm.match(/^description:\s*(?!>)(.+)$/m);
|
|
185
|
+
const modelMatch = fm.match(/^model:\s*(\S+)/m);
|
|
186
|
+
|
|
187
|
+
let description = '';
|
|
188
|
+
if (blockMatch) description = blockMatch[1].split('\n').map(l => l.trim()).filter(Boolean).join(' ');
|
|
189
|
+
else if (quotedMatch) description = quotedMatch[1];
|
|
190
|
+
else if (plainMatch) description = plainMatch[1].trim();
|
|
191
|
+
|
|
192
|
+
return { name: agentName, description, model: modelMatch?.[1] ?? '' };
|
|
193
|
+
} catch {
|
|
194
|
+
return { name: agentName, description: '', model: '' };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function copyAgent(agentName) {
|
|
199
|
+
const src = path.join(agentsRoot, `${agentName}.md`);
|
|
200
|
+
if (!fs.existsSync(src)) return;
|
|
201
|
+
fs.mkdirSync(agentsTargetDir, { recursive: true });
|
|
202
|
+
const dest = path.join(agentsTargetDir, `${agentName}.md`);
|
|
203
|
+
fs.copyFileSync(src, dest);
|
|
204
|
+
console.log(c.green('✓') + ` @${agentName} agent → ${c.dim(dest)}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── Cursor support ───────────────────────────────────────────────────────────
|
|
208
|
+
function getCursorRulesDir() {
|
|
209
|
+
return isGlobal
|
|
210
|
+
? path.join(os.homedir(), '.cursor', 'rules')
|
|
211
|
+
: path.join(process.cwd(), '.cursor', 'rules');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function copyHooks() {
|
|
215
|
+
const hooksDir = path.join(__dirname, '..', 'hooks');
|
|
216
|
+
if (!fs.existsSync(hooksDir)) return;
|
|
217
|
+
// Copy suggest.js to the .claude/ root as booklib-suggest.js
|
|
218
|
+
const suggestSrc = path.join(hooksDir, 'suggest.js');
|
|
219
|
+
if (fs.existsSync(suggestSrc)) {
|
|
220
|
+
const claudeDir = isGlobal ? path.join(os.homedir(), '.claude') : path.join(process.cwd(), '.claude');
|
|
221
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
222
|
+
const dest = path.join(claudeDir, 'booklib-suggest.js');
|
|
223
|
+
fs.copyFileSync(suggestSrc, dest);
|
|
224
|
+
console.log(c.green('✓') + ` booklib-suggest.js hook → ${c.dim(dest)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getAvailableRules() {
|
|
229
|
+
// Returns [{language, name, file}] for each rule file found under rules/
|
|
230
|
+
if (!fs.existsSync(rulesRoot)) return [];
|
|
231
|
+
const result = [];
|
|
232
|
+
for (const lang of fs.readdirSync(rulesRoot).sort()) {
|
|
233
|
+
const langDir = path.join(rulesRoot, lang);
|
|
234
|
+
if (!fs.statSync(langDir).isDirectory()) continue;
|
|
235
|
+
for (const file of fs.readdirSync(langDir).filter(f => f.endsWith('.md')).sort()) {
|
|
236
|
+
result.push({ language: lang, name: file.replace(/\.md$/, ''), file: path.join(langDir, file) });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function copyRules(language) {
|
|
243
|
+
// Copies all rule files for a given language (or 'common') to rulesTargetDir
|
|
244
|
+
const langDir = path.join(rulesRoot, language);
|
|
245
|
+
if (!fs.existsSync(langDir)) {
|
|
246
|
+
console.error(c.red(`✗ No rules for language "${language}".`) + ' Run ' + c.cyan('skills rules') + ' to see available rules.');
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
const destDir = path.join(rulesTargetDir, language);
|
|
250
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
251
|
+
for (const file of fs.readdirSync(langDir).filter(f => f.endsWith('.md'))) {
|
|
252
|
+
const dest = path.join(destDir, file);
|
|
253
|
+
fs.copyFileSync(path.join(langDir, file), dest);
|
|
254
|
+
console.log(c.green('✓') + ` ${c.bold(language + '/' + file.replace(/\.md$/, ''))} rule → ${c.dim(dest)}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function copyAllRules() {
|
|
259
|
+
const rules = getAvailableRules();
|
|
260
|
+
const languages = [...new Set(rules.map(r => r.language))];
|
|
261
|
+
for (const lang of languages) copyRules(lang);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function copySkillToCursor(skillName) {
|
|
265
|
+
const src = path.join(skillsRoot, skillName, 'SKILL.md');
|
|
266
|
+
if (!fs.existsSync(src)) return;
|
|
267
|
+
const dest = path.join(getCursorRulesDir(), `${skillName}.md`);
|
|
268
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
269
|
+
fs.copyFileSync(src, dest);
|
|
270
|
+
console.log(c.green('✓') + ` ${c.bold(skillName)} → ${c.dim(dest)}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── Windsurf support ────────────────────────────────────────────────────────
|
|
274
|
+
function getWindsurfRulesDir() {
|
|
275
|
+
return isGlobal
|
|
276
|
+
? path.join(os.homedir(), '.windsurf', 'rules')
|
|
277
|
+
: path.join(process.cwd(), '.windsurf', 'rules');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function copySkillToWindsurf(skillName) {
|
|
281
|
+
const src = path.join(skillsRoot, skillName, 'SKILL.md');
|
|
282
|
+
if (!fs.existsSync(src)) return;
|
|
283
|
+
const dest = path.join(getWindsurfRulesDir(), `${skillName}.md`);
|
|
284
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
285
|
+
fs.copyFileSync(src, dest);
|
|
286
|
+
console.log(c.green('✓') + ` ${c.bold(skillName)} → ${c.dim(dest)}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─── OpenCode support ─────────────────────────────────────────────────────────
|
|
290
|
+
function getOpencodeInstructionsDir() {
|
|
291
|
+
return isGlobal
|
|
292
|
+
? path.join(os.homedir(), '.opencode', 'instructions')
|
|
293
|
+
: path.join(process.cwd(), '.opencode', 'instructions');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function copySkillToOpencode(skillName) {
|
|
297
|
+
const src = path.join(skillsRoot, skillName, 'SKILL.md');
|
|
298
|
+
if (!fs.existsSync(src)) return;
|
|
299
|
+
const dest = path.join(getOpencodeInstructionsDir(), `${skillName}.md`);
|
|
300
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
301
|
+
fs.copyFileSync(src, dest);
|
|
302
|
+
console.log(c.green('✓') + ` ${c.bold(skillName)} → ${c.dim(dest)}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─── Kiro support ─────────────────────────────────────────────────────────────
|
|
306
|
+
function getKiroSkillsDir() {
|
|
307
|
+
return isGlobal
|
|
308
|
+
? path.join(os.homedir(), '.kiro', 'skills')
|
|
309
|
+
: path.join(process.cwd(), '.kiro', 'skills');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function copySkillToKiro(skillName) {
|
|
313
|
+
const src = path.join(skillsRoot, skillName, 'SKILL.md');
|
|
314
|
+
if (!fs.existsSync(src)) return;
|
|
315
|
+
const dest = path.join(getKiroSkillsDir(), `${skillName}.md`);
|
|
316
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
317
|
+
fs.copyFileSync(src, dest);
|
|
318
|
+
console.log(c.green('✓') + ` ${c.bold(skillName)} → ${c.dim(dest)}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── Copilot support (bundle into single file) ────────────────────────────────
|
|
322
|
+
function getCopilotInstructionsFile() {
|
|
323
|
+
return path.join(process.cwd(), '.github', 'copilot-instructions.md');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function bundleSkillToCopilot(skillName) {
|
|
327
|
+
const src = path.join(skillsRoot, skillName, 'SKILL.md');
|
|
328
|
+
if (!fs.existsSync(src)) return;
|
|
329
|
+
const dest = getCopilotInstructionsFile();
|
|
330
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
331
|
+
const content = fs.readFileSync(src, 'utf8');
|
|
332
|
+
const section = `\n\n<!-- skill: ${skillName} -->\n${content}\n<!-- /skill: ${skillName} -->`;
|
|
333
|
+
if (fs.existsSync(dest)) {
|
|
334
|
+
let existing = fs.readFileSync(dest, 'utf8');
|
|
335
|
+
const pattern = new RegExp(`\\n\\n<!-- skill: ${skillName} -->[\\s\\S]*?<!-- /skill: ${skillName} -->`);
|
|
336
|
+
if (pattern.test(existing)) {
|
|
337
|
+
fs.writeFileSync(dest, existing.replace(pattern, section));
|
|
338
|
+
} else {
|
|
339
|
+
fs.appendFileSync(dest, section);
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
fs.writeFileSync(dest, `# GitHub Copilot Instructions\n\nGenerated by @booklib/skills.${section}`);
|
|
343
|
+
}
|
|
344
|
+
console.log(c.green('✓') + ` ${c.bold(skillName)} → ${c.dim(dest)}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ─── Gemini CLI support (bundle into GEMINI.md) ───────────────────────────────
|
|
348
|
+
function getGeminiFile() {
|
|
349
|
+
return path.join(process.cwd(), 'GEMINI.md');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function bundleSkillToGemini(skillName) {
|
|
353
|
+
const src = path.join(skillsRoot, skillName, 'SKILL.md');
|
|
354
|
+
if (!fs.existsSync(src)) return;
|
|
355
|
+
const dest = getGeminiFile();
|
|
356
|
+
const content = fs.readFileSync(src, 'utf8');
|
|
357
|
+
const section = `\n\n<!-- skill: ${skillName} -->\n${content}\n<!-- /skill: ${skillName} -->`;
|
|
358
|
+
if (fs.existsSync(dest)) {
|
|
359
|
+
let existing = fs.readFileSync(dest, 'utf8');
|
|
360
|
+
const pattern = new RegExp(`\\n\\n<!-- skill: ${skillName} -->[\\s\\S]*?<!-- /skill: ${skillName} -->`);
|
|
361
|
+
if (pattern.test(existing)) {
|
|
362
|
+
fs.writeFileSync(dest, existing.replace(pattern, section));
|
|
363
|
+
} else {
|
|
364
|
+
fs.appendFileSync(dest, section);
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
fs.writeFileSync(dest, `# Gemini Instructions\n\nGenerated by @booklib/skills.${section}`);
|
|
368
|
+
}
|
|
369
|
+
console.log(c.green('✓') + ` ${c.bold(skillName)} → ${c.dim(dest)}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ─── CHECK command ────────────────────────────────────────────────────────────
|
|
373
|
+
function checkSkill(skillName) {
|
|
374
|
+
const skillDir = path.join(skillsRoot, skillName);
|
|
375
|
+
if (!fs.existsSync(path.join(skillDir, 'SKILL.md'))) {
|
|
376
|
+
console.error(c.red(`✗ "${skillName}" not found or has no SKILL.md`));
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const pass = (tier, msg) => ({ ok: true, tier, msg });
|
|
381
|
+
const fail = (tier, msg) => ({ ok: false, tier, msg });
|
|
382
|
+
const checks = [];
|
|
383
|
+
|
|
384
|
+
const skillMdContent = getSkillMdContent(skillName);
|
|
385
|
+
const lines = skillMdContent.split('\n');
|
|
386
|
+
const { name, description } = parseSkillFrontmatter(skillName);
|
|
387
|
+
|
|
388
|
+
// ── Bronze ──────────────────────────────────────────────────────────────────
|
|
389
|
+
checks.push(name === skillName
|
|
390
|
+
? pass('bronze', `name matches folder (${name})`)
|
|
391
|
+
: fail('bronze', `name mismatch — SKILL.md: "${name}", folder: "${skillName}"`));
|
|
392
|
+
|
|
393
|
+
checks.push(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name)
|
|
394
|
+
? pass('bronze', 'name format valid (lowercase, hyphens)')
|
|
395
|
+
: fail('bronze', `name format invalid — must be lowercase letters, numbers, hyphens; no consecutive hyphens`));
|
|
396
|
+
|
|
397
|
+
if (description.length < 50)
|
|
398
|
+
checks.push(fail('bronze', `description too short: ${description.length} chars (min 50)`));
|
|
399
|
+
else if (description.length > 1024)
|
|
400
|
+
checks.push(fail('bronze', `description too long: ${description.length} chars (max 1024)`));
|
|
401
|
+
else
|
|
402
|
+
checks.push(pass('bronze', `description: ${description.length} chars`));
|
|
403
|
+
|
|
404
|
+
checks.push(/trigger|use when|use for|when.*ask|when.*mention/i.test(description)
|
|
405
|
+
? pass('bronze', 'description has trigger conditions')
|
|
406
|
+
: fail('bronze', 'description missing trigger conditions — add "use when…" or "trigger on…"'));
|
|
407
|
+
|
|
408
|
+
checks.push(lines.length <= 500
|
|
409
|
+
? pass('bronze', `SKILL.md: ${lines.length} lines`)
|
|
410
|
+
: fail('bronze', `SKILL.md too long: ${lines.length} lines — move content to references/`));
|
|
411
|
+
|
|
412
|
+
const bodyStart = skillMdContent.indexOf('---', 3);
|
|
413
|
+
const body = bodyStart >= 0 ? skillMdContent.slice(bodyStart + 3).trim() : '';
|
|
414
|
+
checks.push(body.split('\n').length > 30
|
|
415
|
+
? pass('bronze', `body present (${body.split('\n').length} lines of instructions)`)
|
|
416
|
+
: fail('bronze', 'body too thin — add actionable step-by-step instructions'));
|
|
417
|
+
|
|
418
|
+
// ── Silver ──────────────────────────────────────────────────────────────────
|
|
419
|
+
for (const [file, label] of [['before.md', 'examples/before.md'], ['after.md', 'examples/after.md']]) {
|
|
420
|
+
const p = path.join(skillDir, 'examples', file);
|
|
421
|
+
if (!fs.existsSync(p)) {
|
|
422
|
+
checks.push(fail('silver', `${label} missing`));
|
|
423
|
+
} else {
|
|
424
|
+
const n = fs.readFileSync(p, 'utf8').split('\n').length;
|
|
425
|
+
checks.push(n >= 10
|
|
426
|
+
? pass('silver', `${label} (${n} lines)`)
|
|
427
|
+
: fail('silver', `${label} too short: ${n} lines (need 10+)`));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Gold ────────────────────────────────────────────────────────────────────
|
|
432
|
+
const evalsPath = path.join(skillDir, 'evals', 'evals.json');
|
|
433
|
+
if (!fs.existsSync(evalsPath)) {
|
|
434
|
+
checks.push(fail('gold', 'evals/evals.json missing'));
|
|
435
|
+
checks.push(fail('gold', 'eval prompts not checked (no evals.json)'));
|
|
436
|
+
checks.push(fail('gold', 'eval expectations not checked (no evals.json)'));
|
|
437
|
+
} else {
|
|
438
|
+
let evals = [];
|
|
439
|
+
try {
|
|
440
|
+
evals = JSON.parse(fs.readFileSync(evalsPath, 'utf8')).evals || [];
|
|
441
|
+
} catch {
|
|
442
|
+
checks.push(fail('gold', 'evals/evals.json is invalid JSON'));
|
|
443
|
+
}
|
|
444
|
+
if (evals.length) {
|
|
445
|
+
checks.push(evals.length >= 3
|
|
446
|
+
? pass('gold', `evals/evals.json: ${evals.length} evals`)
|
|
447
|
+
: fail('gold', `only ${evals.length} evals (need 3+)`));
|
|
448
|
+
|
|
449
|
+
const avgLines = evals.reduce((s, e) => s + (e.prompt || '').split('\n').length, 0) / evals.length;
|
|
450
|
+
checks.push(avgLines >= 8
|
|
451
|
+
? pass('gold', `eval prompts have code (avg ${Math.round(avgLines)} lines)`)
|
|
452
|
+
: fail('gold', `eval prompts may lack real code (avg ${Math.round(avgLines)} lines, target 10+)`));
|
|
453
|
+
|
|
454
|
+
const avgExp = evals.reduce((s, e) => s + (e.expectations || []).length, 0) / evals.length;
|
|
455
|
+
checks.push(avgExp >= 5
|
|
456
|
+
? pass('gold', `eval expectations thorough (avg ${(avgExp).toFixed(1)} per eval)`)
|
|
457
|
+
: fail('gold', `few expectations per eval (avg ${(avgExp).toFixed(1)}, target 5+)`));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const refsDir = path.join(skillDir, 'references');
|
|
462
|
+
if (!fs.existsSync(refsDir)) {
|
|
463
|
+
checks.push(fail('gold', 'references/ directory missing'));
|
|
464
|
+
} else {
|
|
465
|
+
const refFiles = fs.readdirSync(refsDir).filter(f => f.endsWith('.md'));
|
|
466
|
+
checks.push(refFiles.length >= 1
|
|
467
|
+
? pass('gold', `references/ (${refFiles.length} files: ${refFiles.join(', ')})`)
|
|
468
|
+
: fail('gold', 'references/ has no .md files'));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── Platinum ────────────────────────────────────────────────────────────────
|
|
472
|
+
const scriptsDir = path.join(skillDir, 'scripts');
|
|
473
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
474
|
+
checks.push(fail('platinum', 'scripts/ directory missing'));
|
|
475
|
+
} else {
|
|
476
|
+
const scriptFiles = fs.readdirSync(scriptsDir).filter(f => !f.startsWith('.'));
|
|
477
|
+
checks.push(scriptFiles.length >= 1
|
|
478
|
+
? pass('platinum', `scripts/ (${scriptFiles.join(', ')})`)
|
|
479
|
+
: fail('platinum', 'scripts/ exists but is empty'));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const resultsPath = path.join(skillDir, 'evals', 'results.json');
|
|
483
|
+
if (!fs.existsSync(resultsPath)) {
|
|
484
|
+
checks.push(fail('platinum', 'evals/results.json missing — run: npx @booklib/skills eval <name>'));
|
|
485
|
+
} else {
|
|
486
|
+
let results = null;
|
|
487
|
+
try { results = JSON.parse(fs.readFileSync(resultsPath, 'utf8')); } catch {
|
|
488
|
+
checks.push(fail('platinum', 'evals/results.json is invalid JSON'));
|
|
489
|
+
}
|
|
490
|
+
if (results) {
|
|
491
|
+
if (results.non_standard_provider) {
|
|
492
|
+
checks.push(fail('platinum', `eval results from non-standard provider (${results.model}) — rerun with ANTHROPIC_API_KEY or OPENAI_API_KEY`));
|
|
493
|
+
}
|
|
494
|
+
const pct = Math.round((results.pass_rate || 0) * 100);
|
|
495
|
+
const meta = `(${results.evals_run} evals, ${results.model}, ${results.date})`;
|
|
496
|
+
checks.push(pct >= 80
|
|
497
|
+
? pass('platinum', `eval pass rate: ${pct}% with skill ${meta}`)
|
|
498
|
+
: fail('platinum', `eval pass rate ${pct}% below 80% minimum — run: npx @booklib/skills eval <name>`));
|
|
499
|
+
if (results.delta !== undefined) {
|
|
500
|
+
const deltaPp = Math.round(results.delta * 100);
|
|
501
|
+
const basePct = Math.round((results.baseline_pass_rate || 0) * 100);
|
|
502
|
+
checks.push(deltaPp >= 20
|
|
503
|
+
? pass('platinum', `eval delta: +${deltaPp}pp over baseline (${basePct}% without skill)`)
|
|
504
|
+
: fail('platinum', `eval delta +${deltaPp}pp below 20pp minimum (baseline: ${basePct}%)`));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return checks;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const TIERS = ['bronze', 'silver', 'gold', 'platinum'];
|
|
513
|
+
const BADGE = { bronze: '🥉 Bronze', silver: '🥈 Silver', gold: '🥇 Gold', platinum: '💎 Platinum' };
|
|
514
|
+
const LABEL = { bronze: 'Functional', silver: 'Complete', gold: 'Polished', platinum: 'Exemplary' };
|
|
515
|
+
|
|
516
|
+
function earnedBadge(checks) {
|
|
517
|
+
let badge = null;
|
|
518
|
+
for (const tier of TIERS) {
|
|
519
|
+
const tierChecks = checks.filter(r => r.tier === tier);
|
|
520
|
+
if (tierChecks.every(r => r.ok)) badge = tier;
|
|
521
|
+
else break;
|
|
522
|
+
}
|
|
523
|
+
return badge;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function printCheckResults(skillName, checks) {
|
|
527
|
+
console.log('');
|
|
528
|
+
console.log(c.bold(` ${skillName}`) + c.dim(' — quality check'));
|
|
529
|
+
console.log(' ' + c.line(55));
|
|
530
|
+
|
|
531
|
+
for (const tier of TIERS) {
|
|
532
|
+
const tierChecks = checks.filter(r => r.tier === tier);
|
|
533
|
+
console.log(`\n ${BADGE[tier]} — ${c.dim(LABEL[tier])}`);
|
|
534
|
+
for (const r of tierChecks) {
|
|
535
|
+
const icon = r.ok ? c.green('✓') : c.red('✗');
|
|
536
|
+
console.log(` ${icon} ${r.ok ? r.msg : c.red(r.msg)}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const badge = earnedBadge(checks);
|
|
541
|
+
console.log('');
|
|
542
|
+
console.log(` Result: ${badge ? BADGE[badge] : c.dim('No badge — fix Bronze issues first')}`);
|
|
543
|
+
console.log('');
|
|
544
|
+
return badge;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ─── EVAL command ─────────────────────────────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
function commandExists(cmd) {
|
|
550
|
+
const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [cmd], { stdio: 'ignore' });
|
|
551
|
+
return result.status === 0;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function detectProvider() {
|
|
555
|
+
if (process.env.ANTHROPIC_API_KEY)
|
|
556
|
+
return { type: 'anthropic', defaultModel: 'claude-haiku-4-5-20251001' };
|
|
557
|
+
if (process.env.OPENAI_API_KEY)
|
|
558
|
+
return { type: 'openai-compat', baseUrl: 'https://api.openai.com/v1', key: process.env.OPENAI_API_KEY, defaultModel: 'gpt-4o-mini' };
|
|
559
|
+
if (process.env.EVAL_API_KEY && process.env.EVAL_BASE_URL)
|
|
560
|
+
return { type: 'openai-compat', baseUrl: process.env.EVAL_BASE_URL, key: process.env.EVAL_API_KEY, defaultModel: null };
|
|
561
|
+
if (commandExists('claude'))
|
|
562
|
+
return { type: 'claude-cli', defaultModel: 'default' };
|
|
563
|
+
if (commandExists('ollama'))
|
|
564
|
+
return { type: 'openai-compat', baseUrl: 'http://localhost:11434/v1', key: 'ollama', defaultModel: null };
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function callAnthropicApi(systemPrompt, userMessage, model) {
|
|
569
|
+
const reqBody = { model, max_tokens: 4096, messages: [{ role: 'user', content: userMessage }] };
|
|
570
|
+
if (systemPrompt) reqBody.system = systemPrompt;
|
|
571
|
+
const body = JSON.stringify(reqBody);
|
|
572
|
+
|
|
573
|
+
return new Promise((resolve, reject) => {
|
|
574
|
+
const req = https.request({
|
|
575
|
+
hostname: 'api.anthropic.com',
|
|
576
|
+
path: '/v1/messages',
|
|
577
|
+
method: 'POST',
|
|
578
|
+
headers: {
|
|
579
|
+
'Content-Type': 'application/json',
|
|
580
|
+
'x-api-key': process.env.ANTHROPIC_API_KEY,
|
|
581
|
+
'anthropic-version': '2023-06-01',
|
|
582
|
+
'Content-Length': Buffer.byteLength(body),
|
|
583
|
+
},
|
|
584
|
+
}, res => {
|
|
585
|
+
let data = '';
|
|
586
|
+
res.on('data', chunk => data += chunk);
|
|
587
|
+
res.on('end', () => {
|
|
588
|
+
try {
|
|
589
|
+
const parsed = JSON.parse(data);
|
|
590
|
+
if (parsed.error) reject(new Error(parsed.error.message));
|
|
591
|
+
else resolve(parsed.content?.[0]?.text ?? '');
|
|
592
|
+
} catch (e) { reject(e); }
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
req.on('error', reject);
|
|
596
|
+
req.write(body);
|
|
597
|
+
req.end();
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function callOpenAICompat(baseUrl, apiKey, systemPrompt, userMessage, model) {
|
|
602
|
+
const messages = [];
|
|
603
|
+
if (systemPrompt) messages.push({ role: 'system', content: systemPrompt });
|
|
604
|
+
messages.push({ role: 'user', content: userMessage });
|
|
605
|
+
const body = JSON.stringify({ model, max_tokens: 4096, messages });
|
|
606
|
+
|
|
607
|
+
const url = new URL('/chat/completions', baseUrl);
|
|
608
|
+
const isHttps = url.protocol === 'https:';
|
|
609
|
+
const transport = isHttps ? https : http;
|
|
610
|
+
|
|
611
|
+
return new Promise((resolve, reject) => {
|
|
612
|
+
const req = transport.request({
|
|
613
|
+
hostname: url.hostname,
|
|
614
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
615
|
+
path: url.pathname + url.search,
|
|
616
|
+
method: 'POST',
|
|
617
|
+
headers: {
|
|
618
|
+
'Content-Type': 'application/json',
|
|
619
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
620
|
+
'Content-Length': Buffer.byteLength(body),
|
|
621
|
+
},
|
|
622
|
+
}, res => {
|
|
623
|
+
let data = '';
|
|
624
|
+
res.on('data', chunk => data += chunk);
|
|
625
|
+
res.on('end', () => {
|
|
626
|
+
try {
|
|
627
|
+
const parsed = JSON.parse(data);
|
|
628
|
+
if (parsed.error) reject(new Error(parsed.error.message || JSON.stringify(parsed.error)));
|
|
629
|
+
else resolve(parsed.choices?.[0]?.message?.content ?? '');
|
|
630
|
+
} catch (e) { reject(e); }
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
req.on('error', reject);
|
|
634
|
+
req.write(body);
|
|
635
|
+
req.end();
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function callClaudeCli(systemPrompt, userMessage) {
|
|
640
|
+
// --bare disables OAuth/keychain auth (requires ANTHROPIC_API_KEY), so omit it
|
|
641
|
+
// when using a subscription-based Claude login.
|
|
642
|
+
const cliArgs = ['-p', userMessage, '--tools', ''];
|
|
643
|
+
if (systemPrompt) cliArgs.push('--system-prompt', systemPrompt);
|
|
644
|
+
const result = spawnSync('claude', cliArgs, {
|
|
645
|
+
encoding: 'utf8',
|
|
646
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
647
|
+
timeout: 120000,
|
|
648
|
+
});
|
|
649
|
+
if (result.error) return Promise.reject(result.error);
|
|
650
|
+
if (result.status !== 0) return Promise.reject(new Error(result.stderr?.trim() || 'claude CLI failed'));
|
|
651
|
+
return Promise.resolve(result.stdout.trim());
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
let _provider = null;
|
|
655
|
+
function getProvider() {
|
|
656
|
+
if (!_provider) _provider = detectProvider();
|
|
657
|
+
return _provider;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function callLLM(systemPrompt, userMessage, model) {
|
|
661
|
+
const provider = getProvider();
|
|
662
|
+
if (!provider) throw new Error(
|
|
663
|
+
'No LLM provider found.\n' +
|
|
664
|
+
' Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or EVAL_API_KEY+EVAL_BASE_URL\n' +
|
|
665
|
+
' — or install Claude Code (claude.ai/code) or Ollama (ollama.com)'
|
|
666
|
+
);
|
|
667
|
+
if (provider.type === 'anthropic') return callAnthropicApi(systemPrompt, userMessage, model);
|
|
668
|
+
if (provider.type === 'openai-compat') return callOpenAICompat(provider.baseUrl, provider.key, systemPrompt, userMessage, model);
|
|
669
|
+
if (provider.type === 'claude-cli') return callClaudeCli(systemPrompt, userMessage);
|
|
670
|
+
throw new Error(`Unknown provider type: ${provider.type}`);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function judgeResponse(response, expectations, model) {
|
|
674
|
+
const numbered = expectations.map((e, i) => `${i + 1}. ${e}`).join('\n');
|
|
675
|
+
const judgeSystem = `You are an eval judge. For each numbered expectation, respond with exactly:
|
|
676
|
+
<n>. PASS — <brief one-line reason>
|
|
677
|
+
or
|
|
678
|
+
<n>. FAIL — <brief one-line reason>
|
|
679
|
+
Output only the numbered lines. No other text.`;
|
|
680
|
+
|
|
681
|
+
const judgePrompt = `=== Response to evaluate ===
|
|
682
|
+
${response}
|
|
683
|
+
|
|
684
|
+
=== Expectations ===
|
|
685
|
+
${numbered}`;
|
|
686
|
+
|
|
687
|
+
return callLLM(judgeSystem, judgePrompt, model);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function parseJudgement(judgement, count) {
|
|
691
|
+
const results = [];
|
|
692
|
+
for (let i = 1; i <= count; i++) {
|
|
693
|
+
const match = judgement.match(new RegExp(`${i}\\.\\s*(PASS|FAIL)\\s*[—\\-–]?\\s*(.+)`, 'i'));
|
|
694
|
+
if (match) {
|
|
695
|
+
results.push({ ok: match[1].toUpperCase() === 'PASS', reason: match[2].trim() });
|
|
696
|
+
} else {
|
|
697
|
+
results.push({ ok: false, reason: 'judge did not return a result for this expectation' });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return results;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function runEvalSet(evals, systemPrompt, model, judgeModel, verbose) {
|
|
704
|
+
let totalPass = 0, totalFail = 0, evalsFullyPassed = 0;
|
|
705
|
+
|
|
706
|
+
for (const ev of evals) {
|
|
707
|
+
const promptLines = (ev.prompt || '').split('\n').length;
|
|
708
|
+
const expectations = ev.expectations || [];
|
|
709
|
+
|
|
710
|
+
if (verbose) {
|
|
711
|
+
process.stdout.write(` ${c.cyan('●')} ${c.bold(ev.id)}\n`);
|
|
712
|
+
process.stdout.write(c.dim(` prompt: ${promptLines} lines — calling ${model}...`));
|
|
713
|
+
} else {
|
|
714
|
+
process.stdout.write(c.dim(` ${ev.id}...`));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let response;
|
|
718
|
+
try {
|
|
719
|
+
response = await callLLM(systemPrompt, ev.prompt, model);
|
|
720
|
+
if (verbose) process.stdout.write(c.green(' done\n'));
|
|
721
|
+
else process.stdout.write(c.dim(' ✓\n'));
|
|
722
|
+
} catch (e) {
|
|
723
|
+
if (verbose) process.stdout.write(c.red(` failed: ${e.message}\n`));
|
|
724
|
+
else process.stdout.write(c.red(` ✗\n`));
|
|
725
|
+
totalFail += expectations.length;
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (verbose) process.stdout.write(c.dim(` judging ${expectations.length} expectations...`));
|
|
730
|
+
|
|
731
|
+
let judgement;
|
|
732
|
+
try {
|
|
733
|
+
judgement = await judgeResponse(response, expectations, judgeModel);
|
|
734
|
+
if (verbose) process.stdout.write(c.dim(' done\n'));
|
|
735
|
+
} catch (e) {
|
|
736
|
+
if (verbose) process.stdout.write(c.red(` judge failed: ${e.message}\n`));
|
|
737
|
+
totalFail += expectations.length;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const results = parseJudgement(judgement, expectations.length);
|
|
742
|
+
let evalPass = 0;
|
|
743
|
+
|
|
744
|
+
for (let i = 0; i < expectations.length; i++) {
|
|
745
|
+
const r = results[i];
|
|
746
|
+
if (verbose) {
|
|
747
|
+
const icon = r.ok ? c.green('✓') : c.red('✗');
|
|
748
|
+
const exp = expectations[i].length > 80 ? expectations[i].slice(0, 79) + '…' : expectations[i];
|
|
749
|
+
console.log(` ${icon} ${exp}`);
|
|
750
|
+
if (!r.ok) console.log(c.dim(` → ${r.reason}`));
|
|
751
|
+
}
|
|
752
|
+
if (r.ok) { evalPass++; totalPass++; } else { totalFail++; }
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const evalTotal = expectations.length;
|
|
756
|
+
const allPassed = evalPass === evalTotal;
|
|
757
|
+
if (allPassed) evalsFullyPassed++;
|
|
758
|
+
if (verbose) console.log(c.dim(` ${evalPass}/${evalTotal} expectations passed`) + (allPassed ? ' ' + c.green('✓') : '') + '\n');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const total = totalPass + totalFail;
|
|
762
|
+
return { passed: totalPass, failed: totalFail, total, evalsFullyPassed, pass_rate: total > 0 ? totalPass / total : 0 };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async function runEvals(skillName, opts = {}) {
|
|
766
|
+
const skillDir = path.join(skillsRoot, skillName);
|
|
767
|
+
const evalsPath = path.join(skillDir, 'evals', 'evals.json');
|
|
768
|
+
const provider = getProvider();
|
|
769
|
+
if (!provider) {
|
|
770
|
+
console.error(c.red(
|
|
771
|
+
'✗ No LLM provider found.\n' +
|
|
772
|
+
' Options (pick one):\n' +
|
|
773
|
+
' ANTHROPIC_API_KEY=sk-ant-... (Anthropic API)\n' +
|
|
774
|
+
' OPENAI_API_KEY=sk-... (OpenAI)\n' +
|
|
775
|
+
' EVAL_API_KEY=... EVAL_BASE_URL=https://api.groq.com/openai/v1 (any OpenAI-compatible)\n' +
|
|
776
|
+
' Install Claude Code: claude.ai/code (subscription, no key)\n' +
|
|
777
|
+
' Install Ollama: ollama.com (local, no key)'
|
|
778
|
+
));
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
const defaultModel = provider.defaultModel;
|
|
782
|
+
const model = opts.model || process.env.EVAL_MODEL || defaultModel;
|
|
783
|
+
if (!model) {
|
|
784
|
+
console.error(c.red(`✗ No model specified. Use --model=<name> or set EVAL_MODEL env var.`));
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
const judgeModel = model;
|
|
788
|
+
const filterId = opts.id || null;
|
|
789
|
+
|
|
790
|
+
if (!fs.existsSync(evalsPath)) {
|
|
791
|
+
console.error(c.red(`✗ No evals/evals.json found for "${skillName}"`));
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
let evals;
|
|
796
|
+
try {
|
|
797
|
+
evals = JSON.parse(fs.readFileSync(evalsPath, 'utf8')).evals || [];
|
|
798
|
+
} catch {
|
|
799
|
+
console.error(c.red('✗ evals/evals.json is invalid JSON'));
|
|
800
|
+
process.exit(1);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (filterId) evals = evals.filter(e => e.id === filterId);
|
|
804
|
+
if (!evals.length) {
|
|
805
|
+
console.error(c.red(`✗ No evals found${filterId ? ` matching --id ${filterId}` : ''}`));
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const skillMd = getSkillMdContent(skillName);
|
|
810
|
+
|
|
811
|
+
console.log('');
|
|
812
|
+
console.log(c.bold(` ${skillName}`) + c.dim(` — evals (${evals.length})`));
|
|
813
|
+
console.log(' ' + c.line(55));
|
|
814
|
+
const providerLabel = provider.type === 'claude-cli' ? 'claude CLI' : provider.type === 'anthropic' ? 'Anthropic API' : provider.baseUrl;
|
|
815
|
+
console.log(c.dim(` provider: ${providerLabel} model: ${model}\n`));
|
|
816
|
+
|
|
817
|
+
// ── With-skill run ──────────────────────────────────────────────────────────
|
|
818
|
+
console.log(c.bold(' With skill\n'));
|
|
819
|
+
const withResult = await runEvalSet(evals, skillMd, model, judgeModel, true);
|
|
820
|
+
const withPct = Math.round(withResult.pass_rate * 100);
|
|
821
|
+
const withColor = withPct >= 80 ? c.green : withPct >= 60 ? c.yellow : c.red;
|
|
822
|
+
console.log(' ' + c.line(55));
|
|
823
|
+
console.log(` ${withColor(`${withPct}%`)} — ${withResult.evalsFullyPassed}/${evals.length} evals fully passed, ${withResult.passed}/${withResult.total} expectations met\n`);
|
|
824
|
+
|
|
825
|
+
// ── Baseline run (no skill system prompt) ───────────────────────────────────
|
|
826
|
+
console.log(c.dim(' Baseline (without skill)\n'));
|
|
827
|
+
const baseResult = await runEvalSet(evals, null, model, judgeModel, false);
|
|
828
|
+
const basePct = Math.round(baseResult.pass_rate * 100);
|
|
829
|
+
console.log(' ' + c.line(55));
|
|
830
|
+
console.log(c.dim(` ${basePct}% — ${baseResult.passed}/${baseResult.total} expectations met\n`));
|
|
831
|
+
|
|
832
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
833
|
+
const deltaPp = withPct - basePct;
|
|
834
|
+
const deltaColor = deltaPp >= 20 ? c.green : deltaPp >= 10 ? c.yellow : c.red;
|
|
835
|
+
console.log(' ' + c.line(55));
|
|
836
|
+
console.log(` ${c.bold('Summary')} with skill: ${withColor(`${withPct}%`)} baseline: ${c.dim(`${basePct}%`)} delta: ${deltaColor(`+${deltaPp}pp`)}`);
|
|
837
|
+
|
|
838
|
+
// ── Warn if using a non-standard provider ───────────────────────────────────
|
|
839
|
+
const isLocalModel = provider.type === 'openai-compat' && provider.baseUrl.includes('localhost');
|
|
840
|
+
const isCliModel = provider.type === 'claude-cli';
|
|
841
|
+
if (isLocalModel || isCliModel) {
|
|
842
|
+
const providerName = isLocalModel ? `local model (${model})` : 'claude CLI';
|
|
843
|
+
console.log('');
|
|
844
|
+
console.log(c.yellow(` ⚠ Results generated with ${providerName}.`));
|
|
845
|
+
console.log(c.dim(' For committing to the repo, use a standardized provider so scores'));
|
|
846
|
+
console.log(c.dim(' are comparable across all skills:'));
|
|
847
|
+
console.log(c.dim(' ANTHROPIC_API_KEY=... (recommended: claude-haiku-4-5-20251001)'));
|
|
848
|
+
console.log(c.dim(' OPENAI_API_KEY=... (recommended: gpt-4o-mini)'));
|
|
849
|
+
console.log(c.dim(' results.json will be written but should not be committed as-is.'));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// ── Write results.json ───────────────────────────────────────────────────────
|
|
853
|
+
const resultsData = {
|
|
854
|
+
pass_rate: Math.round(withResult.pass_rate * 1000) / 1000,
|
|
855
|
+
passed: withResult.passed,
|
|
856
|
+
total: withResult.total,
|
|
857
|
+
baseline_pass_rate: Math.round(baseResult.pass_rate * 1000) / 1000,
|
|
858
|
+
baseline_passed: baseResult.passed,
|
|
859
|
+
baseline_total: baseResult.total,
|
|
860
|
+
delta: Math.round((withResult.pass_rate - baseResult.pass_rate) * 1000) / 1000,
|
|
861
|
+
model,
|
|
862
|
+
evals_run: evals.length,
|
|
863
|
+
date: new Date().toISOString().split('T')[0],
|
|
864
|
+
...(isLocalModel || isCliModel ? { non_standard_provider: true } : {}),
|
|
865
|
+
};
|
|
866
|
+
const resultsPath = path.join(skillDir, 'evals', 'results.json');
|
|
867
|
+
fs.writeFileSync(resultsPath, JSON.stringify(resultsData, null, 2));
|
|
868
|
+
console.log(c.dim(`\n ✓ results saved → evals/results.json\n`));
|
|
869
|
+
|
|
870
|
+
if (withPct < 80) {
|
|
871
|
+
console.error(c.red(` ✗ Pass rate ${withPct}% is below the 80% minimum\n`));
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ─── Router ───────────────────────────────────────────────────────────────────
|
|
877
|
+
async function main() {
|
|
878
|
+
switch (command) {
|
|
879
|
+
|
|
880
|
+
case 'list': {
|
|
881
|
+
const skills = getAvailableSkills();
|
|
882
|
+
const nameWidth = Math.max(...skills.map(s => s.length)) + 2;
|
|
883
|
+
console.log('');
|
|
884
|
+
console.log(c.bold(` Skills`) + c.dim(` (${skills.length} available)`));
|
|
885
|
+
console.log(' ' + c.line(nameWidth + 67));
|
|
886
|
+
for (const s of skills) {
|
|
887
|
+
const { description } = parseSkillFrontmatter(s);
|
|
888
|
+
console.log(` ${c.cyan(s.padEnd(nameWidth))}${c.dim(description ? firstSentence(description) : '')}`);
|
|
889
|
+
}
|
|
890
|
+
console.log(' ' + c.line(nameWidth + 67));
|
|
891
|
+
console.log(c.dim(`\n npx @booklib/skills add <name> install (Claude, default)`));
|
|
892
|
+
console.log(c.dim(` npx @booklib/skills add <name> --target=<tool> cursor | windsurf | opencode | kiro | copilot | gemini | all`));
|
|
893
|
+
console.log(c.dim(` npx @booklib/skills info <name> full description`));
|
|
894
|
+
console.log(c.dim(` npx @booklib/skills demo <name> before/after example`));
|
|
895
|
+
console.log(c.dim(` npx @booklib/skills check <name> quality check\n`));
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
case 'info': {
|
|
900
|
+
const skillName = args.find(a => !a.startsWith('--') && a !== 'info');
|
|
901
|
+
if (!skillName) { console.error(c.red('Usage: skills info <skill-name>')); process.exit(1); }
|
|
902
|
+
const skills = getAvailableSkills();
|
|
903
|
+
if (!skills.includes(skillName)) {
|
|
904
|
+
console.error(c.red(`✗ "${skillName}" not found.`) + ' Run ' + c.cyan('skills list') + ' to see available skills.');
|
|
905
|
+
process.exit(1);
|
|
906
|
+
}
|
|
907
|
+
const { description } = parseSkillFrontmatter(skillName);
|
|
908
|
+
const skillMdPath = path.join(skillsRoot, skillName, 'SKILL.md');
|
|
909
|
+
const hasEvals = fs.existsSync(path.join(skillsRoot, skillName, 'evals'));
|
|
910
|
+
const hasExamples = fs.existsSync(path.join(skillsRoot, skillName, 'examples'));
|
|
911
|
+
const hasRefs = fs.existsSync(path.join(skillsRoot, skillName, 'references'));
|
|
912
|
+
const lines = fs.readFileSync(skillMdPath, 'utf8').split('\n').length;
|
|
913
|
+
|
|
914
|
+
console.log('');
|
|
915
|
+
console.log(c.bold(` ${skillName}`));
|
|
916
|
+
console.log(' ' + c.line(60));
|
|
917
|
+
const words = description.split(' ');
|
|
918
|
+
let line = ' ';
|
|
919
|
+
for (const word of words) {
|
|
920
|
+
if (line.length + word.length > 74) { console.log(line); line = ' ' + word + ' '; }
|
|
921
|
+
else line += word + ' ';
|
|
922
|
+
}
|
|
923
|
+
if (line.trim()) console.log(line);
|
|
924
|
+
console.log('');
|
|
925
|
+
console.log(c.dim(' Includes: ') + [
|
|
926
|
+
hasEvals ? c.green('evals') : null,
|
|
927
|
+
hasExamples ? c.green('examples') : null,
|
|
928
|
+
hasRefs ? c.green('references') : null,
|
|
929
|
+
`${lines} lines`,
|
|
930
|
+
].filter(Boolean).join(c.dim(' · ')));
|
|
931
|
+
console.log('');
|
|
932
|
+
console.log(` ${c.cyan('Install:')} npx @booklib/skills add ${skillName}`);
|
|
933
|
+
if (hasExamples) console.log(` ${c.cyan('Demo:')} npx @booklib/skills demo ${skillName}`);
|
|
934
|
+
console.log(` ${c.cyan('Check:')} npx @booklib/skills check ${skillName}`);
|
|
935
|
+
console.log('');
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
case 'demo': {
|
|
940
|
+
const skillName = args.find(a => !a.startsWith('--') && a !== 'demo');
|
|
941
|
+
if (!skillName) { console.error(c.red('Usage: skills demo <skill-name>')); process.exit(1); }
|
|
942
|
+
const skills = getAvailableSkills();
|
|
943
|
+
if (!skills.includes(skillName)) {
|
|
944
|
+
console.error(c.red(`✗ "${skillName}" not found.`) + ' Run ' + c.cyan('skills list') + ' to see available skills.');
|
|
945
|
+
process.exit(1);
|
|
946
|
+
}
|
|
947
|
+
const beforePath = path.join(skillsRoot, skillName, 'examples', 'before.md');
|
|
948
|
+
const afterPath = path.join(skillsRoot, skillName, 'examples', 'after.md');
|
|
949
|
+
if (!fs.existsSync(beforePath) || !fs.existsSync(afterPath)) {
|
|
950
|
+
console.log(c.yellow(` No demo available for "${skillName}" yet.`));
|
|
951
|
+
console.log(c.dim(` Try: npx @booklib/skills info ${skillName}\n`));
|
|
952
|
+
process.exit(0);
|
|
953
|
+
}
|
|
954
|
+
const before = fs.readFileSync(beforePath, 'utf8').trim();
|
|
955
|
+
const after = fs.readFileSync(afterPath, 'utf8').trim();
|
|
956
|
+
console.log('');
|
|
957
|
+
console.log(c.bold(` ${skillName}`) + c.dim(' — before/after example'));
|
|
958
|
+
console.log(' ' + c.line(60));
|
|
959
|
+
console.log('\n' + c.bold(c.yellow(' BEFORE')) + '\n');
|
|
960
|
+
before.split('\n').forEach(l => console.log(' ' + l));
|
|
961
|
+
console.log('\n' + c.bold(c.green(' AFTER')) + '\n');
|
|
962
|
+
after.split('\n').forEach(l => console.log(' ' + l));
|
|
963
|
+
console.log(c.dim(`\n Install: npx @booklib/skills add ${skillName}\n`));
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
case 'add': {
|
|
968
|
+
const addAll = args.includes('--all');
|
|
969
|
+
const addHooks = args.includes('--hooks');
|
|
970
|
+
const noCommands = args.includes('--no-commands');
|
|
971
|
+
const noAgents = args.includes('--no-agents');
|
|
972
|
+
const agentArg = args.find(a => a.startsWith('--agent='))?.split('=')[1];
|
|
973
|
+
const profileArg = args.find(a => a.startsWith('--profile='))?.split('=')[1];
|
|
974
|
+
const rulesArg = args.find(a => a === '--rules' || a.startsWith('--rules='));
|
|
975
|
+
const rulesLang = rulesArg?.includes('=') ? rulesArg.split('=')[1] : null;
|
|
976
|
+
const targetArg = (args.find(a => a.startsWith('--target='))?.split('=')[1] ?? 'claude').toLowerCase();
|
|
977
|
+
const toClaude = targetArg === 'claude' || targetArg === 'all';
|
|
978
|
+
const toCursor = targetArg === 'cursor' || targetArg === 'all';
|
|
979
|
+
const toWindsurf = targetArg === 'windsurf' || targetArg === 'all';
|
|
980
|
+
const toOpencode = targetArg === 'opencode' || targetArg === 'all';
|
|
981
|
+
const toKiro = targetArg === 'kiro' || targetArg === 'all';
|
|
982
|
+
const toCopilot = targetArg === 'copilot' || targetArg === 'all';
|
|
983
|
+
const toGemini = targetArg === 'gemini' || targetArg === 'all';
|
|
984
|
+
const skillName = args.find(a => !a.startsWith('--') && a !== 'add');
|
|
985
|
+
|
|
986
|
+
const activeTargets = () => [
|
|
987
|
+
toClaude && '.claude',
|
|
988
|
+
toCursor && '.cursor/rules',
|
|
989
|
+
toWindsurf && '.windsurf/rules',
|
|
990
|
+
toOpencode && '.opencode/instructions',
|
|
991
|
+
toKiro && '.kiro/skills',
|
|
992
|
+
toCopilot && '.github/copilot-instructions.md',
|
|
993
|
+
toGemini && 'GEMINI.md',
|
|
994
|
+
].filter(Boolean).join(' + ');
|
|
995
|
+
|
|
996
|
+
const installSkills = (list) => {
|
|
997
|
+
if (toClaude) list.forEach(s => copySkill(s, targetDir));
|
|
998
|
+
if (toCursor) list.forEach(s => copySkillToCursor(s));
|
|
999
|
+
if (toWindsurf) list.forEach(s => copySkillToWindsurf(s));
|
|
1000
|
+
if (toOpencode) list.forEach(s => copySkillToOpencode(s));
|
|
1001
|
+
if (toKiro) list.forEach(s => copySkillToKiro(s));
|
|
1002
|
+
if (toCopilot) list.forEach(s => bundleSkillToCopilot(s));
|
|
1003
|
+
if (toGemini) list.forEach(s => bundleSkillToGemini(s));
|
|
1004
|
+
if (toClaude && !noCommands) list.forEach(s => copyCommand(s));
|
|
1005
|
+
};
|
|
1006
|
+
const installAgents = (list) => {
|
|
1007
|
+
if (toClaude && !noAgents) list.forEach(a => copyAgent(a));
|
|
1008
|
+
// agents not applicable to non-Claude tools
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
if (profileArg) {
|
|
1012
|
+
const profile = PROFILES[profileArg];
|
|
1013
|
+
if (!profile) {
|
|
1014
|
+
console.error(c.red(`✗ Profile "${profileArg}" not found.`) + ' Run ' + c.cyan('skills profiles') + ' to see available profiles.');
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
}
|
|
1017
|
+
installSkills(profile.skills);
|
|
1018
|
+
installAgents(profile.agents);
|
|
1019
|
+
const agentStr = (!noAgents && toClaude && profile.agents.length)
|
|
1020
|
+
? `, ${profile.agents.length} agent${profile.agents.length > 1 ? 's' : ''}`
|
|
1021
|
+
: '';
|
|
1022
|
+
console.log(c.dim(`\nInstalled profile "${profileArg}": ${profile.skills.length} skills${agentStr} → ${activeTargets()}`));
|
|
1023
|
+
} else if (agentArg) {
|
|
1024
|
+
const agents = getAvailableAgents();
|
|
1025
|
+
if (!agents.includes(agentArg)) {
|
|
1026
|
+
console.error(c.red(`✗ Agent "${agentArg}" not found.`) + ' Available: ' + c.dim(agents.join(', ')));
|
|
1027
|
+
process.exit(1);
|
|
1028
|
+
}
|
|
1029
|
+
copyAgent(agentArg);
|
|
1030
|
+
console.log(c.dim(`\nInstalled to ${agentsTargetDir}`));
|
|
1031
|
+
} else if (addAll) {
|
|
1032
|
+
const skills = getAvailableSkills();
|
|
1033
|
+
const agents = getAvailableAgents();
|
|
1034
|
+
installSkills(skills);
|
|
1035
|
+
installAgents(agents);
|
|
1036
|
+
if (toClaude) { copyHooks(); copyAllRules(); }
|
|
1037
|
+
const agentCount = (!noAgents && toClaude) ? agents.length : 0;
|
|
1038
|
+
console.log(c.dim(`\nInstalled ${skills.length} skills, ${agentCount} agents, ${getAvailableRules().length} rules → ${activeTargets()}`));
|
|
1039
|
+
} else if (addHooks) {
|
|
1040
|
+
if (toClaude) copyHooks();
|
|
1041
|
+
else console.log(c.yellow(' --hooks only applies to --target=claude.'));
|
|
1042
|
+
break;
|
|
1043
|
+
} else if (rulesArg) {
|
|
1044
|
+
if (!toClaude) {
|
|
1045
|
+
console.log(c.yellow(' --rules only applies to --target=claude (.claude/rules/).'));
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
if (rulesLang) copyRules(rulesLang);
|
|
1049
|
+
else copyAllRules();
|
|
1050
|
+
console.log(c.dim(`\nInstalled rules → ${rulesTargetDir}`));
|
|
1051
|
+
} else if (skillName) {
|
|
1052
|
+
installSkills([skillName]);
|
|
1053
|
+
console.log(c.dim(`\nInstalled to ${activeTargets()}`));
|
|
1054
|
+
} else {
|
|
1055
|
+
console.error(c.red('Usage: skills add <skill-name> | skills add --all | skills add --agent=<name>'));
|
|
1056
|
+
process.exit(1);
|
|
1057
|
+
}
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
case 'check': {
|
|
1062
|
+
const checkAll = args.includes('--all');
|
|
1063
|
+
const skillName = args.find(a => !a.startsWith('--') && a !== 'check');
|
|
1064
|
+
|
|
1065
|
+
if (checkAll) {
|
|
1066
|
+
const skills = getAvailableSkills();
|
|
1067
|
+
const summary = [];
|
|
1068
|
+
for (const s of skills) {
|
|
1069
|
+
const checks = checkSkill(s);
|
|
1070
|
+
const badge = earnedBadge(checks);
|
|
1071
|
+
const pass = checks.filter(r => r.ok).length;
|
|
1072
|
+
const total = checks.length;
|
|
1073
|
+
const icon = badge ? BADGE[badge] : c.red('no badge');
|
|
1074
|
+
summary.push({ name: s, badge, pass, total, icon });
|
|
1075
|
+
}
|
|
1076
|
+
console.log('');
|
|
1077
|
+
console.log(c.bold(' Quality summary'));
|
|
1078
|
+
console.log(' ' + c.line(60));
|
|
1079
|
+
const nameW = Math.max(...summary.map(s => s.name.length)) + 2;
|
|
1080
|
+
for (const s of summary) {
|
|
1081
|
+
const bar = `${s.pass}/${s.total}`.padStart(5);
|
|
1082
|
+
const failures = s.pass < s.total ? c.dim(` (${s.total - s.pass} issues)`) : '';
|
|
1083
|
+
console.log(` ${s.name.padEnd(nameW)}${s.icon} ${c.dim(bar)}${failures}`);
|
|
1084
|
+
}
|
|
1085
|
+
const gold = summary.filter(s => ['gold', 'platinum'].includes(s.badge)).length;
|
|
1086
|
+
const belowGold = summary.filter(s => !['gold', 'platinum'].includes(s.badge));
|
|
1087
|
+
console.log(' ' + c.line(60));
|
|
1088
|
+
console.log(c.dim(`\n ${gold}/${skills.length} skills at Gold or above\n`));
|
|
1089
|
+
if (belowGold.length) {
|
|
1090
|
+
console.error(c.red(` ✗ ${belowGold.length} skill(s) below Gold: ${belowGold.map(s => s.name).join(', ')}\n`));
|
|
1091
|
+
process.exit(1);
|
|
1092
|
+
}
|
|
1093
|
+
} else if (skillName) {
|
|
1094
|
+
const checks = checkSkill(skillName);
|
|
1095
|
+
printCheckResults(skillName, checks);
|
|
1096
|
+
const badge = earnedBadge(checks);
|
|
1097
|
+
process.exit(badge ? 0 : 1);
|
|
1098
|
+
} else {
|
|
1099
|
+
console.error(c.red('Usage: skills check <skill-name> | skills check --all'));
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
}
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
case 'eval': {
|
|
1106
|
+
const skillName = args.find(a => !a.startsWith('--') && a !== 'eval');
|
|
1107
|
+
const modelArg = args.find(a => a.startsWith('--model='))?.split('=')[1];
|
|
1108
|
+
const idArg = args.find(a => a.startsWith('--id='))?.split('=')[1];
|
|
1109
|
+
|
|
1110
|
+
if (!skillName) {
|
|
1111
|
+
console.error(c.red('Usage: skills eval <skill-name> [--model=<model>] [--id=<eval-id>]'));
|
|
1112
|
+
process.exit(1);
|
|
1113
|
+
}
|
|
1114
|
+
const skills = getAvailableSkills();
|
|
1115
|
+
if (!skills.includes(skillName)) {
|
|
1116
|
+
console.error(c.red(`✗ "${skillName}" not found.`) + ' Run ' + c.cyan('skills list') + ' to see available skills.');
|
|
1117
|
+
process.exit(1);
|
|
1118
|
+
}
|
|
1119
|
+
await runEvals(skillName, { model: modelArg, id: idArg });
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
case 'update-readme': {
|
|
1124
|
+
const skills = getAvailableSkills();
|
|
1125
|
+
const rows = skills.map(skillName => {
|
|
1126
|
+
const resultsPath = path.join(skillsRoot, skillName, 'evals', 'results.json');
|
|
1127
|
+
if (!fs.existsSync(resultsPath)) return `| ${skillName} | — | — | — | — | — |`;
|
|
1128
|
+
try {
|
|
1129
|
+
const r = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
|
|
1130
|
+
const pct = Math.round((r.pass_rate || 0) * 100) + '%';
|
|
1131
|
+
const basePct = r.baseline_pass_rate !== undefined ? Math.round(r.baseline_pass_rate * 100) + '%' : '—';
|
|
1132
|
+
const delta = r.delta !== undefined ? `+${Math.round(r.delta * 100)}pp` : '—';
|
|
1133
|
+
return `| ${skillName} | ${pct} | ${basePct} | ${delta} | ${r.evals_run ?? '—'} | ${r.date ?? '—'} |`;
|
|
1134
|
+
} catch { return `| ${skillName} | — | — | — | — | — |`; }
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
const tableHeader = '| Skill | Pass Rate | Baseline | Delta | Evals | Last Run |\n|-------|-----------|----------|-------|-------|----------|';
|
|
1138
|
+
const newTable = `<!-- quality-table-start -->\n${tableHeader}\n${rows.join('\n')}\n<!-- quality-table-end -->`;
|
|
1139
|
+
|
|
1140
|
+
const readmePath = path.join(__dirname, '..', 'README.md');
|
|
1141
|
+
let readme = fs.readFileSync(readmePath, 'utf8');
|
|
1142
|
+
readme = readme.replace(/<!-- quality-table-start -->[\s\S]*?<!-- quality-table-end -->/, newTable);
|
|
1143
|
+
fs.writeFileSync(readmePath, readme);
|
|
1144
|
+
|
|
1145
|
+
const missing = skills.filter(s => !fs.existsSync(path.join(skillsRoot, s, 'evals', 'results.json')));
|
|
1146
|
+
const nonStd = skills.filter(s => {
|
|
1147
|
+
try { return JSON.parse(fs.readFileSync(path.join(skillsRoot, s, 'evals', 'results.json'), 'utf8')).non_standard_provider; }
|
|
1148
|
+
catch { return false; }
|
|
1149
|
+
});
|
|
1150
|
+
console.log('');
|
|
1151
|
+
console.log(c.green('✓') + ` README.md quality table updated (${skills.length} skills)`);
|
|
1152
|
+
if (missing.length) console.log(c.dim(` ${missing.length} pending: ${missing.join(', ')}`));
|
|
1153
|
+
if (nonStd.length) console.log(c.yellow(` ⚠ ${nonStd.length} non-standard provider: ${nonStd.join(', ')}`));
|
|
1154
|
+
console.log('');
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
case 'agents': {
|
|
1159
|
+
const infoArg = args.find(a => a.startsWith('--info='))?.split('=')[1]
|
|
1160
|
+
|| args.find(a => !a.startsWith('--') && a !== 'agents');
|
|
1161
|
+
const available = getAvailableAgents();
|
|
1162
|
+
|
|
1163
|
+
if (infoArg) {
|
|
1164
|
+
if (!available.includes(infoArg)) {
|
|
1165
|
+
console.error(c.red(`✗ Agent "${infoArg}" not found.`) + ' Run ' + c.cyan('skills agents') + ' to see available agents.');
|
|
1166
|
+
process.exit(1);
|
|
1167
|
+
}
|
|
1168
|
+
const { description, model } = parseAgentFrontmatter(infoArg);
|
|
1169
|
+
console.log('');
|
|
1170
|
+
console.log(c.bold(` ${infoArg}`) + c.dim(model ? ` [${model}]` : ''));
|
|
1171
|
+
console.log(' ' + c.line(55));
|
|
1172
|
+
console.log(' ' + description);
|
|
1173
|
+
console.log('');
|
|
1174
|
+
console.log(c.dim(` Install: skills add --agent=${infoArg}`));
|
|
1175
|
+
console.log('');
|
|
1176
|
+
} else {
|
|
1177
|
+
const nameW = Math.max(...available.map(n => n.length)) + 2;
|
|
1178
|
+
console.log('');
|
|
1179
|
+
console.log(c.bold(` @booklib/skills — agents`) + c.dim(` (${available.length})`));
|
|
1180
|
+
console.log(' ' + c.line(60));
|
|
1181
|
+
for (const name of available) {
|
|
1182
|
+
const { description, model } = parseAgentFrontmatter(name);
|
|
1183
|
+
const modelTag = model ? c.dim(` [${model}]`) : '';
|
|
1184
|
+
console.log(` ${c.cyan(name.padEnd(nameW))}${modelTag}`);
|
|
1185
|
+
if (description) console.log(` ${' '.repeat(nameW)}${firstSentence(description, 72)}`);
|
|
1186
|
+
}
|
|
1187
|
+
console.log('');
|
|
1188
|
+
console.log(c.dim(` skills add --agent=<name> install one agent`));
|
|
1189
|
+
console.log(c.dim(` skills agents --info=<name> full description`));
|
|
1190
|
+
console.log('');
|
|
1191
|
+
}
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
case 'rules': {
|
|
1196
|
+
const available = getAvailableRules();
|
|
1197
|
+
if (!available.length) {
|
|
1198
|
+
console.log(c.yellow(' No rules found.'));
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
// Group by language
|
|
1202
|
+
const byLang = {};
|
|
1203
|
+
for (const r of available) {
|
|
1204
|
+
if (!byLang[r.language]) byLang[r.language] = [];
|
|
1205
|
+
byLang[r.language].push(r);
|
|
1206
|
+
}
|
|
1207
|
+
console.log('');
|
|
1208
|
+
console.log(c.bold(' @booklib/skills — rules') + c.dim(` (${available.length} always-on)`));
|
|
1209
|
+
console.log(' ' + c.line(60));
|
|
1210
|
+
for (const [lang, rules] of Object.entries(byLang)) {
|
|
1211
|
+
console.log(` ${c.bold(lang)}`);
|
|
1212
|
+
for (const r of rules) {
|
|
1213
|
+
const content = fs.readFileSync(r.file, 'utf8');
|
|
1214
|
+
const descMatch = content.match(/^description:\s*(.+)$/m);
|
|
1215
|
+
const desc = descMatch ? descMatch[1].trim().replace(/^>$/, '') : '';
|
|
1216
|
+
console.log(` ${c.cyan(r.name.padEnd(28))}${c.dim(firstSentence(desc, 55))}`);
|
|
1217
|
+
}
|
|
1218
|
+
console.log('');
|
|
1219
|
+
}
|
|
1220
|
+
console.log(c.dim(` skills add --rules install all rules → .claude/rules/`));
|
|
1221
|
+
console.log(c.dim(` skills add --rules=<language> install rules for one language`));
|
|
1222
|
+
console.log('');
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
case 'profiles': {
|
|
1227
|
+
const nameW = Math.max(...Object.keys(PROFILES).map(k => k.length)) + 2;
|
|
1228
|
+
console.log('');
|
|
1229
|
+
console.log(c.bold(' Installation profiles'));
|
|
1230
|
+
console.log(' ' + c.line(60));
|
|
1231
|
+
for (const [name, profile] of Object.entries(PROFILES)) {
|
|
1232
|
+
const skillCount = `${profile.skills.length} skill${profile.skills.length !== 1 ? 's' : ''}`;
|
|
1233
|
+
const agentPart = profile.agents.length ? ` + ${profile.agents.length} agent` : '';
|
|
1234
|
+
console.log(` ${c.cyan(name.padEnd(nameW))}${c.dim(skillCount + agentPart)}`);
|
|
1235
|
+
console.log(` ${' '.repeat(nameW)}${profile.description}`);
|
|
1236
|
+
console.log('');
|
|
1237
|
+
}
|
|
1238
|
+
console.log(c.dim(` Install: ${c.cyan('skills add --profile=<name>')}`));
|
|
1239
|
+
console.log('');
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
default:
|
|
1244
|
+
console.log(`
|
|
1245
|
+
${c.bold(' @booklib/skills')} — book knowledge distilled into AI agent skills
|
|
1246
|
+
|
|
1247
|
+
${c.bold(' Usage:')}
|
|
1248
|
+
${c.cyan('skills list')} list all available skills
|
|
1249
|
+
${c.cyan('skills agents')} list all available agents
|
|
1250
|
+
${c.cyan('skills agents')} ${c.dim('--info=<name>')} full description of an agent
|
|
1251
|
+
${c.cyan('skills profiles')} list available profiles
|
|
1252
|
+
${c.cyan('skills rules')} list always-on rule files
|
|
1253
|
+
${c.cyan('skills info')} ${c.dim('<name>')} full description of a skill
|
|
1254
|
+
${c.cyan('skills demo')} ${c.dim('<name>')} before/after example
|
|
1255
|
+
${c.cyan('skills add')} ${c.dim('--profile=<name>')} install a profile (skills + commands + agent)
|
|
1256
|
+
${c.cyan('skills add')} ${c.dim('<name>')} install a single skill + /command
|
|
1257
|
+
${c.cyan('skills add --all')} install everything (skills + agents + rules + hooks)
|
|
1258
|
+
${c.cyan('skills add')} ${c.dim('<name> --global')} install globally (~/.claude/)
|
|
1259
|
+
${c.cyan('skills add')} ${c.dim('--agent=<name>')} install a single agent to .claude/agents/
|
|
1260
|
+
${c.cyan('skills add --rules')} install always-on rules to .claude/rules/
|
|
1261
|
+
${c.cyan('skills add')} ${c.dim('--rules=<language>')} install rules for one language
|
|
1262
|
+
${c.cyan('skills add --hooks')} install the UserPromptSubmit suggestion hook
|
|
1263
|
+
${c.cyan('skills add')} ${c.dim('--target=cursor')} install to .cursor/rules/ (Cursor)
|
|
1264
|
+
${c.cyan('skills add')} ${c.dim('--target=windsurf')} install to .windsurf/rules/ (Windsurf)
|
|
1265
|
+
${c.cyan('skills add')} ${c.dim('--target=opencode')} install to .opencode/instructions/ (OpenCode)
|
|
1266
|
+
${c.cyan('skills add')} ${c.dim('--target=kiro')} install to .kiro/skills/ (Kiro)
|
|
1267
|
+
${c.cyan('skills add')} ${c.dim('--target=copilot')} bundle into .github/copilot-instructions.md
|
|
1268
|
+
${c.cyan('skills add')} ${c.dim('--target=gemini')} bundle into GEMINI.md (Gemini CLI)
|
|
1269
|
+
${c.cyan('skills add')} ${c.dim('--target=all')} install to all supported tools
|
|
1270
|
+
${c.cyan('skills add')} ${c.dim('--no-commands')} skip /command installation
|
|
1271
|
+
${c.cyan('skills add')} ${c.dim('--no-agents')} skip agent installation
|
|
1272
|
+
${c.cyan('skills check')} ${c.dim('<name>')} quality check (Bronze/Silver/Gold/Platinum)
|
|
1273
|
+
${c.cyan('skills check --all')} quality summary for all skills
|
|
1274
|
+
${c.cyan('skills update-readme')} refresh README quality table from results.json files
|
|
1275
|
+
${c.cyan('skills eval')} ${c.dim('<name>')} run evals (auto-detects provider)
|
|
1276
|
+
${c.cyan('skills eval')} ${c.dim('<name> --model=<id>')} use a specific model
|
|
1277
|
+
${c.cyan('skills eval')} ${c.dim('<name> --id=<eval-id>')} run a single eval
|
|
1278
|
+
|
|
1279
|
+
${c.bold('Provider auto-detection (first match wins):')}
|
|
1280
|
+
ANTHROPIC_API_KEY Anthropic API (default model: claude-haiku-4-5-20251001)
|
|
1281
|
+
OPENAI_API_KEY OpenAI API (default model: gpt-4o-mini)
|
|
1282
|
+
EVAL_API_KEY+EVAL_BASE_URL any OpenAI-compatible endpoint (Groq, Together, etc.)
|
|
1283
|
+
ollama installed local Ollama (requires --model or EVAL_MODEL)
|
|
1284
|
+
claude CLI installed Claude Code subscription — no key needed
|
|
1285
|
+
`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
main().catch(err => {
|
|
1290
|
+
console.error(c.red('Error: ') + err.message);
|
|
1291
|
+
process.exit(1);
|
|
1292
|
+
});
|