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/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
- ## 1. When to Use
10
+ ## Overview
20
11
 
21
- Activate this skill when the user wants to explore, discover, compare, or learn about their installed Agent Skills across Claude Code, Codex, local skill folders, and plugin marketplaces. The output is a polished HTML slide presentation opened in the browser.
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
- **Trigger conditions:**
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
- ## 2. Mode Detection
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
- Determine the mode from user input:
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
- | User Input | Mode | Scanner Flag |
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
- **Resolution rules (apply in order):**
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
- ## 3. Workflow
48
-
49
- Follow these steps exactly:
50
-
51
- 1. **Determine mode** from user input using the rules in section 2.
52
- 2. **Detect language**: Determine the user's language from their input.
53
- - Chinese: input contains Chinese characters lang = `zh`
54
- - Japanese: input contains Japanese characters (`/[あ-んア-ンヶッ]/` or hiragana/katakana) lang = `ja`
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.
Binary file
package/demo-cover.png CHANGED
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-guide",
3
- "version": "0.2.1",
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 === '|' || 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
- if (val === '|' || val === '>') {
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
- const sections = extractSections(content);
458
- const contextual = extractContextual(content);
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
  }