skill-guide 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -3
- package/README.md +163 -169
- package/SKILL.md +28 -251
- package/demo-categories.png +0 -0
- package/demo-cover.png +0 -0
- package/demo-highlights.png +0 -0
- package/demo-reference.png +0 -0
- package/package.json +3 -2
- package/scan-skills.js +178 -7
- package/skill-guide.js +2675 -56
- package/skill-registry.js +288 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Cache helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const CACHE_DIR = path.join(os.tmpdir(), 'claude');
|
|
13
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
14
|
+
|
|
15
|
+
function cacheKey(urls) {
|
|
16
|
+
const joined = urls.join('|');
|
|
17
|
+
return crypto.createHash('sha1').update(joined).digest('hex').slice(0, 12);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function cacheFile(urls) {
|
|
21
|
+
return path.join(CACHE_DIR, `skill-registry-${cacheKey(urls)}.json`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readCache(urls) {
|
|
25
|
+
try {
|
|
26
|
+
const raw = fs.readFileSync(cacheFile(urls), 'utf8');
|
|
27
|
+
const cached = JSON.parse(raw);
|
|
28
|
+
if (Date.now() - cached._ts < CACHE_TTL_MS) {
|
|
29
|
+
return cached;
|
|
30
|
+
}
|
|
31
|
+
} catch (_) { /* ignore */ }
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeCache(data, urls) {
|
|
36
|
+
try {
|
|
37
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
38
|
+
data._ts = Date.now();
|
|
39
|
+
fs.writeFileSync(cacheFile(urls), JSON.stringify(data), 'utf8');
|
|
40
|
+
} catch (_) { /* ignore */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function clearCache(urls) {
|
|
44
|
+
try {
|
|
45
|
+
fs.unlinkSync(cacheFile(urls));
|
|
46
|
+
} catch (_) { /* ignore */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Markdown parser for awesome-lists
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
function parseMarkdownList(markdown, source) {
|
|
53
|
+
const entries = [];
|
|
54
|
+
|
|
55
|
+
// Pattern 1: Bullet list — - [name](url) - description
|
|
56
|
+
const bulletRe = /^-\s+\[([^\]]+)\]\(([^)]+)\)\s*[-–—]\s*(.+)$/gm;
|
|
57
|
+
let match;
|
|
58
|
+
while ((match = bulletRe.exec(markdown)) !== null) {
|
|
59
|
+
const url = match[2].trim();
|
|
60
|
+
// Skip TOC anchor links
|
|
61
|
+
if (url.startsWith('#')) continue;
|
|
62
|
+
entries.push({
|
|
63
|
+
name: match[1].trim(),
|
|
64
|
+
url,
|
|
65
|
+
description: match[3].trim(),
|
|
66
|
+
source,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Pattern 2: Table — | [name](url) | description |
|
|
71
|
+
const tableRe = /^\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(.+?)\s*\|$/gm;
|
|
72
|
+
while ((match = tableRe.exec(markdown)) !== null) {
|
|
73
|
+
if (/^[-\s|]+$/.test(match[0])) continue;
|
|
74
|
+
entries.push({
|
|
75
|
+
name: match[1].trim(),
|
|
76
|
+
url: match[2].trim(),
|
|
77
|
+
description: match[3].replace(/[*_`]/g, '').trim(),
|
|
78
|
+
source,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return entries;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Registry URLs
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
const REGISTRY_URLS = [
|
|
89
|
+
'https://raw.githubusercontent.com/ComposioHQ/awesome-claude-skills/master/README.md',
|
|
90
|
+
'https://raw.githubusercontent.com/ComposioHQ/awesome-codex-skills/main/README.md',
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const REGISTRY_SOURCES = {
|
|
94
|
+
'https://raw.githubusercontent.com/ComposioHQ/awesome-claude-skills/master/README.md': 'awesome-claude-skills',
|
|
95
|
+
'https://raw.githubusercontent.com/ComposioHQ/awesome-codex-skills/main/README.md': 'awesome-codex-skills',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function fetchRegistry(opts = {}) {
|
|
99
|
+
const { refresh = false } = opts;
|
|
100
|
+
|
|
101
|
+
if (!refresh) {
|
|
102
|
+
const cached = readCache(REGISTRY_URLS);
|
|
103
|
+
if (cached && cached.entries) return cached.entries;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (process.env.SKILL_REGISTRY_OFFLINE === '1') {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const allEntries = [];
|
|
111
|
+
|
|
112
|
+
for (const url of REGISTRY_URLS) {
|
|
113
|
+
try {
|
|
114
|
+
const { spawnSync } = require('child_process');
|
|
115
|
+
const result = spawnSync('curl', ['-sL', '--max-time', '10', url], {
|
|
116
|
+
encoding: 'utf8',
|
|
117
|
+
timeout: 15000,
|
|
118
|
+
});
|
|
119
|
+
const markdown = result.stdout || '';
|
|
120
|
+
|
|
121
|
+
if (markdown && !markdown.includes('404: Not Found')) {
|
|
122
|
+
const source = REGISTRY_SOURCES[url] || 'unknown';
|
|
123
|
+
const entries = parseMarkdownList(markdown, source);
|
|
124
|
+
allEntries.push(...entries);
|
|
125
|
+
}
|
|
126
|
+
} catch (_) { /* offline or timeout */ }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const seen = new Map();
|
|
130
|
+
for (const entry of allEntries) {
|
|
131
|
+
const key = entry.name.toLowerCase();
|
|
132
|
+
if (!seen.has(key)) {
|
|
133
|
+
seen.set(key, entry);
|
|
134
|
+
} else {
|
|
135
|
+
const existing = seen.get(key);
|
|
136
|
+
if (!existing.sources) existing.sources = [existing.source];
|
|
137
|
+
existing.sources.push(entry.source);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const entries = Array.from(seen.values());
|
|
142
|
+
writeCache({ entries }, REGISTRY_URLS);
|
|
143
|
+
return entries;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// URL validation
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
function isValidUrl(url) {
|
|
150
|
+
if (!url || typeof url !== 'string') return false;
|
|
151
|
+
if (url.startsWith('#')) return false;
|
|
152
|
+
if (/example\.com/i.test(url)) return false;
|
|
153
|
+
return url.startsWith('http://') || url.startsWith('https://');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Category constants (mirrored from scan-skills.js CATEGORY_MAP)
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
const ALL_CATEGORIES = ['testing', 'design', 'security', 'documentation', 'automation', 'deployment', 'code-quality', 'development'];
|
|
160
|
+
|
|
161
|
+
function categorizeOnlineSkill(entry) {
|
|
162
|
+
const text = [entry.name, entry.description].join(' ');
|
|
163
|
+
const rules = [
|
|
164
|
+
{ category: 'testing', keywords: /\b(test|tdd|spec|e2e|qa|assert|jest|vitest|playwright)\b/i },
|
|
165
|
+
{ category: 'design', keywords: /\b(design|ui|ux|css|style|color|layout|figma|theme|visual|interface)\b/i },
|
|
166
|
+
{ category: 'security', keywords: /\b(security|audit|vuln|owasp|xss|inject|auth|encrypt|cve)\b/i },
|
|
167
|
+
{ category: 'documentation', keywords: /\b(doc|readme|changelog|api-doc|markdown|mdx|writing)\b/i },
|
|
168
|
+
{ category: 'automation', keywords: /\b(automat|script|batch|loop|cron|schedule|workflow)\b/i },
|
|
169
|
+
{ category: 'deployment', keywords: /\b(deploy|release|ci.?cd|docker|kubernetes|infra|nginx|vercel)\b/i },
|
|
170
|
+
{ category: 'code-quality', keywords: /\b(review|lint|refactor|simplif|clean|format|pattern)\b/i },
|
|
171
|
+
{ category: 'development', keywords: /\b(develop|build|debug|investigate|plan|brainstorm|feature|implement)\b/i },
|
|
172
|
+
];
|
|
173
|
+
for (const { category, keywords } of rules) {
|
|
174
|
+
if (keywords.test(text)) return category;
|
|
175
|
+
}
|
|
176
|
+
return 'other';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const GAP_ACTIONS = {
|
|
180
|
+
testing: 'Add a TDD skill to catch bugs before they ship',
|
|
181
|
+
design: 'Add a UI/UX skill to improve your frontend output',
|
|
182
|
+
security: 'Add a security audit skill to catch vulnerabilities',
|
|
183
|
+
documentation: 'Add a docs skill to keep your project well-documented',
|
|
184
|
+
automation: 'Add an automation skill to eliminate repetitive tasks',
|
|
185
|
+
deployment: 'Add a deploy skill to streamline your CI/CD pipeline',
|
|
186
|
+
'code-quality': 'Add a code review skill to maintain standards',
|
|
187
|
+
development: 'Add a dev workflow skill to boost productivity',
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
function recommend(installed, onlineEntries) {
|
|
191
|
+
const results = [];
|
|
192
|
+
|
|
193
|
+
const installedByCategory = {};
|
|
194
|
+
for (const skill of installed) {
|
|
195
|
+
const cat = skill.category || 'other';
|
|
196
|
+
if (!installedByCategory[cat]) installedByCategory[cat] = [];
|
|
197
|
+
installedByCategory[cat].push(skill);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const categorizedOnline = onlineEntries.map((e) => ({
|
|
201
|
+
...e,
|
|
202
|
+
category: categorizeOnlineSkill(e),
|
|
203
|
+
}));
|
|
204
|
+
|
|
205
|
+
// 1. Gap analysis — categories with no installed skills
|
|
206
|
+
for (const cat of ALL_CATEGORIES) {
|
|
207
|
+
if (!installedByCategory[cat] || installedByCategory[cat].length === 0) {
|
|
208
|
+
const catSkills = categorizedOnline.filter((e) => e.category === cat).slice(0, 3);
|
|
209
|
+
results.push({
|
|
210
|
+
type: 'gap',
|
|
211
|
+
category: cat,
|
|
212
|
+
message: `You have no ${cat} skills installed`,
|
|
213
|
+
action: GAP_ACTIONS[cat] || `Explore ${cat} skills to fill this gap`,
|
|
214
|
+
skills: catSkills.map((s) => ({ name: s.name, description: s.description, url: s.url })),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 2. Overlap detection — categories with 3+ skills
|
|
220
|
+
for (const [cat, skills] of Object.entries(installedByCategory)) {
|
|
221
|
+
if (skills.length >= 3) {
|
|
222
|
+
const MAX_OVERLAP_SHOWN = 8;
|
|
223
|
+
const sorted = [...skills].sort((a, b) => (b.completeness || 0) - (a.completeness || 0));
|
|
224
|
+
const shown = sorted.slice(0, MAX_OVERLAP_SHOWN);
|
|
225
|
+
const remaining = sorted.length - shown.length;
|
|
226
|
+
results.push({
|
|
227
|
+
type: 'overlap',
|
|
228
|
+
category: cat,
|
|
229
|
+
count: sorted.length,
|
|
230
|
+
message: `You have ${sorted.length} skills in "${cat}" category`,
|
|
231
|
+
skills: shown.map((s) => s.name),
|
|
232
|
+
completeness: shown.map((s) => s.completeness || 0),
|
|
233
|
+
hasMore: remaining > 0,
|
|
234
|
+
remainingCount: remaining,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 3. Popular skills not installed
|
|
240
|
+
const installedNames = new Set(installed.map((s) => s.name.toLowerCase()));
|
|
241
|
+
const popularityMap = new Map();
|
|
242
|
+
for (const entry of categorizedOnline) {
|
|
243
|
+
const key = entry.name.toLowerCase();
|
|
244
|
+
if (!installedNames.has(key)) {
|
|
245
|
+
const existing = popularityMap.get(key) || { ...entry, count: 0, sources: [] };
|
|
246
|
+
existing.count++;
|
|
247
|
+
if (!existing.sources.includes(entry.source)) existing.sources.push(entry.source);
|
|
248
|
+
popularityMap.set(key, existing);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const popular = Array.from(popularityMap.values())
|
|
253
|
+
.filter((entry) => isValidUrl(entry.url))
|
|
254
|
+
.sort((a, b) => b.count - a.count)
|
|
255
|
+
.slice(0, 10);
|
|
256
|
+
|
|
257
|
+
for (const skill of popular) {
|
|
258
|
+
results.push({
|
|
259
|
+
type: 'popular',
|
|
260
|
+
name: skill.name,
|
|
261
|
+
description: skill.description,
|
|
262
|
+
url: skill.url,
|
|
263
|
+
sources: skill.sources,
|
|
264
|
+
message: `Found in ${skill.count} awesome-list(s)`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const order = { gap: 0, popular: 1, overlap: 2 };
|
|
269
|
+
results.sort((a, b) => (order[a.type] || 9) - (order[b.type] || 9));
|
|
270
|
+
|
|
271
|
+
return results;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Exports
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
module.exports = {
|
|
278
|
+
ALL_CATEGORIES: ALL_CATEGORIES,
|
|
279
|
+
GAP_ACTIONS: GAP_ACTIONS,
|
|
280
|
+
_cacheKey: cacheKey,
|
|
281
|
+
_readCache: readCache,
|
|
282
|
+
_writeCache: writeCache,
|
|
283
|
+
_parseMarkdownList: parseMarkdownList,
|
|
284
|
+
REGISTRY_URLS: REGISTRY_URLS,
|
|
285
|
+
clearCache: clearCache,
|
|
286
|
+
fetchRegistry: fetchRegistry,
|
|
287
|
+
recommend: recommend,
|
|
288
|
+
};
|