Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"deepeval>=3.3.4",
"evaluate>=0.4.5",
"fastmcp>=2.11.0",
"minicline>=0.1.0",
"pydantic>=2.11.7",
"pyyaml>=6.0.2",
"ragas>=0.3.0",
Expand Down
139 changes: 139 additions & 0 deletions src/metacoder/coders/minicline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import logging
import os
import shutil
from pathlib import Path
from typing import Any

from metacoder.coders.base_coder import (
BaseCoder,
CoderConfigObject,
CoderOutput,
change_directory,
)
from metacoder.configuration import ConfigFileRole


logger = logging.getLogger(__name__)


class MiniclineCoder(BaseCoder):
"""
Runs minicline over a task.

Minicline is a lightweight, secure command-line interface for AI coding
tasks via OpenRouter API. It provides containerized execution by default
for enhanced security.

Configuration:
- Requires OPENROUTER_API_KEY environment variable
- Optional model parameter (defaults to openai/gpt-4.1-mini)
- Uses Docker for containerized execution

Note: Minicline does not support MCP extensions.
"""

model: str = "openai/gpt-4.1-mini"

@classmethod
def is_available(cls) -> bool:
"""Check if minicline is available."""
try:
import minicline # noqa: F401
return True
except ImportError:
return False

@classmethod
def supports_mcp(cls) -> bool:
"""MiniclineCoder does not support MCP extensions."""
return False

@classmethod
def default_config_paths(cls) -> dict[Path, ConfigFileRole]:
return {
Path("MINICLINE.md"): ConfigFileRole.PRIMARY_INSTRUCTION,
}

def default_config_objects(self) -> list[CoderConfigObject]:
"""Default config objects for MiniclineCoder."""
return []

def run(self, input_text: str) -> CoderOutput:
"""
Run minicline with the given input text.
"""
if not self.is_available():
raise ImportError(
"minicline is not installed. Install it with: pip install minicline"
)

try:
from minicline import perform_task
except ImportError as e:
raise ImportError(
f"Failed to import minicline: {e}. Install it with: pip install minicline"
)

# Check for required environment variable
env = self.expand_env(self.env)
if "OPENROUTER_API_KEY" not in env:
raise ValueError(
"OPENROUTER_API_KEY environment variable is required for minicline. "
"Set it in your environment or pass it via the env parameter."
)

self.prepare_workdir()

with change_directory(self.workdir):
text = self.expand_prompt(input_text)
logger.debug(f"🤖 Running minicline with input: {text}")

# Determine model to use
model = self.model
if self.params and "model" in self.params:
model = self.params["model"]

logger.info(f"🤖 Running minicline with model: {model}")
logger.info(f"🤖 Working directory: {os.getcwd()}")

try:
# Set environment for the subprocess
original_env = os.environ.copy()
os.environ.update(env)

# Run minicline perform_task
result = perform_task(
instructions=text,
cwd=os.getcwd(),
model=model
)

# Restore original environment
os.environ.clear()
os.environ.update(original_env)

# Create CoderOutput from result
# minicline's perform_task doesn't return structured output,
# so we'll capture what we can
output = CoderOutput(
stdout=f"Minicline task completed with model {model}",
stderr="",
result_text=f"Task executed successfully with minicline",
success=True,
)

logger.info("🤖 Minicline task completed successfully")
return output

except Exception as e:
logger.error(f"🚫 Minicline failed: {e}")
# Restore original environment in case of error
os.environ.clear()
os.environ.update(original_env)

return CoderOutput(
stdout="",
stderr=str(e),
result_text=None,
success=False,
)
2 changes: 2 additions & 0 deletions src/metacoder/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from metacoder.coders.gemini import GeminiCoder
from metacoder.coders.opencode import OpencodeCoder
from metacoder.coders.qwen import QwenCoder
from metacoder.coders.minicline import MiniclineCoder


AVAILABLE_CODERS: Dict[str, Type[BaseCoder]] = {
Expand All @@ -19,5 +20,6 @@
"gemini": GeminiCoder,
"opencode": OpencodeCoder,
"qwen": QwenCoder,
"minicline": MiniclineCoder,
"dummy": DummyCoder,
}
Loading