@booklib/skills 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CONTRIBUTING.md +122 -0
  2. package/README.md +20 -2
  3. package/ROADMAP.md +36 -0
  4. package/animation-at-work/evals/evals.json +44 -0
  5. package/animation-at-work/examples/after.md +64 -0
  6. package/animation-at-work/examples/before.md +35 -0
  7. package/animation-at-work/scripts/audit_animations.py +295 -0
  8. package/bin/skills.js +552 -42
  9. package/clean-code-reviewer/SKILL.md +109 -1
  10. package/clean-code-reviewer/evals/evals.json +121 -3
  11. package/clean-code-reviewer/examples/after.md +48 -0
  12. package/clean-code-reviewer/examples/before.md +33 -0
  13. package/clean-code-reviewer/references/api_reference.md +158 -0
  14. package/clean-code-reviewer/references/practices-catalog.md +282 -0
  15. package/clean-code-reviewer/references/review-checklist.md +254 -0
  16. package/clean-code-reviewer/scripts/pre-review.py +206 -0
  17. package/data-intensive-patterns/evals/evals.json +43 -0
  18. package/data-intensive-patterns/examples/after.md +61 -0
  19. package/data-intensive-patterns/examples/before.md +38 -0
  20. package/data-intensive-patterns/scripts/adr.py +213 -0
  21. package/data-pipelines/evals/evals.json +45 -0
  22. package/data-pipelines/examples/after.md +97 -0
  23. package/data-pipelines/examples/before.md +37 -0
  24. package/data-pipelines/scripts/new_pipeline.py +444 -0
  25. package/design-patterns/evals/evals.json +46 -0
  26. package/design-patterns/examples/after.md +52 -0
  27. package/design-patterns/examples/before.md +29 -0
  28. package/design-patterns/scripts/scaffold.py +807 -0
  29. package/domain-driven-design/SKILL.md +120 -0
  30. package/domain-driven-design/evals/evals.json +48 -0
  31. package/domain-driven-design/examples/after.md +80 -0
  32. package/domain-driven-design/examples/before.md +43 -0
  33. package/domain-driven-design/scripts/scaffold.py +421 -0
  34. package/effective-java/evals/evals.json +46 -0
  35. package/effective-java/examples/after.md +83 -0
  36. package/effective-java/examples/before.md +37 -0
  37. package/effective-java/scripts/checkstyle_setup.py +211 -0
  38. package/effective-kotlin/evals/evals.json +45 -0
  39. package/effective-kotlin/examples/after.md +36 -0
  40. package/effective-kotlin/examples/before.md +38 -0
  41. package/effective-python/evals/evals.json +44 -0
  42. package/effective-python/examples/after.md +56 -0
  43. package/effective-python/examples/before.md +40 -0
  44. package/effective-python/references/api_reference.md +218 -0
  45. package/effective-python/references/practices-catalog.md +483 -0
  46. package/effective-python/references/review-checklist.md +190 -0
  47. package/effective-python/scripts/lint.py +173 -0
  48. package/kotlin-in-action/evals/evals.json +43 -0
  49. package/kotlin-in-action/examples/after.md +53 -0
  50. package/kotlin-in-action/examples/before.md +39 -0
  51. package/kotlin-in-action/scripts/setup_detekt.py +224 -0
  52. package/lean-startup/evals/evals.json +43 -0
  53. package/lean-startup/examples/after.md +80 -0
  54. package/lean-startup/examples/before.md +34 -0
  55. package/lean-startup/scripts/new_experiment.py +286 -0
  56. package/microservices-patterns/SKILL.md +140 -0
  57. package/microservices-patterns/evals/evals.json +45 -0
  58. package/microservices-patterns/examples/after.md +69 -0
  59. package/microservices-patterns/examples/before.md +40 -0
  60. package/microservices-patterns/scripts/new_service.py +583 -0
  61. package/package.json +2 -8
  62. package/refactoring-ui/evals/evals.json +45 -0
  63. package/refactoring-ui/examples/after.md +85 -0
  64. package/refactoring-ui/examples/before.md +58 -0
  65. package/refactoring-ui/scripts/audit_css.py +250 -0
  66. package/skill-router/SKILL.md +142 -0
  67. package/skill-router/evals/evals.json +38 -0
  68. package/skill-router/examples/after.md +63 -0
  69. package/skill-router/examples/before.md +39 -0
  70. package/skill-router/references/api_reference.md +24 -0
  71. package/skill-router/references/routing-heuristics.md +89 -0
  72. package/skill-router/references/skill-catalog.md +156 -0
  73. package/skill-router/scripts/route.py +266 -0
  74. package/storytelling-with-data/evals/evals.json +47 -0
  75. package/storytelling-with-data/examples/after.md +50 -0
  76. package/storytelling-with-data/examples/before.md +33 -0
  77. package/storytelling-with-data/scripts/chart_review.py +301 -0
  78. package/system-design-interview/evals/evals.json +45 -0
  79. package/system-design-interview/examples/after.md +94 -0
  80. package/system-design-interview/examples/before.md +27 -0
  81. package/system-design-interview/scripts/new_design.py +421 -0
  82. package/using-asyncio-python/evals/evals.json +43 -0
  83. package/using-asyncio-python/examples/after.md +68 -0
  84. package/using-asyncio-python/examples/before.md +39 -0
  85. package/using-asyncio-python/scripts/check_blocking.py +270 -0
  86. package/web-scraping-python/evals/evals.json +46 -0
  87. package/web-scraping-python/examples/after.md +109 -0
  88. package/web-scraping-python/examples/before.md +40 -0
  89. package/web-scraping-python/scripts/new_scraper.py +231 -0
  90. /package/{effective-python-skill → effective-python}/SKILL.md +0 -0
  91. /package/{effective-python-skill → effective-python}/ref-01-pythonic-thinking.md +0 -0
  92. /package/{effective-python-skill → effective-python}/ref-02-lists-and-dicts.md +0 -0
  93. /package/{effective-python-skill → effective-python}/ref-03-functions.md +0 -0
  94. /package/{effective-python-skill → effective-python}/ref-04-comprehensions-generators.md +0 -0
  95. /package/{effective-python-skill → effective-python}/ref-05-classes-interfaces.md +0 -0
  96. /package/{effective-python-skill → effective-python}/ref-06-metaclasses-attributes.md +0 -0
  97. /package/{effective-python-skill → effective-python}/ref-07-concurrency.md +0 -0
  98. /package/{effective-python-skill → effective-python}/ref-08-robustness-performance.md +0 -0
  99. /package/{effective-python-skill → effective-python}/ref-09-testing-debugging.md +0 -0
  100. /package/{effective-python-skill → effective-python}/ref-10-collaboration.md +0 -0
