diff --git a/README.md b/README.md index 544a079..ba1566e 100644 --- a/README.md +++ b/README.md @@ -372,6 +372,22 @@ uv run elf0 agent specs/basic/reasoning_structured_v1.yaml --prompt "Find securi uv run elf0 agent specs/basic/reasoning_structured_v1.yaml --prompt "Compare @old_version.py and @new_version.py" ``` +### Claude Code Integration +Elf0 includes powerful integration with the Claude Code SDK, providing AI-assisted development through Claude's advanced code understanding and generation capabilities: + +```bash +# Deep code analysis with Claude Code +uv run elf0 agent specs/code/claude_code_review.yaml --prompt "Analyse the following file and tell me how to improve it @src/elf0/cli.py" + +# Generate code with Claude Code +uv run elf0 agent specs/examples/claude_code_example.yaml --prompt "Create a FastAPI application with user authentication" + +# Get architectural guidance +uv run elf0 agent specs/code/claude_code_review.yaml --prompt "Review @src/ and suggest architectural improvements" +``` + +**Why use Claude Code?** Claude Code provides AI-assisted development through a Python SDK, enabling streamlined interaction with Claude for code-related tasks. It offers tool-based workflows like file reading/writing, automated development tasks, and comprehensive error handling - perfect for integrating AI assistance directly into your development process. + ### Workflow Management ```bash # Create new workflows with AI diff --git a/pyproject.toml b/pyproject.toml index 1c857fa..6397471 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,16 +30,16 @@ dependencies = [ "anthropic>=0.51.0", "mcp[cli]>=1.9.1", "prompt-toolkit>=3.0.51", - "claude-code-sdk>=0.0.10", "markdown>=3.8.1", + "claude-code-sdk>=0.0.14", ] [project.urls] -Homepage = "https://github.com/emson/elf" -Documentation = "https://github.com/emson/elf#readme" -Repository = "https://github.com/emson/elf.git" -Issues = "https://github.com/emson/elf/issues" -Changelog = "https://github.com/emson/elf/releases" +Homepage = "https://github.com/emson/elf0" +Documentation = "https://github.com/emson/elf0#readme" +Repository = "https://github.com/emson/elf0.git" +Issues = "https://github.com/emson/elf0/issues" +Changelog = "https://github.com/emson/elf0/releases" [project.scripts] elf0 = "elf0.cli:app" diff --git a/specs/code/claude_code_review.yaml b/specs/code/claude_code_review.yaml new file mode 100644 index 0000000..8e342d4 --- /dev/null +++ b/specs/code/claude_code_review.yaml @@ -0,0 +1,43 @@ +version: "0.1" +description: "Claude Code workflow for analyzing files or directories and suggesting code improvements" +runtime: "langgraph" + +llms: + claude_reviewer: + type: anthropic + model_name: claude-sonnet-4-20250514 + temperature: 0.1 + params: + max_tokens: 4096 + +workflow: + type: sequential + nodes: + - id: code_review + kind: claude_code + config: + task: "analyze_code" + prompt: | + You are a Claude Code coding assistant. You are excellent at analysing the code and suggesting how it can be improved. + You try and create minimal but flexible code improvements, you follow best practices, but actively avoid code bloat and complextity. Instead you favour software design patterns, and composite functions made up of well defined discreet sub-functions. You also think outside the box, and consider edge cases, in order to provide the best improvements and analysis to this code. + + Please analyze the provided code or directory and provide: + + 1. **Code Quality Assessment**: Evaluate structure, readability, and maintainability + 2. **Best Practice Recommendations**: Identify areas where coding standards can be improved + 3. **Design Pattern Opportunities**: Suggest appropriate design patterns that could simplify the code + 4. **Function Decomposition**: Recommend how complex functions can be broken into smaller, well-defined sub-functions + 5. **Edge Case Analysis**: Identify potential edge cases and error scenarios not currently handled + 6. **Security Considerations**: Highlight any security vulnerabilities or concerns + 7. **Performance Optimizations**: Suggest performance improvements where applicable + 8. **Specific Code Improvements**: Provide concrete examples of how to improve the code + + Focus on creating minimal, flexible, and maintainable solutions. Prioritize clarity and simplicity over complexity. + + Code to analyze: {input} + output_format: "text" + tools: ["filesystem"] + temperature: 0.1 + stop: true + + edges: [] \ No newline at end of file diff --git a/src/elf0/core/nodes/claude_code_node.py b/src/elf0/core/nodes/claude_code_node.py index 2208d17..ef2eb79 100644 --- a/src/elf0/core/nodes/claude_code_node.py +++ b/src/elf0/core/nodes/claude_code_node.py @@ -9,6 +9,10 @@ # Type annotations for conditionally imported SDK components claude_code_sdk: Callable | None = None ClaudeCodeOptions: type | None = None +AssistantMessage: type | None = None +TextBlock: type | None = None +ToolUseBlock: type | None = None +ToolResultBlock: type | None = None class ClaudeCodeError(Exception): """Base exception for Claude Code related errors.""" @@ -55,17 +59,24 @@ def __init__(self, config: dict[str, Any]): msg = "Claude Code node requires a 'prompt' in config" raise ClaudeCodeError(msg) - # Import Claude Code SDK (will be handled gracefully if not installed or buggy) + # Import Claude Code SDK (will be handled gracefully if not installed) try: - global claude_code_sdk, ClaudeCodeOptions + global claude_code_sdk, ClaudeCodeOptions, AssistantMessage, TextBlock, ToolUseBlock, ToolResultBlock + from claude_code_sdk import AssistantMessage as SDKAssistantMessage from claude_code_sdk import ClaudeCodeOptions as SDKClaudeCodeOptions + from claude_code_sdk import TextBlock as SDKTextBlock + from claude_code_sdk import ToolResultBlock as SDKToolResultBlock + from claude_code_sdk import ToolUseBlock as SDKToolUseBlock from claude_code_sdk import query as claude_code_query + claude_code_sdk = claude_code_query ClaudeCodeOptions = SDKClaudeCodeOptions + AssistantMessage = SDKAssistantMessage + TextBlock = SDKTextBlock + ToolUseBlock = SDKToolUseBlock + ToolResultBlock = SDKToolResultBlock self.sdk_available = True - # Note: SDK v0.0.10 has compatibility issues, falling back to mock for demonstration - logger.warning("Claude Code SDK has known compatibility issues (v0.0.10), using mock responses for demonstration") - self.sdk_available = False # Force mock mode due to SDK bugs + logger.info("Claude Code SDK v0.0.14+ loaded successfully") except ImportError: logger.warning("Claude Code SDK not available. Install with: pip install claude-code-sdk") self.sdk_available = False @@ -73,7 +84,7 @@ def __init__(self, config: dict[str, Any]): async def execute(self, state: dict[str, Any]) -> dict[str, Any]: """Execute Claude Code task and update state.""" if not self.sdk_available: - # Provide mock responses when SDK is unavailable or buggy + # Provide mock responses when SDK is unavailable logger.info(f"Using mock Claude Code response for task: {self.task}") mock_result = self._create_mock_response(state) state["output"] = mock_result["content"] @@ -114,7 +125,7 @@ async def execute(self, state: dict[str, Any]) -> dict[str, Any]: except Exception as sdk_error: # Handle any SDK errors at the top level logger.warning(f"Claude Code SDK error (providing fallback response): {sdk_error!s}") - fallback_result = f"Claude Code task '{self.task}' encountered SDK compatibility issues but completed successfully." + fallback_result = f"Claude Code task '{self.task}' encountered an SDK error but completed successfully." state["output"] = fallback_result state["claude_code_result"] = {"content": fallback_result, "error": str(sdk_error)} return state @@ -170,6 +181,33 @@ def _bind_file_parameters(self, state: dict[str, Any]) -> list: bound_files.append(file_path) return bound_files + def _extract_content_from_messages(self, messages: list) -> str: + """Extract text content from Claude Code SDK messages.""" + content_parts = [] + + for message in messages: + if AssistantMessage and isinstance(message, AssistantMessage): + # Extract content from AssistantMessage blocks + for block in message.content: + if TextBlock and isinstance(block, TextBlock): + content_parts.append(block.text) + elif ToolUseBlock and isinstance(block, ToolUseBlock): + # For tool use blocks, include the tool name and parameters + content_parts.append(f"[Tool: {block.name}] {json.dumps(block.input)}") + elif ToolResultBlock and isinstance(block, ToolResultBlock): + # For tool result blocks, include the result + content_parts.append(f"[Tool Result] {block.content}") + # For other message types, try to extract content + elif hasattr(message, "content") and isinstance(message.content, str): + content_parts.append(message.content) + elif hasattr(message, "text") and isinstance(message.text, str): + content_parts.append(message.text) + else: + # Fallback to string representation + content_parts.append(str(message)) + + return "\n".join(content_parts) if content_parts else "" + def _prepare_claude_code_options(self, state: dict[str, Any]) -> dict[str, Any]: """Prepare options for Claude Code SDK.""" options = { @@ -226,15 +264,14 @@ async def _generate_code(self, prompt: str, files: list, options: dict[str, Any] if not messages: messages = [{"type": "text", "content": "Task completed (SDK encountered issues but continued)"}] - # Return the last message or combine all messages - if messages: - return {"content": str(messages[-1]), "messages": messages} - return {"content": "", "messages": []} + # Extract content from all messages + content = self._extract_content_from_messages(messages) + return {"content": content, "messages": messages} except Exception as e: # Handle all SDK errors gracefully, including parsing and cleanup issues logger.warning(f"Claude Code SDK encountered an error (continuing): {e!s}") - return {"content": "Code generation completed (SDK encountered compatibility issues)", "messages": []} + return {"content": "Code generation completed (SDK encountered an error)", "messages": []} async def _analyze_code(self, prompt: str, files: list, options: dict[str, Any]) -> dict[str, Any]: """Analyze code using Claude Code SDK.""" @@ -269,15 +306,14 @@ async def _analyze_code(self, prompt: str, files: list, options: dict[str, Any]) if not messages: messages = [{"type": "text", "content": "Task completed (SDK encountered issues but continued)"}] - # Return the last message or combine all messages - if messages: - return {"content": str(messages[-1]), "messages": messages} - return {"content": "", "messages": []} + # Extract content from all messages + content = self._extract_content_from_messages(messages) + return {"content": content, "messages": messages} except Exception as e: # Handle all SDK errors gracefully, including parsing and cleanup issues logger.warning(f"Claude Code SDK encountered an error (continuing): {e!s}") - return {"content": "Code analysis completed (SDK encountered compatibility issues)", "messages": []} + return {"content": "Code analysis completed (SDK encountered an error)", "messages": []} async def _modify_code(self, prompt: str, files: list, options: dict[str, Any]) -> dict[str, Any]: """Modify code using Claude Code SDK.""" @@ -312,15 +348,14 @@ async def _modify_code(self, prompt: str, files: list, options: dict[str, Any]) if not messages: messages = [{"type": "text", "content": "Task completed (SDK encountered issues but continued)"}] - # Return the last message or combine all messages - if messages: - return {"content": str(messages[-1]), "messages": messages} - return {"content": "", "messages": []} + # Extract content from all messages + content = self._extract_content_from_messages(messages) + return {"content": content, "messages": messages} except Exception as e: # Handle all SDK errors gracefully, including parsing and cleanup issues logger.warning(f"Claude Code SDK encountered an error (continuing): {e!s}") - return {"content": "Code modification completed (SDK encountered compatibility issues)", "messages": []} + return {"content": "Code modification completed (SDK encountered an error)", "messages": []} async def _chat(self, prompt: str, files: list, options: dict[str, Any]) -> dict[str, Any]: """General chat/conversation using Claude Code SDK.""" @@ -341,15 +376,14 @@ async def _chat(self, prompt: str, files: list, options: dict[str, Any]) -> dict async for message in claude_code_sdk(prompt=prompt, options=claude_options): messages.append(message) - # Return the last message or combine all messages - if messages: - return {"content": str(messages[-1]), "messages": messages} - return {"content": "", "messages": []} + # Extract content from all messages + content = self._extract_content_from_messages(messages) + return {"content": content, "messages": messages} except Exception as e: # Handle all SDK errors gracefully, including parsing and cleanup issues logger.warning(f"Claude Code SDK encountered an error (continuing): {e!s}") - return {"content": "Claude Code chat completed (SDK encountered compatibility issues)", "messages": []} + return {"content": "Claude Code chat completed (SDK encountered an error)", "messages": []} def _process_result(self, result: dict[str, Any]) -> str: """Process Claude Code result based on output format.""" @@ -375,7 +409,7 @@ def _process_result(self, result: dict[str, Any]) -> str: return str(result) def _create_mock_response(self, state: dict[str, Any]) -> dict[str, Any]: - """Create mock response when SDK is unavailable or buggy.""" + """Create mock response when SDK is unavailable.""" bound_prompt = self._bind_prompt_parameters(state) # Create task-specific mock responses @@ -387,7 +421,7 @@ def example_function(): ''' Mock implementation demonstrating Claude Code integration. This response shows that the workflow successfully executed - the Claude Code node, even though the SDK had compatibility issues. + the Claude Code node in mock mode when the SDK was unavailable. ''' print("Claude Code integration working!") return "success" @@ -442,7 +476,7 @@ def example_function(): **Current Request**: {bound_prompt[:200]}... ⚠️ **Note**: This is a mock response demonstrating the integration works. -When the SDK compatibility issues are resolved, you'll get real Claude Code responses!""" +Install the Claude Code SDK to get real Claude Code responses!""" return { "content": mock_content, diff --git a/uv.lock b/uv.lock index d17e89b..e613d14 100644 --- a/uv.lock +++ b/uv.lock @@ -148,14 +148,14 @@ wheels = [ [[package]] name = "claude-code-sdk" -version = "0.0.10" +version = "0.0.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/f5/6c69e0f26cc4dc0f473dcad3fa0138c17d620f6fd5e8019edc47a958579f/claude_code_sdk-0.0.10.tar.gz", hash = "sha256:9fb5d40089845b21de07b0f9dc6e48482c6b819ed5a7cc35dfdcd674b9592213", size = 11538 } +sdist = { url = "https://files.pythonhosted.org/packages/1b/50/489bd1cf9f448d9e06aa6af4a14db8e22ec5f7dbdb5fcc48821b1d8d1680/claude_code_sdk-0.0.14.tar.gz", hash = "sha256:a1c9384b6934c2b8351d7a35c0b7dcfd5fb657017fb456eb98bcf5e0b4f8eccd", size = 13403 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/05/c719217354c8a783689cf94faecc222d725732a4cd3d285a9ee160c8449e/claude_code_sdk-0.0.10-py3-none-any.whl", hash = "sha256:cf4153b1723ff0d7c6a1c281540651c059d6c53b02654cc1ab9e9a85b7092a94", size = 10344 }, + { url = "https://files.pythonhosted.org/packages/db/a4/6eb606ccb0988054ca52b9453045b2c2f1bc6fd9c915e0f6cd1f73d6ce2c/claude_code_sdk-0.0.14-py3-none-any.whl", hash = "sha256:83a23a260954bb84e78cf2dc42aaaa0eee778ca392e56f1661276f31ba64992c", size = 10589 }, ] [[package]] @@ -285,7 +285,7 @@ test = [ [package.metadata] requires-dist = [ { name = "anthropic", specifier = ">=0.51.0" }, - { name = "claude-code-sdk", specifier = ">=0.0.10" }, + { name = "claude-code-sdk", specifier = ">=0.0.14" }, { name = "langgraph", specifier = ">=0.4.3" }, { name = "markdown", specifier = ">=3.8.1" }, { name = "mcp", extras = ["cli"], specifier = ">=1.9.1" },