Clover/cli/commands.py
Jarian Cottingham 392bcfa2ef feat: Implement AI agent with multi-turn conversation and tool calling
Major enhancements to Clover CLI:

 New Features:
- AI agent with multi-turn conversation capabilities
- Tool calling system with 11+ tools for file operations, Git, linting, etc.
- Step-by-step AI assistance with play-by-play commentary
- Enhanced interactive mode with better UX

🔧 Core Components Added:
- ai_agent.py: Main AI agent with conversation management
- models/: API client and model management system
- Comprehensive tool system for development tasks

🛠️ Tools Available:
- File operations (create, read, update, delete)
- Command execution with safety checks
- Git operations (status, diff, commit, push)
- Code linting and formatting
- Project structure analysis
- Security scanning and dependency management

💡 User Experience:
- Real-time tool execution summaries
- File creation with full path visibility
- Error handling and retry mechanisms
- Clean conversation flow until task completion

🧹 Repository Cleanup:
- Added comprehensive .gitignore
- Removed __pycache__ directories and build artifacts
- Organized project structure

The AI can now actually create files, run commands, and work through complex
development tasks step-by-step with full transparency.
2026-01-15 01:23:40 -06:00

376 lines
12 KiB
Python

"""
Command handling module for Clover - A terminal assistant for AI-powered project management
"""
import os
import re
import sys
from pathlib import Path
# Add the current directory to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ai_agent import AIAgent
from config.settings import load_config
from models.model_manager import ModelManager
from tools.commandline_tool import commandline, safe_execute
from tools.file_tools import create_file, delete_file, read_file, update_file
from tools.git_tools import git_commit, git_diff, git_push, git_status
from tools.lint_format_tools import format_code, lint_code
from tools.project_tools import (
aggregate_summaries,
get_project_structure,
summarize_file,
)
# Global AI agent instance
ai_agent = None
def reset_ai_agent():
"""Reset the global AI agent to reload configuration"""
global ai_agent
ai_agent = None
def handle_command(args):
"""
Handle the parsed CLI arguments and execute corresponding commands
"""
# Load configuration
config = load_config()
# Initialize AI agent (always reload to get fresh config)
global ai_agent
ai_agent = AIAgent()
try:
# Check if this is an interactive prompt (not a special command)
if (
args.prompt
and not args.init
and not args.list
and not args.timeout
and not args.threads
):
# Handle regular prompts - use AI agent for multi-turn conversation with tools
print("\n" + "=" * 60)
print("🤖 AI DEVELOPMENT SESSION STARTING")
print("=" * 60)
print(f"📝 Your Request: {args.prompt}")
print("\n🧠 AI is analyzing your request and planning the approach...")
print(
"💡 The AI will use tools to create files, run commands, and solve the task step-by-step"
)
print("📊 You'll see detailed summaries of each action the AI takes")
print("-" * 60)
try:
# Use AI agent for intelligent conversation with tool usage
response = ai_agent.chat(args.prompt)
print(f"\n🎯 Final Status: {response}")
except Exception as e:
print(f"\n❌ Error during AI session: {e}")
print("🔧 Try rephrasing your request or check the system status")
elif args.init:
# Handle /init command
init_project()
elif args.list:
# Handle /list command
list_models(ai_agent)
elif args.timeout is not None:
# Handle /timeout command
set_timeout(args.timeout)
elif args.threads is not None:
# Handle /threads command
set_threads(args.threads)
elif args.prompt and args.prompt == "/git_status":
# Handle git status command from interactive mode
print("Repository Status:")
result = git_status()
if "error" in result:
print(f"Error: {result['error']}")
else:
print(f"Staged files: {len(result['staged'])}")
print(f"Unstaged files: {len(result['unstaged'])}")
print(f"Untracked files: {len(result['untracked'])}")
elif args.prompt and args.prompt.startswith("/lint_file "):
# Handle linting command
try:
file_path = args.prompt.split(" ", 2)[2]
result = lint_code([file_path])
print(f"Linting results for {file_path}:")
if "error" in result:
print(f"Error: {result['error']}")
else:
print("Return code:", result.get("return_code"))
print("Success:", result.get("success"))
except Exception as e:
print(f"Error linting file: {e}")
elif args.prompt and args.prompt == "/reset":
# Reset AI conversation
if ai_agent:
ai_agent.reset_conversation()
else:
print("No active AI session to reset.")
elif args.prompt and args.prompt == "/summary":
# Show conversation summary
if ai_agent:
summary = ai_agent.get_conversation_summary()
print("📋 Conversation Summary:")
print(summary)
else:
print("No active AI session.")
else:
# No specific command, just show help for now
from cli.parser import print_help
print_help()
except Exception as e:
print(f"Error executing command: {e}")
sys.exit(1)
def init_project():
"""Initialize project with a summary file"""
try:
# Create or update clover.md file
project_summary = """
# Project Summary
This is the project summary file for the Clover CLI tool.
All project information and progress will be tracked here.
## Current Status
- Project initialized
- Basic structure created
- Configuration loaded
## Next Steps
1. Review existing files
2. Define project goals
3. Begin implementation tasks
"""
with open("clover.md", "w") as f:
f.write(project_summary)
print("Project initialized! Created clover.md file.")
# Create structure.md if it doesn't exist
if not os.path.exists("structure.md"):
# In a real implementation, this would call the LLM to generate structure
with open("structure.md", "w") as f:
f.write(
"# Project Structure\n\nThis is a placeholder for the project structure generated by LLM.\n"
)
print("Created structure.md file.")
except Exception as e:
print(f"Error initializing project: {e}")
def list_models(ai_agent_instance):
"""List available models on the server"""
try:
result = ai_agent_instance.model_manager.list_models()
if "error" in result:
print(f"Error listing models: {result['error']}")
return
models = result.get("models", [])
print("Available models:")
for model in models:
name = model.get("name", "unknown")
print(f"- {name}")
if not models:
# Fallback to default models
print("No models found, fallback to defaults:")
default_models = [
"gpt-4",
"gpt-3.5-turbo",
"claude-3-opus",
"claude-3-sonnet",
"llama2-70b",
"qwen3-coder:30b",
]
for model in default_models:
print(f"- {model}")
print(f"Active model: {result.get('active_model', 'gpt-4')}")
print(f"Base URL: {result.get('base_url', 'http://192.168.8.223:11434')}")
except Exception as e:
print(f"Error listing models: {e}")
def set_timeout(seconds):
"""Set timeout duration for AI operations"""
try:
config = load_config()
config["timeout"] = seconds
# In a full implementation, save to config file
print(f"Timeout set to {seconds} seconds")
except Exception as e:
print(f"Error setting timeout: {e}")
def set_threads(count):
"""Set maximum number of threads for concurrent operations"""
try:
config = load_config()
config["threads"] = count
# In a full implementation, save to config file
print(f"Thread limit set to {count}")
except Exception as e:
print(f"Error setting thread limit: {e}")
def execute_command(cmd):
"""Execute a system command with permission prompt"""
try:
response = commandline(cmd)
print(response)
except Exception as e:
print(f"Error executing command: {e}")
def extract_code_blocks(text):
"""
Extract code blocks from AI response text
Args:
text (str): The AI response text
Returns:
list: List of dictionaries with 'language' and 'code' keys
"""
code_blocks = []
# Pattern to match code blocks with optional language specification
pattern = r"```(\w+)?\n(.*?)\n```"
matches = re.findall(pattern, text, re.DOTALL)
for match in matches:
language = match[0] if match[0] else "text"
code = match[1].strip()
if code: # Only add non-empty code blocks
code_blocks.append({"language": language, "code": code})
return code_blocks
def suggest_filename(code, language):
"""
Suggest a filename based on code content and language
Args:
code (str): The code content
language (str): Programming language
Returns:
str: Suggested filename
"""
# Extract potential class names, function names, or descriptive words
if language.lower() == "python":
# Look for class definitions
class_match = re.search(r"class\s+(\w+)", code)
if class_match:
return f"{class_match.group(1).lower()}.py"
# Look for function definitions
func_match = re.search(r"def\s+(\w+)", code)
if func_match:
return f"{func_match.group(1).lower()}.py"
return "script.py"
elif language.lower() in ["javascript", "js"]:
return "script.js"
elif language.lower() in ["html"]:
return "index.html"
elif language.lower() in ["css"]:
return "styles.css"
elif language.lower() in ["bash", "shell", "sh"]:
return "script.sh"
elif language.lower() in ["json"]:
return "data.json"
elif language.lower() in ["yaml", "yml"]:
return "config.yml"
else:
return f"code.{language.lower()}" if language != "text" else "code.txt"
def handle_code_blocks(response_text):
"""
Handle code blocks in AI response - extract and offer to save them
Args:
response_text (str): The AI response containing potential code blocks
"""
code_blocks = extract_code_blocks(response_text)
if not code_blocks:
return
print(f"\n📝 Found {len(code_blocks)} code block(s) in the response.")
for i, block in enumerate(code_blocks, 1):
language = block["language"]
code = block["code"]
suggested_name = suggest_filename(code, language)
print(f"\n--- Code Block {i} ({language}) ---")
print(f"Suggested filename: {suggested_name}")
print("Preview:")
# Show first few lines
lines = code.split("\n")
preview_lines = lines[:3]
for line in preview_lines:
print(f" {line}")
if len(lines) > 3:
print(f" ... ({len(lines) - 3} more lines)")
try:
save_choice = (
input(f"\nSave this code block? [y/N/c=custom filename]: ")
.strip()
.lower()
)
if save_choice in ["y", "yes"]:
filename = suggested_name
elif save_choice in ["c", "custom"]:
filename = input("Enter filename: ").strip()
if not filename:
print("Skipping - no filename provided")
continue
else:
print("Skipping code block")
continue
# Create the file
if create_file(filename, code):
print(f"✅ Created file: {filename}")
else:
print(f"❌ Failed to create file: {filename}")
except KeyboardInterrupt:
print("\nSkipping remaining code blocks")
break
except EOFError:
print("\nSkipping remaining code blocks")
break