[docs] Add OSS Skills Marketplace (#6752)

This commit is contained in:
Ebony Louis 2026-01-28 22:46:18 -05:00 committed by GitHub
parent 7ea38402a3
commit 4d49fd5423
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1590 additions and 5 deletions

View file

@ -8,6 +8,8 @@
.docusaurus
.cache-loader
static/goose-docs-map.md
static/skills-manifest.json
static/skills-data-zips/
# Misc
.DS_Store
@ -17,3 +19,4 @@ static/goose-docs-map.md
.env.production.local
npm-debug.log*
.tmp/

View file

@ -391,6 +391,10 @@ const config: Config = {
to: '/extensions',
label: 'Extensions',
},
{
to: '/skills',
label: 'Skills Marketplace',
},
{
to: '/recipe-generator',
label: 'Recipe Generator',

View file

@ -250,6 +250,7 @@
"resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.42.0.tgz",
"integrity": "sha512-NZR7yyHj2WzK6D5X8gn+/KOxPdzYEXOqVdSaK/biU8QfYUpUuEA0sCWg/XlO05tPVEcJelF/oLrrNY3UjRbOww==",
"license": "MIT",
"peer": true,
"dependencies": {
"@algolia/client-common": "5.42.0",
"@algolia/requester-browser-xhr": "5.42.0",
@ -387,6 +388,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@ -2176,6 +2178,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@ -2198,6 +2201,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@ -2307,6 +2311,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"license": "MIT",
"peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@ -2728,6 +2733,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"license": "MIT",
"peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@ -3688,6 +3694,7 @@
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz",
"integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@docusaurus/core": "3.9.2",
"@docusaurus/logger": "3.9.2",
@ -5032,6 +5039,7 @@
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz",
"integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/mdx": "^2.0.0"
},
@ -5366,6 +5374,7 @@
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
@ -5737,6 +5746,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -6087,6 +6097,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -6172,6 +6183,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@ -6217,6 +6229,7 @@
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.42.0.tgz",
"integrity": "sha512-X5+PtWc9EJIPafT/cj8ZG+6IU3cjRRnlHGtqMHK/9gsiupQbAyYlH5y7qt/FtsAhfX5AICHffZy69ZAsVrxWkQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@algolia/abtesting": "1.8.0",
"@algolia/client-abtesting": "5.42.0",
@ -6715,6 +6728,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@ -7643,6 +7657,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"license": "MIT",
"peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@ -7955,7 +7970,8 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/debounce": {
"version": "1.2.1",
@ -9040,6 +9056,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -13690,6 +13707,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -14260,6 +14278,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -15272,6 +15291,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"license": "MIT",
"peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@ -16089,6 +16109,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -16098,6 +16119,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@ -16170,6 +16192,7 @@
"resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz",
"integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/react": "*"
},
@ -16225,6 +16248,7 @@
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
@ -18196,7 +18220,8 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
"license": "0BSD",
"peer": true
},
"node_modules/turndown": {
"version": "7.2.2",
@ -18268,6 +18293,7 @@
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -18609,6 +18635,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -18816,6 +18843,7 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@ -19429,6 +19457,7 @@
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"devOptional": true,
"license": "ISC",
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
@ -19468,6 +19497,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View file

@ -5,7 +5,7 @@
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "node scripts/generate-docs-map.js && docusaurus build",
"build": "node scripts/generate-docs-map.js && node scripts/generate-skills-manifest.js && node scripts/generate-skills-zips.js && docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",

View file

@ -1,15 +1,19 @@
module.exports = function () {
return {
name: 'custom-webpack-loaders',
configureWebpack(config) {
configureWebpack(config, isServer, utils) {
// Add YAML loader
config.module.rules.push({
test: /\.ya?ml$/,
use: 'yaml-loader',
});
// Add .raw file loader
config.module.rules.push({
test: /\.raw$/,
type: 'asset/source',
});
return {};
},
};

View file

@ -0,0 +1,360 @@
/**
* Generate skills manifest from Agent-Skills repository
*
* This script clones the block/Agent-Skills repository and reads all SKILL.md files
* to generate a skills-manifest.json file that the frontend can fetch.
*
* It also supports external skills defined in a local external-skills.json file.
*
* Run this before building the documentation site.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const matter = require('gray-matter');
// Configuration
const AGENT_SKILLS_REPO = 'https://github.com/block/Agent-Skills.git';
const AGENT_SKILLS_REPO_URL = 'https://github.com/block/Agent-Skills';
const TEMP_DIR = path.join(__dirname, '..', '.tmp');
const CLONED_REPO_DIR = path.join(TEMP_DIR, 'agent-skills');
const MANIFEST_OUTPUT = path.join(__dirname, '..', 'static', 'skills-manifest.json');
const EXTERNAL_SKILLS_FILE = path.join(__dirname, '..', 'static', 'external-skills.json');
// Directories to skip when scanning for skills (not skill folders)
const SKIP_DIRS = ['.github', 'node_modules', '.git'];
/**
* Clone or update the Agent-Skills repository
*/
function cloneAgentSkillsRepo() {
console.log('[generate-skills-manifest] Fetching Agent-Skills repository...');
// Create temp directory if it doesn't exist
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true });
}
// Remove existing clone if present
if (fs.existsSync(CLONED_REPO_DIR)) {
console.log('[generate-skills-manifest] Removing existing clone...');
fs.rmSync(CLONED_REPO_DIR, { recursive: true, force: true });
}
// Shallow clone the repository
try {
execSync(`git clone --depth 1 ${AGENT_SKILLS_REPO} ${CLONED_REPO_DIR}`, {
stdio: 'pipe',
timeout: 60000 // 60 second timeout
});
console.log('[generate-skills-manifest] Successfully cloned Agent-Skills repository');
} catch (error) {
console.error('[generate-skills-manifest] ERROR: Failed to clone Agent-Skills repository');
console.error('[generate-skills-manifest] Error:', error.message);
throw new Error('Failed to fetch Agent-Skills repository. Build cannot continue.');
}
}
/**
* Clean up temporary files
*/
function cleanup() {
if (fs.existsSync(CLONED_REPO_DIR)) {
console.log('[generate-skills-manifest] Cleaning up temporary files...');
fs.rmSync(CLONED_REPO_DIR, { recursive: true, force: true });
}
}
/**
* Determine install method based on source configuration
*/
function determineInstallMethod(isExternal, sourceUrl) {
if (isExternal && sourceUrl) {
// External skill with a source URL
const simpleRepoPattern = /^https:\/\/github\.com\/[^\/]+\/[^\/]+\/?$/;
if (simpleRepoPattern.test(sourceUrl)) {
return 'npx-single';
}
return 'npx-multi';
}
// Official skill from Agent-Skills repo
return 'npx-multi';
}
/**
* Generate install command based on method and source
*/
function generateInstallCommand(skillId, isExternal, sourceUrl) {
if (isExternal && sourceUrl) {
const simpleRepoPattern = /^https:\/\/github\.com\/[^\/]+\/[^\/]+\/?$/;
if (simpleRepoPattern.test(sourceUrl)) {
const match = sourceUrl.match(/github\.com\/([^\/]+\/[^\/]+)/);
if (match) {
return `npx skills add ${match[1]}`;
}
}
return `npx skills add ${sourceUrl} --skill ${skillId}`;
}
// Official skill from Agent-Skills repo
return `npx skills add ${AGENT_SKILLS_REPO_URL} --skill ${skillId}`;
}
/**
* Generate view source URL for a skill
*/
function generateViewSourceUrl(skillId, isExternal, sourceUrl) {
if (isExternal && sourceUrl) {
return sourceUrl;
}
// Official skill from Agent-Skills repo
return `${AGENT_SKILLS_REPO_URL}/tree/main/${skillId}`;
}
/**
* Get supporting files in a skill directory (excluding SKILL.md)
*/
function getSupportingFiles(skillDir) {
const files = [];
function walkDir(dir, prefix = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
walkDir(fullPath, relativePath);
} else if (entry.name !== 'SKILL.md') {
files.push(relativePath);
}
}
}
walkDir(skillDir);
return files;
}
/**
* Determine the supporting files type based on file contents
* Returns: 'scripts' | 'templates' | 'multi-file' | 'none'
*/
function determineSupportingFilesType(supportingFiles) {
if (supportingFiles.length === 0) {
return 'none';
}
// Executable file extensions
const executableExtensions = ['.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd', '.py', '.rb', '.js', '.mjs', '.ts'];
// Template-like patterns (file names or extensions)
const templatePatterns = [
/\.template\./i,
/\.tmpl\./i,
/\.tpl\./i,
/template\./i,
/\.example\./i,
/\.sample\./i,
/\.skeleton\./i,
/\.stub\./i,
/\.j2$/i,
/\.jinja2?$/i,
/\.mustache$/i,
/\.hbs$/i,
/\.handlebars$/i,
/\.ejs$/i,
/\.erb$/i,
];
const hasExecutable = supportingFiles.some(file => {
const ext = path.extname(file).toLowerCase();
return executableExtensions.includes(ext);
});
if (hasExecutable) {
return 'scripts';
}
const hasTemplates = supportingFiles.some(file => {
return templatePatterns.some(pattern => pattern.test(file));
});
if (hasTemplates) {
return 'templates';
}
return 'multi-file';
}
/**
* Check if a directory contains a SKILL.md file (i.e., is a skill folder)
*/
function isSkillDirectory(dirPath) {
const skillMdPath = path.join(dirPath, 'SKILL.md');
return fs.existsSync(skillMdPath);
}
/**
* Process official skills from the cloned Agent-Skills repo
*/
function processOfficialSkills() {
const skills = [];
// Get all directories in the cloned repo
const entries = fs.readdirSync(CLONED_REPO_DIR, { withFileTypes: true });
for (const entry of entries) {
// Skip non-directories and special directories
if (!entry.isDirectory() || SKIP_DIRS.includes(entry.name)) {
continue;
}
const skillId = entry.name;
const skillDir = path.join(CLONED_REPO_DIR, skillId);
// Skip if not a skill directory (no SKILL.md)
if (!isSkillDirectory(skillDir)) {
continue;
}
const skillMdPath = path.join(skillDir, 'SKILL.md');
try {
const rawContent = fs.readFileSync(skillMdPath, 'utf8');
const parsed = matter(rawContent);
const frontmatter = parsed.data || {};
const content = parsed.content || '';
const supportingFiles = getSupportingFiles(skillDir);
const sourceUrl = frontmatter.source_url || frontmatter.sourceUrl;
const author = frontmatter.author;
const isCommunity = author && author.toLowerCase() !== 'goose';
const supportingFilesType = determineSupportingFilesType(supportingFiles);
const skill = {
id: skillId,
name: frontmatter.name || skillId,
description: frontmatter.description || 'No description provided.',
author,
version: frontmatter.version,
tags: Array.isArray(frontmatter.tags) ? frontmatter.tags : [],
sourceUrl, // Optional external source if skill references another repo
content,
hasSupporting: supportingFiles.length > 0,
supportingFiles,
supportingFilesType,
installMethod: determineInstallMethod(false, sourceUrl),
installCommand: generateInstallCommand(skillId, false, sourceUrl),
viewSourceUrl: generateViewSourceUrl(skillId, false, sourceUrl),
repoUrl: AGENT_SKILLS_REPO_URL,
isCommunity,
};
skills.push(skill);
console.log(`[generate-skills-manifest] Processed official skill: ${skillId}`);
} catch (error) {
console.error(`[generate-skills-manifest] Error processing ${skillId}:`, error.message);
}
}
return skills;
}
/**
* Process external skills from external-skills.json
*/
function processExternalSkills() {
const skills = [];
if (!fs.existsSync(EXTERNAL_SKILLS_FILE)) {
console.log('[generate-skills-manifest] No external-skills.json found, skipping external skills');
return skills;
}
try {
const externalData = JSON.parse(fs.readFileSync(EXTERNAL_SKILLS_FILE, 'utf8'));
const externalSkills = externalData.skills || [];
for (const extSkill of externalSkills) {
const skillId = extSkill.id;
const sourceUrl = extSkill.sourceUrl || extSkill.source_url;
const author = extSkill.author;
const isCommunity = author && author.toLowerCase() !== 'goose';
const skill = {
id: skillId,
name: extSkill.name || skillId,
description: extSkill.description || 'No description provided.',
author,
version: extSkill.version,
tags: Array.isArray(extSkill.tags) ? extSkill.tags : [],
sourceUrl,
content: extSkill.content || '', // External skills may not have content
hasSupporting: false,
supportingFiles: [],
supportingFilesType: 'none',
installMethod: determineInstallMethod(true, sourceUrl),
installCommand: generateInstallCommand(skillId, true, sourceUrl),
viewSourceUrl: generateViewSourceUrl(skillId, true, sourceUrl),
repoUrl: sourceUrl,
isCommunity,
};
skills.push(skill);
console.log(`[generate-skills-manifest] Processed external skill: ${skillId}`);
}
} catch (error) {
console.error('[generate-skills-manifest] Error processing external skills:', error.message);
}
return skills;
}
/**
* Main function to generate the manifest
*/
function generateManifest() {
console.log('[generate-skills-manifest] Starting...');
try {
// Clone the Agent-Skills repository
cloneAgentSkillsRepo();
// Process official skills from the cloned repo
const officialSkills = processOfficialSkills();
// Process external skills from local JSON file
const externalSkills = processExternalSkills();
// Combine all skills
const allSkills = [...officialSkills, ...externalSkills];
// Check if we have any skills
if (allSkills.length === 0) {
console.error('[generate-skills-manifest] ERROR: No skills found. Build cannot continue.');
throw new Error('No skills found in Agent-Skills repository.');
}
// Generate manifest
const manifest = {
skills: allSkills,
generatedAt: new Date().toISOString(),
count: allSkills.length,
officialCount: officialSkills.length,
externalCount: externalSkills.length,
sourceRepo: AGENT_SKILLS_REPO_URL,
};
// Write manifest
fs.writeFileSync(MANIFEST_OUTPUT, JSON.stringify(manifest, null, 2));
console.log(`[generate-skills-manifest] Generated manifest with ${allSkills.length} skills (${officialSkills.length} official, ${externalSkills.length} external): ${MANIFEST_OUTPUT}`);
} finally {
// Always clean up
cleanup();
}
}
// Run the script
generateManifest();

