skill-guide 0.2.1 → 0.3.1
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 +28 -3
- package/README.md +114 -28
- package/SKILL.md +25 -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 +2450 -34
- package/skill-registry.js +288 -0
package/scan-skills.js
CHANGED
|
@@ -122,7 +122,7 @@ function parseFrontmatter(content) {
|
|
|
122
122
|
continue;
|
|
123
123
|
}
|
|
124
124
|
// End of multiline block
|
|
125
|
-
if (multilineType
|
|
125
|
+
if (/^[>|]/.test(multilineType)) {
|
|
126
126
|
if (multilineValue && !Array.isArray(result[currentKey])) {
|
|
127
127
|
result[currentKey] = multilineValue.trim();
|
|
128
128
|
}
|
|
@@ -137,8 +137,9 @@ function parseFrontmatter(content) {
|
|
|
137
137
|
currentKey = kvMatch[1];
|
|
138
138
|
let val = kvMatch[2].trim();
|
|
139
139
|
|
|
140
|
-
// Multi-line indicators
|
|
141
|
-
|
|
140
|
+
// Multi-line indicators — skip if value is quoted
|
|
141
|
+
const isQuoted = (val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"));
|
|
142
|
+
if (!isQuoted && /^[>|](-|\+)?$/.test(val)) {
|
|
142
143
|
inMultiline = true;
|
|
143
144
|
multilineType = val;
|
|
144
145
|
multilineValue = '';
|
|
@@ -202,7 +203,16 @@ const CATEGORY_MAP = [
|
|
|
202
203
|
{ category: 'development', keywords: /\b(develop|build|debug|investigate|plan|brainstorm|feature|implement)\b/i },
|
|
203
204
|
];
|
|
204
205
|
|
|
205
|
-
function categorize(name, description, triggers) {
|
|
206
|
+
function categorize(name, description, triggers, tags) {
|
|
207
|
+
// Priority 1: tags match
|
|
208
|
+
if (tags && tags.length > 0) {
|
|
209
|
+
const tagText = tags.join(' ');
|
|
210
|
+
for (const { category, keywords } of CATEGORY_MAP) {
|
|
211
|
+
if (keywords.test(tagText)) return category;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Priority 2: description match
|
|
206
216
|
const text = [name, description, ...(triggers || [])].join(' ');
|
|
207
217
|
for (const { category, keywords } of CATEGORY_MAP) {
|
|
208
218
|
if (keywords.test(text)) return category;
|
|
@@ -220,6 +230,26 @@ function smartTruncate(text, maxLen) {
|
|
|
220
230
|
return truncated + '...';
|
|
221
231
|
}
|
|
222
232
|
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Layer 1.5: Extract summary (first body paragraph before headings)
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
function extractSummary(bodyContent) {
|
|
237
|
+
const paragraphs = bodyContent.split(/\n\s*\n/);
|
|
238
|
+
for (const para of paragraphs) {
|
|
239
|
+
const trimmed = para.trim();
|
|
240
|
+
if (!trimmed) continue;
|
|
241
|
+
if (trimmed.startsWith('##')) break;
|
|
242
|
+
if (trimmed.startsWith('```')) continue;
|
|
243
|
+
if (trimmed.match(/^!\[/)) continue;
|
|
244
|
+
if (trimmed.startsWith('|')) continue;
|
|
245
|
+
const text = trimmed.replace(/\s+/g, ' ').trim();
|
|
246
|
+
if (text.length >= 20) {
|
|
247
|
+
return smartTruncate(text, 200);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return '';
|
|
251
|
+
}
|
|
252
|
+
|
|
223
253
|
// ---------------------------------------------------------------------------
|
|
224
254
|
// Layer 2: Extract sections (## headings with first paragraph)
|
|
225
255
|
// ---------------------------------------------------------------------------
|
|
@@ -392,12 +422,16 @@ function loadSkill(dir, mdFile) {
|
|
|
392
422
|
let allowedTools = fm['allowed-tools'] || fm.allowedTools || [];
|
|
393
423
|
if (typeof allowedTools === 'string') allowedTools = [allowedTools];
|
|
394
424
|
|
|
425
|
+
let tags = fm.tags || [];
|
|
426
|
+
if (typeof tags === 'string') tags = [tags];
|
|
427
|
+
|
|
395
428
|
return {
|
|
396
429
|
name,
|
|
397
430
|
description,
|
|
398
|
-
category: categorize(name, description, triggers),
|
|
431
|
+
category: categorize(name, description, triggers, tags),
|
|
399
432
|
triggers,
|
|
400
433
|
allowedTools,
|
|
434
|
+
tags,
|
|
401
435
|
version: fm.version || '',
|
|
402
436
|
dir,
|
|
403
437
|
_mdFile: mdFile,
|
|
@@ -454,18 +488,70 @@ function loadFullData(skill) {
|
|
|
454
488
|
} catch (_) { content = ''; }
|
|
455
489
|
}
|
|
456
490
|
|
|
457
|
-
|
|
458
|
-
const
|
|
491
|
+
// Strip frontmatter before body extraction
|
|
492
|
+
const bodyContent = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
493
|
+
|
|
494
|
+
const sections = extractSections(bodyContent);
|
|
495
|
+
const contextual = extractContextual(bodyContent);
|
|
496
|
+
const summary = extractSummary(bodyContent);
|
|
459
497
|
|
|
460
498
|
return {
|
|
461
499
|
...skill,
|
|
462
500
|
sections,
|
|
501
|
+
summary,
|
|
463
502
|
howItWorks: contextual.howItWorks,
|
|
464
503
|
whenToUse: contextual.whenToUse,
|
|
465
504
|
limitations: contextual.limitations,
|
|
466
505
|
};
|
|
467
506
|
}
|
|
468
507
|
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
// Compute completeness score (0-100) for documentation quality
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
const GARBAGE_PATTERNS = /^[>|](-|\+)?$|^---|^\s*$|^category:|^tags:/;
|
|
512
|
+
|
|
513
|
+
function computeCompleteness(skill, full) {
|
|
514
|
+
let score = 0;
|
|
515
|
+
|
|
516
|
+
// description: 20 points — non-empty and not a YAML artifact
|
|
517
|
+
if (skill.description && skill.description.length > 2 && !GARBAGE_PATTERNS.test(skill.description)) {
|
|
518
|
+
score += 20;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// summary: 20 points — non-empty and not a YAML artifact
|
|
522
|
+
if (full && full.summary && full.summary.length > 0 && !GARBAGE_PATTERNS.test(full.summary)) {
|
|
523
|
+
score += 20;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// whenToUse: 20 points — non-empty and no YAML leakage
|
|
527
|
+
if (full && full.whenToUse && full.whenToUse.length > 20 && !full.whenToUse.startsWith('---')) {
|
|
528
|
+
score += 20;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// howItWorks: 10 points — no YAML metadata
|
|
532
|
+
if (full && full.howItWorks && full.howItWorks.length > 20 &&
|
|
533
|
+
!/^---/.test(full.howItWorks) && !/^category:/.test(full.howItWorks) && !/^tags:/.test(full.howItWorks)) {
|
|
534
|
+
score += 10;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// tags: 10 points
|
|
538
|
+
if (skill.tags && skill.tags.length > 0) {
|
|
539
|
+
score += 10;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// triggers: 10 points
|
|
543
|
+
if (skill.triggers && skill.triggers.length > 0) {
|
|
544
|
+
score += 10;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// limitations: 10 points
|
|
548
|
+
if (full && full.limitations && full.limitations.length > 10) {
|
|
549
|
+
score += 10;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return score;
|
|
553
|
+
}
|
|
554
|
+
|
|
469
555
|
// ---------------------------------------------------------------------------
|
|
470
556
|
// Clean skill for output (remove internal fields)
|
|
471
557
|
// ---------------------------------------------------------------------------
|
|
@@ -476,22 +562,107 @@ function cleanSkill(skill, includeFull) {
|
|
|
476
562
|
category: skill.category,
|
|
477
563
|
sources: skill.sources,
|
|
478
564
|
triggers: skill.triggers,
|
|
565
|
+
tags: skill.tags,
|
|
479
566
|
allowedTools: skill.allowedTools,
|
|
480
567
|
version: skill.version,
|
|
481
568
|
dir: skill.dir.replace(HOME, '~'),
|
|
569
|
+
tokenCost: estimateTokens(skill.description),
|
|
482
570
|
};
|
|
483
571
|
|
|
484
572
|
if (includeFull) {
|
|
485
573
|
const full = loadFullData(skill);
|
|
486
574
|
base.sections = full.sections;
|
|
575
|
+
base.summary = full.summary;
|
|
487
576
|
base.howItWorks = full.howItWorks;
|
|
488
577
|
base.whenToUse = full.whenToUse;
|
|
489
578
|
base.limitations = full.limitations;
|
|
579
|
+
base.completeness = computeCompleteness(skill, full);
|
|
490
580
|
}
|
|
491
581
|
|
|
492
582
|
return base;
|
|
493
583
|
}
|
|
494
584
|
|
|
585
|
+
function estimateTokens(text) {
|
|
586
|
+
if (!text) return 0;
|
|
587
|
+
// Rough estimate: ~4 characters per token for English text
|
|
588
|
+
// This is conservative; actual tokenization varies by model
|
|
589
|
+
return Math.ceil(text.length / 4);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function computeHealthStats(skills) {
|
|
593
|
+
const CONTEXT_WINDOW = 200_000; // Claude's context window
|
|
594
|
+
const DESCRIPTION_BUDGET = 16_000; // ~1% of context for skill descriptions
|
|
595
|
+
const STALE_DAYS = 30;
|
|
596
|
+
|
|
597
|
+
let totalDescriptionLength = 0;
|
|
598
|
+
let totalTokenEstimate = 0;
|
|
599
|
+
const staleSkills = [];
|
|
600
|
+
const securityFlags = [];
|
|
601
|
+
const duplicates = new Map();
|
|
602
|
+
|
|
603
|
+
for (const skill of skills) {
|
|
604
|
+
// Token cost
|
|
605
|
+
const descLen = (skill.description || '').length;
|
|
606
|
+
totalDescriptionLength += descLen;
|
|
607
|
+
totalTokenEstimate += estimateTokens(skill.description);
|
|
608
|
+
|
|
609
|
+
// Stale detection (based on file mtime)
|
|
610
|
+
try {
|
|
611
|
+
const stat = fs.statSync(skill._mdFile);
|
|
612
|
+
const daysSinceModified = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24);
|
|
613
|
+
if (daysSinceModified > STALE_DAYS) {
|
|
614
|
+
staleSkills.push({
|
|
615
|
+
name: skill.name,
|
|
616
|
+
daysSinceModified: Math.floor(daysSinceModified),
|
|
617
|
+
lastModified: stat.mtime.toISOString().slice(0, 10),
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
} catch (_) { /* ignore stat errors */ }
|
|
621
|
+
|
|
622
|
+
// Security red flags (simple patterns)
|
|
623
|
+
const content = (skill._content || '').toLowerCase();
|
|
624
|
+
const flags = [];
|
|
625
|
+
if (content.includes('curl ') && content.includes(' | ')) flags.push('pipe-from-curl');
|
|
626
|
+
if (content.includes('eval(') || content.includes('exec(')) flags.push('eval-exec');
|
|
627
|
+
if (content.includes('api_key') || content.includes('apikey') || content.includes('token')) flags.push('handles-secrets');
|
|
628
|
+
if (content.includes('rm -rf') || content.includes('rmdir /s')) flags.push('destructive-commands');
|
|
629
|
+
if (flags.length > 0) {
|
|
630
|
+
securityFlags.push({ name: skill.name, flags });
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Duplicate detection (by normalized name)
|
|
634
|
+
const normalizedName = skill.name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
635
|
+
if (duplicates.has(normalizedName)) {
|
|
636
|
+
duplicates.get(normalizedName).push(skill.name);
|
|
637
|
+
} else {
|
|
638
|
+
duplicates.set(normalizedName, [skill.name]);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Filter out non-duplicates
|
|
643
|
+
const duplicateGroups = [...duplicates.entries()]
|
|
644
|
+
.filter(([, names]) => names.length > 1)
|
|
645
|
+
.map(([normalized, names]) => ({ normalized, names }));
|
|
646
|
+
|
|
647
|
+
// Hidden skills calculation
|
|
648
|
+
const hiddenCount = DESCRIPTION_BUDGET > 0
|
|
649
|
+
? Math.max(0, Math.floor((totalDescriptionLength - DESCRIPTION_BUDGET) / 100))
|
|
650
|
+
: 0;
|
|
651
|
+
|
|
652
|
+
return {
|
|
653
|
+
totalSkills: skills.length,
|
|
654
|
+
totalDescriptionLength,
|
|
655
|
+
totalTokenEstimate,
|
|
656
|
+
descriptionBudget: DESCRIPTION_BUDGET,
|
|
657
|
+
budgetUsedPercent: Math.round((totalDescriptionLength / DESCRIPTION_BUDGET) * 100),
|
|
658
|
+
hiddenSkillEstimate: Math.min(hiddenCount, skills.length),
|
|
659
|
+
staleSkills: staleSkills.sort((a, b) => b.daysSinceModified - a.daysSinceModified),
|
|
660
|
+
securityFlags,
|
|
661
|
+
duplicateGroups,
|
|
662
|
+
contextWindowPercent: Math.round((totalTokenEstimate / CONTEXT_WINDOW) * 100 * 100) / 100,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
495
666
|
function normalizeSkillName(name) {
|
|
496
667
|
return String(name || '').replace(/^[^:]+:/, '').toLowerCase();
|
|
497
668
|
}
|