Add multi-agent local enhancement support

This commit is contained in:
Robert Dean
2026-01-30 12:39:40 +01:00
parent 4e8ad835ed
commit 0654ca5bcc
2 changed files with 263 additions and 60 deletions

View File

@@ -1,8 +1,8 @@
#!/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!
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)
@@ -11,8 +11,8 @@ Usage:
# 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
# Disable force mode (enable confirmations)
skill-seekers enhance output/react/ --no-force
# Daemon mode (persistent background process)
skill-seekers enhance output/react/ --daemon
@@ -20,9 +20,17 @@ Usage:
# 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 claude CLI directly, BLOCKS until done (default)
- background: Runs claude CLI in background, returns immediately
- 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)
@@ -38,6 +46,7 @@ Terminal Selection:
import json
import os
import shlex
import subprocess
import sys
import tempfile
@@ -104,19 +113,157 @@ def detect_terminal_app():
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):
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 _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."
)
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.
@@ -190,7 +337,7 @@ class LocalSkillEnhancer:
return "\n".join(result)
def create_enhancement_prompt(self, use_summarization=False, summarization_ratio=0.3):
"""Create the prompt file for Claude Code
"""Create the prompt file for a local coding agent
Args:
use_summarization: If True, apply smart summarization to reduce size
@@ -415,7 +562,7 @@ CRITICAL INSTRUCTIONS:
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.
Even if running from within another coding agent session, this modification is ALLOWED and EXPECTED.
VERIFICATION:
After writing, the file SKILL.md should:
@@ -464,7 +611,7 @@ After writing, the file SKILL.md should:
"""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.
to reduce input size for local coding agent CLIs.
Smart summarization strategy:
- Keeps first 20% (introduction/overview)
@@ -473,7 +620,7 @@ After writing, the file SKILL.md should:
- Reduces to ~30% of original size
Args:
headless: If True, run claude directly without opening terminal (default: True)
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)
@@ -490,6 +637,7 @@ After writing, the file SKILL.md should:
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
@@ -517,7 +665,10 @@ After writing, the file SKILL.md should:
if use_summarization:
print("⚠️ LARGE SKILL DETECTED")
print(f" 📊 Reference content: {total_size:,} characters")
print(" 💡 Claude CLI limit: ~30,000-40,000 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")
@@ -542,27 +693,31 @@ After writing, the file SKILL.md should:
if use_summarization:
print(f" ✓ Prompt created and optimized ({len(prompt):,} characters)")
print(" ✓ Ready for Claude CLI (within safe limits)")
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 claude directly without opening terminal
# Headless mode: Run local agent 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...")
# 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 Claude Code with the enhancement task")
print(" 3. Claude will read the docs and enhance SKILL.md")
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
claude {prompt_file}
{command_line}
echo ""
echo "✅ Enhancement complete!"
echo "Press any key to close..."
@@ -602,12 +757,12 @@ rm {prompt_file}
else:
print("⚠️ Auto-launch only works on macOS")
print("\nManually run this command in a new terminal:")
print(f" claude '{prompt_file}'")
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("✅ New terminal launched with Claude Code!")
print(f"✅ New terminal launched with {self.agent_display}!")
print()
print("📊 Status:")
print(f" - Prompt file: {prompt_file}")
@@ -617,7 +772,7 @@ rm {prompt_file}
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("⏳ Wait for the local agent to finish in the other terminal...")
print(" (Usually takes 30-60 seconds)")
print()
print("💡 When done:")
@@ -630,7 +785,7 @@ rm {prompt_file}
return True
def _run_headless(self, prompt_file, timeout):
"""Run Claude enhancement in headless mode (no terminal window)
"""Run local agent enhancement in headless mode (no terminal window)
Args:
prompt_file: Path to prompt file
@@ -641,7 +796,7 @@ rm {prompt_file}
"""
import time
print("✨ Running Claude Code enhancement (headless mode)...")
print(f"✨ Running {self.agent_display} enhancement (headless mode)...")
print(f" Timeout: {timeout} seconds ({timeout // 60} minutes)")
print()
@@ -653,21 +808,21 @@ rm {prompt_file}
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}")
# Run local agent command directly (this WAITS for completion)
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
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
@@ -688,14 +843,14 @@ rm {prompt_file}
return True
else:
print("⚠️ Claude finished but SKILL.md was not updated")
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 Claude:")
print(" Last output from agent:")
lines = result.stdout.strip().split("\n")[-20:]
for line in lines:
print(f" | {line}")
@@ -705,7 +860,9 @@ rm {prompt_file}
print("❌ SKILL.md not found after enhancement")
return False
else:
print(f"❌ Claude Code returned error (exit code: {result.returncode})")
print(
f"{self.agent_display} returned error (exit code: {result.returncode})"
)
if result.stderr:
print(f" Error: {result.stderr[:200]}")
return False
@@ -717,7 +874,7 @@ rm {prompt_file}
print()
print(" Possible reasons:")
print(" - Skill is very large (many references)")
print(" - Claude is taking longer than usual")
print(" - Agent is taking longer than usual")
print(" - Network issues")
print()
print(" Try:")
@@ -732,10 +889,9 @@ rm {prompt_file}
return False
except FileNotFoundError:
print("'claude' command not found")
print(f"'{self._build_agent_command(prompt_file, True)[0][0]}' command not found")
print()
print(" Make sure Claude Code CLI is installed:")
print(" See: https://docs.claude.com/claude-code")
print(" Make sure your local coding agent CLI is installed and on PATH.")
print()
print(" Try terminal mode instead: --interactive-enhancement")
@@ -776,7 +932,7 @@ rm {prompt_file}
self.write_status("failed", error="No reference files found")
return
total_size = sum(len(c) for c in references.values())
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)
@@ -794,26 +950,30 @@ rm {prompt_file}
prompt_file = f.name
f.write(prompt)
self.write_status("running", "Running Claude Code enhancement...", progress=0.5)
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 = subprocess.run(
["claude", prompt_file], capture_output=True, text=True, timeout=timeout
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 result.returncode == 0:
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"Claude returned error: {result.returncode}"
"failed", error=f"Agent returned error: {result.returncode}"
)
else:
# Terminal mode in background doesn't make sense
@@ -895,7 +1055,11 @@ try:
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}")
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)
@@ -910,14 +1074,14 @@ try:
prompt_file = f.name
f.write(prompt)
write_status("running", "Running Claude Code...", progress=0.5)
write_status("running", "Running local agent...", progress=0.5)
# Run Claude
result = subprocess.run(
['claude', prompt_file],
capture_output=True,
text=True,
timeout={timeout}
# Run local agent
result, error = enhancer._run_agent_command(
prompt_file,
timeout={timeout},
include_permissions_flag=True,
quiet=True
)
# Clean up
@@ -926,11 +1090,14 @@ try:
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"Claude returned error: {{result.returncode}}")
write_status("failed", error=f"Agent returned error: {{result.returncode}}")
sys.exit(1)
except subprocess.TimeoutExpired:
@@ -1005,7 +1172,7 @@ def main():
import argparse
parser = argparse.ArgumentParser(
description="Enhance a skill with Claude Code (local)",
description="Enhance a skill with a local coding agent (no API key)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
@@ -1028,7 +1195,7 @@ Examples:
skill-seekers enhance output/react/ --timeout 1200
Mode Comparison:
- headless: Runs claude CLI directly, BLOCKS until done (default)
- 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)
@@ -1041,6 +1208,20 @@ Force Mode (Default ON):
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",
@@ -1083,7 +1264,16 @@ Force Mode (Default ON):
# Run enhancement
# Force mode is ON by default, use --no-force to disable
enhancer = LocalSkillEnhancer(args.skill_directory, force=not args.no_force)
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