View file

@ -0,0 +1,130 @@
/**
* Generate ZIP files for skills from Agent-Skills repository
*
* This script creates ZIP files for each skill in the Agent-Skills repo
* and outputs them to static/skills-data-zips/<skillId>.zip
*
* Note: This script should run AFTER generate-skills-manifest.js
* because it relies on the cloned repo being present in .tmp/agent-skills
*
* Run this before building the documentation site.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Configuration - must match generate-skills-manifest.js
const AGENT_SKILLS_REPO = 'https://github.com/block/Agent-Skills.git';
const TEMP_DIR = path.join(__dirname, '..', '.tmp');
const CLONED_REPO_DIR = path.join(TEMP_DIR, 'agent-skills');
const ZIPS_OUTPUT_DIR = path.join(__dirname, '..', 'static', 'skills-data-zips');
// Directories to skip when scanning for skills (not skill folders)
const SKIP_DIRS = ['.github', 'node_modules', '.git'];
/**
* Clone the Agent-Skills repository if not already present
*/
function ensureRepoCloned() {
if (fs.existsSync(CLONED_REPO_DIR)) {
console.log('[generate-skills-zips] Agent-Skills repo already cloned');
return true;
}
console.log('[generate-skills-zips] Cloning Agent-Skills repository...');
// Create temp directory if it doesn't exist
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true });
}
try {
execSync(`git clone --depth 1 ${AGENT_SKILLS_REPO} ${CLONED_REPO_DIR}`, {
stdio: 'pipe',
timeout: 60000
});
console.log('[generate-skills-zips] Successfully cloned Agent-Skills repository');
return true;
} catch (error) {
console.error('[generate-skills-zips] ERROR: Failed to clone Agent-Skills repository');
console.error('[generate-skills-zips] Error:', error.message);
return false;
}
}
/**
* Check if a directory contains a SKILL.md file (i.e., is a skill folder)
*/
function isSkillDirectory(dirPath) {
const skillMdPath = path.join(dirPath, 'SKILL.md');
return fs.existsSync(skillMdPath);
}
/**
* Clean up temporary files
*/
function cleanup() {
if (fs.existsSync(CLONED_REPO_DIR)) {
console.log('[generate-skills-zips] Cleaning up temporary files...');
fs.rmSync(CLONED_REPO_DIR, { recursive: true, force: true });
}
}
function generateSkillZips() {
console.log('[generate-skills-zips] Starting...');
// Ensure repo is cloned
if (!ensureRepoCloned()) {
console.error('[generate-skills-zips] Cannot generate ZIPs without Agent-Skills repo');
process.exit(1);
}
// Create output directory if it doesn't exist
if (!fs.existsSync(ZIPS_OUTPUT_DIR)) {
fs.mkdirSync(ZIPS_OUTPUT_DIR, { recursive: true });
console.log(`[generate-skills-zips] Created output directory: ${ZIPS_OUTPUT_DIR}`);
}
// Clean existing ZIPs
const existingZips = fs.readdirSync(ZIPS_OUTPUT_DIR).filter(f => f.endsWith('.zip'));
for (const zip of existingZips) {
fs.unlinkSync(path.join(ZIPS_OUTPUT_DIR, zip));
}
console.log(`[generate-skills-zips] Cleaned ${existingZips.length} existing ZIP files`);
// Get all skill directories from the cloned repo
const entries = fs.readdirSync(CLONED_REPO_DIR, { withFileTypes: true });
const skillDirs = entries
.filter(d => d.isDirectory() && !SKIP_DIRS.includes(d.name))
.map(d => d.name)
.filter(name => isSkillDirectory(path.join(CLONED_REPO_DIR, name)));
let generatedCount = 0;
for (const skillId of skillDirs) {
const skillDir = path.join(CLONED_REPO_DIR, skillId);
const zipPath = path.join(ZIPS_OUTPUT_DIR, `${skillId}.zip`);
try {
// Use the system zip command to create the archive
// cd into the cloned repo and zip the skill folder to preserve the folder name
execSync(`cd "${CLONED_REPO_DIR}" && zip -r "${zipPath}" "${skillId}"`, {
stdio: 'pipe'
});
const stats = fs.statSync(zipPath);
console.log(`[generate-skills-zips] Created: ${skillId}.zip (${(stats.size / 1024).toFixed(1)} KB)`);
generatedCount++;
} catch (error) {
console.error(`[generate-skills-zips] Error creating ZIP for ${skillId}:`, error.message);
}
}
console.log(`[generate-skills-zips] Generated ${generatedCount} ZIP files in ${ZIPS_OUTPUT_DIR}`);
// Clean up the cloned repo
cleanup();
}
generateSkillZips();

