Skip to content

Commit 5bd7ebf

Browse files
authored
feat: viral growth features — quick setup, badges, matrix, SKILL.md, federated search (#41)
* feat: add viral growth features (quick setup, badges, matrix, SKILL.md, federated search) Tier 2: skillkit quick zero-friction entry, shareable badge generator, agent compatibility matrix on website. Tier 3: reposition as npm for agent skills, SKILL.md validate/init/check commands, federated GitHub skill search with --federated flag on find. * fix: matrix filtering, path traversal, cross-platform paths, fetch timeout - CompatibilityMatrix: query matching agents now shows all categories (and vice versa) instead of hiding both dimensions independently - quick.ts: sanitize skill names and validate targetInstallPath stays inside installDir before rmSync/cpSync/symlinkSync - skillmd.ts: use path.relative() instead of string replace for cross-platform path display - registry: add AbortController timeout (10s default) and distinct RateLimitError on 403 instead of silent empty return - find.ts: handle RateLimitError with user-facing message - Add quick-security.test.ts regression tests for path traversal * fix: harden isPathInside with path.resolve, propagate RateLimitError - isPathInside now uses path.resolve() + path.sep comparison instead of naive string replacement, fixing detection of paths completely outside the parent directory (both packages/core and src/core copies) - FederatedSearch.search re-throws RateLimitError from rejected Promise.allSettled results so callers (find.ts) can handle 403s - Updated security test to use the corrected isPathInside logic * fix: use GITHUB_TOKEN/GH_TOKEN for authenticated GitHub API requests GitHubSkillRegistry now reads GITHUB_TOKEN or GH_TOKEN from env and sends Bearer auth header, raising the rate limit from 10 to 30 req/min for code search. Matches the guidance shown to users on 403 errors.
1 parent 549a21a commit 5bd7ebf

File tree

18 files changed

+1751
-19
lines changed

18 files changed

+1751
-19
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
[![Website](https://img.shields.io/badge/Website-agenstskills.com-black)](https://agenstskills.com)
1515
[![Docs](https://img.shields.io/badge/Docs-agenstskills.com/docs-blue)](https://agenstskills.com/docs)
1616
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
17+
[![SKILL.md](https://img.shields.io/badge/SKILL.md-compatible-black?style=flat-square)](https://agenstskills.com)
1718

18-
## Supercharge Every AI Coding Agent
19+
## One Skill. Every Agent.
1920

20-
**Give your AI agents new abilities with portable, reusable skills.** Install from a curated marketplace, create your own, and use them across Claude Code, Cursor, Codex, Windsurf, GitHub Copilot, and 27 more agents (32 total).
21+
**The package manager for AI agent skills. Install, translate, and share skills across 32 coding agents.**
22+
23+
> Think npm, but for AI agent skills. Write once, deploy to Claude Code, Cursor, Codex, Windsurf, Copilot, and 27 more.
2124
2225
> **What are AI Agent Skills?** Skills are instruction files that teach AI coding agents how to handle specific tasks - like processing PDFs, following React best practices, or enforcing security patterns. Think of them as plugins for your AI assistant.
2326

apps/skillkit/src/cli.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ import {
9898
GuidelineCreateCommand,
9999
GuidelineRemoveCommand,
100100
TreeCommand,
101+
QuickCommand,
102+
SkillMdValidateCommand,
103+
SkillMdInitCommand,
104+
SkillMdCheckCommand,
101105
} from '@skillkit/cli';
102106

103107
const __filename = fileURLToPath(import.meta.url);
@@ -220,4 +224,9 @@ cli.register(GuidelineRemoveCommand);
220224

221225
cli.register(TreeCommand);
222226

227+
cli.register(QuickCommand);
228+
cli.register(SkillMdValidateCommand);
229+
cli.register(SkillMdInitCommand);
230+
cli.register(SkillMdCheckCommand);
231+
223232
cli.runExit(process.argv.slice(2));

docs/skillkit/App.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { Attribution } from './components/Attribution';
1212
import { AdvancedFeatures } from './components/AdvancedFeatures';
1313
import { UseCases } from './components/UseCases';
1414
import { TeamEnterprise } from './components/TeamEnterprise';
15+
import { BadgeGenerator } from './components/BadgeGenerator';
16+
import { CompatibilityMatrix } from './components/CompatibilityMatrix';
1517
import { useStats } from './hooks/useStats';
1618

1719
const GITHUB_ICON = (
@@ -61,6 +63,20 @@ export default function App(): React.ReactElement {
6163
>
6264
Stack
6365
</a>
66+
<a
67+
href="#matrix"
68+
onClick={(e) => scrollToSection(e, 'matrix')}
69+
className="text-zinc-500 hover:text-white transition-colors"
70+
>
71+
Matrix
72+
</a>
73+
<a
74+
href="#badge"
75+
onClick={(e) => scrollToSection(e, 'badge')}
76+
className="text-zinc-500 hover:text-white transition-colors"
77+
>
78+
Badge
79+
</a>
6480
<a
6581
href="#advanced"
6682
onClick={(e) => scrollToSection(e, 'advanced')}
@@ -172,6 +188,16 @@ export default function App(): React.ReactElement {
172188
<StackBuilder />
173189
</section>
174190

191+
{/* Compatibility Matrix */}
192+
<section id="matrix" style={{ scrollMarginTop: '4rem' }}>
193+
<CompatibilityMatrix />
194+
</section>
195+
196+
{/* Badge Generator */}
197+
<section id="badge" style={{ scrollMarginTop: '4rem' }}>
198+
<BadgeGenerator />
199+
</section>
200+
175201
{/* Advanced Capabilities: Memory, Primer, Mesh, Messaging */}
176202
<section id="advanced" style={{ scrollMarginTop: '4rem' }}>
177203
<AdvancedFeatures />
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import React, { useState, useMemo } from 'react';
2+
3+
const SKILLKIT_LOGO_SVG_BASE64 =
4+
'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0id2hpdGUiPjxyZWN0IHdpZHRoPSI2IiBoZWlnaHQ9IjYiIHg9IjEiIHk9IjEiLz48cmVjdCB3aWR0aD0iNiIgaGVpZ2h0PSI2IiB4PSI5IiB5PSIxIi8+PHJlY3Qgd2lkdGg9IjYiIGhlaWdodD0iNiIgeD0iMSIgeT0iOSIvPjxyZWN0IHdpZHRoPSI2IiBoZWlnaHQ9IjYiIHg9IjkiIHk9IjkiLz48L3N2Zz4=';
5+
6+
const SUGGESTED_SKILLS = [
7+
'react', 'typescript', 'nextjs', 'python', 'docker',
8+
'kubernetes', 'tailwind', 'graphql', 'prisma', 'langchain',
9+
'openai', 'rust', 'go', 'vue', 'svelte',
10+
'security', 'testing', 'cicd', 'aws', 'terraform',
11+
];
12+
13+
function encodeBadgeSegment(text: string): string {
14+
return encodeURIComponent(text).replace(/-/g, '--');
15+
}
16+
17+
function buildBadgeUrl(skills: string[]): string {
18+
if (skills.length === 0) {
19+
return `https://img.shields.io/badge/SkillKit-No%20Skills-555555?style=flat-square&logo=data:image/svg+xml;base64,${SKILLKIT_LOGO_SVG_BASE64}`;
20+
}
21+
22+
const maxDisplay = 5;
23+
const displayNames = skills.slice(0, maxDisplay);
24+
const suffix = skills.length > maxDisplay ? ` +${skills.length - maxDisplay} more` : '';
25+
const label = `SkillKit ${skills.length} skills`;
26+
const message = displayNames.join(' | ') + suffix;
27+
28+
return `https://img.shields.io/badge/${encodeBadgeSegment(label)}-${encodeBadgeSegment(message)}-black?style=flat-square&logo=data:image/svg+xml;base64,${SKILLKIT_LOGO_SVG_BASE64}`;
29+
}
30+
31+
export function BadgeGenerator(): React.ReactElement {
32+
const [skills, setSkills] = useState<string[]>(['react', 'typescript', 'nextjs']);
33+
const [inputValue, setInputValue] = useState('');
34+
const [copiedType, setCopiedType] = useState<string | null>(null);
35+
36+
const badgeUrl = useMemo(() => buildBadgeUrl(skills), [skills]);
37+
38+
const markdown = `[![SkillKit Stack](${badgeUrl})](https://agenstskills.com)`;
39+
const html = `<a href="https://agenstskills.com"><img src="${badgeUrl}" alt="SkillKit Stack" /></a>`;
40+
41+
function addSkill(skill: string): void {
42+
const trimmed = skill.trim().toLowerCase();
43+
if (trimmed && !skills.includes(trimmed)) {
44+
setSkills([...skills, trimmed]);
45+
}
46+
setInputValue('');
47+
}
48+
49+
function removeSkill(skill: string): void {
50+
setSkills(skills.filter(s => s !== skill));
51+
}
52+
53+
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>): void {
54+
if (e.key === 'Enter' && inputValue.trim()) {
55+
e.preventDefault();
56+
addSkill(inputValue);
57+
}
58+
}
59+
60+
async function copyToClipboard(text: string, type: string): Promise<void> {
61+
try {
62+
await navigator.clipboard.writeText(text);
63+
setCopiedType(type);
64+
setTimeout(() => setCopiedType(null), 2000);
65+
} catch {
66+
// silently ignore
67+
}
68+
}
69+
70+
const availableSuggestions = SUGGESTED_SKILLS.filter(s => !skills.includes(s));
71+
72+
return (
73+
<section className="py-12 border-b border-zinc-800">
74+
<div className="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
75+
<div className="mb-6">
76+
<h2 className="text-xl font-bold text-white mb-1 font-mono">Badge Generator</h2>
77+
<p className="text-zinc-500 font-mono text-xs">
78+
Create a shareable badge for your skill stack. Add it to your README.
79+
</p>
80+
</div>
81+
82+
<div className="flex flex-col lg:flex-row gap-6">
83+
<div className="flex-1 space-y-4">
84+
<div className="relative">
85+
<input
86+
type="text"
87+
value={inputValue}
88+
onChange={(e) => setInputValue(e.target.value)}
89+
onKeyDown={handleKeyDown}
90+
placeholder="Type a skill name and press Enter..."
91+
className="w-full bg-zinc-900 border border-zinc-700 text-white px-4 py-3 focus:ring-2 focus:ring-white focus:border-transparent outline-none font-mono text-sm placeholder-zinc-600"
92+
/>
93+
</div>
94+
95+
<div className="space-y-2">
96+
<span className="text-zinc-500 text-xs font-mono block">Quick add:</span>
97+
<div className="flex flex-wrap gap-1.5">
98+
{availableSuggestions.slice(0, 12).map(skill => (
99+
<button
100+
key={skill}
101+
onClick={() => addSkill(skill)}
102+
className="text-[10px] sm:text-xs bg-zinc-900 text-zinc-400 px-2 sm:px-3 py-1 sm:py-1.5 border border-zinc-700 hover:border-zinc-500 hover:text-white transition-colors font-mono"
103+
>
104+
+ {skill}
105+
</button>
106+
))}
107+
</div>
108+
</div>
109+
110+
{skills.length > 0 && (
111+
<div className="space-y-2">
112+
<div className="flex items-center justify-between">
113+
<div className="text-xs font-mono text-zinc-500 uppercase tracking-wider">
114+
Skills in Badge ({skills.length})
115+
</div>
116+
<button
117+
onClick={() => setSkills([])}
118+
className="text-zinc-500 hover:text-red-400 font-mono text-xs transition-colors"
119+
>
120+
Clear All
121+
</button>
122+
</div>
123+
<div className="flex flex-wrap gap-2">
124+
{skills.map(skill => (
125+
<div
126+
key={skill}
127+
className="flex items-center gap-1.5 bg-zinc-900/50 border border-zinc-800 px-3 py-1.5 group"
128+
>
129+
<span className="font-mono text-sm text-white">{skill}</span>
130+
<button
131+
onClick={() => removeSkill(skill)}
132+
aria-label={`Remove ${skill}`}
133+
className="text-zinc-600 hover:text-red-400 transition-colors opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
134+
>
135+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
136+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
137+
</svg>
138+
</button>
139+
</div>
140+
))}
141+
</div>
142+
</div>
143+
)}
144+
</div>
145+
146+
<div className="lg:w-80 lg:flex-shrink-0">
147+
<div className="lg:sticky lg:top-20 bg-zinc-900/30 border border-zinc-800 p-4 space-y-4">
148+
<div className="text-xs font-mono text-zinc-500 uppercase tracking-wider">Preview</div>
149+
150+
<div className="flex justify-center py-3 bg-black/50 border border-zinc-800">
151+
{skills.length > 0 ? (
152+
<img
153+
src={badgeUrl}
154+
alt="SkillKit Stack Badge"
155+
className="h-5"
156+
/>
157+
) : (
158+
<span className="text-zinc-600 text-xs font-mono">Add skills to preview</span>
159+
)}
160+
</div>
161+
162+
<div className="space-y-2">
163+
<div className="text-xs font-mono text-zinc-500">Markdown</div>
164+
<pre className="bg-black/50 p-2 overflow-x-auto text-[10px] border border-zinc-800">
165+
<code className="font-mono text-zinc-300 whitespace-pre-wrap break-all">
166+
{markdown}
167+
</code>
168+
</pre>
169+
<button
170+
onClick={() => copyToClipboard(markdown, 'markdown')}
171+
className="w-full bg-zinc-800 text-zinc-300 py-1.5 font-mono text-xs hover:bg-zinc-700 hover:text-white transition-colors flex items-center justify-center gap-1.5 border border-zinc-700"
172+
>
173+
{copiedType === 'markdown' ? 'Copied!' : 'Copy Markdown'}
174+
</button>
175+
</div>
176+
177+
<div className="space-y-2">
178+
<div className="text-xs font-mono text-zinc-500">HTML</div>
179+
<pre className="bg-black/50 p-2 overflow-x-auto text-[10px] border border-zinc-800">
180+
<code className="font-mono text-zinc-300 whitespace-pre-wrap break-all">
181+
{html}
182+
</code>
183+
</pre>
184+
<button
185+
onClick={() => copyToClipboard(html, 'html')}
186+
className="w-full bg-zinc-800 text-zinc-300 py-1.5 font-mono text-xs hover:bg-zinc-700 hover:text-white transition-colors flex items-center justify-center gap-1.5 border border-zinc-700"
187+
>
188+
{copiedType === 'html' ? 'Copied!' : 'Copy HTML'}
189+
</button>
190+
</div>
191+
192+
<div className="text-[10px] font-mono text-zinc-600 text-center pt-2 border-t border-zinc-800">
193+
Powered by shields.io
194+
</div>
195+
</div>
196+
</div>
197+
</div>
198+
</div>
199+
</section>
200+
);
201+
}

0 commit comments

Comments
 (0)