npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

claude-plugin-catalog

v1.0.0

Published

Walk a Claude Code plugin tree, parse every SKILL.md into a structured catalog, and render MDX docs pages — with a tolerant YAML fallback for frontmatter that strict parsers reject.

Readme

claude-plugin-catalog

Reads a directory of Claude Code plugins, turns every SKILL.md into a plain JavaScript object, and renders those objects as MDX pages you can drop into a static site generator.

It exists because I kept writing the same 200 lines in every docs site that needed to document a plugin marketplace, and because the naive version of those 200 lines breaks on real input. Skill authors write frontmatter for humans, not for YAML parsers, and a docs build that dies on one malformed argument hint is not much use.

Install

npm install claude-plugin-catalog

Node 18 or newer. One runtime dependency, gray-matter.

Using it

import { buildCatalog, renderSkillMdx } from 'claude-plugin-catalog';
import { mkdirSync, writeFileSync } from 'node:fs';

const catalog = buildCatalog('/path/to/marketplace/plugins');

for (const plugin of catalog.plugins) {
  mkdirSync(`src/content/docs/skills/${plugin.name}`, { recursive: true });
  for (const skill of plugin.skills) {
    writeFileSync(
      `src/content/docs/skills/${plugin.name}/${skill.name}.mdx`,
      renderSkillMdx(skill)
    );
  }
}

The layout it expects under the plugins directory:

<plugin>/.claude-plugin/plugin.json   name, version, description
<plugin>/.mcp.json                    mcpServers keys
<plugin>/skills/<skill>/SKILL.md      frontmatter + prose
<plugin>/scripts/<skill>/             presence marks the skill "compound"
<plugin>/agents/*.md                  agent frontmatter

Everything in that list is optional. A plugin with no .mcp.json gets an empty mcpServers array, a missing plugin.json gives version: "unknown", and a skill directory without a SKILL.md is skipped rather than reported as an empty skill.

The frontmatter fallback

This is the part worth the install.

Skill authors write argument hints like this:

argument-hint: [--page=Name] [--module=a|b] [--severity=INFO|WARN]

YAML sees the leading [, decides it is reading a flow sequence, hits the second [, and throws can not read an implicit mapping pair. On the marketplace this was extracted from, six of fifty-seven skills failed strict parsing for exactly this reason. Under the obvious implementation that is a dead docs build, or six missing pages.

So parseSkill tries strict YAML first and falls back to a line-by-line key: value reader when strict parsing throws. The fallback recovers the hint verbatim, brackets and pipes intact, and sets looseFrontmatter: true on the result so you can tell the difference. It also emits a warning, through console.warn by default or through a warn function you pass in.

The fallback only understands single-line scalars. If a file that strict YAML already rejected also had a multi-line value, that value is lost. That is the intended trade.

There is a quieter version of the same problem. A hint with one group, [--severity=INFO|WARN], is valid flow-sequence syntax, so strict parsing succeeds and hands back ['--severity=INFO|WARN']. Stringify that and the brackets the author wrote are gone. When the strict parse returns a non-string for argument-hint, the raw line wins instead.

Phases

Skills can be grouped into phases for navigation. The grouping is yours, not the library's:

const phases = {
  Plan: ['scope-work', 'estimate'],
  Build: ['build-docs', 'tag-release'],
  Verify: ['audit-links'],
};

const catalog = buildCatalog(pluginsDir, { phases, defaultPhase: 'Unclassified' });

Leave phases out and every skill comes back Unclassified. No taxonomy is bundled, and nothing is read from disk at import time.

Templates

renderSkillMdx and renderAgentMdx ship a default layout, and both take body and frontmatter overrides so you are not stuck with it:

renderSkillMdx(skill, {
  descriptionLimit: 200,
  frontmatter: (s, { truncate }) => ({
    title: s.name,
    description: truncate(s.description, 200),
    sidebar_label: s.name,
    order: s.isCompound ? 0 : 1,
  }),
  body: (s, { escape }) => `# ${s.name}\n\n${escape(s.description)}`,
});

body may be a function or a literal string. frontmatter may be a function or an object. Quoting and escaping frontmatter values stays the library's job whatever you pass, so a custom template cannot accidentally emit YAML that will not parse. Escaping body prose is the template's job, which is what escape is for: MDX reads a bare < as a JSX tag and a bare { as an expression, and both will fail a build.

renderMdx({ frontmatter, body }) is available directly if you want the safety without either default layout.

What it deliberately gets right

Four things here are the whole reason this is a package instead of a snippet.

Nothing is read from disk when you import it. The version this was extracted from loaded its phase map with a top-level readFileSync of a file that sat outside the published tree. It worked in the repo and threw ENOENT the moment anyone installed it. The phase map is now an argument with an empty default, and there is a test that copies the source files somewhere with no siblings and imports them there.

Descriptions are truncated before they are escaped, not after. Escaping first turns a " into a two-character \", and a 160-character cut can land between those two characters. The surviving backslash escapes the closing quote and the frontmatter no longer parses. Both renderers go through one code path that truncates the raw text first, and both are tested against a quote sitting exactly on the boundary.

Titles are quoted. A skill named deploy: staging written as an unquoted YAML scalar is invalid frontmatter. Every value, title included, is quoted and escaped.

The gray-matter cache is bypassed. gray-matter keeps a global cache keyed on file content and stores the entry before it parses. When parsing throws, that leaves a cached entry holding empty data, and every later call with the same content returns it without throwing. The fallback never fires and the skill comes back blank. Two plugins shipping identical skill files is enough to hit it. Passing an options object opts out of the cache.

Output is also deterministic. Skills, agents and MCP server names are sorted, plugin order is the order you asked for, and generatedAt is null unless you set it, so regenerating docs produces a byte-identical diff when nothing changed.

API

  • buildCatalog(pluginsDir, pluginNames?, options?) — walk the tree. Omit pluginNames to discover every directory. Options: phases, defaultPhase, generatedAt, warn.
  • parseSkill(markdown, { plugin, warn }) — one SKILL.md to an object. Never throws.
  • classifyPhase(name, phaseMap?, { defaultPhase }) and createPhaseClassifier(phaseMap, options).
  • renderSkillMdx(skill, options?), renderAgentMdx(agent, options?), renderMdx({ frontmatter, body }).
  • escapeMdxBody(text), truncateScalar(text, limit), escapeYamlDoubleQuoted(text) for custom templates.
  • parseFrontmatterLoose(markdown) if you want the tolerant reader on its own.

Tests

npm test

Node's built-in runner, no test framework. Every behaviour described above has a test that fails if it regresses, including one that asserts strict YAML really does reject the sample argument hint, so the fallback test cannot quietly become a tautology.

License

MIT