SkillCheck validator, Cursor exports, and per-agent installers (#27)
Three more learnings from alirezarezvani/claude-skills, applied: 1. SkillCheck validator (scripts/skillcheck.mjs) — validates every SKILL.md against the authoring standard (frontmatter, name/folder match, trigger + produces clauses, required headings) plus tier referential integrity. Errors fail CI; --strict fails on warnings too. New skillcheck.yml workflow and a SkillCheck status badge in the README. Current: 0 errors / 14 advisory warnings across 172 skills. 2. Cursor export platform — build-exports.mjs now generates exports/cursor/<bundle>/<skill>/<skill>.mdc rule files. The PLATFORMS registry now supports per-skill filenames (file as a function). 3. Per-agent installers — scripts/install.sh unifies install for claude/hermes/codex/openclaw/cursor (--link, --target, --dry-run, --list). Curl-able one-liners codex-install.sh, openclaw-install.sh, and cursor-install.sh clone the library and install in a single command. README documents the one-line installs and Cursor exports; CHANGELOG and the authoring standard updated. Claude-Session: https://claude.ai/code/session_016JWn5jRD5tcEFKrubjQ6Px Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+21
-13
@@ -26,8 +26,9 @@ const pluginsDir = join(root, 'plugins');
|
||||
const exportsDir = join(root, 'exports');
|
||||
|
||||
// ── Platform registry ───────────────────────────────────────────────────────
|
||||
// To add a new platform (Gemini, Cursor, …), add an entry here. `render` gets
|
||||
// To add a new platform, add an entry here. `render` gets
|
||||
// { name, description, title, body, bundle } and returns the file contents.
|
||||
// `file` is a fixed filename, or a function (skill) => filename for per-skill names.
|
||||
const PLATFORMS = {
|
||||
chatgpt: {
|
||||
label: 'ChatGPT — Custom GPT instructions',
|
||||
@@ -49,15 +50,16 @@ const PLATFORMS = {
|
||||
render: ({ description, body }) =>
|
||||
`You are a specialised assistant. ${description}\n\nFollow these instructions:\n\n${body.trim()}\n`,
|
||||
},
|
||||
// Example of how a future platform slots in (kept commented, not generated):
|
||||
// cursor: {
|
||||
// label: 'Cursor — project rule (.mdc)',
|
||||
// dir: 'exports/cursor',
|
||||
// file: 'rule.mdc',
|
||||
// groupByBundle: false,
|
||||
// render: ({ description, body }) =>
|
||||
// `---\ndescription: ${JSON.stringify(description)}\nalwaysApply: false\n---\n\n${body.trim()}\n`,
|
||||
// },
|
||||
cursor: {
|
||||
label: 'Cursor — project rule (.mdc)',
|
||||
dir: 'exports/cursor',
|
||||
file: (s) => `${s.name}.mdc`,
|
||||
groupByBundle: true,
|
||||
// Cursor reads `.cursor/rules/*.mdc`. Each rule is YAML frontmatter + the body.
|
||||
// alwaysApply:false keeps it an opt-in rule the agent pulls in by description.
|
||||
render: ({ description, body }) =>
|
||||
`---\ndescription: ${JSON.stringify(description)}\nglobs:\nalwaysApply: false\n---\n\n${body.trim()}\n`,
|
||||
},
|
||||
};
|
||||
|
||||
// ── Helpers (shared shape with web/build-skills.mjs) ────────────────────────
|
||||
@@ -115,6 +117,10 @@ function loadSkills() {
|
||||
return skills;
|
||||
}
|
||||
|
||||
// Resolve a platform's output filename for a skill (string or function).
|
||||
const fileNameFor = (platform, skill) =>
|
||||
typeof platform.file === 'function' ? platform.file(skill) : platform.file;
|
||||
|
||||
// Build the full path->content map a platform should produce.
|
||||
function planPlatform(key, platform, skills) {
|
||||
const files = new Map();
|
||||
@@ -122,22 +128,24 @@ function planPlatform(key, platform, skills) {
|
||||
for (const skill of skills) {
|
||||
const parts = [base];
|
||||
if (platform.groupByBundle) parts.push(skill.bundle);
|
||||
parts.push(skill.name, platform.file);
|
||||
parts.push(skill.name, fileNameFor(platform, skill));
|
||||
files.set(join(...parts), platform.render(skill));
|
||||
}
|
||||
// Generated index for the platform.
|
||||
const fileHint = typeof platform.file === 'function' ? '.mdc rule' : platform.file;
|
||||
const index = [
|
||||
`# ${platform.label}`,
|
||||
'',
|
||||
`> Auto-generated from \`skills/*/SKILL.md\` by \`scripts/build-exports.mjs\`.`,
|
||||
`> **Do not edit these files by hand** — edit the source skill and regenerate.`,
|
||||
'',
|
||||
`${skills.length} skills exported. Copy a \`${platform.file}\` into the tool to use it.`,
|
||||
`${skills.length} skills exported. Copy a \`${fileHint}\` into the tool to use it.`,
|
||||
'',
|
||||
'| Skill | Bundle | Path |',
|
||||
'|---|---|---|',
|
||||
...skills.map((s) => {
|
||||
const rel = relative(base, [...(platform.groupByBundle ? [join(base, s.bundle)] : [base]), s.name, platform.file].reduce((a, b) => join(a, b)));
|
||||
const leaf = [...(platform.groupByBundle ? [join(base, s.bundle)] : [base]), s.name, fileNameFor(platform, s)].reduce((a, b) => join(a, b));
|
||||
const rel = relative(base, leaf);
|
||||
return `| ${s.title} | \`${s.bundle}\` | \`${rel}\` |`;
|
||||
}),
|
||||
'',
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-line installer for OpenAI Codex. Clones the library (or updates an existing
|
||||
# clone) and installs all skills where OpenAI Codex can discover them.
|
||||
#
|
||||
# bash <(curl -fsSL https://raw.githubusercontent.com/mohitagw15856/pm-claude-skills/main/scripts/codex-install.sh)
|
||||
#
|
||||
# Pass extra flags straight through to install.sh, e.g. --link, --target, --dry-run.
|
||||
set -euo pipefail
|
||||
|
||||
AGENT="codex"
|
||||
REPO_URL="https://github.com/mohitagw15856/pm-claude-skills.git"
|
||||
DEST="${PM_SKILLS_DIR:-$HOME/.pm-claude-skills}"
|
||||
|
||||
if [ -d "$DEST/.git" ]; then
|
||||
echo "Updating existing clone at $DEST"
|
||||
git -C "$DEST" pull --ff-only --quiet || echo "(could not fast-forward; using existing checkout)"
|
||||
else
|
||||
echo "Cloning library into $DEST"
|
||||
git clone --depth 1 "$REPO_URL" "$DEST"
|
||||
fi
|
||||
|
||||
exec bash "$DEST/scripts/install.sh" --agent "$AGENT" "$@"
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-line installer for Cursor. Clones the library (or updates an existing
|
||||
# clone) and installs all skills where Cursor can discover them.
|
||||
#
|
||||
# bash <(curl -fsSL https://raw.githubusercontent.com/mohitagw15856/pm-claude-skills/main/scripts/cursor-install.sh)
|
||||
#
|
||||
# Pass extra flags straight through to install.sh, e.g. --link, --target, --dry-run.
|
||||
set -euo pipefail
|
||||
|
||||
AGENT="cursor"
|
||||
REPO_URL="https://github.com/mohitagw15856/pm-claude-skills.git"
|
||||
DEST="${PM_SKILLS_DIR:-$HOME/.pm-claude-skills}"
|
||||
|
||||
if [ -d "$DEST/.git" ]; then
|
||||
echo "Updating existing clone at $DEST"
|
||||
git -C "$DEST" pull --ff-only --quiet || echo "(could not fast-forward; using existing checkout)"
|
||||
else
|
||||
echo "Cloning library into $DEST"
|
||||
git clone --depth 1 "$REPO_URL" "$DEST"
|
||||
fi
|
||||
|
||||
exec bash "$DEST/scripts/install.sh" --agent "$AGENT" "$@"
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# Unified skill installer for SKILL.md-native agents (and Cursor rules).
|
||||
#
|
||||
# Native agents (Claude Code, Hermes, OpenAI Codex, OpenClaw) read the same
|
||||
# SKILL.md standard — installing is just placing the skill folders where the
|
||||
# agent discovers them. Cursor uses .mdc rule files (from exports/cursor/).
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/install.sh --agent claude
|
||||
# ./scripts/install.sh --agent codex --link
|
||||
# ./scripts/install.sh --agent cursor --target ./.cursor/rules
|
||||
# ./scripts/install.sh --list
|
||||
#
|
||||
# Flags:
|
||||
# --agent <name> claude | hermes | codex | openclaw | cursor
|
||||
# --target <path> override the default install directory
|
||||
# --link symlink instead of copy (native agents only; updates follow git pull)
|
||||
# --dry-run print actions without writing
|
||||
# --list list supported agents and their default targets
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SKILLS_DIR="$REPO_DIR/skills"
|
||||
|
||||
AGENT=""; TARGET=""; LINK=0; DRYRUN=0
|
||||
|
||||
default_target() {
|
||||
case "$1" in
|
||||
claude) echo "$HOME/.claude/skills" ;;
|
||||
hermes) echo "$HOME/.hermes/skills" ;;
|
||||
codex) echo "$HOME/.codex/skills" ;;
|
||||
openclaw) echo "$HOME/.openclaw/skills" ;;
|
||||
cursor) echo "$PWD/.cursor/rules" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
list_agents() {
|
||||
echo "Supported agents and default targets:"
|
||||
for a in claude hermes codex openclaw cursor; do
|
||||
printf " %-9s %s\n" "$a" "$(default_target "$a")"
|
||||
done
|
||||
echo
|
||||
echo "Native SKILL.md agents: claude, hermes, codex, openclaw (install skill folders)."
|
||||
echo "Cursor installs generated .mdc rules from exports/cursor/."
|
||||
echo "Targets are sensible defaults — override with --target if your setup differs."
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--agent) AGENT="${2:-}"; shift 2 ;;
|
||||
--target) TARGET="${2:-}"; shift 2 ;;
|
||||
--link) LINK=1; shift ;;
|
||||
--dry-run) DRYRUN=1; shift ;;
|
||||
--list) list_agents; exit 0 ;;
|
||||
-h|--help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "Unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$AGENT" ]; then echo "Error: --agent is required (see --list)." >&2; exit 2; fi
|
||||
if ! default_target "$AGENT" >/dev/null 2>&1; then
|
||||
echo "Error: unknown agent '$AGENT'. Run --list." >&2; exit 2
|
||||
fi
|
||||
[ -n "$TARGET" ] || TARGET="$(default_target "$AGENT")"
|
||||
|
||||
if [ ! -d "$SKILLS_DIR" ]; then
|
||||
echo "Error: cannot find skills/ at $SKILLS_DIR (run from a clone of the repo)." >&2; exit 1
|
||||
fi
|
||||
|
||||
place() { # place <src> <dest>
|
||||
local src="$1" dest="$2"
|
||||
if [ "$DRYRUN" = 1 ]; then echo " would install $(basename "$src") -> $dest"; return; fi
|
||||
rm -rf "$dest"
|
||||
if [ "$LINK" = 1 ]; then ln -s "$src" "$dest"; else cp -R "$src" "$dest"; fi
|
||||
}
|
||||
|
||||
count=0
|
||||
if [ "$DRYRUN" = 1 ]; then echo "[dry-run] Installing skills for '$AGENT' into $TARGET";
|
||||
else echo "Installing skills for '$AGENT' into $TARGET"; mkdir -p "$TARGET"; fi
|
||||
|
||||
if [ "$AGENT" = "cursor" ]; then
|
||||
# Install generated .mdc rules (flattened) into .cursor/rules/.
|
||||
CURSOR_DIR="$REPO_DIR/exports/cursor"
|
||||
if [ ! -d "$CURSOR_DIR" ]; then
|
||||
echo "Error: $CURSOR_DIR missing. Run: node scripts/build-exports.mjs" >&2; exit 1
|
||||
fi
|
||||
while IFS= read -r mdc; do
|
||||
base="$(basename "$mdc")"
|
||||
if [ "$DRYRUN" = 1 ]; then echo " would install $base -> $TARGET/$base";
|
||||
else cp "$mdc" "$TARGET/$base"; fi
|
||||
count=$((count + 1))
|
||||
done < <(find "$CURSOR_DIR" -name '*.mdc' | sort)
|
||||
else
|
||||
# Native SKILL.md agents: place each skill folder.
|
||||
for skill in "$SKILLS_DIR"/*/; do
|
||||
[ -f "$skill/SKILL.md" ] || continue
|
||||
name="$(basename "$skill")"
|
||||
place "${skill%/}" "$TARGET/$name"
|
||||
count=$((count + 1))
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$DRYRUN" = 1 ]; then
|
||||
echo "[dry-run] Would install $count item(s) for '$AGENT'."
|
||||
else
|
||||
echo "Installed $count item(s) for '$AGENT'."
|
||||
case "$AGENT" in
|
||||
cursor) echo "Cursor will pick up the rules in $TARGET on its next session." ;;
|
||||
*) echo "Restart $AGENT — it auto-discovers SKILL.md skills in $TARGET by their description." ;;
|
||||
esac
|
||||
fi
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-line installer for OpenClaw. Clones the library (or updates an existing
|
||||
# clone) and installs all skills where OpenClaw can discover them.
|
||||
#
|
||||
# bash <(curl -fsSL https://raw.githubusercontent.com/mohitagw15856/pm-claude-skills/main/scripts/openclaw-install.sh)
|
||||
#
|
||||
# Pass extra flags straight through to install.sh, e.g. --link, --target, --dry-run.
|
||||
set -euo pipefail
|
||||
|
||||
AGENT="openclaw"
|
||||
REPO_URL="https://github.com/mohitagw15856/pm-claude-skills.git"
|
||||
DEST="${PM_SKILLS_DIR:-$HOME/.pm-claude-skills}"
|
||||
|
||||
if [ -d "$DEST/.git" ]; then
|
||||
echo "Updating existing clone at $DEST"
|
||||
git -C "$DEST" pull --ff-only --quiet || echo "(could not fast-forward; using existing checkout)"
|
||||
else
|
||||
echo "Cloning library into $DEST"
|
||||
git clone --depth 1 "$REPO_URL" "$DEST"
|
||||
fi
|
||||
|
||||
exec bash "$DEST/scripts/install.sh" --agent "$AGENT" "$@"
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env node
|
||||
// SkillCheck — validates every skills/<name>/SKILL.md against the project's
|
||||
// authoring standard (see SKILL-AUTHORING-STANDARD.md). Errors fail the build;
|
||||
// warnings are reported but don't fail unless --strict is passed.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/skillcheck.mjs # validate all skills
|
||||
// node scripts/skillcheck.mjs --strict # treat warnings as errors
|
||||
// node scripts/skillcheck.mjs --json # machine-readable report
|
||||
//
|
||||
// No dependencies.
|
||||
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(__dirname, '..');
|
||||
const skillsDir = join(root, 'skills');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const strict = args.includes('--strict');
|
||||
const asJson = args.includes('--json');
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const m = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
||||
if (!m) return { meta: null, 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] };
|
||||
}
|
||||
|
||||
// Validate one skill folder. Returns { name, errors[], warnings[] }.
|
||||
function checkSkill(name) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const file = join(skillsDir, name, 'SKILL.md');
|
||||
|
||||
if (!existsSync(file)) {
|
||||
errors.push('No SKILL.md in folder.');
|
||||
return { name, errors, warnings };
|
||||
}
|
||||
|
||||
const text = readFileSync(file, 'utf8');
|
||||
const { meta, body } = parseFrontmatter(text);
|
||||
|
||||
if (!meta) {
|
||||
errors.push('Missing or malformed YAML frontmatter (--- name/description ---).');
|
||||
return { name, errors, warnings };
|
||||
}
|
||||
|
||||
// --- Frontmatter: hard requirements ---
|
||||
if (!meta.name) errors.push('Frontmatter is missing `name`.');
|
||||
else if (meta.name !== name) errors.push(`Frontmatter name "${meta.name}" does not match folder "${name}".`);
|
||||
|
||||
if (!meta.description) {
|
||||
errors.push('Frontmatter is missing `description`.');
|
||||
} else {
|
||||
const d = meta.description;
|
||||
if (/your-skill-name|one sentence\.|trigger condition|output description/i.test(d))
|
||||
errors.push('Description still contains template placeholder text.');
|
||||
if (!/\buse when\b/i.test(d)) warnings.push('Description has no "Use when …" trigger clause.');
|
||||
if (!/\bproduce(s|d)?\b/i.test(d)) warnings.push('Description does not state what it Produces.');
|
||||
if (d.length < 40) warnings.push(`Description is very short (${d.length} chars).`);
|
||||
if (d.length > 700) warnings.push(`Description is very long (${d.length} chars) — trim for the trigger budget.`);
|
||||
}
|
||||
|
||||
// --- Body checks ---
|
||||
const trimmed = body.trim();
|
||||
if (!/^#\s+.+/m.test(trimmed)) errors.push('Body has no top-level `# Title` heading.');
|
||||
if (/\[Instructions for Claude to follow/i.test(trimmed)) errors.push('Body still contains the template stub line.');
|
||||
if (trimmed.length < 300) warnings.push(`Body is very short (${trimmed.length} chars) for a reusable skill.`);
|
||||
|
||||
// --- Recommended sections (warn only) ---
|
||||
if (!/^#{2,3}\s+.*quality check/im.test(trimmed)) warnings.push('No "Quality Checks" section.');
|
||||
if (!/^#{2,3}\s+.*anti-?pattern/im.test(trimmed)) warnings.push('No "Anti-Patterns" section.');
|
||||
|
||||
return { name, errors, warnings };
|
||||
}
|
||||
|
||||
// --- Run across all skills ---
|
||||
const names = readdirSync(skillsDir).filter((n) => statSync(join(skillsDir, n)).isDirectory());
|
||||
const results = names.map(checkSkill);
|
||||
|
||||
// --- Cross-file: tier membership must reference real skills ---
|
||||
const tierErrors = [];
|
||||
const tiersFile = join(root, 'skill-tiers.json');
|
||||
if (existsSync(tiersFile)) {
|
||||
const tiers = JSON.parse(readFileSync(tiersFile, 'utf8'));
|
||||
const valid = new Set(names);
|
||||
for (const key of ['productionReady', 'experimental']) {
|
||||
for (const n of tiers[key] || []) {
|
||||
if (!valid.has(n)) tierErrors.push(`skill-tiers.json "${key}" references unknown skill "${n}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalErrors = results.reduce((a, r) => a + r.errors.length, 0) + tierErrors.length;
|
||||
const totalWarnings = results.reduce((a, r) => a + r.warnings.length, 0);
|
||||
|
||||
if (asJson) {
|
||||
console.log(JSON.stringify(
|
||||
{ skills: names.length, errors: totalErrors, warnings: totalWarnings, tierErrors, results: results.filter((r) => r.errors.length || r.warnings.length) },
|
||||
null, 2
|
||||
));
|
||||
} else {
|
||||
for (const r of results) {
|
||||
for (const e of r.errors) console.log(` ✖ ${r.name}: ${e}`);
|
||||
for (const w of r.warnings) console.log(` ⚠ ${r.name}: ${w}`);
|
||||
}
|
||||
for (const e of tierErrors) console.log(` ✖ tiers: ${e}`);
|
||||
console.log(`\nSkillCheck — ${names.length} skills · ${totalErrors} error(s) · ${totalWarnings} warning(s)`);
|
||||
}
|
||||
|
||||
const failed = totalErrors > 0 || (strict && totalWarnings > 0);
|
||||
if (failed) {
|
||||
if (!asJson) console.log(strict && totalWarnings && !totalErrors ? 'Failed (--strict: warnings count as errors).' : 'Failed.');
|
||||
process.exit(1);
|
||||
} else if (!asJson) {
|
||||
console.log('All skills valid. ✓');
|
||||
}
|
||||
Regular → Executable
Reference in New Issue
Block a user