feat: workflow recipes, eval badges, one-click MCP, playground upgrades, sample gallery, skill-of-the-week
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>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
#!/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.`);
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env node
|
||||
// Generates WORKFLOWS.md from workflows.json and validates that every recipe
|
||||
// references a real skill (skills/<skill>/SKILL.md) and ships a slash command
|
||||
// (commands/<id>.md). Run: node scripts/build-workflows.mjs [--check]
|
||||
// --check exits non-zero if WORKFLOWS.md is out of sync (for CI).
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const { workflows } = JSON.parse(readFileSync(join(root, 'workflows.json'), 'utf8'));
|
||||
|
||||
// --- Validate ---
|
||||
const errors = [];
|
||||
for (const w of workflows) {
|
||||
if (!existsSync(join(root, 'commands', `${w.id}.md`))) errors.push(`Missing command file: commands/${w.id}.md`);
|
||||
for (const step of w.steps) {
|
||||
if (!existsSync(join(root, 'skills', step.skill, 'SKILL.md'))) errors.push(`${w.id}: unknown skill "${step.skill}"`);
|
||||
}
|
||||
}
|
||||
if (errors.length) {
|
||||
console.error('Workflow validation failed:\n ' + errors.join('\n '));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// --- Render WORKFLOWS.md ---
|
||||
const lines = [];
|
||||
lines.push('# 🧩 Workflow Recipes', '');
|
||||
lines.push('> **Skills you can chain.** A recipe runs several skills in sequence and *passes each output forward as context* for the next — so a fuzzy idea comes out the other end as a finished, joined-up set of artifacts. No other skills library chains across professions like this.', '');
|
||||
lines.push('Run one as a slash command in Claude Code (e.g. `/ship-a-feature a referral program for B2B users`), or fetch it over MCP with the `get_workflow` tool.', '');
|
||||
lines.push('<!-- Generated from workflows.json by scripts/build-workflows.mjs — do not edit by hand. -->', '');
|
||||
lines.push(`There are **${workflows.length} recipes** today:`, '');
|
||||
|
||||
// Index table
|
||||
lines.push('| Recipe | Command | Lifecycle | Chains |');
|
||||
lines.push('|--------|---------|-----------|--------|');
|
||||
for (const w of workflows) {
|
||||
lines.push(`| **${w.name}** | \`${w.command}\` | ${w.lifecycle} | ${w.steps.length} skills |`);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
// Per-recipe detail
|
||||
for (const w of workflows) {
|
||||
lines.push(`## ${w.name} — \`${w.command}\``, '');
|
||||
lines.push(`*${w.lifecycle}* · ${w.summary}`, '');
|
||||
const chain = w.steps.map((s) => `\`${s.skill}\``).join(' → ');
|
||||
lines.push(chain, '');
|
||||
w.steps.forEach((s, i) => {
|
||||
lines.push(`${i + 1}. **${s.skill}** → produces ${s.produces}.`);
|
||||
});
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('---', '');
|
||||
lines.push('**Add your own:** define it in [`workflows.json`](workflows.json), add a matching `commands/<id>.md`, and run `node scripts/build-workflows.mjs`. Recipes are just composition — every step is an existing skill you can already run on its own.', '');
|
||||
|
||||
const md = lines.join('\n');
|
||||
const target = join(root, 'WORKFLOWS.md');
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
const current = existsSync(target) ? readFileSync(target, 'utf8') : '';
|
||||
if (current !== md) {
|
||||
console.error('WORKFLOWS.md is out of sync with workflows.json. Run: node scripts/build-workflows.mjs');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('WORKFLOWS.md is in sync.');
|
||||
} else {
|
||||
writeFileSync(target, md);
|
||||
console.log(`Wrote WORKFLOWS.md — ${workflows.length} recipes, all skills + commands validated.`);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
// Picks a "skill of the week" deterministically (rotates through the
|
||||
// production-tier skills by ISO week), composes ready-to-post copy for X and
|
||||
// LinkedIn, and:
|
||||
// - writes web/skill-of-the-week.json (for the site / README to display)
|
||||
// - appends the posts to the GitHub Actions job summary
|
||||
// - if POST_WEBHOOK_URL is set, POSTs { text, network } so you can wire it to
|
||||
// Zapier / Make / Buffer / a Slack webhook to auto-publish.
|
||||
//
|
||||
// Run locally: node scripts/skill-of-the-week.mjs
|
||||
import { readFileSync, writeFileSync, readdirSync, existsSync, appendFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const REPO = 'https://github.com/mohitagw15856/pm-claude-skills';
|
||||
const PLAY = 'https://mohitagw15856.github.io/pm-claude-skills';
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const m = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
||||
const meta = {};
|
||||
if (m) for (const line of m[1].split('\n')) {
|
||||
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
||||
if (kv) { let v = kv[2].trim(); if (/^["'].*["']$/.test(v)) v = v.slice(1, -1); meta[kv[1]] = v; }
|
||||
}
|
||||
const body = m ? m[2] : text;
|
||||
const title = (body.match(/^#\s+(.+)$/m)?.[1] || '').replace(/\s+Skill$/i, '');
|
||||
return { meta, title };
|
||||
}
|
||||
|
||||
function firstSentence(desc) {
|
||||
let s = (desc || '').split(/(?<=\.)\s+/)[0].trim();
|
||||
if (/^use\b/i.test(s)) s = (desc || '').replace(/\s+/g, ' ');
|
||||
return s.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
// Pool = production-tier skills (the most impressive to feature).
|
||||
const tiers = existsSync(join(root, 'skill-tiers.json'))
|
||||
? JSON.parse(readFileSync(join(root, 'skill-tiers.json'), 'utf8'))
|
||||
: { productionReady: [] };
|
||||
let pool = (tiers.productionReady || []).filter((n) => existsSync(join(root, 'skills', n, 'SKILL.md')));
|
||||
if (!pool.length) pool = readdirSync(join(root, 'skills')).filter((n) => existsSync(join(root, 'skills', n, 'SKILL.md')));
|
||||
pool.sort();
|
||||
|
||||
// Deterministic weekly rotation.
|
||||
const weeks = Math.floor(Date.now() / (7 * 24 * 3600 * 1000));
|
||||
const name = pool[weeks % pool.length];
|
||||
const { meta, title } = parseFrontmatter(readFileSync(join(root, 'skills', name, 'SKILL.md'), 'utf8'));
|
||||
const summary = firstSentence(meta.description);
|
||||
const link = `${PLAY}/index.html?skill=${name}`;
|
||||
|
||||
const xPost =
|
||||
`🛠️ Skill of the week: ${title}\n\n${summary}\n\n` +
|
||||
`Run it free in your browser (no install) 👇\n${link}\n\n#ClaudeAI #AItools #ProductManagement`;
|
||||
|
||||
const liPost =
|
||||
`🛠️ Skill of the week: ${title}\n\n${summary}\n\n` +
|
||||
`It's one of 174 open-source AI skills that make Claude, ChatGPT, and Gemini produce real professional work. ` +
|
||||
`Try this one free in the browser — no install:\n${link}\n\n⭐ ${REPO}`;
|
||||
|
||||
const out = { week: weeks, skill: name, title, summary, link, generatedAt: new Date().toISOString(), posts: { x: xPost, linkedin: liPost } };
|
||||
writeFileSync(join(root, 'web', 'skill-of-the-week.json'), JSON.stringify(out, null, 2) + '\n');
|
||||
|
||||
console.log(`Skill of the week (#${weeks}): ${title} (${name})\n`);
|
||||
console.log('--- X ---\n' + xPost + '\n\n--- LinkedIn ---\n' + liPost);
|
||||
|
||||
if (process.env.GITHUB_STEP_SUMMARY) {
|
||||
appendFileSync(process.env.GITHUB_STEP_SUMMARY,
|
||||
`## 🛠️ Skill of the week: ${title}\n\n${summary}\n\n[Run it](${link})\n\n` +
|
||||
`### X / Twitter\n\`\`\`\n${xPost}\n\`\`\`\n\n### LinkedIn\n\`\`\`\n${liPost}\n\`\`\`\n`);
|
||||
}
|
||||
|
||||
// Optional auto-publish hook (Zapier / Make / Buffer / Slack incoming webhook).
|
||||
const webhook = process.env.POST_WEBHOOK_URL;
|
||||
if (webhook) {
|
||||
try {
|
||||
const res = await fetch(webhook, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ text: xPost, linkedin: liPost, skill: name, link }),
|
||||
});
|
||||
console.log(`\nPosted to webhook: HTTP ${res.status}`);
|
||||
} catch (e) {
|
||||
console.error('Webhook post failed:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user