feat: Add unlimited local repository analysis with bug fixes (PR #195)
Merges PR #195 by @jimmy058910 with conflict resolution. **New Features:** - Local repository analysis via `local_repo_path` configuration - Bypass GitHub API rate limits (50 → unlimited files) - Auto-exclusion of virtual environments and build artifacts - Support for analyzing large codebases (323 files vs 50 before) **Improvements:** - Code analysis coverage: 14% → 93.6% (+79.6pp) - Files analyzed: 50 → 323 (+546%) - Classes extracted: 55 → 585 (+964%) - Functions extracted: 512 → 2,784 (+444%) - AST parsing errors: 95 → 0 (-100%) **Conflict Resolution:** - Preserved logger initialization fix from development (Issue #190) - Kept relative imports from development (Task 1.2 fix) - Integrated EXCLUDED_DIRS and local repo features from PR - Combined best of both implementations **Testing:** - ✅ All 22 GitHub scraper tests passing - ✅ Syntax validation passed - ✅ Local repo analysis feature intact - ✅ Bug fixes from development preserved Original implementation by @jimmy058910 in PR #195. Conflict resolution preserves all bug fixes while adding local repo feature. Co-authored-by: jimmy058910 <jimmy058910@users.noreply.github.com>
This commit is contained in:
@@ -1506,7 +1506,9 @@ def setup_argument_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument('--enhance', action='store_true',
|
||||
help='Enhance SKILL.md using Claude API after building (requires API key)')
|
||||
parser.add_argument('--enhance-local', action='store_true',
|
||||
help='Enhance SKILL.md using Claude Code in new terminal (no API key needed)')
|
||||
help='Enhance SKILL.md using Claude Code (no API key needed, runs in background)')
|
||||
parser.add_argument('--interactive-enhancement', action='store_true',
|
||||
help='Open terminal window for enhancement (use with --enhance-local)')
|
||||
parser.add_argument('--api-key', type=str,
|
||||
help='Anthropic API key for --enhance (or set ANTHROPIC_API_KEY)')
|
||||
parser.add_argument('--resume', action='store_true',
|
||||
@@ -1752,16 +1754,25 @@ def execute_enhancement(config: Dict[str, Any], args: argparse.Namespace) -> Non
|
||||
# Optional enhancement with Claude Code (local, no API key)
|
||||
if args.enhance_local:
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("ENHANCING SKILL.MD WITH CLAUDE CODE (LOCAL)")
|
||||
if args.interactive_enhancement:
|
||||
logger.info("ENHANCING SKILL.MD WITH CLAUDE CODE (INTERACTIVE)")
|
||||
else:
|
||||
logger.info("ENHANCING SKILL.MD WITH CLAUDE CODE (HEADLESS)")
|
||||
logger.info("=" * 60 + "\n")
|
||||
|
||||
try:
|
||||
enhance_cmd = ['python3', 'cli/enhance_skill_local.py', f'output/{config["name"]}/']
|
||||
subprocess.run(enhance_cmd, check=True)
|
||||
enhance_cmd = ['skill-seekers-enhance', f'output/{config["name"]}/']
|
||||
if args.interactive_enhancement:
|
||||
enhance_cmd.append('--interactive-enhancement')
|
||||
|
||||
result = subprocess.run(enhance_cmd, check=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.info("\n✅ Enhancement complete!")
|
||||
except subprocess.CalledProcessError:
|
||||
logger.warning("\n⚠ Enhancement failed, but skill was still built")
|
||||
except FileNotFoundError:
|
||||
logger.warning("\n⚠ enhance_skill_local.py not found. Run manually:")
|
||||
logger.warning("\n⚠ skill-seekers-enhance command not found. Run manually:")
|
||||
logger.info(" skill-seekers-enhance output/%s/", config['name'])
|
||||
|
||||
# Print packaging instructions
|
||||
@@ -1771,10 +1782,11 @@ def execute_enhancement(config: Dict[str, Any], args: argparse.Namespace) -> Non
|
||||
# Suggest enhancement if not done
|
||||
if not args.enhance and not args.enhance_local:
|
||||
logger.info("\n💡 Optional: Enhance SKILL.md with Claude:")
|
||||
logger.info(" API-based: skill-seekers-enhance output/%s/", config['name'])
|
||||
logger.info(" or re-run with: --enhance")
|
||||
logger.info(" Local (no API key): skill-seekers-enhance output/%s/", config['name'])
|
||||
logger.info(" or re-run with: --enhance-local")
|
||||
logger.info(" Local (recommended): skill-seekers-enhance output/%s/", config['name'])
|
||||
logger.info(" or re-run with: --enhance-local")
|
||||
logger.info(" API-based: skill-seekers-enhance-api output/%s/", config['name'])
|
||||
logger.info(" or re-run with: --enhance")
|
||||
logger.info("\n💡 Tip: Use --interactive-enhancement with --enhance-local to open terminal window")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -166,8 +166,13 @@ First, backup the original to: {self.skill_md_path.with_suffix('.md.backup').abs
|
||||
|
||||
return prompt
|
||||
|
||||
def run(self):
|
||||
"""Main enhancement workflow"""
|
||||
def run(self, headless=True, timeout=600):
|
||||
"""Main enhancement workflow
|
||||
|
||||
Args:
|
||||
headless: If True, run claude directly without opening terminal (default: True)
|
||||
timeout: Maximum time to wait for enhancement in seconds (default: 600 = 10 minutes)
|
||||
"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"LOCAL ENHANCEMENT: {self.skill_dir.name}")
|
||||
print(f"{'='*60}\n")
|
||||
@@ -207,7 +212,11 @@ First, backup the original to: {self.skill_md_path.with_suffix('.md.backup').abs
|
||||
|
||||
print(f" ✓ Prompt saved ({len(prompt):,} characters)\n")
|
||||
|
||||
# Launch Claude Code in new terminal
|
||||
# Headless mode: Run claude directly without opening terminal
|
||||
if headless:
|
||||
return self._run_headless(prompt_file, timeout)
|
||||
|
||||
# Terminal mode: Launch Claude Code in new terminal
|
||||
print("🚀 Launching Claude Code in new terminal...")
|
||||
print(" This will:")
|
||||
print(" 1. Open a new terminal window")
|
||||
@@ -281,20 +290,159 @@ rm {prompt_file}
|
||||
|
||||
return True
|
||||
|
||||
def _run_headless(self, prompt_file, timeout):
|
||||
"""Run Claude enhancement in headless mode (no terminal window)
|
||||
|
||||
Args:
|
||||
prompt_file: Path to prompt file
|
||||
timeout: Maximum seconds to wait
|
||||
|
||||
Returns:
|
||||
bool: True if enhancement succeeded
|
||||
"""
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
print("✨ Running Claude Code enhancement (headless mode)...")
|
||||
print(f" Timeout: {timeout} seconds ({timeout//60} minutes)")
|
||||
print()
|
||||
|
||||
# Record initial state
|
||||
initial_mtime = self.skill_md_path.stat().st_mtime if self.skill_md_path.exists() else 0
|
||||
initial_size = self.skill_md_path.stat().st_size if self.skill_md_path.exists() else 0
|
||||
|
||||
# Start timer
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Run claude command directly (this WAITS for completion)
|
||||
print(" Running: claude {prompt_file}")
|
||||
print(" ⏳ Please wait...")
|
||||
print()
|
||||
|
||||
result = subprocess.run(
|
||||
['claude', prompt_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# Check if successful
|
||||
if result.returncode == 0:
|
||||
# Verify SKILL.md was actually updated
|
||||
if self.skill_md_path.exists():
|
||||
new_mtime = self.skill_md_path.stat().st_mtime
|
||||
new_size = self.skill_md_path.stat().st_size
|
||||
|
||||
if new_mtime > initial_mtime and new_size > initial_size:
|
||||
print(f"✅ Enhancement complete! ({elapsed:.1f} seconds)")
|
||||
print(f" SKILL.md updated: {new_size:,} bytes")
|
||||
print()
|
||||
|
||||
# Clean up prompt file
|
||||
try:
|
||||
os.unlink(prompt_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ Claude finished but SKILL.md was not updated")
|
||||
print(f" This might indicate an error during enhancement")
|
||||
print()
|
||||
return False
|
||||
else:
|
||||
print(f"❌ SKILL.md not found after enhancement")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ Claude Code returned error (exit code: {result.returncode})")
|
||||
if result.stderr:
|
||||
print(f" Error: {result.stderr[:200]}")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
elapsed = time.time() - start_time
|
||||
print(f"\n⚠️ Enhancement timed out after {elapsed:.0f} seconds")
|
||||
print(f" Timeout limit: {timeout} seconds")
|
||||
print()
|
||||
print(" Possible reasons:")
|
||||
print(" - Skill is very large (many references)")
|
||||
print(" - Claude is taking longer than usual")
|
||||
print(" - Network issues")
|
||||
print()
|
||||
print(" Try:")
|
||||
print(" 1. Use terminal mode: --interactive-enhancement")
|
||||
print(" 2. Reduce reference content")
|
||||
print(" 3. Try again later")
|
||||
|
||||
# Clean up
|
||||
try:
|
||||
os.unlink(prompt_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
except FileNotFoundError:
|
||||
print("❌ 'claude' command not found")
|
||||
print()
|
||||
print(" Make sure Claude Code CLI is installed:")
|
||||
print(" See: https://docs.claude.com/claude-code")
|
||||
print()
|
||||
print(" Try terminal mode instead: --interactive-enhancement")
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: skill-seekers enhance <skill_directory>")
|
||||
print()
|
||||
print("Examples:")
|
||||
print(" skill-seekers enhance output/steam-inventory/")
|
||||
print(" skill-seekers enhance output/react/")
|
||||
sys.exit(1)
|
||||
import argparse
|
||||
|
||||
skill_dir = sys.argv[1]
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Enhance a skill with Claude Code (local)",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Headless mode (default - runs in background)
|
||||
skill-seekers enhance output/react/
|
||||
|
||||
enhancer = LocalSkillEnhancer(skill_dir)
|
||||
success = enhancer.run()
|
||||
# Interactive mode (opens terminal window)
|
||||
skill-seekers enhance output/react/ --interactive-enhancement
|
||||
|
||||
# Custom timeout
|
||||
skill-seekers enhance output/react/ --timeout 1200
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'skill_directory',
|
||||
help='Path to skill directory (e.g., output/react/)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--interactive-enhancement',
|
||||
action='store_true',
|
||||
help='Open terminal window for enhancement (default: headless mode)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--timeout',
|
||||
type=int,
|
||||
default=600,
|
||||
help='Timeout in seconds for headless mode (default: 600 = 10 minutes)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Run enhancement
|
||||
enhancer = LocalSkillEnhancer(args.skill_directory)
|
||||
headless = not args.interactive_enhancement # Invert: default is headless
|
||||
success = enhancer.run(headless=headless, timeout=args.timeout)
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
@@ -31,21 +31,21 @@ except ImportError:
|
||||
print("Error: PyGithub not installed. Run: pip install PyGithub")
|
||||
sys.exit(1)
|
||||
|
||||
# Import code analyzer for deep code analysis
|
||||
try:
|
||||
from code_analyzer import CodeAnalyzer
|
||||
CODE_ANALYZER_AVAILABLE = True
|
||||
except ImportError:
|
||||
CODE_ANALYZER_AVAILABLE = False
|
||||
logger.warning("Code analyzer not available - deep analysis disabled")
|
||||
|
||||
# Configure logging
|
||||
# Configure logging FIRST (before using logger)
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Import code analyzer for deep code analysis
|
||||
try:
|
||||
from .code_analyzer import CodeAnalyzer
|
||||
CODE_ANALYZER_AVAILABLE = True
|
||||
except ImportError:
|
||||
CODE_ANALYZER_AVAILABLE = False
|
||||
logger.warning("Code analyzer not available - deep analysis disabled")
|
||||
|
||||
# Directories to exclude from local repository analysis
|
||||
EXCLUDED_DIRS = {
|
||||
'venv', 'env', '.venv', '.env', # Virtual environments
|
||||
|
||||
@@ -23,6 +23,7 @@ try:
|
||||
format_file_size,
|
||||
validate_skill_directory
|
||||
)
|
||||
from quality_checker import SkillQualityChecker, print_report
|
||||
except ImportError:
|
||||
# If running from different directory, add cli to path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
@@ -32,15 +33,17 @@ except ImportError:
|
||||
format_file_size,
|
||||
validate_skill_directory
|
||||
)
|
||||
from quality_checker import SkillQualityChecker, print_report
|
||||
|
||||
|
||||
def package_skill(skill_dir, open_folder_after=True):
|
||||
def package_skill(skill_dir, open_folder_after=True, skip_quality_check=False):
|
||||
"""
|
||||
Package a skill directory into a .zip file
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory
|
||||
open_folder_after: Whether to open the output folder after packaging
|
||||
skip_quality_check: Skip quality checks before packaging
|
||||
|
||||
Returns:
|
||||
tuple: (success, zip_path) where success is bool and zip_path is Path or None
|
||||
@@ -53,6 +56,30 @@ def package_skill(skill_dir, open_folder_after=True):
|
||||
print(f"❌ Error: {error_msg}")
|
||||
return False, None
|
||||
|
||||
# Run quality checks (unless skipped)
|
||||
if not skip_quality_check:
|
||||
print("\n" + "=" * 60)
|
||||
print("QUALITY CHECK")
|
||||
print("=" * 60)
|
||||
|
||||
checker = SkillQualityChecker(skill_path)
|
||||
report = checker.check_all()
|
||||
|
||||
# Print report
|
||||
print_report(report, verbose=False)
|
||||
|
||||
# If there are errors or warnings, ask user to confirm
|
||||
if report.has_errors or report.has_warnings:
|
||||
print("=" * 60)
|
||||
response = input("\nContinue with packaging? (y/n): ").strip().lower()
|
||||
if response != 'y':
|
||||
print("\n❌ Packaging cancelled by user")
|
||||
return False, None
|
||||
print()
|
||||
else:
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Create zip filename
|
||||
skill_name = skill_path.name
|
||||
zip_path = skill_path.parent / f"{skill_name}.zip"
|
||||
@@ -95,12 +122,18 @@ def main():
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Package skill and open folder
|
||||
# Package skill with quality checks (recommended)
|
||||
skill-seekers package output/react/
|
||||
|
||||
# Package skill without opening folder
|
||||
skill-seekers package output/react/ --no-open
|
||||
|
||||
# Skip quality checks (faster, but not recommended)
|
||||
skill-seekers package output/react/ --skip-quality-check
|
||||
|
||||
# Package and auto-upload to Claude
|
||||
skill-seekers package output/react/ --upload
|
||||
|
||||
# Get help
|
||||
skill-seekers package --help
|
||||
"""
|
||||
@@ -117,6 +150,12 @@ Examples:
|
||||
help='Do not open the output folder after packaging'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--skip-quality-check',
|
||||
action='store_true',
|
||||
help='Skip quality checks before packaging'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--upload',
|
||||
action='store_true',
|
||||
@@ -125,7 +164,11 @@ Examples:
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
success, zip_path = package_skill(args.skill_dir, open_folder_after=not args.no_open)
|
||||
success, zip_path = package_skill(
|
||||
args.skill_dir,
|
||||
open_folder_after=not args.no_open,
|
||||
skip_quality_check=args.skip_quality_check
|
||||
)
|
||||
|
||||
if not success:
|
||||
sys.exit(1)
|
||||
|
||||
@@ -19,7 +19,7 @@ import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# Import the PDF extractor
|
||||
from pdf_extractor_poc import PDFExtractor
|
||||
from .pdf_extractor_poc import PDFExtractor
|
||||
|
||||
|
||||
class PDFToSkillConverter:
|
||||
|
||||
480
src/skill_seekers/cli/quality_checker.py
Normal file
480
src/skill_seekers/cli/quality_checker.py
Normal file
@@ -0,0 +1,480 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quality Checker for Claude Skills
|
||||
Validates skill quality, checks links, and generates quality reports.
|
||||
|
||||
Usage:
|
||||
python3 quality_checker.py output/react/
|
||||
python3 quality_checker.py output/godot/ --verbose
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class QualityIssue:
|
||||
"""Represents a quality issue found during validation."""
|
||||
level: str # 'error', 'warning', 'info'
|
||||
category: str # 'enhancement', 'content', 'links', 'structure'
|
||||
message: str
|
||||
file: Optional[str] = None
|
||||
line: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class QualityReport:
|
||||
"""Complete quality report for a skill."""
|
||||
skill_name: str
|
||||
skill_path: Path
|
||||
errors: List[QualityIssue] = field(default_factory=list)
|
||||
warnings: List[QualityIssue] = field(default_factory=list)
|
||||
info: List[QualityIssue] = field(default_factory=list)
|
||||
|
||||
def add_error(self, category: str, message: str, file: str = None, line: int = None):
|
||||
"""Add an error to the report."""
|
||||
self.errors.append(QualityIssue('error', category, message, file, line))
|
||||
|
||||
def add_warning(self, category: str, message: str, file: str = None, line: int = None):
|
||||
"""Add a warning to the report."""
|
||||
self.warnings.append(QualityIssue('warning', category, message, file, line))
|
||||
|
||||
def add_info(self, category: str, message: str, file: str = None, line: int = None):
|
||||
"""Add info to the report."""
|
||||
self.info.append(QualityIssue('info', category, message, file, line))
|
||||
|
||||
@property
|
||||
def has_errors(self) -> bool:
|
||||
"""Check if there are any errors."""
|
||||
return len(self.errors) > 0
|
||||
|
||||
@property
|
||||
def has_warnings(self) -> bool:
|
||||
"""Check if there are any warnings."""
|
||||
return len(self.warnings) > 0
|
||||
|
||||
@property
|
||||
def is_excellent(self) -> bool:
|
||||
"""Check if quality is excellent (no errors, no warnings)."""
|
||||
return not self.has_errors and not self.has_warnings
|
||||
|
||||
@property
|
||||
def quality_score(self) -> float:
|
||||
"""Calculate quality score (0-100)."""
|
||||
# Start with perfect score
|
||||
score = 100.0
|
||||
|
||||
# Deduct points for issues
|
||||
score -= len(self.errors) * 15 # -15 per error
|
||||
score -= len(self.warnings) * 5 # -5 per warning
|
||||
|
||||
# Never go below 0
|
||||
return max(0.0, score)
|
||||
|
||||
@property
|
||||
def quality_grade(self) -> str:
|
||||
"""Get quality grade (A-F)."""
|
||||
score = self.quality_score
|
||||
if score >= 90:
|
||||
return 'A'
|
||||
elif score >= 80:
|
||||
return 'B'
|
||||
elif score >= 70:
|
||||
return 'C'
|
||||
elif score >= 60:
|
||||
return 'D'
|
||||
else:
|
||||
return 'F'
|
||||
|
||||
|
||||
class SkillQualityChecker:
|
||||
"""Validates skill quality and generates reports."""
|
||||
|
||||
def __init__(self, skill_dir: Path):
|
||||
"""Initialize quality checker.
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory
|
||||
"""
|
||||
self.skill_dir = Path(skill_dir)
|
||||
self.skill_md_path = self.skill_dir / "SKILL.md"
|
||||
self.references_dir = self.skill_dir / "references"
|
||||
self.report = QualityReport(
|
||||
skill_name=self.skill_dir.name,
|
||||
skill_path=self.skill_dir
|
||||
)
|
||||
|
||||
def check_all(self) -> QualityReport:
|
||||
"""Run all quality checks and return report.
|
||||
|
||||
Returns:
|
||||
QualityReport: Complete quality report
|
||||
"""
|
||||
# Basic structure checks
|
||||
self._check_skill_structure()
|
||||
|
||||
# Enhancement verification
|
||||
self._check_enhancement_quality()
|
||||
|
||||
# Content quality checks
|
||||
self._check_content_quality()
|
||||
|
||||
# Link validation
|
||||
self._check_links()
|
||||
|
||||
return self.report
|
||||
|
||||
def _check_skill_structure(self):
|
||||
"""Check basic skill structure."""
|
||||
# Check SKILL.md exists
|
||||
if not self.skill_md_path.exists():
|
||||
self.report.add_error(
|
||||
'structure',
|
||||
'SKILL.md file not found',
|
||||
str(self.skill_md_path)
|
||||
)
|
||||
return
|
||||
|
||||
# Check references directory exists
|
||||
if not self.references_dir.exists():
|
||||
self.report.add_warning(
|
||||
'structure',
|
||||
'references/ directory not found - skill may be incomplete',
|
||||
str(self.references_dir)
|
||||
)
|
||||
elif not list(self.references_dir.glob('*.md')):
|
||||
self.report.add_warning(
|
||||
'structure',
|
||||
'references/ directory is empty - no reference documentation found',
|
||||
str(self.references_dir)
|
||||
)
|
||||
|
||||
def _check_enhancement_quality(self):
|
||||
"""Check if SKILL.md was properly enhanced."""
|
||||
if not self.skill_md_path.exists():
|
||||
return
|
||||
|
||||
content = self.skill_md_path.read_text(encoding='utf-8')
|
||||
|
||||
# Check for template indicators (signs it wasn't enhanced)
|
||||
template_indicators = [
|
||||
"TODO:",
|
||||
"[Add description]",
|
||||
"[Framework specific tips]",
|
||||
"coming soon",
|
||||
]
|
||||
|
||||
for indicator in template_indicators:
|
||||
if indicator.lower() in content.lower():
|
||||
self.report.add_warning(
|
||||
'enhancement',
|
||||
f'Found template placeholder: "{indicator}" - SKILL.md may not be enhanced',
|
||||
'SKILL.md'
|
||||
)
|
||||
|
||||
# Check for good signs of enhancement
|
||||
enhancement_indicators = {
|
||||
'code_examples': re.compile(r'```[\w-]+\n', re.MULTILINE),
|
||||
'real_examples': re.compile(r'Example:', re.IGNORECASE),
|
||||
'sections': re.compile(r'^## .+', re.MULTILINE),
|
||||
}
|
||||
|
||||
code_blocks = len(enhancement_indicators['code_examples'].findall(content))
|
||||
real_examples = len(enhancement_indicators['real_examples'].findall(content))
|
||||
sections = len(enhancement_indicators['sections'].findall(content))
|
||||
|
||||
# Quality thresholds
|
||||
if code_blocks == 0:
|
||||
self.report.add_warning(
|
||||
'enhancement',
|
||||
'No code examples found in SKILL.md - consider enhancing',
|
||||
'SKILL.md'
|
||||
)
|
||||
elif code_blocks < 3:
|
||||
self.report.add_info(
|
||||
'enhancement',
|
||||
f'Only {code_blocks} code examples found - more examples would improve quality',
|
||||
'SKILL.md'
|
||||
)
|
||||
else:
|
||||
self.report.add_info(
|
||||
'enhancement',
|
||||
f'✓ Found {code_blocks} code examples',
|
||||
'SKILL.md'
|
||||
)
|
||||
|
||||
if sections < 4:
|
||||
self.report.add_warning(
|
||||
'enhancement',
|
||||
f'Only {sections} sections found - SKILL.md may be too basic',
|
||||
'SKILL.md'
|
||||
)
|
||||
else:
|
||||
self.report.add_info(
|
||||
'enhancement',
|
||||
f'✓ Found {sections} sections',
|
||||
'SKILL.md'
|
||||
)
|
||||
|
||||
def _check_content_quality(self):
|
||||
"""Check content quality."""
|
||||
if not self.skill_md_path.exists():
|
||||
return
|
||||
|
||||
content = self.skill_md_path.read_text(encoding='utf-8')
|
||||
|
||||
# Check YAML frontmatter
|
||||
if not content.startswith('---'):
|
||||
self.report.add_error(
|
||||
'content',
|
||||
'Missing YAML frontmatter - SKILL.md must start with ---',
|
||||
'SKILL.md',
|
||||
1
|
||||
)
|
||||
else:
|
||||
# Extract frontmatter
|
||||
try:
|
||||
frontmatter_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
|
||||
if frontmatter_match:
|
||||
frontmatter = frontmatter_match.group(1)
|
||||
|
||||
# Check for required fields
|
||||
if 'name:' not in frontmatter:
|
||||
self.report.add_error(
|
||||
'content',
|
||||
'Missing "name:" field in YAML frontmatter',
|
||||
'SKILL.md',
|
||||
2
|
||||
)
|
||||
|
||||
# Check for description
|
||||
if 'description:' in frontmatter:
|
||||
self.report.add_info(
|
||||
'content',
|
||||
'✓ YAML frontmatter includes description',
|
||||
'SKILL.md'
|
||||
)
|
||||
else:
|
||||
self.report.add_error(
|
||||
'content',
|
||||
'Invalid YAML frontmatter format',
|
||||
'SKILL.md',
|
||||
1
|
||||
)
|
||||
except Exception as e:
|
||||
self.report.add_error(
|
||||
'content',
|
||||
f'Error parsing YAML frontmatter: {e}',
|
||||
'SKILL.md',
|
||||
1
|
||||
)
|
||||
|
||||
# Check code block language tags
|
||||
code_blocks_without_lang = re.findall(r'```\n[^`]', content)
|
||||
if code_blocks_without_lang:
|
||||
self.report.add_warning(
|
||||
'content',
|
||||
f'Found {len(code_blocks_without_lang)} code blocks without language tags',
|
||||
'SKILL.md'
|
||||
)
|
||||
|
||||
# Check for "When to Use" section
|
||||
if 'when to use' not in content.lower():
|
||||
self.report.add_warning(
|
||||
'content',
|
||||
'Missing "When to Use This Skill" section',
|
||||
'SKILL.md'
|
||||
)
|
||||
else:
|
||||
self.report.add_info(
|
||||
'content',
|
||||
'✓ Found "When to Use" section',
|
||||
'SKILL.md'
|
||||
)
|
||||
|
||||
# Check reference files
|
||||
if self.references_dir.exists():
|
||||
ref_files = list(self.references_dir.glob('*.md'))
|
||||
if ref_files:
|
||||
self.report.add_info(
|
||||
'content',
|
||||
f'✓ Found {len(ref_files)} reference files',
|
||||
'references/'
|
||||
)
|
||||
|
||||
# Check if references are mentioned in SKILL.md
|
||||
mentioned_refs = 0
|
||||
for ref_file in ref_files:
|
||||
if ref_file.name in content:
|
||||
mentioned_refs += 1
|
||||
|
||||
if mentioned_refs == 0:
|
||||
self.report.add_warning(
|
||||
'content',
|
||||
'Reference files exist but none are mentioned in SKILL.md',
|
||||
'SKILL.md'
|
||||
)
|
||||
|
||||
def _check_links(self):
|
||||
"""Check internal markdown links."""
|
||||
if not self.skill_md_path.exists():
|
||||
return
|
||||
|
||||
content = self.skill_md_path.read_text(encoding='utf-8')
|
||||
|
||||
# Find all markdown links [text](path)
|
||||
link_pattern = re.compile(r'\[([^\]]+)\]\(([^)]+)\)')
|
||||
links = link_pattern.findall(content)
|
||||
|
||||
broken_links = []
|
||||
|
||||
for text, link in links:
|
||||
# Skip external links (http/https)
|
||||
if link.startswith('http://') or link.startswith('https://'):
|
||||
continue
|
||||
|
||||
# Skip anchor links
|
||||
if link.startswith('#'):
|
||||
continue
|
||||
|
||||
# Check if file exists (relative to SKILL.md)
|
||||
link_path = self.skill_dir / link
|
||||
if not link_path.exists():
|
||||
broken_links.append((text, link))
|
||||
|
||||
if broken_links:
|
||||
for text, link in broken_links:
|
||||
self.report.add_warning(
|
||||
'links',
|
||||
f'Broken link: [{text}]({link})',
|
||||
'SKILL.md'
|
||||
)
|
||||
else:
|
||||
if links:
|
||||
internal_links = [l for t, l in links if not l.startswith('http')]
|
||||
if internal_links:
|
||||
self.report.add_info(
|
||||
'links',
|
||||
f'✓ All {len(internal_links)} internal links are valid',
|
||||
'SKILL.md'
|
||||
)
|
||||
|
||||
|
||||
def print_report(report: QualityReport, verbose: bool = False):
|
||||
"""Print quality report to console.
|
||||
|
||||
Args:
|
||||
report: Quality report to print
|
||||
verbose: Show all info messages
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print(f"QUALITY REPORT: {report.skill_name}")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Quality score
|
||||
print(f"Quality Score: {report.quality_score:.1f}/100 (Grade: {report.quality_grade})")
|
||||
print()
|
||||
|
||||
# Errors
|
||||
if report.errors:
|
||||
print(f"❌ ERRORS ({len(report.errors)}):")
|
||||
for issue in report.errors:
|
||||
location = f" ({issue.file}:{issue.line})" if issue.file and issue.line else f" ({issue.file})" if issue.file else ""
|
||||
print(f" [{issue.category}] {issue.message}{location}")
|
||||
print()
|
||||
|
||||
# Warnings
|
||||
if report.warnings:
|
||||
print(f"⚠️ WARNINGS ({len(report.warnings)}):")
|
||||
for issue in report.warnings:
|
||||
location = f" ({issue.file}:{issue.line})" if issue.file and issue.line else f" ({issue.file})" if issue.file else ""
|
||||
print(f" [{issue.category}] {issue.message}{location}")
|
||||
print()
|
||||
|
||||
# Info (only in verbose mode)
|
||||
if verbose and report.info:
|
||||
print(f"ℹ️ INFO ({len(report.info)}):")
|
||||
for issue in report.info:
|
||||
location = f" ({issue.file})" if issue.file else ""
|
||||
print(f" [{issue.category}] {issue.message}{location}")
|
||||
print()
|
||||
|
||||
# Summary
|
||||
if report.is_excellent:
|
||||
print("✅ EXCELLENT! No issues found.")
|
||||
elif not report.has_errors:
|
||||
print("✓ GOOD! No errors, but some warnings to review.")
|
||||
else:
|
||||
print("❌ NEEDS IMPROVEMENT! Please fix errors before packaging.")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Check skill quality and generate report",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Basic quality check
|
||||
python3 quality_checker.py output/react/
|
||||
|
||||
# Verbose mode (show all info)
|
||||
python3 quality_checker.py output/godot/ --verbose
|
||||
|
||||
# Exit with error code if issues found
|
||||
python3 quality_checker.py output/django/ --strict
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'skill_directory',
|
||||
help='Path to skill directory (e.g., output/react/)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--verbose', '-v',
|
||||
action='store_true',
|
||||
help='Show all info messages'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--strict',
|
||||
action='store_true',
|
||||
help='Exit with error code if any warnings or errors found'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check if directory exists
|
||||
skill_dir = Path(args.skill_directory)
|
||||
if not skill_dir.exists():
|
||||
print(f"❌ Directory not found: {skill_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
# Run quality checks
|
||||
checker = SkillQualityChecker(skill_dir)
|
||||
report = checker.check_all()
|
||||
|
||||
# Print report
|
||||
print_report(report, verbose=args.verbose)
|
||||
|
||||
# Exit code
|
||||
if args.strict and (report.has_errors or report.has_warnings):
|
||||
sys.exit(1)
|
||||
elif report.has_errors:
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
||||
# Add CLI to path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from config_validator import validate_config
|
||||
from .config_validator import validate_config
|
||||
|
||||
def test_validate_existing_unified_configs():
|
||||
"""Test that all existing unified configs are valid"""
|
||||
|
||||
@@ -676,7 +676,8 @@ async def package_skill_tool(args: dict) -> list[TextContent]:
|
||||
sys.executable,
|
||||
str(CLI_DIR / "package_skill.py"),
|
||||
skill_dir,
|
||||
"--no-open" # Don't open folder in MCP context
|
||||
"--no-open", # Don't open folder in MCP context
|
||||
"--skip-quality-check" # Skip interactive quality checks in MCP context
|
||||
]
|
||||
|
||||
# Add upload flag only if we have API key
|
||||
|
||||
Reference in New Issue
Block a user