skill-guide 0.2.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 +45 -0
- package/CONTRIBUTING.md +51 -0
- package/LICENSE +21 -0
- package/README.md +236 -0
- package/SKILL.md +266 -0
- package/agents/openai.yaml +8 -0
- package/bin/skill-guide +4 -0
- 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/demo.gif +0 -0
- package/demo.html +1861 -0
- package/package.json +63 -0
- package/scan-skills.js +580 -0
- package/skill-guide.js +782 -0
- package/social-preview.png +0 -0
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skill-guide",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Scan 6+ skill directories across Claude Code, Codex, and cc-switch, then generate beautiful HTML slide presentations.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"skill-guide": "bin/skill-guide",
|
|
9
|
+
"skill-guide-scan": "scan-skills.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"agents/openai.yaml",
|
|
13
|
+
"bin/skill-guide",
|
|
14
|
+
"SKILL.md",
|
|
15
|
+
"skill-guide.js",
|
|
16
|
+
"scan-skills.js",
|
|
17
|
+
"demo.html",
|
|
18
|
+
"demo.gif",
|
|
19
|
+
"social-preview.png",
|
|
20
|
+
"demo-*.png",
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"CONTRIBUTING.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"scan": "node scan-skills.js --list",
|
|
28
|
+
"scan:refresh": "node scan-skills.js --refresh --list",
|
|
29
|
+
"scan:skill": "node scan-skills.js --skill",
|
|
30
|
+
"scan:search": "node scan-skills.js --search",
|
|
31
|
+
"scan:full": "node scan-skills.js --full",
|
|
32
|
+
"guide": "node skill-guide.js --open",
|
|
33
|
+
"doctor": "node skill-guide.js --doctor",
|
|
34
|
+
"test": "node --check scan-skills.js && node --check skill-guide.js && node --test test/*.test.js && node scan-skills.js --list >/dev/null && node scan-skills.js --search security >/dev/null && node scan-skills.js --full >/dev/null"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"claude-code",
|
|
41
|
+
"codex",
|
|
42
|
+
"agent-skills",
|
|
43
|
+
"skills",
|
|
44
|
+
"skill",
|
|
45
|
+
"skill-discovery",
|
|
46
|
+
"skill-scanner",
|
|
47
|
+
"skill-browser",
|
|
48
|
+
"claude-code-skills",
|
|
49
|
+
"codex-skills",
|
|
50
|
+
"html-slides",
|
|
51
|
+
"developer-tools",
|
|
52
|
+
"agent-tools",
|
|
53
|
+
"cli"
|
|
54
|
+
],
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/gtskevin/skill-guide.git"
|
|
58
|
+
},
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/gtskevin/skill-guide/issues"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://gtskevin.github.io/skill-guide/"
|
|
63
|
+
}
|
package/scan-skills.js
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
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
|
+
// CLI parsing
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
function getArgValue(flag) {
|
|
15
|
+
const idx = args.indexOf(flag);
|
|
16
|
+
if (idx === -1) return null;
|
|
17
|
+
return args[idx + 1] || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hasFlag(flag) {
|
|
21
|
+
return args.includes(flag);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const mode = hasFlag('--list') ? 'list'
|
|
25
|
+
: hasFlag('--skill') ? 'skill'
|
|
26
|
+
: hasFlag('--search') ? 'search'
|
|
27
|
+
: hasFlag('--full') ? 'full'
|
|
28
|
+
: null;
|
|
29
|
+
|
|
30
|
+
if (!mode) {
|
|
31
|
+
process.stderr.write(
|
|
32
|
+
'Usage:\n' +
|
|
33
|
+
' scan-skills.js --list # name + description + category\n' +
|
|
34
|
+
' scan-skills.js --skill <name> # full data for one skill\n' +
|
|
35
|
+
' scan-skills.js --search <query> # match triggers + description\n' +
|
|
36
|
+
' scan-skills.js --full # all skills with full data\n' +
|
|
37
|
+
' scan-skills.js --refresh # force re-scan (combine with any mode)\n'
|
|
38
|
+
);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const skillName = getArgValue('--skill');
|
|
43
|
+
const searchQuery = getArgValue('--search');
|
|
44
|
+
const refresh = hasFlag('--refresh');
|
|
45
|
+
|
|
46
|
+
if (mode === 'skill' && !skillName) {
|
|
47
|
+
process.stderr.write('Error: --skill requires a name argument\n');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (mode === 'search' && !searchQuery) {
|
|
51
|
+
process.stderr.write('Error: --search requires a query argument\n');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Cache helpers
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
const CACHE_DIR = path.join(os.tmpdir(), 'claude');
|
|
59
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
60
|
+
|
|
61
|
+
function getCacheKey() {
|
|
62
|
+
const roots = SCAN_SOURCES.map((source) => `${source.label}:${path.resolve(source.dir)}`).join('|');
|
|
63
|
+
return crypto.createHash('sha1').update(roots).digest('hex').slice(0, 12);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getCacheFile() {
|
|
67
|
+
return path.join(CACHE_DIR, `skill-guide-cache-${getCacheKey()}.json`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readCache() {
|
|
71
|
+
try {
|
|
72
|
+
const CACHE_FILE = getCacheFile();
|
|
73
|
+
const raw = fs.readFileSync(CACHE_FILE, 'utf8');
|
|
74
|
+
const cached = JSON.parse(raw);
|
|
75
|
+
if (Date.now() - cached._ts < CACHE_TTL_MS) {
|
|
76
|
+
return cached;
|
|
77
|
+
}
|
|
78
|
+
} catch (_) { /* ignore */ }
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeCache(data) {
|
|
83
|
+
try {
|
|
84
|
+
const CACHE_FILE = getCacheFile();
|
|
85
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
86
|
+
data._ts = Date.now();
|
|
87
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(data), 'utf8');
|
|
88
|
+
} catch (_) { /* ignore */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Frontmatter parser (regex-based, no YAML library)
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
function parseFrontmatter(content) {
|
|
95
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
96
|
+
if (!match) return {};
|
|
97
|
+
const raw = match[1];
|
|
98
|
+
const result = {};
|
|
99
|
+
let currentKey = null;
|
|
100
|
+
let inMultiline = false;
|
|
101
|
+
let multilineValue = '';
|
|
102
|
+
let multilineType = '';
|
|
103
|
+
|
|
104
|
+
const lines = raw.split('\n');
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
// Multi-line continuation
|
|
107
|
+
if (inMultiline) {
|
|
108
|
+
if (line.trim() === '' && multilineValue.length > 0) {
|
|
109
|
+
multilineValue += '\n';
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (line.startsWith(' ') || line.startsWith('\t')) {
|
|
113
|
+
const val = line.trim();
|
|
114
|
+
if (val.startsWith('- ')) {
|
|
115
|
+
if (!Array.isArray(result[currentKey])) {
|
|
116
|
+
result[currentKey] = [];
|
|
117
|
+
}
|
|
118
|
+
result[currentKey].push(stripQuotes(val.slice(2)));
|
|
119
|
+
} else {
|
|
120
|
+
multilineValue += (multilineValue ? '\n' : '') + val;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// End of multiline block
|
|
125
|
+
if (multilineType === '|' || multilineType === '>') {
|
|
126
|
+
if (multilineValue && !Array.isArray(result[currentKey])) {
|
|
127
|
+
result[currentKey] = multilineValue.trim();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
inMultiline = false;
|
|
131
|
+
multilineValue = '';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Key: value
|
|
135
|
+
const kvMatch = line.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.*)/);
|
|
136
|
+
if (kvMatch) {
|
|
137
|
+
currentKey = kvMatch[1];
|
|
138
|
+
let val = kvMatch[2].trim();
|
|
139
|
+
|
|
140
|
+
// Multi-line indicators
|
|
141
|
+
if (val === '|' || val === '>') {
|
|
142
|
+
inMultiline = true;
|
|
143
|
+
multilineType = val;
|
|
144
|
+
multilineValue = '';
|
|
145
|
+
result[currentKey] = '';
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Inline list [a, b, c]
|
|
150
|
+
if (val.startsWith('[') && val.endsWith(']')) {
|
|
151
|
+
result[currentKey] = val.slice(1, -1).split(',').map(s => stripQuotes(s.trim())).filter(Boolean);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Empty value means next lines could be list
|
|
156
|
+
if (val === '') {
|
|
157
|
+
result[currentKey] = [];
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
result[currentKey] = stripQuotes(val);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// List item continuation
|
|
166
|
+
const listItem = line.match(/^\s+-\s+(.*)/);
|
|
167
|
+
if (listItem && currentKey) {
|
|
168
|
+
if (!Array.isArray(result[currentKey])) {
|
|
169
|
+
result[currentKey] = [];
|
|
170
|
+
}
|
|
171
|
+
result[currentKey].push(stripQuotes(listItem[1]));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Finalize trailing multiline
|
|
176
|
+
if (inMultiline && multilineValue && currentKey && !Array.isArray(result[currentKey])) {
|
|
177
|
+
result[currentKey] = multilineValue.trim();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function stripQuotes(val) {
|
|
184
|
+
if (typeof val !== 'string') return val;
|
|
185
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
186
|
+
return val.slice(1, -1);
|
|
187
|
+
}
|
|
188
|
+
return val;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Auto-categorization
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
const CATEGORY_MAP = [
|
|
195
|
+
{ category: 'testing', keywords: /\b(test|tdd|spec|e2e|qa|assert|jest|vitest|playwright)\b/i },
|
|
196
|
+
{ category: 'design', keywords: /\b(design|ui|ux|css|style|color|layout|figma|theme|visual|interface)\b/i },
|
|
197
|
+
{ category: 'security', keywords: /\b(security|audit|vuln|owasp|xss|inject|auth|encrypt|cve)\b/i },
|
|
198
|
+
{ category: 'documentation', keywords: /\b(doc|readme|changelog|api-doc|markdown|mdx|writing)\b/i },
|
|
199
|
+
{ category: 'automation', keywords: /\b(automat|script|batch|loop|cron|schedule|workflow)\b/i },
|
|
200
|
+
{ category: 'deployment', keywords: /\b(deploy|release|ci.?cd|docker|kubernetes|infra|nginx|vercel)\b/i },
|
|
201
|
+
{ category: 'code-quality', keywords: /\b(review|lint|refactor|simplif|clean|format|pattern)\b/i },
|
|
202
|
+
{ category: 'development', keywords: /\b(develop|build|debug|investigate|plan|brainstorm|feature|implement)\b/i },
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
function categorize(name, description, triggers) {
|
|
206
|
+
const text = [name, description, ...(triggers || [])].join(' ');
|
|
207
|
+
for (const { category, keywords } of CATEGORY_MAP) {
|
|
208
|
+
if (keywords.test(text)) return category;
|
|
209
|
+
}
|
|
210
|
+
return 'other';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function smartTruncate(text, maxLen) {
|
|
214
|
+
if (text.length <= maxLen) return text;
|
|
215
|
+
const truncated = text.slice(0, maxLen);
|
|
216
|
+
const lastSentence = Math.max(truncated.lastIndexOf('. '), truncated.lastIndexOf('! '), truncated.lastIndexOf('? '));
|
|
217
|
+
if (lastSentence > maxLen * 0.5) return truncated.slice(0, lastSentence + 1);
|
|
218
|
+
const lastSpace = truncated.lastIndexOf(' ');
|
|
219
|
+
if (lastSpace > maxLen * 0.5) return truncated.slice(0, lastSpace) + '...';
|
|
220
|
+
return truncated + '...';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Layer 2: Extract sections (## headings with first paragraph)
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
function extractSections(content) {
|
|
227
|
+
const sections = [];
|
|
228
|
+
// Remove code blocks
|
|
229
|
+
const cleaned = content.replace(/```[\s\S]*?```/g, '');
|
|
230
|
+
const lines = cleaned.split('\n');
|
|
231
|
+
let currentHeading = null;
|
|
232
|
+
let para = '';
|
|
233
|
+
|
|
234
|
+
for (const line of lines) {
|
|
235
|
+
const headingMatch = line.match(/^##\s+(.+)/);
|
|
236
|
+
if (headingMatch) {
|
|
237
|
+
// Save previous section
|
|
238
|
+
if (currentHeading && para.trim().length > 0) {
|
|
239
|
+
sections.push({ title: currentHeading, summary: smartTruncate(para.trim(), 600) });
|
|
240
|
+
if (sections.length >= 15) break;
|
|
241
|
+
}
|
|
242
|
+
currentHeading = headingMatch[1].trim();
|
|
243
|
+
para = '';
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!currentHeading) continue;
|
|
248
|
+
|
|
249
|
+
// Skip table lines, image lines
|
|
250
|
+
if (line.startsWith('|') || line.match(/!\[.*\]/)) continue;
|
|
251
|
+
if (line.trim() === '') {
|
|
252
|
+
if (para.trim().length >= 20) continue; // end of paragraph
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// Skip sub-headings inside section
|
|
256
|
+
if (line.startsWith('###')) continue;
|
|
257
|
+
|
|
258
|
+
para += (para ? ' ' : '') + line.trim();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Last section
|
|
262
|
+
if (currentHeading && para.trim().length > 0 && sections.length < 15) {
|
|
263
|
+
sections.push({ title: currentHeading, summary: smartTruncate(para.trim(), 600) });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return sections;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Layer 3: Extract contextual paragraphs
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
const PATTERN_MAP = {
|
|
273
|
+
whenToUse: /when\s+(?:to\s+)?use|何时用|适用场景/i,
|
|
274
|
+
howItWorks: /how\s+it\s+works|运作原理|how\s+to\s+use|workflow|流程/i,
|
|
275
|
+
limitations: /limit|局限|not\s+do|anti.?pattern|caveat|注意事项/i,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
function extractContextual(content) {
|
|
279
|
+
const result = { whenToUse: '', howItWorks: '', limitations: '' };
|
|
280
|
+
|
|
281
|
+
// Remove code blocks first
|
|
282
|
+
const cleaned = content.replace(/```[\s\S]*?```/g, '');
|
|
283
|
+
const paragraphs = cleaned.split(/\n\s*\n/);
|
|
284
|
+
|
|
285
|
+
for (const para of paragraphs) {
|
|
286
|
+
const trimmed = para.trim();
|
|
287
|
+
const lines = trimmed.split('\n').filter(l => !l.startsWith('|') && !l.match(/^!\[/) && l.trim().length > 0);
|
|
288
|
+
const text = lines.join(' ').trim();
|
|
289
|
+
if (text.length < 20 || text.length > 800) continue;
|
|
290
|
+
|
|
291
|
+
for (const [key, regex] of Object.entries(PATTERN_MAP)) {
|
|
292
|
+
if (!result[key] && regex.test(text)) {
|
|
293
|
+
result[key] = text.slice(0, 500);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Skill scanning
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
const HOME = os.homedir();
|
|
305
|
+
const CODEX_HOME = process.env.CODEX_HOME || path.join(HOME, '.codex');
|
|
306
|
+
|
|
307
|
+
function buildScanSources() {
|
|
308
|
+
const rawSources = [
|
|
309
|
+
{ dir: path.join(HOME, '.claude', 'skills'), label: 'claude-user', priority: 1, depth: 1 },
|
|
310
|
+
{ dir: path.join(CODEX_HOME, 'skills', '.system'), label: 'openai-system', priority: 0, depth: 1 },
|
|
311
|
+
{ dir: path.join(CODEX_HOME, 'skills'), label: 'codex-user', priority: 1, depth: 1 },
|
|
312
|
+
{ dir: path.join(HOME, '.cc-switch', 'skills'), label: 'cc-switch', priority: 2, depth: 1 },
|
|
313
|
+
{ dir: path.join(HOME, '.claude', 'plugins', 'marketplaces'), label: 'claude-plugin', priority: 3, depth: 2 },
|
|
314
|
+
{ dir: path.join(CODEX_HOME, 'plugins', 'cache'), label: 'codex-plugin', priority: 3, depth: 4 },
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
const seen = new Set();
|
|
318
|
+
return rawSources.filter((source) => {
|
|
319
|
+
const key = path.resolve(source.dir);
|
|
320
|
+
if (seen.has(key)) return false;
|
|
321
|
+
seen.add(key);
|
|
322
|
+
return true;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const SCAN_SOURCES = buildScanSources();
|
|
327
|
+
|
|
328
|
+
function tryStatDir(p) {
|
|
329
|
+
try { return fs.statSync(p).isDirectory(); } catch (_) { return false; }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function scanDirectory(dirPath, maxDepth, currentDepth, options = {}) {
|
|
333
|
+
if (currentDepth > maxDepth) return [];
|
|
334
|
+
let entries;
|
|
335
|
+
try {
|
|
336
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
337
|
+
} catch (_) { return []; }
|
|
338
|
+
|
|
339
|
+
const results = [];
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
// Follow symlinks — entry.isDirectory() is false for symlinks without this
|
|
342
|
+
const isDir = entry.isDirectory() || (entry.isSymbolicLink() && tryStatDir(path.join(dirPath, entry.name)));
|
|
343
|
+
if (!isDir) continue;
|
|
344
|
+
if (!options.includeHidden && entry.name.startsWith('.')) continue;
|
|
345
|
+
|
|
346
|
+
const subDir = path.join(dirPath, entry.name);
|
|
347
|
+
|
|
348
|
+
// Check for SKILL.md or README.md
|
|
349
|
+
const skillMd = path.join(subDir, 'SKILL.md');
|
|
350
|
+
const readmeMd = path.join(subDir, 'README.md');
|
|
351
|
+
let mdFile = null;
|
|
352
|
+
|
|
353
|
+
if (fileExists(skillMd)) {
|
|
354
|
+
mdFile = skillMd;
|
|
355
|
+
} else if (fileExists(readmeMd)) {
|
|
356
|
+
mdFile = readmeMd;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (mdFile) {
|
|
360
|
+
results.push({ dir: subDir, mdFile });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Recurse deeper
|
|
364
|
+
if (currentDepth < maxDepth) {
|
|
365
|
+
const deeper = scanDirectory(subDir, maxDepth, currentDepth + 1, options);
|
|
366
|
+
results.push(...deeper);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return results;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function fileExists(p) {
|
|
373
|
+
try { return fs.statSync(p).isFile(); } catch (_) { return false; }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function loadSkill(dir, mdFile) {
|
|
377
|
+
let content;
|
|
378
|
+
try {
|
|
379
|
+
content = fs.readFileSync(mdFile, 'utf8');
|
|
380
|
+
} catch (_) { return null; }
|
|
381
|
+
|
|
382
|
+
const fm = parseFrontmatter(content);
|
|
383
|
+
const name = fm.name || path.basename(dir);
|
|
384
|
+
const description = (fm.description || '').slice(0, 500);
|
|
385
|
+
|
|
386
|
+
// Skill is valid if it has name or description in frontmatter
|
|
387
|
+
if (!fm.name && !fm.description) return null;
|
|
388
|
+
|
|
389
|
+
let triggers = fm.triggers || [];
|
|
390
|
+
if (typeof triggers === 'string') triggers = [triggers];
|
|
391
|
+
|
|
392
|
+
let allowedTools = fm['allowed-tools'] || fm.allowedTools || [];
|
|
393
|
+
if (typeof allowedTools === 'string') allowedTools = [allowedTools];
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
name,
|
|
397
|
+
description,
|
|
398
|
+
category: categorize(name, description, triggers),
|
|
399
|
+
triggers,
|
|
400
|
+
allowedTools,
|
|
401
|
+
version: fm.version || '',
|
|
402
|
+
dir,
|
|
403
|
+
_mdFile: mdFile,
|
|
404
|
+
_content: content,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function scanAllSkills() {
|
|
409
|
+
const allSkills = {};
|
|
410
|
+
const sourceCounts = Object.fromEntries(SCAN_SOURCES.map((source) => [source.label, 0]));
|
|
411
|
+
|
|
412
|
+
for (const source of SCAN_SOURCES) {
|
|
413
|
+
const dirs = scanDirectory(source.dir, source.depth, 0, { includeHidden: source.includeHidden });
|
|
414
|
+
for (const { dir, mdFile } of dirs) {
|
|
415
|
+
const skill = loadSkill(dir, mdFile);
|
|
416
|
+
if (!skill) continue;
|
|
417
|
+
|
|
418
|
+
skill._source = source.label;
|
|
419
|
+
sourceCounts[source.label] = (sourceCounts[source.label] || 0) + 1;
|
|
420
|
+
|
|
421
|
+
const key = skill.name.toLowerCase();
|
|
422
|
+
if (allSkills[key]) {
|
|
423
|
+
// Keep higher priority, track all sources
|
|
424
|
+
const existing = allSkills[key];
|
|
425
|
+
if (!existing.sources.includes(source.label)) {
|
|
426
|
+
existing.sources.push(source.label);
|
|
427
|
+
}
|
|
428
|
+
if (source.priority < existing._priority) {
|
|
429
|
+
// Replace with higher priority data
|
|
430
|
+
skill.sources = existing.sources;
|
|
431
|
+
skill._priority = source.priority;
|
|
432
|
+
allSkills[key] = skill;
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
skill.sources = [source.label];
|
|
436
|
+
skill._priority = source.priority;
|
|
437
|
+
allSkills[key] = skill;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return { skills: Object.values(allSkills), sourceCounts };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// Load full data (Layer 2 + 3) for a specific skill
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
function loadFullData(skill) {
|
|
449
|
+
// Use cached content if available, otherwise re-read
|
|
450
|
+
let content = skill._content;
|
|
451
|
+
if (!content) {
|
|
452
|
+
try {
|
|
453
|
+
content = fs.readFileSync(skill._mdFile, 'utf8');
|
|
454
|
+
} catch (_) { content = ''; }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const sections = extractSections(content);
|
|
458
|
+
const contextual = extractContextual(content);
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
...skill,
|
|
462
|
+
sections,
|
|
463
|
+
howItWorks: contextual.howItWorks,
|
|
464
|
+
whenToUse: contextual.whenToUse,
|
|
465
|
+
limitations: contextual.limitations,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
// Clean skill for output (remove internal fields)
|
|
471
|
+
// ---------------------------------------------------------------------------
|
|
472
|
+
function cleanSkill(skill, includeFull) {
|
|
473
|
+
const base = {
|
|
474
|
+
name: skill.name,
|
|
475
|
+
description: skill.description,
|
|
476
|
+
category: skill.category,
|
|
477
|
+
sources: skill.sources,
|
|
478
|
+
triggers: skill.triggers,
|
|
479
|
+
allowedTools: skill.allowedTools,
|
|
480
|
+
version: skill.version,
|
|
481
|
+
dir: skill.dir.replace(HOME, '~'),
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
if (includeFull) {
|
|
485
|
+
const full = loadFullData(skill);
|
|
486
|
+
base.sections = full.sections;
|
|
487
|
+
base.howItWorks = full.howItWorks;
|
|
488
|
+
base.whenToUse = full.whenToUse;
|
|
489
|
+
base.limitations = full.limitations;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return base;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
// Main
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
function main() {
|
|
499
|
+
let scanResult;
|
|
500
|
+
|
|
501
|
+
// Try cache first
|
|
502
|
+
if (!refresh) {
|
|
503
|
+
const cached = readCache();
|
|
504
|
+
if (cached && cached.skills) {
|
|
505
|
+
scanResult = cached;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!scanResult) {
|
|
510
|
+
scanResult = scanAllSkills();
|
|
511
|
+
// Cache the raw result
|
|
512
|
+
const toCache = {
|
|
513
|
+
scanDate: new Date().toISOString().slice(0, 10),
|
|
514
|
+
totalCount: scanResult.skills.length,
|
|
515
|
+
sourceCounts: scanResult.sourceCounts,
|
|
516
|
+
skills: scanResult.skills.map(s => ({ ...s })),
|
|
517
|
+
};
|
|
518
|
+
writeCache(toCache);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Fix scanDate if from cache
|
|
522
|
+
if (!scanResult.scanDate) {
|
|
523
|
+
scanResult.scanDate = new Date().toISOString().slice(0, 10);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const skills = scanResult.skills;
|
|
527
|
+
const sourceCounts = scanResult.sourceCounts || {};
|
|
528
|
+
|
|
529
|
+
let output;
|
|
530
|
+
|
|
531
|
+
if (mode === 'list') {
|
|
532
|
+
output = {
|
|
533
|
+
scanDate: scanResult.scanDate,
|
|
534
|
+
totalCount: skills.length,
|
|
535
|
+
sources: sourceCounts,
|
|
536
|
+
skills: skills.map(s => cleanSkill(s, false)),
|
|
537
|
+
};
|
|
538
|
+
} else if (mode === 'skill') {
|
|
539
|
+
// Strip plugin prefix (e.g. "everything-claude-code:tdd-workflow" → "tdd-workflow")
|
|
540
|
+
const bareName = skillName.replace(/^[^:]+:/, '');
|
|
541
|
+
const found = skills.find(s => s.name.toLowerCase() === bareName.toLowerCase()
|
|
542
|
+
|| s.name.toLowerCase() === skillName.toLowerCase());
|
|
543
|
+
if (!found) {
|
|
544
|
+
output = { error: `Skill "${skillName}" not found`, skills: [] };
|
|
545
|
+
} else {
|
|
546
|
+
output = {
|
|
547
|
+
scanDate: scanResult.scanDate,
|
|
548
|
+
totalCount: skills.length,
|
|
549
|
+
sources: sourceCounts,
|
|
550
|
+
skills: [cleanSkill(found, true)],
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
} else if (mode === 'search') {
|
|
554
|
+
const q = searchQuery.toLowerCase();
|
|
555
|
+
const matched = skills.filter(s => {
|
|
556
|
+
const nameMatch = s.name.toLowerCase().includes(q);
|
|
557
|
+
const descMatch = s.description.toLowerCase().includes(q);
|
|
558
|
+
const triggerMatch = s.triggers.some(t => t.toLowerCase().includes(q));
|
|
559
|
+
return nameMatch || descMatch || triggerMatch;
|
|
560
|
+
});
|
|
561
|
+
output = {
|
|
562
|
+
scanDate: scanResult.scanDate,
|
|
563
|
+
totalCount: skills.length,
|
|
564
|
+
matchedCount: matched.length,
|
|
565
|
+
sources: sourceCounts,
|
|
566
|
+
skills: matched.map(s => cleanSkill(s, false)),
|
|
567
|
+
};
|
|
568
|
+
} else if (mode === 'full') {
|
|
569
|
+
output = {
|
|
570
|
+
scanDate: scanResult.scanDate,
|
|
571
|
+
totalCount: skills.length,
|
|
572
|
+
sources: sourceCounts,
|
|
573
|
+
skills: skills.map(s => cleanSkill(s, true)),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
main();
|