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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,10 @@ Official skills and plugins for OpenHands — the open-source AI software engine
2. Create a new directory: `skills/<your-skill-name>/`
3. Add `skills/<your-skill-name>/SKILL.md`
4. (Optional) Add `README.md`, `references/`, `scripts/`, etc.
5. Submit a pull request
5. Add an entry to `marketplaces/openhands-extensions.json` with a `category` — this is what the OpenHands Skills page uses to group the skill. Skills with no entry are grouped as "Uncategorized". Marketplace entries also require `.plugin/plugin.json` and vendor symlinks.
6. Submit a pull request

Valid skill categories: `automations`, `environment`, `code-hosting`, `agent-authoring`, `code-quality`, `integrations`, `writing`, `design`, `other`.

### Adding a Plugin

Expand Down
4 changes: 2 additions & 2 deletions marketplaces/large-codebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"name": "add-javadoc",
"source": "./skills/add-javadoc",
"description": "Add comprehensive JavaDoc documentation to Java classes and methods. Use when documenting Java code, adding API documentation, or improving code documentation.",
"category": "documentation",
"category": "code-quality",
"keywords": [
"javadoc",
"java",
Expand Down Expand Up @@ -52,7 +52,7 @@
"name": "spark-version-upgrade",
"source": "./skills/spark-version-upgrade",
"description": "Upgrade Apache Spark applications between major versions (2.x→3.x, 3.x→4.x). Covers build files, deprecated APIs, configuration changes, SQL/DataFrame updates, and test validation.",
"category": "development",
"category": "environment",
"keywords": [
"spark",
"upgrade",
Expand Down
94 changes: 47 additions & 47 deletions marketplaces/openhands-extensions.json

Large diffs are not rendered by default.

79 changes: 76 additions & 3 deletions scripts/build-skills-catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,62 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
const SKILLS_DIR = join(__dirname, "..", "skills");
const OUTPUT = join(SKILLS_DIR, "index.js");

const MARKETPLACES_DIR = join(__dirname, "..", "marketplaces");
const SKILL_SOURCE_PREFIX = "./skills/";

/**
* Categories for skill entries, consumed by the agent-canvas /skills facet rail.
*
* Distinct from the `category` on marketplace *plugin* entries, which serves Claude Code marketplace browsing and keeps its own values.
*/
export const SKILL_CATEGORY_IDS = [
"automations",
"environment",
"code-hosting",
"agent-authoring",
"code-quality",
"integrations",
"writing",
"design",
"other",
];

const FALLBACK_CATEGORY = "other";

/** Build a `skill directory name -> {category, file}` map from every manifest. */
export function buildCategoryMap(marketplacesDir) {
const map = new Map();

for (const filename of readdirSync(marketplacesDir).filter((f) => f.endsWith(".json")).sort()) {
const manifest = JSON.parse(readFileSync(join(marketplacesDir, filename), "utf-8"));

for (const entry of manifest.plugins ?? []) {
const source = entry.source ?? "";
if (!source.startsWith(SKILL_SOURCE_PREFIX)) continue;

const name = source.slice(SKILL_SOURCE_PREFIX.length);
const { category } = entry;

if (!SKILL_CATEGORY_IDS.includes(category)) {
throw new Error(
`${filename}: skill "${name}" has category "${category}", expected one of: ${SKILL_CATEGORY_IDS.join(", ")}`,
);
}

const existing = map.get(name);
if (existing && existing.category !== category) {
throw new Error(
`Conflicting categories for skill "${name}": ${existing.file} says "${existing.category}", ${filename} says "${category}"`,
);
}

map.set(name, { category, file: filename });
}
}

return map;
}

/** Minimal YAML frontmatter parser for the flat format used by SKILL.md. */
export function parseFrontmatter(raw) {
const result = {};
Expand Down Expand Up @@ -51,9 +107,15 @@ export function parseFrontmatter(raw) {
};
}

/** Build the catalog from SKILL.md files in the given directory. */
export function buildCatalog(skillsDir) {
/**
* Build the catalog from SKILL.md files in the given directory.
*
* Pass an isolated `marketplacesDir` when building from fixtures; the default reads this repo's real manifests.
*/
export function buildCatalog(skillsDir, marketplacesDir = MARKETPLACES_DIR) {
const entries = [];
const categories = buildCategoryMap(marketplacesDir);
const uncategorized = [];

for (const dir of readdirSync(skillsDir).sort()) {
const dirPath = join(skillsDir, dir);
Expand All @@ -71,16 +133,26 @@ export function buildCatalog(skillsDir) {
const fm = parseFrontmatter(parts[1]);
const body = parts.slice(2).join("---").trim();

const mapped = categories.get(dir);
if (!mapped) uncategorized.push(dir);

entries.push({
name: fm.name?.trim() || dir,
description: fm.description,
triggers: fm.triggers,
content: body,
category: mapped?.category ?? FALLBACK_CATEGORY,
...(fm.license ? { license: fm.license } : {}),
...(fm.compatibility ? { compatibility: fm.compatibility } : {}),
});
}

if (uncategorized.length > 0) {
console.warn(
`Warning: no marketplace entry, category defaults to "${FALLBACK_CATEGORY}": ${uncategorized.join(", ")}`,
);
}

return entries;
}

Expand All @@ -90,7 +162,8 @@ if (isMain) {
const entries = buildCatalog(SKILLS_DIR);

const source = `// Auto-generated by scripts/build-skills-catalog.mjs — do not edit.
// Source of truth: skills/*/SKILL.md
// Source of truth: skills/*/SKILL.md and marketplaces/*.json (category)
export const SKILL_CATEGORY_IDS = ${JSON.stringify(SKILL_CATEGORY_IDS)};
export const SKILLS_CATALOG = ${JSON.stringify(entries, null, 2)};
export default SKILLS_CATALOG;
`;
Expand Down
21 changes: 21 additions & 0 deletions skills/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
/**
* Categories for skill entries, consumed by the agent-canvas /skills facet rail.
*
* Sourced from the `category` field on marketplace entries whose `source` starts with `./skills/`.
* Distinct from the `category` on marketplace *plugin* entries, which serves Claude Code marketplace browsing.
*/
export type SkillCategoryId =
| "automations"
| "environment"
| "code-hosting"
| "agent-authoring"
| "code-quality"
| "integrations"
| "writing"
| "design"
| "other";

export const SKILL_CATEGORY_IDS: readonly SkillCategoryId[];

export interface SkillCatalogEntry {
name: string;
description: string;
triggers: string[];
content: string;
/** `"other"` when the skill has no marketplace entry. */
category: SkillCategoryId;
license?: string;
compatibility?: string;
}
Expand Down
Loading
Loading