View file

@ -0,0 +1,137 @@
import React, { useState } from "react";
import Link from "@docusaurus/Link";
import { Check } from "lucide-react";
import type { Skill } from "@site/src/pages/skills/types";
function generateInstallCommand(repoUrl: string, skillId: string): string {
return `npx skills add ${repoUrl} --skill ${skillId}`;
}
export function SkillCard({ skill }: { skill: Skill }) {
const [copied, setCopied] = useState(false);
const handleCopyInstall = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const command = generateInstallCommand(skill.repoUrl, skill.id);
navigator.clipboard.writeText(command);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="relative w-full h-full">
<Link
to={`/skills/detail?id=${skill.id}`}
className="block no-underline hover:no-underline h-full"
>
<div className="absolute inset-0 rounded-2xl bg-purple-500 opacity-10 blur-2xl" />
<div className="relative z-10 w-full h-full rounded-2xl border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-[#1A1A1A] flex flex-col justify-between p-6 transition-shadow duration-200 ease-in-out hover:shadow-[0_0_0_2px_rgba(99,102,241,0.4),_0_4px_20px_rgba(99,102,241,0.1)]">
<div className="space-y-4">
{/* Header with name and badges */}
<div className="flex items-start justify-between gap-2">
<h3 className="font-semibold text-base text-zinc-900 dark:text-white leading-snug">
{skill.name}
</h3>
<div className="flex gap-2 flex-shrink-0">
{skill.isCommunity && (
<span className="inline-flex items-center h-6 px-2 rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 text-xs font-medium border border-yellow-200 dark:border-yellow-800">
Community
</span>
)}
{skill.version && (
<span className="inline-flex items-center h-6 px-2 rounded-full bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 text-xs font-medium">
v{skill.version}
</span>
)}
</div>
</div>
{/* Description */}
<p className="text-sm text-zinc-600 dark:text-zinc-400">
{skill.description}
</p>
{/* Tags */}
{skill.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{skill.tags.map((tag, index) => (
<span
key={index}
className="inline-flex items-center h-7 px-3 rounded-full border border-zinc-300 bg-zinc-100 text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 text-xs font-medium"
>
{tag}
</span>
))}
</div>
)}
{/* Supporting files indicator */}
{skill.supportingFilesType === 'scripts' && (
<div className="text-xs text-zinc-500 dark:text-zinc-500">
Runs scripts
</div>
)}
{skill.supportingFilesType === 'templates' && (
<div className="text-xs text-zinc-500 dark:text-zinc-500">
📄 Includes templates
</div>
)}
{skill.supportingFilesType === 'multi-file' && (
<div className="text-xs text-zinc-500 dark:text-zinc-500">
📁 Multi-file skill
</div>
)}
</div>
{/* Footer with actions */}
<div className="flex justify-between items-center pt-6 mt-2 border-t border-zinc-100 dark:border-zinc-800">
{/* Install button */}
<div className="relative group">
<button
onClick={handleCopyInstall}
className={`text-sm font-medium px-3 py-1 rounded cursor-pointer flex items-center gap-1.5 transition-colors ${
copied
? "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300"
: "text-zinc-700 bg-zinc-200 dark:bg-zinc-700 dark:text-white dark:hover:bg-zinc-600 hover:bg-zinc-300"
}`}
>
{copied ? (
<>
<Check className="h-3.5 w-3.5" />
Copied!
</>
) : (
"Copy Install"
)}
</button>
</div>
{/* View Source link - always show, links to Agent-Skills repo */}
<a
href={skill.viewSourceUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-purple-600 hover:underline dark:text-purple-400"
onClick={(e) => e.stopPropagation()}
>
View Source
</a>
{/* Author */}
{skill.author && (
<span className="text-sm text-zinc-500 dark:text-zinc-400">
by {skill.author}
</span>
)}
</div>
</div>
</Link>
</div>
);
}
export type { Skill };

