mirror of
https://github.com/block/goose.git
synced 2026-04-26 10:40:45 +00:00
[docs] Add OSS Skills Marketplace (#6752)
This commit is contained in:
parent
7ea38402a3
commit
4d49fd5423
13 changed files with 1590 additions and 5 deletions
3
documentation/.gitignore
vendored
3
documentation/.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -391,6 +391,10 @@ const config: Config = {
|
|||
to: '/extensions',
|
||||
label: 'Extensions',
|
||||
},
|
||||
{
|
||||
to: '/skills',
|
||||
label: 'Skills Marketplace',
|
||||
},
|
||||
{
|
||||
to: '/recipe-generator',
|
||||
label: 'Recipe Generator',
|
||||
|
|
|
|||
34
documentation/package-lock.json
generated
34
documentation/package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
360
documentation/scripts/generate-skills-manifest.js
Normal file
360
documentation/scripts/generate-skills-manifest.js
Normal 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();
|
||||
130
documentation/scripts/generate-skills-zips.js
Normal file
130
documentation/scripts/generate-skills-zips.js
Normal 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();
|
||||
137
documentation/src/components/skill-card.tsx
Normal file
137
documentation/src/components/skill-card.tsx
Normal 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 };
|
||||
316
documentation/src/pages/skills/detail.tsx
Normal file
316
documentation/src/pages/skills/detail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
223
documentation/src/pages/skills/index.tsx
Normal file
223
documentation/src/pages/skills/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
125
documentation/src/pages/skills/types/index.tsx
Normal file
125
documentation/src/pages/skills/types/index.tsx
Normal 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;
|
||||
|
|
@ -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 && (
|
||||
|
|
|
|||
246
documentation/src/utils/skills.ts
Normal file
246
documentation/src/utils/skills.ts
Normal 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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue