54f76456ab
Signature features that turn breadth (174 skills) into a differentiated product: - Workflow recipes: 5 cross-profession chains (workflows.json) that pass each output forward — slash commands (/ship-a-feature etc.), WORKFLOWS.md generated by scripts/build-workflows.mjs, README + MCP (list_workflows/get_workflow) wired - Eval-backed quality: real per-skill scores from evals/results.json surfaced as badges in the playground and an honest README section (6 scored skills) - One-click MCP: 'claude mcp add' install + workflow tools, works in any MCP client - Playground: 'which skill?' recommender, with/without compare toggle, shareable ?skill= deep-links with prefilled inputs - Sample-output gallery: hand-written examples for the hero five + generator (scripts/build-samples.mjs) + web/examples.html - Skill-of-the-week: scheduled workflow + script that composes X/LinkedIn posts and posts to an optional webhook Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
3.4 KiB
JavaScript
70 lines
3.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Builds web/samples.json from examples/samples/*.md for the sample-output gallery.
|
|
// Each sample file has frontmatter (skill, title, input, source) + the output body.
|
|
//
|
|
// Build the gallery data: node scripts/build-samples.mjs
|
|
// Generate a new sample: ANTHROPIC_API_KEY=sk-ant-... node scripts/build-samples.mjs --generate <skill>
|
|
// (runs the skill on its eval case input and writes examples/samples/<skill>.md)
|
|
import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const samplesDir = join(root, 'examples', 'samples');
|
|
|
|
function parseFrontmatter(text) {
|
|
const m = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
if (!m) return { meta: {}, body: text };
|
|
const meta = {};
|
|
for (const line of m[1].split('\n')) {
|
|
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
if (kv) {
|
|
let v = kv[2].trim();
|
|
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
meta[kv[1]] = v;
|
|
}
|
|
}
|
|
return { meta, body: m[2].trim() };
|
|
}
|
|
|
|
async function generate(skillName) {
|
|
const { complete, parseSkill } = await import('../bin/lib/anthropic.mjs');
|
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
if (!apiKey) { console.error('Set ANTHROPIC_API_KEY to generate.'); process.exit(1); }
|
|
const skillFile = join(root, 'skills', skillName, 'SKILL.md');
|
|
if (!existsSync(skillFile)) { console.error(`Unknown skill: ${skillName}`); process.exit(1); }
|
|
const { body } = parseSkill(readFileSync(skillFile, 'utf8'));
|
|
const cases = JSON.parse(readFileSync(join(root, 'evals', 'cases.json'), 'utf8')).cases;
|
|
const input = (cases.find((c) => c.skill === skillName) || {}).input;
|
|
if (!input) { console.error(`No eval case input for ${skillName}; add one to evals/cases.json first.`); process.exit(1); }
|
|
const system = body + '\n\n---\nExecute this skill now on the input below and produce the complete output. Do not ask questions.';
|
|
const output = await complete({ apiKey, model: 'claude-sonnet-4-6', system, messages: [{ role: 'user', content: input }], maxTokens: 4096 });
|
|
const title = skillName.split('-').map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');
|
|
if (!existsSync(samplesDir)) mkdirSync(samplesDir, { recursive: true });
|
|
const fm = `---\nskill: ${skillName}\ntitle: ${title}\ninput: ${JSON.stringify(input)}\nsource: generated by claude-sonnet-4-6\n---\n\n${output.trim()}\n`;
|
|
writeFileSync(join(samplesDir, `${skillName}.md`), fm);
|
|
console.log(`Wrote examples/samples/${skillName}.md`);
|
|
}
|
|
|
|
const genIdx = process.argv.indexOf('--generate');
|
|
if (genIdx !== -1) {
|
|
await generate(process.argv[genIdx + 1]);
|
|
}
|
|
|
|
// Build samples.json
|
|
const samples = [];
|
|
if (existsSync(samplesDir)) {
|
|
for (const f of readdirSync(samplesDir).filter((f) => f.endsWith('.md')).sort()) {
|
|
const { meta, body } = parseFrontmatter(readFileSync(join(samplesDir, f), 'utf8'));
|
|
samples.push({
|
|
skill: meta.skill || f.replace(/\.md$/, ''),
|
|
title: meta.title || meta.skill || f,
|
|
input: meta.input || '',
|
|
source: meta.source || '',
|
|
output: body,
|
|
});
|
|
}
|
|
}
|
|
writeFileSync(join(root, 'web', 'samples.json'), JSON.stringify({ count: samples.length, samples }));
|
|
console.log(`Wrote web/samples.json — ${samples.length} sample outputs.`);
|