Skip to content
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ The current priorities are to improve core capabilities and user experience of t
4. **Context Delivery** - [Discussion](https://github.com/dwash96/cecli/issues/47)
* [ ] Use workflow for internal discovery to better target file snippets needed for specific tasks
* [ ] Add support for partial files and code snippets in model completion messages
* [ ] Update message request structure for optimal caching

5. **TUI Experience** - [Discussion](https://github.com/dwash96/cecli/issues/48)
* [x] Add a full TUI (probably using textual) to have a visual interface competitive with the other coding agent terminal programs
Expand All @@ -157,8 +158,8 @@ The current priorities are to improve core capabilities and user experience of t
* [x] Add an explicit "finished" internal tool
* [x] Add a configuration json setting for agent mode to specify allowed local tools to use, tool call limits, etc.
* [ ] Add a RAG tool for the model to ask questions about the codebase
* [ ] Make the system prompts more aggressive about removing unneeded files/content from the context
* [ ] Add a plugin-like system for allowing agent mode to use user-defined tools in simple python files
* [x] Make the system prompts more aggressive about removing unneeded files/content from the context
* [x] Add a plugin-like system for allowing agent mode to use user-defined tools in simple python files
* [x] Add a dynamic tool discovery tool to allow the system to have only the tools it needs in context

### All Contributors (Both Cecli and Aider main)
Expand Down
2 changes: 1 addition & 1 deletion cecli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from packaging import version

__version__ = "0.95.9.dev"
__version__ = "0.95.10.dev"
safe_version = __version__

try:
Expand Down
16 changes: 13 additions & 3 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1330,14 +1330,24 @@ async def _run_linear(self, with_message=None, preproc=True):
await self.io.input_task
user_message = self.io.input_task.result()

if self.args and not self.args.tui:
if (
self.args
and not self.args.tui
and self.show_pretty()
and self.args.fancy_input
):
self.io.tool_output("Processing...\n")

self.io.output_task = asyncio.create_task(self.generate(user_message, preproc))

await self.io.output_task

if self.args and not self.args.tui:
if (
self.args
and not self.args.tui
and self.show_pretty()
and self.args.fancy_input
):
self.io.tool_output("Finished.")

self.io.ring_bell()
Expand Down Expand Up @@ -2766,7 +2776,7 @@ async def get_server_tools(server):
return None

async def get_all_server_tools():
tasks = [get_server_tools(server) for server in self.mcp_manager]
tasks = [get_server_tools(server) for server in self.mcp_manager if server.is_enabled]
results = await asyncio.gather(*tasks)
return [result for result in results if result is not None]

Expand Down
8 changes: 7 additions & 1 deletion cecli/commands/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ async def execute(cls, io, coder, args, **kwargs):
io.tool_output(f"You can add to git with: /git add {fname}")
continue

if await io.confirm_ask(f"No files matched '{word}'. Do you want to create {fname}?"):
confirm_fname = os.path.relpath(fname)
if len(confirm_fname) > 64:
confirm_fname = f".../{os.path.basename(confirm_fname)}"

if await io.confirm_ask(
f"No files matched '{confirm_fname}'. Do you want to create this file?"
):
try:
fname.parent.mkdir(parents=True, exist_ok=True)
fname.touch()
Expand Down
30 changes: 28 additions & 2 deletions cecli/commands/utils/base_command.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
from abc import ABC, abstractmethod
from abc import ABC, ABCMeta, abstractmethod
from typing import List


class BaseCommand(ABC):
class CommandMeta(ABCMeta):
"""Metaclass for validating command classes at definition time."""

def __new__(mcs, name, bases, namespace):
# Create the class first
cls = super().__new__(mcs, name, bases, namespace)

# Skip validation for BaseCommand itself
if name == "BaseCommand":
return cls

if not name.endswith("Command"):
raise TypeError(f"Command class must end with 'Command', got '{name}'")

if getattr(cls, "NORM_NAME", None) is None:
raise TypeError("Command class must define NORM_NAME")

if getattr(cls, "DESCRIPTION", None) is None:
raise TypeError("Command class must define DESCRIPTION")

if "execute" not in namespace:
raise TypeError("Command class must implement execute method")

return cls


class BaseCommand(ABC, metaclass=CommandMeta):
"""Abstract base class for all commands."""

# Class properties (similar to BaseTool)
Expand Down
15 changes: 12 additions & 3 deletions cecli/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

from cecli.commands import SwitchCoderSignal
from cecli.helpers import coroutines
from cecli.report import update_error_prefix

from .dump import dump # noqa: F401
from .editor import pipe_editor
Expand Down Expand Up @@ -1039,14 +1040,22 @@ async def stop_output_task(self):
await output_task
except (
asyncio.CancelledError,
Exception,
EOFError,
IndexError,
RuntimeError,
SystemExit,
SwitchCoderSignal,
):
pass
except (
Exception,
IndexError,
RuntimeError,
):
import traceback

traceback_str = traceback.format_exc()
update_error_prefix(traceback_str)
update_error_prefix(str(output_task))
pass

async def stop_task_streams(self):
input_task = asyncio.create_task(self.stop_input_task())
Expand Down
3 changes: 3 additions & 0 deletions cecli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,9 @@ def apply_model_overrides(model_name):
ratio = args.context_compaction_max_tokens
if max_input_tokens:
args.context_compaction_max_tokens = int(max_input_tokens * ratio)
else:
# Default since some models do not have max_input_tokens specified somehow
args.context_compaction_max_tokens = 65536
try:
mcp_servers = load_mcp_servers(
args.mcp_servers, args.mcp_servers_file, io, args.verbose, args.mcp_transport
Expand Down
14 changes: 10 additions & 4 deletions cecli/mcp/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ def __init__(
Initialize the MCP server manager.

Args:
mcp_servers: JSON string containing MCP server configurations
mcp_servers_file: Path to a JSON file containing MCP server configurations
servers: List of MCP Servers to manage
io: InputOutput instance for user interaction
verbose: Whether to output verbose logging
"""
self.io = io
self.verbose = verbose
self._servers = servers

self._server_tools: dict[str, list] = {} # Maps server name to its tools
self._connected_servers: set[McpServer] = set()

Expand Down Expand Up @@ -74,7 +74,7 @@ def get_server(self, name: str) -> McpServer | None:
return None

async def connect_all(self) -> None:
"""Connect to all MCP servers."""
"""Connect to all MCP servers while skipping ones that are not enabled."""
if self.is_connected:
self._log_verbose("Some MCP servers already connected")
return
Expand All @@ -93,7 +93,9 @@ async def connect_server(server: McpServer) -> tuple[McpServer, bool]:
self._log_error(f"Failed to connect to MCP server {server.name}: {e}")
return (server, False)

results = await asyncio.gather(*[connect_server(server) for server in self._servers])
results = await asyncio.gather(
*[connect_server(server) for server in self._servers if server.is_enabled]
)

for server, success in results:
if success:
Expand Down Expand Up @@ -142,6 +144,10 @@ async def connect_server(self, name: str) -> bool:
self._log_warning(f"MCP server not found: {name}")
return False

if not server.is_enabled:
self._log_verbose("MCP is not enabled.")
return False

if server in self._connected_servers:
self._log_verbose(f"MCP server already connected: {name}")
return True
Expand Down
16 changes: 14 additions & 2 deletions cecli/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def __init__(self, server_config, io=None, verbose=False):
"""
self.config = server_config
self.name = server_config.get("name", "unnamed-server")
self.is_enabled = server_config.get("enabled", True)
self.io = io
self.verbose = verbose
self.session = None
Expand All @@ -52,8 +53,13 @@ async def connect(self):
Otherwise, establishes a new connection and initializes the session.

Returns:
ClientSession: The active session
ClientSession: The active session if mcp is not disabled
"""
if not self.is_enabled:
if self.verbose and self.io:
self.io.tool_output(f"Enabled option is set to false for MCP server: {self.name}")
return None

if self.session is not None:
if self.verbose and self.io:
self.io.tool_output(f"Using existing session for MCP server: {self.name}")
Expand Down Expand Up @@ -123,7 +129,8 @@ async def _create_oauth_provider(self):
existing_redirect_uri = redirect_uris[0]
if self.verbose and self.io:
self.io.tool_output(
f"Found existing redirect URI: {existing_redirect_uri}", log_only=True
f"Found existing redirect URI: {existing_redirect_uri}",
log_only=True,
)

from .utils import find_available_port
Expand Down Expand Up @@ -187,6 +194,11 @@ def _create_transport(self, url, http_client):
raise NotImplementedError("Subclasses must implement _create_transport")

async def connect(self):
if not self.is_enabled:
if self.verbose and self.io:
self.io.tool_output(f"Enabled option is set to false for MCP server: {self.name}")
return None

if self.session is not None:
if self.verbose and self.io:
self.io.tool_output(f"Using existing session for {self.name}")
Expand Down
28 changes: 23 additions & 5 deletions cecli/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

# Global variable to store resolved args data for error reporting
resolved_args_data = None
error_prefix = []

FENCE = "`" * 3

Expand Down Expand Up @@ -68,10 +69,11 @@ def format_args_for_reporting(args):
pass

# Format the output line by line
lines = ["Configuration:"]
lines = ["Configuration:\n```yaml"]
for key, value in sorted(args_dict.items()):
lines.append(f"{key}: {value}")

lines.append("```")
return "\n".join(lines)


Expand All @@ -85,6 +87,17 @@ def set_args_error_data(args):
resolved_args_data = args


def get_error_prefix():
global error_prefix
return error_prefix


def update_error_prefix(prefix):
global error_prefix
error_prefix.append(f"{prefix}\n")
error_prefix = error_prefix[-10:]


def report_github_issue(issue_text, title=None, confirm=True):
"""
Compose a URL to open a new GitHub issue with the given text prefilled,
Expand All @@ -102,6 +115,7 @@ def report_github_issue(issue_text, title=None, confirm=True):
os_info = get_os_info() + "\n"
git_info = get_git_info() + "\n"
args_info = format_args_for_reporting(get_args_error_data()) + "\n"
prefix_info = "".join(get_error_prefix())

system_info = (
version_info
Expand All @@ -111,6 +125,8 @@ def report_github_issue(issue_text, title=None, confirm=True):
+ os_info
+ git_info
+ "\n"
+ prefix_info
+ "\n"
+ args_info
+ "\n"
)
Expand All @@ -123,11 +139,13 @@ def report_github_issue(issue_text, title=None, confirm=True):
issue_url = f"{github_issues}?{urllib.parse.urlencode(params)}"

if confirm:
print(f"\n# {title}\n")
print(issue_text.strip())
print()
# print(f"\n# {title}\n")
# print(issue_text.strip())
# print()
print("Please consider reporting this bug to help improve cecli!")
prompt = "Open a GitHub Issue pre-filled with the above error in your browser? (Y/n) "
prompt = (
"Open a GitHub Issue pre-filled with the above error messages in your browser? (Y/n) "
)
confirmation = input(prompt).strip().lower()

yes = not confirmation or confirmation.startswith("y")
Expand Down
6 changes: 5 additions & 1 deletion cecli/website/docs/config/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ mcp-servers-file: /path/to/mcp.json

These options are configurable in any of Aider's config file formats.

Also, you are able to say if you would like an mcp enabled/disabled in the config itself via `"enabled"` key
By default MCP servers are enabled, so you MUST explicitly disable them in the config if you dont wish
for them to be included when cecli starts up

### Flags

You can specify MCP servers directly on the command line using the `--mcp-servers` option with a JSON or YAML string:
Expand Down Expand Up @@ -204,7 +208,7 @@ mcp-servers:
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN=<access_token>",
"GITHUB_PERSONAL_ACCESS_TOKEN=<access_token>",
"ghcr.io/github/github-mcp-server"
]
```
Loading
Loading