View file

@ -0,0 +1,316 @@
import Layout from "@theme/Layout";
import { ArrowLeft, Download, Copy, ExternalLink, FileText, Check } from "lucide-react";
import { useLocation } from "@docusaurus/router";
import { useEffect, useState } from "react";
import Link from "@docusaurus/Link";
import CodeBlock from "@theme/CodeBlock";
import { Button } from "@site/src/components/ui/button";
import { getSkillById } from "@site/src/utils/skills";
import type { Skill } from "@site/src/pages/skills/types";
import ReactMarkdown from "react-markdown";
type PackageManager = 'npx' | 'pnpm' | 'bun';
const PACKAGE_MANAGERS: { id: PackageManager; label: string; prefix: string }[] = [
{ id: 'npx', label: 'npx', prefix: 'npx' },
{ id: 'pnpm', label: 'pnpm', prefix: 'pnpm dlx' },
{ id: 'bun', label: 'bun', prefix: 'bunx' },
];
function generateInstallCommand(repoUrl: string, skillId: string, packageManager: PackageManager): string {
const prefix = PACKAGE_MANAGERS.find(pm => pm.id === packageManager)?.prefix || 'npx';
return `${prefix} skills add ${repoUrl} --skill ${skillId}`;
}
export default function SkillDetailPage(): JSX.Element {
const location = useLocation();
const [skill, setSkill] = useState<Skill | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedPM, setSelectedPM] = useState<PackageManager>('npx');
const [copied, setCopied] = useState(false);
useEffect(() => {
const loadSkill = async () => {
try {
setLoading(true);
setError(null);
const params = new URLSearchParams(location.search);
const id = params.get("id");
if (!id) {
setError("No skill ID provided");
return;
}
const skillData = getSkillById(id);
if (skillData) {
setSkill(skillData);
} else {
setError("Skill not found");
}
} catch (err) {
setError("Failed to load skill details");
console.error(err);
} finally {
setLoading(false);
}
};
loadSkill();
}, [location]);
const handleCopyInstall = () => {
if (skill) {
const command = generateInstallCommand(skill.repoUrl, skill.id, selectedPM);
navigator.clipboard.writeText(command);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleDownload = () => {
if (skill) {
const zipUrl = `/goose/skills-data-zips/${skill.id}.zip`;
const link = document.createElement('a');
link.href = zipUrl;
link.download = `${skill.id}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
if (loading) {
return (
<Layout>
<div className="min-h-screen flex items-start justify-center py-16">
<div className="container max-w-5xl mx-auto px-4 animate-pulse">
<div className="h-12 w-48 bg-bgSubtle dark:bg-zinc-800 rounded-lg mb-4"></div>
<div className="h-6 w-full bg-bgSubtle dark:bg-zinc-800 rounded-lg mb-2"></div>
<div className="h-6 w-2/3 bg-bgSubtle dark:bg-zinc-800 rounded-lg"></div>
</div>
</div>
</Layout>
);
}
if (error || !skill) {
return (
<Layout>
<div className="min-h-screen flex items-start justify-center py-16">
<div className="container max-w-5xl mx-auto px-4 text-red-500">
{error || "Skill not found"}
</div>
</div>
</Layout>
);
}
const currentCommand = generateInstallCommand(skill.repoUrl, skill.id, selectedPM);
return (
<Layout
title={skill.name}
description={skill.description}
>
<div className="min-h-screen py-12">
<div className="max-w-4xl mx-auto px-4">
{/* Header */}
<div className="mb-8 flex justify-between items-start">
<Link to="/skills">
<Button className="flex items-center gap-2">
<ArrowLeft className="h-4 w-4" />
Back to Skills
</Button>
</Link>
{skill.author && (
<span className="text-sm text-textSubtle">
by {skill.author}
</span>
)}
</div>
<div className="bg-white dark:bg-[#1A1A1A] border border-borderSubtle dark:border-zinc-700 rounded-xl p-8 shadow-md">
{/* Title and badges */}
<div className="flex items-start justify-between gap-4 mb-4">
<h1 className="text-4xl font-semibold text-textProminent dark:text-white">
{skill.name}
</h1>
<div className="flex gap-2 flex-shrink-0">
{skill.isCommunity && (
<span className="inline-flex items-center h-7 px-3 rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 text-sm font-medium border border-yellow-200 dark:border-yellow-800">
Community
</span>
)}
{skill.version && (
<span className="inline-flex items-center h-7 px-3 rounded-full bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 text-sm font-medium">
v{skill.version}
</span>
)}
</div>
</div>
{/* Description */}
<p className="text-textSubtle dark:text-zinc-400 text-lg mb-6">
{skill.description}
</p>
{/* Tags */}
{skill.tags.length > 0 && (
<div className="mb-6">
<div className="flex flex-wrap gap-2">
{skill.tags.map((tag, index) => (
<Link
key={index}
to={`/skills?tag=${tag}`}
className="inline-flex items-center h-7 px-3 rounded-full border border-zinc-300 bg-zinc-100 text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 text-xs font-medium hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors no-underline"
>
{tag}
</Link>
))}
</div>
</div>
)}
{/* Install section with tabs */}
<div className="mb-6 p-4 bg-zinc-50 dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-700">
<h2 className="text-lg font-medium mb-3 text-textProminent dark:text-white flex items-center gap-2">
<Download className="h-5 w-5" />
Install
</h2>
{/* Package manager tabs */}
<div className="flex gap-1 mb-3 border-b border-zinc-200 dark:border-zinc-700">
{PACKAGE_MANAGERS.map((pm) => (
<button
key={pm.id}
onClick={() => setSelectedPM(pm.id)}
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
selectedPM === pm.id
? 'text-purple-600 dark:text-purple-400'
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300'
}`}
>
{pm.label}
{selectedPM === pm.id && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-purple-600 dark:bg-purple-400" />
)}
</button>
))}
</div>
{/* Install command */}
<div className="flex items-center gap-2 mb-2">
<code className="flex-1 bg-zinc-200 dark:bg-zinc-800 px-3 py-2 rounded text-sm font-mono text-zinc-800 dark:text-zinc-200 overflow-x-auto">
{currentCommand}
</code>
<Button
onClick={handleCopyInstall}
className={`flex items-center gap-2 flex-shrink-0 transition-colors ${
copied
? "bg-green-600 hover:bg-green-700 text-white"
: "bg-purple-600 hover:bg-purple-700 text-white"
}`}
>
{copied ? (
<>
<Check className="h-4 w-4" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4" />
Copy
</>
)}
</Button>
</div>
<p className="text-xs text-zinc-500 dark:text-zinc-500">
Requires <a href="/docs/guides/using-extensions#goose-skills" className="text-purple-600 hover:underline">Goose Skills extension</a> enabled
</p>
</div>
{/* ZIP Download - secondary option */}
<div className="mb-6 flex items-center gap-3 text-sm">
<span className="text-zinc-500 dark:text-zinc-400">Prefer manual install?</span>
<button
onClick={handleDownload}
className="inline-flex items-center gap-1.5 text-zinc-600 dark:text-zinc-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
>
<Download className="h-4 w-4" />
Download ZIP
</button>
</div>
{/* View Source - always show, links to Agent-Skills repo */}
<div className="mb-6">
<a
href={skill.viewSourceUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-purple-600 hover:underline dark:text-purple-400"
>
<ExternalLink className="h-4 w-4" />
View Source on GitHub
</a>
</div>
{/* Supporting files */}
{skill.hasSupporting && skill.supportingFiles.length > 0 && (
<div className="mb-6 border-t border-borderSubtle dark:border-zinc-700 pt-6">
<h2 className="text-xl font-medium mb-3 text-textProminent dark:text-white flex items-center gap-2">
<FileText className="h-5 w-5" />
Supporting Files
</h2>
<p className="text-sm text-zinc-600 dark:text-zinc-400 mb-3">
This skill includes additional files that will be installed with it:
</p>
<ul className="list-disc list-inside text-sm text-zinc-600 dark:text-zinc-400 space-y-1">
{skill.supportingFiles.map((file, index) => (
<li key={index}>
<code className="bg-zinc-100 dark:bg-zinc-800 px-1 rounded">{file}</code>
</li>
))}
</ul>
</div>
)}
{/* Skill content (markdown) */}
<div className="border-t border-borderSubtle dark:border-zinc-700 pt-6">
<h2 className="text-2xl font-medium mb-4 text-textProminent dark:text-white">
Skill Instructions
</h2>
<div className="prose prose-zinc dark:prose-invert max-w-none">
<ReactMarkdown
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
if (!inline && match) {
return (
<CodeBlock language={match[1]}>
{String(children).replace(/\n$/, '')}
</CodeBlock>
);
}
return (
<code className={className} {...props}>
{children}
</code>
);
},
h1({ children }) {
return <h2 className="text-2xl font-semibold mt-6 mb-4">{children}</h2>;
},
}}
>
{skill.content}
</ReactMarkdown>
</div>
</div>
</div>
</div>
</div>
</Layout>
);
}