View File

@@ -170,9 +170,18 @@ For more information: https://github.com/yusufkaraaslan/Skill_Seekers
enhance_parser = subparsers.add_parser(
"enhance",
help="AI-powered enhancement (local, no API key)",
description="Enhance SKILL.md using Claude Code (local)",
description="Enhance SKILL.md using a local coding agent",
)
enhance_parser.add_argument("skill_directory", help="Skill directory path")
enhance_parser.add_argument(
"--agent",
choices=["claude", "codex", "copilot", "opencode", "custom"],
help="Local coding agent to use (default: claude or SKILL_SEEKER_AGENT)",
)
enhance_parser.add_argument(
"--agent-cmd",
help="Override agent command template (use {prompt_file} or stdin).",
)
enhance_parser.add_argument("--background", action="store_true", help="Run in background")
enhance_parser.add_argument("--daemon", action="store_true", help="Run as daemon")
enhance_parser.add_argument(
@@ -486,6 +495,10 @@ def main(argv: list[str] | None = None) -> int:
from skill_seekers.cli.enhance_skill_local import main as enhance_main
sys.argv = ["enhance_skill_local.py", args.skill_directory]
if args.agent:
sys.argv.extend(["--agent", args.agent])
if args.agent_cmd:
sys.argv.extend(["--agent-cmd", args.agent_cmd])
if args.background:
sys.argv.append("--background")
if args.daemon: