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.
376 lines
12 KiB
Python
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
|