package/bin/skills.js CHANGED
@@ -3,83 +3,593 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
+ const https = require('https');
6
7
 
7
8
  const args = process.argv.slice(2);
8
9
  const command = args[0];
9
10
  const skillsRoot = path.join(__dirname, '..');
10
11
 
12
+ // ─── ANSI helpers ─────────────────────────────────────────────────────────────
13
+ const c = {
14
+ bold: s => `\x1b[1m${s}\x1b[0m`,
15
+ dim: s => `\x1b[2m${s}\x1b[0m`,
16
+ green: s => `\x1b[32m${s}\x1b[0m`,
17
+ cyan: s => `\x1b[36m${s}\x1b[0m`,
18
+ yellow: s => `\x1b[33m${s}\x1b[0m`,
19
+ red: s => `\x1b[31m${s}\x1b[0m`,
20
+ blue: s => `\x1b[34m${s}\x1b[0m`,
21
+ line: (len = 60) => `\x1b[2m${'─'.repeat(len)}\x1b[0m`,
22
+ };
23
+
24
+ // ─── SKILL.md helpers ─────────────────────────────────────────────────────────
25
+ function parseSkillFrontmatter(skillName) {
26
+ const skillMdPath = path.join(skillsRoot, skillName, 'SKILL.md');
27
+ try {
28
+ const content = fs.readFileSync(skillMdPath, 'utf8');
29
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
30
+ if (!fmMatch) return { name: skillName, description: '' };
31
+ const fm = fmMatch[1];
32
+
33
+ const blockMatch = fm.match(/^description:\s*>\s*\n((?:[ \t]+.+\n?)+)/m);
34
+ const quotedMatch = fm.match(/^description:\s*["'](.+?)["']\s*$/m);
35
+ const plainMatch = fm.match(/^description:\s*(?!>)(.+)$/m);
36
+
37
+ let description = '';
38
+ if (blockMatch) description = blockMatch[1].split('\n').map(l => l.trim()).filter(Boolean).join(' ');
39
+ else if (quotedMatch) description = quotedMatch[1];
40
+ else if (plainMatch) description = plainMatch[1].trim();
41
+
42
+ return { name: skillName, description };
43
+ } catch {
44
+ return { name: skillName, description: '' };
45
+ }
46
+ }
47
+
48
+ function getSkillMdContent(skillName) {
49
+ return fs.readFileSync(path.join(skillsRoot, skillName, 'SKILL.md'), 'utf8');
50
+ }
51
+
11
52
  function getAvailableSkills() {
12
- return fs.readdirSync(skillsRoot).filter(name => {
13
- const skillPath = path.join(skillsRoot, name);
14
- return (
15
- fs.statSync(skillPath).isDirectory() &&
16
- fs.existsSync(path.join(skillPath, 'SKILL.md'))
17
- );
18
- });
53
+ return fs.readdirSync(skillsRoot)
54
+ .filter(name => {
55
+ const p = path.join(skillsRoot, name);
56
+ return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md'));
57
+ })
58
+ .sort();
19
59
  }
20
60
 
61
+ function firstSentence(text, maxLen = 65) {
62
+ const end = text.search(/[.!?](\s|$)/);
63
+ const s = end >= 0 ? text.slice(0, end + 1) : text;
64
+ return s.length <= maxLen ? s : s.slice(0, maxLen - 1) + '…';
65
+ }
66
+
67
+ // ─── File copy ────────────────────────────────────────────────────────────────
21
68
  function copyDir(src, dest) {
22
69
  fs.mkdirSync(dest, { recursive: true });
23
70
  for (const entry of fs.readdirSync(src)) {
24
71
  const srcPath = path.join(src, entry);
25
72
  const destPath = path.join(dest, entry);
26
- if (fs.statSync(srcPath).isDirectory()) {
27
- copyDir(srcPath, destPath);
28
- } else {
29
- fs.copyFileSync(srcPath, destPath);
30
- }
73
+ if (fs.statSync(srcPath).isDirectory()) copyDir(srcPath, destPath);
74
+ else fs.copyFileSync(srcPath, destPath);
31
75
  }
32
76
  }
33
77
 
34
78
  function copySkill(skillName, targetDir) {
35
79
  const src = path.join(skillsRoot, skillName);
36
80
  if (!fs.existsSync(src)) {
37
- console.error(`Skill "${skillName}" not found. Run "skills list" to see available skills.`);
81
+ console.error(c.red(`✗ Skill "${skillName}" not found.`) + ' Run ' + c.cyan('skills list') + ' to see available skills.');
38
82
  process.exit(1);
39
83
  }
40
84
  const dest = path.join(targetDir, skillName);
41
85
  copyDir(src, dest);
42
- console.log(`✓ ${skillName} → ${dest}`);
86
+ console.log(c.green('✓') + ` ${c.bold(skillName)} → ${c.dim(dest)}`);
43
87
  }
44
88
 
45
- const isGlobal = args.includes('--global');
89
+ const isGlobal = args.includes('--global');
46
90
  const targetDir = isGlobal
47
91
  ? path.join(os.homedir(), '.claude', 'skills')
48
92
  : path.join(process.cwd(), '.claude', 'skills');
49
93
 
50
- switch (command) {
51
- case 'list': {
52
- const skills = getAvailableSkills();
53
- console.log('Available skills:\n');
54
- skills.forEach(s => console.log(` ${s}`));
55
- break;
94
+ // ─── CHECK command ────────────────────────────────────────────────────────────
95
+ function checkSkill(skillName) {
96
+ const skillDir = path.join(skillsRoot, skillName);
97
+ if (!fs.existsSync(path.join(skillDir, 'SKILL.md'))) {
98
+ console.error(c.red(`✗ "${skillName}" not found or has no SKILL.md`));
99
+ process.exit(1);
56
100
  }
57
101
 
58
- case 'add': {
59
- const addAll = args.includes('--all');
60
- const skillName = args.find(a => !a.startsWith('--') && a !== 'add');
102
+ const pass = (tier, msg) => ({ ok: true, tier, msg });
103
+ const fail = (tier, msg) => ({ ok: false, tier, msg });
104
+ const checks = [];
61
105
 
62
- if (addAll) {
63
- const skills = getAvailableSkills();
64
- skills.forEach(s => copySkill(s, targetDir));
65
- console.log(`\nInstalled ${skills.length} skills to ${targetDir}`);
66
- } else if (skillName) {
67
- copySkill(skillName, targetDir);
68
- console.log(`\nInstalled to ${targetDir}`);
106
+ const skillMdContent = getSkillMdContent(skillName);
107
+ const lines = skillMdContent.split('\n');
108
+ const { name, description } = parseSkillFrontmatter(skillName);
109
+
110
+ // ── Bronze ──────────────────────────────────────────────────────────────────
111
+ checks.push(name === skillName
112
+ ? pass('bronze', `name matches folder (${name})`)
113
+ : fail('bronze', `name mismatch — SKILL.md: "${name}", folder: "${skillName}"`));
114
+
115
+ checks.push(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name)
116
+ ? pass('bronze', 'name format valid (lowercase, hyphens)')
117
+ : fail('bronze', `name format invalid — must be lowercase letters, numbers, hyphens; no consecutive hyphens`));
118
+
119
+ if (description.length < 50)
120
+ checks.push(fail('bronze', `description too short: ${description.length} chars (min 50)`));
121
+ else if (description.length > 1024)
122
+ checks.push(fail('bronze', `description too long: ${description.length} chars (max 1024)`));
123
+ else
124
+ checks.push(pass('bronze', `description: ${description.length} chars`));
125
+
126
+ checks.push(/trigger|use when|use for|when.*ask|when.*mention/i.test(description)
127
+ ? pass('bronze', 'description has trigger conditions')
128
+ : fail('bronze', 'description missing trigger conditions — add "use when…" or "trigger on…"'));
129
+
130
+ checks.push(lines.length <= 500
131
+ ? pass('bronze', `SKILL.md: ${lines.length} lines`)
132
+ : fail('bronze', `SKILL.md too long: ${lines.length} lines — move content to references/`));
133
+
134
+ const bodyStart = skillMdContent.indexOf('---', 3);
135
+ const body = bodyStart >= 0 ? skillMdContent.slice(bodyStart + 3).trim() : '';
136
+ checks.push(body.split('\n').length > 30
137
+ ? pass('bronze', `body present (${body.split('\n').length} lines of instructions)`)
138
+ : fail('bronze', 'body too thin — add actionable step-by-step instructions'));
139
+
140
+ // ── Silver ──────────────────────────────────────────────────────────────────
141
+ for (const [file, label] of [['before.md', 'examples/before.md'], ['after.md', 'examples/after.md']]) {
142
+ const p = path.join(skillDir, 'examples', file);
143
+ if (!fs.existsSync(p)) {
144
+ checks.push(fail('silver', `${label} missing`));
145
+ } else {
146
+ const n = fs.readFileSync(p, 'utf8').split('\n').length;
147
+ checks.push(n >= 10
148
+ ? pass('silver', `${label} (${n} lines)`)
149
+ : fail('silver', `${label} too short: ${n} lines (need 10+)`));
150
+ }
151
+ }
152
+
153
+ // ── Gold ────────────────────────────────────────────────────────────────────
154
+ const evalsPath = path.join(skillDir, 'evals', 'evals.json');
155
+ if (!fs.existsSync(evalsPath)) {
156
+ checks.push(fail('gold', 'evals/evals.json missing'));
157
+ checks.push(fail('gold', 'eval prompts not checked (no evals.json)'));
158
+ checks.push(fail('gold', 'eval expectations not checked (no evals.json)'));
159
+ } else {
160
+ let evals = [];
161
+ try {
162
+ evals = JSON.parse(fs.readFileSync(evalsPath, 'utf8')).evals || [];
163
+ } catch {
164
+ checks.push(fail('gold', 'evals/evals.json is invalid JSON'));
165
+ }
166
+ if (evals.length) {
167
+ checks.push(evals.length >= 3
168
+ ? pass('gold', `evals/evals.json: ${evals.length} evals`)
169
+ : fail('gold', `only ${evals.length} evals (need 3+)`));
170
+
171
+ const avgLines = evals.reduce((s, e) => s + (e.prompt || '').split('\n').length, 0) / evals.length;
172
+ checks.push(avgLines >= 8
173
+ ? pass('gold', `eval prompts have code (avg ${Math.round(avgLines)} lines)`)
174
+ : fail('gold', `eval prompts may lack real code (avg ${Math.round(avgLines)} lines, target 10+)`));
175
+
176
+ const avgExp = evals.reduce((s, e) => s + (e.expectations || []).length, 0) / evals.length;
177
+ checks.push(avgExp >= 5
178
+ ? pass('gold', `eval expectations thorough (avg ${(avgExp).toFixed(1)} per eval)`)
179
+ : fail('gold', `few expectations per eval (avg ${(avgExp).toFixed(1)}, target 5+)`));
180
+ }
181
+ }
182
+
183
+ const refsDir = path.join(skillDir, 'references');
184
+ if (!fs.existsSync(refsDir)) {
185
+ checks.push(fail('gold', 'references/ directory missing'));
186
+ } else {
187
+ const refFiles = fs.readdirSync(refsDir).filter(f => f.endsWith('.md'));
188
+ checks.push(refFiles.length >= 1
189
+ ? pass('gold', `references/ (${refFiles.length} files: ${refFiles.join(', ')})`)
190
+ : fail('gold', 'references/ has no .md files'));
191
+ }
192
+
193
+ // ── Platinum ────────────────────────────────────────────────────────────────
194
+ const scriptsDir = path.join(skillDir, 'scripts');
195
+ if (!fs.existsSync(scriptsDir)) {
196
+ checks.push(fail('platinum', 'scripts/ directory missing'));
197
+ } else {
198
+ const scriptFiles = fs.readdirSync(scriptsDir).filter(f => !f.startsWith('.'));
199
+ checks.push(scriptFiles.length >= 1
200
+ ? pass('platinum', `scripts/ (${scriptFiles.join(', ')})`)
201
+ : fail('platinum', 'scripts/ exists but is empty'));
202
+ }
203
+
204
+ return checks;
205
+ }
206
+
207
+ const TIERS = ['bronze', 'silver', 'gold', 'platinum'];
208
+ const BADGE = { bronze: '🥉 Bronze', silver: '🥈 Silver', gold: '🥇 Gold', platinum: '💎 Platinum' };
209
+ const LABEL = { bronze: 'Functional', silver: 'Complete', gold: 'Polished', platinum: 'Exemplary' };
210
+
211
+ function earnedBadge(checks) {
212
+ let badge = null;
213
+ for (const tier of TIERS) {
214
+ const tierChecks = checks.filter(r => r.tier === tier);
215
+ if (tierChecks.every(r => r.ok)) badge = tier;
216
+ else break;
217
+ }
218
+ return badge;
219
+ }
220
+
221
+ function printCheckResults(skillName, checks) {
222
+ console.log('');
223
+ console.log(c.bold(` ${skillName}`) + c.dim(' — quality check'));
224
+ console.log(' ' + c.line(55));
225
+
226
+ for (const tier of TIERS) {
227
+ const tierChecks = checks.filter(r => r.tier === tier);
228
+ console.log(`\n ${BADGE[tier]} — ${c.dim(LABEL[tier])}`);
229
+ for (const r of tierChecks) {
230
+ const icon = r.ok ? c.green('✓') : c.red('✗');
231
+ console.log(` ${icon} ${r.ok ? r.msg : c.red(r.msg)}`);
232
+ }
233
+ }
234
+
235
+ const badge = earnedBadge(checks);
236
+ console.log('');
237
+ console.log(` Result: ${badge ? BADGE[badge] : c.dim('No badge — fix Bronze issues first')}`);
238
+ console.log('');
239
+ return badge;
240
+ }
241
+
242
+ // ─── EVAL command ─────────────────────────────────────────────────────────────
243
+ function callClaude(systemPrompt, userMessage, model) {
244
+ const apiKey = process.env.ANTHROPIC_API_KEY;
245
+ if (!apiKey) throw new Error('ANTHROPIC_API_KEY environment variable not set');
246
+
247
+ const body = JSON.stringify({
248
+ model,
249
+ max_tokens: 4096,
250
+ system: systemPrompt,
251
+ messages: [{ role: 'user', content: userMessage }],
252
+ });
253
+
254
+ return new Promise((resolve, reject) => {
255
+ const req = https.request({
256
+ hostname: 'api.anthropic.com',
257
+ path: '/v1/messages',
258
+ method: 'POST',
259
+ headers: {
260
+ 'Content-Type': 'application/json',
261
+ 'x-api-key': apiKey,
262
+ 'anthropic-version': '2023-06-01',
263
+ 'Content-Length': Buffer.byteLength(body),
264
+ },
265
+ }, res => {
266
+ let data = '';
267
+ res.on('data', chunk => data += chunk);
268
+ res.on('end', () => {
269
+ try {
270
+ const parsed = JSON.parse(data);
271
+ if (parsed.error) reject(new Error(parsed.error.message));
272
+ else resolve(parsed.content?.[0]?.text ?? '');
273
+ } catch (e) { reject(e); }
274
+ });
275
+ });
276
+ req.on('error', reject);
277
+ req.write(body);
278
+ req.end();
279
+ });
280
+ }
281
+
282
+ function judgeResponse(response, expectations, model) {
283
+ const numbered = expectations.map((e, i) => `${i + 1}. ${e}`).join('\n');
284
+ const judgeSystem = `You are an eval judge. For each numbered expectation, respond with exactly:
285
+ <n>. PASS — <brief one-line reason>
286
+ or
287
+ <n>. FAIL — <brief one-line reason>
288
+ Output only the numbered lines. No other text.`;
289
+
290
+ const judgePrompt = `=== Response to evaluate ===
291
+ ${response}
292
+
293
+ === Expectations ===
294
+ ${numbered}`;
295
+
296
+ return callClaude(judgeSystem, judgePrompt, model);
297
+ }
298
+
299
+ function parseJudgement(judgement, count) {
300
+ const results = [];
301
+ for (let i = 1; i <= count; i++) {
302
+ const match = judgement.match(new RegExp(`${i}\\.\\s*(PASS|FAIL)\\s*[—\\-–]?\\s*(.+)`, 'i'));
303
+ if (match) {
304
+ results.push({ ok: match[1].toUpperCase() === 'PASS', reason: match[2].trim() });
69
305
  } else {
70
- console.error('Usage: skills add <skill-name> | skills add --all');
71
- process.exit(1);
306
+ results.push({ ok: false, reason: 'judge did not return a result for this expectation' });
307
+ }
308
+ }
309
+ return results;
310
+ }
311
+
312
+ async function runEvals(skillName, opts = {}) {
313
+ const skillDir = path.join(skillsRoot, skillName);
314
+ const evalsPath = path.join(skillDir, 'evals', 'evals.json');
315
+ const model = opts.model || 'claude-haiku-4-5-20251001';
316
+ const judgeModel = opts.judgeModel || 'claude-haiku-4-5-20251001';
317
+ const filterId = opts.id || null;
318
+
319
+ if (!fs.existsSync(evalsPath)) {
320
+ console.error(c.red(`✗ No evals/evals.json found for "${skillName}"`));
321
+ process.exit(1);
322
+ }
323
+
324
+ let evals;
325
+ try {
326
+ evals = JSON.parse(fs.readFileSync(evalsPath, 'utf8')).evals || [];
327
+ } catch {
328
+ console.error(c.red('✗ evals/evals.json is invalid JSON'));
329
+ process.exit(1);
330
+ }
331
+
332
+ if (filterId) evals = evals.filter(e => e.id === filterId);
333
+ if (!evals.length) {
334
+ console.error(c.red(`✗ No evals found${filterId ? ` matching --id ${filterId}` : ''}`));
335
+ process.exit(1);
336
+ }
337
+
338
+ const skillMd = getSkillMdContent(skillName);
339
+
340
+ console.log('');
341
+ console.log(c.bold(` ${skillName}`) + c.dim(` — evals (${evals.length})`));
342
+ console.log(' ' + c.line(55));
343
+ console.log(c.dim(` model: ${model} judge: ${judgeModel}\n`));
344
+
345
+ let totalPass = 0, totalFail = 0, evalsFullyPassed = 0;
346
+
347
+ for (const ev of evals) {
348
+ const promptLines = (ev.prompt || '').split('\n').length;
349
+ const expectations = ev.expectations || [];
350
+
351
+ process.stdout.write(` ${c.cyan('●')} ${c.bold(ev.id)}\n`);
352
+ process.stdout.write(c.dim(` prompt: ${promptLines} lines — calling ${model}...`));
353
+
354
+ let response;
355
+ try {
356
+ response = await callClaude(skillMd, ev.prompt, model);
357
+ process.stdout.write(c.green(' done\n'));
358
+ } catch (e) {
359
+ process.stdout.write(c.red(` failed: ${e.message}\n`));
360
+ totalFail += expectations.length;
361
+ continue;
362
+ }
363
+
364
+ process.stdout.write(c.dim(` judging ${expectations.length} expectations...`));
365
+
366
+ let judgement;
367
+ try {
368
+ judgement = await judgeResponse(response, expectations, judgeModel);
369
+ process.stdout.write(c.dim(' done\n'));
370
+ } catch (e) {
371
+ process.stdout.write(c.red(` judge failed: ${e.message}\n`));
372
+ totalFail += expectations.length;
373
+ continue;
374
+ }
375
+
376
+ const results = parseJudgement(judgement, expectations.length);
377
+ let evalPass = 0;
378
+
379
+ for (let i = 0; i < expectations.length; i++) {
380
+ const r = results[i];
381
+ const icon = r.ok ? c.green('✓') : c.red('✗');
382
+ const exp = expectations[i].length > 80 ? expectations[i].slice(0, 79) + '…' : expectations[i];
383
+ console.log(` ${icon} ${exp}`);
384
+ if (!r.ok) console.log(c.dim(` → ${r.reason}`));
385
+ if (r.ok) { evalPass++; totalPass++; } else { totalFail++; }
72
386
  }
73
- break;
387
+
388
+ const evalTotal = expectations.length;
389
+ const allPassed = evalPass === evalTotal;
390
+ if (allPassed) evalsFullyPassed++;
391
+ console.log(c.dim(` ${evalPass}/${evalTotal} expectations passed`) + (allPassed ? ' ' + c.green('✓') : '') + '\n');
74
392
  }
75
393
 
76
- default:
77
- console.log(`
78
- Usage:
79
- skills list List all available skills
80
- skills add <name> Add a skill to .claude/skills/ in current project
81
- skills add --all Add all skills to current project
82
- skills add <name> --global Add a skill to ~/.claude/skills/ (global)
83
- skills add --all --global Add all skills globally
394
+ const total = totalPass + totalFail;
395
+ const pct = total > 0 ? Math.round((totalPass / total) * 100) : 0;
396
+ const color = pct >= 80 ? c.green : pct >= 60 ? c.yellow : c.red;
397
+
398
+ console.log(' ' + c.line(55));
399
+ console.log(` ${color(`${pct}%`)} ${evalsFullyPassed}/${evals.length} evals fully passed, ${totalPass}/${total} expectations met`);
400
+ console.log('');
401
+ }
402
+
403
+ // ─── Router ───────────────────────────────────────────────────────────────────
404
+ async function main() {
405
+ switch (command) {
406
+
407
+ case 'list': {
408
+ const skills = getAvailableSkills();
409
+ const nameWidth = Math.max(...skills.map(s => s.length)) + 2;
410
+ console.log('');
411
+ console.log(c.bold(` Skills`) + c.dim(` (${skills.length} available)`));
412
+ console.log(' ' + c.line(nameWidth + 67));
413
+ for (const s of skills) {
414
+ const { description } = parseSkillFrontmatter(s);
415
+ console.log(` ${c.cyan(s.padEnd(nameWidth))}${c.dim(description ? firstSentence(description) : '')}`);
416
+ }
417
+ console.log(' ' + c.line(nameWidth + 67));
418
+ console.log(c.dim(`\n npx @booklib/skills add <name> install to .claude/skills/`));
419
+ console.log(c.dim(` npx @booklib/skills info <name> full description`));
420
+ console.log(c.dim(` npx @booklib/skills demo <name> before/after example`));
421
+ console.log(c.dim(` npx @booklib/skills check <name> quality check\n`));
422
+ break;
423
+ }
424
+
425
+ case 'info': {
426
+ const skillName = args.find(a => !a.startsWith('--') && a !== 'info');
427
+ if (!skillName) { console.error(c.red('Usage: skills info <skill-name>')); process.exit(1); }
428
+ const skills = getAvailableSkills();
429
+ if (!skills.includes(skillName)) {
430
+ console.error(c.red(`✗ "${skillName}" not found.`) + ' Run ' + c.cyan('skills list') + ' to see available skills.');
431
+ process.exit(1);
432
+ }
433
+ const { description } = parseSkillFrontmatter(skillName);
434
+ const skillMdPath = path.join(skillsRoot, skillName, 'SKILL.md');
435
+ const hasEvals = fs.existsSync(path.join(skillsRoot, skillName, 'evals'));
436
+ const hasExamples = fs.existsSync(path.join(skillsRoot, skillName, 'examples'));
437
+ const hasRefs = fs.existsSync(path.join(skillsRoot, skillName, 'references'));
438
+ const lines = fs.readFileSync(skillMdPath, 'utf8').split('\n').length;
439
+
440
+ console.log('');
441
+ console.log(c.bold(` ${skillName}`));
442
+ console.log(' ' + c.line(60));
443
+ const words = description.split(' ');
444
+ let line = ' ';
445
+ for (const word of words) {
446
+ if (line.length + word.length > 74) { console.log(line); line = ' ' + word + ' '; }
447
+ else line += word + ' ';
448
+ }
449
+ if (line.trim()) console.log(line);
450
+ console.log('');
451
+ console.log(c.dim(' Includes: ') + [
452
+ hasEvals ? c.green('evals') : null,
453
+ hasExamples ? c.green('examples') : null,
454
+ hasRefs ? c.green('references') : null,
455
+ `${lines} lines`,
456
+ ].filter(Boolean).join(c.dim(' · ')));
457
+ console.log('');
458
+ console.log(` ${c.cyan('Install:')} npx @booklib/skills add ${skillName}`);
459
+ if (hasExamples) console.log(` ${c.cyan('Demo:')} npx @booklib/skills demo ${skillName}`);
460
+ console.log(` ${c.cyan('Check:')} npx @booklib/skills check ${skillName}`);
461
+ console.log('');
462
+ break;
463
+ }
464
+
465
+ case 'demo': {
466
+ const skillName = args.find(a => !a.startsWith('--') && a !== 'demo');
467
+ if (!skillName) { console.error(c.red('Usage: skills demo <skill-name>')); process.exit(1); }
468
+ const skills = getAvailableSkills();
469
+ if (!skills.includes(skillName)) {
470
+ console.error(c.red(`✗ "${skillName}" not found.`) + ' Run ' + c.cyan('skills list') + ' to see available skills.');
471
+ process.exit(1);
472
+ }
473
+ const beforePath = path.join(skillsRoot, skillName, 'examples', 'before.md');
474
+ const afterPath = path.join(skillsRoot, skillName, 'examples', 'after.md');
475
+ if (!fs.existsSync(beforePath) || !fs.existsSync(afterPath)) {
476
+ console.log(c.yellow(` No demo available for "${skillName}" yet.`));
477
+ console.log(c.dim(` Try: npx @booklib/skills info ${skillName}\n`));
478
+ process.exit(0);
479
+ }
480
+ const before = fs.readFileSync(beforePath, 'utf8').trim();
481
+ const after = fs.readFileSync(afterPath, 'utf8').trim();
482
+ console.log('');
483
+ console.log(c.bold(` ${skillName}`) + c.dim(' — before/after example'));
484
+ console.log(' ' + c.line(60));
485
+ console.log('\n' + c.bold(c.yellow(' BEFORE')) + '\n');
486
+ before.split('\n').forEach(l => console.log(' ' + l));
487
+ console.log('\n' + c.bold(c.green(' AFTER')) + '\n');
488
+ after.split('\n').forEach(l => console.log(' ' + l));
489
+ console.log(c.dim(`\n Install: npx @booklib/skills add ${skillName}\n`));
490
+ break;
491
+ }
492
+
493
+ case 'add': {
494
+ const addAll = args.includes('--all');
495
+ const skillName = args.find(a => !a.startsWith('--') && a !== 'add');
496
+ if (addAll) {
497
+ const skills = getAvailableSkills();
498
+ skills.forEach(s => copySkill(s, targetDir));
499
+ console.log(c.dim(`\nInstalled ${skills.length} skills to ${targetDir}`));
500
+ } else if (skillName) {
501
+ copySkill(skillName, targetDir);
502
+ console.log(c.dim(`\nInstalled to ${targetDir}`));
503
+ } else {
504
+ console.error(c.red('Usage: skills add <skill-name> | skills add --all'));
505
+ process.exit(1);
506
+ }
507
+ break;
508
+ }
509
+
510
+ case 'check': {
511
+ const checkAll = args.includes('--all');
512
+ const skillName = args.find(a => !a.startsWith('--') && a !== 'check');
513
+
514
+ if (checkAll) {
515
+ const skills = getAvailableSkills();
516
+ const summary = [];
517
+ for (const s of skills) {
518
+ const checks = checkSkill(s);
519
+ const badge = earnedBadge(checks);
520
+ const pass = checks.filter(r => r.ok).length;
521
+ const total = checks.length;
522
+ const icon = badge ? BADGE[badge] : c.red('no badge');
523
+ summary.push({ name: s, badge, pass, total, icon });
524
+ }
525
+ console.log('');
526
+ console.log(c.bold(' Quality summary'));
527
+ console.log(' ' + c.line(60));
528
+ const nameW = Math.max(...summary.map(s => s.name.length)) + 2;
529
+ for (const s of summary) {
530
+ const bar = `${s.pass}/${s.total}`.padStart(5);
531
+ const failures = s.pass < s.total ? c.dim(` (${s.total - s.pass} issues)`) : '';
532
+ console.log(` ${s.name.padEnd(nameW)}${s.icon} ${c.dim(bar)}${failures}`);
533
+ }
534
+ const gold = summary.filter(s => ['gold', 'platinum'].includes(s.badge)).length;
535
+ const belowGold = summary.filter(s => !['gold', 'platinum'].includes(s.badge));
536
+ console.log(' ' + c.line(60));
537
+ console.log(c.dim(`\n ${gold}/${skills.length} skills at Gold or above\n`));
538
+ if (belowGold.length) {
539
+ console.error(c.red(` ✗ ${belowGold.length} skill(s) below Gold: ${belowGold.map(s => s.name).join(', ')}\n`));
540
+ process.exit(1);
541
+ }
542
+ } else if (skillName) {
543
+ const checks = checkSkill(skillName);
544
+ printCheckResults(skillName, checks);
545
+ const badge = earnedBadge(checks);
546
+ process.exit(badge ? 0 : 1);
547
+ } else {
548
+ console.error(c.red('Usage: skills check <skill-name> | skills check --all'));
549
+ process.exit(1);
550
+ }
551
+ break;
552
+ }
553
+
554
+ case 'eval': {
555
+ const skillName = args.find(a => !a.startsWith('--') && a !== 'eval');
556
+ const modelArg = args.find(a => a.startsWith('--model='))?.split('=')[1];
557
+ const idArg = args.find(a => a.startsWith('--id='))?.split('=')[1];
558
+
559
+ if (!skillName) {
560
+ console.error(c.red('Usage: skills eval <skill-name> [--model=<model>] [--id=<eval-id>]'));
561
+ process.exit(1);
562
+ }
563
+ const skills = getAvailableSkills();
564
+ if (!skills.includes(skillName)) {
565
+ console.error(c.red(`✗ "${skillName}" not found.`) + ' Run ' + c.cyan('skills list') + ' to see available skills.');
566
+ process.exit(1);
567
+ }
568
+ await runEvals(skillName, { model: modelArg, id: idArg });
569
+ break;
570
+ }
571
+
572
+ default:
573
+ console.log(`
574
+ ${c.bold(' @booklib/skills')} — book knowledge distilled into AI agent skills
575
+
576
+ ${c.bold(' Usage:')}
577
+ ${c.cyan('skills list')} list all available skills
578
+ ${c.cyan('skills info')} ${c.dim('<name>')} full description of a skill
579
+ ${c.cyan('skills demo')} ${c.dim('<name>')} before/after example
580
+ ${c.cyan('skills add')} ${c.dim('<name>')} install to .claude/skills/
581
+ ${c.cyan('skills add --all')} install all skills
582
+ ${c.cyan('skills add')} ${c.dim('<name> --global')} install globally
583
+ ${c.cyan('skills check')} ${c.dim('<name>')} quality check (Bronze/Silver/Gold/Platinum)
584
+ ${c.cyan('skills check --all')} quality summary for all skills
585
+ ${c.cyan('skills eval')} ${c.dim('<name>')} run evals against Claude (needs ANTHROPIC_API_KEY)
586
+ ${c.cyan('skills eval')} ${c.dim('<name> --model=<id>')} use a specific model
587
+ ${c.cyan('skills eval')} ${c.dim('<name> --id=<eval-id>')} run a single eval
84
588
  `);
589
+ }
85
590
  }
591
+
592
+ main().catch(err => {
593
+ console.error(c.red('Error: ') + err.message);
594
+ process.exit(1);
595
+ });