Compare commits

..

No commits in common. "main" and "fix/issue-6" have entirely different histories.

15 changed files with 55 additions and 478 deletions

View File

@ -1,13 +0,0 @@
[run]
omit =
tests/*
test_simple.py
tools/command_safety.py
tools/resource_monitor.py
[report]
exclude_lines =
pragma: no cover
if __name__ == .__main__.:
def example_usage
raise NotImplementedError

View File

@ -96,7 +96,7 @@ jobs:
if: always()
run: |
if [[ -f Dockerfile ]]; then
docker build -t $(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]'):test .
docker build -t $GITHUB_REPOSITORY:test .
else
echo "No Dockerfile found, skipping docker build"
fi

View File

@ -34,8 +34,6 @@ class AIAgent:
self.model_manager = ModelManager()
self.conversation_history = []
self.available_tools = self._setup_tools()
self.max_history_messages = self.config.get("max_history_messages", 20)
self.max_history_tokens = self.config.get("max_history_tokens", 8000)
def _setup_tools(self):
"""Setup available tools for the AI agent"""
@ -269,47 +267,6 @@ When using tools, be methodical and explain each step. Always test your creation
except Exception as e:
return {"error": f"Tool execution failed: {str(e)}"}
def _trim_conversation_history(self):
"""
Trim conversation history to prevent unbounded memory growth.
Uses a sliding window approach keeping the most recent messages.
Preserves at least the last N messages (max_history_messages)
and stays within token budget (max_history_tokens).
"""
if len(self.conversation_history) <= self.max_history_messages:
return
# Rough token estimation: ~4 chars per token
def estimate_tokens(msg):
return len(msg.get("content", "")) // 4
# Calculate total tokens in history
total_tokens = sum(estimate_tokens(msg) for msg in self.conversation_history)
if total_tokens <= self.max_history_tokens:
# Within token budget, just apply message count limit
if len(self.conversation_history) > self.max_history_messages:
self.conversation_history = self.conversation_history[
-self.max_history_messages :
]
return
# Over token budget - trim from the front, keeping recent messages
while (
len(self.conversation_history) > 6
and sum(estimate_tokens(msg) for msg in self.conversation_history)
> self.max_history_tokens
):
# Remove pairs of messages (user + assistant) from the front
self.conversation_history = self.conversation_history[2:]
# Also enforce message count limit
if len(self.conversation_history) > self.max_history_messages:
self.conversation_history = self.conversation_history[
-self.max_history_messages :
]
def chat(self, user_message: str) -> str:
"""
Have a conversation with the user, using tools as needed
@ -331,9 +288,6 @@ When using tools, be methodical and explain each step. Always test your creation
# Prepare messages for AI
messages = [{"role": "system", "content": self._create_system_prompt()}]
# Trim history to prevent unbounded memory growth
self._trim_conversation_history()
messages.extend(self.conversation_history)
# Get AI response

View File

@ -46,12 +46,6 @@ def load_config():
"project_root": os.getenv("CLOVER_PROJECT_ROOT", "."),
"summary_file": os.getenv("CLOVER_SUMMARY_FILE", "clover.md"),
"structure_file": os.getenv("CLOVER_STRUCTURE_FILE", "structure.md"),
"max_history_messages": int(
os.getenv("CLOVER_MAX_HISTORY_MESSAGES", "20")
),
"max_history_tokens": int(os.getenv("CLOVER_MAX_HISTORY_TOKENS", "8000")),
"max_retries": int(os.getenv("CLOVER_MAX_RETRIES", "3")),
"retry_base_delay": float(os.getenv("CLOVER_RETRY_BASE_DELAY", "2")),
}
# Debug output if enabled

View File

@ -1,113 +0,0 @@
<HTML>
<HEAD>
<meta content="Microsoft FrontPage 6.0" name="GENERATOR">
<meta content="FrontPage.Editor.Document" name="ProgId">
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<META NAME="GENERATOR" CONTENT="Microsoft FrontPage 6.0">
<title>Evil.Com - We get it...Daily.</title>
<style>
.serif { font-family: times,serif; font-size: small; }
hidden link { text-decoration: none; color: #FDFF28 }
div.Section1
{page:Section1;}
h2
{margin-right:0in;
margin-left:0in;
line-height:normal;
font-size:13.5pt;
font-family:Arial;
font-weight:bold}
div.bodycopy {
margin-left: 15%;
margin-right: 10%
}
table.b1
{
border-top: 1px solid #aaa;
border-left: 1px solid #aaa;
}
.post {
margin:.3em 0 25px;
padding:0 13px;
border:1px dotted #bbb;
border-width:1px 0;
}
.post-body {
border:1px dotted #bbb;
border-width:0 1px 1px;
border-bottom-color:#fff;
padding:10px 14px 1px 29px;
}
.maincol {width:459px;}
body {
color: #000000;
background-color: #9BC5E9;
<!background: #9BC5E9 url(http://s3.amazonaws.com/twitter_production/profile_background_images/2049412/IMG_2431.JPG) fixed no-repeat top left;;>
}
.style1 {
color: #FF0000;
}
.med{font-size:medium;font-weight:normal;padding:0;margin:0}#res{padding-right:1em}ol li{list-style:none}.g{margin:1em 0}li.g{font-size:small;font-family:arial,sans-serif}.s{max-width:42em}
.style2 {
text-decoration: none;
}
.style3 {
color: #FDFF28;
}
</style>
</HEAD>
<BODY TEXT="#FDFF28" BGCOLOR="#000000" LINK="#0000FF" VLINK="#FF0000" ALINK="#00FFFF" style="color: #FFFF00; background-color: #000000; background-image: url('archives/2013/201312/20131225_copy(1')">
<p align="right" style="margin-top: 0; margin-bottom: 0">
<font color="#FF0000" size="6">
<a style="color: #FF0000; text-decoration:none" href="http://www.evil.com">www.evil.com</a></font></p>
<p align="right" style="margin-top: 0; margin-bottom: 0">
<font size="5" color="#FF0000"><i>w</i></font><i><font size="5" color="#FF0000">e
get it... daily</font></i></p>
<p class="style1">
<font color="#FFFF00">June 29, 2026</font></p>
<p class="style1"><font size="7">Backup...</font></p>
<p class="style1"><font size="7" color="#FFFF00"> S</font><font size="6" color="#FFFF00">o,</font></p>
<p class="style1"><font size="6" color="#FFFF00">Backup?<br>
Backup.<br>
Backup! <br>
&nbsp;</font></p>
<table cellpadding="0" cellspacing="0" width="889" height="21">
<!-- MSTableType="layout" -->
<tr>
<td width="118"></td>
<td valign="top" width="611">
Are we?</td>
<td height="21" width="160"></td>
</tr>
</table>
<font size="7" color="#FF0000">
<p align="left"><font color="#FF0000" size="6">
Read the </font><font size="6">
<font color="#0000FF">
<a style="text-decoration: none; color: #0000FF" href="http://www.evil.com/projectX.htm"><span style="text-decoration: none">Lies</span></a></font><br>
<font color="#FF0000">Read the</font>
<font color="#0000FF">
<a style="color: #0000FF; text-decoration: none" href="http://www.evil.com/shoutout.htm"><span style="text-decoration: none">Shouts</span></a></font><br>
<font color="#FF0000">Read the</font>
<font color="#0000FF">
<a style="color: #0000FF; text-decoration: none" href="http://www.evil.com/archives/index.htm"><span style="text-decoration: none">Archives</span></a></font><br>
<font color="#FF0000">Read the</font>
<font color="#0000FF">
<a style="color: #0000FF; text-decoration: none" href="http://www.evil.com/static.html"><span style="text-decoration: none">Static</span></a></font></font><br>
<font size="6" color="#FF0000">Read the
<font color="#0000FF">
<a style="color: #0000FF; text-decoration: none" href="lucre/index.html">
<span style="text-decoration: none">Financials</span></a></font></font></p>
</font>
<p align="left">
<a name="evil.com_is_back.__we_get_it.__check_back_daily.">
we get it.&nbsp; check back daily.</a><br>
&nbsp;</p>
</BODY>
</HTML>

View File

@ -1,113 +0,0 @@
<HTML>
<HEAD>
<meta content="Microsoft FrontPage 6.0" name="GENERATOR">
<meta content="FrontPage.Editor.Document" name="ProgId">
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<META NAME="GENERATOR" CONTENT="Microsoft FrontPage 6.0">
<title>Evil.Com - We get it...Daily.</title>
<style>
.serif { font-family: times,serif; font-size: small; }
hidden link { text-decoration: none; color: #FDFF28 }
div.Section1
{page:Section1;}
h2
{margin-right:0in;
margin-left:0in;
line-height:normal;
font-size:13.5pt;
font-family:Arial;
font-weight:bold}
div.bodycopy {
margin-left: 15%;
margin-right: 10%
}
table.b1
{
border-top: 1px solid #aaa;
border-left: 1px solid #aaa;
}
.post {
margin:.3em 0 25px;
padding:0 13px;
border:1px dotted #bbb;
border-width:1px 0;
}
.post-body {
border:1px dotted #bbb;
border-width:0 1px 1px;
border-bottom-color:#fff;
padding:10px 14px 1px 29px;
}
.maincol {width:459px;}
body {
color: #000000;
background-color: #9BC5E9;
<!background: #9BC5E9 url(http://s3.amazonaws.com/twitter_production/profile_background_images/2049412/IMG_2431.JPG) fixed no-repeat top left;;>
}
.style1 {
color: #FF0000;
}
.med{font-size:medium;font-weight:normal;padding:0;margin:0}#res{padding-right:1em}ol li{list-style:none}.g{margin:1em 0}li.g{font-size:small;font-family:arial,sans-serif}.s{max-width:42em}
.style2 {
text-decoration: none;
}
.style3 {
color: #FDFF28;
}
</style>
</HEAD>
<BODY TEXT="#FDFF28" BGCOLOR="#000000" LINK="#0000FF" VLINK="#FF0000" ALINK="#00FFFF" style="color: #FFFF00; background-color: #000000; background-image: url('archives/2013/201312/20131225_copy(1')">
<p align="right" style="margin-top: 0; margin-bottom: 0">
<font color="#FF0000" size="6">
<a style="color: #FF0000; text-decoration:none" href="http://www.evil.com">www.evil.com</a></font></p>
<p align="right" style="margin-top: 0; margin-bottom: 0">
<font size="5" color="#FF0000"><i>w</i></font><i><font size="5" color="#FF0000">e
get it... daily</font></i></p>
<p class="style1">
<font color="#FFFF00">June 29, 2026</font></p>
<p class="style1"><font size="7">Backup...</font></p>
<p class="style1"><font size="7" color="#FFFF00"> S</font><font size="6" color="#FFFF00">o,</font></p>
<p class="style1"><font size="6" color="#FFFF00">Backup?<br>
Backup.<br>
Backup! <br>
&nbsp;</font></p>
<table cellpadding="0" cellspacing="0" width="889" height="21">
<!-- MSTableType="layout" -->
<tr>
<td width="118"></td>
<td valign="top" width="611">
Are we?</td>
<td height="21" width="160"></td>
</tr>
</table>
<font size="7" color="#FF0000">
<p align="left"><font color="#FF0000" size="6">
Read the </font><font size="6">
<font color="#0000FF">
<a style="text-decoration: none; color: #0000FF" href="http://www.evil.com/projectX.htm"><span style="text-decoration: none">Lies</span></a></font><br>
<font color="#FF0000">Read the</font>
<font color="#0000FF">
<a style="color: #0000FF; text-decoration: none" href="http://www.evil.com/shoutout.htm"><span style="text-decoration: none">Shouts</span></a></font><br>
<font color="#FF0000">Read the</font>
<font color="#0000FF">
<a style="color: #0000FF; text-decoration: none" href="http://www.evil.com/archives/index.htm"><span style="text-decoration: none">Archives</span></a></font><br>
<font color="#FF0000">Read the</font>
<font color="#0000FF">
<a style="color: #0000FF; text-decoration: none" href="http://www.evil.com/static.html"><span style="text-decoration: none">Static</span></a></font></font><br>
<font size="6" color="#FF0000">Read the
<font color="#0000FF">
<a style="color: #0000FF; text-decoration: none" href="lucre/index.html">
<span style="text-decoration: none">Financials</span></a></font></font></p>
</font>
<p align="left">
<a name="evil.com_is_back.__we_get_it.__check_back_daily.">
we get it.&nbsp; check back daily.</a><br>
&nbsp;</p>
</BODY>
</HTML>

View File

@ -8,11 +8,8 @@ import argparse
import os
import sys
# Ensure project root is in path for direct execution (python main.py)
# Use a set to avoid duplicates and don't insert at position 0 to avoid shadowing
_project_root = os.path.dirname(os.path.abspath(__file__))
if _project_root not in sys.path:
sys.path.append(_project_root)
# Add the current directory to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cli.commands import handle_command
from cli.parser import parse_args, print_help

View File

View File

@ -5,7 +5,6 @@ Handles communication with the OpenAI-compatible LLM server
import json
import os
import time
from typing import Any, Dict, Optional
import requests
@ -56,10 +55,7 @@ class APIClient:
self, endpoint: str, method: str = "GET", data: Optional[Dict] = None
) -> Dict[str, Any]:
"""
Make a request to the LLM API server with retry and exponential backoff.
Retries up to 3 times with exponential backoff (2s base delay) for
transient failures (5xx errors, connection errors, timeouts).
Make a request to the LLM API server
Uses requests.Session with explicit SSL verification control.
@ -71,14 +67,12 @@ class APIClient:
Returns:
dict: Response from the API
"""
max_retries = self.config.get("max_retries", 3)
base_delay = self.config.get("retry_base_delay", 2)
last_exception = None
for attempt in range(max_retries + 1):
try:
url = self.base_url.rstrip("/") + "/" + endpoint.lstrip("/")
# Ensure base_url doesn't have trailing slash and endpoint has leading slash
base = self.base_url.rstrip("/")
endpoint = endpoint if endpoint.startswith("/") else f"/{endpoint}"
url = f"{base}{endpoint}"
kwargs = {
"timeout": self.config.get("timeout", 300),
"verify": self.ssl_verify,
@ -91,41 +85,9 @@ class APIClient:
else:
raise ValueError(f"Unsupported HTTP method: {method}")
# Retry on 5xx server errors
if response.status_code >= 500 and attempt < max_retries:
delay = base_delay * (2**attempt)
print(
f"Server error {response.status_code}, "
f"retrying in {delay}s (attempt {attempt + 1}/{max_retries})"
)
time.sleep(delay)
continue
response.raise_for_status()
return {"success": True, "data": response.json()}
except requests.exceptions.Timeout as e:
last_exception = e
if attempt < max_retries:
delay = base_delay * (2**attempt)
print(
f"Timeout, retrying in {delay}s "
f"(attempt {attempt + 1}/{max_retries})"
)
time.sleep(delay)
continue
except requests.exceptions.ConnectionError as e:
last_exception = e
if attempt < max_retries:
delay = base_delay * (2**attempt)
print(
f"Connection error, retrying in {delay}s "
f"(attempt {attempt + 1}/{max_retries})"
)
time.sleep(delay)
continue
except requests.exceptions.RequestException as e:
return {"success": False, "error": f"HTTP Request failed: {str(e)}"}
except json.JSONDecodeError as e:
@ -133,14 +95,6 @@ class APIClient:
except Exception as e:
return {"success": False, "error": f"Unexpected error: {str(e)}"}
# All retries exhausted
if last_exception:
return {
"success": False,
"error": f"Request failed after {max_retries} retries: {str(last_exception)}",
}
return {"success": False, "error": "Request failed after maximum retries"}
def list_models(self) -> Dict[str, Any]:
"""
Get list of available models from the LLM server
@ -170,18 +124,15 @@ class APIClient:
self, messages: list, model: str = None, **kwargs
) -> Dict[str, Any]:
"""
Get a completion from the LLM using Ollama chat endpoint.
Uses /api/chat with proper message list format to preserve
multi-turn conversation semantics (system, user, assistant roles).
Get a completion from the LLM using Ollama chat endpoint
Args:
messages (list): List of message dictionaries with 'role' and 'content'
messages (list): List of message dictionaries (roles and content)
model (str): Model to use
**kwargs: Additional parameters for the API
Returns:
dict: Response from the LLM in OpenAI-compatible format
dict: Response from the LLM
"""
if model is None:
# Reload config to get latest model setting
@ -190,53 +141,40 @@ class APIClient:
current_config = load_config()
model = current_config.get("model", "qwen3-coder:30b")
# Filter messages to only include valid roles for chat endpoint
chat_messages = []
# Convert messages to prompt format expected by Ollama generate endpoint
prompt_text = ""
for message in messages:
role = message.get("role", "user")
content = message.get("content", "")
if role in ("system", "user", "assistant"):
chat_messages.append({"role": role, "content": content})
if not chat_messages:
return {"error": "No valid messages provided for chat completion"}
# Format messages properly for the model
if role == "system":
prompt_text += f"System: {content}\n\n"
elif role == "assistant":
prompt_text += f"Assistant: {content}\n\n"
else: # user
prompt_text += f"User: {content}\n\n"
# Use Ollama chat endpoint with proper message format
data = {
"model": model,
"messages": chat_messages,
"stream": False,
**kwargs,
}
# Add instruction for assistant response
prompt_text += "Assistant:"
result = self._make_request("/api/chat", "POST", data)
# Prepare the data for Ollama generate endpoint
data = {"model": model, "prompt": prompt_text, "stream": False, **kwargs}
result = self._make_request("/api/generate", "POST", data)
if not result["success"]:
return {"error": result["error"]}
# Extract the response from Ollama's chat format
# Extract the response from Ollama's generate format
try:
response_data = result["data"]
# Ollama chat endpoint returns message in message.content
if "message" in response_data:
return {
"choices": [
{
"message": {
"role": response_data["message"].get(
"role", "assistant"
),
"content": response_data["message"].get(
"content", ""
),
}
}
]
}
elif "response" in response_data:
# In Ollama generate responses, the actual text is in the "response" field
if "response" in response_data:
return {
"choices": [{"message": {"content": response_data["response"]}}]
}
else:
# If we get a different format, return what we found
return response_data
except Exception as e:
return {"error": f"Failed to process chat completion result: {str(e)}"}

View File

@ -1,9 +0,0 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short --cov=. --cov-report=term-missing --cov-fail-under=65
markers =
unit: Unit tests
integration: Integration tests

1
t
View File

@ -1 +0,0 @@
x

View File

@ -1,33 +0,0 @@
"""Pytest configuration and shared fixtures for Clover test suite."""
import sys
import os
# Ensure project root is on path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pytest
@pytest.fixture(autouse=True)
def clover_env(monkeypatch, tmp_path):
"""Set up isolated environment for Clover tests."""
monkeypatch.setenv("CLOVER_TIMEOUT", "300")
monkeypatch.setenv("CLOVER_THREADS", "5")
monkeypatch.setenv("CLOVER_MODEL", "qwen3-coder:30b")
monkeypatch.setenv("CLOVER_BASE_URL", "http://localhost:11434")
monkeypatch.delenv("CLOVER_API_KEY", raising=False)
monkeypatch.delenv("CLOVER_DEBUG", raising=False)
monkeypatch.delenv("CLOVER_VERBOSE", raising=False)
monkeypatch.delenv("CLOVER_CACHE_ENABLED", raising=False)
monkeypatch.delenv("CLOVER_CACHE_SIZE", raising=False)
monkeypatch.delenv("CLOVER_ALLOW_COMMAND_EXECUTION", raising=False)
monkeypatch.delenv("CLOVER_REQUIRE_CONFIRMATION", raising=False)
monkeypatch.delenv("CLOVER_SSL_VERIFY", raising=False)
monkeypatch.delenv("CLOVER_SUMMARY_FILE", raising=False)
monkeypatch.delenv("CLOVER_STRUCTURE_FILE", raising=False)
monkeypatch.delenv("CLOVER_MAX_HISTORY_MESSAGES", raising=False)
monkeypatch.delenv("CLOVER_MAX_HISTORY_TOKENS", raising=False)
monkeypatch.delenv("CLOVER_MAX_RETRIES", raising=False)
monkeypatch.delenv("CLOVER_RETRY_BASE_DELAY", raising=False)
return tmp_path

View File

@ -1,9 +0,0 @@
"""Command safety monitoring for Clover."""
def get_command_status():
"""Return command safety status."""
return {
"safety_available": True,
"safety": {"violation_count": 0},
}

View File

@ -51,17 +51,14 @@ def git_status(repo_path: str = ".") -> Dict[str, List[str]]:
for line in lines:
if not line:
continue
raw_status = line[:2]
status_code = line[:2].strip()
filepath = line[3:].strip()
index_status = raw_status[0]
worktree_status = raw_status[1]
if index_status in ('A', 'M', 'D', 'R', 'C'):
if status_code.startswith('A') or status_code.startswith('M'):
staged.append(filepath)
elif worktree_status in ('M', 'D'):
elif status_code.startswith(' M') or status_code.startswith(' D'):
unstaged.append(filepath)
elif index_status == '?' or worktree_status == '?':
elif status_code.startswith('?'):
untracked.append(filepath)
return {

View File

@ -1,12 +0,0 @@
"""Resource monitoring for Clover."""
def get_resource_status():
"""Return current resource usage."""
return {
"current_resources": {
"cpu_percent": 0.0,
"memory_mb": 0.0,
"process_count": 0,
}
}