@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.
- package/CONTRIBUTING.md +122 -0
- package/README.md +20 -2
- package/ROADMAP.md +36 -0
- package/animation-at-work/evals/evals.json +44 -0
- package/animation-at-work/examples/after.md +64 -0
- package/animation-at-work/examples/before.md +35 -0
- package/animation-at-work/scripts/audit_animations.py +295 -0
- package/bin/skills.js +552 -42
- package/clean-code-reviewer/SKILL.md +109 -1
- package/clean-code-reviewer/evals/evals.json +121 -3
- package/clean-code-reviewer/examples/after.md +48 -0
- package/clean-code-reviewer/examples/before.md +33 -0
- package/clean-code-reviewer/references/api_reference.md +158 -0
- package/clean-code-reviewer/references/practices-catalog.md +282 -0
- package/clean-code-reviewer/references/review-checklist.md +254 -0
- package/clean-code-reviewer/scripts/pre-review.py +206 -0
- package/data-intensive-patterns/evals/evals.json +43 -0
- package/data-intensive-patterns/examples/after.md +61 -0
- package/data-intensive-patterns/examples/before.md +38 -0
- package/data-intensive-patterns/scripts/adr.py +213 -0
- package/data-pipelines/evals/evals.json +45 -0
- package/data-pipelines/examples/after.md +97 -0
- package/data-pipelines/examples/before.md +37 -0
- package/data-pipelines/scripts/new_pipeline.py +444 -0
- package/design-patterns/evals/evals.json +46 -0
- package/design-patterns/examples/after.md +52 -0
- package/design-patterns/examples/before.md +29 -0
- package/design-patterns/scripts/scaffold.py +807 -0
- package/domain-driven-design/SKILL.md +120 -0
- package/domain-driven-design/evals/evals.json +48 -0
- package/domain-driven-design/examples/after.md +80 -0
- package/domain-driven-design/examples/before.md +43 -0
- package/domain-driven-design/scripts/scaffold.py +421 -0
- package/effective-java/evals/evals.json +46 -0
- package/effective-java/examples/after.md +83 -0
- package/effective-java/examples/before.md +37 -0
- package/effective-java/scripts/checkstyle_setup.py +211 -0
- package/effective-kotlin/evals/evals.json +45 -0
- package/effective-kotlin/examples/after.md +36 -0
- package/effective-kotlin/examples/before.md +38 -0
- package/effective-python/evals/evals.json +44 -0
- package/effective-python/examples/after.md +56 -0
- package/effective-python/examples/before.md +40 -0
- package/effective-python/references/api_reference.md +218 -0
- package/effective-python/references/practices-catalog.md +483 -0
- package/effective-python/references/review-checklist.md +190 -0
- package/effective-python/scripts/lint.py +173 -0
- package/kotlin-in-action/evals/evals.json +43 -0
- package/kotlin-in-action/examples/after.md +53 -0
- package/kotlin-in-action/examples/before.md +39 -0
- package/kotlin-in-action/scripts/setup_detekt.py +224 -0
- package/lean-startup/evals/evals.json +43 -0
- package/lean-startup/examples/after.md +80 -0
- package/lean-startup/examples/before.md +34 -0
- package/lean-startup/scripts/new_experiment.py +286 -0
- package/microservices-patterns/SKILL.md +140 -0
- package/microservices-patterns/evals/evals.json +45 -0
- package/microservices-patterns/examples/after.md +69 -0
- package/microservices-patterns/examples/before.md +40 -0
- package/microservices-patterns/scripts/new_service.py +583 -0
- package/package.json +2 -8
- package/refactoring-ui/evals/evals.json +45 -0
- package/refactoring-ui/examples/after.md +85 -0
- package/refactoring-ui/examples/before.md +58 -0
- package/refactoring-ui/scripts/audit_css.py +250 -0
- package/skill-router/SKILL.md +142 -0
- package/skill-router/evals/evals.json +38 -0
- package/skill-router/examples/after.md +63 -0
- package/skill-router/examples/before.md +39 -0
- package/skill-router/references/api_reference.md +24 -0
- package/skill-router/references/routing-heuristics.md +89 -0
- package/skill-router/references/skill-catalog.md +156 -0
- package/skill-router/scripts/route.py +266 -0
- package/storytelling-with-data/evals/evals.json +47 -0
- package/storytelling-with-data/examples/after.md +50 -0
- package/storytelling-with-data/examples/before.md +33 -0
- package/storytelling-with-data/scripts/chart_review.py +301 -0
- package/system-design-interview/evals/evals.json +45 -0
- package/system-design-interview/examples/after.md +94 -0
- package/system-design-interview/examples/before.md +27 -0
- package/system-design-interview/scripts/new_design.py +421 -0
- package/using-asyncio-python/evals/evals.json +43 -0
- package/using-asyncio-python/examples/after.md +68 -0
- package/using-asyncio-python/examples/before.md +39 -0
- package/using-asyncio-python/scripts/check_blocking.py +270 -0
- package/web-scraping-python/evals/evals.json +46 -0
- package/web-scraping-python/examples/after.md +109 -0
- package/web-scraping-python/examples/before.md +40 -0
- package/web-scraping-python/scripts/new_scraper.py +231 -0
- /package/{effective-python-skill → effective-python}/SKILL.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-01-pythonic-thinking.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-02-lists-and-dicts.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-03-functions.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-04-comprehensions-generators.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-05-classes-interfaces.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-06-metaclasses-attributes.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-07-concurrency.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-08-robustness-performance.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-09-testing-debugging.md +0 -0
- /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)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
fs.statSync(
|
|
16
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
86
|
+
console.log(c.green('✓') + ` ${c.bold(skillName)} → ${c.dim(dest)}`);
|
|
43
87
|
}
|
|
44
88
|
|
|
45
|
-
const isGlobal
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
102
|
+
const pass = (tier, msg) => ({ ok: true, tier, msg });
|
|
103
|
+
const fail = (tier, msg) => ({ ok: false, tier, msg });
|
|
104
|
+
const checks = [];
|
|
61
105
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
});
|