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
package/SKILL.md
CHANGED
|
@@ -1,266 +1,43 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skill-guide
|
|
3
|
-
description:
|
|
4
|
-
Discover, understand, compare, and choose from installed Agent Skills across
|
|
5
|
-
Claude Code, Codex, local skill folders, and plugin marketplaces. Generates
|
|
6
|
-
HTML slide presentations with skill overviews, deep-dives, and tool
|
|
7
|
-
recommendations. Use when user says "skill-guide", asks what skills they
|
|
8
|
-
have, wants to explore Claude Code or Codex skills, asks "tell me about
|
|
9
|
-
a skill", "which skill for X", "help me understand my skills", or wants to
|
|
10
|
-
map installed agent capabilities.
|
|
3
|
+
description: Use when the user wants to discover, inspect, compare, diagnose, or choose installed Agent Skills across Codex, Claude Code, cc-switch, or plugin directories.
|
|
11
4
|
allowed-tools:
|
|
12
5
|
- Bash
|
|
13
|
-
- Read
|
|
14
|
-
- Write
|
|
15
6
|
---
|
|
16
7
|
|
|
17
8
|
# Skill Guide
|
|
18
9
|
|
|
19
|
-
##
|
|
10
|
+
## Overview
|
|
20
11
|
|
|
21
|
-
|
|
12
|
+
Use the bundled zero-dependency CLI to scan local skill metadata and generate a browser-ready HTML dashboard. Treat health scores and recommendations as local review prompts, not usage metrics or deletion decisions.
|
|
22
13
|
|
|
23
|
-
|
|
24
|
-
- User types `/skill-guide` (bare or with arguments)
|
|
25
|
-
- User asks "what skills do I have", "show me my skills", "what Codex skills do I have"
|
|
26
|
-
- User asks about a specific skill: "tell me about frontend-slides"
|
|
27
|
-
- User describes a task and wants to know which skill fits: "which skill for code review"
|
|
28
|
-
- User says "help me understand my skills"
|
|
14
|
+
## Workflow
|
|
29
15
|
|
|
30
|
-
|
|
16
|
+
1. Infer the requested mode from the user's request.
|
|
17
|
+
2. Run the matching command from this skill directory.
|
|
18
|
+
3. Add `--lang zh` for a Chinese dashboard. English is the default.
|
|
19
|
+
4. Add `--open` unless the user asks for terminal output or a file only.
|
|
20
|
+
5. Summarize the generated result and report the output path when HTML is created.
|
|
31
21
|
|
|
32
|
-
|
|
22
|
+
| User goal | Command |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Dashboard | `node <skill-dir>/skill-guide.js` |
|
|
25
|
+
| Find or inspect a skill | `node <skill-dir>/skill-guide.js --find "<query>"` |
|
|
26
|
+
| Diagnose local setup | `node <skill-dir>/skill-guide.js --doctor` |
|
|
27
|
+
| Review directory mentions | `node <skill-dir>/skill-guide.js --recommend` |
|
|
28
|
+
| Generate a share page | `node <skill-dir>/skill-guide.js --share [--user <name>]` |
|
|
29
|
+
| Generate a full manual | `node <skill-dir>/skill-guide.js --full` |
|
|
30
|
+
| Return structured data | `node <skill-dir>/skill-guide.js --format json` |
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|------------|------|-------------|
|
|
36
|
-
| `/skill-guide` (bare, no args) | discovery | `--list` |
|
|
37
|
-
| `/skill-guide <name>` or "tell me about <name>" | deep-dive | `--skill <name>` |
|
|
38
|
-
| Task description or "which skill for X" | tool-selection | `--search <query>` |
|
|
39
|
-
| `/skill-guide all` | full-manual | `--full` |
|
|
32
|
+
Use `--refresh` when the user expects newly installed or removed skills to appear. Use `--all` only when the user wants a cross-platform inventory. Add `--platform claude` or `--platform codex` to force a specific platform view regardless of auto-detection.
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
1. If argument is "all" (case-insensitive) -> full-manual mode
|
|
43
|
-
2. If argument exactly matches a known skill name -> deep-dive mode
|
|
44
|
-
3. If argument contains verbs (e.g., "review", "build", "test") or question words ("which", "what", "how") -> tool-selection mode
|
|
45
|
-
4. If bare `/skill-guide` with no argument -> discovery mode
|
|
34
|
+
## Guardrails
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- Korean: input contains Hangul (`/[가-힣]/`) → lang = `ko`
|
|
56
|
-
- Any other non-English language: if the user writes in a language other than English, detect it and set lang accordingly (e.g., `fr`, `de`, `es`, `pt`, `ru`, etc.)
|
|
57
|
-
- English or cannot determine: lang = `en` (default, no translation needed)
|
|
58
|
-
3. **Run the deterministic CLI**: `node <skill-dir>/skill-guide.js <flag> [args] [--lang <lang>]`, where `<skill-dir>` is the directory containing this SKILL.md file.
|
|
59
|
-
- If lang is `en`, add `--open` to the CLI command (open directly, no translation needed).
|
|
60
|
-
- If lang is NOT `en`, do NOT add `--open` (translation happens before opening).
|
|
61
|
-
4. Map modes to CLI flags:
|
|
62
|
-
- Discovery: `node <skill-dir>/skill-guide.js`
|
|
63
|
-
- Deep-dive: `node <skill-dir>/skill-guide.js --skill <name>`
|
|
64
|
-
- Tool-selection: `node <skill-dir>/skill-guide.js --search "<query>"`
|
|
65
|
-
- Full manual: `node <skill-dir>/skill-guide.js --full`
|
|
66
|
-
- Diagnostics: `node <skill-dir>/skill-guide.js --doctor`
|
|
67
|
-
Append `--lang <lang>` to any command when a non-English language is detected.
|
|
68
|
-
5. If `skill-guide.js` is unavailable, fall back to running `scan-skills.js` and generating HTML using the rules below.
|
|
69
|
-
6. **Post-translate for non-English languages** (when lang is NOT `en`):
|
|
70
|
-
After the CLI generates the HTML file, perform a full translation pass:
|
|
71
|
-
a. Read the generated HTML file.
|
|
72
|
-
b. Translate ALL English text content inside elements with `data-i18n` attributes, AND all UI label text (headings, kicker, subtitle, section headers like "运作原理", "何时使用", etc.) to the user's detected language.
|
|
73
|
-
- The `data-i18n` attributes mark translatable content: `desc` (descriptions), `section-title` (step/section titles), `section-body` (step/section content), `how-it-works`, `when-to-use`, `limitations`.
|
|
74
|
-
- Also translate any other visible text: cover titles, category names, stats labels, navigation text, and any Chinese/English UI labels already in the HTML.
|
|
75
|
-
- Preserve ALL HTML tags, CSS classes, JavaScript, attribute names, and document structure exactly as-is.
|
|
76
|
-
- Only translate the **text content** between tags — never modify tag names, attributes, CSS, or JavaScript.
|
|
77
|
-
- Do NOT translate: content inside `<code>` tags, skill names, tool names, CSS property values, JavaScript code.
|
|
78
|
-
- Translate naturally as a fluent native speaker would write it — not word-by-word. Adapt sentence structure for natural readability in the target language.
|
|
79
|
-
- Keep markdown-derived formatting: `<strong>` stays `<strong>`, `<code>` stays `<code>`, `<blockquote>` stays `<blockquote>`.
|
|
80
|
-
c. Write the translated HTML back to the same file path using the Write tool.
|
|
81
|
-
d. Open the file in the browser: `open <filepath>` (macOS) or equivalent.
|
|
82
|
-
e. Report completion to the user with a brief summary in their language.
|
|
83
|
-
7. **English mode**: When lang is `en`, add `--open` to the CLI command directly and skip step 6.
|
|
84
|
-
|
|
85
|
-
## 4. HTML Generation Rules
|
|
86
|
-
|
|
87
|
-
### Technical Requirements
|
|
88
|
-
|
|
89
|
-
- Single-file HTML with all CSS and JS inlined. Zero external dependencies.
|
|
90
|
-
- Full-screen scroll-snap slides:
|
|
91
|
-
```css
|
|
92
|
-
.slide {
|
|
93
|
-
height: 100vh;
|
|
94
|
-
height: 100dvh;
|
|
95
|
-
overflow: hidden;
|
|
96
|
-
scroll-snap-align: start;
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
- CSS custom properties with `clamp()` for responsive sizing.
|
|
100
|
-
- Keyboard navigation (arrow keys), scroll wheel, and touch swipe.
|
|
101
|
-
- IntersectionObserver for entrance animations: elements with class `.rv` get class `.v` added when visible.
|
|
102
|
-
- Navigation dots on the right side.
|
|
103
|
-
- Progress bar at the top.
|
|
104
|
-
- Page number: bottom-right corner as `<div class="sn-txt">N/total</div>`.
|
|
105
|
-
- `@media (prefers-reduced-motion: reduce)` disables animations.
|
|
106
|
-
- Every slide MUST fit within the viewport. If content overflows, split into multiple slides.
|
|
107
|
-
|
|
108
|
-
### Color Theme CSS Variables
|
|
109
|
-
|
|
110
|
-
```css
|
|
111
|
-
:root {
|
|
112
|
-
--bg: #eef2ff;
|
|
113
|
-
--card: #fff;
|
|
114
|
-
--cs: 0 4px 20px rgba(100,100,180,0.07);
|
|
115
|
-
--t: #1e293b;
|
|
116
|
-
--ts2: #64748b;
|
|
117
|
-
--ab: #818cf8;
|
|
118
|
-
--ap: #f0abfc;
|
|
119
|
-
--am: #6ee7b7;
|
|
120
|
-
--ao: #fdba74;
|
|
121
|
-
--ay: #fde047;
|
|
122
|
-
--al: #c4b5fd;
|
|
123
|
-
--ar: #fda4af;
|
|
124
|
-
--as: #7dd3fc;
|
|
125
|
-
--r: clamp(10px,1.8vw,18px);
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Each slide gets a subtle gradient background and decorative blurred circles (pseudo-elements with `filter: blur(80px)`, low opacity, positioned absolutely) for atmosphere.
|
|
130
|
-
|
|
131
|
-
### Per-Mode Page Structure
|
|
132
|
-
|
|
133
|
-
#### Discovery Mode (4 pages)
|
|
134
|
-
|
|
135
|
-
**Page 1 - Cover:**
|
|
136
|
-
- Title: If scanner sources include both Claude and Codex, use "Your Agent Skills" (EN) or "你的 Agent Skills 技能库" (ZH). If only Claude sources are present, use "Your Claude Code Skills" / "你的 Claude Code 技能库". If only Codex sources are present, use "Your Codex Skills" / "你的 Codex 技能库".
|
|
137
|
-
- Subtitle: total skill count + per-source breakdown (e.g., "12 Claude skills, 8 Codex skills, 20 plugin skills")
|
|
138
|
-
- Decorative background with gradient and blurred circles
|
|
139
|
-
|
|
140
|
-
**Page 2 - Category Map:**
|
|
141
|
-
- Group skills by their `category` field
|
|
142
|
-
- 3-column card grid
|
|
143
|
-
- Each card shows: skill name, 1-line description, category badge (colored pill)
|
|
144
|
-
- Use distinct accent colors per category for visual separation
|
|
145
|
-
|
|
146
|
-
**Page 3 - Highlights:**
|
|
147
|
-
- Top 5-8 most versatile skills (ranked by number of triggers or multiple sources)
|
|
148
|
-
- Each highlight: skill name + full description + trigger words as small badges
|
|
149
|
-
- Use a prominent card layout with the accent color
|
|
150
|
-
|
|
151
|
-
**Page 4 - Quick Reference:**
|
|
152
|
-
- Table with columns: Name | Description | Triggers | Category
|
|
153
|
-
- Compact rows, alternating background for readability
|
|
154
|
-
- Sticky header if content is long
|
|
155
|
-
|
|
156
|
-
#### Deep-dive Mode (1-3 pages per skill)
|
|
157
|
-
|
|
158
|
-
**Page 1 - Overview:**
|
|
159
|
-
- Large skill name as title
|
|
160
|
-
- Full description as subtitle
|
|
161
|
-
- Category badge (colored pill)
|
|
162
|
-
- Source badge (showing where the skill comes from)
|
|
163
|
-
- List of `allowedTools` as code-styled tags
|
|
164
|
-
|
|
165
|
-
**Page 2 - How It Works:**
|
|
166
|
-
- Render `sections` array as a numbered step flow with circle numbers (1, 2, 3...)
|
|
167
|
-
- If `howItWorks` field exists, render it as a detail/expandable box
|
|
168
|
-
- Use connecting lines or arrows between steps for flow visualization
|
|
169
|
-
|
|
170
|
-
**Page 3 - When to Use:**
|
|
171
|
-
- `whenToUse` items as green "Use when..." tags
|
|
172
|
-
- `limitations` items as orange "Caution" tags
|
|
173
|
-
- `triggers` as keyword badges in a separate section
|
|
174
|
-
- If Page 2 + Page 3 content combined is short enough for one slide, merge them
|
|
175
|
-
|
|
176
|
-
#### Tool-selection Mode (2-3 pages)
|
|
177
|
-
|
|
178
|
-
**Page 1 - Match Results:**
|
|
179
|
-
- User's task description displayed as a quote/callout
|
|
180
|
-
- Matched skills ranked by relevance (count of trigger matches + description keyword overlap)
|
|
181
|
-
- Each match shows: name, relevance score, top matching triggers
|
|
182
|
-
|
|
183
|
-
**Page 2 - Side-by-side Comparison:**
|
|
184
|
-
- Top 2-3 matches in a comparison layout
|
|
185
|
-
- Columns: Feature | Skill A | Skill B | Skill C
|
|
186
|
-
- Rows: Description, Triggers, When to Use, Limitations, Tools
|
|
187
|
-
|
|
188
|
-
**Page 3 - Workflow Suggestion:**
|
|
189
|
-
- Flow diagram showing how skills can combine: Skill A -> Skill B -> Result
|
|
190
|
-
- Use CSS-only arrows between skill boxes
|
|
191
|
-
- Brief explanation of why this combination works
|
|
192
|
-
|
|
193
|
-
#### Full Manual Mode
|
|
194
|
-
|
|
195
|
-
**Page 1 - Cover:**
|
|
196
|
-
- Title: "Complete Skill Manual" / "完整技能手册"
|
|
197
|
-
- Total skill count + source breakdown
|
|
198
|
-
|
|
199
|
-
**Page 2 - Category Index:**
|
|
200
|
-
- Category names with skill count per category
|
|
201
|
-
- Clickable/visual index layout
|
|
202
|
-
|
|
203
|
-
**One page per skill (compact format):**
|
|
204
|
-
- Skill name as heading
|
|
205
|
-
- 2-line description
|
|
206
|
-
- Trigger words as badges
|
|
207
|
-
- When-to-use summary (1-2 lines)
|
|
208
|
-
- Keep each skill to one page maximum
|
|
209
|
-
|
|
210
|
-
**Last page - Quick Reference Table:**
|
|
211
|
-
- Same as Discovery Page 4 but covering all skills
|
|
212
|
-
|
|
213
|
-
**Overflow protection:** If total skills > 30, warn the user before generating: "This will produce N pages. Continue?" Then proceed only after confirmation.
|
|
214
|
-
|
|
215
|
-
### Content Mapping from JSON
|
|
216
|
-
|
|
217
|
-
Map scanner JSON fields to HTML content:
|
|
218
|
-
|
|
219
|
-
| JSON Field | HTML Section |
|
|
220
|
-
|-----------|-------------|
|
|
221
|
-
| `name` + `description` | Slide title, subtitle, card header |
|
|
222
|
-
| `howItWorks` | "How it works" detail box |
|
|
223
|
-
| `sections` | Numbered step flow |
|
|
224
|
-
| `whenToUse` + `triggers` | "When to use" tags + trigger keyword list |
|
|
225
|
-
| `limitations` | "Limitations" caution area |
|
|
226
|
-
| `allowedTools` | Tools list (collapsible, for advanced users) |
|
|
227
|
-
| `category` | Category badge, used for grouping and coloring |
|
|
228
|
-
| `source` | Source badge showing origin |
|
|
229
|
-
|
|
230
|
-
### Language Handling
|
|
231
|
-
|
|
232
|
-
The CLI uses `--lang zh` for Chinese labels as a built-in default. For any other language, the agent-side translation step (workflow step 6) handles ALL UI labels and content translation. The agent translates everything — both `data-i18n` marked content and any other visible text (headings, kicker, stats labels, etc.) — to the detected target language.
|
|
233
|
-
|
|
234
|
-
The CLI's built-in Chinese label map is used as a starting point when `--lang zh` is passed. For all other languages, the CLI generates English labels and the agent translates them.
|
|
235
|
-
|
|
236
|
-
| English Label | Chinese Label (built-in) |
|
|
237
|
-
|--------------|---------------|
|
|
238
|
-
| Your Agent Skills | 你的 Agent Skills 技能库 |
|
|
239
|
-
| Your Claude Code Skills | 你的 Claude Code 技能库 |
|
|
240
|
-
| Your Codex Skills | 你的 Codex 技能库 |
|
|
241
|
-
| Category Map | 分类概览 |
|
|
242
|
-
| Highlights | 精选推荐 |
|
|
243
|
-
| Quick Reference | 快速参考 |
|
|
244
|
-
| How It Works | 运作原理 |
|
|
245
|
-
| When to Use | 何时使用 |
|
|
246
|
-
| Limitations | 使用限制 |
|
|
247
|
-
| Triggers | 触发词 |
|
|
248
|
-
| Complete Skill Manual | 完整技能手册 |
|
|
249
|
-
| Match Results | 匹配结果 |
|
|
250
|
-
| Comparison | 对比分析 |
|
|
251
|
-
| Workflow Suggestion | 工作流建议 |
|
|
252
|
-
| Use when... | 适用场景... |
|
|
253
|
-
| Caution | 注意 |
|
|
254
|
-
| Tools | 工具列表 |
|
|
255
|
-
| Source | 来源 |
|
|
256
|
-
| Category | 分类 |
|
|
257
|
-
|
|
258
|
-
For all other languages (Japanese, Korean, French, German, etc.), the agent translates these labels as part of step 6.
|
|
259
|
-
|
|
260
|
-
## 5. Anti-Patterns
|
|
261
|
-
|
|
262
|
-
- **Never fabricate skill descriptions.** Only use data returned by the scanner. If the scanner returns empty or fails, show an error slide instead of guessing.
|
|
263
|
-
- **Never skip running the scanner.** The scanner is the sole source of truth for installed skills.
|
|
264
|
-
- **Never generate more than 30 pages without asking.** Prompt the user first.
|
|
265
|
-
- **Never include skills without a name.** Skip malformed entries and log a warning to console.
|
|
266
|
-
- **Never hard-code skill lists.** Always derive from scanner output at generation time.
|
|
36
|
+
- The scanner reads local metadata; it does not measure actual usage frequency.
|
|
37
|
+
- Same-category suggestions and directory mentions require human review before installing, editing, or deleting anything.
|
|
38
|
+
- Never delete skills, install packages, or publish results based only on a generated score.
|
|
39
|
+
- The default dashboard includes a "Review Candidates" slide with evidence-based findings and a Copy Prompt button. The CLI only prepares evidence and questions — semantic judgment comes from the agent. Always wait for agent assessment before acting on any finding.
|
|
40
|
+
- The `--review --format json` flag outputs a structured brief for programmatic agent consumption (no HTML).
|
|
41
|
+
- Built-in dashboard labels support English and Chinese. For other languages, summarize the result in the user's language without rewriting generated HTML unless explicitly requested.
|
|
42
|
+
- If the CLI is unavailable, report the missing file instead of silently replacing its behavior.
|
|
43
|
+
- All modes default to the current platform (auto-detected from env vars or install path). Cross-agent duplicates are normal — each agent needs its own skill copies. Use `--all` to see the full inventory across all agents.
|
package/demo-categories.png
CHANGED
|
Binary file
|
package/demo-cover.png
CHANGED
|
Binary file
|
package/demo-highlights.png
CHANGED
|
Binary file
|
package/demo-reference.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-guide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Scan 6+ skill directories across Claude Code, Codex, and cc-switch, then generate beautiful HTML slide presentations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"SKILL.md",
|
|
15
15
|
"skill-guide.js",
|
|
16
16
|
"scan-skills.js",
|
|
17
|
+
"skill-registry.js",
|
|
17
18
|
"demo.html",
|
|
18
19
|
"demo.gif",
|
|
19
20
|
"social-preview.png",
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
"scan:full": "node scan-skills.js --full",
|
|
32
33
|
"guide": "node skill-guide.js --open",
|
|
33
34
|
"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
|
+
"test": "node --check scan-skills.js && node --check skill-guide.js && node --check skill-registry.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
|
},
|
|
36
37
|
"engines": {
|
|
37
38
|
"node": ">=18"
|
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
|
}
|