Files
skill-seekers-reference/src/skill_seekers/cli/enhance_skill_local.py
yusyus ec3e0bf491 fix: Resolve 61 critical linting errors
Fixed priority linting errors to improve code quality:

Critical Fixes:
- F821 (2 errors): Fixed undefined name 'original_result' in config_enhancer.py
- UP035 (2 errors): Removed deprecated typing.Dict and typing.Type imports
- F401 (27 errors): Removed unused imports and added noqa for availability checks
- E722 (19 errors): Replaced bare 'except:' with 'except Exception:'

Code Quality Improvements:
- SIM201 (4 errors): Simplified 'not x == y' to 'x != y'
- SIM118 (2 errors): Removed unnecessary .keys() in dict iterations
- E741 (4 errors): Renamed ambiguous variable 'l' to 'line'
- I001 (1 error): Sorted imports in test_bootstrap_skill.py

All modified areas tested and passing:
- test_scraper_features.py: 42 passed
- test_integration.py: 51 passed
- test_architecture_scenarios.py: 11 passed
- test_real_world_fastmcp.py: 19 passed (1 skipped)

Remaining linting errors: 249 (mostly code style suggestions like ARG002, F841, SIM102)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 22:54:40 +03:00

1101 lines
40 KiB
Python

#!/usr/bin/env python3
"""
SKILL.md Enhancement Script (Local - Using Claude Code)
Opens a new terminal with Claude Code to enhance SKILL.md, then reports back.
No API key needed - uses your existing Claude Code Max plan!
Usage:
# Headless mode (default - runs in foreground, waits for completion)
skill-seekers enhance output/react/
# Background mode (runs in background, returns immediately)
skill-seekers enhance output/react/ --background
# Force mode (no confirmations, auto-yes to everything)
skill-seekers enhance output/react/ --force
# Daemon mode (persistent background process)
skill-seekers enhance output/react/ --daemon
# Interactive terminal mode
skill-seekers enhance output/react/ --interactive-enhancement
Modes:
- headless: Runs claude CLI directly, BLOCKS until done (default)
- background: Runs claude CLI in background, returns immediately
- daemon: Runs as persistent background process with monitoring
- terminal: Opens new terminal window (interactive)
Terminal Selection:
The script automatically detects which terminal app to use:
1. SKILL_SEEKER_TERMINAL env var (highest priority)
Example: export SKILL_SEEKER_TERMINAL="Ghostty"
2. TERM_PROGRAM env var (current terminal)
3. Terminal.app (fallback)
Supported terminals: Ghostty, iTerm, Terminal, WezTerm
"""
import json
import os
import subprocess
import sys
import tempfile
import threading
import time
from datetime import datetime
from pathlib import Path
# Add parent directory to path for imports when run as script
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from skill_seekers.cli.constants import LOCAL_CONTENT_LIMIT, LOCAL_PREVIEW_LIMIT
from skill_seekers.cli.utils import read_reference_files
def detect_terminal_app():
"""Detect which terminal app to use with cascading priority.
Priority order:
1. SKILL_SEEKER_TERMINAL environment variable (explicit user preference)
2. TERM_PROGRAM environment variable (inherit current terminal)
3. Terminal.app (fallback default)
Returns:
tuple: (terminal_app_name, detection_method)
- terminal_app_name (str): Name of terminal app to launch (e.g., "Ghostty", "Terminal")
- detection_method (str): How the terminal was detected (for logging)
Examples:
>>> os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
>>> detect_terminal_app()
('Ghostty', 'SKILL_SEEKER_TERMINAL')
>>> os.environ['TERM_PROGRAM'] = 'iTerm.app'
>>> detect_terminal_app()
('iTerm', 'TERM_PROGRAM')
"""
# Map TERM_PROGRAM values to macOS app names
TERMINAL_MAP = {
"Apple_Terminal": "Terminal",
"iTerm.app": "iTerm",
"ghostty": "Ghostty",
"WezTerm": "WezTerm",
}
# Priority 1: Check SKILL_SEEKER_TERMINAL env var (explicit preference)
preferred_terminal = os.environ.get("SKILL_SEEKER_TERMINAL", "").strip()
if preferred_terminal:
return preferred_terminal, "SKILL_SEEKER_TERMINAL"
# Priority 2: Check TERM_PROGRAM (inherit current terminal)
term_program = os.environ.get("TERM_PROGRAM", "").strip()
if term_program and term_program in TERMINAL_MAP:
return TERMINAL_MAP[term_program], "TERM_PROGRAM"
# Priority 3: Fallback to Terminal.app
if term_program:
# TERM_PROGRAM is set but unknown
return "Terminal", f"unknown TERM_PROGRAM ({term_program})"
else:
# No TERM_PROGRAM set
return "Terminal", "default"
class LocalSkillEnhancer:
def __init__(self, skill_dir, force=True):
"""Initialize enhancer.
Args:
skill_dir: Path to skill directory
force: If True, skip all confirmations (default: True, use --no-force to disable)
"""
self.skill_dir = Path(skill_dir)
self.references_dir = self.skill_dir / "references"
self.skill_md_path = self.skill_dir / "SKILL.md"
self.force = force
self.status_file = self.skill_dir / ".enhancement_status.json"
def summarize_reference(self, content: str, target_ratio: float = 0.3) -> str:
"""Intelligently summarize reference content to reduce size.
Strategy:
1. Keep first 20% (introduction/overview)
2. Extract code blocks (prioritize examples)
3. Keep headings and their first paragraph
4. Skip repetitive content
Args:
content: Full reference content
target_ratio: Target size as ratio of original (0.3 = 30%)
Returns:
Summarized content
"""
lines = content.split("\n")
target_lines = int(len(lines) * target_ratio)
# Priority 1: Keep introduction (first 20%)
intro_lines = int(len(lines) * 0.2)
result_lines = lines[:intro_lines]
# Priority 2: Extract code blocks
in_code_block = False
code_blocks = []
current_block = []
block_start_idx = 0
for i, line in enumerate(lines[intro_lines:], start=intro_lines):
if line.strip().startswith("```"):
if in_code_block:
# End of code block - add closing ``` and save
current_block.append(line)
code_blocks.append((block_start_idx, current_block))
current_block = []
in_code_block = False
else:
# Start of code block
in_code_block = True
block_start_idx = i
current_block = [line]
elif in_code_block:
current_block.append(line)
# Combine: intro + code blocks + headings
result = result_lines.copy()
# Add code blocks first (prioritize code examples)
for idx, block in code_blocks[:5]: # Max 5 code blocks
result.append("") # Add blank line before code block
result.extend(block)
# Priority 3: Keep headings with first paragraph
i = intro_lines
headings_added = 0
while i < len(lines) and headings_added < 10:
line = lines[i]
if line.startswith("#"):
# Found heading - keep it and next 3 lines
chunk = lines[i : min(i + 4, len(lines))]
result.extend(chunk)
headings_added += 1
i += 4
else:
i += 1
result.append("\n\n[Content intelligently summarized - full details in reference files]")
return "\n".join(result)
def create_enhancement_prompt(self, use_summarization=False, summarization_ratio=0.3):
"""Create the prompt file for Claude Code
Args:
use_summarization: If True, apply smart summarization to reduce size
summarization_ratio: Target size ratio when summarizing (0.3 = 30%)
"""
# Read reference files (with enriched metadata)
references = read_reference_files(
self.skill_dir, max_chars=LOCAL_CONTENT_LIMIT, preview_limit=LOCAL_PREVIEW_LIMIT
)
if not references:
print("❌ No reference files found")
return None
# Analyze sources
sources_found = set()
for metadata in references.values():
sources_found.add(metadata["source"])
# Calculate total size
total_ref_size = sum(meta["size"] for meta in references.values())
# Apply summarization if requested or if content is too large
if use_summarization or total_ref_size > 30000:
if not use_summarization:
print(f" ⚠️ Large skill detected ({total_ref_size:,} chars)")
print(
f" 📊 Applying smart summarization (target: {int(summarization_ratio * 100)}% of original)"
)
print()
# Summarize each reference
for filename, metadata in references.items():
summarized = self.summarize_reference(metadata["content"], summarization_ratio)
metadata["content"] = summarized
metadata["size"] = len(summarized)
new_size = sum(meta["size"] for meta in references.values())
print(
f" ✓ Reduced from {total_ref_size:,} to {new_size:,} chars ({int(new_size / total_ref_size * 100)}%)"
)
print()
# Read current SKILL.md
current_skill_md = ""
if self.skill_md_path.exists():
current_skill_md = self.skill_md_path.read_text(encoding="utf-8")
# Analyze conflicts if present
has_conflicts = any("conflicts" in meta["path"] for meta in references.values())
# Build prompt with multi-source awareness
prompt = f"""I need you to enhance the SKILL.md file for the {self.skill_dir.name} skill.
SKILL OVERVIEW:
- Name: {self.skill_dir.name}
- Source Types: {", ".join(sorted(sources_found))}
- Multi-Source: {"Yes" if len(sources_found) > 1 else "No"}
- Conflicts Detected: {"Yes - see conflicts.md in references" if has_conflicts else "No"}
CURRENT SKILL.MD:
{"-" * 60}
{current_skill_md if current_skill_md else "(No existing SKILL.md - create from scratch)"}
{"-" * 60}
SOURCE ANALYSIS:
{"-" * 60}
This skill combines knowledge from {len(sources_found)} source type(s):
"""
# Group references by (source_type, repo_id) for multi-source support
by_source = {}
for filename, metadata in references.items():
source = metadata["source"]
repo_id = metadata.get("repo_id") # None for single-source
key = (source, repo_id) if repo_id else (source, None)
if key not in by_source:
by_source[key] = []
by_source[key].append((filename, metadata))
# Add source breakdown with repo identity
for source, repo_id in sorted(by_source.keys()):
files = by_source[(source, repo_id)]
if repo_id:
prompt += f"\n**{source.upper()} - {repo_id} ({len(files)} file(s))**\n"
else:
prompt += f"\n**{source.upper()} ({len(files)} file(s))**\n"
for filename, metadata in files[:5]: # Top 5 per source
prompt += f"- {filename} (confidence: {metadata['confidence']}, {metadata['size']:,} chars)\n"
if len(files) > 5:
prompt += f"- ... and {len(files) - 5} more\n"
prompt += f"""
{"-" * 60}
REFERENCE DOCUMENTATION:
{"-" * 60}
"""
# Add references grouped by (source, repo_id) with metadata
for source, repo_id in sorted(by_source.keys()):
if repo_id:
prompt += f"\n### {source.upper()} SOURCES - {repo_id}\n\n"
else:
prompt += f"\n### {source.upper()} SOURCES\n\n"
for filename, metadata in by_source[(source, repo_id)]:
# Further limit per-file to 12K to be safe
content = metadata["content"]
max_per_file = 12000
if len(content) > max_per_file:
content = content[:max_per_file] + "\n\n[Content truncated for size...]"
prompt += f"\n#### {filename}\n"
if repo_id:
prompt += f"*Source: {metadata['source']} ({repo_id}), Confidence: {metadata['confidence']}*\n\n"
else:
prompt += (
f"*Source: {metadata['source']}, Confidence: {metadata['confidence']}*\n\n"
)
prompt += f"{content}\n"
prompt += f"""
{"-" * 60}
REFERENCE PRIORITY (when sources differ):
1. **Code patterns (codebase_analysis)**: Ground truth - what the code actually does
2. **Official documentation**: Intended API and usage patterns
3. **GitHub issues**: Real-world usage and known problems
4. **PDF documentation**: Additional context and tutorials
MULTI-REPOSITORY HANDLING:
"""
# Detect multiple repos from same source type
repo_ids = set()
for metadata in references.values():
if metadata.get("repo_id"):
repo_ids.add(metadata["repo_id"])
if len(repo_ids) > 1:
prompt += f"""
⚠️ MULTIPLE REPOSITORIES DETECTED: {", ".join(sorted(repo_ids))}
This skill combines codebase analysis from {len(repo_ids)} different repositories.
Each repo has its own ARCHITECTURE.md, patterns, examples, and configuration.
When synthesizing:
- Clearly identify which content comes from which repo
- Compare and contrast patterns across repos (e.g., "httpx uses Strategy pattern 50 times, httpcore uses it 32 times")
- Highlight relationships (e.g., "httpx is a client library built on top of httpcore")
- Present examples from BOTH repos to show different use cases
- If repos serve different purposes, explain when to use each
"""
else:
prompt += "\nSingle repository - standard synthesis applies.\n"
prompt += """
YOUR TASK:
Create an EXCELLENT SKILL.md file that synthesizes knowledge from multiple sources.
Requirements:
1. **Multi-Source Synthesis**
- Acknowledge that this skill combines multiple sources
- Highlight agreements between sources (builds confidence)
- Note discrepancies transparently (if present)
- Use source priority when synthesizing conflicting information
2. **Clear "When to Use This Skill" section**
- Be SPECIFIC about trigger conditions
- List concrete use cases
- Include perspective from both docs AND real-world usage (if GitHub/codebase data available)
3. **Excellent Quick Reference section**
- Extract 5-10 of the BEST, most practical code examples
- Prefer examples from HIGH CONFIDENCE sources first
- If code examples exist from codebase analysis, prioritize those (real usage)
- If docs examples exist, include those too (official patterns)
- Choose SHORT, clear examples (5-20 lines max)
- Use proper language tags (cpp, python, javascript, json, etc.)
- Add clear descriptions noting the source (e.g., "From official docs" or "From codebase")
4. **Detailed Reference Files description**
- Explain what's in each reference file
- Note the source type and confidence level
- Help users navigate multi-source documentation
5. **Practical "Working with This Skill" section**
- Clear guidance for beginners, intermediate, and advanced users
- Navigation tips for multi-source references
- How to resolve conflicts if present
6. **Key Concepts section** (if applicable)
- Explain core concepts
- Define important terminology
- Reconcile differences between sources if needed
7. **Conflict Handling** (if conflicts detected)
- Add a "Known Discrepancies" section
- Explain major conflicts transparently
- Provide guidance on which source to trust in each case
IMPORTANT:
- Extract REAL examples from the reference docs above
- Prioritize HIGH CONFIDENCE sources when synthesizing
- Note source attribution when helpful (e.g., "Official docs say X, but codebase shows Y")
- Make discrepancies transparent, not hidden
- Prioritize SHORT, clear examples
- Make it actionable and practical
- Keep the frontmatter (---\\nname: ...\\n---) intact
- Use proper markdown formatting
SAVE THE RESULT:
You MUST save the complete enhanced SKILL.md file.
CRITICAL INSTRUCTIONS:
1. First, create a backup: Write the current SKILL.md content to SKILL.md.backup
2. Then, write the enhanced content to: SKILL.md
This is NOT a read-only task - you have permission to modify SKILL.md.
Even if running from within another Claude Code session, this modification is ALLOWED and EXPECTED.
VERIFICATION:
After writing, the file SKILL.md should:
- Exist in the current directory
- Be larger than the original (200-1000+ lines)
- Contain all the enhancements from the references above
"""
return prompt
def write_status(self, status, message="", progress=0.0, error=None):
"""Write enhancement status to file for monitoring.
Args:
status: One of: pending, running, completed, failed
message: Status message
progress: Progress percentage (0.0-1.0)
error: Error message if failed
"""
status_data = {
"status": status,
"message": message,
"progress": progress,
"timestamp": datetime.now().isoformat(),
"skill_dir": str(self.skill_dir),
"error": error,
}
self.status_file.write_text(json.dumps(status_data, indent=2), encoding="utf-8")
def read_status(self):
"""Read enhancement status from file.
Returns:
dict: Status data or None if not found
"""
if not self.status_file.exists():
return None
try:
return json.loads(self.status_file.read_text(encoding="utf-8"))
except Exception:
return None
def run(self, headless=True, timeout=600, background=False, daemon=False):
"""Main enhancement workflow with automatic smart summarization for large skills.
Automatically detects large skills (>30K chars) and applies smart summarization
to ensure compatibility with Claude CLI's ~30-40K character limit.
Smart summarization strategy:
- Keeps first 20% (introduction/overview)
- Extracts up to 5 best code blocks
- Keeps up to 10 section headings with first paragraph
- Reduces to ~30% of original size
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)
background: If True, run in background and return immediately (default: False)
daemon: If True, run as persistent daemon with monitoring (default: False)
Returns:
bool: True if enhancement process started successfully, False otherwise
"""
# Background mode: Run in background thread, return immediately
if background:
return self._run_background(headless, timeout)
# Daemon mode: Run as persistent process with monitoring
if daemon:
return self._run_daemon(timeout)
print(f"\n{'=' * 60}")
print(f"LOCAL ENHANCEMENT: {self.skill_dir.name}")
print(f"{'=' * 60}\n")
# Validate
if not self.skill_dir.exists():
print(f"❌ Directory not found: {self.skill_dir}")
return False
# Read reference files
print("📖 Reading reference documentation...")
references = read_reference_files(
self.skill_dir, max_chars=LOCAL_CONTENT_LIMIT, preview_limit=LOCAL_PREVIEW_LIMIT
)
if not references:
print("❌ No reference files found to analyze")
return False
print(f" ✓ Read {len(references)} reference files")
total_size = sum(ref["size"] for ref in references.values())
print(f" ✓ Total size: {total_size:,} characters\n")
# Check if we need smart summarization
use_summarization = total_size > 30000
if use_summarization:
print("⚠️ LARGE SKILL DETECTED")
print(f" 📊 Reference content: {total_size:,} characters")
print(" 💡 Claude CLI limit: ~30,000-40,000 characters")
print()
print(" 🔧 Applying smart summarization to ensure success...")
print(" • Keeping introductions and overviews")
print(" • Extracting best code examples")
print(" • Preserving key concepts and headings")
print(" • Target: ~30% of original size")
print()
# Create prompt
print("📝 Creating enhancement prompt...")
prompt = self.create_enhancement_prompt(use_summarization=use_summarization)
if not prompt:
return False
# Save prompt to temp file
with tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False, encoding="utf-8"
) as f:
prompt_file = f.name
f.write(prompt)
if use_summarization:
print(f" ✓ Prompt created and optimized ({len(prompt):,} characters)")
print(" ✓ Ready for Claude CLI (within safe limits)")
print()
else:
print(f" ✓ Prompt saved ({len(prompt):,} characters)\n")
# 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")
print(" 2. Run Claude Code with the enhancement task")
print(" 3. Claude will read the docs and enhance SKILL.md")
print(" 4. Terminal will auto-close when done")
print()
# Create a shell script to run in the terminal
shell_script = f"""#!/bin/bash
claude {prompt_file}
echo ""
echo "✅ Enhancement complete!"
echo "Press any key to close..."
read -n 1
rm {prompt_file}
"""
# Save shell script
with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f:
script_file = f.name
f.write(shell_script)
os.chmod(script_file, 0o755)
# Launch in new terminal (macOS specific)
if sys.platform == "darwin":
# Detect which terminal app to use
terminal_app, detection_method = detect_terminal_app()
# Show detection info
if detection_method == "SKILL_SEEKER_TERMINAL":
print(f" Using terminal: {terminal_app} (from SKILL_SEEKER_TERMINAL)")
elif detection_method == "TERM_PROGRAM":
print(f" Using terminal: {terminal_app} (inherited from current terminal)")
elif detection_method.startswith("unknown TERM_PROGRAM"):
print(f"⚠️ {detection_method}")
print(" → Using Terminal.app as fallback")
else:
print(f" Using terminal: {terminal_app} (default)")
try:
subprocess.Popen(["open", "-a", terminal_app, script_file])
except Exception as e:
print(f"⚠️ Error launching {terminal_app}: {e}")
print(f"\nManually run: {script_file}")
return False
else:
print("⚠️ Auto-launch only works on macOS")
print("\nManually run this command in a new terminal:")
print(f" claude '{prompt_file}'")
print("\nThen delete the prompt file:")
print(f" rm '{prompt_file}'")
return False
print("✅ New terminal launched with Claude Code!")
print()
print("📊 Status:")
print(f" - Prompt file: {prompt_file}")
print(f" - Skill directory: {self.skill_dir.absolute()}")
print(f" - SKILL.md will be saved to: {self.skill_md_path.absolute()}")
print(
f" - Original backed up to: {self.skill_md_path.with_suffix('.md.backup').absolute()}"
)
print()
print("⏳ Wait for Claude Code to finish in the other terminal...")
print(" (Usually takes 30-60 seconds)")
print()
print("💡 When done:")
print(f" 1. Check the enhanced SKILL.md: {self.skill_md_path}")
print(
f" 2. If you don't like it, restore: mv {self.skill_md_path.with_suffix('.md.backup')} {self.skill_md_path}"
)
print(f" 3. Package: skill-seekers package {self.skill_dir}/")
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
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)
# Use --dangerously-skip-permissions to bypass ALL permission checks
print(f" Running: claude --dangerously-skip-permissions {prompt_file}")
print(" ⏳ Please wait...")
print(f" Working directory: {self.skill_dir}")
print()
result = subprocess.run(
["claude", "--dangerously-skip-permissions", prompt_file],
capture_output=True,
text=True,
timeout=timeout,
cwd=str(self.skill_dir), # Run from skill directory
)
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 Exception:
pass
return True
else:
print("⚠️ Claude finished but SKILL.md was not updated")
print(f" Initial: mtime={initial_mtime}, size={initial_size}")
print(f" Final: mtime={new_mtime}, size={new_size}")
print(" This might indicate an error during enhancement")
print()
# Show last 20 lines of stdout for debugging
if result.stdout:
print(" Last output from Claude:")
lines = result.stdout.strip().split("\n")[-20:]
for line in lines:
print(f" | {line}")
print()
return False
else:
print("❌ 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 Exception:
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 _run_background(self, headless, timeout):
"""Run enhancement in background thread, return immediately.
Args:
headless: Run headless mode
timeout: Timeout in seconds
Returns:
bool: True if background task started successfully
"""
print(f"\n{'=' * 60}")
print(f"BACKGROUND ENHANCEMENT: {self.skill_dir.name}")
print(f"{'=' * 60}\n")
# Write initial status
self.write_status("pending", "Starting background enhancement...")
def background_worker():
"""Worker function for background thread"""
try:
self.write_status("running", "Enhancement in progress...", progress=0.1)
# Read reference files
references = read_reference_files(
self.skill_dir, max_chars=LOCAL_CONTENT_LIMIT, preview_limit=LOCAL_PREVIEW_LIMIT
)
if not references:
self.write_status("failed", error="No reference files found")
return
total_size = sum(len(c) for c in references.values())
use_summarization = total_size > 30000
self.write_status("running", "Creating enhancement prompt...", progress=0.3)
# Create prompt
prompt = self.create_enhancement_prompt(use_summarization=use_summarization)
if not prompt:
self.write_status("failed", error="Failed to create prompt")
return
# Save prompt to temp file
with tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False, encoding="utf-8"
) as f:
prompt_file = f.name
f.write(prompt)
self.write_status("running", "Running Claude Code enhancement...", progress=0.5)
# Run enhancement
if headless:
# Run headless (subprocess.run - blocking in thread)
result = subprocess.run(
["claude", prompt_file], capture_output=True, text=True, timeout=timeout
)
# Clean up
try:
os.unlink(prompt_file)
except Exception:
pass
if result.returncode == 0:
self.write_status(
"completed", "Enhancement completed successfully!", progress=1.0
)
else:
self.write_status(
"failed", error=f"Claude returned error: {result.returncode}"
)
else:
# Terminal mode in background doesn't make sense
self.write_status("failed", error="Terminal mode not supported in background")
except subprocess.TimeoutExpired:
self.write_status("failed", error=f"Enhancement timed out after {timeout} seconds")
except Exception as e:
self.write_status("failed", error=str(e))
# Start background thread
thread = threading.Thread(target=background_worker, daemon=True)
thread.start()
print("✅ Background enhancement started!")
print()
print("📊 Monitoring:")
print(f" - Status file: {self.status_file}")
print(f" - Check status: cat {self.status_file}")
print(f" - Or use: skill-seekers enhance-status {self.skill_dir}")
print()
print("💡 The enhancement will continue in the background.")
print(" You can close this terminal - the process will keep running.")
print()
return True
def _run_daemon(self, timeout):
"""Run as persistent daemon process with monitoring.
Creates a detached background process that continues running even if parent exits.
Args:
timeout: Timeout in seconds
Returns:
bool: True if daemon started successfully
"""
print(f"\n{'=' * 60}")
print(f"DAEMON MODE: {self.skill_dir.name}")
print(f"{'=' * 60}\n")
# Write initial status
self.write_status("pending", "Starting daemon process...")
print("🔧 Creating daemon process...")
# Create Python script for daemon
daemon_script = f'''#!/usr/bin/env python3
import os
import sys
import time
import subprocess
import tempfile
import json
from pathlib import Path
from datetime import datetime
skill_dir = Path("{self.skill_dir}")
status_file = skill_dir / ".enhancement_status.json"
skill_md_path = skill_dir / "SKILL.md"
def write_status(status, message="", progress=0.0, error=None):
status_data = {{
"status": status,
"message": message,
"progress": progress,
"timestamp": datetime.now().isoformat(),
"skill_dir": str(skill_dir),
"error": error,
"pid": os.getpid()
}}
status_file.write_text(json.dumps(status_data, indent=2), encoding='utf-8')
try:
write_status("running", "Daemon started, loading references...", progress=0.1)
# Import enhancement logic
sys.path.insert(0, "{os.path.dirname(os.path.dirname(os.path.abspath(__file__)))}")
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer("{self.skill_dir}")
# Create prompt
write_status("running", "Creating enhancement prompt...", progress=0.3)
prompt = enhancer.create_enhancement_prompt(use_summarization=True)
if not prompt:
write_status("failed", error="Failed to create prompt")
sys.exit(1)
# Save prompt
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
prompt_file = f.name
f.write(prompt)
write_status("running", "Running Claude Code...", progress=0.5)
# Run Claude
result = subprocess.run(
['claude', prompt_file],
capture_output=True,
text=True,
timeout={timeout}
)
# Clean up
try:
os.unlink(prompt_file)
except Exception:
pass
if result.returncode == 0:
write_status("completed", "Enhancement completed successfully!", progress=1.0)
sys.exit(0)
else:
write_status("failed", error=f"Claude returned error: {{result.returncode}}")
sys.exit(1)
except subprocess.TimeoutExpired:
write_status("failed", error=f"Enhancement timed out after {timeout} seconds")
sys.exit(1)
except Exception as e:
write_status("failed", error=str(e))
sys.exit(1)
'''
# Save daemon script
daemon_script_path = self.skill_dir / ".enhancement_daemon.py"
daemon_script_path.write_text(daemon_script, encoding="utf-8")
daemon_script_path.chmod(0o755)
# Start daemon process (fully detached)
try:
# Use nohup to detach from terminal
log_file = self.skill_dir / ".enhancement_daemon.log"
if self.force:
# Force mode: No output, fully silent
subprocess.Popen(
["nohup", "python3", str(daemon_script_path)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
else:
# Normal mode: Log to file
with open(log_file, "w") as log:
subprocess.Popen(
["nohup", "python3", str(daemon_script_path)],
stdout=log,
stderr=log,
start_new_session=True,
)
# Give daemon time to start
time.sleep(1)
# Read status to verify it started
status = self.read_status()
if status and status.get("status") in ["pending", "running"]:
print("✅ Daemon process started successfully!")
print()
print("📊 Monitoring:")
print(f" - Status file: {self.status_file}")
print(f" - Log file: {log_file}")
print(f" - PID: {status.get('pid', 'unknown')}")
print()
print("💡 Commands:")
print(f" - Check status: cat {self.status_file}")
print(f" - View logs: tail -f {log_file}")
print(f" - Or use: skill-seekers enhance-status {self.skill_dir}")
print()
print("🔥 The daemon will continue running even if you close this terminal!")
print()
return True
else:
print("❌ Daemon failed to start")
return False
except Exception as e:
print(f"❌ Failed to start daemon: {e}")
return False
def main():
import argparse
parser = argparse.ArgumentParser(
description="Enhance a skill with Claude Code (local)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Headless mode (default - runs in foreground, waits for completion, auto-force)
skill-seekers enhance output/react/
# Background mode (runs in background, returns immediately)
skill-seekers enhance output/react/ --background
# Daemon mode (persistent background process, fully detached)
skill-seekers enhance output/react/ --daemon
# Disable force mode (ask for confirmations)
skill-seekers enhance output/react/ --no-force
# Interactive mode (opens terminal window)
skill-seekers enhance output/react/ --interactive-enhancement
# Custom timeout
skill-seekers enhance output/react/ --timeout 1200
Mode Comparison:
- headless: Runs claude CLI directly, BLOCKS until done (default)
- background: Runs in background thread, returns immediately
- daemon: Fully detached process, continues after parent exits
- terminal: Opens new terminal window (interactive)
Force Mode (Default ON):
By default, all modes skip confirmations (auto-yes).
Use --no-force to enable confirmation prompts.
""",
)
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(
"--background",
action="store_true",
help="Run in background and return immediately (non-blocking)",
)
parser.add_argument(
"--daemon", action="store_true", help="Run as persistent daemon process (fully detached)"
)
parser.add_argument(
"--no-force",
action="store_true",
help="Disable force mode: enable confirmation prompts (default: force mode ON)",
)
parser.add_argument(
"--timeout",
type=int,
default=600,
help="Timeout in seconds for headless mode (default: 600 = 10 minutes)",
)
args = parser.parse_args()
# Validate mutually exclusive options
mode_count = sum([args.interactive_enhancement, args.background, args.daemon])
if mode_count > 1:
print(
"❌ Error: --interactive-enhancement, --background, and --daemon are mutually exclusive"
)
print(" Choose only one mode")
sys.exit(1)
# Run enhancement
# Force mode is ON by default, use --no-force to disable
enhancer = LocalSkillEnhancer(args.skill_directory, force=not args.no_force)
headless = not args.interactive_enhancement # Invert: default is headless
success = enhancer.run(
headless=headless, timeout=args.timeout, background=args.background, daemon=args.daemon
)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()