51bf4be52f
Three features riding 2026 trends (agentic CI, codegen, evals), sharing one dependency-free Anthropic client (bin/lib/anthropic.mjs). 1. GitHub Action (action/) — run any skill in a consumer repo's CI: uses: mohitagw15856/pm-claude-skills/action@main. Composite action + run.mjs (loads the bundled SKILL.md, calls the API, exposes result as a step output / file). Docs with auto-PR-description example. 2. generate command — `npx pm-claude-skills generate --from <url|file>` turns a team's docs into a SKILL.md following the authoring standard (bin/generate.mjs, wired into the CLI; needs ANTHROPIC_API_KEY). 3. Skill evals + Leaderboard — evals/run-evals.mjs runs each case across models and scores output with an LLM judge (structure/completeness/usefulness/ grounding); scripts/build-leaderboard.mjs renders web/leaderboard.html (built in the Pages deploy, falls back to clearly-labelled example data). Linked from README, catalog, and playground. Offline-testable parts verified (prompt building, skill loading, graceful errors, leaderboard render). SkillCheck/audit/exports all green. Claude-Session: https://claude.ai/code/session_016JWn5jRD5tcEFKrubjQ6Px Co-authored-by: Claude <noreply@anthropic.com>
110 lines
4.0 KiB
JavaScript
110 lines
4.0 KiB
JavaScript
// `pm-claude-skills generate` — turn a doc (URL or file) into a SKILL.md that
|
|
// follows this library's authoring standard. Uses the Anthropic API.
|
|
//
|
|
// ANTHROPIC_API_KEY=sk-ant-... npx pm-claude-skills generate --from ./process.md
|
|
// ... generate --from https://example.com/runbook --name incident-runbook
|
|
// ... generate --from notes.txt --out ./skills --dry-run
|
|
import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { complete, parseSkill } from './lib/anthropic.mjs';
|
|
|
|
function getArg(argv, name, def) {
|
|
const i = argv.indexOf(`--${name}`);
|
|
return i !== -1 ? argv[i + 1] : def;
|
|
}
|
|
|
|
// Strip tags/scripts/styles from HTML to rough text (good enough for an LLM).
|
|
function htmlToText(html) {
|
|
return html
|
|
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
.replace(/<[^>]+>/g, ' ')
|
|
.replace(/&[a-z]+;/gi, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
async function loadSource(from) {
|
|
if (/^https?:\/\//i.test(from)) {
|
|
const res = await fetch(from);
|
|
if (!res.ok) throw new Error(`Could not fetch ${from} (HTTP ${res.status}).`);
|
|
const text = await res.text();
|
|
return /<html|<body|<div/i.test(text) ? htmlToText(text) : text;
|
|
}
|
|
if (!existsSync(from)) throw new Error(`No such file: ${from}`);
|
|
return readFileSync(from, 'utf8');
|
|
}
|
|
|
|
const META_PROMPT = `You convert a team's documentation into a single Claude/Agent "skill" file (SKILL.md) that follows this exact standard. Output ONLY the file content, starting with the YAML frontmatter — no code fences, no preamble.
|
|
|
|
Required structure:
|
|
---
|
|
name: <lowercase-hyphenated, derived from the doc's purpose>
|
|
description: "<one sentence on what it does>. Use when <trigger phrases a user would say>. Produces <the concrete artifact>."
|
|
---
|
|
|
|
# <Title> Skill
|
|
|
|
<one-line value summary>
|
|
|
|
## What This Skill Produces
|
|
- <deliverables>
|
|
|
|
## Required Inputs
|
|
Ask for (if not provided):
|
|
- <inputs to gather; never invent them>
|
|
|
|
## Process
|
|
1. <steps>
|
|
|
|
## Output Format
|
|
<a concrete template — headings/tables — of the final artifact>
|
|
|
|
## Quality Checks
|
|
- [ ] <checks the output must pass>
|
|
|
|
## Anti-Patterns
|
|
- [ ] Do not <mistakes this skill prevents>
|
|
|
|
Rules: be specific to the documentation provided; turn its rules/process into the skill. The description MUST contain "Use when" and "Produces". Do not include any text outside the file.`;
|
|
|
|
export async function run(argv) {
|
|
const from = getArg(argv, 'from');
|
|
if (!from || argv.includes('--help')) {
|
|
console.log('Usage: pm-claude-skills generate --from <url|file> [--name x] [--out dir] [--model m] [--dry-run]');
|
|
return from ? 0 : 1;
|
|
}
|
|
const apiKey = process.env.ANTHROPIC_API_KEY || '';
|
|
if (!apiKey) { console.error('Set ANTHROPIC_API_KEY to generate a skill.'); return 1; }
|
|
const model = getArg(argv, 'model', 'claude-sonnet-4-6');
|
|
const outDir = getArg(argv, 'out', 'skills');
|
|
const dryRun = argv.includes('--dry-run');
|
|
|
|
console.error(`Reading ${from}…`);
|
|
const source = (await loadSource(from)).slice(0, 24000); // cap context
|
|
|
|
console.error(`Generating a SKILL.md with ${model}…`);
|
|
const out = await complete({
|
|
apiKey, model, system: META_PROMPT,
|
|
messages: [{ role: 'user', content: `Documentation to convert into a skill:\n\n${source}` }],
|
|
maxTokens: 3000,
|
|
});
|
|
|
|
const cleaned = out.replace(/^```[a-z]*\n?/i, '').replace(/\n?```$/i, '').trim();
|
|
const { meta } = parseSkill(cleaned);
|
|
const name = getArg(argv, 'name', meta.name);
|
|
if (!name) { console.error('Could not determine a skill name — pass --name.'); return 1; }
|
|
|
|
if (dryRun) {
|
|
console.log(cleaned);
|
|
console.error(`\n[dry-run] Would write ${join(outDir, name, 'SKILL.md')}`);
|
|
return 0;
|
|
}
|
|
const dir = join(outDir, name);
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(join(dir, 'SKILL.md'), cleaned + '\n');
|
|
console.log(`Created ${join(dir, 'SKILL.md')}`);
|
|
console.log('Next: review it, then validate — node scripts/skillcheck.mjs && node scripts/skill-audit.mjs');
|
|
return 0;
|
|
}
|