Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/validate-plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ on:
paths:
- ".cursor-plugin/marketplace.json"
- "**/plugin.json"
- "**/SKILL.md"
- "schemas/**"
- "scripts/**"

jobs:
validate:
Expand All @@ -18,7 +20,7 @@ jobs:
node-version: 20

- name: Install dependencies
run: npm install --no-save ajv ajv-formats
run: npm install --no-save ajv ajv-formats yaml

- name: Validate plugin definitions
run: node scripts/validate-plugins.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: check-agent-compatibility
description: Run the full repository compatibility pass: scanner score, startup path, validation loop, and docs reliability.
description: "Run the full repository compatibility pass: scanner score, startup path, validation loop, and docs reliability."
---

# Check agent compatibility
Expand Down
2 changes: 1 addition & 1 deletion pstack/skills/make-bot-ui/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: Make Bot UI
name: make-bot-ui
description: >-
Use when building a custom UI (page, dashboard, buttons) that should wake a
Grok Bot over a webhook, when the user must provide a webhook sender key, or
Expand Down
2 changes: 1 addition & 1 deletion pstack/skills/poteto-mode/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: Poteto Mode
name: poteto-mode
description: poteto's agent style for concise, detailed responses, deliberate subagents, unslopped prose, simple code, and verified work. Use for poteto, /poteto-mode, or requests to work in this style.
disable-model-invocation: true
mode: true
Expand Down
22 changes: 22 additions & 0 deletions schemas/skill.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://cursor.com/schemas/cursor-plugin/skill.json",
"title": "Cursor Skill Frontmatter",
"description": "Schema for the YAML frontmatter of a SKILL.md — defines a single skill's metadata. Unknown keys are permitted so plugins can add harness-specific options.",
"type": "object",
"required": ["name", "description"],
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"minLength": 1,
"pattern": "^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$",
"description": "Unique skill identifier in kebab-case. Must match the skill's folder name."
},
"description": {
"type": "string",
"minLength": 1,
"description": "Short description of what the skill does and when to use it."
}
}
}
83 changes: 80 additions & 3 deletions scripts/validate-plugins.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#!/usr/bin/env node

import { readFileSync, existsSync } from "fs";
import { resolve, dirname } from "path";
import { readFileSync, existsSync, readdirSync } from "fs";
import { resolve, dirname, basename, relative } from "path";
import { fileURLToPath } from "url";
import Ajv from "ajv";
import addFormats from "ajv-formats";
import { parse as parseYaml } from "yaml";

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, "..");
Expand All @@ -17,12 +18,14 @@ const marketplaceSchema = loadJSON(
resolve(root, "schemas/marketplace.schema.json")
);
const pluginSchema = loadJSON(resolve(root, "schemas/plugin.schema.json"));
const skillSchema = loadJSON(resolve(root, "schemas/skill.schema.json"));

const ajv = new Ajv({ allErrors: true });
addFormats(ajv);

const validateMarketplace = ajv.compile(marketplaceSchema);
const validatePlugin = ajv.compile(pluginSchema);
const validateSkill = ajv.compile(skillSchema);

let errors = 0;

Expand Down Expand Up @@ -92,7 +95,81 @@ for (const entry of marketplace.plugins ?? []) {
}
}

// 3. Report results
// 3. Check every plugin in the repository is registered in the marketplace
const marketplaceSources = new Set(
(marketplace.plugins ?? []).map((entry) => resolve(root, entry.source))
);

const pluginDirs = [];

(function collectPluginDirs(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.name === ".git" || entry.name === "node_modules") continue;
const full = resolve(dir, entry.name);
if (!entry.isDirectory()) continue;
if (existsSync(resolve(full, ".cursor-plugin/plugin.json"))) {
pluginDirs.push(full);
} else {
collectPluginDirs(full);
}
}
})(root);

for (const pluginDir of pluginDirs) {
if (!marketplaceSources.has(pluginDir)) {
fail(
`${relative(root, pluginDir)}: has a .cursor-plugin/plugin.json but is not listed in .cursor-plugin/marketplace.json`
);
}
}

// 4. Validate skill frontmatter
const skillFiles = [];

(function collectSkillFiles(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.name === ".git" || entry.name === "node_modules") continue;
const full = resolve(dir, entry.name);
if (entry.isDirectory()) collectSkillFiles(full);
else if (entry.name === "SKILL.md") skillFiles.push(full);
}
})(root);

for (const skillPath of skillFiles.sort()) {
const skillRel = relative(root, skillPath);
const text = readFileSync(skillPath, "utf-8");
const frontmatterMatch = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);

if (!frontmatterMatch) {
fail(`${skillRel}: missing YAML frontmatter (file must start with "---")`);
continue;
}

let frontmatter;
try {
frontmatter = parseYaml(frontmatterMatch[1]) ?? {};
} catch (err) {
fail(`${skillRel}: frontmatter is not valid YAML (${err.message.split("\n")[0]})`);
continue;
}

if (!validateSkill(frontmatter)) {
fail(`${skillRel}: frontmatter does not match schemas/skill.schema.json:`);
for (const err of validateSkill.errors) {
console.error(` ${err.instancePath || "/"}: ${err.message}`);
}
continue;
}

const folder = basename(dirname(skillPath));
if (frontmatter.name !== folder) {
fail(
`${skillRel}: frontmatter name "${frontmatter.name}" does not match folder name "${folder}"`
);
}
}

// 5. Report results
if (errors > 0) {
console.error(`\nValidation failed with ${errors} error(s).`);
process.exit(1);
Expand Down
2 changes: 1 addition & 1 deletion third_party/x-money/skills/x-money-guide/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: X Money guide
name: x-money-guide
description: >-
Read this before the first X Money action in a session and again on any
X Money error, refusal, or missing capability. Covers the approval rule for
Expand Down
2 changes: 1 addition & 1 deletion third_party/x/skills/x-api-mcp-guide/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: X MCP guide
name: x-api-mcp-guide
description: >-
ALWAYS read this when a user connects the X plugin or any X MCP, before using
any X connection, and again on any X error. Do not call an X tool until this
Expand Down
2 changes: 1 addition & 1 deletion third_party/x/skills/x-chat/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: X Chat
name: x-chat
description: >-
Read, summarize, or send encrypted X Chat (XChat) DMs via the X plugin MCP
plus local chatxdk / xchat_lite.py. Use when the user mentions X Chat, xchat,
Expand Down