feat: C3.9 documentation extraction, AI enhancement optimization, and C# support
Complete implementation of C3.9, granular AI enhancement control, performance optimizations, and bug fixes. Features: - C3.9 Project Documentation Extraction (markdown files) - Granular AI enhancement control (--enhance-level 0-3) - C# test extraction support - 6-12x faster LOCAL mode with parallel execution - Auto-enhancement UX improvements - LOCAL mode fallback for all AI enhancements Bug Fixes: - C# language support - Config type field compatibility - LocalSkillEnhancer import Documentation: - Updated CHANGELOG.md - Updated CLAUDE.md - Removed client-specific files Tests: All 1,257 tests passing Critical linter errors: Fixed
This commit is contained in:
committed by
GitHub
parent
5a78522dbc
commit
aa57164d34
@@ -12,17 +12,34 @@ Features:
|
||||
- Groups related examples into tutorials
|
||||
- Identifies best practices
|
||||
|
||||
Modes:
|
||||
- API mode: Uses Claude API (requires ANTHROPIC_API_KEY)
|
||||
- LOCAL mode: Uses Claude Code CLI (no API key needed, uses your Claude Max plan)
|
||||
- AUTO mode: Tries API first, falls back to LOCAL
|
||||
|
||||
Credits:
|
||||
- Uses Claude AI (Anthropic) for analysis
|
||||
- Graceful degradation if API unavailable
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Import config manager for settings
|
||||
try:
|
||||
from skill_seekers.cli.config_manager import get_config_manager
|
||||
CONFIG_AVAILABLE = True
|
||||
except ImportError:
|
||||
CONFIG_AVAILABLE = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class AIAnalysis:
|
||||
@@ -47,29 +64,32 @@ class AIEnhancer:
|
||||
api_key: Anthropic API key (uses ANTHROPIC_API_KEY env if None)
|
||||
enabled: Enable AI enhancement (default: True)
|
||||
mode: Enhancement mode - "auto" (default), "api", or "local"
|
||||
- "auto": Use API if key available, otherwise disable
|
||||
- "auto": Use API if key available, otherwise fall back to LOCAL
|
||||
- "api": Force API mode (fails if no key)
|
||||
- "local": Use Claude Code local mode (opens terminal)
|
||||
- "local": Use Claude Code CLI (no API key needed)
|
||||
"""
|
||||
self.enabled = enabled
|
||||
self.mode = mode
|
||||
self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
|
||||
self.client = None
|
||||
|
||||
# Get settings from config (with defaults)
|
||||
if CONFIG_AVAILABLE:
|
||||
config = get_config_manager()
|
||||
self.local_batch_size = config.get_local_batch_size()
|
||||
self.local_parallel_workers = config.get_local_parallel_workers()
|
||||
else:
|
||||
self.local_batch_size = 20 # Default
|
||||
self.local_parallel_workers = 3 # Default
|
||||
|
||||
# Determine actual mode
|
||||
if mode == "auto":
|
||||
if self.api_key:
|
||||
self.mode = "api"
|
||||
else:
|
||||
# For now, disable if no API key
|
||||
# LOCAL mode for batch processing is complex
|
||||
self.mode = "disabled"
|
||||
self.enabled = False
|
||||
logger.info("ℹ️ AI enhancement disabled (no API key found)")
|
||||
logger.info(
|
||||
" Set ANTHROPIC_API_KEY to enable, or use 'skill-seekers enhance' for SKILL.md"
|
||||
)
|
||||
return
|
||||
# Fall back to LOCAL mode (Claude Code CLI)
|
||||
self.mode = "local"
|
||||
logger.info("ℹ️ No API key found, using LOCAL mode (Claude Code CLI)")
|
||||
|
||||
if self.mode == "api" and self.enabled:
|
||||
try:
|
||||
@@ -84,23 +104,44 @@ class AIEnhancer:
|
||||
self.client = anthropic.Anthropic(**client_kwargs)
|
||||
logger.info("✅ AI enhancement enabled (using Claude API)")
|
||||
except ImportError:
|
||||
logger.warning("⚠️ anthropic package not installed. AI enhancement disabled.")
|
||||
logger.warning(" Install with: pip install anthropic")
|
||||
self.enabled = False
|
||||
logger.warning("⚠️ anthropic package not installed, falling back to LOCAL mode")
|
||||
self.mode = "local"
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Failed to initialize AI client: {e}")
|
||||
logger.warning(f"⚠️ Failed to initialize API client: {e}, falling back to LOCAL mode")
|
||||
self.mode = "local"
|
||||
|
||||
if self.mode == "local" and self.enabled:
|
||||
# Verify Claude CLI is available
|
||||
if self._check_claude_cli():
|
||||
logger.info("✅ AI enhancement enabled (using LOCAL mode - Claude Code CLI)")
|
||||
else:
|
||||
logger.warning("⚠️ Claude Code CLI not found. AI enhancement disabled.")
|
||||
logger.warning(" Install with: npm install -g @anthropic-ai/claude-code")
|
||||
self.enabled = False
|
||||
elif self.mode == "local":
|
||||
# LOCAL mode requires Claude Code to be available
|
||||
# For patterns/examples, this is less practical than API mode
|
||||
logger.info("ℹ️ LOCAL mode not yet supported for pattern/example enhancement")
|
||||
logger.info(
|
||||
" Use API mode (set ANTHROPIC_API_KEY) or 'skill-seekers enhance' for SKILL.md"
|
||||
|
||||
def _check_claude_cli(self) -> bool:
|
||||
"""Check if Claude Code CLI is available"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["claude", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
self.enabled = False
|
||||
return result.returncode == 0
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
return False
|
||||
|
||||
def _call_claude(self, prompt: str, max_tokens: int = 1000) -> str | None:
|
||||
"""Call Claude API with error handling"""
|
||||
"""Call Claude (API or LOCAL mode) with error handling"""
|
||||
if self.mode == "api":
|
||||
return self._call_claude_api(prompt, max_tokens)
|
||||
elif self.mode == "local":
|
||||
return self._call_claude_local(prompt)
|
||||
return None
|
||||
|
||||
def _call_claude_api(self, prompt: str, max_tokens: int = 1000) -> str | None:
|
||||
"""Call Claude API"""
|
||||
if not self.client:
|
||||
return None
|
||||
|
||||
@@ -115,6 +156,82 @@ class AIEnhancer:
|
||||
logger.warning(f"⚠️ AI API call failed: {e}")
|
||||
return None
|
||||
|
||||
def _call_claude_local(self, prompt: str) -> str | None:
|
||||
"""Call Claude using LOCAL mode (Claude Code CLI)"""
|
||||
try:
|
||||
# Create a temporary directory for this enhancement
|
||||
with tempfile.TemporaryDirectory(prefix="ai_enhance_") as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create prompt file
|
||||
prompt_file = temp_path / "prompt.md"
|
||||
output_file = temp_path / "response.json"
|
||||
|
||||
# Write prompt with instructions to output JSON
|
||||
full_prompt = f"""# AI Analysis Task
|
||||
|
||||
IMPORTANT: You MUST write your response as valid JSON to this file:
|
||||
{output_file}
|
||||
|
||||
## Task
|
||||
|
||||
{prompt}
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Analyze the input carefully
|
||||
2. Generate the JSON response as specified
|
||||
3. Use the Write tool to save the JSON to: {output_file}
|
||||
4. The JSON must be valid and parseable
|
||||
|
||||
DO NOT include any explanation - just write the JSON file.
|
||||
"""
|
||||
prompt_file.write_text(full_prompt)
|
||||
|
||||
# Run Claude CLI
|
||||
result = subprocess.run(
|
||||
["claude", "--dangerously-skip-permissions", str(prompt_file)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120, # 2 minute timeout per call
|
||||
cwd=str(temp_path),
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.warning(f"⚠️ Claude CLI returned error: {result.returncode}")
|
||||
return None
|
||||
|
||||
# Read output file
|
||||
if output_file.exists():
|
||||
response_text = output_file.read_text()
|
||||
# Try to extract JSON from response
|
||||
try:
|
||||
# Validate it's valid JSON
|
||||
json.loads(response_text)
|
||||
return response_text
|
||||
except json.JSONDecodeError:
|
||||
# Try to find JSON in the response
|
||||
import re
|
||||
json_match = re.search(r'\[[\s\S]*\]|\{[\s\S]*\}', response_text)
|
||||
if json_match:
|
||||
return json_match.group()
|
||||
logger.warning("⚠️ Could not parse JSON from LOCAL response")
|
||||
return None
|
||||
else:
|
||||
# Look for any JSON file created
|
||||
for json_file in temp_path.glob("*.json"):
|
||||
if json_file.name != "prompt.json":
|
||||
return json_file.read_text()
|
||||
logger.warning("⚠️ No output file from LOCAL mode")
|
||||
return None
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("⚠️ Claude CLI timeout (2 minutes)")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ LOCAL mode error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
class PatternEnhancer(AIEnhancer):
|
||||
"""Enhance design pattern detection with AI analysis"""
|
||||
@@ -132,20 +249,68 @@ class PatternEnhancer(AIEnhancer):
|
||||
if not self.enabled or not patterns:
|
||||
return patterns
|
||||
|
||||
logger.info(f"🤖 Enhancing {len(patterns)} detected patterns with AI...")
|
||||
|
||||
# Batch patterns to minimize API calls (max 5 per batch)
|
||||
batch_size = 5
|
||||
enhanced = []
|
||||
# Use larger batch size for LOCAL mode (configurable)
|
||||
if self.mode == "local":
|
||||
batch_size = self.local_batch_size
|
||||
parallel_workers = self.local_parallel_workers
|
||||
logger.info(
|
||||
f"🤖 Enhancing {len(patterns)} patterns with AI "
|
||||
f"(LOCAL mode: {batch_size} per batch, {parallel_workers} parallel workers)..."
|
||||
)
|
||||
else:
|
||||
batch_size = 5 # API mode uses smaller batches
|
||||
parallel_workers = 1 # API mode is sequential
|
||||
logger.info(f"🤖 Enhancing {len(patterns)} detected patterns with AI...")
|
||||
|
||||
# Create batches
|
||||
batches = []
|
||||
for i in range(0, len(patterns), batch_size):
|
||||
batch = patterns[i : i + batch_size]
|
||||
batch_results = self._enhance_pattern_batch(batch)
|
||||
enhanced.extend(batch_results)
|
||||
batches.append(patterns[i : i + batch_size])
|
||||
|
||||
# Process batches (parallel for LOCAL, sequential for API)
|
||||
if parallel_workers > 1 and len(batches) > 1:
|
||||
enhanced = self._enhance_patterns_parallel(batches, parallel_workers)
|
||||
else:
|
||||
enhanced = []
|
||||
for batch in batches:
|
||||
batch_results = self._enhance_pattern_batch(batch)
|
||||
enhanced.extend(batch_results)
|
||||
|
||||
logger.info(f"✅ Enhanced {len(enhanced)} patterns")
|
||||
return enhanced
|
||||
|
||||
def _enhance_patterns_parallel(self, batches: list[list[dict]], workers: int) -> list[dict]:
|
||||
"""Process pattern batches in parallel using ThreadPoolExecutor."""
|
||||
results = [None] * len(batches) # Preserve order
|
||||
|
||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# Submit all batches
|
||||
future_to_idx = {
|
||||
executor.submit(self._enhance_pattern_batch, batch): idx
|
||||
for idx, batch in enumerate(batches)
|
||||
}
|
||||
|
||||
# Collect results as they complete
|
||||
completed = 0
|
||||
total = len(batches)
|
||||
for future in as_completed(future_to_idx):
|
||||
idx = future_to_idx[future]
|
||||
try:
|
||||
results[idx] = future.result()
|
||||
completed += 1
|
||||
if completed % 5 == 0 or completed == total:
|
||||
logger.info(f" Progress: {completed}/{total} batches completed")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Batch {idx} failed: {e}")
|
||||
results[idx] = batches[idx] # Return unenhanced on failure
|
||||
|
||||
# Flatten results
|
||||
enhanced = []
|
||||
for batch_result in results:
|
||||
if batch_result:
|
||||
enhanced.extend(batch_result)
|
||||
return enhanced
|
||||
|
||||
def _enhance_pattern_batch(self, patterns: list[dict]) -> list[dict]:
|
||||
"""Enhance a batch of patterns"""
|
||||
# Prepare prompt
|
||||
@@ -176,8 +341,6 @@ Format as JSON array matching input order. Be concise and actionable.
|
||||
return patterns
|
||||
|
||||
try:
|
||||
import json
|
||||
|
||||
analyses = json.loads(response)
|
||||
|
||||
# Merge AI analysis into patterns
|
||||
@@ -223,20 +386,68 @@ class TestExampleEnhancer(AIEnhancer):
|
||||
if not self.enabled or not examples:
|
||||
return examples
|
||||
|
||||
logger.info(f"🤖 Enhancing {len(examples)} test examples with AI...")
|
||||
|
||||
# Batch examples to minimize API calls
|
||||
batch_size = 5
|
||||
enhanced = []
|
||||
# Use larger batch size for LOCAL mode (configurable)
|
||||
if self.mode == "local":
|
||||
batch_size = self.local_batch_size
|
||||
parallel_workers = self.local_parallel_workers
|
||||
logger.info(
|
||||
f"🤖 Enhancing {len(examples)} test examples with AI "
|
||||
f"(LOCAL mode: {batch_size} per batch, {parallel_workers} parallel workers)..."
|
||||
)
|
||||
else:
|
||||
batch_size = 5 # API mode uses smaller batches
|
||||
parallel_workers = 1 # API mode is sequential
|
||||
logger.info(f"🤖 Enhancing {len(examples)} test examples with AI...")
|
||||
|
||||
# Create batches
|
||||
batches = []
|
||||
for i in range(0, len(examples), batch_size):
|
||||
batch = examples[i : i + batch_size]
|
||||
batch_results = self._enhance_example_batch(batch)
|
||||
enhanced.extend(batch_results)
|
||||
batches.append(examples[i : i + batch_size])
|
||||
|
||||
# Process batches (parallel for LOCAL, sequential for API)
|
||||
if parallel_workers > 1 and len(batches) > 1:
|
||||
enhanced = self._enhance_examples_parallel(batches, parallel_workers)
|
||||
else:
|
||||
enhanced = []
|
||||
for batch in batches:
|
||||
batch_results = self._enhance_example_batch(batch)
|
||||
enhanced.extend(batch_results)
|
||||
|
||||
logger.info(f"✅ Enhanced {len(enhanced)} examples")
|
||||
return enhanced
|
||||
|
||||
def _enhance_examples_parallel(self, batches: list[list[dict]], workers: int) -> list[dict]:
|
||||
"""Process example batches in parallel using ThreadPoolExecutor."""
|
||||
results = [None] * len(batches) # Preserve order
|
||||
|
||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# Submit all batches
|
||||
future_to_idx = {
|
||||
executor.submit(self._enhance_example_batch, batch): idx
|
||||
for idx, batch in enumerate(batches)
|
||||
}
|
||||
|
||||
# Collect results as they complete
|
||||
completed = 0
|
||||
total = len(batches)
|
||||
for future in as_completed(future_to_idx):
|
||||
idx = future_to_idx[future]
|
||||
try:
|
||||
results[idx] = future.result()
|
||||
completed += 1
|
||||
if completed % 5 == 0 or completed == total:
|
||||
logger.info(f" Progress: {completed}/{total} batches completed")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Batch {idx} failed: {e}")
|
||||
results[idx] = batches[idx] # Return unenhanced on failure
|
||||
|
||||
# Flatten results
|
||||
enhanced = []
|
||||
for batch_result in results:
|
||||
if batch_result:
|
||||
enhanced.extend(batch_result)
|
||||
return enhanced
|
||||
|
||||
def _enhance_example_batch(self, examples: list[dict]) -> list[dict]:
|
||||
"""Enhance a batch of examples"""
|
||||
# Prepare prompt
|
||||
@@ -268,8 +479,6 @@ Format as JSON array matching input order. Focus on educational value.
|
||||
return examples
|
||||
|
||||
try:
|
||||
import json
|
||||
|
||||
analyses = json.loads(response)
|
||||
|
||||
# Merge AI analysis into examples
|
||||
|
||||
@@ -75,6 +75,53 @@ LANGUAGE_EXTENSIONS = {
|
||||
".php": "PHP",
|
||||
}
|
||||
|
||||
# Markdown extension mapping
|
||||
MARKDOWN_EXTENSIONS = {".md", ".markdown", ".mdown", ".mkd"}
|
||||
|
||||
# Common documentation folders to scan
|
||||
DOC_FOLDERS = {"docs", "doc", "documentation", "wiki", ".github"}
|
||||
|
||||
# Root-level doc files → category mapping
|
||||
ROOT_DOC_CATEGORIES = {
|
||||
"readme": "overview",
|
||||
"contributing": "contributing",
|
||||
"changelog": "changelog",
|
||||
"history": "changelog",
|
||||
"license": "license",
|
||||
"authors": "authors",
|
||||
"code_of_conduct": "community",
|
||||
"security": "security",
|
||||
"architecture": "architecture",
|
||||
"design": "architecture",
|
||||
}
|
||||
|
||||
# Folder name → category mapping
|
||||
FOLDER_CATEGORIES = {
|
||||
"architecture": "architecture",
|
||||
"arch": "architecture",
|
||||
"design": "architecture",
|
||||
"guides": "guides",
|
||||
"guide": "guides",
|
||||
"tutorials": "guides",
|
||||
"tutorial": "guides",
|
||||
"howto": "guides",
|
||||
"how-to": "guides",
|
||||
"workflows": "workflows",
|
||||
"workflow": "workflows",
|
||||
"templates": "templates",
|
||||
"template": "templates",
|
||||
"api": "api",
|
||||
"reference": "api",
|
||||
"examples": "examples",
|
||||
"example": "examples",
|
||||
"specs": "specifications",
|
||||
"spec": "specifications",
|
||||
"rfcs": "specifications",
|
||||
"rfc": "specifications",
|
||||
"features": "features",
|
||||
"feature": "features",
|
||||
}
|
||||
|
||||
# Default directories to exclude
|
||||
DEFAULT_EXCLUDED_DIRS = {
|
||||
"node_modules",
|
||||
@@ -216,6 +263,469 @@ def walk_directory(
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def walk_markdown_files(
|
||||
root: Path,
|
||||
gitignore_spec: pathspec.PathSpec | None = None,
|
||||
excluded_dirs: set | None = None,
|
||||
) -> list[Path]:
|
||||
"""
|
||||
Walk directory tree and collect markdown documentation files.
|
||||
|
||||
Args:
|
||||
root: Root directory to walk
|
||||
gitignore_spec: Optional PathSpec object for .gitignore rules
|
||||
excluded_dirs: Set of directory names to exclude
|
||||
|
||||
Returns:
|
||||
List of markdown file paths
|
||||
"""
|
||||
if excluded_dirs is None:
|
||||
excluded_dirs = DEFAULT_EXCLUDED_DIRS
|
||||
|
||||
files = []
|
||||
root = Path(root).resolve()
|
||||
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
current_dir = Path(dirpath)
|
||||
|
||||
# Filter out excluded directories (in-place modification)
|
||||
dirnames[:] = [d for d in dirnames if not should_exclude_dir(d, excluded_dirs)]
|
||||
|
||||
for filename in filenames:
|
||||
file_path = current_dir / filename
|
||||
|
||||
# Check .gitignore rules
|
||||
if gitignore_spec:
|
||||
try:
|
||||
rel_path = file_path.relative_to(root)
|
||||
if gitignore_spec.match_file(str(rel_path)):
|
||||
logger.debug(f"Skipping (gitignore): {rel_path}")
|
||||
continue
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Check if markdown file
|
||||
if file_path.suffix.lower() not in MARKDOWN_EXTENSIONS:
|
||||
continue
|
||||
|
||||
files.append(file_path)
|
||||
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def categorize_markdown_file(file_path: Path, root: Path) -> str:
|
||||
"""
|
||||
Categorize a markdown file based on its location and filename.
|
||||
|
||||
Args:
|
||||
file_path: Path to the markdown file
|
||||
root: Root directory of the project
|
||||
|
||||
Returns:
|
||||
Category name (e.g., 'overview', 'guides', 'architecture')
|
||||
"""
|
||||
try:
|
||||
rel_path = file_path.relative_to(root)
|
||||
except ValueError:
|
||||
return "other"
|
||||
|
||||
# Check root-level files by filename
|
||||
if len(rel_path.parts) == 1:
|
||||
filename_lower = file_path.stem.lower().replace("-", "_").replace(" ", "_")
|
||||
for key, category in ROOT_DOC_CATEGORIES.items():
|
||||
if key in filename_lower:
|
||||
return category
|
||||
return "overview" # Default for root .md files
|
||||
|
||||
# Check folder-based categorization
|
||||
for part in rel_path.parts[:-1]: # Exclude filename
|
||||
part_lower = part.lower().replace("-", "_").replace(" ", "_")
|
||||
for key, category in FOLDER_CATEGORIES.items():
|
||||
if key in part_lower:
|
||||
return category
|
||||
|
||||
# Default category
|
||||
return "other"
|
||||
|
||||
|
||||
def extract_markdown_structure(content: str) -> dict[str, Any]:
|
||||
"""
|
||||
Extract structure from markdown content (headers, code blocks, links).
|
||||
|
||||
Args:
|
||||
content: Markdown file content
|
||||
|
||||
Returns:
|
||||
Dictionary with extracted structure
|
||||
"""
|
||||
import re
|
||||
|
||||
structure = {
|
||||
"title": None,
|
||||
"headers": [],
|
||||
"code_blocks": [],
|
||||
"links": [],
|
||||
"word_count": len(content.split()),
|
||||
"line_count": len(content.split("\n")),
|
||||
}
|
||||
|
||||
lines = content.split("\n")
|
||||
|
||||
# Extract headers
|
||||
for i, line in enumerate(lines):
|
||||
header_match = re.match(r"^(#{1,6})\s+(.+)$", line)
|
||||
if header_match:
|
||||
level = len(header_match.group(1))
|
||||
text = header_match.group(2).strip()
|
||||
structure["headers"].append({
|
||||
"level": level,
|
||||
"text": text,
|
||||
"line": i + 1,
|
||||
})
|
||||
# First h1 is the title
|
||||
if level == 1 and structure["title"] is None:
|
||||
structure["title"] = text
|
||||
|
||||
# Extract code blocks (fenced)
|
||||
code_block_pattern = re.compile(r"```(\w*)\n(.*?)```", re.DOTALL)
|
||||
for match in code_block_pattern.finditer(content):
|
||||
language = match.group(1) or "text"
|
||||
code = match.group(2).strip()
|
||||
if len(code) > 0:
|
||||
structure["code_blocks"].append({
|
||||
"language": language,
|
||||
"code": code[:500], # Truncate long code blocks
|
||||
"full_length": len(code),
|
||||
})
|
||||
|
||||
# Extract links
|
||||
link_pattern = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
|
||||
for match in link_pattern.finditer(content):
|
||||
structure["links"].append({
|
||||
"text": match.group(1),
|
||||
"url": match.group(2),
|
||||
})
|
||||
|
||||
return structure
|
||||
|
||||
|
||||
def generate_markdown_summary(content: str, structure: dict[str, Any], max_length: int = 500) -> str:
|
||||
"""
|
||||
Generate a summary of markdown content.
|
||||
|
||||
Args:
|
||||
content: Full markdown content
|
||||
structure: Extracted structure from extract_markdown_structure()
|
||||
max_length: Maximum summary length
|
||||
|
||||
Returns:
|
||||
Summary string
|
||||
"""
|
||||
# Start with title if available
|
||||
summary_parts = []
|
||||
|
||||
if structure.get("title"):
|
||||
summary_parts.append(f"**{structure['title']}**")
|
||||
|
||||
# Add header outline (first 5 h2/h3 headers)
|
||||
h2_h3 = [h for h in structure.get("headers", []) if h["level"] in (2, 3)][:5]
|
||||
if h2_h3:
|
||||
sections = [h["text"] for h in h2_h3]
|
||||
summary_parts.append(f"Sections: {', '.join(sections)}")
|
||||
|
||||
# Extract first paragraph (skip headers and empty lines)
|
||||
lines = content.split("\n")
|
||||
first_para = []
|
||||
in_para = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("#") or stripped.startswith("```"):
|
||||
if in_para:
|
||||
break
|
||||
continue
|
||||
if stripped:
|
||||
in_para = True
|
||||
first_para.append(stripped)
|
||||
elif in_para:
|
||||
break
|
||||
|
||||
if first_para:
|
||||
para_text = " ".join(first_para)
|
||||
if len(para_text) > 200:
|
||||
para_text = para_text[:200] + "..."
|
||||
summary_parts.append(para_text)
|
||||
|
||||
# Add stats
|
||||
stats = f"({structure.get('word_count', 0)} words, {len(structure.get('code_blocks', []))} code blocks)"
|
||||
summary_parts.append(stats)
|
||||
|
||||
summary = "\n".join(summary_parts)
|
||||
if len(summary) > max_length:
|
||||
summary = summary[:max_length] + "..."
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
def process_markdown_docs(
|
||||
directory: Path,
|
||||
output_dir: Path,
|
||||
depth: str = "deep",
|
||||
gitignore_spec: pathspec.PathSpec | None = None,
|
||||
enhance_with_ai: bool = False,
|
||||
ai_mode: str = "none",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Process all markdown documentation files in a directory.
|
||||
|
||||
Args:
|
||||
directory: Root directory to scan
|
||||
output_dir: Output directory for processed docs
|
||||
depth: Processing depth ('surface', 'deep', 'full')
|
||||
gitignore_spec: Optional .gitignore spec
|
||||
enhance_with_ai: Whether to use AI enhancement
|
||||
ai_mode: AI mode ('none', 'auto', 'api', 'local')
|
||||
|
||||
Returns:
|
||||
Dictionary with processed documentation data
|
||||
"""
|
||||
logger.info("Scanning for markdown documentation...")
|
||||
|
||||
# Find all markdown files
|
||||
md_files = walk_markdown_files(directory, gitignore_spec)
|
||||
logger.info(f"Found {len(md_files)} markdown files")
|
||||
|
||||
if not md_files:
|
||||
return {"files": [], "categories": {}, "total_files": 0}
|
||||
|
||||
# Process each file
|
||||
processed_docs = []
|
||||
categories = {}
|
||||
|
||||
for md_path in md_files:
|
||||
try:
|
||||
content = md_path.read_text(encoding="utf-8", errors="ignore")
|
||||
rel_path = str(md_path.relative_to(directory))
|
||||
category = categorize_markdown_file(md_path, directory)
|
||||
|
||||
doc_data = {
|
||||
"path": rel_path,
|
||||
"filename": md_path.name,
|
||||
"category": category,
|
||||
"size_bytes": len(content.encode("utf-8")),
|
||||
}
|
||||
|
||||
# Surface depth: just path and category
|
||||
if depth == "surface":
|
||||
processed_docs.append(doc_data)
|
||||
else:
|
||||
# Deep/Full: extract structure and summary
|
||||
structure = extract_markdown_structure(content)
|
||||
summary = generate_markdown_summary(content, structure)
|
||||
|
||||
doc_data.update({
|
||||
"title": structure.get("title") or md_path.stem,
|
||||
"structure": structure,
|
||||
"summary": summary,
|
||||
"content": content if depth == "full" else None,
|
||||
})
|
||||
processed_docs.append(doc_data)
|
||||
|
||||
# Track categories
|
||||
if category not in categories:
|
||||
categories[category] = []
|
||||
categories[category].append(rel_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to process {md_path}: {e}")
|
||||
continue
|
||||
|
||||
# AI Enhancement (if enabled and enhance_level >= 2)
|
||||
if enhance_with_ai and ai_mode != "none" and processed_docs:
|
||||
logger.info("🤖 Enhancing documentation analysis with AI...")
|
||||
try:
|
||||
processed_docs = _enhance_docs_with_ai(processed_docs, ai_mode)
|
||||
logger.info("✅ AI documentation enhancement complete")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ AI enhancement failed: {e}")
|
||||
|
||||
# Save processed docs to output
|
||||
docs_output_dir = output_dir / "documentation"
|
||||
docs_output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy files organized by category
|
||||
for doc in processed_docs:
|
||||
try:
|
||||
src_path = directory / doc["path"]
|
||||
category = doc["category"]
|
||||
category_dir = docs_output_dir / category
|
||||
category_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy file to category folder
|
||||
dest_path = category_dir / doc["filename"]
|
||||
import shutil
|
||||
shutil.copy2(src_path, dest_path)
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to copy {doc['path']}: {e}")
|
||||
|
||||
# Save documentation index
|
||||
index_data = {
|
||||
"total_files": len(processed_docs),
|
||||
"categories": categories,
|
||||
"files": processed_docs,
|
||||
}
|
||||
|
||||
index_json = docs_output_dir / "documentation_index.json"
|
||||
with open(index_json, "w", encoding="utf-8") as f:
|
||||
json.dump(index_data, f, indent=2, default=str)
|
||||
|
||||
logger.info(f"✅ Processed {len(processed_docs)} documentation files in {len(categories)} categories")
|
||||
logger.info(f"📁 Saved to: {docs_output_dir}")
|
||||
|
||||
return index_data
|
||||
|
||||
|
||||
def _enhance_docs_with_ai(docs: list[dict], ai_mode: str) -> list[dict]:
|
||||
"""
|
||||
Enhance documentation analysis with AI.
|
||||
|
||||
Args:
|
||||
docs: List of processed document dictionaries
|
||||
ai_mode: AI mode ('api' or 'local')
|
||||
|
||||
Returns:
|
||||
Enhanced document list
|
||||
"""
|
||||
# Try API mode first
|
||||
if ai_mode in ("api", "auto"):
|
||||
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
||||
if api_key:
|
||||
return _enhance_docs_api(docs, api_key)
|
||||
|
||||
# Fall back to LOCAL mode
|
||||
if ai_mode in ("local", "auto"):
|
||||
return _enhance_docs_local(docs)
|
||||
|
||||
return docs
|
||||
|
||||
|
||||
def _enhance_docs_api(docs: list[dict], api_key: str) -> list[dict]:
|
||||
"""Enhance docs using Claude API."""
|
||||
try:
|
||||
import anthropic
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
|
||||
# Batch documents for efficiency
|
||||
batch_size = 10
|
||||
for i in range(0, len(docs), batch_size):
|
||||
batch = docs[i:i + batch_size]
|
||||
|
||||
# Create prompt for batch
|
||||
docs_text = "\n\n".join([
|
||||
f"## {d.get('title', d['filename'])}\nCategory: {d['category']}\nSummary: {d.get('summary', 'N/A')}"
|
||||
for d in batch if d.get("summary")
|
||||
])
|
||||
|
||||
if not docs_text:
|
||||
continue
|
||||
|
||||
prompt = f"""Analyze these documentation files and provide:
|
||||
1. A brief description of what each document covers
|
||||
2. Key topics/concepts mentioned
|
||||
3. How they relate to each other
|
||||
|
||||
Documents:
|
||||
{docs_text}
|
||||
|
||||
Return JSON with format:
|
||||
{{"enhancements": [{{"filename": "...", "description": "...", "key_topics": [...], "related_to": [...]}}]}}"""
|
||||
|
||||
response = client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=2000,
|
||||
messages=[{"role": "user", "content": prompt}]
|
||||
)
|
||||
|
||||
# Parse response and merge enhancements
|
||||
try:
|
||||
import re
|
||||
json_match = re.search(r"\{.*\}", response.content[0].text, re.DOTALL)
|
||||
if json_match:
|
||||
enhancements = json.loads(json_match.group())
|
||||
for enh in enhancements.get("enhancements", []):
|
||||
for doc in batch:
|
||||
if doc["filename"] == enh.get("filename"):
|
||||
doc["ai_description"] = enh.get("description")
|
||||
doc["ai_topics"] = enh.get("key_topics", [])
|
||||
doc["ai_related"] = enh.get("related_to", [])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"API enhancement failed: {e}")
|
||||
|
||||
return docs
|
||||
|
||||
|
||||
def _enhance_docs_local(docs: list[dict]) -> list[dict]:
|
||||
"""Enhance docs using Claude Code CLI (LOCAL mode)."""
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
# Prepare batch of docs for enhancement
|
||||
docs_with_summary = [d for d in docs if d.get("summary")]
|
||||
if not docs_with_summary:
|
||||
return docs
|
||||
|
||||
docs_text = "\n\n".join([
|
||||
f"## {d.get('title', d['filename'])}\nCategory: {d['category']}\nPath: {d['path']}\nSummary: {d.get('summary', 'N/A')}"
|
||||
for d in docs_with_summary[:20] # Limit to 20 docs
|
||||
])
|
||||
|
||||
prompt = f"""Analyze these documentation files from a codebase and provide insights.
|
||||
|
||||
For each document, provide:
|
||||
1. A brief description of what it covers
|
||||
2. Key topics/concepts
|
||||
3. Related documents
|
||||
|
||||
Documents:
|
||||
{docs_text}
|
||||
|
||||
Output JSON only:
|
||||
{{"enhancements": [{{"filename": "...", "description": "...", "key_topics": ["..."], "related_to": ["..."]}}]}}"""
|
||||
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
|
||||
f.write(prompt)
|
||||
prompt_file = f.name
|
||||
|
||||
result = subprocess.run(
|
||||
["claude", "--dangerously-skip-permissions", "-p", prompt],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
|
||||
os.unlink(prompt_file)
|
||||
|
||||
if result.returncode == 0 and result.stdout:
|
||||
import re
|
||||
json_match = re.search(r"\{.*\}", result.stdout, re.DOTALL)
|
||||
if json_match:
|
||||
enhancements = json.loads(json_match.group())
|
||||
for enh in enhancements.get("enhancements", []):
|
||||
for doc in docs:
|
||||
if doc["filename"] == enh.get("filename"):
|
||||
doc["ai_description"] = enh.get("description")
|
||||
doc["ai_topics"] = enh.get("key_topics", [])
|
||||
doc["ai_related"] = enh.get("related_to", [])
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"LOCAL enhancement failed: {e}")
|
||||
|
||||
return docs
|
||||
|
||||
|
||||
def analyze_codebase(
|
||||
directory: Path,
|
||||
output_dir: Path,
|
||||
@@ -229,8 +739,8 @@ def analyze_codebase(
|
||||
extract_test_examples: bool = True,
|
||||
build_how_to_guides: bool = True,
|
||||
extract_config_patterns: bool = True,
|
||||
enhance_with_ai: bool = True,
|
||||
ai_mode: str = "auto",
|
||||
extract_docs: bool = True,
|
||||
enhance_level: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Analyze local codebase and extract code knowledge.
|
||||
@@ -248,12 +758,26 @@ def analyze_codebase(
|
||||
extract_test_examples: Extract usage examples from test files
|
||||
build_how_to_guides: Build how-to guides from workflow examples (C3.3)
|
||||
extract_config_patterns: Extract configuration patterns from config files (C3.4)
|
||||
enhance_with_ai: Enhance patterns and examples with AI analysis (C3.6)
|
||||
ai_mode: AI enhancement mode for how-to guides (auto, api, local, none)
|
||||
extract_docs: Extract and process markdown documentation files (default: True)
|
||||
enhance_level: AI enhancement level (0=off, 1=SKILL.md only, 2=+config+arch+docs, 3=full)
|
||||
|
||||
Returns:
|
||||
Analysis results dictionary
|
||||
"""
|
||||
# Determine AI enhancement settings based on level
|
||||
# Level 0: No AI enhancement
|
||||
# Level 1: SKILL.md only (handled in main.py)
|
||||
# Level 2: Architecture + Config AI enhancement
|
||||
# Level 3: Full AI enhancement (patterns, tests, config, architecture)
|
||||
enhance_patterns = enhance_level >= 3
|
||||
enhance_tests = enhance_level >= 3
|
||||
enhance_config = enhance_level >= 2
|
||||
enhance_architecture = enhance_level >= 2
|
||||
ai_mode = "auto" if enhance_level > 0 else "none"
|
||||
|
||||
if enhance_level > 0:
|
||||
level_names = {1: "SKILL.md only", 2: "SKILL.md+Architecture+Config", 3: "full"}
|
||||
logger.info(f"🤖 AI Enhancement Level: {enhance_level} ({level_names.get(enhance_level, 'unknown')})")
|
||||
# Resolve directory to absolute path to avoid relative_to() errors
|
||||
directory = Path(directory).resolve()
|
||||
|
||||
@@ -405,7 +929,7 @@ def analyze_codebase(
|
||||
logger.info("Detecting design patterns...")
|
||||
from skill_seekers.cli.pattern_recognizer import PatternRecognizer
|
||||
|
||||
pattern_recognizer = PatternRecognizer(depth=depth, enhance_with_ai=enhance_with_ai)
|
||||
pattern_recognizer = PatternRecognizer(depth=depth, enhance_with_ai=enhance_patterns)
|
||||
pattern_results = []
|
||||
|
||||
for file_path in files:
|
||||
@@ -447,7 +971,7 @@ def analyze_codebase(
|
||||
min_confidence=0.5,
|
||||
max_per_file=10,
|
||||
languages=languages,
|
||||
enhance_with_ai=enhance_with_ai,
|
||||
enhance_with_ai=enhance_tests,
|
||||
)
|
||||
|
||||
# Extract examples from directory
|
||||
@@ -486,8 +1010,8 @@ def analyze_codebase(
|
||||
try:
|
||||
from skill_seekers.cli.how_to_guide_builder import HowToGuideBuilder
|
||||
|
||||
# Create guide builder
|
||||
guide_builder = HowToGuideBuilder(enhance_with_ai=enhance_with_ai)
|
||||
# Create guide builder (uses same enhance level as test examples)
|
||||
guide_builder = HowToGuideBuilder(enhance_with_ai=enhance_tests)
|
||||
|
||||
# Build guides from workflow examples
|
||||
tutorials_dir = output_dir / "tutorials"
|
||||
@@ -505,7 +1029,7 @@ def analyze_codebase(
|
||||
examples_list,
|
||||
grouping_strategy="ai-tutorial-group",
|
||||
output_dir=tutorials_dir,
|
||||
enhance_with_ai=enhance_with_ai,
|
||||
enhance_with_ai=enhance_tests,
|
||||
ai_mode=ai_mode,
|
||||
)
|
||||
|
||||
@@ -538,8 +1062,8 @@ def analyze_codebase(
|
||||
# Convert to dict for enhancement
|
||||
result_dict = config_extractor.to_dict(extraction_result)
|
||||
|
||||
# AI Enhancement (if enabled)
|
||||
if enhance_with_ai and ai_mode != "none":
|
||||
# AI Enhancement (if enabled - level 2+)
|
||||
if enhance_config and ai_mode != "none":
|
||||
try:
|
||||
from skill_seekers.cli.config_enhancer import ConfigEnhancer
|
||||
|
||||
@@ -591,7 +1115,7 @@ def analyze_codebase(
|
||||
logger.info("Analyzing architectural patterns...")
|
||||
from skill_seekers.cli.architectural_pattern_detector import ArchitecturalPatternDetector
|
||||
|
||||
arch_detector = ArchitecturalPatternDetector(enhance_with_ai=enhance_with_ai)
|
||||
arch_detector = ArchitecturalPatternDetector(enhance_with_ai=enhance_architecture)
|
||||
arch_report = arch_detector.analyze(directory, results["files"])
|
||||
|
||||
if arch_report.patterns:
|
||||
@@ -610,6 +1134,33 @@ def analyze_codebase(
|
||||
else:
|
||||
logger.info("No clear architectural patterns detected")
|
||||
|
||||
# Extract markdown documentation (C3.9)
|
||||
docs_data = None
|
||||
if extract_docs:
|
||||
logger.info("Extracting project documentation...")
|
||||
try:
|
||||
# Determine AI enhancement for docs (level 2+)
|
||||
enhance_docs_ai = enhance_level >= 2
|
||||
docs_data = process_markdown_docs(
|
||||
directory=directory,
|
||||
output_dir=output_dir,
|
||||
depth=depth,
|
||||
gitignore_spec=gitignore_spec,
|
||||
enhance_with_ai=enhance_docs_ai,
|
||||
ai_mode=ai_mode,
|
||||
)
|
||||
|
||||
if docs_data and docs_data.get("total_files", 0) > 0:
|
||||
logger.info(
|
||||
f"✅ Extracted {docs_data['total_files']} documentation files "
|
||||
f"in {len(docs_data.get('categories', {}))} categories"
|
||||
)
|
||||
else:
|
||||
logger.info("No markdown documentation files found")
|
||||
except Exception as e:
|
||||
logger.warning(f"Documentation extraction failed: {e}")
|
||||
docs_data = None
|
||||
|
||||
# Generate SKILL.md and references/ directory
|
||||
logger.info("Generating SKILL.md and references...")
|
||||
_generate_skill_md(
|
||||
@@ -622,6 +1173,8 @@ def analyze_codebase(
|
||||
detect_patterns=detect_patterns,
|
||||
extract_test_examples=extract_test_examples,
|
||||
extract_config_patterns=extract_config_patterns,
|
||||
extract_docs=extract_docs,
|
||||
docs_data=docs_data,
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -637,6 +1190,8 @@ def _generate_skill_md(
|
||||
detect_patterns: bool,
|
||||
extract_test_examples: bool,
|
||||
extract_config_patterns: bool,
|
||||
extract_docs: bool = True,
|
||||
docs_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""
|
||||
Generate rich SKILL.md from codebase analysis results.
|
||||
@@ -716,7 +1271,10 @@ Use this skill when you need to:
|
||||
skill_content += "- ✅ Test Examples (C3.2)\n"
|
||||
if extract_config_patterns:
|
||||
skill_content += "- ✅ Configuration Patterns (C3.4)\n"
|
||||
skill_content += "- ✅ Architectural Analysis (C3.7)\n\n"
|
||||
skill_content += "- ✅ Architectural Analysis (C3.7)\n"
|
||||
if extract_docs:
|
||||
skill_content += "- ✅ Project Documentation (C3.9)\n"
|
||||
skill_content += "\n"
|
||||
|
||||
# Add design patterns if available
|
||||
if detect_patterns:
|
||||
@@ -747,6 +1305,12 @@ Use this skill when you need to:
|
||||
if config_content:
|
||||
skill_content += config_content
|
||||
|
||||
# Add project documentation if available
|
||||
if extract_docs and docs_data:
|
||||
docs_content = _format_documentation_section(output_dir, docs_data)
|
||||
if docs_content:
|
||||
skill_content += docs_content
|
||||
|
||||
# Available references
|
||||
skill_content += "## 📚 Available References\n\n"
|
||||
skill_content += "This skill includes detailed reference documentation:\n\n"
|
||||
@@ -776,6 +1340,9 @@ Use this skill when you need to:
|
||||
if (output_dir / "architecture").exists():
|
||||
skill_content += "- **Architecture**: `references/architecture/` - Architectural patterns\n"
|
||||
refs_added = True
|
||||
if extract_docs and (output_dir / "documentation").exists():
|
||||
skill_content += "- **Documentation**: `references/documentation/` - Project documentation\n"
|
||||
refs_added = True
|
||||
|
||||
if not refs_added:
|
||||
skill_content += "No additional references generated (analysis features disabled).\n"
|
||||
@@ -1005,6 +1572,75 @@ def _format_config_section(output_dir: Path) -> str:
|
||||
return content
|
||||
|
||||
|
||||
def _format_documentation_section(output_dir: Path, docs_data: dict[str, Any]) -> str:
|
||||
"""Format project documentation section from extracted markdown files."""
|
||||
if not docs_data or docs_data.get("total_files", 0) == 0:
|
||||
return ""
|
||||
|
||||
categories = docs_data.get("categories", {})
|
||||
files = docs_data.get("files", [])
|
||||
|
||||
content = "## 📖 Project Documentation\n\n"
|
||||
content += "*Extracted from markdown files in the project (C3.9)*\n\n"
|
||||
content += f"**Total Documentation Files:** {docs_data['total_files']}\n"
|
||||
content += f"**Categories:** {len(categories)}\n\n"
|
||||
|
||||
# List documents by category (most important first)
|
||||
priority_order = ["overview", "architecture", "guides", "workflows", "features", "api", "examples"]
|
||||
|
||||
# Sort categories by priority
|
||||
sorted_categories = []
|
||||
for cat in priority_order:
|
||||
if cat in categories:
|
||||
sorted_categories.append(cat)
|
||||
for cat in sorted(categories.keys()):
|
||||
if cat not in sorted_categories:
|
||||
sorted_categories.append(cat)
|
||||
|
||||
for category in sorted_categories[:6]: # Limit to 6 categories in SKILL.md
|
||||
cat_files = categories[category]
|
||||
content += f"### {category.title()}\n\n"
|
||||
|
||||
# Get file details for this category
|
||||
cat_docs = [f for f in files if f.get("category") == category]
|
||||
|
||||
for doc in cat_docs[:5]: # Limit to 5 docs per category
|
||||
title = doc.get("title") or doc.get("filename", "Unknown")
|
||||
path = doc.get("path", "")
|
||||
|
||||
# Add summary if available (deep/full depth)
|
||||
if doc.get("ai_description"):
|
||||
content += f"- **{title}**: {doc['ai_description']}\n"
|
||||
elif doc.get("summary"):
|
||||
# Extract first sentence from summary
|
||||
summary = doc["summary"].split("\n")[0]
|
||||
if len(summary) > 100:
|
||||
summary = summary[:100] + "..."
|
||||
content += f"- **{title}**: {summary}\n"
|
||||
else:
|
||||
content += f"- **{title}** (`{path}`)\n"
|
||||
|
||||
if len(cat_files) > 5:
|
||||
content += f"- *...and {len(cat_files) - 5} more*\n"
|
||||
|
||||
content += "\n"
|
||||
|
||||
# AI-enhanced topics if available
|
||||
all_topics = []
|
||||
for doc in files:
|
||||
all_topics.extend(doc.get("ai_topics", []))
|
||||
|
||||
if all_topics:
|
||||
# Deduplicate and count
|
||||
from collections import Counter
|
||||
topic_counts = Counter(all_topics)
|
||||
top_topics = [t for t, _ in topic_counts.most_common(10)]
|
||||
content += f"**Key Topics:** {', '.join(top_topics)}\n\n"
|
||||
|
||||
content += "*See `references/documentation/` for all project documentation*\n\n"
|
||||
return content
|
||||
|
||||
|
||||
def _generate_references(output_dir: Path):
|
||||
"""
|
||||
Generate references/ directory structure by symlinking analysis output.
|
||||
@@ -1023,6 +1659,7 @@ def _generate_references(output_dir: Path):
|
||||
"tutorials": "tutorials",
|
||||
"config_patterns": "config_patterns",
|
||||
"architecture": "architecture",
|
||||
"documentation": "documentation",
|
||||
}
|
||||
|
||||
for source, target in mappings.items():
|
||||
@@ -1132,6 +1769,12 @@ Examples:
|
||||
default=False,
|
||||
help="Skip configuration pattern extraction from config files (JSON, YAML, TOML, ENV, etc.) (default: enabled)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-docs",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Skip project documentation extraction from markdown files (README, docs/, etc.) (default: enabled)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ai-mode",
|
||||
choices=["auto", "api", "local", "none"],
|
||||
@@ -1147,6 +1790,19 @@ Examples:
|
||||
)
|
||||
parser.add_argument("--no-comments", action="store_true", help="Skip comment extraction")
|
||||
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
|
||||
parser.add_argument(
|
||||
"--enhance-level",
|
||||
type=int,
|
||||
choices=[0, 1, 2, 3],
|
||||
default=0,
|
||||
help=(
|
||||
"AI enhancement level: "
|
||||
"0=off (default), "
|
||||
"1=SKILL.md only, "
|
||||
"2=SKILL.md+Architecture+Config, "
|
||||
"3=full (patterns, tests, config, architecture, SKILL.md)"
|
||||
),
|
||||
)
|
||||
|
||||
# Check for deprecated flags
|
||||
deprecated_flags = {
|
||||
@@ -1232,8 +1888,8 @@ Examples:
|
||||
extract_test_examples=not args.skip_test_examples,
|
||||
build_how_to_guides=not args.skip_how_to_guides,
|
||||
extract_config_patterns=not args.skip_config_patterns,
|
||||
enhance_with_ai=True, # Auto-disables if no API key present
|
||||
ai_mode=args.ai_mode, # NEW: AI enhancement mode for how-to guides
|
||||
extract_docs=not args.skip_docs,
|
||||
enhance_level=args.enhance_level, # AI enhancement level (0-3)
|
||||
)
|
||||
|
||||
# Print summary
|
||||
|
||||
@@ -165,12 +165,16 @@ class ConfigEnhancer:
|
||||
for cf in config_files[:10]: # Limit to first 10 files
|
||||
settings_summary = []
|
||||
for setting in cf.get("settings", [])[:5]: # First 5 settings per file
|
||||
# Support both "type" (from config_extractor) and "value_type" (legacy)
|
||||
value_type = setting.get("type", setting.get("value_type", "unknown"))
|
||||
settings_summary.append(
|
||||
f" - {setting['key']}: {setting['value']} ({setting['value_type']})"
|
||||
f" - {setting['key']}: {setting['value']} ({value_type})"
|
||||
)
|
||||
|
||||
# Support both "type" (from config_extractor) and "config_type" (legacy)
|
||||
config_type = cf.get("type", cf.get("config_type", "unknown"))
|
||||
config_summary.append(f"""
|
||||
File: {cf["relative_path"]} ({cf["config_type"]})
|
||||
File: {cf["relative_path"]} ({config_type})
|
||||
Purpose: {cf["purpose"]}
|
||||
Settings:
|
||||
{chr(10).join(settings_summary)}
|
||||
@@ -252,124 +256,184 @@ Focus on actionable insights that help developers understand and improve their c
|
||||
def _enhance_via_local(self, result: dict) -> dict:
|
||||
"""Enhance configs using Claude Code CLI"""
|
||||
try:
|
||||
# Create temporary prompt file
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
|
||||
prompt_file = Path(f.name)
|
||||
f.write(self._create_local_prompt(result))
|
||||
# Create a temporary directory for this enhancement session
|
||||
with tempfile.TemporaryDirectory(prefix="config_enhance_") as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create output file path
|
||||
output_file = prompt_file.parent / f"{prompt_file.stem}_enhanced.json"
|
||||
# Define output file path (absolute path that Claude will write to)
|
||||
output_file = temp_path / "config_enhancement.json"
|
||||
|
||||
logger.info("🖥️ Launching Claude Code CLI for config analysis...")
|
||||
logger.info("⏱️ This will take 30-60 seconds...")
|
||||
# Create prompt file with the output path embedded
|
||||
prompt_file = temp_path / "enhance_prompt.md"
|
||||
prompt_content = self._create_local_prompt(result, output_file)
|
||||
prompt_file.write_text(prompt_content)
|
||||
|
||||
# Run Claude Code CLI
|
||||
result_data = self._run_claude_cli(prompt_file, output_file)
|
||||
logger.info("🖥️ Launching Claude Code CLI for config analysis...")
|
||||
logger.info("⏱️ This will take 30-60 seconds...")
|
||||
|
||||
# Clean up
|
||||
prompt_file.unlink()
|
||||
if output_file.exists():
|
||||
output_file.unlink()
|
||||
# Run Claude Code CLI
|
||||
result_data = self._run_claude_cli(prompt_file, output_file, temp_path)
|
||||
|
||||
if result_data:
|
||||
# Merge LOCAL enhancements
|
||||
result["ai_enhancements"] = result_data
|
||||
logger.info("✅ LOCAL enhancement complete")
|
||||
return result
|
||||
else:
|
||||
logger.warning("⚠️ LOCAL enhancement produced no results")
|
||||
return result
|
||||
if result_data:
|
||||
# Merge LOCAL enhancements
|
||||
result["ai_enhancements"] = result_data
|
||||
logger.info("✅ LOCAL enhancement complete")
|
||||
return result
|
||||
else:
|
||||
logger.warning("⚠️ LOCAL enhancement produced no results")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ LOCAL enhancement failed: {e}")
|
||||
return result
|
||||
|
||||
def _create_local_prompt(self, result: dict) -> str:
|
||||
"""Create prompt file for Claude Code CLI"""
|
||||
def _create_local_prompt(self, result: dict, output_file: Path) -> str:
|
||||
"""Create prompt file for Claude Code CLI
|
||||
|
||||
Args:
|
||||
result: Config extraction result dict
|
||||
output_file: Absolute path where Claude should write the JSON output
|
||||
|
||||
Returns:
|
||||
Prompt content string
|
||||
"""
|
||||
config_files = result.get("config_files", [])
|
||||
|
||||
# Format config data for Claude
|
||||
# Format config data for Claude (limit to 15 files for reasonable prompt size)
|
||||
config_data = []
|
||||
for cf in config_files[:10]:
|
||||
for cf in config_files[:15]:
|
||||
# Support both "type" (from config_extractor) and "config_type" (legacy)
|
||||
config_type = cf.get("type", cf.get("config_type", "unknown"))
|
||||
settings_preview = []
|
||||
for s in cf.get("settings", [])[:3]: # Show first 3 settings
|
||||
settings_preview.append(f" - {s.get('key', 'unknown')}: {str(s.get('value', ''))[:50]}")
|
||||
|
||||
config_data.append(f"""
|
||||
### {cf["relative_path"]} ({cf["config_type"]})
|
||||
### {cf["relative_path"]} ({config_type})
|
||||
- Purpose: {cf["purpose"]}
|
||||
- Patterns: {", ".join(cf.get("patterns", []))}
|
||||
- Settings count: {len(cf.get("settings", []))}
|
||||
- Patterns: {", ".join(cf.get("patterns", [])) or "none detected"}
|
||||
- Settings: {len(cf.get("settings", []))} total
|
||||
{chr(10).join(settings_preview) if settings_preview else " (no settings)"}
|
||||
""")
|
||||
|
||||
prompt = f"""# Configuration Analysis Task
|
||||
|
||||
I need you to analyze these configuration files and provide AI-enhanced insights.
|
||||
IMPORTANT: You MUST write the output to this EXACT file path:
|
||||
{output_file}
|
||||
|
||||
## Configuration Files ({len(config_files)} total)
|
||||
## Configuration Files ({len(config_files)} total, showing first 15)
|
||||
|
||||
{chr(10).join(config_data)}
|
||||
|
||||
## Your Task
|
||||
|
||||
Analyze these configs and create a JSON file with the following structure:
|
||||
Analyze these configuration files and write a JSON file to the path specified above.
|
||||
|
||||
The JSON must have this EXACT structure:
|
||||
|
||||
```json
|
||||
{{
|
||||
"file_enhancements": [
|
||||
{{
|
||||
"file_path": "path/to/file",
|
||||
"explanation": "What this config does",
|
||||
"best_practice": "Suggested improvements",
|
||||
"security_concern": "Security issues (if any)",
|
||||
"migration_suggestion": "Consolidation opportunities",
|
||||
"context": "Pattern explanation"
|
||||
"file_path": "relative/path/to/config.json",
|
||||
"explanation": "Brief explanation of what this config file does",
|
||||
"best_practice": "Suggested improvement or 'None'",
|
||||
"security_concern": "Security issue if any, or 'None'",
|
||||
"migration_suggestion": "Consolidation opportunity or 'None'",
|
||||
"context": "What pattern or purpose this serves"
|
||||
}}
|
||||
],
|
||||
"overall_insights": {{
|
||||
"config_count": {len(config_files)},
|
||||
"security_issues_found": 0,
|
||||
"consolidation_opportunities": [],
|
||||
"recommended_actions": []
|
||||
"consolidation_opportunities": ["List of suggestions"],
|
||||
"recommended_actions": ["List of actions"]
|
||||
}}
|
||||
}}
|
||||
```
|
||||
|
||||
Please save the JSON output to a file named `config_enhancement.json` in the current directory.
|
||||
## Instructions
|
||||
|
||||
Focus on actionable insights:
|
||||
1. Explain what each config does
|
||||
2. Suggest best practices
|
||||
3. Identify security concerns (hardcoded secrets, exposed credentials)
|
||||
4. Suggest consolidation opportunities
|
||||
5. Explain the detected patterns
|
||||
1. Use the Write tool to create the JSON file at: {output_file}
|
||||
2. Include an enhancement entry for each config file shown above
|
||||
3. Focus on actionable insights:
|
||||
- Explain what each config does in 1-2 sentences
|
||||
- Identify any hardcoded secrets or security issues
|
||||
- Suggest consolidation if configs have overlapping settings
|
||||
- Note any missing best practices
|
||||
|
||||
DO NOT explain your work - just write the JSON file directly.
|
||||
"""
|
||||
return prompt
|
||||
|
||||
def _run_claude_cli(self, prompt_file: Path, _output_file: Path) -> dict | None:
|
||||
"""Run Claude Code CLI and wait for completion"""
|
||||
def _run_claude_cli(
|
||||
self, prompt_file: Path, output_file: Path, working_dir: Path
|
||||
) -> dict | None:
|
||||
"""Run Claude Code CLI and wait for completion
|
||||
|
||||
Args:
|
||||
prompt_file: Path to the prompt markdown file
|
||||
output_file: Expected path where Claude will write the JSON output
|
||||
working_dir: Working directory to run Claude from
|
||||
|
||||
Returns:
|
||||
Parsed JSON dict if successful, None otherwise
|
||||
"""
|
||||
import time
|
||||
|
||||
try:
|
||||
# Run claude command
|
||||
start_time = time.time()
|
||||
|
||||
# Run claude command with --dangerously-skip-permissions to bypass all prompts
|
||||
# This allows Claude to write files without asking for confirmation
|
||||
logger.info(f" Running: claude --dangerously-skip-permissions {prompt_file.name}")
|
||||
logger.info(f" Output expected at: {output_file}")
|
||||
|
||||
result = subprocess.run(
|
||||
["claude", str(prompt_file)],
|
||||
["claude", "--dangerously-skip-permissions", str(prompt_file)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300, # 5 minute timeout
|
||||
cwd=str(working_dir),
|
||||
)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(f" Claude finished in {elapsed:.1f} seconds")
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"❌ Claude CLI failed: {result.stderr}")
|
||||
logger.error(f"❌ Claude CLI failed (exit code {result.returncode})")
|
||||
if result.stderr:
|
||||
logger.error(f" Error: {result.stderr[:200]}")
|
||||
return None
|
||||
|
||||
# Try to find output file (Claude might save it with different name)
|
||||
# Look for JSON files created in the last minute
|
||||
import time
|
||||
# Check if the expected output file was created
|
||||
if output_file.exists():
|
||||
try:
|
||||
with open(output_file) as f:
|
||||
data = json.load(f)
|
||||
if "file_enhancements" in data or "overall_insights" in data:
|
||||
logger.info(f"✅ Found enhancement data in {output_file.name}")
|
||||
return data
|
||||
else:
|
||||
logger.warning("⚠️ Output file exists but missing expected keys")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"❌ Failed to parse output JSON: {e}")
|
||||
return None
|
||||
|
||||
# Fallback: Look for any JSON files created in the working directory
|
||||
logger.info(" Looking for JSON files in working directory...")
|
||||
current_time = time.time()
|
||||
potential_files = []
|
||||
|
||||
for json_file in prompt_file.parent.glob("*.json"):
|
||||
if current_time - json_file.stat().st_mtime < 120: # Created in last 2 minutes
|
||||
for json_file in working_dir.glob("*.json"):
|
||||
# Check if created recently (within last 2 minutes)
|
||||
if current_time - json_file.stat().st_mtime < 120:
|
||||
potential_files.append(json_file)
|
||||
|
||||
# Try to load the most recent JSON file
|
||||
for json_file in sorted(potential_files, key=lambda f: f.stat().st_mtime, reverse=True):
|
||||
# Try to load the most recent JSON file with expected structure
|
||||
for json_file in sorted(
|
||||
potential_files, key=lambda f: f.stat().st_mtime, reverse=True
|
||||
):
|
||||
try:
|
||||
with open(json_file) as f:
|
||||
data = json.load(f)
|
||||
@@ -380,11 +444,17 @@ Focus on actionable insights:
|
||||
continue
|
||||
|
||||
logger.warning("⚠️ Could not find enhancement output file")
|
||||
logger.info(f" Expected file: {output_file}")
|
||||
logger.info(f" Files in dir: {list(working_dir.glob('*'))}")
|
||||
return None
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("❌ Claude CLI timeout (5 minutes)")
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
logger.error("❌ 'claude' command not found. Is Claude Code CLI installed?")
|
||||
logger.error(" Install with: npm install -g @anthropic-ai/claude-code")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error running Claude CLI: {e}")
|
||||
return None
|
||||
|
||||
@@ -34,6 +34,11 @@ class ConfigManager:
|
||||
},
|
||||
"resume": {"auto_save_interval_seconds": 60, "keep_progress_days": 7},
|
||||
"api_keys": {"anthropic": None, "google": None, "openai": None},
|
||||
"ai_enhancement": {
|
||||
"default_enhance_level": 1, # Default AI enhancement level (0-3)
|
||||
"local_batch_size": 20, # Patterns per Claude CLI call (default was 5)
|
||||
"local_parallel_workers": 3, # Concurrent Claude CLI calls
|
||||
},
|
||||
"first_run": {"completed": False, "version": "2.7.0"},
|
||||
}
|
||||
|
||||
@@ -378,6 +383,43 @@ class ConfigManager:
|
||||
if deleted_count > 0:
|
||||
print(f"🧹 Cleaned up {deleted_count} old progress file(s)")
|
||||
|
||||
# AI Enhancement Settings
|
||||
|
||||
def get_default_enhance_level(self) -> int:
|
||||
"""Get default AI enhancement level (0-3)."""
|
||||
return self.config.get("ai_enhancement", {}).get("default_enhance_level", 1)
|
||||
|
||||
def set_default_enhance_level(self, level: int):
|
||||
"""Set default AI enhancement level (0-3)."""
|
||||
if level not in [0, 1, 2, 3]:
|
||||
raise ValueError("enhance_level must be 0, 1, 2, or 3")
|
||||
if "ai_enhancement" not in self.config:
|
||||
self.config["ai_enhancement"] = {}
|
||||
self.config["ai_enhancement"]["default_enhance_level"] = level
|
||||
self.save_config()
|
||||
|
||||
def get_local_batch_size(self) -> int:
|
||||
"""Get batch size for LOCAL mode AI enhancement."""
|
||||
return self.config.get("ai_enhancement", {}).get("local_batch_size", 20)
|
||||
|
||||
def set_local_batch_size(self, size: int):
|
||||
"""Set batch size for LOCAL mode AI enhancement."""
|
||||
if "ai_enhancement" not in self.config:
|
||||
self.config["ai_enhancement"] = {}
|
||||
self.config["ai_enhancement"]["local_batch_size"] = size
|
||||
self.save_config()
|
||||
|
||||
def get_local_parallel_workers(self) -> int:
|
||||
"""Get number of parallel workers for LOCAL mode AI enhancement."""
|
||||
return self.config.get("ai_enhancement", {}).get("local_parallel_workers", 3)
|
||||
|
||||
def set_local_parallel_workers(self, workers: int):
|
||||
"""Set number of parallel workers for LOCAL mode AI enhancement."""
|
||||
if "ai_enhancement" not in self.config:
|
||||
self.config["ai_enhancement"] = {}
|
||||
self.config["ai_enhancement"]["local_parallel_workers"] = workers
|
||||
self.save_config()
|
||||
|
||||
# First Run Experience
|
||||
|
||||
def is_first_run(self) -> bool:
|
||||
@@ -443,6 +485,14 @@ class ConfigManager:
|
||||
print(f" • Auto-switch profiles: {self.config['rate_limit']['auto_switch_profiles']}")
|
||||
print(f" • Keep progress for: {self.config['resume']['keep_progress_days']} days")
|
||||
|
||||
# AI Enhancement settings
|
||||
level_names = {0: "off", 1: "SKILL.md only", 2: "standard", 3: "full"}
|
||||
default_level = self.get_default_enhance_level()
|
||||
print("\nAI Enhancement:")
|
||||
print(f" • Default level: {default_level} ({level_names.get(default_level, 'unknown')})")
|
||||
print(f" • Batch size: {self.get_local_batch_size()} patterns per call")
|
||||
print(f" • Parallel workers: {self.get_local_parallel_workers()} concurrent calls")
|
||||
|
||||
# Resumable jobs
|
||||
jobs = self.list_resumable_jobs()
|
||||
if jobs:
|
||||
|
||||
@@ -34,6 +34,7 @@ Examples:
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from skill_seekers.cli import __version__
|
||||
|
||||
@@ -299,7 +300,14 @@ For more information: https://github.com/yusufkaraaslan/Skill_Seekers
|
||||
)
|
||||
analyze_parser.add_argument("--file-patterns", help="Comma-separated file patterns")
|
||||
analyze_parser.add_argument(
|
||||
"--enhance", action="store_true", help="Enable AI enhancement (auto-detects API or LOCAL)"
|
||||
"--enhance", action="store_true", help="Enable AI enhancement (default level 1 = SKILL.md only)"
|
||||
)
|
||||
analyze_parser.add_argument(
|
||||
"--enhance-level",
|
||||
type=int,
|
||||
choices=[0, 1, 2, 3],
|
||||
default=None,
|
||||
help="AI enhancement level: 0=off, 1=SKILL.md only (default), 2=+Architecture+Config, 3=full"
|
||||
)
|
||||
analyze_parser.add_argument("--skip-api-reference", action="store_true", help="Skip API docs")
|
||||
analyze_parser.add_argument("--skip-dependency-graph", action="store_true", help="Skip dep graph")
|
||||
@@ -307,6 +315,7 @@ For more information: https://github.com/yusufkaraaslan/Skill_Seekers
|
||||
analyze_parser.add_argument("--skip-test-examples", action="store_true", help="Skip test examples")
|
||||
analyze_parser.add_argument("--skip-how-to-guides", action="store_true", help="Skip guides")
|
||||
analyze_parser.add_argument("--skip-config-patterns", action="store_true", help="Skip config")
|
||||
analyze_parser.add_argument("--skip-docs", action="store_true", help="Skip project docs (README, docs/)")
|
||||
analyze_parser.add_argument("--no-comments", action="store_true", help="Skip comments")
|
||||
analyze_parser.add_argument("--verbose", action="store_true", help="Verbose logging")
|
||||
|
||||
@@ -547,9 +556,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if args.output:
|
||||
sys.argv.extend(["--output", args.output])
|
||||
|
||||
# Handle preset flags (new)
|
||||
# Handle preset flags (depth and features)
|
||||
if args.quick:
|
||||
# Quick = surface depth + skip advanced features
|
||||
# Quick = surface depth + skip advanced features + no AI
|
||||
sys.argv.extend([
|
||||
"--depth", "surface",
|
||||
"--skip-patterns",
|
||||
@@ -558,17 +567,35 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"--skip-config-patterns",
|
||||
])
|
||||
elif args.comprehensive:
|
||||
# Comprehensive = full depth + all features + AI
|
||||
sys.argv.extend(["--depth", "full", "--ai-mode", "auto"])
|
||||
# Comprehensive = full depth + all features (AI level is separate)
|
||||
sys.argv.extend(["--depth", "full"])
|
||||
elif args.depth:
|
||||
sys.argv.extend(["--depth", args.depth])
|
||||
|
||||
# Determine enhance_level (independent of --comprehensive)
|
||||
# Priority: explicit --enhance-level > --enhance (uses config default) > --quick (level 0) > 0
|
||||
if args.enhance_level is not None:
|
||||
enhance_level = args.enhance_level
|
||||
elif args.quick:
|
||||
enhance_level = 0 # Quick mode disables AI
|
||||
elif args.enhance:
|
||||
# Use default from config (default: 1)
|
||||
try:
|
||||
from skill_seekers.cli.config_manager import get_config_manager
|
||||
config = get_config_manager()
|
||||
enhance_level = config.get_default_enhance_level()
|
||||
except Exception:
|
||||
enhance_level = 1 # Fallback to level 1
|
||||
else:
|
||||
enhance_level = 0 # Default: no AI
|
||||
|
||||
# Pass enhance_level to codebase_scraper
|
||||
sys.argv.extend(["--enhance-level", str(enhance_level)])
|
||||
|
||||
if args.languages:
|
||||
sys.argv.extend(["--languages", args.languages])
|
||||
if args.file_patterns:
|
||||
sys.argv.extend(["--file-patterns", args.file_patterns])
|
||||
if args.enhance:
|
||||
sys.argv.extend(["--ai-mode", "auto"])
|
||||
|
||||
# Pass through skip flags
|
||||
if args.skip_api_reference:
|
||||
@@ -583,12 +610,51 @@ def main(argv: list[str] | None = None) -> int:
|
||||
sys.argv.append("--skip-how-to-guides")
|
||||
if args.skip_config_patterns:
|
||||
sys.argv.append("--skip-config-patterns")
|
||||
if args.skip_docs:
|
||||
sys.argv.append("--skip-docs")
|
||||
if args.no_comments:
|
||||
sys.argv.append("--no-comments")
|
||||
if args.verbose:
|
||||
sys.argv.append("--verbose")
|
||||
|
||||
return analyze_main() or 0
|
||||
result = analyze_main() or 0
|
||||
|
||||
# Enhance SKILL.md if enhance_level >= 1
|
||||
if result == 0 and enhance_level >= 1:
|
||||
skill_dir = Path(args.output)
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
|
||||
if skill_md.exists():
|
||||
print("\n" + "=" * 60)
|
||||
print(f"ENHANCING SKILL.MD WITH AI (Level {enhance_level})")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
try:
|
||||
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
|
||||
|
||||
enhancer = LocalSkillEnhancer(str(skill_dir), force=True)
|
||||
# Use headless mode (runs claude directly, waits for completion)
|
||||
success = enhancer.run(
|
||||
headless=True,
|
||||
timeout=600, # 10 minute timeout
|
||||
)
|
||||
|
||||
if success:
|
||||
print("\n✅ SKILL.md enhancement complete!")
|
||||
# Re-read line count
|
||||
with open(skill_md) as f:
|
||||
lines = len(f.readlines())
|
||||
print(f" Enhanced SKILL.md: {lines} lines")
|
||||
else:
|
||||
print("\n⚠️ SKILL.md enhancement did not complete")
|
||||
print(" You can retry with: skill-seekers enhance " + str(skill_dir))
|
||||
except Exception as e:
|
||||
print(f"\n⚠️ SKILL.md enhancement failed: {e}")
|
||||
print(" You can retry with: skill-seekers enhance " + str(skill_dir))
|
||||
else:
|
||||
print(f"\n⚠️ SKILL.md not found at {skill_md}, skipping enhancement")
|
||||
|
||||
return result
|
||||
|
||||
elif args.command == "install-agent":
|
||||
from skill_seekers.cli.install_agent import main as install_agent_main
|
||||
|
||||
@@ -651,9 +651,20 @@ class GenericTestAnalyzer:
|
||||
"test_function": r"@Test\s+public\s+void\s+(\w+)\(\)",
|
||||
},
|
||||
"csharp": {
|
||||
"instantiation": r"var\s+(\w+)\s*=\s*new\s+(\w+)\(([^)]*)\)",
|
||||
"assertion": r"Assert\.(?:AreEqual|IsTrue|IsFalse|IsNotNull)\(([^)]+)\)",
|
||||
"test_function": r"\[Test\]\s+public\s+void\s+(\w+)\(\)",
|
||||
# Object instantiation patterns (var, explicit type, generic)
|
||||
"instantiation": r"(?:var|[\w<>]+)\s+(\w+)\s*=\s*new\s+([\w<>]+)\(([^)]*)\)",
|
||||
# NUnit assertions (Assert.AreEqual, Assert.That, etc.)
|
||||
"assertion": r"Assert\.(?:AreEqual|AreNotEqual|IsTrue|IsFalse|IsNull|IsNotNull|That|Throws|DoesNotThrow|Greater|Less|Contains)\(([^)]+)\)",
|
||||
# NUnit test attributes ([Test], [TestCase], [TestCaseSource])
|
||||
"test_function": r"\[(?:Test|TestCase|TestCaseSource|Theory|Fact)\(?[^\]]*\)?\]\s*(?:\[[\w\(\)\"',\s]+\]\s*)*public\s+(?:async\s+)?(?:Task|void)\s+(\w+)\s*\(",
|
||||
# Setup/Teardown patterns
|
||||
"setup": r"\[(?:SetUp|OneTimeSetUp|TearDown|OneTimeTearDown)\]\s*public\s+(?:async\s+)?(?:Task|void)\s+(\w+)\s*\(",
|
||||
# Mock/substitute patterns (NSubstitute, Moq)
|
||||
"mock": r"(?:Substitute\.For<([\w<>]+)>|new\s+Mock<([\w<>]+)>|MockRepository\.GenerateMock<([\w<>]+)>)\(",
|
||||
# Dependency injection patterns (Zenject, etc.)
|
||||
"injection": r"Container\.(?:Bind|BindInterfacesTo|BindInterfacesAndSelfTo)<([\w<>]+)>",
|
||||
# Configuration/setup dictionaries
|
||||
"config": r"(?:var|[\w<>]+)\s+\w+\s*=\s*new\s+(?:Dictionary|List|HashSet)<[^>]+>\s*\{[\s\S]{20,500}?\}",
|
||||
},
|
||||
"php": {
|
||||
"instantiation": r"\$(\w+)\s*=\s*new\s+(\w+)\(([^)]*)\)",
|
||||
@@ -667,11 +678,21 @@ class GenericTestAnalyzer:
|
||||
},
|
||||
}
|
||||
|
||||
# Language name normalization mapping
|
||||
LANGUAGE_ALIASES = {
|
||||
"c#": "csharp",
|
||||
"c++": "cpp",
|
||||
"c plus plus": "cpp",
|
||||
}
|
||||
|
||||
def extract(self, file_path: str, code: str, language: str) -> list[TestExample]:
|
||||
"""Extract examples from test file using regex patterns"""
|
||||
examples = []
|
||||
|
||||
language_lower = language.lower()
|
||||
# Normalize language name (e.g., "C#" -> "csharp")
|
||||
language_lower = self.LANGUAGE_ALIASES.get(language_lower, language_lower)
|
||||
|
||||
if language_lower not in self.PATTERNS:
|
||||
logger.warning(f"Language {language} not supported for regex extraction")
|
||||
return []
|
||||
@@ -715,6 +736,54 @@ class GenericTestAnalyzer:
|
||||
)
|
||||
examples.append(example)
|
||||
|
||||
# Extract mock/substitute patterns (if pattern exists)
|
||||
if "mock" in patterns:
|
||||
for mock_match in re.finditer(patterns["mock"], test_body):
|
||||
example = self._create_example(
|
||||
test_name=test_name,
|
||||
category="setup",
|
||||
code=mock_match.group(0),
|
||||
language=language,
|
||||
file_path=file_path,
|
||||
line_number=code[: start_pos + mock_match.start()].count("\n") + 1,
|
||||
)
|
||||
examples.append(example)
|
||||
|
||||
# Extract dependency injection patterns (if pattern exists)
|
||||
if "injection" in patterns:
|
||||
for inject_match in re.finditer(patterns["injection"], test_body):
|
||||
example = self._create_example(
|
||||
test_name=test_name,
|
||||
category="setup",
|
||||
code=inject_match.group(0),
|
||||
language=language,
|
||||
file_path=file_path,
|
||||
line_number=code[: start_pos + inject_match.start()].count("\n") + 1,
|
||||
)
|
||||
examples.append(example)
|
||||
|
||||
# Also extract setup/teardown methods (outside test functions)
|
||||
if "setup" in patterns:
|
||||
for setup_match in re.finditer(patterns["setup"], code):
|
||||
setup_name = setup_match.group(1)
|
||||
# Get setup function body
|
||||
setup_start = setup_match.end()
|
||||
# Find next method (setup or test)
|
||||
next_pattern = patterns.get("setup", patterns["test_function"])
|
||||
next_setup = re.search(next_pattern, code[setup_start:])
|
||||
setup_end = setup_start + next_setup.start() if next_setup else min(setup_start + 500, len(code))
|
||||
setup_body = code[setup_start:setup_end]
|
||||
|
||||
example = self._create_example(
|
||||
test_name=setup_name,
|
||||
category="setup",
|
||||
code=setup_match.group(0) + setup_body[:200], # Include some of the body
|
||||
language=language,
|
||||
file_path=file_path,
|
||||
line_number=code[: setup_match.start()].count("\n") + 1,
|
||||
)
|
||||
examples.append(example)
|
||||
|
||||
return examples
|
||||
|
||||
def _create_example(
|
||||
|
||||
Reference in New Issue
Block a user