fix: Rewrite config_enhancer LOCAL mode to use Claude CLI properly
The previous implementation had a design flaw - it ran `claude prompt_file` and expected Claude to magically create a JSON file in a temp directory. This never worked because Claude CLI is interactive and doesn't auto-save. Changes: - Use `--dangerously-skip-permissions` flag to bypass permission prompts - Create a dedicated temp directory for each enhancement session - Embed absolute output file path in the prompt so Claude knows where to write - Run Claude from the temp directory as working_dir - Improved prompt with explicit Write tool instruction - Better error handling and logging (file not found, JSON parse errors) - Show settings preview in prompt for better AI context The LOCAL mode now follows the same pattern as enhance_skill_local.py which works correctly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -256,126 +256,184 @@ Focus on actionable insights that help developers understand and improve their c
|
|||||||
def _enhance_via_local(self, result: dict) -> dict:
|
def _enhance_via_local(self, result: dict) -> dict:
|
||||||
"""Enhance configs using Claude Code CLI"""
|
"""Enhance configs using Claude Code CLI"""
|
||||||
try:
|
try:
|
||||||
# Create temporary prompt file
|
# Create a temporary directory for this enhancement session
|
||||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
|
with tempfile.TemporaryDirectory(prefix="config_enhance_") as temp_dir:
|
||||||
prompt_file = Path(f.name)
|
temp_path = Path(temp_dir)
|
||||||
f.write(self._create_local_prompt(result))
|
|
||||||
|
|
||||||
# Create output file path
|
# Define output file path (absolute path that Claude will write to)
|
||||||
output_file = prompt_file.parent / f"{prompt_file.stem}_enhanced.json"
|
output_file = temp_path / "config_enhancement.json"
|
||||||
|
|
||||||
logger.info("🖥️ Launching Claude Code CLI for config analysis...")
|
# Create prompt file with the output path embedded
|
||||||
logger.info("⏱️ This will take 30-60 seconds...")
|
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
|
logger.info("🖥️ Launching Claude Code CLI for config analysis...")
|
||||||
result_data = self._run_claude_cli(prompt_file, output_file)
|
logger.info("⏱️ This will take 30-60 seconds...")
|
||||||
|
|
||||||
# Clean up
|
# Run Claude Code CLI
|
||||||
prompt_file.unlink()
|
result_data = self._run_claude_cli(prompt_file, output_file, temp_path)
|
||||||
if output_file.exists():
|
|
||||||
output_file.unlink()
|
|
||||||
|
|
||||||
if result_data:
|
if result_data:
|
||||||
# Merge LOCAL enhancements
|
# Merge LOCAL enhancements
|
||||||
result["ai_enhancements"] = result_data
|
result["ai_enhancements"] = result_data
|
||||||
logger.info("✅ LOCAL enhancement complete")
|
logger.info("✅ LOCAL enhancement complete")
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
logger.warning("⚠️ LOCAL enhancement produced no results")
|
logger.warning("⚠️ LOCAL enhancement produced no results")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ LOCAL enhancement failed: {e}")
|
logger.error(f"❌ LOCAL enhancement failed: {e}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _create_local_prompt(self, result: dict) -> str:
|
def _create_local_prompt(self, result: dict, output_file: Path) -> str:
|
||||||
"""Create prompt file for Claude Code CLI"""
|
"""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", [])
|
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 = []
|
config_data = []
|
||||||
for cf in config_files[:10]:
|
for cf in config_files[:15]:
|
||||||
# Support both "type" (from config_extractor) and "config_type" (legacy)
|
# Support both "type" (from config_extractor) and "config_type" (legacy)
|
||||||
config_type = cf.get("type", cf.get("config_type", "unknown"))
|
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"""
|
config_data.append(f"""
|
||||||
### {cf["relative_path"]} ({config_type})
|
### {cf["relative_path"]} ({config_type})
|
||||||
- Purpose: {cf["purpose"]}
|
- Purpose: {cf["purpose"]}
|
||||||
- Patterns: {", ".join(cf.get("patterns", []))}
|
- Patterns: {", ".join(cf.get("patterns", [])) or "none detected"}
|
||||||
- Settings count: {len(cf.get("settings", []))}
|
- Settings: {len(cf.get("settings", []))} total
|
||||||
|
{chr(10).join(settings_preview) if settings_preview else " (no settings)"}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
prompt = f"""# Configuration Analysis Task
|
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)}
|
{chr(10).join(config_data)}
|
||||||
|
|
||||||
## Your Task
|
## 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
|
```json
|
||||||
{{
|
{{
|
||||||
"file_enhancements": [
|
"file_enhancements": [
|
||||||
{{
|
{{
|
||||||
"file_path": "path/to/file",
|
"file_path": "relative/path/to/config.json",
|
||||||
"explanation": "What this config does",
|
"explanation": "Brief explanation of what this config file does",
|
||||||
"best_practice": "Suggested improvements",
|
"best_practice": "Suggested improvement or 'None'",
|
||||||
"security_concern": "Security issues (if any)",
|
"security_concern": "Security issue if any, or 'None'",
|
||||||
"migration_suggestion": "Consolidation opportunities",
|
"migration_suggestion": "Consolidation opportunity or 'None'",
|
||||||
"context": "Pattern explanation"
|
"context": "What pattern or purpose this serves"
|
||||||
}}
|
}}
|
||||||
],
|
],
|
||||||
"overall_insights": {{
|
"overall_insights": {{
|
||||||
"config_count": {len(config_files)},
|
"config_count": {len(config_files)},
|
||||||
"security_issues_found": 0,
|
"security_issues_found": 0,
|
||||||
"consolidation_opportunities": [],
|
"consolidation_opportunities": ["List of suggestions"],
|
||||||
"recommended_actions": []
|
"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. Use the Write tool to create the JSON file at: {output_file}
|
||||||
1. Explain what each config does
|
2. Include an enhancement entry for each config file shown above
|
||||||
2. Suggest best practices
|
3. Focus on actionable insights:
|
||||||
3. Identify security concerns (hardcoded secrets, exposed credentials)
|
- Explain what each config does in 1-2 sentences
|
||||||
4. Suggest consolidation opportunities
|
- Identify any hardcoded secrets or security issues
|
||||||
5. Explain the detected patterns
|
- 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
|
return prompt
|
||||||
|
|
||||||
def _run_claude_cli(self, prompt_file: Path, _output_file: Path) -> dict | None:
|
def _run_claude_cli(
|
||||||
"""Run Claude Code CLI and wait for completion"""
|
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:
|
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(
|
result = subprocess.run(
|
||||||
["claude", str(prompt_file)],
|
["claude", "--dangerously-skip-permissions", str(prompt_file)],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=300, # 5 minute timeout
|
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:
|
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
|
return None
|
||||||
|
|
||||||
# Try to find output file (Claude might save it with different name)
|
# Check if the expected output file was created
|
||||||
# Look for JSON files created in the last minute
|
if output_file.exists():
|
||||||
import time
|
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()
|
current_time = time.time()
|
||||||
potential_files = []
|
potential_files = []
|
||||||
|
|
||||||
for json_file in prompt_file.parent.glob("*.json"):
|
for json_file in working_dir.glob("*.json"):
|
||||||
if current_time - json_file.stat().st_mtime < 120: # Created in last 2 minutes
|
# Check if created recently (within last 2 minutes)
|
||||||
|
if current_time - json_file.stat().st_mtime < 120:
|
||||||
potential_files.append(json_file)
|
potential_files.append(json_file)
|
||||||
|
|
||||||
# Try to load the most recent JSON file
|
# 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):
|
for json_file in sorted(
|
||||||
|
potential_files, key=lambda f: f.stat().st_mtime, reverse=True
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
with open(json_file) as f:
|
with open(json_file) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
@@ -386,11 +444,17 @@ Focus on actionable insights:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
logger.warning("⚠️ Could not find enhancement output file")
|
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
|
return None
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
logger.error("❌ Claude CLI timeout (5 minutes)")
|
logger.error("❌ Claude CLI timeout (5 minutes)")
|
||||||
return None
|
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:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error running Claude CLI: {e}")
|
logger.error(f"❌ Error running Claude CLI: {e}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user