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-guide.js CHANGED
@@ -8,6 +8,7 @@ const path = require('path');
8
8
 
9
9
  const ROOT = __dirname;
10
10
  const SCANNER = path.join(ROOT, 'scan-skills.js');
11
+ const registryModule = require('./skill-registry');
11
12
  const args = process.argv.slice(2);
12
13
 
13
14
  function hasFlag(flag) {
@@ -23,17 +24,26 @@ function getArgValue(flag) {
23
24
  function usage() {
24
25
  return [
25
26
  'Usage:',
26
- ' skill-guide [--open] [--output <file>] [--format html|json] [--lang en|zh] [--refresh]',
27
- ' skill-guide --search <query> [--open] [--output <file>] [--format html|json] [--lang en|zh]',
28
- ' skill-guide --skill <name> [--open] [--output <file>] [--format html|json] [--lang en|zh]',
29
- ' skill-guide --full [--open] [--output <file>] [--format html|json] [--lang en|zh]',
30
- ' skill-guide --doctor [--refresh]',
27
+ ' skill-guide # Dashboard: personality, radar, insights (opens HTML)',
28
+ ' skill-guide --find <name|query> # Deep dive or search (opens HTML)',
29
+ ' skill-guide --doctor # Quick environment diagnostic',
30
+ ' skill-guide --review --format json # Structured review brief for agents',
31
+ '',
32
+ 'Options:',
33
+ ' --output <file> Write to file instead of default',
34
+ ' --format json JSON output (no HTML)',
35
+ ' --lang en|zh UI language',
36
+ ' --refresh Force re-scan (ignore cache)',
37
+ ' --all Show skills from all platforms (default: current platform)',
38
+ ' --platform <name> Force platform: claude | codex',
39
+ ' --no-open Do not open HTML in browser',
31
40
  '',
32
41
  'Examples:',
33
- ' npx skill-guide --open',
34
- ' npx skill-guide --search security --open',
35
- ' npx skill-guide --skill tdd --lang zh --open',
36
- ' npx skill-guide --doctor',
42
+ ' npx skill-guide # See your dashboard',
43
+ ' npx skill-guide --find investigate # Deep dive into a skill',
44
+ ' npx skill-guide --find security # Search for security skills',
45
+ ' npx skill-guide --doctor # Check for issues',
46
+ ' npx skill-guide --review --format json # Review brief for agents',
37
47
  ].join('\n');
38
48
  }
39
49
 
@@ -66,6 +76,46 @@ const LABELS = {
66
76
  category: 'Category',
67
77
  description: 'Description',
68
78
  triggers: 'Triggers',
79
+ skillRecommendations: 'Skill Recommendations',
80
+ yourSkillStack: 'Your skill stack',
81
+ gapAnalysis: 'Gap Analysis',
82
+ noSkillsInCategory: 'You have no {category} skills installed',
83
+ tryThese: 'Try these',
84
+ overlapAlert: 'Review Candidates',
85
+ skillsInCategory: 'You have {count} skills in "{category}" category',
86
+ considerKeeping: 'Review which ones you actually use',
87
+ popularYoureMissing: 'Skills Mentioned in Directories',
88
+ categoriesCovered: 'categories covered',
89
+ myAiSkillStack: 'My AI Skill Stack',
90
+ sharedBy: 'Shared by {user}',
91
+ poweredBy: 'Powered by skill-guide',
92
+ installSkillGuide: 'Install skill-guide to discover your skills',
93
+ topPicks: 'Top Picks',
94
+ nMore: '+ {count} more',
95
+ ctaHeadline: 'Stop guessing. Start using.',
96
+ ctaSubtext: 'Join developers who discovered skills they never knew they had',
97
+ ctaGithub: 'Star on GitHub',
98
+ strongest: 'Strongest',
99
+ weakest: 'Weakest',
100
+ cleanupOpportunities: 'Review Candidates',
101
+ significantOverlap: 'same-category review candidates',
102
+ mostDocumented: 'Most documented:',
103
+ basedOnCompleteness: 'Based on documentation completeness',
104
+ stackInsights: 'Stack Insights',
105
+ capabilityMap: 'Capability Map',
106
+ gapHint: '{action}',
107
+ scatteredSkills: 'Scattered skills, no idea what you have?',
108
+ manySkillsPain: '{count}+ skills but no idea what you have?',
109
+ reviewBrief: 'Skill Review Brief',
110
+ reviewItems: 'review items',
111
+ reviewCopyPrompt: 'Copy Review Prompt',
112
+ reviewPasteToAgent: 'Paste to your agent for semantic review',
113
+ reviewNoItems: 'No review items found. Your skill stack looks clean!',
114
+ reviewSecurity: 'Security Flags',
115
+ reviewDuplicates: 'Duplicate Candidates',
116
+ reviewOverlap: 'Category Overlap',
117
+ reviewMalformed: 'Malformed Skills',
118
+ reviewBudget: 'Budget Overflow',
69
119
  },
70
120
  zh: {
71
121
  yourAgentSkills: '你的 Agent Skills 技能库',
@@ -92,6 +142,46 @@ const LABELS = {
92
142
  category: '分类',
93
143
  description: '描述',
94
144
  triggers: '触发词',
145
+ skillRecommendations: '技能推荐',
146
+ yourSkillStack: '你的技能栈',
147
+ gapAnalysis: '空白分析',
148
+ noSkillsInCategory: '你没有安装 {category} 类技能',
149
+ tryThese: '试试这些',
150
+ overlapAlert: '待复核候选',
151
+ skillsInCategory: '你在 "{category}" 分类下有 {count} 个技能',
152
+ considerKeeping: '请复核哪些技能确实常用',
153
+ popularYoureMissing: '目录中提到的技能',
154
+ categoriesCovered: '个分类已覆盖',
155
+ myAiSkillStack: '我的 AI 技能栈',
156
+ sharedBy: '由 {user} 分享',
157
+ poweredBy: '由 skill-guide 驱动',
158
+ installSkillGuide: '安装 skill-guide 来发现你的技能',
159
+ topPicks: '精选推荐',
160
+ nMore: '+ {count} 更多',
161
+ ctaHeadline: '别再猜了,开始用吧',
162
+ ctaSubtext: '加入已发现隐藏技能的开发者行列',
163
+ ctaGithub: '在 GitHub 上 Star',
164
+ strongest: '最强',
165
+ weakest: '最弱',
166
+ cleanupOpportunities: '待复核候选',
167
+ significantOverlap: '同类技能候选',
168
+ mostDocumented: '文档最完善:',
169
+ basedOnCompleteness: '基于文档完整度',
170
+ stackInsights: '技能栈洞察',
171
+ capabilityMap: '能力图谱',
172
+ gapHint: '{action}',
173
+ scatteredSkills: '技能散落,不知道自己有什么?',
174
+ manySkillsPain: '{count}+ 个技能但不知道自己有什么?',
175
+ reviewBrief: '技能审查简报',
176
+ reviewItems: '个审查项',
177
+ reviewCopyPrompt: '复制审查提示词',
178
+ reviewPasteToAgent: '粘贴给 Agent 进行语义审查',
179
+ reviewNoItems: '没有发现需要审查的项目,技能栈看起来很干净!',
180
+ reviewSecurity: '安全标记',
181
+ reviewDuplicates: '重复候选',
182
+ reviewOverlap: '类别重叠',
183
+ reviewMalformed: '配置不完整',
184
+ reviewBudget: '预算溢出',
95
185
  },
96
186
  };
97
187
 
@@ -319,17 +409,18 @@ function te(text) {
319
409
  function parseMode() {
320
410
  if (hasFlag('--help') || hasFlag('-h')) return { mode: 'help' };
321
411
  if (hasFlag('--doctor')) return { mode: 'doctor' };
322
- if (hasFlag('--full') || args[0] === 'all') return { mode: 'full' };
323
-
324
- const skill = getArgValue('--skill');
325
- if (skill) return { mode: 'skill', value: skill };
412
+ if (hasFlag('--review')) return { mode: 'review' };
413
+ if (hasFlag('--recommend')) return { mode: 'recommend' };
414
+ if (hasFlag('--share')) return { mode: 'share' };
326
415
 
327
- const search = getArgValue('--search');
328
- if (search) return { mode: 'search', value: search };
416
+ // --find: unified search + deep dive (also supports legacy --search, --skill)
417
+ const find = getArgValue('--find') || getArgValue('--search') || getArgValue('--skill');
418
+ if (find) return { mode: 'find', value: find };
329
419
 
330
- const valueFlags = new Set(['--output', '--skill', '--search', '--format', '--lang']);
420
+ // Positional arg: treat as --find
421
+ const valueFlags = new Set(['--output', '--find', '--search', '--skill', '--format', '--lang', '--user', '--platform']);
331
422
  const positional = args.find((arg, index) => !arg.startsWith('-') && !valueFlags.has(args[index - 1]));
332
- if (positional) return { mode: 'skill', value: positional };
423
+ if (positional) return { mode: 'find', value: positional };
333
424
 
334
425
  return { mode: 'list' };
335
426
  }
@@ -340,11 +431,12 @@ function scannerArgsFor(mode) {
340
431
 
341
432
  if (mode.mode === 'list' || mode.mode === 'doctor') {
342
433
  scannerArgs.push('--list');
343
- } else if (mode.mode === 'skill') {
434
+ } else if (mode.mode === 'review') {
435
+ scannerArgs.push('--full');
436
+ } else if (mode.mode === 'find') {
437
+ // Try as skill first, fall back to search
344
438
  scannerArgs.push('--skill', mode.value);
345
- } else if (mode.mode === 'search') {
346
- scannerArgs.push('--search', mode.value);
347
- } else if (mode.mode === 'full') {
439
+ } else {
348
440
  scannerArgs.push('--full');
349
441
  }
350
442
 
@@ -353,7 +445,8 @@ function scannerArgsFor(mode) {
353
445
 
354
446
  function runScanner(mode) {
355
447
  const args = scannerArgsFor(mode);
356
- if (mode.mode === 'full') {
448
+ const needsFullBuffer = mode.mode !== 'list' && mode.mode !== 'doctor';
449
+ if (needsFullBuffer) {
357
450
  const result = spawnSync(process.execPath, [SCANNER, ...args], {
358
451
  cwd: process.cwd(),
359
452
  encoding: 'utf8',
@@ -469,11 +562,20 @@ function renderCover(data, mode) {
469
562
  full: t('completeManual'),
470
563
  }[mode.mode] || t('discovery');
471
564
 
565
+ // Add personality for default mode
566
+ const skills = data.skills || [];
567
+ let personalityLine = '';
568
+ if (mode.mode === 'list' && skills.length > 0) {
569
+ const personality = analyzeSkillPersonality(skills);
570
+ personalityLine = `<p class="sub" style="font-size:1.3rem;color:var(--accent);font-weight:600;margin-top:8px">${personality.emoji} ${escapeHtml(personality.type)} · ${lang() === 'zh' ? '本地画像' : 'local profile'}</p>`;
571
+ }
572
+
472
573
  return `<section class="slide cover">
473
574
  <div class="rv center">
474
575
  <div class="kicker" data-i18n="label">${escapeHtml(modeLabel)}</div>
475
576
  <h1><span class="grad" data-i18n="label">${escapeHtml(title)}</span></h1>
476
577
  <p class="sub">${escapeHtml(data.totalCount || 0)} ${t('skillsScanned')} · ${escapeHtml(subtitle)}</p>
578
+ ${personalityLine}
477
579
  <div class="stats">${Object.entries(data.sources || {}).map(([source, count]) => `<div class="stat"><b>${count}</b><span data-i18n="label">${escapeHtml(source)}</span></div>`).join('')}</div>
478
580
  </div>
479
581
  </section>`;
@@ -498,19 +600,43 @@ function renderCategorySlide(skills) {
498
600
  }
499
601
 
500
602
  function renderHighlights(skills) {
501
- const highlights = [...skills]
502
- .sort((a, b) => ((b.triggers || []).length + (b.sources || []).length) - ((a.triggers || []).length + (a.sources || []).length))
503
- .slice(0, 8);
603
+ const isZh = lang() === 'zh';
604
+
605
+ // Score skills by configuration quality (readiness)
606
+ function readinessScore(s) {
607
+ let score = 0;
608
+ if ((s.description || '').length > 100) score += 20;
609
+ if ((s.description || '').length > 200) score += 10;
610
+ if ((s.allowedTools || []).length > 0) score += 30;
611
+ if ((s.triggers || []).length > 0) score += 20;
612
+ if ((s.tokenCost || 0) > 50) score += 10;
613
+ if ((s.sources || []).length > 1) score += 10;
614
+ return score;
615
+ }
616
+
617
+ // Pick the best skill from each major category
618
+ const groups = groupBy(skills, 'category');
619
+ const topPicks = Object.entries(groups)
620
+ .filter(([, items]) => items.length > 0)
621
+ .sort((a, b) => b[1].length - a[1].length)
622
+ .slice(0, 6)
623
+ .map(([category, items]) => {
624
+ const best = [...items].sort((a, b) => readinessScore(b) - readinessScore(a))[0];
625
+ return { ...best, _pickCategory: category };
626
+ });
504
627
 
505
628
  return `<section class="slide">
506
629
  <div class="rv wide">
507
- <h2 data-i18n="label">${t('highlights')}</h2>
508
- <div class="list">${highlights.map((skill, index) => `<article class="row">
630
+ <h2 data-i18n="label">${isZh ? '每类最佳' : 'Best in Category'}</h2>
631
+ <p class="sub" style="margin-bottom:24px">${isZh
632
+ ? '从每个领域中选出配置最完整的技能'
633
+ : 'The best-configured skill from each category'}</p>
634
+ <div class="list">${topPicks.map((skill, index) => `<article class="row">
509
635
  <strong>${index + 1}</strong>
510
636
  <div>
511
637
  <h3>${escapeHtml(skill.name)}</h3>
512
638
  <p data-i18n="desc">${te(truncate(skill.description, 180))}</p>
513
- <div>${categoryBadge(skill.category)}${sourceBadges(skill.sources)}</div>
639
+ <div>${categoryBadge(skill._pickCategory)}${(skill.triggers || []).length > 0 ? `<span class="badge" style="background:rgba(34,197,94,.08);color:var(--accent2);border:1px solid rgba(34,197,94,.15)">${skill.triggers.length} triggers</span>` : ''}${(skill.sources || []).length > 1 ? `<span class="badge" style="background:rgba(129,140,248,.08);color:var(--ab);border:1px solid rgba(129,140,248,.15)">${skill.sources.length} platforms</span>` : ''}</div>
514
640
  </div>
515
641
  </article>`).join('')}</div>
516
642
  </div>
@@ -568,6 +694,173 @@ function renderSelection(data, mode) {
568
694
  </section>${renderReference(data.skills.slice(0, 20), t('comparisonReference'))}`;
569
695
  }
570
696
 
697
+
698
+ function renderInsightDashboardSlide(skills) {
699
+ const isZh = lang() === 'zh';
700
+ const health = computeHealthStats(skills);
701
+ const personality = analyzeSkillPersonality(skills);
702
+ const radar = computeRadarScores(skills, health);
703
+ const wrapped = computeWrappedStats(skills, health);
704
+ const totalTokens = skills.reduce((sum, s) => sum + (s.tokenCost || 0), 0);
705
+ const tokenK = (totalTokens / 1000).toFixed(1);
706
+ const pct = Math.round((totalTokens / 200000) * 100 * 100) / 100;
707
+
708
+ return `<section class="slide">
709
+ <div class="rv center">
710
+ <div class="kicker" data-i18n="label">${isZh ? '你的技能画像' : 'YOUR SKILL PROFILE'}</div>
711
+ <h2>${personality.emoji} ${escapeHtml(personality.type)}</h2>
712
+ <p class="sub">${escapeHtml(personality.description)}</p>
713
+ <div class="stats" style="margin:24px 0">
714
+ <div class="stat"><b>${skills.length}</b><span>${isZh ? '技能' : 'skills'}</span></div>
715
+ <div class="stat"><b>${radar.overall}/100</b><span>${isZh ? '健康度' : 'health'}</span></div>
716
+ <div class="stat"><b>${Object.keys(groupBy(skills, 'category')).length}/9</b><span>${isZh ? '领域' : 'categories'}</span></div>
717
+ <div class="stat"><b>~${tokenK}K</b><span>tokens</span></div>
718
+ </div>
719
+ ${renderDimensionRadar(radar.dimensions)}
720
+ <p class="sub" style="margin-top:16px;font-size:14px;color:var(--muted)">${isZh
721
+ ? `🔤 描述 Token 估算:约为 200K 参考 context 的 ${pct}%`
722
+ : `🔤 Estimated description tokens: ${pct}% of a 200K reference context`}</p>
723
+ </div>
724
+ </section>`;
725
+ }
726
+
727
+ function renderCleanupSlide(skills) {
728
+ const isZh = lang() === 'zh';
729
+
730
+ // Build review brief for evidence-based candidates
731
+ const data = { skills };
732
+ const details = doctorDetails(data);
733
+ const brief = buildReviewBrief(data, details);
734
+ const copyPrompt = buildCopyPrompt(brief);
735
+ const promptId = `rp-${crypto.randomUUID().slice(0, 8)}`;
736
+
737
+ // Source breakdown
738
+ const userSkills = skills.filter(s => (s.sources || []).some(src => ['claude-user', 'codex-user', 'cc-switch'].includes(src)));
739
+ const pluginSkills = skills.filter(s => (s.sources || []).some(src => ['claude-plugin', 'codex-plugin'].includes(src)));
740
+ const systemSkills = skills.filter(s => (s.sources || []).includes('openai-system'));
741
+
742
+ const userPct = skills.length > 0 ? Math.round((userSkills.length / skills.length) * 100) : 0;
743
+ const pluginPct = skills.length > 0 ? Math.round((pluginSkills.length / skills.length) * 100) : 0;
744
+
745
+ const hasReviewItems = brief.totalReviewItems > 0;
746
+ const severityColor = { high: '#ef4444', medium: '#f59e0b', low: '#6b7280', info: '#818cf8' };
747
+ const typeIcon = { security: '🔴', duplicate: '🟡', malformed: '⚪', overlap: '🟣', budget: '🔵' };
748
+ const typeLabel = {
749
+ security: isZh ? '安全标记' : 'Security flags',
750
+ duplicate: isZh ? '重复候选' : 'Duplicate candidates',
751
+ malformed: isZh ? '配置不完整' : 'Malformed',
752
+ overlap: isZh ? '类别重叠' : 'Category overlap',
753
+ budget: isZh ? '预算溢出' : 'Budget overflow',
754
+ };
755
+
756
+ // Build review cards by type
757
+ const typeOrder = ['security', 'duplicate', 'malformed', 'overlap', 'budget'];
758
+ let reviewCards = '';
759
+ for (const type of typeOrder) {
760
+ const typeItems = brief.items.filter(i => i.type === type);
761
+ if (typeItems.length === 0) continue;
762
+ const itemsHtml = typeItems.slice(0, 3).map(item => `
763
+ <div style="padding:8px 0;border-bottom:1px solid var(--border)">
764
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
765
+ <span style="font-size:11px;font-weight:700;color:${severityColor[item.severity]};background:${severityColor[item.severity]}22;padding:2px 6px;border-radius:4px">${item.severity.toUpperCase()}</span>
766
+ <span style="font-size:13px;font-weight:600">${item.skills.map(s => escapeHtml(s)).join(', ')}</span>
767
+ </div>
768
+ <p style="font-size:12px;color:var(--muted);margin:0">${escapeHtml(truncate(item.evidence, 80))}</p>
769
+ </div>`).join('');
770
+ const moreCount = typeItems.length - 3;
771
+ reviewCards += `<div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:12px 16px;max-width:580px;margin:0 auto 10px;text-align:left">
772
+ <p style="margin:0 0 6px;font-weight:600;font-size:13px">${typeIcon[type]} ${typeLabel[type]} (${typeItems.length})</p>
773
+ ${itemsHtml}
774
+ ${moreCount > 0 ? `<p style="font-size:11px;color:var(--muted);margin:4px 0 0">+ ${moreCount} more</p>` : ''}
775
+ </div>`;
776
+ }
777
+
778
+ return `<section class="slide">
779
+ <div class="rv center">
780
+ <div class="kicker" data-i18n="label">${isZh ? '复核候选' : 'REVIEW CANDIDATES'}</div>
781
+ <h2>${isZh ? '你的技能从哪来的?' : 'Where did your skills come from?'}</h2>
782
+ <div class="stats" style="margin:20px 0">
783
+ <div class="stat" style="border-color:var(--accent)"><b>${userSkills.length}</b><span>${isZh ? '用户目录来源' : 'user-directory sources'}</span></div>
784
+ <div class="stat"><b>${pluginSkills.length}</b><span>${isZh ? '插件目录来源' : 'plugin-directory sources'}</span></div>
785
+ <div class="stat"><b>${systemSkills.length}</b><span>${isZh ? '系统目录来源' : 'system-directory sources'}</span></div>
786
+ </div>
787
+ <div style="background:var(--bg);border-radius:8px;height:12px;max-width:400px;margin:0 auto 20px;overflow:hidden;display:flex">
788
+ <div style="background:var(--accent);height:100%;width:${userPct}%" title="${isZh ? '用户目录来源' : 'User-directory sources'}"></div>
789
+ <div style="background:var(--ab);height:100%;width:${pluginPct}%" title="${isZh ? '插件目录来源' : 'Plugin-directory sources'}"></div>
790
+ </div>
791
+
792
+ ${hasReviewItems ? `
793
+ <div style="max-width:580px;margin:0 auto">
794
+ <p style="font-size:13px;color:var(--muted);margin:0 0 12px">${isZh
795
+ ? `${brief.totalReviewItems} 个候选需要复核 — 点击下方按钮生成审查提示词,粘贴给你的 Agent 进行语义判断`
796
+ : `${brief.totalReviewItems} candidates to review — click below to generate a review prompt, paste it to your agent for semantic judgment`}</p>
797
+ ${reviewCards}
798
+ <div style="text-align:center;margin-top:12px">
799
+ <button onclick="copyText(window['${promptId}'])" style="background:var(--accent);color:#0F172A;border:none;border-radius:var(--r);padding:10px 20px;font-size:14px;font-weight:600;cursor:pointer">${isZh ? '📋 复制审查提示词' : '📋 Copy Review Prompt'}</button>
800
+ <p style="font-size:11px;color:var(--muted);margin:8px 0 0;font-style:italic">${isZh
801
+ ? 'Agent 会对每项回复 CONFIRM / DISMISS / SUGGEST,确认前不要删除任何东西'
802
+ : 'Agent responds CONFIRM / DISMISS / SUGGEST per item; do not delete anything until you confirm'}</p>
803
+ </div>
804
+ <script>window['${promptId}']=${JSON.stringify(copyPrompt)};</script>
805
+ </div>
806
+ ` : `<p style="font-size:14px;color:var(--accent)">${isZh ? '没有发现需要复核的候选,技能栈看起来很干净!' : 'No review candidates found. Your skill stack looks clean!'}</p>`}
807
+ </div>
808
+ </section>`;
809
+ }
810
+
811
+ function renderNextStepsSlide(skills) {
812
+ const isZh = lang() === 'zh';
813
+ const sample = (skills || []).filter(s => (s.description || '').length > 100).slice(0, 3);
814
+ const sampleName = sample.length > 0 ? sample[0].name : 'investigate';
815
+ const sampleName2 = sample.length > 1 ? sample[1].name : 'security-audit';
816
+ const searchCmd = `npx skill-guide security`;
817
+ const skillCmd = `npx skill-guide ${sampleName}`;
818
+ const fullCmd = `npx skill-guide --full`;
819
+ const recommendCmd = `npx skill-guide --recommend`;
820
+ const shareCmd = `npx skill-guide --share`;
821
+ const doctorCmd = `npx skill-guide --doctor`;
822
+
823
+ return `<section class="slide">
824
+ <div class="rv center">
825
+ <div class="kicker" data-i18n="label">${isZh ? '下一步' : 'GO DEEPER'}</div>
826
+ <h2 data-i18n="label">${isZh ? '试试这些命令' : 'Try these commands'}</h2>
827
+ <p class="sub" style="font-size:14px">${isZh ? '点击命令即可复制' : 'Click any command to copy'}</p>
828
+ <div style="max-width:640px;margin:24px auto 0;text-align:left">
829
+ <div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:12px">
830
+ <p style="margin:0 0 6px;color:var(--accent);font-weight:600;font-size:14px">${isZh ? '🔍 搜索技能' : '🔍 Search for a skill'}</p>
831
+ <code style="display:block;padding:8px 12px;background:var(--bg);border-radius:6px;font-size:13px;color:var(--accent2);cursor:pointer" onclick="copyText('${searchCmd}')">${searchCmd}</code>
832
+ <p style="margin:6px 0 0;font-size:12px;color:var(--muted)">${isZh ? '试试替换关键词: debug, deploy, design, test...' : 'Replace the keyword: try debug, deploy, design, test...'}</p>
833
+ </div>
834
+ <div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:12px">
835
+ <p style="margin:0 0 6px;color:var(--accent);font-weight:600;font-size:14px">${isZh ? '📖 深入了解一个技能' : '📖 Deep dive into a skill'}</p>
836
+ <code style="display:block;padding:8px 12px;background:var(--bg);border-radius:6px;font-size:13px;color:var(--accent2);cursor:pointer" onclick="copyText('${skillCmd}')">${skillCmd}</code>
837
+ <p style="margin:6px 0 0;font-size:12px;color:var(--muted)">${isZh ? `查看 ${sampleName} 的触发词、使用场景和限制` : `See ${sampleName}\'s triggers, use cases, and limitations`}</p>
838
+ </div>
839
+ <div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:12px">
840
+ <p style="margin:0 0 6px;color:var(--accent);font-weight:600;font-size:14px">${isZh ? '📊 完整参考手册' : '📊 Full reference'}</p>
841
+ <code style="display:block;padding:8px 12px;background:var(--bg);border-radius:6px;font-size:13px;color:var(--accent2);cursor:pointer" onclick="copyText('${fullCmd}')">${fullCmd}</code>
842
+ <p style="margin:6px 0 0;font-size:12px;color:var(--muted)">${isZh ? '一页一个技能,完整的使用手册' : 'One page per skill, complete manual'}</p>
843
+ </div>
844
+ <div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:12px">
845
+ <p style="margin:0 0 6px;color:var(--accent);font-weight:600;font-size:14px">${isZh ? '🌐 在线推荐' : '🌐 Get recommendations'}</p>
846
+ <code style="display:block;padding:8px 12px;background:var(--bg);border-radius:6px;font-size:13px;color:var(--accent2);cursor:pointer" onclick="copyText('${recommendCmd}')">${recommendCmd}</code>
847
+ <p style="margin:6px 0 0;font-size:12px;color:var(--muted)">${isZh ? '查看目录提及项,并复核同类技能候选' : 'Review directory mentions and same-category candidates'}</p>
848
+ </div>
849
+ <div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:12px">
850
+ <p style="margin:0 0 6px;color:var(--accent);font-weight:600;font-size:14px">${isZh ? '📤 分享你的技能栈' : '📤 Share your skill stack'}</p>
851
+ <code style="display:block;padding:8px 12px;background:var(--bg);border-radius:6px;font-size:13px;color:var(--accent2);cursor:pointer" onclick="copyText('${shareCmd}')">${shareCmd}</code>
852
+ <p style="margin:6px 0 0;font-size:12px;color:var(--muted)">${isZh ? '生成一个可分享的技能组合页面' : 'Generate a shareable portfolio page of your skills'}</p>
853
+ </div>
854
+ <div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px">
855
+ <p style="margin:0 0 6px;color:var(--accent);font-weight:600;font-size:14px">${isZh ? '🩺 诊断环境' : '🩺 Diagnose environment'}</p>
856
+ <code style="display:block;padding:8px 12px;background:var(--bg);border-radius:6px;font-size:13px;color:var(--accent2);cursor:pointer" onclick="copyText('${doctorCmd}')">${doctorCmd}</code>
857
+ <p style="margin:6px 0 0;font-size:12px;color:var(--muted)">${isZh ? '检查损坏文件、重复技能、路径问题' : 'Check broken files, duplicates, path issues'}</p>
858
+ </div>
859
+ </div>
860
+ </div>
861
+ </section>`;
862
+ }
863
+
571
864
  function renderSlides(data, mode) {
572
865
  if (data.error) {
573
866
  return `${renderCover(data, mode)}<section class="slide"><div class="rv center"><h2>Error</h2><p class="sub">${escapeHtml(data.error)}</p></div></section>`;
@@ -576,7 +869,7 @@ function renderSlides(data, mode) {
576
869
  if (mode.mode === 'search') return `${renderCover(data, mode)}${renderSelection(data, mode)}`;
577
870
  if (mode.mode === 'skill') return `${renderCover(data, mode)}${renderSkillDetails(data.skills)}`;
578
871
  if (mode.mode === 'full') return `${renderCover(data, mode)}${renderCategorySlide(data.skills)}${renderSkillDetails(data.skills)}${renderReference(data.skills, t('completeReference'))}`;
579
- return `${renderCover(data, mode)}${renderCategorySlide(data.skills)}${renderHighlights(data.skills)}${renderReference(data.skills)}`;
872
+ return `${renderCover(data, mode)}${renderInsightDashboardSlide(data.skills)}${renderCleanupSlide(data.skills)}${renderCategorySlide(data.skills)}${renderHighlights(data.skills)}${renderReference(data.skills)}${renderNextStepsSlide(data.skills)}`;
580
873
  }
581
874
 
582
875
  function renderHtml(data, mode) {
@@ -625,6 +918,7 @@ ${slides}
625
918
  <nav class="progress" aria-label="${lang() === 'zh' ? '幻灯片导航' : 'Slide navigation'}"></nav>
626
919
  <div class="shortcut" aria-hidden="true">↓ ↑ Space</div>
627
920
  <script>
921
+ function copyText(t){var ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.left='-9999px';document.body.appendChild(ta);ta.select();try{document.execCommand('copy');var b=event&&event.target?event.target.closest('code,button'):null;if(b){var o=b.textContent;b.textContent='Copied!';setTimeout(function(){b.textContent=o},1200)}}catch(e){}document.body.removeChild(ta)}
628
922
  const seen=new IntersectionObserver(es=>es.forEach(e=>{if(e.isIntersecting)e.target.classList.add('v')}),{threshold:.12});
629
923
  document.querySelectorAll('.rv').forEach(el=>seen.observe(el));
630
924
  const slides=[...document.querySelectorAll('.slide')];
@@ -645,6 +939,71 @@ function openFile(file) {
645
939
  spawnSync(command, argsForOpen, { stdio: 'ignore', detached: true });
646
940
  }
647
941
 
942
+ function shouldAutoOpen() {
943
+ if (hasFlag('--no-open')) return false;
944
+ if (hasFlag('--open')) return true;
945
+ const format = getArgValue('--format');
946
+ if (format === 'json') return false;
947
+ if (!process.stdout.isTTY) return false;
948
+ return true;
949
+ }
950
+
951
+ const PLATFORM_LABELS = { claude: 'Claude Code', codex: 'Codex', all: 'All Platforms' };
952
+
953
+ function detectPlatform() {
954
+ if (hasFlag('--all')) return 'all';
955
+ const explicit = getArgValue('--platform');
956
+ if (explicit && PLATFORM_LABELS[explicit]) return explicit;
957
+ if (process.env.CODEX_AGENT) return 'codex';
958
+ if (process.env.CLAUDE_CODE) return 'claude';
959
+ const dir = __dirname;
960
+ if (dir.includes(path.join('.claude', 'skills')) || dir.includes(path.join('.claude', 'plugins'))) return 'claude';
961
+ if (dir.includes(path.join('.codex', 'skills')) || dir.includes(path.join('.codex', 'plugins'))) return 'codex';
962
+ return 'all';
963
+ }
964
+
965
+ function filterSkillsByPlatform(skills, platform) {
966
+ if (platform === 'all') return skills;
967
+ if (platform === 'codex') {
968
+ return skills.filter(s => (s.sources || []).some(src =>
969
+ ['codex-user', 'codex-plugin', 'openai-system', 'cc-switch'].includes(src)));
970
+ }
971
+ if (platform === 'claude') {
972
+ return skills.filter(s => (s.sources || []).some(src =>
973
+ ['claude-user', 'claude-plugin', 'cc-switch'].includes(src)));
974
+ }
975
+ return skills;
976
+ }
977
+
978
+ function applyPlatformFilter(data) {
979
+ const platform = detectPlatform();
980
+ if (platform === 'all') {
981
+ data._platform = 'all';
982
+ return data;
983
+ }
984
+ data._platform = platform;
985
+ if (data.skills) {
986
+ data.skills = filterSkillsByPlatform(data.skills, platform);
987
+ data.totalCount = data.skills.length;
988
+ }
989
+ if (data.sources) {
990
+ const keep = platform === 'claude'
991
+ ? ['claude-user', 'claude-plugin', 'cc-switch']
992
+ : ['codex-user', 'codex-plugin', 'openai-system', 'cc-switch'];
993
+ const filtered = {};
994
+ for (const [k, v] of Object.entries(data.sources)) {
995
+ if (keep.includes(k)) filtered[k] = v;
996
+ }
997
+ data.sources = filtered;
998
+ }
999
+ return data;
1000
+ }
1001
+
1002
+ function platformLabel() {
1003
+ const p = detectPlatform();
1004
+ return PLATFORM_LABELS[p] || p;
1005
+ }
1006
+
648
1007
  function skillRoots() {
649
1008
  const home = os.homedir();
650
1009
  const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
@@ -755,49 +1114,2309 @@ function formatScannerError(data) {
755
1114
  return lines.join('\n');
756
1115
  }
757
1116
 
758
- function main() {
759
- const mode = parseMode();
760
- if (mode.mode === 'help') {
761
- process.stdout.write(`${usage()}\n`);
762
- return;
1117
+ function renderRecommendTerminal(data, recommendations) {
1118
+ const lines = [];
1119
+ const totalCategories = new Set(data.skills.map((s) => s.category)).size;
1120
+
1121
+ const groups = groupBy(data.skills, 'category');
1122
+ const categoryCounts = Object.entries(groups)
1123
+ .filter(([cat]) => cat !== 'other')
1124
+ .map(([cat, items]) => ({ cat, count: items.length }))
1125
+ .sort((a, b) => b.count - a.count);
1126
+
1127
+ const strongest = categoryCounts[0];
1128
+ const weakest = categoryCounts[categoryCounts.length - 1];
1129
+
1130
+ lines.push('');
1131
+ lines.push('┌─ skill-guide recommend ─────────────────────┐');
1132
+ lines.push('│ │');
1133
+ lines.push(`│ ${t('yourSkillStack')}: ${data.totalCount} skills, ${totalCategories}/9 ${t('categoriesCovered')}`);
1134
+ lines.push('│ │');
1135
+
1136
+ if (strongest) {
1137
+ lines.push(`│ 💪 ${t('strongest')}: ${strongest.cat} (${strongest.count})`);
763
1138
  }
1139
+ if (weakest && weakest !== strongest) {
1140
+ lines.push(`│ ⚠️ ${t('weakest')}: ${weakest.cat} (${weakest.count})`);
1141
+ }
1142
+ lines.push('│');
764
1143
 
765
- const data = runScanner(mode);
766
- if (mode.mode === 'doctor') {
767
- process.stdout.write(`${printDoctor(data)}\n`);
768
- return;
1144
+ const gaps = recommendations.filter((r) => r.type === 'gap');
1145
+ if (gaps.length > 0) {
1146
+ lines.push(`│ ⚠️ ${t('gapAnalysis')} (${gaps.length}):`);
1147
+ for (const gap of gaps) {
1148
+ lines.push(`│ • ${gap.category} — 0 skills`);
1149
+ if (gap.action) lines.push(`│ 💡 ${gap.action}`);
1150
+ }
1151
+ lines.push('│');
769
1152
  }
770
1153
 
771
- const format = getArgValue('--format') || 'html';
772
- if (!['html', 'json'].includes(format)) {
773
- process.stderr.write('Error: --format must be "html" or "json"\n');
774
- process.exit(1);
1154
+ const overlaps = recommendations.filter((r) => r.type === 'overlap');
1155
+ const topOverlaps = [...overlaps].sort((a, b) => b.count - a.count).slice(0, 3);
1156
+ if (topOverlaps.length > 0) {
1157
+ lines.push(`│ 📋 ${t('cleanupOpportunities')}:`);
1158
+ for (const overlap of topOverlaps) {
1159
+ lines.push(`│ • ${overlap.category} (${overlap.count} skills) — ${t('significantOverlap')}`);
1160
+ const top3 = overlap.skills.slice(0, 3);
1161
+ if (overlap.completeness) {
1162
+ top3.forEach((name, i) => {
1163
+ const score = overlap.completeness[i];
1164
+ lines.push(`│ ${i + 1}. ${name} (${score}/100)`);
1165
+ });
1166
+ }
1167
+ lines.push(`│ ${t('basedOnCompleteness')}`);
1168
+ }
1169
+ lines.push('│');
775
1170
  }
776
1171
 
777
- if (format === 'json') {
778
- const serialized = JSON.stringify(data, null, 2);
779
- const jsonOutput = getArgValue('--output');
780
- if (jsonOutput) {
781
- const outputPath = path.resolve(jsonOutput);
782
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
783
- fs.writeFileSync(outputPath, `${serialized}\n`, 'utf8');
784
- process.stdout.write(`Generated ${outputPath}\n`);
1172
+ const popular = recommendations.filter((r) => r.type === 'popular');
1173
+ if (popular.length > 0) {
1174
+ lines.push(`│ 🔥 ${t('popularYoureMissing')}:`);
1175
+ for (const skill of popular.filter((s) => s.url).slice(0, 5)) {
1176
+ lines.push(`│ • ${skill.name} (${skill.message})`);
1177
+ }
1178
+ lines.push('');
1179
+ }
1180
+
1181
+ lines.push('└──────────────────────────────────────────────┘');
1182
+ lines.push('');
1183
+ return lines.join('\n');
1184
+ }
1185
+
1186
+ function renderRecommendHTML(data, recommendations, user) {
1187
+ const totalCategories = new Set(data.skills.map((s) => s.category)).size;
1188
+ const gaps = recommendations.filter((r) => r.type === 'gap');
1189
+ const popular = recommendations.filter((r) => r.type === 'popular');
1190
+ const overlaps = recommendations.filter((r) => r.type === 'overlap');
1191
+
1192
+ // Sort overlaps by count descending, take top 3
1193
+ const topOverlaps = [...overlaps].sort((a, b) => b.count - a.count).slice(0, 3);
1194
+
1195
+ const categoryBreakdown = Object.entries(
1196
+ data.skills.reduce((acc, s) => { const c = s.category || 'other'; acc[c] = (acc[c] || 0) + 1; return acc; }, {})
1197
+ ).sort((a, b) => b[1] - a[1]);
1198
+
1199
+ const breakdownColors = {
1200
+ testing: '#10b981', design: '#f59e0b', security: '#ef4444', documentation: '#8b5cf6',
1201
+ automation: '#06b6d4', deployment: '#ec4899', 'code-quality': '#14b8a6', development: '#f97316', other: '#6b7280',
1202
+ };
1203
+
1204
+ // Stack overview
1205
+ const categoryCounts = categoryBreakdown.filter(([cat]) => cat !== 'other');
1206
+ const strongest = categoryCounts[0];
1207
+ const weakest = categoryCounts[categoryCounts.length - 1];
1208
+
1209
+ const gapCards = gaps.map((gap) => `
1210
+ <article class="card gap-card">
1211
+ <h3>${escapeHtml(gap.category)}</h3>
1212
+ <p>${escapeHtml(t('noSkillsInCategory').replace('{category}', gap.category))}</p>
1213
+ ${gap.action ? `<p class="meta">${escapeHtml(gap.action)}</p>` : ''}
1214
+ ${gap.skills.length > 0 ? `<div class="chips">${gap.skills.map((s) =>
1215
+ `<a href="${escapeHtml(s.url || '#')}" class="chip" title="${escapeHtml(s.description)}">${escapeHtml(s.name)}</a>`
1216
+ ).join('')}</div>` : ''}
1217
+ </article>
1218
+ `).join('');
1219
+
1220
+ const popularItems = popular.filter((s) => s.url).slice(0, 5).map((skill) => `
1221
+ <article class="card popular-card">
1222
+ <h3>${escapeHtml(skill.name)}</h3>
1223
+ <p>${escapeHtml(skill.description || '')}</p>
1224
+ <p class="meta">${escapeHtml(skill.message)}</p>
1225
+ <a href="${escapeHtml(skill.url)}" class="link">GitHub →</a>
1226
+ </article>
1227
+ `).join('');
1228
+
1229
+ const overlapItems = topOverlaps.map((overlap) => {
1230
+ const skillsWithScores = overlap.skills.map((name, i) => {
1231
+ const score = overlap.completeness ? overlap.completeness[i] : null;
1232
+ return { name, score };
1233
+ });
1234
+ return `
1235
+ <article class="card overlap-card">
1236
+ <h3>${escapeHtml(overlap.category)} <span class="count">${overlap.count}</span></h3>
1237
+ <p>${escapeHtml(t('significantOverlap'))}</p>
1238
+ <p class="meta">${escapeHtml(t('mostDocumented'))}</p>
1239
+ <div class="skill-scores">${skillsWithScores.filter((s) => s.score !== null).map((s) =>
1240
+ `<div class="score-row"><span class="score-name">${escapeHtml(s.name)}</span><span class="score-value">${s.score}/100</span></div>`
1241
+ ).join('')}</div>
1242
+ <p class="meta score-label">${escapeHtml(t('basedOnCompleteness'))}</p>
1243
+ <div class="chips">${overlap.skills.map((s) => `<span>${escapeHtml(s)}</span>`).join('')}${
1244
+ overlap.hasMore ? `<span class="chip-more">${escapeHtml(t('nMore').replace('{count}', overlap.remainingCount))}</span>` : ''
1245
+ }</div>
1246
+ </article>
1247
+ `;
1248
+ }).join('');
1249
+
1250
+ return `<!DOCTYPE html>
1251
+ <html lang="en">
1252
+ <head>
1253
+ <meta charset="utf-8">
1254
+ <meta name="viewport" content="width=device-width,initial-scale=1">
1255
+ <title>${escapeHtml(t('skillRecommendations'))}</title>
1256
+ <meta property="og:title" content="${escapeHtml(t('skillRecommendations'))} — skill-guide">
1257
+ <meta property="og:description" content="${escapeHtml(t('yourSkillStack'))}: ${data.totalCount} skills, ${totalCategories}/9 ${t('categoriesCovered')}">
1258
+ <style>
1259
+ :root{--bg:#0f0f23;--card:#1a1a2e;--text:#e0e0e0;--muted:#888;--accent:#7c3aed;--accent2:#06b6d4;--gap:#f59e0b;--overlap:#ef4444;--popular:#10b981}
1260
+ *{margin:0;padding:0;box-sizing:border-box}
1261
+ body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;padding:2rem}
1262
+ .container{max-width:960px;margin:0 auto}
1263
+ h1{font-size:2.5rem;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:0.5rem}
1264
+ h2{font-size:1.5rem;margin:2rem 0 1rem;color:var(--accent2)}
1265
+ .stats{display:flex;gap:1rem;margin:1rem 0;flex-wrap:wrap}
1266
+ .stat{background:var(--card);padding:1rem 1.5rem;border-radius:12px;text-align:center}
1267
+ .stat b{font-size:2rem;display:block}
1268
+ .overview{background:var(--card);padding:1.5rem;border-radius:12px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.05)}
1269
+ .overview p{margin:0.5rem 0;font-size:1rem}
1270
+ .overview strong{color:var(--accent)}
1271
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem}
1272
+ .card{background:var(--card);padding:1.5rem;border-radius:12px;border:1px solid rgba(255,255,255,0.05)}
1273
+ .card h3{margin-bottom:0.5rem;font-size:1.1rem;display:flex;align-items:center;gap:0.5rem}
1274
+ .card .count{background:rgba(124,58,237,0.2);padding:0.15rem 0.5rem;border-radius:999px;font-size:0.8rem;color:var(--accent)}
1275
+ .card p{color:var(--muted);font-size:0.9rem}
1276
+ .card .meta{font-size:0.8rem;margin-top:0.5rem}
1277
+ .gap-card{border-left:3px solid var(--gap)}
1278
+ .overlap-card{border-left:3px solid var(--overlap)}
1279
+ .popular-card{border-left:3px solid var(--popular)}
1280
+ .chips{display:flex;flex-wrap:wrap;gap:0.5rem;margin-top:0.75rem}
1281
+ .chip{background:rgba(124,58,237,0.2);padding:0.25rem 0.75rem;border-radius:999px;font-size:0.85rem;text-decoration:none;color:var(--accent);transition:background 0.2s}
1282
+ .chip:hover{background:rgba(124,58,237,0.4)}
1283
+ .chip-more{background:rgba(255,255,255,0.05);padding:0.25rem 0.75rem;border-radius:999px;font-size:0.85rem;color:var(--muted)}
1284
+ .link{color:var(--accent2);text-decoration:none;font-size:0.85rem}
1285
+ .link:hover{text-decoration:underline}
1286
+ .skill-scores{margin:0.75rem 0}
1287
+ .score-row{display:flex;justify-content:space-between;padding:0.25rem 0;font-size:0.9rem}
1288
+ .score-name{color:var(--text)}
1289
+ .score-value{color:var(--accent);font-family:monospace}
1290
+ .score-label{color:var(--muted);font-size:0.75rem;font-style:italic}
1291
+ .cta{text-align:center;margin:3rem 0;padding:2rem;background:linear-gradient(135deg,rgba(124,58,237,0.1),rgba(6,182,212,0.1));border-radius:16px}
1292
+ .cta h2{margin:0 0 0.5rem}
1293
+ .cta code{background:var(--card);padding:0.5rem 1rem;border-radius:8px;font-size:1.1rem;display:inline-block;margin:0.5rem 0}
1294
+ .cta a{color:var(--accent);text-decoration:none}
1295
+ .cta-sub{color:var(--muted);margin:0.5rem 0 1.5rem;font-size:1rem}
1296
+ .cta-actions{display:flex;gap:1rem;justify-content:center;margin-top:1.5rem}
1297
+ .cta-btn{display:inline-block;padding:0.75rem 2rem;border-radius:8px;font-weight:600;text-decoration:none;font-size:1rem;transition:transform 0.2s,box-shadow 0.2s}
1298
+ .cta-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(124,58,237,0.3)}
1299
+ .cta-btn.primary{background:linear-gradient(135deg,#7c3aed,#06b6d4);color:#fff}
1300
+ .user-tag{color:var(--muted);font-size:0.9rem;margin-bottom:1rem}
1301
+ .breakdown-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;margin:1rem 0 0.5rem}
1302
+ .breakdown-segment{min-width:2px;transition:width 0.3s}
1303
+ .breakdown-legend{display:flex;flex-wrap:wrap;gap:0.75rem;margin-bottom:1.5rem}
1304
+ .legend-item{display:flex;align-items:center;gap:0.35rem;font-size:0.8rem;color:var(--muted)}
1305
+ .legend-dot{width:8px;height:8px;border-radius:50%;display:inline-block}
1306
+ </style>
1307
+ </head>
1308
+ <body>
1309
+ <div class="container">
1310
+ ${user ? `<p class="user-tag">${escapeHtml(t('sharedBy').replace('{user}', user))}</p>` : ''}
1311
+ <h1>${escapeHtml(t('skillRecommendations'))}</h1>
1312
+ <div class="stats">
1313
+ <div class="stat"><b>${data.totalCount}</b><span>${t('skillsScanned')}</span></div>
1314
+ <div class="stat"><b>${totalCategories}/9</b><span>${t('categoriesCovered')}</span></div>
1315
+ </div>
1316
+ <div class="breakdown-bar">${categoryBreakdown.map(([cat, count]) => {
1317
+ const pct = Math.round((count / data.totalCount) * 100);
1318
+ const color = breakdownColors[cat] || '#6b7280';
1319
+ return `<div class="breakdown-segment" style="width:${pct}%;background:${color}" title="${cat}: ${count} (${pct}%)"></div>`;
1320
+ }).join('')}</div>
1321
+ <div class="breakdown-legend">${categoryBreakdown.map(([cat, count]) =>
1322
+ `<span class="legend-item"><span class="legend-dot" style="background:${breakdownColors[cat] || '#6b7280'}"></span>${escapeHtml(cat)} (${count})</span>`
1323
+ ).join('')}</div>
1324
+
1325
+ <div class="overview">
1326
+ ${strongest ? `<p>💪 <strong>${escapeHtml(t('strongest'))}:</strong> ${escapeHtml(strongest[0])} (${strongest[1]} skills)</p>` : ''}
1327
+ ${weakest ? `<p>⚠️ <strong>${escapeHtml(t('weakest'))}:</strong> ${escapeHtml(weakest[0])} (${weakest[1]} skills)</p>` : ''}
1328
+ </div>
1329
+
1330
+ ${topOverlaps.length > 0 ? `<h2>${escapeHtml(t('cleanupOpportunities'))}</h2><div class="grid">${overlapItems}</div>` : ''}
1331
+ ${popular.length > 0 ? `<h2>🔥 ${escapeHtml(t('popularYoureMissing'))}</h2><div class="grid">${popularItems}</div>` : ''}
1332
+ ${gaps.length > 0 ? `<h2>Gap Analysis</h2><div class="grid">${gapCards}</div>` : ''}
1333
+
1334
+ <div class="cta">
1335
+ <h2>${escapeHtml(t('ctaHeadline'))}</h2>
1336
+ <p class="cta-sub">${escapeHtml(t('ctaSubtext'))}</p>
1337
+ <code>npx skill-guide --open</code>
1338
+ <div class="cta-actions">
1339
+ <a href="https://github.com/gtskevin/skill-guide" class="cta-btn primary">${escapeHtml(t('ctaGithub'))}</a>
1340
+ </div>
1341
+ </div>
1342
+ </div>
1343
+ </body>
1344
+ </html>`;
1345
+ }
1346
+
1347
+
1348
+ function renderDimensionRadar(dimensions) {
1349
+ const size = 280;
1350
+ const center = size / 2;
1351
+ const radius = 100;
1352
+ const levels = 5;
1353
+ const n = dimensions.length;
1354
+
1355
+ function polygonPoints(r) {
1356
+ return Array.from({ length: n }, (_, i) => {
1357
+ const angle = (Math.PI * 2 * i) / n - Math.PI / 2;
1358
+ return `${center + r * Math.cos(angle)},${center + r * Math.sin(angle)}`;
1359
+ }).join(' ');
1360
+ }
1361
+
1362
+ const dataPoints = dimensions.map((d, i) => {
1363
+ const angle = (Math.PI * 2 * i) / n - Math.PI / 2;
1364
+ const r = (d.score / 100) * radius;
1365
+ return `${center + r * Math.cos(angle)},${center + r * Math.sin(angle)}`;
1366
+ }).join(' ');
1367
+
1368
+ const labels = dimensions.map((d, i) => {
1369
+ const angle = (Math.PI * 2 * i) / n - Math.PI / 2;
1370
+ const labelR = radius + 22;
1371
+ const x = center + labelR * Math.cos(angle);
1372
+ const y = center + labelR * Math.sin(angle);
1373
+ const anchor = i === 0 || i === n / 2 ? 'middle' : i < n / 2 ? 'start' : 'end';
1374
+ return `<text x="${x}" y="${y}" text-anchor="${anchor}" fill="#94a3b8" font-size="10" font-family="monospace">${d.name}</text>`;
1375
+ }).join('');
1376
+
1377
+ const scoreLabels = dimensions.map((d, i) => {
1378
+ const angle = (Math.PI * 2 * i) / n - Math.PI / 2;
1379
+ const r = (d.score / 100) * radius;
1380
+ const x = center + (r + 12) * Math.cos(angle);
1381
+ const y = center + (r + 12) * Math.sin(angle);
1382
+ return `<text x="${x}" y="${y}" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">${d.score}</text>`;
1383
+ }).join('');
1384
+
1385
+ return `
1386
+ <svg viewBox="0 0 ${size} ${size}" style="max-width:280px;margin:0 auto;display:block">
1387
+ ${Array.from({ length: levels }, (_, i) => {
1388
+ const r = (radius / levels) * (i + 1);
1389
+ return `<polygon points="${polygonPoints(r)}" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>`;
1390
+ }).join('')}
1391
+ <polygon points="${dataPoints}" fill="rgba(34,197,94,0.15)" stroke="#22c55e" stroke-width="2"/>
1392
+ ${labels}
1393
+ ${scoreLabels}
1394
+ </svg>
1395
+ `;
1396
+ }
1397
+
1398
+ function renderRadarChart(skills) {
1399
+ const CATEGORIES = ['testing', 'design', 'security', 'documentation', 'automation', 'deployment', 'code-quality', 'development'];
1400
+ const LABELS_SHORT = ['Test', 'Design', 'Security', 'Docs', 'Auto', 'Deploy', 'Quality', 'Dev'];
1401
+
1402
+ const counts = {};
1403
+ for (const cat of CATEGORIES) counts[cat] = 0;
1404
+ for (const s of skills) {
1405
+ const cat = s.category || 'other';
1406
+ if (counts[cat] !== undefined) counts[cat]++;
1407
+ }
1408
+
1409
+ const maxCount = Math.max(...Object.values(counts), 1);
1410
+ const cx = 150, cy = 150, r = 120;
1411
+ const angleStep = (2 * Math.PI) / CATEGORIES.length;
1412
+
1413
+ // Grid rings
1414
+ const rings = [0.25, 0.5, 0.75, 1.0].map((scale) => {
1415
+ const points = CATEGORIES.map((_, i) => {
1416
+ const angle = i * angleStep - Math.PI / 2;
1417
+ const x = cx + r * scale * Math.cos(angle);
1418
+ const y = cy + r * scale * Math.sin(angle);
1419
+ return `${x},${y}`;
1420
+ }).join(' ');
1421
+ return `<polygon points="${points}" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>`;
1422
+ }).join('\n ');
1423
+
1424
+ // Data polygon
1425
+ const dataPoints = CATEGORIES.map((cat, i) => {
1426
+ const angle = i * angleStep - Math.PI / 2;
1427
+ const value = counts[cat] / maxCount;
1428
+ const x = cx + r * value * Math.cos(angle);
1429
+ const y = cy + r * value * Math.sin(angle);
1430
+ return `${x},${y}`;
1431
+ }).join(' ');
1432
+
1433
+ // Labels
1434
+ const labels = CATEGORIES.map((cat, i) => {
1435
+ const angle = i * angleStep - Math.PI / 2;
1436
+ const lx = cx + (r + 25) * Math.cos(angle);
1437
+ const ly = cy + (r + 25) * Math.sin(angle);
1438
+ return `<text x="${lx}" y="${ly}" text-anchor="middle" dominant-baseline="middle" fill="#888" font-size="11">${LABELS_SHORT[i]}</text>`;
1439
+ }).join('\n ');
1440
+
1441
+ return `<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg" class="radar-chart">
1442
+ ${rings}
1443
+ <polygon points="${dataPoints}" fill="rgba(124,58,237,0.2)" stroke="#7c3aed" stroke-width="2"/>
1444
+ ${labels}
1445
+ </svg>`;
1446
+ }
1447
+
1448
+ function generatePersona(skills) {
1449
+ const categories = {};
1450
+ for (const s of skills) {
1451
+ const cat = s.category || 'other';
1452
+ categories[cat] = (categories[cat] || 0) + 1;
1453
+ }
1454
+
1455
+ const total = skills.length;
1456
+ const personas = [];
1457
+
1458
+ if ((categories.security || 0) / total > 0.15) personas.push('Security Champion');
1459
+ if ((categories.testing || 0) / total > 0.15) personas.push('Quality Engineer');
1460
+ if ((categories.deployment || 0) / total > 0.15) personas.push('DevOps Builder');
1461
+ if ((categories.automation || 0) / total > 0.15) personas.push('Automation Architect');
1462
+ if ((categories.design || 0) / total > 0.15) personas.push('Design System Crafter');
1463
+ if ((categories.documentation || 0) / total > 0.1) personas.push('Documentation Advocate');
1464
+ if ((categories['code-quality'] || 0) / total > 0.1) personas.push('Code Quality Guardian');
1465
+
1466
+ if (personas.length === 0) {
1467
+ if (total > 50) personas.push('Skill Collector');
1468
+ else if (total > 20) personas.push('Full-Stack Explorer');
1469
+ else personas.push('Focused Builder');
1470
+ }
1471
+
1472
+ return personas.slice(0, 2).join(' · ');
1473
+ }
1474
+
1475
+ function isGarbage(text) {
1476
+ if (!text || typeof text !== 'string') return true;
1477
+ const trimmed = text.trim();
1478
+ if (trimmed.length <= 2) return true;
1479
+ if (/^[>|](-|\+)?$/.test(trimmed)) return true;
1480
+ if (/^---/.test(trimmed)) return true;
1481
+ if (/^(category|tags|name|description)\s*:/.test(trimmed)) return true;
1482
+ return false;
1483
+ }
1484
+
1485
+ function capabilityPrefix(count) {
1486
+ if (count >= 20) return 'Extensive coverage.';
1487
+ if (count >= 10) return 'Solid coverage.';
1488
+ if (count >= 3) return 'Some coverage.';
1489
+ return 'Getting started.';
1490
+ }
1491
+
1492
+ function renderShareHTML(data, user) {
1493
+ const groups = groupBy(data.skills, 'category');
1494
+ const totalCategories = Object.keys(groups).length;
1495
+ const persona = generatePersona(data.skills);
1496
+ const radarChart = renderRadarChart(data.skills);
1497
+
1498
+ // Capability map: one entry per non-empty category (excluding "other")
1499
+ const capabilityCards = Object.entries(groups)
1500
+ .filter(([cat]) => cat !== 'other')
1501
+ .sort((a, b) => b[1].length - a[1].length)
1502
+ .map(([category, items]) => {
1503
+ const prefix = capabilityPrefix(items.length);
1504
+ const example = items.find((s) => s.description) || items[0];
1505
+ const desc = isGarbage(example?.description) ? '' : truncate(example?.description || '', 80);
1506
+ return `
1507
+ <article class="card cap-card">
1508
+ <h3>${escapeHtml(category)} <span class="count">${items.length}</span></h3>
1509
+ <p class="cap-prefix">${escapeHtml(prefix)}</p>
1510
+ ${desc ? `<p class="cap-desc">${escapeHtml(desc)}</p>` : ''}
1511
+ ${example ? `<p class="cap-example">e.g. ${escapeHtml(example.name)}</p>` : ''}
1512
+ </article>
1513
+ `;
1514
+ }).join('');
1515
+
1516
+ // Stack insights
1517
+ const categoryCounts = Object.entries(groups)
1518
+ .filter(([cat]) => cat !== 'other')
1519
+ .map(([cat, items]) => ({ cat, count: items.length }))
1520
+ .sort((a, b) => b.count - a.count);
1521
+
1522
+ const strongest = categoryCounts[0];
1523
+ const weakest = categoryCounts[categoryCounts.length - 1];
1524
+
1525
+ const ALL_CATS = ['testing', 'design', 'security', 'documentation', 'automation', 'deployment', 'code-quality', 'development'];
1526
+ const missingCats = ALL_CATS.filter((cat) => !groups[cat] || groups[cat].length === 0);
1527
+ const gapCategory = missingCats[0] || (weakest && weakest.count <= 2 ? weakest.cat : null);
1528
+
1529
+ const insightsSection = `
1530
+ <h2>${escapeHtml(t('stackInsights'))}</h2>
1531
+ <div class="insights">
1532
+ ${strongest ? `<p class="insight-strong">💪 ${escapeHtml(t('strongest'))}: ${escapeHtml(strongest.cat)} (${strongest.count} skills)</p>` : ''}
1533
+ ${gapCategory ? `<p class="insight-gap">⚠️ Gap: ${escapeHtml(gapCategory)} (${groups[gapCategory]?.length || 0} skills)<br><span class="gap-hint">${escapeHtml(t('gapHint').replace('{action}', registryModule.GAP_ACTIONS[gapCategory] || ''))}</span></p>` : ''}
1534
+ <p class="insight-cta">Run <code>--recommend</code> for full analysis</p>
1535
+ </div>
1536
+ `;
1537
+
1538
+ // OG tags
1539
+ const ogTitle = `${escapeHtml(persona)} · ${data.totalCount} AI Skills — skill-guide`;
1540
+ const topCapabilities = Object.entries(groups)
1541
+ .filter(([cat]) => cat !== 'other')
1542
+ .sort((a, b) => b[1].length - a[1].length)
1543
+ .slice(0, 3)
1544
+ .map(([, items]) => items[0]?.name)
1545
+ .filter(Boolean);
1546
+ const ogDescription = topCapabilities.length > 0
1547
+ ? `I can ${topCapabilities.join(', ')}. Here's my full AI skill stack.`
1548
+ : `${data.totalCount} skills across ${totalCategories} categories`;
1549
+
1550
+ return `<!DOCTYPE html>
1551
+ <html lang="en">
1552
+ <head>
1553
+ <meta charset="utf-8">
1554
+ <meta name="viewport" content="width=device-width,initial-scale=1">
1555
+ <title>${escapeHtml(t('myAiSkillStack'))} — skill-guide</title>
1556
+ <meta property="og:title" content="${ogTitle}">
1557
+ <meta property="og:description" content="${escapeHtml(ogDescription)}">
1558
+ <meta property="og:type" content="website">
1559
+ <style>
1560
+ :root{--bg:#0f0f23;--card:#1a1a2e;--text:#e0e0e0;--muted:#888;--accent:#7c3aed;--accent2:#06b6d4;--pick:#10b981;--gap:#f59e0b}
1561
+ *{margin:0;padding:0;box-sizing:border-box}
1562
+ body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;padding:2rem}
1563
+ .container{max-width:960px;margin:0 auto}
1564
+ .hero{text-align:center;padding:3rem 0}
1565
+ h1{font-size:2.5rem;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:0.5rem}
1566
+ h2{font-size:1.5rem;margin:2.5rem 0 1rem;color:var(--accent2)}
1567
+ .pain{font-size:1.8rem;color:var(--text);font-weight:700;margin:0.5rem 0}
1568
+ .persona{font-size:1.3rem;color:var(--accent);font-weight:600;margin:0.5rem 0;letter-spacing:0.05em}
1569
+ .user-tag{color:var(--muted);font-size:0.9rem;margin-bottom:0.5rem}
1570
+ .subtitle{color:var(--muted);font-size:1rem}
1571
+ .stats{display:flex;gap:1.5rem;justify-content:center;margin:1.5rem 0}
1572
+ .stat{background:var(--card);padding:1rem 2rem;border-radius:12px;text-align:center;min-width:120px}
1573
+ .stat b{font-size:2.5rem;display:block;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
1574
+ .stat span{color:var(--muted);font-size:0.85rem}
1575
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem}
1576
+ .card{background:var(--card);padding:1.5rem;border-radius:12px;border:1px solid rgba(255,255,255,0.05)}
1577
+ .card h3{margin-bottom:0.5rem;font-size:1.1rem;display:flex;align-items:center;gap:0.5rem}
1578
+ .card .count{background:rgba(124,58,237,0.2);padding:0.15rem 0.5rem;border-radius:999px;font-size:0.8rem;color:var(--accent)}
1579
+ .cap-prefix{color:var(--accent);font-size:0.9rem;font-weight:600;margin:0.25rem 0}
1580
+ .cap-desc{color:var(--muted);font-size:0.9rem}
1581
+ .cap-example{color:var(--muted);font-size:0.8rem;font-style:italic;margin-top:0.5rem}
1582
+ .insights{background:var(--card);padding:1.5rem;border-radius:12px;border:1px solid rgba(255,255,255,0.05)}
1583
+ .insight-strong{color:var(--pick);font-size:1.1rem;margin:0.5rem 0}
1584
+ .insight-gap{color:var(--gap);font-size:1.1rem;margin:0.5rem 0}
1585
+ .gap-hint{color:var(--muted);font-size:0.9rem;font-weight:normal}
1586
+ .insight-cta{color:var(--muted);font-size:0.9rem;margin-top:1rem}
1587
+ .insight-cta code{background:rgba(124,58,237,0.2);padding:0.15rem 0.5rem;border-radius:4px;color:var(--accent)}
1588
+ .radar-container{display:flex;justify-content:center;margin:2rem 0}
1589
+ .radar-chart{width:300px;height:300px}
1590
+ .cta{text-align:center;margin:3rem 0;padding:2.5rem;background:linear-gradient(135deg,rgba(124,58,237,0.1),rgba(6,182,212,0.1));border-radius:16px}
1591
+ .cta h2{margin:0 0 0.5rem}
1592
+ .cta p{color:var(--muted);margin:0.5rem 0}
1593
+ .cta code{background:var(--card);padding:0.5rem 1.5rem;border-radius:8px;font-size:1.2rem;display:inline-block;margin:0.75rem 0;color:var(--accent2)}
1594
+ .cta-sub{color:var(--muted);margin:0.5rem 0 1.5rem;font-size:1rem}
1595
+ .cta-actions{display:flex;gap:1rem;justify-content:center;margin-top:1.5rem}
1596
+ .cta-btn{display:inline-block;padding:0.75rem 2rem;border-radius:8px;font-weight:600;text-decoration:none;font-size:1rem;transition:transform 0.2s,box-shadow 0.2s}
1597
+ .cta-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(124,58,237,0.3)}
1598
+ .cta-btn.primary{background:linear-gradient(135deg,#7c3aed,#06b6d4);color:#fff}
1599
+ footer{text-align:center;padding:2rem 0;color:var(--muted);font-size:0.8rem}
1600
+ </style>
1601
+ </head>
1602
+ <body>
1603
+ <div class="container">
1604
+ <div class="hero">
1605
+ ${user ? `<p class="user-tag">${escapeHtml(t('sharedBy').replace('{user}', user))}</p>` : ''}
1606
+ <p class="pain">${escapeHtml(data.totalCount >= 100 ? t('manySkillsPain').replace('{count}', data.totalCount) : t('scatteredSkills'))}</p>
1607
+ <h1>${escapeHtml(t('myAiSkillStack'))}</h1>
1608
+ <p class="persona">${escapeHtml(persona)}</p>
1609
+ <p class="subtitle">${data.totalCount} ${t('skillsScanned')} · ${totalCategories} ${t('categoriesCovered')}</p>
1610
+ <div class="radar-container">${radarChart}</div>
1611
+ <div class="stats">
1612
+ <div class="stat"><b>${data.totalCount}</b><span>${t('skillsScanned')}</span></div>
1613
+ <div class="stat"><b>${totalCategories}</b><span>${t('categoriesCovered')}</span></div>
1614
+ </div>
1615
+ </div>
1616
+
1617
+ <h2>${escapeHtml(t('capabilityMap'))}</h2>
1618
+ <div class="grid">${capabilityCards}</div>
1619
+
1620
+ ${insightsSection}
1621
+
1622
+ <div class="cta">
1623
+ <h2>${escapeHtml(t('ctaHeadline'))}</h2>
1624
+ <p class="cta-sub">${escapeHtml(t('ctaSubtext'))}</p>
1625
+ <code>npx skill-guide --open</code>
1626
+ <div class="cta-actions">
1627
+ <a href="https://github.com/gtskevin/skill-guide" class="cta-btn primary">${escapeHtml(t('ctaGithub'))}</a>
1628
+ </div>
1629
+ </div>
1630
+ </div>
1631
+ <footer>Generated by skill-guide</footer>
1632
+ </body>
1633
+ </html>`;
1634
+ }
1635
+
1636
+ function estimateTokens(text) {
1637
+ if (!text) return 0;
1638
+ return Math.ceil(text.length / 4);
1639
+ }
1640
+
1641
+ function computeHealthStats(skills) {
1642
+ const CONTEXT_WINDOW = 200_000;
1643
+ const DESCRIPTION_BUDGET = 16_000;
1644
+
1645
+ let totalDescriptionLength = 0;
1646
+ let totalTokenEstimate = 0;
1647
+ const securityFlags = [];
1648
+ const duplicates = new Map();
1649
+
1650
+ for (const skill of skills) {
1651
+ const descLen = (skill.description || '').length;
1652
+ totalDescriptionLength += descLen;
1653
+ totalTokenEstimate += estimateTokens(skill.description);
1654
+
1655
+ // Security red flags
1656
+ const content = (skill.description || '').toLowerCase();
1657
+ const flags = [];
1658
+ if (content.includes('curl ') && content.includes(' | ')) flags.push('pipe-from-curl');
1659
+ if (content.includes('eval(') || content.includes('exec(')) flags.push('eval-exec');
1660
+ if (content.includes('api_key') || content.includes('apikey') || content.includes('token')) flags.push('handles-secrets');
1661
+ if (content.includes('rm -rf') || content.includes('rmdir /s')) flags.push('destructive-commands');
1662
+ if (flags.length > 0) {
1663
+ securityFlags.push({ name: skill.name, flags });
1664
+ }
1665
+
1666
+ // Duplicate detection
1667
+ const normalizedName = skill.name.toLowerCase().replace(/[^a-z0-9]/g, '');
1668
+ if (duplicates.has(normalizedName)) {
1669
+ duplicates.get(normalizedName).push(skill.name);
785
1670
  } else {
786
- process.stdout.write(`${serialized}\n`);
1671
+ duplicates.set(normalizedName, [skill.name]);
787
1672
  }
788
- return;
789
1673
  }
790
1674
 
791
- if (data.error) {
792
- process.stderr.write(`${formatScannerError(data)}\n`);
793
- process.exit(1);
1675
+ const duplicateGroups = [...duplicates.entries()]
1676
+ .filter(([, names]) => names.length > 1)
1677
+ .map(([normalized, names]) => ({ normalized, names }));
1678
+
1679
+ return {
1680
+ totalSkills: skills.length,
1681
+ totalDescriptionLength,
1682
+ totalTokenEstimate,
1683
+ descriptionBudget: DESCRIPTION_BUDGET,
1684
+ budgetUsedPercent: Math.round((totalDescriptionLength / DESCRIPTION_BUDGET) * 100),
1685
+ staleSkills: [], // Scanner doesn't pass _mdFile to skill-guide
1686
+ securityFlags,
1687
+ duplicateGroups,
1688
+ contextWindowPercent: Math.round((totalTokenEstimate / CONTEXT_WINDOW) * 100 * 100) / 100,
1689
+ };
1690
+ }
1691
+
1692
+ // ---------------------------------------------------------------------------
1693
+ // --review: LLM review brief
1694
+ // ---------------------------------------------------------------------------
1695
+ function buildReviewBrief(data, details) {
1696
+ const skills = data.skills || [];
1697
+ const health = computeHealthStats(skills);
1698
+ const items = [];
1699
+ let counter = 0;
1700
+ function nextId(prefix) { return `${prefix}-${++counter}`; }
1701
+
1702
+ // 1. Security flags
1703
+ for (const flag of health.securityFlags) {
1704
+ const skill = skills.find(s => s.name === flag.name);
1705
+ items.push({
1706
+ id: nextId('sec'),
1707
+ type: 'security',
1708
+ severity: 'high',
1709
+ skills: [flag.name],
1710
+ evidence: `Patterns: ${flag.flags.join(', ')}`,
1711
+ context: `Description: ${truncate(skill?.description || '', 200)}`,
1712
+ question: `Does "${flag.name}" actually pose a security risk, or are these patterns used safely in context?`,
1713
+ actionHint: 'Review the full skill content. If the patterns are in example code or are safe, dismiss. If genuinely risky, flag for revision.',
1714
+ });
1715
+ }
1716
+
1717
+ // 2. Duplicate candidates
1718
+ for (const group of health.duplicateGroups) {
1719
+ const descriptions = group.names.map(n => {
1720
+ const s = skills.find(sk => sk.name === n);
1721
+ return `${n}: ${truncate(s?.description || '(no description)', 100)}`;
1722
+ });
1723
+ items.push({
1724
+ id: nextId('dup'),
1725
+ type: 'duplicate',
1726
+ severity: 'medium',
1727
+ skills: group.names,
1728
+ evidence: `Normalized name: "${group.normalized}"`,
1729
+ context: descriptions.join('\n'),
1730
+ question: `Are "${group.names.join('" and "')}" truly duplicate skills, or do they serve different purposes despite similar names?`,
1731
+ actionHint: 'If they differ in scope or purpose, consider renaming to clarify. If truly redundant, keep the one with better documentation.',
1732
+ });
1733
+ }
1734
+
1735
+ // 3. Malformed skills
1736
+ for (const file of (details.malformed || []).slice(0, 10)) {
1737
+ const basename = path.basename(path.dirname(file));
1738
+ items.push({
1739
+ id: nextId('mal'),
1740
+ type: 'malformed',
1741
+ severity: 'low',
1742
+ skills: [basename],
1743
+ evidence: `Missing frontmatter in: ${file.replace(os.homedir(), '~')}`,
1744
+ context: '',
1745
+ question: `What name, description, and triggers should be added to the skill at "${basename}"?`,
1746
+ actionHint: 'Read the file, understand its purpose, and suggest appropriate frontmatter fields.',
1747
+ });
1748
+ }
1749
+
1750
+ // 4. Category overlap (3+ skills in same category)
1751
+ const groups = groupBy(skills, 'category');
1752
+ for (const [cat, catSkills] of Object.entries(groups)) {
1753
+ if (catSkills.length >= 3) {
1754
+ const names = catSkills.slice(0, 5).map(s => s.name);
1755
+ const descs = catSkills.slice(0, 5).map(s =>
1756
+ ` - ${s.name}: ${truncate(s.description || '(no description)', 80)}`
1757
+ );
1758
+ items.push({
1759
+ id: nextId('ovr'),
1760
+ type: 'overlap',
1761
+ severity: 'info',
1762
+ skills: names,
1763
+ evidence: `${catSkills.length} skills in "${cat}" category`,
1764
+ context: descs.join('\n'),
1765
+ question: `Are these ${catSkills.length} "${cat}" skills overlapping (doing the same thing) or complementary (covering different aspects)?`,
1766
+ actionHint: 'If overlapping, suggest which to consolidate. If complementary, clarify how they differ.',
1767
+ });
1768
+ }
1769
+ }
1770
+
1771
+ // 5. Budget overflow
1772
+ if (health.budgetUsedPercent > 100) {
1773
+ const topConsumers = [...skills]
1774
+ .sort((a, b) => (b.description?.length || 0) - (a.description?.length || 0))
1775
+ .slice(0, 5);
1776
+ items.push({
1777
+ id: nextId('bud'),
1778
+ type: 'budget',
1779
+ severity: 'medium',
1780
+ skills: topConsumers.map(s => s.name),
1781
+ evidence: `Description budget used: ${health.budgetUsedPercent}% (~${health.totalTokenEstimate} tokens)`,
1782
+ context: topConsumers.map(s => ` - ${s.name}: ${(s.description || '').length} chars`).join('\n'),
1783
+ question: 'Which skill descriptions should be shortened to fit within the token budget?',
1784
+ actionHint: 'Prioritize trimming the longest descriptions. Preserve key information about triggers and use cases.',
1785
+ });
1786
+ }
1787
+
1788
+ return {
1789
+ generatedAt: new Date().toISOString(),
1790
+ totalSkills: skills.length,
1791
+ totalReviewItems: items.length,
1792
+ summary: {
1793
+ security: items.filter(i => i.type === 'security').length,
1794
+ duplicate: items.filter(i => i.type === 'duplicate').length,
1795
+ malformed: items.filter(i => i.type === 'malformed').length,
1796
+ overlap: items.filter(i => i.type === 'overlap').length,
1797
+ budget: items.filter(i => i.type === 'budget').length,
1798
+ },
1799
+ items,
1800
+ };
1801
+ }
1802
+
1803
+ function buildCopyPrompt(brief) {
1804
+ const lines = [
1805
+ `Review the following ${brief.totalReviewItems} findings from skill-guide and provide your assessment.`,
1806
+ '',
1807
+ ];
1808
+ for (const item of brief.items) {
1809
+ lines.push(`## [${item.severity.toUpperCase()}] ${item.type}: ${item.skills.join(', ')}`);
1810
+ lines.push(`Evidence: ${item.evidence}`);
1811
+ if (item.context) lines.push(`Context:\n${item.context}`);
1812
+ lines.push(`Question: ${item.question}`);
1813
+ lines.push('');
1814
+ }
1815
+ lines.push('For each item, respond with: CONFIRM (agree with the finding), DISMISS (false positive with reason), or SUGGEST (alternative action).');
1816
+ return lines.join('\n');
1817
+ }
1818
+
1819
+ function renderHealthTerminal(data) {
1820
+ const skills = data.skills || [];
1821
+ const health = computeHealthStats(skills);
1822
+ const personality = analyzeSkillPersonality(skills);
1823
+ const radar = computeRadarScores(skills, health);
1824
+ const topConsumers = renderTopConsumers(skills, 5);
1825
+ const prescriptions = generatePrescription(skills, health);
1826
+ const isZh = lang() === 'zh';
1827
+
1828
+ const scoreColor = (s) => s >= 80 ? '🟢' : s >= 60 ? '🟡' : '🔴';
1829
+
1830
+ const lines = [
1831
+ '╔══════════════════════════════════════════════════════════════╗',
1832
+ '║ Skill Health Report ║',
1833
+ '╚══════════════════════════════════════════════════════════════╝',
1834
+ '',
1835
+ `${scoreColor(radar.overall)} Health Score: ${radar.overall}/100`,
1836
+ '',
1837
+ `${personality.emoji} ${isZh ? '本地画像' : 'Local profile'}: ${personality.type} (${personality.title})`,
1838
+ ` ${personality.description}`,
1839
+ '',
1840
+ isZh ? '── 你的数据 ─────────────────────────────────────────────' : '── Your Stats ─────────────────────────────────────────────',
1841
+ ` 📦 ${isZh ? '总技能数' : 'Total Skills'}: ${skills.length}`,
1842
+ ` 🔤 ${isZh ? '描述 Token 估算' : 'Description Token Estimate'}: ~${(health.totalTokenEstimate / 1000).toFixed(1)}K (${health.contextWindowPercent}% ${isZh ? 'of 200K reference context' : 'of 200K reference context'})`,
1843
+ ` 📏 ${isZh ? '本地参考预算使用' : 'Local Reference Budget Usage'}: ${health.budgetUsedPercent}%`,
1844
+ '',
1845
+ ];
1846
+
1847
+ // Description estimate
1848
+ const tokenPerSkill = skills.length > 0 ? Math.round(health.totalTokenEstimate / skills.length) : 0;
1849
+ lines.push(isZh ? '── 描述估算 ─────────────────────────────────────────────' : '── Description Estimate ─────────────────────────────────');
1850
+ lines.push(isZh
1851
+ ? ` 💡 你的 ${skills.length} 个技能,平均每个 ~${tokenPerSkill} tokens。`
1852
+ : ` 💡 Your ${skills.length} skills average ~${tokenPerSkill} tokens each.`);
1853
+ lines.push(isZh
1854
+ ? ` 描述总量估算约为 200K 参考 context 的 ${health.contextWindowPercent}%。`
1855
+ : ` The estimated description total is ${health.contextWindowPercent}% of a 200K reference context.`);
1856
+ lines.push('');
1857
+
1858
+ if (topConsumers.length > 0) {
1859
+ lines.push(isZh ? '── Top 5 最长描述 ───────────────────────────────────────' : '── Top 5 Longest Descriptions ─────────────────────────────');
1860
+ for (const c of topConsumers) {
1861
+ const bar = '█'.repeat(Math.round(c.barWidth / 10)) + '░'.repeat(10 - Math.round(c.barWidth / 10));
1862
+ lines.push(` ${c.rank}. ${c.name} ${bar} ${c.tokenCost.toLocaleString()} tokens`);
1863
+ }
1864
+ lines.push('');
1865
+ }
1866
+
1867
+ if (prescriptions.length > 0) {
1868
+ lines.push(isZh ? '── 复核提示 ──────────────────────────────────────────────' : '── Review Prompts ─────────────────────────────────────────');
1869
+ for (const p of prescriptions) {
1870
+ lines.push(` ${p.emoji} ${isZh ? p.title : p.titleEn} [${p.impact}]`);
1871
+ lines.push(` ${isZh ? p.description : p.descriptionEn}`);
1872
+ }
1873
+ lines.push('');
1874
+ }
1875
+
1876
+ lines.push(isZh ? '── 五维评分 ──────────────────────────────────────────────' : '── Five Dimensions ────────────────────────────────────────');
1877
+ for (const d of radar.dimensions) {
1878
+ const bar = '█'.repeat(Math.round(d.score / 10)) + '░'.repeat(10 - Math.round(d.score / 10));
1879
+ lines.push(` ${d.name} ${bar} ${d.score}/100`);
1880
+ }
1881
+ lines.push('');
1882
+ lines.push(isZh ? '💡 使用 --open 打开交互式仪表盘,支持一键分享' : '💡 Run with --open for interactive dashboard with shareable report');
1883
+
1884
+ return lines.join('\n');
1885
+ }
1886
+
1887
+ function renderDefaultTerminal(skills) {
1888
+ const isZh = lang() === 'zh';
1889
+ const health = computeHealthStats(skills);
1890
+ const personality = analyzeSkillPersonality(skills);
1891
+ const radar = computeRadarScores(skills, health);
1892
+ const wrapped = computeWrappedStats(skills, health);
1893
+ const totalTokens = skills.reduce((sum, s) => sum + (s.tokenCost || 0), 0);
1894
+ const tokenK = (totalTokens / 1000).toFixed(1);
1895
+ const pct = Math.round((totalTokens / 200000) * 100 * 100) / 100;
1896
+ const scoreColor = (s) => s >= 80 ? '🟢' : s >= 60 ? '🟡' : '🔴';
1897
+ const groups = groupBy(skills, 'category');
1898
+ const cats = Object.entries(groups).sort((a, b) => b[1].length - a[1].length);
1899
+
1900
+ const lines = [
1901
+ '╔══════════════════════════════════════════════════════════════╗',
1902
+ isZh
1903
+ ? `║ skill-guide · ${skills.length} 个技能 · 本地画像:${personality.type} ║`
1904
+ : `║ skill-guide · ${skills.length} skills · Local profile: ${personality.type} ║`,
1905
+ '╚══════════════════════════════════════════════════════════════╝',
1906
+ '',
1907
+ ` ${scoreColor(radar.overall)} ${isZh ? '健康度' : 'Health'}: ${radar.overall}/100`,
1908
+ ` ${personality.emoji} ${personality.description}`,
1909
+ '',
1910
+ ` 📦 ${skills.length} ${isZh ? '个技能' : 'skills'} · ${cats.length}/9 ${isZh ? '个领域' : 'categories'} · 🔤 ~${tokenK}K ${isZh ? '描述 tokens' : 'description tokens'} (${pct}% ${isZh ? 'of 200K 参考 context' : 'of 200K reference context'})`,
1911
+ ` 📍 ${isZh ? '本地画像:仅基于当前扫描结果' : 'Local profile: based only on the current scan'}`,
1912
+ '',
1913
+ ];
1914
+
1915
+ // Radar
1916
+ lines.push(isZh ? ' ── 五维雷达 ──────────────────────────────────────────' : ' ── Radar ──────────────────────────────────────────────');
1917
+ for (const d of radar.dimensions) {
1918
+ const bar = '█'.repeat(Math.round(d.score / 10)) + '░'.repeat(10 - Math.round(d.score / 10));
1919
+ lines.push(` ${d.name.padEnd(10)} ${bar} ${d.score}/100`);
1920
+ }
1921
+ lines.push('');
1922
+
1923
+ // Top 3 insights
1924
+ lines.push(isZh ? ' ── 关键洞察 ──────────────────────────────────────────' : ' ── Insights ───────────────────────────────────────────');
1925
+
1926
+ // Source breakdown
1927
+ const userCount = skills.filter(s => (s.sources || []).some(src => ['claude-user', 'codex-user', 'cc-switch'].includes(src))).length;
1928
+ const pluginCount = skills.filter(s => (s.sources || []).some(src => ['claude-plugin', 'codex-plugin'].includes(src))).length;
1929
+ lines.push(isZh
1930
+ ? ` 📂 来源: ${userCount} 个用户目录来源 · ${pluginCount} 个插件目录来源`
1931
+ : ` 📂 Sources: ${userCount} user-directory · ${pluginCount} plugin-directory`);
1932
+
1933
+ // Dormant skills
1934
+ const dormant = wrapped.untappedCount || 0;
1935
+ const dormantPct = skills.length > 0 ? Math.round((dormant / skills.length) * 100) : 0;
1936
+ if (dormant > 0) {
1937
+ lines.push(isZh
1938
+ ? ` ⚠️ ${dormant} 个技能(${dormantPct}%)元数据较少 — 建议复核描述和触发词`
1939
+ : ` ⚠️ ${dormant} skills (${dormantPct}%) have sparse metadata — review descriptions and triggers`);
1940
+ }
1941
+
1942
+ // Budget
1943
+ if (pct > 5) {
1944
+ lines.push(isZh
1945
+ ? ` 🔤 描述 Token 估算约为 200K 参考 context 的 ${pct}%`
1946
+ : ` 🔤 Estimated description tokens: ${pct}% of a 200K reference context`);
1947
+ }
1948
+
1949
+ // Short descriptions are review candidates, not usage signals.
1950
+ if (wrapped.shortestDescriptions && wrapped.shortestDescriptions.length > 0) {
1951
+ lines.push(isZh
1952
+ ? ` 🔍 描述最短: ${wrapped.shortestDescriptions.slice(0, 3).join(', ')}`
1953
+ : ` 🔍 Shortest descriptions: ${wrapped.shortestDescriptions.slice(0, 3).join(', ')}`);
1954
+ }
1955
+
1956
+ lines.push('');
1957
+ lines.push(isZh ? ' 💡 使用 --open 打开完整的交互式报告' : ' 💡 Run --open for the full interactive report');
1958
+ lines.push('');
1959
+
1960
+ return lines.join('\n');
1961
+ }
1962
+
1963
+ function renderInsightTerminal(data) {
1964
+ const skills = data.skills || [];
1965
+ const health = computeHealthStats(skills);
1966
+ const personality = analyzeSkillPersonality(skills);
1967
+ const radar = computeRadarScores(skills, health);
1968
+ const wrapped = computeWrappedStats(skills, health);
1969
+ const isZh = lang() === 'zh';
1970
+ const scoreColor = (s) => s >= 80 ? '🟢' : s >= 60 ? '🟡' : '🔴';
1971
+ const groups = groupBy(skills, 'category');
1972
+ const categoryCounts = Object.entries(groups)
1973
+ .filter(([cat]) => cat !== 'other')
1974
+ .map(([cat, items]) => ({ cat, count: items.length }))
1975
+ .sort((a, b) => b.count - a.count);
1976
+ const strongest = categoryCounts[0];
1977
+ const weakest = categoryCounts[categoryCounts.length - 1];
1978
+
1979
+ const lines = [
1980
+ '╔══════════════════════════════════════════════════════════════╗',
1981
+ isZh ? '║ 技能洞察报告 ║'
1982
+ : '║ Skill Insight Report ║',
1983
+ '╚══════════════════════════════════════════════════════════════╝',
1984
+ '',
1985
+ ];
1986
+
1987
+ // Health section
1988
+ lines.push(isZh ? '── 健康度 ───────────────────────────────────────────────' : '── Health ─────────────────────────────────────────────────');
1989
+ lines.push(` ${scoreColor(radar.overall)} ${isZh ? '分数' : 'Score'}: ${radar.overall}/100 · ${personality.emoji} ${isZh ? '类型' : 'Type'}: ${personality.type}`);
1990
+ lines.push(` ${personality.description}`);
1991
+ lines.push(` ⚡ ${wrapped.coreCount} ${isZh ? '个元数据较完整' : 'metadata-rich'} | ${wrapped.readyCount} ${isZh ? '个元数据一般' : 'partial metadata'} | ${wrapped.untappedCount} ${isZh ? '个元数据较少' : 'sparse metadata'}`);
1992
+ lines.push('');
1993
+
1994
+ // Radar
1995
+ lines.push(isZh ? '── 五维评分 ─────────────────────────────────────────────' : '── Dimensions ────────────────────────────────────────────');
1996
+ for (const d of radar.dimensions) {
1997
+ const bar = '█'.repeat(Math.round(d.score / 10)) + '░'.repeat(10 - Math.round(d.score / 10));
1998
+ lines.push(` ${d.name} ${bar} ${d.score}/100`);
1999
+ }
2000
+ lines.push('');
2001
+
2002
+ // Budget
2003
+ lines.push(isZh ? '── Token 预算 ───────────────────────────────────────────' : '── Token Budget ──────────────────────────────────────────');
2004
+ lines.push(isZh
2005
+ ? ` 📦 ${skills.length} 个技能 · 🔤 ~${(health.totalTokenEstimate / 1000).toFixed(1)}K 描述 tokens(约为 200K 参考 context 的 ${health.contextWindowPercent}%)`
2006
+ : ` 📦 ${skills.length} skills · 🔤 ~${(health.totalTokenEstimate / 1000).toFixed(1)}K description tokens (${health.contextWindowPercent}% of 200K reference context)`);
2007
+ lines.push(isZh
2008
+ ? ` 📏 本地参考预算使用: ${health.budgetUsedPercent}%`
2009
+ : ` 📏 Local reference budget usage: ${health.budgetUsedPercent}%`);
2010
+ lines.push('');
2011
+
2012
+ // Gaps / cleanup
2013
+ lines.push(isZh ? '── 优化建议 ─────────────────────────────────────────────' : '── Gaps & Cleanup ────────────────────────────────────────');
2014
+ if (strongest) {
2015
+ lines.push(isZh
2016
+ ? ` 💪 最强领域: ${strongest.cat} (${strongest.count})`
2017
+ : ` 💪 Strongest: ${strongest.cat} (${strongest.count})`);
2018
+ }
2019
+ if (weakest && weakest !== strongest) {
2020
+ lines.push(isZh
2021
+ ? ` ⚠️ 最弱领域: ${weakest.cat} (${weakest.count})`
2022
+ : ` ⚠️ Weakest: ${weakest.cat} (${weakest.count})`);
2023
+ }
2024
+ if (wrapped.shortestDescriptions.length > 0) {
2025
+ lines.push(isZh
2026
+ ? ` 🔍 描述最短: ${wrapped.shortestDescriptions.slice(0, 3).join(', ')}`
2027
+ : ` 🔍 Shortest descriptions: ${wrapped.shortestDescriptions.slice(0, 3).join(', ')}`);
2028
+ }
2029
+ lines.push('');
2030
+
2031
+ // CTA
2032
+ lines.push(isZh ? '── 下一步 ───────────────────────────────────────────────' : '── Next Steps ────────────────────────────────────────────');
2033
+ lines.push(isZh
2034
+ ? ' 💡 使用 --open 生成完整的交互式 HTML 报告'
2035
+ : ' 💡 Run with --open for the full interactive HTML report');
2036
+ lines.push(isZh
2037
+ ? ' 🔗 使用 --open 生成可分享的 HTML 报告'
2038
+ : ' 🔗 Use --open to generate a shareable HTML report');
2039
+ lines.push('');
2040
+
2041
+ return lines.join('\n');
2042
+ }
2043
+
2044
+
2045
+ function renderHealthHTML(data) {
2046
+ const skills = data.skills || [];
2047
+ const health = computeHealthStats(skills);
2048
+ const personality = analyzeSkillPersonality(skills);
2049
+ const radar = computeRadarScores(skills, health);
2050
+ const topConsumers = renderTopConsumers(skills, 10);
2051
+ const prescriptions = generatePrescription(skills, health);
2052
+
2053
+ function renderRadarChart(dimensions) {
2054
+ const size = 200;
2055
+ const center = size / 2;
2056
+ const radius = 80;
2057
+ const levels = 5;
2058
+
2059
+ function pentagonPoints(r) {
2060
+ return Array.from({ length: 5 }, (_, i) => {
2061
+ const angle = (Math.PI * 2 * i) / 5 - Math.PI / 2;
2062
+ return `${center + r * Math.cos(angle)},${center + r * Math.sin(angle)}`;
2063
+ }).join(' ');
2064
+ }
2065
+
2066
+ const dataPoints = dimensions.map((d, i) => {
2067
+ const angle = (Math.PI * 2 * i) / 5 - Math.PI / 2;
2068
+ const r = (d.score / 100) * radius;
2069
+ return `${center + r * Math.cos(angle)},${center + r * Math.sin(angle)}`;
2070
+ }).join(' ');
2071
+
2072
+ const labels = dimensions.map((d, i) => {
2073
+ const angle = (Math.PI * 2 * i) / 5 - Math.PI / 2;
2074
+ const labelR = radius + 25;
2075
+ const x = center + labelR * Math.cos(angle);
2076
+ const y = center + labelR * Math.sin(angle);
2077
+ const anchor = i === 0 ? 'middle' : i < 3 ? 'start' : 'end';
2078
+ return `<text x="${x}" y="${y}" text-anchor="${anchor}" fill="#94a3b8" font-size="11">${d.name}</text>`;
2079
+ }).join('');
2080
+
2081
+ return `
2082
+ <svg viewBox="0 0 ${size} ${size}" class="radar-chart">
2083
+ ${Array.from({ length: levels }, (_, i) => {
2084
+ const r = (radius / levels) * (i + 1);
2085
+ return `<polygon points="${pentagonPoints(r)}" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>`;
2086
+ }).join('')}
2087
+ <polygon points="${dataPoints}" fill="rgba(59,130,246,0.3)" stroke="#3b82f6" stroke-width="2"/>
2088
+ ${labels}
2089
+ </svg>
2090
+ `;
2091
+ }
2092
+
2093
+ function scoreColor(score) {
2094
+ if (score >= 80) return '#22c55e';
2095
+ if (score >= 60) return '#f59e0b';
2096
+ return '#ef4444';
2097
+ }
2098
+
2099
+ function scoreLabel(score) {
2100
+ if (score >= 90) return 'A+';
2101
+ if (score >= 80) return 'A';
2102
+ if (score >= 70) return 'B';
2103
+ if (score >= 60) return 'C';
2104
+ if (score >= 50) return 'D';
2105
+ return 'F';
2106
+ }
2107
+
2108
+ return `<!DOCTYPE html>
2109
+ <html lang="${lang()}">
2110
+ <head>
2111
+ <meta charset="utf-8">
2112
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2113
+ <title>Skill Health Report</title>
2114
+ <style>
2115
+ :root {
2116
+ --bg: #0f172a;
2117
+ --card: #1e293b;
2118
+ --text: #e2e8f0;
2119
+ --muted: #94a3b8;
2120
+ --accent: #3b82f6;
2121
+ --good: #22c55e;
2122
+ --warn: #f59e0b;
2123
+ --bad: #ef4444;
2124
+ }
2125
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2126
+ body {
2127
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
2128
+ background: var(--bg);
2129
+ color: var(--text);
2130
+ min-height: 100vh;
2131
+ }
2132
+ .hero {
2133
+ text-align: center;
2134
+ padding: 4rem 2rem;
2135
+ background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%);
2136
+ }
2137
+ .score-circle {
2138
+ width: 180px;
2139
+ height: 180px;
2140
+ border-radius: 50%;
2141
+ display: flex;
2142
+ flex-direction: column;
2143
+ align-items: center;
2144
+ justify-content: center;
2145
+ margin: 0 auto 2rem;
2146
+ border: 4px solid;
2147
+ }
2148
+ .score-number {
2149
+ font-size: 4rem;
2150
+ font-weight: 700;
2151
+ line-height: 1;
2152
+ }
2153
+ .score-label {
2154
+ font-size: 1.5rem;
2155
+ font-weight: 600;
2156
+ margin-top: 0.5rem;
2157
+ }
2158
+ .personality {
2159
+ margin-top: 1rem;
2160
+ }
2161
+ .personality-emoji {
2162
+ font-size: 3rem;
2163
+ }
2164
+ .personality-title {
2165
+ font-size: 1.5rem;
2166
+ font-weight: 600;
2167
+ margin-top: 0.5rem;
2168
+ }
2169
+ .personality-desc {
2170
+ color: var(--muted);
2171
+ max-width: 500px;
2172
+ margin: 1rem auto;
2173
+ line-height: 1.6;
2174
+ }
2175
+ .container {
2176
+ max-width: 1000px;
2177
+ margin: 0 auto;
2178
+ padding: 2rem;
2179
+ }
2180
+ .section {
2181
+ background: var(--card);
2182
+ border-radius: 16px;
2183
+ padding: 2rem;
2184
+ margin-bottom: 2rem;
2185
+ border: 1px solid rgba(255,255,255,0.1);
2186
+ }
2187
+ .section-title {
2188
+ font-size: 1.25rem;
2189
+ font-weight: 600;
2190
+ margin-bottom: 1.5rem;
2191
+ display: flex;
2192
+ align-items: center;
2193
+ gap: 0.5rem;
2194
+ }
2195
+ .radar-container {
2196
+ display: flex;
2197
+ justify-content: center;
2198
+ padding: 1rem;
2199
+ }
2200
+ .radar-chart {
2201
+ width: 300px;
2202
+ height: 300px;
2203
+ }
2204
+ .consumer-row {
2205
+ display: flex;
2206
+ align-items: center;
2207
+ gap: 1rem;
2208
+ padding: 0.75rem 0;
2209
+ border-bottom: 1px solid rgba(255,255,255,0.05);
2210
+ }
2211
+ .consumer-rank {
2212
+ width: 24px;
2213
+ font-weight: 600;
2214
+ color: var(--muted);
2215
+ }
2216
+ .consumer-name {
2217
+ flex: 1;
2218
+ font-weight: 500;
2219
+ }
2220
+ .consumer-bar {
2221
+ width: 120px;
2222
+ height: 8px;
2223
+ background: rgba(255,255,255,0.1);
2224
+ border-radius: 4px;
2225
+ overflow: hidden;
2226
+ }
2227
+ .consumer-fill {
2228
+ height: 100%;
2229
+ border-radius: 4px;
2230
+ background: var(--accent);
2231
+ }
2232
+ .consumer-tokens {
2233
+ width: 80px;
2234
+ text-align: right;
2235
+ font-size: 0.875rem;
2236
+ color: var(--muted);
2237
+ }
2238
+ .prescription-card {
2239
+ background: rgba(255,255,255,0.05);
2240
+ border-radius: 12px;
2241
+ padding: 1.5rem;
2242
+ margin-bottom: 1rem;
2243
+ cursor: pointer;
2244
+ transition: background 0.2s;
2245
+ }
2246
+ .prescription-card:hover {
2247
+ background: rgba(255,255,255,0.08);
2248
+ }
2249
+ .prescription-header {
2250
+ display: flex;
2251
+ align-items: center;
2252
+ gap: 1rem;
2253
+ margin-bottom: 0.5rem;
2254
+ }
2255
+ .prescription-emoji {
2256
+ font-size: 1.5rem;
2257
+ }
2258
+ .prescription-title {
2259
+ font-weight: 600;
2260
+ flex: 1;
2261
+ }
2262
+ .prescription-impact {
2263
+ font-size: 0.75rem;
2264
+ padding: 2px 8px;
2265
+ border-radius: 4px;
2266
+ text-transform: uppercase;
2267
+ }
2268
+ .impact-high { background: rgba(239,68,68,0.2); color: #ef4444; }
2269
+ .impact-medium { background: rgba(245,158,11,0.2); color: #f59e0b; }
2270
+ .impact-low { background: rgba(34,197,94,0.2); color: #22c55e; }
2271
+ .prescription-desc {
2272
+ color: var(--muted);
2273
+ font-size: 0.875rem;
2274
+ }
2275
+ .prescription-items {
2276
+ margin-top: 1rem;
2277
+ display: none;
2278
+ }
2279
+ .prescription-card.expanded .prescription-items {
2280
+ display: block;
2281
+ }
2282
+ .prescription-item {
2283
+ display: flex;
2284
+ align-items: center;
2285
+ gap: 0.75rem;
2286
+ padding: 0.5rem 0;
2287
+ font-size: 0.875rem;
2288
+ }
2289
+ .item-name {
2290
+ font-weight: 500;
2291
+ min-width: 120px;
2292
+ }
2293
+ .item-action {
2294
+ color: var(--muted);
2295
+ }
2296
+ .share-section {
2297
+ text-align: center;
2298
+ padding: 2rem;
2299
+ }
2300
+ .share-btn {
2301
+ background: var(--accent);
2302
+ color: white;
2303
+ border: none;
2304
+ padding: 0.75rem 2rem;
2305
+ border-radius: 8px;
2306
+ font-size: 1rem;
2307
+ font-weight: 600;
2308
+ cursor: pointer;
2309
+ transition: transform 0.2s;
2310
+ }
2311
+ .share-btn:hover {
2312
+ transform: scale(1.05);
2313
+ }
2314
+ .copy-feedback {
2315
+ color: var(--good);
2316
+ margin-top: 1rem;
2317
+ display: none;
2318
+ }
2319
+ .stats-row {
2320
+ display: grid;
2321
+ grid-template-columns: repeat(4, 1fr);
2322
+ gap: 1rem;
2323
+ margin-bottom: 2rem;
2324
+ }
2325
+ .stat-card {
2326
+ background: var(--card);
2327
+ border-radius: 12px;
2328
+ padding: 1.5rem;
2329
+ text-align: center;
2330
+ border: 1px solid rgba(255,255,255,0.1);
2331
+ }
2332
+ .stat-number {
2333
+ font-size: 2rem;
2334
+ font-weight: 700;
2335
+ margin-bottom: 0.5rem;
2336
+ }
2337
+ .stat-label {
2338
+ font-size: 0.75rem;
2339
+ color: var(--muted);
2340
+ text-transform: uppercase;
2341
+ }
2342
+ </style>
2343
+ </head>
2344
+ <body>
2345
+ <div class="hero">
2346
+ <div class="score-circle" style="border-color: ${scoreColor(radar.overall)}">
2347
+ <div class="score-number" style="color: ${scoreColor(radar.overall)}">${radar.overall}</div>
2348
+ <div class="score-label" style="color: ${scoreColor(radar.overall)}">${scoreLabel(radar.overall)}</div>
2349
+ </div>
2350
+ <div class="personality">
2351
+ <div class="personality-emoji">${personality.emoji}</div>
2352
+ <div class="personality-title">${personality.type} · ${personality.title}</div>
2353
+ <div class="personality-desc">${personality.description}</div>
2354
+ </div>
2355
+ </div>
2356
+
2357
+ <div class="container">
2358
+ <div class="stats-row">
2359
+ <div class="stat-card">
2360
+ <div class="stat-number">${skills.length}</div>
2361
+ <div class="stat-label">Total Skills</div>
2362
+ </div>
2363
+ <div class="stat-card">
2364
+ <div class="stat-number">~${(health.totalTokenEstimate / 1000).toFixed(1)}K</div>
2365
+ <div class="stat-label">Description Token Estimate</div>
2366
+ </div>
2367
+ <div class="stat-card">
2368
+ <div class="stat-number">${health.contextWindowPercent}%</div>
2369
+ <div class="stat-label">200K Reference Context</div>
2370
+ </div>
2371
+ <div class="stat-card">
2372
+ <div class="stat-number">${health.budgetUsedPercent}%</div>
2373
+ <div class="stat-label">Local Reference Budget</div>
2374
+ </div>
2375
+ </div>
2376
+
2377
+ <div class="section">
2378
+ <div class="section-title">📊 Health Radar</div>
2379
+ <div class="radar-container">
2380
+ ${renderRadarChart(radar.dimensions)}
2381
+ </div>
2382
+ </div>
2383
+
2384
+ <div class="section">
2385
+ <div class="section-title">🏆 Top 10 Longest Descriptions</div>
2386
+ ${topConsumers.map(c => `
2387
+ <div class="consumer-row">
2388
+ <div class="consumer-rank">${c.rank}</div>
2389
+ <div class="consumer-name">${escapeHtml(c.name)}</div>
2390
+ <div class="consumer-bar">
2391
+ <div class="consumer-fill" style="width: ${c.barWidth}%"></div>
2392
+ </div>
2393
+ <div class="consumer-tokens">${c.tokenCost.toLocaleString()} tokens</div>
2394
+ </div>
2395
+ `).join('')}
2396
+ </div>
2397
+
2398
+ <div class="section">
2399
+ <div class="section-title">📋 Review Prompts</div>
2400
+ ${prescriptions.map(p => `
2401
+ <div class="prescription-card" onclick="this.classList.toggle('expanded')">
2402
+ <div class="prescription-header">
2403
+ <div class="prescription-emoji">${p.emoji}</div>
2404
+ <div class="prescription-title">${p.title}</div>
2405
+ <div class="prescription-impact impact-${p.impact}">${p.impact}</div>
2406
+ </div>
2407
+ <div class="prescription-desc">${p.description}</div>
2408
+ <div class="prescription-items">
2409
+ ${p.items.map(item => `
2410
+ <div class="prescription-item">
2411
+ <div class="item-name">${escapeHtml(item.name)}</div>
2412
+ <div class="item-action">${item.action}</div>
2413
+ </div>
2414
+ `).join('')}
2415
+ </div>
2416
+ </div>
2417
+ `).join('')}
2418
+ </div>
2419
+
2420
+ <div class="section share-section">
2421
+ <div class="section-title">📤 Share Your Health Report</div>
2422
+ <p style="color: var(--muted); margin-bottom: 1.5rem;">Copy to clipboard and share on social media</p>
2423
+ <button class="share-btn" onclick="copyReport()">Copy Report to Clipboard</button>
2424
+ <div class="copy-feedback" id="copyFeedback">✅ Copied!</div>
2425
+ </div>
2426
+ </div>
2427
+
2428
+ <script>
2429
+ function copyReport() {
2430
+ const report = \`🏆 Skill Health Report
2431
+
2432
+ Score: ${radar.overall}/100 (${scoreLabel(radar.overall)})
2433
+ Local Profile: ${personality.emoji} ${personality.type} · ${personality.title}
2434
+
2435
+ 📊 Stats:
2436
+ • Total Skills: ${skills.length}
2437
+ • Description Token Estimate: ~${(health.totalTokenEstimate / 1000).toFixed(1)}K
2438
+ • 200K Reference Context: ${health.contextWindowPercent}%
2439
+ • Local Reference Budget: ${health.budgetUsedPercent}%
2440
+
2441
+ ${personality.description}
2442
+
2443
+ Generate your report: npx skill-guide --health --open\`;
2444
+
2445
+ navigator.clipboard.writeText(report).then(() => {
2446
+ const feedback = document.getElementById('copyFeedback');
2447
+ feedback.style.display = 'block';
2448
+ setTimeout(() => feedback.style.display = 'none', 2000);
2449
+ });
2450
+ }
2451
+ </script>
2452
+ </body>
2453
+ </html>`;
2454
+ }
2455
+
2456
+ function analyzeSkillPersonality(skills) {
2457
+ const total = skills.length;
2458
+ const categories = {};
2459
+ let totalTokens = 0;
2460
+ let securityCount = 0;
2461
+ let pluginCount = 0;
2462
+
2463
+ for (const skill of skills) {
2464
+ categories[skill.category] = (categories[skill.category] || 0) + 1;
2465
+ totalTokens += skill.tokenCost || 0;
2466
+ if ((skill.allowedTools || []).length > 0) securityCount++;
2467
+ if ((skill.sources || []).some(s => s.includes('plugin'))) pluginCount++;
2468
+ }
2469
+
2470
+ const topCategory = Object.entries(categories).sort((a, b) => b[1] - a[1])[0];
2471
+ const isZh = lang() === 'zh';
2472
+
2473
+ if (total > 100) {
2474
+ return {
2475
+ type: isZh ? '收藏家' : 'Collector',
2476
+ emoji: '🏛️',
2477
+ title: 'The Collector',
2478
+ description: isZh
2479
+ ? '本地扫描发现了较大的技能库。建议定期复核描述、来源和实际需要。'
2480
+ : 'The local scan found a large skill inventory. Review descriptions, sources, and actual needs periodically.',
2481
+ advice: isZh ? '建议:定期审视,保留精品。质量 > 数量。' : 'Advice: Review regularly, keep the best. Quality > Quantity.',
2482
+ };
2483
+ }
2484
+
2485
+ if (total < 10) {
2486
+ return {
2487
+ type: isZh ? '极简主义者' : 'Minimalist',
2488
+ emoji: '🧘',
2489
+ title: 'The Minimalist',
2490
+ description: isZh
2491
+ ? '本地扫描发现了较小的技能库。建议复核它是否覆盖你实际需要的工作。'
2492
+ : 'The local scan found a small skill inventory. Review whether it covers the work you actually need.',
2493
+ advice: isZh ? '建议:保持精简,但可以探索新领域。' : 'Advice: Stay lean, but explore new domains.',
2494
+ };
2495
+ }
2496
+
2497
+ if (securityCount > total * 0.3) {
2498
+ return {
2499
+ type: isZh ? '工具声明较多' : 'Tool-Declared Profile',
2500
+ emoji: '🛡️',
2501
+ title: 'The Tool-Declared Profile',
2502
+ description: isZh
2503
+ ? '较多技能声明了可用工具。建议按需复核权限、命令和风险。'
2504
+ : 'Many skills declare allowed tools. Review permissions, commands, and risks where needed.',
2505
+ advice: isZh ? '建议:按需复核权限和命令。' : 'Advice: Review permissions and commands where needed.',
2506
+ };
2507
+ }
2508
+
2509
+ if (pluginCount > total * 0.5) {
2510
+ return {
2511
+ type: isZh ? '插件来源较多' : 'Plugin-Heavy Profile',
2512
+ emoji: '🔌',
2513
+ title: 'The Plugin-Heavy Profile',
2514
+ description: isZh
2515
+ ? '本地扫描发现较多插件目录来源。建议关注来源、质量和维护状态。'
2516
+ : 'The local scan found many plugin-directory sources. Review provenance, quality, and maintenance status.',
2517
+ advice: isZh ? '建议:关注来源、质量和维护状态。' : 'Advice: Review provenance, quality, and maintenance status.',
2518
+ };
2519
+ }
2520
+
2521
+ if (topCategory && topCategory[1] > total * 0.4) {
2522
+ const categoryNames = {
2523
+ 'coding': { zh: '代码工匠', en: 'Code Craftsman' },
2524
+ 'testing': { zh: '质量守护者', en: 'Quality Guardian' },
2525
+ 'devops': { zh: '部署大师', en: 'DevOps Master' },
2526
+ 'documentation': { zh: '文档专家', en: 'Documentation Expert' },
2527
+ 'analysis': { zh: '数据分析师', en: 'Data Analyst' },
2528
+ };
2529
+ const cat = categoryNames[topCategory[0]] || { zh: '领域专家', en: 'Domain Expert' };
2530
+ return {
2531
+ type: isZh ? cat.zh : cat.en,
2532
+ emoji: '🎯',
2533
+ title: 'The Specialist',
2534
+ description: isZh
2535
+ ? `本地元数据集中在 ${topCategory[0]} 类别。建议复核这种分布是否符合实际需要。`
2536
+ : `Local metadata is concentrated in the ${topCategory[0]} category. Review whether that distribution matches your needs.`,
2537
+ advice: isZh ? '建议:复核类别分布是否符合实际需要。' : 'Advice: Review whether the category distribution matches your needs.',
2538
+ };
2539
+ }
2540
+
2541
+ return {
2542
+ type: isZh ? '覆盖较广' : 'Broad Coverage',
2543
+ emoji: '🌟',
2544
+ title: 'The All-Rounder',
2545
+ description: isZh
2546
+ ? '本地扫描显示技能分布覆盖多个类别。建议复核是否存在不再需要的来源。'
2547
+ : 'The local scan shows skills across several categories. Review whether any sources are no longer needed.',
2548
+ advice: isZh ? '建议:复核来源和类别分布。' : 'Advice: Review sources and category distribution.',
2549
+ };
2550
+ }
2551
+
2552
+ function renderTopConsumers(skills, limit = 10) {
2553
+ const sorted = [...skills]
2554
+ .sort((a, b) => (b.tokenCost || 0) - (a.tokenCost || 0))
2555
+ .slice(0, limit);
2556
+
2557
+ const totalTokens = skills.reduce((sum, s) => sum + (s.tokenCost || 0), 0);
2558
+
2559
+ return sorted.map((skill, i) => {
2560
+ const percent = totalTokens > 0 ? ((skill.tokenCost || 0) / totalTokens * 100).toFixed(1) : 0;
2561
+ const barWidth = Math.min(percent * 2, 100);
2562
+ return {
2563
+ rank: i + 1,
2564
+ name: skill.name,
2565
+ tokenCost: skill.tokenCost || 0,
2566
+ percent: parseFloat(percent),
2567
+ barWidth,
2568
+ category: skill.category,
2569
+ };
2570
+ });
2571
+ }
2572
+
2573
+ function computeRadarScores(skills, health) {
2574
+ const isZh = lang() === 'zh';
2575
+ const tokenScore = Math.max(0, 100 - health.contextWindowPercent * 2);
2576
+ const dupScore = Math.max(0, 100 - health.duplicateGroups.length * 10);
2577
+ const secScore = Math.max(0, 100 - health.securityFlags.length * 10); // Changed from 15 to 10
2578
+ const freshScore = Math.max(0, 100 - health.staleSkills.length * 5);
2579
+ const budgetScore = Math.max(0, 100 - Math.max(0, health.budgetUsedPercent - 100) / 5);
2580
+
2581
+ return {
2582
+ dimensions: [
2583
+ { name: isZh ? '效率' : 'Efficiency', nameEn: 'Efficiency', score: Math.round(tokenScore) },
2584
+ { name: isZh ? '组织' : 'Organize', nameEn: 'Organize', score: Math.round(dupScore) },
2585
+ { name: isZh ? '安全' : 'Security', nameEn: 'Security', score: Math.round(secScore) },
2586
+ { name: isZh ? '新鲜' : 'Fresh', nameEn: 'Fresh', score: Math.round(freshScore) },
2587
+ { name: isZh ? '预算' : 'Budget', nameEn: 'Budget', score: Math.round(budgetScore) },
2588
+ ],
2589
+ overall: Math.round((tokenScore + dupScore + secScore + freshScore + budgetScore) / 5),
2590
+ };
2591
+ }
2592
+
2593
+ function computeWrappedStats(skills, health) {
2594
+ const total = skills.length;
2595
+ const categories = {};
2596
+ for (const skill of skills) {
2597
+ categories[skill.category] = (categories[skill.category] || 0) + 1;
2598
+ }
2599
+ const categoryCount = Object.keys(categories).length;
2600
+ const totalTokens = skills.reduce((sum, s) => sum + (s.tokenCost || 0), 0);
2601
+
2602
+ // Category breakdown
2603
+ const categoryBreakdown = Object.entries(categories)
2604
+ .sort((a, b) => b[1] - a[1])
2605
+ .map(([name, count]) => ({
2606
+ name,
2607
+ count,
2608
+ percent: Math.round((count / total) * 100),
2609
+ }));
2610
+
2611
+ // Short descriptions are useful review candidates, but do not imply low usage.
2612
+ const shortestDescriptions = [...skills]
2613
+ .sort((a, b) => (a.tokenCost || 0) - (b.tokenCost || 0))
2614
+ .slice(0, 5)
2615
+ .map(s => s.name);
2616
+
2617
+ // --- Metadata completeness analysis (description + tools + triggers + tokens) ---
2618
+ function computeReadinessScore(skill) {
2619
+ let score = 0;
2620
+ const descLen = (skill.description || '').length;
2621
+ if (descLen > 100) score += 20;
2622
+ if (descLen > 200) score += 10;
2623
+ if (descLen > 400) score += 10;
2624
+ if ((skill.allowedTools || []).length > 0) score += 30;
2625
+ if ((skill.triggers || []).length > 0) score += 20;
2626
+ const tokens = skill.tokenCost || 0;
2627
+ if (tokens > 50) score += 5;
2628
+ if (tokens > 100) score += 5;
2629
+ return score;
2630
+ }
2631
+
2632
+ const scored = skills.map(s => ({ ...s, _readiness: computeReadinessScore(s) }));
2633
+ const coreSkills = scored.filter(s => s._readiness >= 50);
2634
+ const readySkills = scored.filter(s => s._readiness >= 20 && s._readiness < 50);
2635
+ const untappedSkills = scored.filter(s => s._readiness < 20);
2636
+ const coreCount = coreSkills.length;
2637
+ const readyCount = readySkills.length;
2638
+ const untappedCount = untappedSkills.length;
2639
+ const corePercent = Math.round((coreCount / total) * 100);
2640
+ const readyPercent = Math.round((readyCount / total) * 100);
2641
+ const untappedPercent = Math.round((untappedCount / total) * 100);
2642
+
2643
+ // Top categories for display (still useful for archetype)
2644
+ const sortedCats = Object.entries(categories).sort((a, b) => b[1] - a[1]);
2645
+
2646
+ // --- Local inventory profile ---
2647
+ const topCatPercent = sortedCats.length > 0 ? Math.round((sortedCats[0][1] / total) * 100) : 0;
2648
+ let archetype;
2649
+ if (total <= 3) {
2650
+ archetype = { name: 'Small Inventory', emoji: '🌱', tagline: 'A few local skills were detected' };
2651
+ } else if (total <= 10) {
2652
+ archetype = { name: 'Compact Inventory', emoji: '🔍', tagline: 'A compact set of local skills was detected' };
2653
+ } else if (topCatPercent >= 60 && total >= 20) {
2654
+ archetype = { name: 'Category-Focused Inventory', emoji: '🎯', tagline: 'Local metadata is concentrated in one category' };
2655
+ } else if (total >= 200 && categoryCount >= 7) {
2656
+ archetype = { name: 'Large Broad Inventory', emoji: '🏗️', tagline: 'Many local skills span several categories' };
2657
+ } else if (total >= 200) {
2658
+ archetype = { name: 'Large Inventory', emoji: '⚡', tagline: 'Many local skills were detected' };
2659
+ } else if (categoryCount >= 6 && total < 100) {
2660
+ archetype = { name: 'Broad Inventory', emoji: '🧭', tagline: 'Local skills span several categories' };
2661
+ } else if (total >= 50 && categoryCount <= 4) {
2662
+ archetype = { name: 'Focused Inventory', emoji: '🔬', tagline: 'Many local skills are concentrated in a few categories' };
2663
+ } else {
2664
+ archetype = { name: 'Balanced Inventory', emoji: '⚖️', tagline: 'Local skills are distributed across categories' };
2665
+ }
2666
+
2667
+ // Core categories from high-readiness skills
2668
+ const coreCatsFromScore = {};
2669
+ for (const s of coreSkills) {
2670
+ coreCatsFromScore[s.category] = (coreCatsFromScore[s.category] || 0) + 1;
2671
+ }
2672
+ const coreCats = Object.entries(coreCatsFromScore)
2673
+ .sort((a, b) => b[1] - a[1])
2674
+ .slice(0, 3)
2675
+ .map(([name]) => name);
2676
+ if (coreCats.length === 0 && sortedCats.length > 0) {
2677
+ coreCats.push(...sortedCats.slice(0, 2).map(([name]) => name));
2678
+ }
2679
+
2680
+ return {
2681
+ total,
2682
+ categoryCount,
2683
+ totalTokens,
2684
+ categoryBreakdown,
2685
+ shortestDescriptions,
2686
+ coreCount,
2687
+ readyCount,
2688
+ untappedCount,
2689
+ corePercent,
2690
+ readyPercent,
2691
+ untappedPercent,
2692
+ coreCats,
2693
+ archetype,
2694
+ };
2695
+ }
2696
+
2697
+ function renderWrappedTerminal(data) {
2698
+ const skills = data.skills || [];
2699
+ const health = computeHealthStats(skills);
2700
+ const personality = analyzeSkillPersonality(skills);
2701
+ const wrapped = computeWrappedStats(skills, health);
2702
+ const isZh = lang() === 'zh';
2703
+
2704
+ const lines = [
2705
+ '╔══════════════════════════════════════════════════════════════╗',
2706
+ isZh ? '║ 你的 AI 技能报告 ║'
2707
+ : '║ Your AI Skill Report ║',
2708
+ '╚══════════════════════════════════════════════════════════════╝',
2709
+ '',
2710
+ `${wrapped.archetype.emoji} ${isZh ? '本地技能画像' : 'Local Inventory Profile'}: ${wrapped.archetype.name}`,
2711
+ ` ${isZh ? '本地元数据特征:' : 'Local metadata pattern:'} ${wrapped.archetype.tagline}`,
2712
+ '',
2713
+ isZh ? '── 你的数据 ─────────────────────────────────────────────' : '── Your Stats ─────────────────────────────────────────────',
2714
+ ` 📦 ${isZh ? '总技能数' : 'Total Skills'}: ${wrapped.total}`,
2715
+ ` 📂 ${isZh ? '覆盖领域' : 'Categories'}: ${wrapped.categoryCount}`,
2716
+ ` 🔤 ${isZh ? '描述 Token 估算' : 'Description Token Estimate'}: ~${(wrapped.totalTokens / 1000).toFixed(1)}K`,
2717
+ '',
2718
+ isZh ? '── 元数据完整度 ───────────────────────────────────────' : '── Metadata Completeness ──────────────────────────────────',
2719
+ isZh
2720
+ ? ` ⚡ ${wrapped.total} 个技能中,${wrapped.coreCount} 个元数据较完整`
2721
+ : ` ⚡ ${wrapped.coreCount} of ${wrapped.total} skills have richer metadata`,
2722
+ isZh
2723
+ ? ` ${wrapped.readyCount} 个元数据一般 | ${wrapped.untappedCount} 个元数据较少`
2724
+ : ` ${wrapped.readyCount} partial metadata | ${wrapped.untappedCount} sparse metadata`,
2725
+ isZh
2726
+ ? ` 🎯 核心领域: ${wrapped.coreCats.join(', ')}`
2727
+ : ` 🎯 Core domains: ${wrapped.coreCats.join(', ')}`,
2728
+ '',
2729
+ ];
2730
+
2731
+ // Category breakdown
2732
+ lines.push(isZh ? '── 技能 DNA ─────────────────────────────────────────────' : '── Skill DNA ─────────────────────────────────────────────');
2733
+ for (const cat of wrapped.categoryBreakdown.slice(0, 6)) {
2734
+ const bar = '█'.repeat(Math.max(1, Math.round(cat.percent / 5)));
2735
+ lines.push(` ${cat.name.padEnd(15)} ${bar} ${cat.count} (${cat.percent}%)`);
2736
+ }
2737
+ lines.push('');
2738
+
2739
+ // Local review candidates
2740
+ lines.push(isZh ? '── 本地复核提示 ─────────────────────────────────────────' : '── Local Review Notes ─────────────────────────────────────');
2741
+ if (wrapped.shortestDescriptions.length > 0) {
2742
+ lines.push(isZh
2743
+ ? ` 🔍 描述最短的技能: ${wrapped.shortestDescriptions[0]}`
2744
+ : ` 🔍 Shortest skill description: ${wrapped.shortestDescriptions[0]}`);
2745
+ }
2746
+ lines.push(isZh
2747
+ ? ' 📍 以上结论仅基于本地扫描结果,不代表社区排名或实际使用频率'
2748
+ : ' 📍 These notes use local scan data only; they are not community rankings or usage metrics');
2749
+ lines.push('');
2750
+
2751
+ // CTA with suspense-driven share text
2752
+ lines.push(isZh ? '── 分享你的报告 ─────────────────────────────────────────' : '── Share Your Report ──────────────────────────────────────');
2753
+ const shareHint = isZh
2754
+ ? ` 📤 "${wrapped.archetype.name} — 本地扫描到 ${wrapped.total} 个技能,其中 ${wrapped.untappedCount} 个元数据较少。"`
2755
+ : ` 📤 "${wrapped.archetype.name} — ${wrapped.total} local skills scanned, including ${wrapped.untappedCount} with sparse metadata."`;
2756
+ lines.push(shareHint);
2757
+ lines.push(isZh
2758
+ ? ' 🔗 使用 --open 生成可分享的 HTML 报告'
2759
+ : ' 🔗 Use --open to generate a shareable HTML report');
2760
+ lines.push('');
2761
+
2762
+ return lines.join('\n');
2763
+ }
2764
+
2765
+ function renderWrappedHTML(data) {
2766
+ const skills = data.skills || [];
2767
+ const health = computeHealthStats(skills);
2768
+ const personality = analyzeSkillPersonality(skills);
2769
+ const radar = computeRadarScores(skills, health);
2770
+ const wrapped = computeWrappedStats(skills, health);
2771
+ const isZh = lang() === 'zh';
2772
+
2773
+ function renderRadarChart(dimensions) {
2774
+ const size = 200;
2775
+ const center = size / 2;
2776
+ const radius = 80;
2777
+ const levels = 5;
2778
+
2779
+ function pentagonPoints(r) {
2780
+ return Array.from({ length: 5 }, (_, i) => {
2781
+ const angle = (Math.PI * 2 * i) / 5 - Math.PI / 2;
2782
+ return `${center + r * Math.cos(angle)},${center + r * Math.sin(angle)}`;
2783
+ }).join(' ');
2784
+ }
2785
+
2786
+ const dataPoints = dimensions.map((d, i) => {
2787
+ const angle = (Math.PI * 2 * i) / 5 - Math.PI / 2;
2788
+ const r = (d.score / 100) * radius;
2789
+ return `${center + r * Math.cos(angle)},${center + r * Math.sin(angle)}`;
2790
+ }).join(' ');
2791
+
2792
+ const labels = dimensions.map((d, i) => {
2793
+ const angle = (Math.PI * 2 * i) / 5 - Math.PI / 2;
2794
+ const labelR = radius + 25;
2795
+ const x = center + labelR * Math.cos(angle);
2796
+ const y = center + labelR * Math.sin(angle);
2797
+ const anchor = i === 0 ? 'middle' : i < 3 ? 'start' : 'end';
2798
+ return `<text x="${x}" y="${y}" text-anchor="${anchor}" fill="#94a3b8" font-size="11">${d.name}</text>`;
2799
+ }).join('');
2800
+
2801
+ return `
2802
+ <svg viewBox="0 0 ${size} ${size}" class="radar-chart">
2803
+ ${Array.from({ length: levels }, (_, i) => {
2804
+ const r = (radius / levels) * (i + 1);
2805
+ return `<polygon points="${pentagonPoints(r)}" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>`;
2806
+ }).join('')}
2807
+ <polygon points="${dataPoints}" fill="rgba(124,58,237,0.3)" stroke="#7c3aed" stroke-width="2"/>
2808
+ ${labels}
2809
+ </svg>
2810
+ `;
2811
+ }
2812
+
2813
+ const categoryBars = wrapped.categoryBreakdown.slice(0, 6).map(cat => {
2814
+ const barWidth = Math.max(2, cat.percent);
2815
+ const colors = ['#7c3aed', '#06b6d4', '#f59e0b', '#10b981', '#ef4444', '#ec4899'];
2816
+ const color = colors[wrapped.categoryBreakdown.indexOf(cat) % colors.length];
2817
+ return `
2818
+ <div class="dna-row">
2819
+ <span class="dna-name">${escapeHtml(cat.name)}</span>
2820
+ <div class="dna-bar"><div class="dna-fill" style="width:${barWidth}%;background:${color}"></div></div>
2821
+ <span class="dna-count">${cat.count} (${cat.percent}%)</span>
2822
+ </div>`;
2823
+ }).join('');
2824
+
2825
+ const shareText = isZh
2826
+ ? `${wrapped.archetype.emoji} ${wrapped.archetype.name} — 本地扫描到 ${wrapped.total} 个技能,其中 ${wrapped.untappedCount} 个元数据较少。`
2827
+ : `${wrapped.archetype.emoji} ${wrapped.archetype.name} — ${wrapped.total} local skills scanned, including ${wrapped.untappedCount} with sparse metadata.`;
2828
+
2829
+ return `<!DOCTYPE html>
2830
+ <html lang="${lang()}">
2831
+ <head>
2832
+ <meta charset="utf-8">
2833
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2834
+ <title>${isZh ? '我的 AI 技能报告' : 'My AI Skill Report'} — skill-guide</title>
2835
+ <meta property="og:title" content="${isZh ? '我的 AI 技能报告' : 'My AI Skill Report'} — skill-guide">
2836
+ <meta property="og:description" content="${escapeHtml(shareText)}">
2837
+ <style>
2838
+ :root {
2839
+ --bg: #0f0f23;
2840
+ --card: #1a1a2e;
2841
+ --text: #e0e0e0;
2842
+ --muted: #888;
2843
+ --accent: #7c3aed;
2844
+ --accent2: #06b6d4;
2845
+ --good: #10b981;
2846
+ }
2847
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2848
+ body {
2849
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
2850
+ background: var(--bg);
2851
+ color: var(--text);
2852
+ min-height: 100vh;
2853
+ }
2854
+ .hero {
2855
+ text-align: center;
2856
+ padding: 4rem 2rem;
2857
+ background: linear-gradient(135deg, #1e1b4b 0%, #0f172a 50%, #0c1222 100%);
2858
+ }
2859
+ .hero-emoji { font-size: 4rem; margin-bottom: 1rem; }
2860
+ .hero-title {
2861
+ font-size: 2.5rem;
2862
+ font-weight: 800;
2863
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
2864
+ -webkit-background-clip: text;
2865
+ -webkit-text-fill-color: transparent;
2866
+ margin-bottom: 0.5rem;
2867
+ }
2868
+ .hero-subtitle { color: var(--muted); font-size: 1.1rem; max-width: 600px; margin: 0 auto; }
2869
+ .container { max-width: 900px; margin: 0 auto; padding: 2rem; }
2870
+ .section {
2871
+ background: var(--card);
2872
+ border-radius: 16px;
2873
+ padding: 2rem;
2874
+ margin-bottom: 2rem;
2875
+ border: 1px solid rgba(255,255,255,0.05);
2876
+ }
2877
+ .section-title {
2878
+ font-size: 1.25rem;
2879
+ font-weight: 600;
2880
+ margin-bottom: 1.5rem;
2881
+ display: flex;
2882
+ align-items: center;
2883
+ gap: 0.5rem;
2884
+ }
2885
+ .stats-grid {
2886
+ display: grid;
2887
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
2888
+ gap: 1rem;
2889
+ margin-bottom: 1.5rem;
2890
+ }
2891
+ .stat-card {
2892
+ background: rgba(255,255,255,0.05);
2893
+ border-radius: 12px;
2894
+ padding: 1.25rem;
2895
+ text-align: center;
2896
+ }
2897
+ .stat-value {
2898
+ font-size: 2rem;
2899
+ font-weight: 700;
2900
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
2901
+ -webkit-background-clip: text;
2902
+ -webkit-text-fill-color: transparent;
2903
+ }
2904
+ .stat-label { color: var(--muted); font-size: 0.85rem; margin-top: 0.25rem; }
2905
+ .compare-row {
2906
+ display: flex;
2907
+ align-items: center;
2908
+ gap: 1rem;
2909
+ padding: 0.75rem 0;
2910
+ border-bottom: 1px solid rgba(255,255,255,0.05);
2911
+ }
2912
+ .compare-label { width: 140px; font-weight: 500; }
2913
+ .compare-bar-wrap { flex: 1; }
2914
+ .compare-bar {
2915
+ height: 10px;
2916
+ background: rgba(255,255,255,0.1);
2917
+ border-radius: 5px;
2918
+ overflow: hidden;
2919
+ position: relative;
2920
+ }
2921
+ .compare-fill {
2922
+ height: 100%;
2923
+ border-radius: 5px;
2924
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
2925
+ }
2926
+ .compare-value { width: 80px; text-align: right; font-weight: 600; color: var(--accent); }
2927
+ .dna-row {
2928
+ display: flex;
2929
+ align-items: center;
2930
+ gap: 0.75rem;
2931
+ padding: 0.5rem 0;
2932
+ }
2933
+ .dna-name { width: 120px; font-size: 0.9rem; color: var(--muted); }
2934
+ .dna-bar { flex: 1; height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; }
2935
+ .dna-fill { height: 100%; border-radius: 4px; transition: width 0.5s; }
2936
+ .dna-count { width: 80px; text-align: right; font-size: 0.85rem; color: var(--muted); }
2937
+ .radar-container { display: flex; justify-content: center; padding: 1rem; }
2938
+ .radar-chart { width: min(280px, 80vw); height: min(280px, 80vw); }
2939
+ .share-section {
2940
+ text-align: center;
2941
+ padding: 2rem;
2942
+ background: linear-gradient(135deg, rgba(124,58,237,0.1), rgba(6,182,212,0.1));
2943
+ border-radius: 16px;
2944
+ margin-top: 2rem;
2945
+ }
2946
+ .share-btn {
2947
+ display: inline-block;
2948
+ padding: 0.75rem 2rem;
2949
+ border-radius: 8px;
2950
+ font-weight: 600;
2951
+ text-decoration: none;
2952
+ font-size: 1rem;
2953
+ cursor: pointer;
2954
+ border: none;
2955
+ margin: 0.5rem;
2956
+ transition: transform 0.2s, box-shadow 0.2s;
2957
+ }
2958
+ .share-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(124,58,237,0.3); }
2959
+ .share-btn.primary { background: linear-gradient(135deg, #7c3aed, #06b6d4); color: #fff; }
2960
+ .share-btn.secondary { background: rgba(255,255,255,0.1); color: var(--text); }
2961
+ .share-text {
2962
+ background: var(--card);
2963
+ border-radius: 12px;
2964
+ padding: 1.5rem;
2965
+ margin: 1rem auto;
2966
+ max-width: 600px;
2967
+ text-align: left;
2968
+ font-size: 0.95rem;
2969
+ line-height: 1.6;
2970
+ border: 1px solid rgba(255,255,255,0.1);
2971
+ }
2972
+ .cta {
2973
+ text-align: center;
2974
+ padding: 2rem;
2975
+ margin-top: 2rem;
2976
+ }
2977
+ .cta code {
2978
+ background: var(--card);
2979
+ padding: 0.5rem 1rem;
2980
+ border-radius: 8px;
2981
+ font-size: 1.1rem;
2982
+ display: inline-block;
2983
+ margin: 0.5rem 0;
2984
+ }
2985
+ .footer {
2986
+ text-align: center;
2987
+ padding: 2rem;
2988
+ color: var(--muted);
2989
+ font-size: 0.85rem;
2990
+ }
2991
+ .gap-visual {
2992
+ display: flex;
2993
+ align-items: center;
2994
+ justify-content: center;
2995
+ gap: 2rem;
2996
+ margin: 1.5rem 0;
2997
+ }
2998
+ .gap-core, .gap-ready, .gap-untapped {
2999
+ text-align: center;
3000
+ flex: 1;
3001
+ padding: 1.25rem;
3002
+ border-radius: 12px;
3003
+ }
3004
+ .gap-core {
3005
+ background: rgba(124,58,237,0.1);
3006
+ border: 1px solid rgba(124,58,237,0.2);
3007
+ }
3008
+ .gap-ready {
3009
+ background: rgba(6,182,212,0.08);
3010
+ border: 1px solid rgba(6,182,212,0.15);
3011
+ }
3012
+ .gap-untapped {
3013
+ background: rgba(245,158,11,0.08);
3014
+ border: 1px solid rgba(245,158,11,0.15);
3015
+ }
3016
+ .gap-number {
3017
+ font-size: 2.5rem;
3018
+ font-weight: 800;
3019
+ }
3020
+ .gap-core .gap-number { color: var(--accent); }
3021
+ .gap-ready .gap-number { color: var(--accent2); }
3022
+ .gap-untapped .gap-number { color: #f59e0b; }
3023
+ .gap-label { font-weight: 600; margin-top: 0.25rem; }
3024
+ .gap-detail { color: var(--muted); font-size: 0.85rem; margin-top: 0.5rem; }
3025
+ .gap-divider { display: flex; align-items: center; }
3026
+ .gap-vs {
3027
+ background: rgba(255,255,255,0.1);
3028
+ color: var(--muted);
3029
+ font-weight: 700;
3030
+ font-size: 0.85rem;
3031
+ width: 36px;
3032
+ height: 36px;
3033
+ border-radius: 50%;
3034
+ display: flex;
3035
+ align-items: center;
3036
+ justify-content: center;
3037
+ }
3038
+ .gap-bar-wrap {
3039
+ display: flex;
3040
+ height: 8px;
3041
+ border-radius: 4px;
3042
+ overflow: hidden;
3043
+ margin-top: 1rem;
3044
+ }
3045
+ .gap-bar-core { background: var(--accent); }
3046
+ .gap-bar-ready { background: var(--accent2); }
3047
+ .gap-bar-untapped { background: #f59e0b; }
3048
+ .gap-bar-labels {
3049
+ display: flex;
3050
+ justify-content: space-between;
3051
+ font-size: 0.8rem;
3052
+ color: var(--muted);
3053
+ margin-top: 0.4rem;
3054
+ }
3055
+ @media (max-width: 600px) {
3056
+ .hero-title { font-size: 1.8rem; }
3057
+ .stats-grid { grid-template-columns: repeat(2, 1fr); }
3058
+ .gap-visual { flex-direction: column; gap: 1rem; }
3059
+ .gap-divider { transform: rotate(90deg); }
3060
+ }
3061
+ </style>
3062
+ </head>
3063
+ <body>
3064
+ <div class="hero">
3065
+ <div class="hero-emoji">${wrapped.archetype.emoji}</div>
3066
+ <h1 class="hero-title">${isZh ? wrapped.archetype.name : wrapped.archetype.name}</h1>
3067
+ <p class="hero-subtitle">${escapeHtml(wrapped.archetype.tagline)}</p>
3068
+ </div>
3069
+
3070
+ <div class="container">
3071
+ <div class="section">
3072
+ <h2 class="section-title">${isZh ? '📊 你的数据' : '📊 Your Stats'}</h2>
3073
+ <div class="stats-grid">
3074
+ <div class="stat-card">
3075
+ <div class="stat-value">${wrapped.total}</div>
3076
+ <div class="stat-label">${isZh ? '总技能数' : 'Total Skills'}</div>
3077
+ </div>
3078
+ <div class="stat-card">
3079
+ <div class="stat-value">${wrapped.categoryCount}</div>
3080
+ <div class="stat-label">${isZh ? '覆盖领域' : 'Categories'}</div>
3081
+ </div>
3082
+ <div class="stat-card">
3083
+ <div class="stat-value">${(wrapped.totalTokens / 1000).toFixed(1)}K</div>
3084
+ <div class="stat-label">${isZh ? '描述 Token 估算' : 'Description Token Estimate'}</div>
3085
+ </div>
3086
+ </div>
3087
+ </div>
3088
+
3089
+ <div class="section" style="background:linear-gradient(135deg, rgba(245,158,11,0.08), rgba(124,58,237,0.08));border:1px solid rgba(245,158,11,0.15)">
3090
+ <h2 class="section-title">${isZh ? '⚡ 元数据完整度' : '⚡ Metadata Completeness'}</h2>
3091
+ <p style="color:var(--muted);margin-bottom:1rem;font-size:0.9rem">
3092
+ ${isZh ? '基于描述完整度、工具配置和触发词' : 'Based on description depth, tool config, and triggers'}
3093
+ </p>
3094
+ <div class="gap-visual">
3095
+ <div class="gap-core">
3096
+ <div class="gap-number">${wrapped.coreCount}</div>
3097
+ <div class="gap-label">${isZh ? '元数据较完整' : 'Richer Metadata'}</div>
3098
+ <div class="gap-detail">${isZh ? '描述、工具或触发词较完整' : 'More description, tool, or trigger metadata'}</div>
3099
+ </div>
3100
+ <div class="gap-divider"><div class="gap-vs">+</div></div>
3101
+ <div class="gap-ready">
3102
+ <div class="gap-number">${wrapped.readyCount}</div>
3103
+ <div class="gap-label">${isZh ? '元数据一般' : 'Partial Metadata'}</div>
3104
+ <div class="gap-detail">${isZh ? '可以按需补充' : 'Review whether more detail is useful'}</div>
3105
+ </div>
3106
+ <div class="gap-divider"><div class="gap-vs">+</div></div>
3107
+ <div class="gap-untapped">
3108
+ <div class="gap-number">${wrapped.untappedCount}</div>
3109
+ <div class="gap-label">${isZh ? '元数据较少' : 'Sparse Metadata'}</div>
3110
+ <div class="gap-detail">${isZh ? '建议复核描述和触发词' : 'Review descriptions and triggers'}</div>
3111
+ </div>
3112
+ </div>
3113
+ <div class="gap-bar-wrap">
3114
+ <div class="gap-bar-core" style="width:${wrapped.corePercent}%"></div>
3115
+ <div class="gap-bar-ready" style="width:${wrapped.readyPercent}%"></div>
3116
+ <div class="gap-bar-untapped" style="width:${wrapped.untappedPercent}%"></div>
3117
+ </div>
3118
+ <div class="gap-bar-labels">
3119
+ <span>${wrapped.corePercent}% ${isZh ? '较完整' : 'richer'}</span>
3120
+ <span>${wrapped.readyPercent}% ${isZh ? '一般' : 'partial'}</span>
3121
+ <span>${wrapped.untappedPercent}% ${isZh ? '较少' : 'sparse'}</span>
3122
+ </div>
3123
+ </div>
3124
+
3125
+ <div class="section">
3126
+ <h2 class="section-title">${isZh ? '📍 本地画像说明' : '📍 Local Profile Notes'}</h2>
3127
+ <p style="color:var(--muted);font-size:0.9rem">
3128
+ ${isZh
3129
+ ? '此报告只使用当前设备扫描到的技能元数据。它不会推断社区排名、实际使用频率或删除安全性。'
3130
+ : 'This report uses skill metadata scanned on this device only. It does not infer community rank, actual usage, or whether deletion is safe.'}
3131
+ </p>
3132
+ </div>
3133
+
3134
+ <div class="section">
3135
+ <h2 class="section-title">${isZh ? '🧬 技能 DNA' : '🧬 Skill DNA'}</h2>
3136
+ ${categoryBars}
3137
+ </div>
3138
+
3139
+ <div class="section">
3140
+ <h2 class="section-title">${isZh ? '📈 五维健康雷达' : '📈 Health Radar'}</h2>
3141
+ <div class="radar-container">
3142
+ ${renderRadarChart(radar.dimensions)}
3143
+ </div>
3144
+ </div>
3145
+
3146
+ <div class="share-section">
3147
+ <h2>${isZh ? '📤 分享你的报告' : '📤 Share Your Report'}</h2>
3148
+ <p style="color:var(--muted);margin:0.5rem 0">${isZh ? '让朋友也看看自己的技能 DNA' : 'Let friends discover their skill DNA'}</p>
3149
+ <div class="share-text">${escapeHtml(shareText)}</div>
3150
+ <button class="share-btn primary" onclick="copyShare()">${isZh ? '复制分享文案' : 'Copy Share Text'}</button>
3151
+ <button class="share-btn secondary" onclick="copyLink()">${isZh ? '复制命令' : 'Copy Command'}</button>
3152
+ </div>
3153
+
3154
+ <div class="cta">
3155
+ <p style="color:var(--muted);margin-bottom:0.5rem">${isZh ? '生成你自己的技能报告' : 'Generate your own skill report'}</p>
3156
+ <code>npx skill-guide --wrapped --open</code>
3157
+ </div>
3158
+ </div>
3159
+
3160
+ <div class="footer">
3161
+ <p>Powered by <a href="https://github.com/gtskevin/skill-guide" style="color:var(--accent)">skill-guide</a></p>
3162
+ </div>
3163
+
3164
+ <script>
3165
+ function copyShare() {
3166
+ navigator.clipboard.writeText(${JSON.stringify(shareText + '\n\nnpx skill-guide --wrapped --open')}).then(() => {
3167
+ alert('${isZh ? '已复制到剪贴板!' : 'Copied to clipboard!'}');
3168
+ });
3169
+ }
3170
+ function copyLink() {
3171
+ navigator.clipboard.writeText('npx skill-guide --wrapped --open').then(() => {
3172
+ alert('${isZh ? '已复制到剪贴板!' : 'Copied to clipboard!'}');
3173
+ });
3174
+ }
3175
+ </script>
3176
+ </body>
3177
+ </html>`;
3178
+ }
3179
+
3180
+ function generatePrescription(skills, health) {
3181
+ const prescriptions = [];
3182
+ const totalTokens = skills.reduce((sum, s) => sum + (s.tokenCost || 0), 0);
3183
+
3184
+ const topConsumers = [...skills]
3185
+ .sort((a, b) => (b.tokenCost || 0) - (a.tokenCost || 0))
3186
+ .slice(0, 5);
3187
+
3188
+ if (topConsumers.length > 0) {
3189
+ const topTokens = topConsumers.reduce((sum, s) => sum + (s.tokenCost || 0), 0);
3190
+ const percent = totalTokens > 0 ? (topTokens / totalTokens * 100).toFixed(0) : 0;
3191
+ const isSignificant = parseInt(percent) >= 10;
3192
+
3193
+ prescriptions.push({
3194
+ type: 'optimize',
3195
+ emoji: isSignificant ? '🎯' : '💡',
3196
+ title: isSignificant ? '描述复核' : '优化建议',
3197
+ titleEn: isSignificant ? 'Description Review' : 'Optimization Tips',
3198
+ description: isSignificant
3199
+ ? `这 ${topConsumers.length} 个技能占描述 Token 估算的 ${percent}%,建议复核是否需要精简`
3200
+ : `你的技能库很均衡,没有明显的瘦身空间。Top ${topConsumers.length} 只占 ${percent}%`,
3201
+ descriptionEn: isSignificant
3202
+ ? `These ${topConsumers.length} skills account for ${percent}% of estimated description tokens; review whether their descriptions should be shortened`
3203
+ : `Your skill library is well-balanced. Top ${topConsumers.length} only account for ${percent}%`,
3204
+ items: topConsumers.map(s => ({
3205
+ name: s.name,
3206
+ tokens: s.tokenCost,
3207
+ action: isSignificant ? '复核并按需精简描述' : '保持现状',
3208
+ actionEn: isSignificant ? 'Review and shorten the description if appropriate' : 'Keep as is',
3209
+ })),
3210
+ impact: isSignificant ? 'high' : 'low',
3211
+ });
3212
+ }
3213
+
3214
+ if (health.securityFlags.length > 0) {
3215
+ prescriptions.push({
3216
+ type: 'security',
3217
+ emoji: '🛡️',
3218
+ title: '安全审查',
3219
+ titleEn: 'Security Review',
3220
+ description: `${health.securityFlags.length} 个技能有安全风险标记,建议人工审查`,
3221
+ descriptionEn: `${health.securityFlags.length} skills have security flags, recommend manual review`,
3222
+ items: health.securityFlags.slice(0, 5).map(s => ({
3223
+ name: s.name,
3224
+ flags: s.flags,
3225
+ action: '审查权限和命令',
3226
+ actionEn: 'Review permissions and commands',
3227
+ })),
3228
+ impact: 'medium',
3229
+ });
3230
+ }
3231
+
3232
+ if (health.duplicateGroups.length > 0) {
3233
+ prescriptions.push({
3234
+ type: 'dedup',
3235
+ emoji: '🔄',
3236
+ title: '去重优化',
3237
+ titleEn: 'Deduplication',
3238
+ description: `发现 ${health.duplicateGroups.length} 组同名技能,请复核来源和自定义改动`,
3239
+ descriptionEn: `Found ${health.duplicateGroups.length} same-name groups; review sources and custom changes`,
3240
+ items: health.duplicateGroups.slice(0, 3).map(g => ({
3241
+ name: g.names.join(' = '),
3242
+ action: '复核来源后再决定是否处理',
3243
+ actionEn: 'Review sources before deciding what to change',
3244
+ })),
3245
+ impact: 'low',
3246
+ });
3247
+ }
3248
+
3249
+ if (health.budgetUsedPercent > 100) {
3250
+ const overAmount = health.totalDescriptionLength - health.descriptionBudget;
3251
+ prescriptions.push({
3252
+ type: 'budget',
3253
+ emoji: '📦',
3254
+ title: '预算超支',
3255
+ titleEn: 'Budget Overage',
3256
+ description: `描述总长度超出本地参考预算 ${overAmount.toLocaleString()} 字符,建议复核较长描述`,
3257
+ descriptionEn: `Total description length exceeds the local reference budget by ${overAmount.toLocaleString()} chars; review longer descriptions`,
3258
+ items: [{
3259
+ name: '整体',
3260
+ action: '精简技能描述,删除不必要的细节',
3261
+ actionEn: 'Shorten skill descriptions, remove unnecessary details',
3262
+ }],
3263
+ impact: 'high',
3264
+ });
3265
+ }
3266
+
3267
+ return prescriptions;
3268
+ }
3269
+
3270
+ function main() {
3271
+ const mode = parseMode();
3272
+ if (mode.mode === 'help') {
3273
+ process.stdout.write(`${usage()}\n`);
3274
+ return;
3275
+ }
3276
+
3277
+ // --doctor: terminal-only diagnostic
3278
+ if (mode.mode === 'doctor') {
3279
+ const data = runScanner(mode);
3280
+ applyPlatformFilter(data);
3281
+ process.stdout.write(`${printDoctor(data)}\n`);
3282
+ return;
3283
+ }
3284
+
3285
+ const format = getArgValue('--format') || 'html';
3286
+
3287
+ // --find mode: try skill deep-dive, fall back to search
3288
+ if (mode.mode === 'find') {
3289
+ const skillData = runScanner(mode);
3290
+ if (skillData.error) {
3291
+ // Skill not found — fall back to search
3292
+ mode.mode = 'search';
3293
+ const searchArgs = ['--search', mode.value];
3294
+ if (hasFlag('--refresh')) searchArgs.push('--refresh');
3295
+ const result = spawnSync(process.execPath, [SCANNER, ...searchArgs], {
3296
+ cwd: process.cwd(), encoding: 'utf8',
3297
+ stdio: ['ignore', 'pipe', 'pipe'],
3298
+ });
3299
+ if (result.status !== 0) {
3300
+ process.stderr.write(result.stderr || '');
3301
+ process.exit(result.status || 1);
3302
+ }
3303
+ const data = JSON.parse(result.stdout);
3304
+ applyPlatformFilter(data);
3305
+ if (format === 'json') {
3306
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
3307
+ return;
3308
+ }
3309
+ process.stdout.write(renderDefaultTerminal(data.skills || []));
3310
+ const output = path.resolve(getArgValue('--output') || defaultOutputPath(mode));
3311
+ fs.mkdirSync(path.dirname(output), { recursive: true });
3312
+ fs.writeFileSync(output, renderHtml(data, mode), 'utf8');
3313
+ if (shouldAutoOpen()) openFile(output);
3314
+ process.stdout.write(`Generated ${output}\n`);
3315
+ } else {
3316
+ // Skill found — deep dive
3317
+ mode.mode = 'skill';
3318
+ applyPlatformFilter(skillData);
3319
+ if (format === 'json') {
3320
+ process.stdout.write(JSON.stringify(skillData, null, 2) + '\n');
3321
+ return;
3322
+ }
3323
+ const output = path.resolve(getArgValue('--output') || defaultOutputPath(mode));
3324
+ fs.mkdirSync(path.dirname(output), { recursive: true });
3325
+ fs.writeFileSync(output, renderHtml(skillData, mode), 'utf8');
3326
+ if (shouldAutoOpen()) openFile(output);
3327
+ process.stdout.write(`Generated ${output}\n`);
3328
+ }
3329
+ return;
3330
+ }
3331
+
3332
+ // --review mode (JSON-only for agent programmatic consumption)
3333
+ if (mode.mode === 'review') {
3334
+ const data = runScanner(mode);
3335
+ applyPlatformFilter(data);
3336
+ const details = doctorDetails(data);
3337
+ const brief = buildReviewBrief(data, details);
3338
+ brief.copyPrompt = buildCopyPrompt(brief);
3339
+ process.stdout.write(JSON.stringify(brief, null, 2) + '\n');
3340
+ return;
3341
+ }
3342
+
3343
+ // --recommend mode (online registry data)
3344
+ if (mode.mode === 'recommend') {
3345
+ const data = runScanner(mode);
3346
+ applyPlatformFilter(data);
3347
+ const installed = data.skills;
3348
+ const refresh = hasFlag('--refresh');
3349
+ const onlineEntries = registryModule.fetchRegistry({ refresh });
3350
+ const recommendations = registryModule.recommend(installed, onlineEntries);
3351
+ if (format === 'json') {
3352
+ process.stdout.write(JSON.stringify({ installed, recommendations }, null, 2) + '\n');
3353
+ return;
3354
+ }
3355
+ process.stdout.write(renderRecommendTerminal(data, recommendations));
3356
+ const outputFile = getArgValue('--output');
3357
+ const html = renderRecommendHTML(data, recommendations, getArgValue('--user'));
3358
+ const defaultFile = path.join(os.tmpdir(), 'skill-guide-recommend.html');
3359
+ const targetFile = outputFile ? path.resolve(outputFile) : defaultFile;
3360
+ fs.mkdirSync(path.dirname(targetFile), { recursive: true });
3361
+ fs.writeFileSync(targetFile, html, 'utf8');
3362
+ if (shouldAutoOpen()) openFile(targetFile);
3363
+ process.stdout.write(`Generated: ${targetFile}\n`);
3364
+ return;
3365
+ }
3366
+
3367
+ // --share mode (portfolio with --user flag)
3368
+ if (mode.mode === 'share') {
3369
+ const data = runScanner(mode);
3370
+ applyPlatformFilter(data);
3371
+ const user = getArgValue('--user');
3372
+ if (format === 'json') {
3373
+ process.stdout.write(JSON.stringify({ skills: data.skills, totalCount: data.totalCount }, null, 2) + '\n');
3374
+ return;
3375
+ }
3376
+ const outputFile = getArgValue('--output');
3377
+ const html = renderShareHTML(data, user);
3378
+ const defaultFile = path.join(os.tmpdir(), 'skill-guide-share.html');
3379
+ const targetFile = outputFile ? path.resolve(outputFile) : defaultFile;
3380
+ fs.mkdirSync(path.dirname(targetFile), { recursive: true });
3381
+ fs.writeFileSync(targetFile, html, 'utf8');
3382
+ if (shouldAutoOpen()) openFile(targetFile);
3383
+ process.stdout.write(`Generated: ${targetFile}\n`);
3384
+ return;
3385
+ }
3386
+
3387
+ // Default mode (list): overview dashboard
3388
+ const data = runScanner(mode);
3389
+ applyPlatformFilter(data);
3390
+
3391
+ if (format === 'json') {
3392
+ const serialized = JSON.stringify(data, null, 2);
3393
+ const jsonOutput = getArgValue('--output');
3394
+ if (jsonOutput) {
3395
+ const outputPath = path.resolve(jsonOutput);
3396
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
3397
+ fs.writeFileSync(outputPath, `${serialized}\n`, 'utf8');
3398
+ process.stdout.write(`Generated ${outputPath}\n`);
3399
+ } else {
3400
+ process.stdout.write(`${serialized}\n`);
3401
+ }
3402
+ return;
3403
+ }
3404
+
3405
+ if (data.error) {
3406
+ process.stderr.write(`${formatScannerError(data)}\n`);
3407
+ process.exit(1);
3408
+ }
3409
+
3410
+ if (data._platform && data._platform !== 'all') {
3411
+ process.stdout.write(` Showing: ${platformLabel()} (use --all to see all)\n\n`);
794
3412
  }
3413
+ process.stdout.write(renderDefaultTerminal(data.skills || []));
795
3414
 
796
3415
  const output = path.resolve(getArgValue('--output') || defaultOutputPath(mode));
797
3416
  fs.mkdirSync(path.dirname(output), { recursive: true });
798
3417
  fs.writeFileSync(output, renderHtml(data, mode), 'utf8');
799
3418
 
800
- if (hasFlag('--open') && !hasFlag('--no-open')) openFile(output);
3419
+ if (shouldAutoOpen()) openFile(output);
801
3420
  process.stdout.write(`Generated ${output}\n`);
802
3421
  }
803
3422