#!/usr/bin/env python3 """ SKILL.md Enhancement Script (Local - Using CLI Coding Agents) Uses a local coding agent CLI (Claude Code, Codex CLI, Copilot CLI, OpenCode CLI) to enhance SKILL.md, then reports back. No API key needed. 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 # Disable force mode (enable confirmations) skill-seekers enhance output/react/ --no-force # Daemon mode (persistent background process) skill-seekers enhance output/react/ --daemon # Interactive terminal mode skill-seekers enhance output/react/ --interactive-enhancement # Use a different local coding agent skill-seekers enhance output/react/ --agent codex skill-seekers enhance output/react/ --agent copilot skill-seekers enhance output/react/ --agent opencode # Custom agent command (advanced) skill-seekers enhance output/react/ --agent custom --agent-cmd "my-agent --prompt {prompt_file}" Modes: - headless: Runs local CLI directly, BLOCKS until done (default) - background: Runs local 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 shlex import shutil 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__)))) import contextlib 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" AGENT_PRESETS = { "claude": { "display_name": "Claude Code", "command": ["claude", "{prompt_file}"], "supports_skip_permissions": True, }, "codex": { "display_name": "OpenAI Codex CLI", "command": ["codex", "exec", "--full-auto", "--skip-git-repo-check", "-"], "supports_skip_permissions": False, }, "copilot": { "display_name": "GitHub Copilot CLI", "command": ["gh", "copilot", "chat"], "supports_skip_permissions": False, }, "opencode": { "display_name": "OpenCode CLI", "command": ["opencode"], "supports_skip_permissions": False, }, } def _normalize_agent_name(agent_name: str) -> str: if not agent_name: return "claude" normalized = agent_name.strip().lower() aliases = { "claude-code": "claude", "claude_code": "claude", "codex-cli": "codex", "copilot-cli": "copilot", "open-code": "opencode", "open_code": "opencode", } return aliases.get(normalized, normalized) class LocalSkillEnhancer: def __init__(self, skill_dir, force=True, agent=None, agent_cmd=None): """Initialize enhancer. Args: skill_dir: Path to skill directory force: If True, skip all confirmations (default: True, use --no-force to disable) agent: Local coding agent identifier (claude, codex, copilot, opencode, custom) agent_cmd: Override command template (use {prompt_file} placeholder or stdin) """ 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" self.agent, self.agent_cmd, self.agent_display = self._resolve_agent(agent, agent_cmd) def _validate_custom_command(self, cmd_template: str) -> None: """Validate custom command template for basic safety and executability.""" dangerous_chars = [";", "&", "|", "$", "`", "\n", "\r"] if any(char in cmd_template for char in dangerous_chars): raise ValueError( f"Custom command contains dangerous shell characters. Command: {cmd_template}" ) try: cmd_parts = shlex.split(cmd_template) except ValueError as exc: raise ValueError(f"Invalid command template: {exc}") from exc if not cmd_parts: raise ValueError("Custom command is empty.") executable = cmd_parts[0] if "/" in executable: executable_path = Path(executable) if not executable_path.is_file(): raise ValueError(f"Custom command executable not found: {executable}") else: if not shutil.which(executable): raise ValueError(f"Executable '{executable}' not found in PATH") def _resolve_agent(self, agent, agent_cmd): env_agent = os.environ.get("SKILL_SEEKER_AGENT", "").strip() env_cmd = os.environ.get("SKILL_SEEKER_AGENT_CMD", "").strip() agent_name = _normalize_agent_name(agent or env_agent or "claude") cmd_override = agent_cmd or env_cmd or None if agent_name == "custom": if not cmd_override: raise ValueError( "Custom agent requires --agent-cmd or SKILL_SEEKER_AGENT_CMD to be set." ) self._validate_custom_command(cmd_override) display_name = "Custom CLI Agent" return agent_name, cmd_override, display_name if agent_name not in AGENT_PRESETS: available = ", ".join(sorted(AGENT_PRESETS.keys())) raise ValueError( f"Unknown agent '{agent_name}'. Choose one of: {available} or use --agent custom." ) display_name = AGENT_PRESETS[agent_name]["display_name"] return agent_name, cmd_override, display_name def _build_agent_command(self, prompt_file, include_permissions_flag): if self.agent_cmd: cmd_parts = shlex.split(self.agent_cmd) supports_skip_permissions = False else: preset = AGENT_PRESETS[self.agent] cmd_parts = list(preset["command"]) supports_skip_permissions = preset.get("supports_skip_permissions", False) if ( include_permissions_flag and supports_skip_permissions and "--dangerously-skip-permissions" not in cmd_parts ): cmd_parts.insert(1, "--dangerously-skip-permissions") uses_prompt_file = False for idx, arg in enumerate(cmd_parts): if "{prompt_file}" in arg: cmd_parts[idx] = arg.replace("{prompt_file}", prompt_file) uses_prompt_file = True return cmd_parts, uses_prompt_file def _format_agent_command(self, prompt_file, include_permissions_flag): cmd_parts, uses_prompt_file = self._build_agent_command( prompt_file, include_permissions_flag ) cmd_str = shlex.join(cmd_parts) if uses_prompt_file: return cmd_str return f"cat {shlex.quote(prompt_file)} | {cmd_str}" def _run_agent_command(self, prompt_file, timeout, include_permissions_flag, quiet=False): cmd_parts, uses_prompt_file = self._build_agent_command( prompt_file, include_permissions_flag ) if not quiet: cmd_display = self._format_agent_command(prompt_file, include_permissions_flag) print(f" Command: {cmd_display}") try: if uses_prompt_file: return ( subprocess.run( cmd_parts, capture_output=True, text=True, timeout=timeout, cwd=str(self.skill_dir), ), None, ) prompt_text = Path(prompt_file).read_text(encoding="utf-8") return ( subprocess.run( cmd_parts, capture_output=True, text=True, timeout=timeout, cwd=str(self.skill_dir), input=prompt_text, ), None, ) except FileNotFoundError: return None, f"Command not found: {cmd_parts[0]}" except Exception as e: return None, str(e) 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 a local coding agent 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 coding agent 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 reduce input size for local coding agent CLIs. 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 local agent 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"Agent: {self.agent_display}") 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") if self.agent == "claude": print(" šŸ’” Claude CLI limit: ~30,000-40,000 characters") else: print(" šŸ’” Local CLI agents often have input limits; summarizing to be safe") 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)") if self.agent == "claude": print(" āœ“ Ready for Claude CLI (within safe limits)") else: print(" āœ“ Ready for local CLI (within safe limits)") print() else: print(f" āœ“ Prompt saved ({len(prompt):,} characters)\n") # Headless mode: Run local agent directly without opening terminal if headless: return self._run_headless(prompt_file, timeout) # Terminal mode: Launch local agent in new terminal print(f"šŸš€ Launching {self.agent_display} in new terminal...") print(" This will:") print(" 1. Open a new terminal window") print(" 2. Run the local coding agent with the enhancement task") print(" 3. The agent 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 command_line = self._format_agent_command(prompt_file, include_permissions_flag=False) shell_script = f"""#!/bin/bash {command_line} 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" {self._format_agent_command(prompt_file, include_permissions_flag=False)}") print("\nThen delete the prompt file:") print(f" rm '{prompt_file}'") return False print(f"āœ… New terminal launched with {self.agent_display}!") 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 the local agent 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 local agent 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(f"✨ Running {self.agent_display} 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 local agent command directly (this WAITS for completion) print(" ā³ Please wait...") print(f" Working directory: {self.skill_dir}") print() result, error = self._run_agent_command( prompt_file, timeout, include_permissions_flag=True ) if error: print(f"āŒ {error}") with contextlib.suppress(Exception): os.unlink(prompt_file) return False 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 with contextlib.suppress(Exception): os.unlink(prompt_file) return True else: print("āš ļø Agent 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 agent:") 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"āŒ {self.agent_display} 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(" - Agent 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 with contextlib.suppress(Exception): os.unlink(prompt_file) return False except FileNotFoundError: print(f"āŒ '{self._build_agent_command(prompt_file, True)[0][0]}' command not found") print() print(" Make sure your local coding agent CLI is installed and on PATH.") 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(meta["size"] for meta 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", f"Running {self.agent_display} enhancement...", progress=0.5 ) # Run enhancement if headless: # Run headless (subprocess.run - blocking in thread) result, error = self._run_agent_command( prompt_file, timeout, include_permissions_flag=True, quiet=True ) # Clean up with contextlib.suppress(Exception): os.unlink(prompt_file) if error: self.write_status("failed", error=error) elif result.returncode == 0: self.write_status( "completed", "Enhancement completed successfully!", progress=1.0 ) else: self.write_status( "failed", error=f"Agent 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}", agent="{self.agent}", agent_cmd={repr(self.agent_cmd)} ) # 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 local agent...", progress=0.5) # Run local agent result, error = enhancer._run_agent_command( prompt_file, timeout={timeout}, include_permissions_flag=True, quiet=True ) # Clean up try: os.unlink(prompt_file) except Exception: pass if error: write_status("failed", error=error) sys.exit(1) if result.returncode == 0: write_status("completed", "Enhancement completed successfully!", progress=1.0) sys.exit(0) else: write_status("failed", error=f"Agent 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 a local coding agent (no API key)", 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 local agent 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( "--agent", choices=sorted(list(AGENT_PRESETS.keys()) + ["custom"]), help="Local coding agent to use (default: claude or SKILL_SEEKER_AGENT)", ) parser.add_argument( "--agent-cmd", help=( "Override agent command template. Use {prompt_file} placeholder or omit to use stdin. " "Can also be set via SKILL_SEEKER_AGENT_CMD." ), ) 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 try: enhancer = LocalSkillEnhancer( args.skill_directory, force=not args.no_force, agent=args.agent, agent_cmd=args.agent_cmd, ) except ValueError as e: print(f"āŒ Error: {e}") sys.exit(1) 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()