View file

@ -0,0 +1,223 @@
import { SkillCard } from "@site/src/components/skill-card";
import { searchSkills, getAllTags } from "@site/src/utils/skills";
import type { Skill } from "@site/src/pages/skills/types";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import Layout from "@theme/Layout";
import Admonition from "@theme/Admonition";
import { Button } from "@site/src/components/ui/button";
import { SidebarFilter, type SidebarFilterGroup } from "@site/src/components/ui/sidebar-filter";
import { Menu, X } from "lucide-react";
import Link from '@docusaurus/Link';
export default function SkillsPage() {
const [skills, setSkills] = useState<Skill[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [selectedFilters, setSelectedFilters] = useState<Record<string, string[]>>({});
const [isMobileFilterOpen, setIsMobileFilterOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const skillsPerPage = 10;
// Build tag filter options from loaded skills
const uniqueTags = Array.from(
new Set(
skills.flatMap((s) => s.tags || [])
)
).sort().map((tag) => ({
label: tag.charAt(0).toUpperCase() + tag.slice(1),
value: tag
}));
// Build source filter options (Community only - official is the default)
const sourceOptions = [
{ label: "Community", value: "community" }
];
const sidebarFilterGroups: SidebarFilterGroup[] = [
{
title: "Tags",
options: uniqueTags
},
{
title: "Source",
options: sourceOptions
}
];
useEffect(() => {
const loadSkills = async () => {
try {
setIsLoading(true);
setError(null);
const results = await searchSkills(searchQuery);
setSkills(results);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Unknown error";
setError(`Failed to load skills: ${errorMessage}`);
console.error("Error loading skills:", err);
} finally {
setIsLoading(false);
}
};
const timeoutId = setTimeout(loadSkills, 300);
return () => clearTimeout(timeoutId);
}, [searchQuery]);
// Apply filters
let filteredSkills = skills;
Object.entries(selectedFilters).forEach(([group, values]) => {
if (values.length > 0) {
filteredSkills = filteredSkills.filter((skill) => {
if (group === "Tags") {
return skill.tags?.some((tag) => values.includes(tag)) ?? false;
}
if (group === "Source") {
// Use isCommunity field from manifest (true if author is not "goose")
const isCommunity = skill.isCommunity ?? false;
if (values.includes("community")) return isCommunity;
return true;
}
return true;
});
}
});
return (
<Layout
title="Skills Marketplace"
description="Browse and install community-contributed skills for goose"
>
<div className="container mx-auto px-4 py-8 md:p-24">
<div className="pb-8 md:pb-16">
<div className="flex justify-between items-start mb-4">
<h1 className="text-4xl md:text-[64px] font-medium text-textProminent">
Skills Marketplace
</h1>
<Button
onClick={() => window.open('https://github.com/block/Agent-Skills?tab=readme-ov-file#contributing-a-skill', '_blank')}
className="bg-purple-600 hover:bg-purple-700 text-white flex items-center gap-2 cursor-pointer"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 5v14M5 12h14"/>
</svg>
Submit Skill
</Button>
</div>
<p className="text-textProminent">
Browse community-contributed{" "}
<Link to="/docs/guides/context-engineering/using-skills" className="text-purple-600 hover:underline">
skills
</Link>{" "}
that teach goose how to perform specific tasks. Skills are reusable instruction sets with optional supporting files.
</p>
</div>
<div className="search-container mb-6 md:mb-8">
<input
className="bg-bgApp font-light text-textProminent placeholder-textPlaceholder w-full px-3 py-2 md:py-3 text-2xl md:text-[40px] leading-tight md:leading-[52px] border-b border-borderSubtle focus:outline-none focus:ring-purple-500 focus:border-borderProminent caret-[#FF4F00] pl-0"
placeholder="Search skills by name, description, or tag"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setCurrentPage(1);
}}
/>
</div>
<div className="md:hidden mb-4">
<Button onClick={() => setIsMobileFilterOpen(!isMobileFilterOpen)}>
{isMobileFilterOpen ? <X size={20} /> : <Menu size={20} />}
{isMobileFilterOpen ? "Close Filters" : "Show Filters"}
</Button>
</div>
<div className="flex flex-col md:flex-row gap-8">
<div className={`${isMobileFilterOpen ? "block" : "hidden"} md:block md:w-64 mt-6`}>
<SidebarFilter
groups={sidebarFilterGroups}
selectedValues={selectedFilters}
onChange={(group, values) => {
setSelectedFilters(prev => ({ ...prev, [group]: values }));
setCurrentPage(1);
}}
/>
</div>
<div className="flex-1">
<div className={`${searchQuery ? "pb-2" : "pb-4 md:pb-8"}`}>
<p className="text-gray-600">
{searchQuery
? `${filteredSkills.length} result${filteredSkills.length !== 1 ? "s" : ""} for "${searchQuery}"`
: `${filteredSkills.length} skill${filteredSkills.length !== 1 ? "s" : ""} available`}
</p>
</div>
{error && (
<Admonition type="danger" title="Error">
<p>{error}</p>
</Admonition>
)}
{isLoading ? (
<div className="py-8 text-xl text-gray-600">Loading skills...</div>
) : filteredSkills.length === 0 ? (
<Admonition type="info">
<p>
{searchQuery
? "No skills found matching your search."
: "No skills have been submitted yet."}
</p>
</Admonition>
) : (
<>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
{filteredSkills
.slice((currentPage - 1) * skillsPerPage, currentPage * skillsPerPage)
.map((skill) => (
<motion.div
key={skill.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.6 }}
>
<SkillCard skill={skill} />
</motion.div>
))}
</div>
{filteredSkills.length > skillsPerPage && (
<div className="flex justify-center items-center gap-2 md:gap-4 mt-6 md:mt-8">
<Button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="px-3 md:px-4 py-2 rounded-md border border-border bg-surfaceHighlight hover:bg-surface text-textProminent disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm md:text-base"
>
Previous
</Button>
<span className="text-textProminent text-sm md:text-base">
Page {currentPage} of {Math.ceil(filteredSkills.length / skillsPerPage)}
</span>
<Button
onClick={() => setCurrentPage(prev => Math.min(Math.ceil(filteredSkills.length / skillsPerPage), prev + 1))}
disabled={currentPage >= Math.ceil(filteredSkills.length / skillsPerPage)}
className="px-3 md:px-4 py-2 rounded-md border border-border bg-surfaceHighlight hover:bg-surface text-textProminent disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm md:text-base"
>
Next
</Button>
</div>
)}
</>
)}
</div>
</div>
</div>
</Layout>
);
}

View file

@ -0,0 +1,125 @@
import React from 'react';
import Layout from '@docusaurus/theme-classic/lib/theme/Layout';
import CodeBlock from '@docusaurus/theme-classic/lib/theme/CodeBlock';
/**
* Skill status indicator
*/
export type SkillStatus = 'experimental' | 'stable';
/**
* Install method for a skill
* - 'npx-single': npx skills add <owner>/<repo>
* - 'npx-multi': npx skills add <url> --skill <name>
* - 'download': No repo, show download button
*/
export type SkillInstallMethod = 'npx-single' | 'npx-multi' | 'download';
/**
* Supporting files type - indicates what kind of extra files the skill includes
* - 'scripts': Contains executable files (.sh, .py, .js, etc.)
* - 'templates': Contains template files (.template., .example., etc.)
* - 'multi-file': Contains other supporting files
* - 'none': No supporting files
*/
export type SupportingFilesType = 'scripts' | 'templates' | 'multi-file' | 'none';
/**
* Skill type definition
*/
export type Skill = {
id: string; // Derived from directory name
name: string; // From frontmatter (required)
description: string; // From frontmatter (required)
author?: string; // From frontmatter
version?: string; // From frontmatter
status: SkillStatus; // From frontmatter (default: 'stable')
tags: string[]; // From frontmatter (default: [])
sourceUrl?: string; // From frontmatter - optional external source URL
content: string; // Markdown content after frontmatter
hasSupporting: boolean; // Computed: has files beyond SKILL.md
supportingFiles: string[]; // Computed: list of supporting file paths
supportingFilesType: SupportingFilesType; // Computed: type of supporting files
installMethod: SkillInstallMethod; // Computed based on source
installCommand?: string; // Computed: npx command
viewSourceUrl: string; // Computed: GitHub link to skill source
repoUrl: string; // Repository URL (Agent-Skills for official, sourceUrl for external)
isCommunity: boolean; // True if author is not "goose" (community-contributed)
};
/**
* Filter group for sidebar
*/
export type SkillFilterGroup = {
title: string;
options: { label: string; value: string; count?: number }[];
};
/**
* Types documentation page
*/
const SkillTypes: React.FC = () => {
return (
<Layout title="Skill Types" description="Type definitions for the Skills Marketplace">
<div className="container margin-vert--lg">
<h1>Skill Type Definitions</h1>
<p>This page contains the type definitions used in the Skills Marketplace.</p>
<h2>Skill Status</h2>
<CodeBlock language="typescript">
{`type SkillStatus = 'experimental' | 'stable';`}
</CodeBlock>
<h2>Skill Install Method</h2>
<CodeBlock language="typescript">
{`// Install method for a skill
// - 'npx-single': npx skills add <owner>/<repo>
// - 'npx-multi': npx skills add <url> --skill <name>
// - 'download': No repo, show download button
type SkillInstallMethod = 'npx-single' | 'npx-multi' | 'download';`}
</CodeBlock>
<h2>Skill</h2>
<CodeBlock language="typescript">
{`type Skill = {
id: string; // Derived from directory name
name: string; // From frontmatter (required)
description: string; // From frontmatter (required)
author?: string; // From frontmatter
version?: string; // From frontmatter
status: SkillStatus; // From frontmatter (default: 'stable')
tags: string[]; // From frontmatter (default: [])
sourceUrl?: string; // From frontmatter - optional external source URL
content: string; // Markdown content after frontmatter
hasSupporting: boolean; // Computed: has files beyond SKILL.md
supportingFiles: string[]; // Computed: list of supporting file paths
installMethod: SkillInstallMethod; // Computed based on source
installCommand?: string; // Computed: npx command
viewSourceUrl: string; // Computed: GitHub link to skill source
repoUrl: string; // Repository URL (Agent-Skills for official, sourceUrl for external)
isCommunity: boolean; // True if author is not "goose" (community-contributed)
};`}
</CodeBlock>
<h2>SKILL.md Frontmatter Schema</h2>
<CodeBlock language="yaml">
{`---
# Required fields
name: string # Skill identifier
description: string # Brief description (1-2 sentences)
# Optional fields
author: string # Author name or GitHub handle
version: string # Semantic version (e.g., "1.0")
status: experimental | stable # Development status (default: stable)
tags: # Array of category tags
- string
source_url: string # GitHub repo URL for npx install
---`}
</CodeBlock>
</div>
</Layout>
);
};
export default SkillTypes;

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import type { ReactNode } from 'react';
interface Props {
@ -8,6 +8,13 @@ interface Props {
const SHOW_BANNER = false;
export default function Root({ children }: Props): JSX.Element {
// Initialize gtag as no-op if not present (prevents errors in development)
useEffect(() => {
if (typeof window !== 'undefined' && !window.gtag) {
(window as any).gtag = function() {};
}
}, []);
return (
<>
{SHOW_BANNER && (

View file

@ -0,0 +1,246 @@
import type { Skill, SkillStatus, SkillInstallMethod, SupportingFilesType } from "@site/src/pages/skills/types";
import siteConfig from "@generated/docusaurus.config";
// Skills data is loaded from a generated JSON manifest at build time
// Generated at: documentation/static/skills-manifest.json
// Cache for loaded skills
let skillsCache: Skill[] | null = null;
let skillsPromise: Promise<Skill[]> | null = null;
/**
* Get a skill by its ID
*/
export function getSkillById(id: string): Skill | null {
const allSkills = loadAllSkillsSync();
return allSkills.find((skill) => skill.id === id) || null;
}
/**
* Search skills by query string
* Searches name, description, and tags
*/
export async function searchSkills(query: string): Promise<Skill[]> {
const allSkills = await loadAllSkills();
if (!query) return allSkills;
const lowerQuery = query.toLowerCase();
return allSkills.filter(
(skill) =>
skill.name?.toLowerCase().includes(lowerQuery) ||
skill.description?.toLowerCase().includes(lowerQuery) ||
skill.tags?.some((tag) => tag.toLowerCase().includes(lowerQuery))
);
}
/**
* Load all skills - async version that fetches from manifest
*/
export async function loadAllSkills(): Promise<Skill[]> {
// Never fetch/cache during SSR (prevents "empty list" getting locked in on preview)
if (typeof window === "undefined") return [];
if (skillsCache) return skillsCache;
if (skillsPromise) return skillsPromise;
skillsPromise = fetchSkillsManifest();
const skills = await skillsPromise;
// Only cache if we actually got data (avoid caching [] due to a transient 404)
if (skills.length > 0) skillsCache = skills;
return skills;
}
/**
* Load all skills synchronously (uses cache, returns empty if not loaded)
*/
export function loadAllSkillsSync(): Skill[] {
if (skillsCache) return skillsCache;
// Trigger async load on client
if (typeof window !== "undefined") {
void loadAllSkills();
}
return [];
}
/**
* Fetch skills manifest from static files
*/
async function fetchSkillsManifest(): Promise<Skill[]> {
try {
// In Docusaurus, baseUrl changes automatically for PR previews.
// Example:
// prod: /goose/
// PR preview: /goose/pr-preview/pr-6752/
const baseUrl = siteConfig.baseUrl.endsWith("/")
? siteConfig.baseUrl
: `${siteConfig.baseUrl}/`;
const manifestUrl = `${baseUrl}skills-manifest.json`;
const response = await fetch(manifestUrl);
if (!response.ok) {
console.error("Failed to fetch skills manifest:", response.status, manifestUrl);
return [];
}
const manifest = await response.json();
return manifest.skills || [];
} catch (error) {
console.error("Error loading skills manifest:", error);
return [];
}
}
/**
* Normalize raw frontmatter-like data to Skill type
* (kept here in case you reuse it elsewhere)
*/
export function normalizeSkill(
parsed: { frontmatter: Record<string, any>; content: string },
id: string,
supportingFiles: string[]
): Skill {
const { frontmatter, content } = parsed;
const sourceUrl = frontmatter.source_url || frontmatter.sourceUrl;
const repoUrl = frontmatter.repo_url || frontmatter.repoUrl || sourceUrl;
const author = frontmatter.author;
const isCommunity = !!author && author.toLowerCase() !== "goose";
const installMethod = determineInstallMethod(sourceUrl, id);
const installCommand = generateInstallCommand(sourceUrl, id, installMethod);
const supportingFilesType = determineSupportingFilesType(supportingFiles);
return {
id,
name: frontmatter.name || id,
description: frontmatter.description || "No description provided.",
author,
version: frontmatter.version,
status: (frontmatter.status as SkillStatus) || "stable",
tags: Array.isArray(frontmatter.tags) ? frontmatter.tags : [],
sourceUrl,
repoUrl,
isCommunity,
content,
hasSupporting: supportingFiles.length > 0,
supportingFiles,
supportingFilesType,
installMethod,
installCommand,
viewSourceUrl: generateViewSourceUrl(id),
};
}
/**
* Determine the supporting files type based on file contents
*/
function determineSupportingFilesType(supportingFiles: string[]): SupportingFilesType {
if (supportingFiles.length === 0) {
return 'none';
}
// Executable file extensions
const executableExtensions = ['.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd', '.py', '.rb', '.js', '.mjs', '.ts'];
// Template-like patterns
const templatePatterns = [
/\.template\./i,
/\.tmpl\./i,
/\.tpl\./i,
/template\./i,
/\.example\./i,
/\.sample\./i,
/\.skeleton\./i,
/\.stub\./i,
/\.j2$/i,
/\.jinja2?$/i,
/\.mustache$/i,
/\.hbs$/i,
/\.handlebars$/i,
/\.ejs$/i,
/\.erb$/i,
];
const hasExecutable = supportingFiles.some(file => {
const ext = file.substring(file.lastIndexOf('.')).toLowerCase();
return executableExtensions.includes(ext);
});
if (hasExecutable) {
return 'scripts';
}
const hasTemplates = supportingFiles.some(file => {
return templatePatterns.some(pattern => pattern.test(file));
});
if (hasTemplates) {
return 'templates';
}
return 'multi-file';
}
/**
* Determine the install method based on source URL
*/
function determineInstallMethod(sourceUrl: string | undefined, skillId: string): SkillInstallMethod {
if (!sourceUrl) return "download";
if (sourceUrl.includes("block/goose")) return "npx-multi";
const simpleRepoPattern = /^https:\/\/github\.com\/[^\/]+\/[^\/]+\/?$/;
if (simpleRepoPattern.test(sourceUrl)) return "npx-single";
return "npx-multi";
}
/**
* Generate the install command based on method
*/
function generateInstallCommand(
sourceUrl: string | undefined,
skillId: string,
method: SkillInstallMethod
): string | undefined {
if (method === "download" || !sourceUrl) return undefined;
if (method === "npx-single") {
const match = sourceUrl.match(/github\.com\/([^\/]+\/[^\/]+)/);
if (match) return `npx skills add ${match[1]}`;
}
if (method === "npx-multi") {
return `npx skills add ${sourceUrl} --skill ${skillId}`;
}
return undefined;
}
/**
* Generate the view source URL for a skill in the Agent-Skills repo
*/
function generateViewSourceUrl(skillId: string): string {
return `https://github.com/block/Agent-Skills/tree/main/${skillId}`;
}
/**
* Get all unique tags from all skills (async)
*/
export async function getAllTags(): Promise<string[]> {
const allSkills = await loadAllSkills();
const tagSet = new Set<string>();
allSkills.forEach((skill) => {
skill.tags.forEach((tag) => tagSet.add(tag));
});
return Array.from(tagSet).sort();
}