Skip to content
Merged
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
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.8.dev"
__version__ = "0.95.9.dev"
safe_version = __version__

try:
Expand Down
6 changes: 0 additions & 6 deletions cecli/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,12 +757,6 @@ def get_parser(default_config_files, git_root):
help="Show release notes on first run of new version (default: None, ask user)",
default=None,
)
group.add_argument(
"--install-main-branch",
action="store_true",
help="Install the latest version from the main branch",
default=False,
)
group.add_argument(
"--upgrade",
"--update",
Expand Down
21 changes: 11 additions & 10 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
normalize_vector,
)
from cecli.helpers.skills import SkillsManager
from cecli.mcp.server import LocalServer
from cecli.mcp import LocalServer, McpServerManager
from cecli.repo import ANY_GIT_ERROR
from cecli.tools.utils.registry import ToolRegistry

Expand Down Expand Up @@ -209,14 +209,17 @@ async def initialize_mcp_tools(self):
local_tools = self.get_local_tool_schemas()
if not local_tools:
return

local_server_config = {"name": server_name}
local_server = LocalServer(local_server_config)
if not self.mcp_servers:
self.mcp_servers = []
if not any(isinstance(s, LocalServer) for s in self.mcp_servers):
self.mcp_servers.append(local_server)

if not self.mcp_manager:
self.mcp_manager = McpServerManager()
if not self.mcp_manager.get_server(server_name):
await self.mcp_manager.add_server(local_server)
if not self.mcp_tools:
self.mcp_tools = []

if server_name not in [name for name, _ in self.mcp_tools]:
self.mcp_tools.append((local_server.name, local_tools))

Expand Down Expand Up @@ -257,9 +260,7 @@ async def _execute_local_tool_calls(self, tool_calls_list):
t.get("function", {}).get("name") == norm_tool_name
for t in server_tools
):
server = next(
(s for s in self.mcp_servers if s.name == server_name), None
)
server = self.mcp_manager.get_server(server_name)
if server:
for params in parsed_args_list:
tasks.append(
Expand Down Expand Up @@ -955,7 +956,7 @@ async def _execute_tool_with_registry(self, norm_tool_name, params):
if self.mcp_tools:
for server_name, server_tools in self.mcp_tools:
if any(t.get("function", {}).get("name") == norm_tool_name for t in server_tools):
server = next((s for s in self.mcp_servers if s.name == server_name), None)
server = self.mcp_manager.get_server(server_name)
if server:
return await self._execute_mcp_tool(server, norm_tool_name, params)
else:
Expand Down Expand Up @@ -1499,7 +1500,7 @@ async def _apply_edits_from_response(self):
if shared_output:
self.io.tool_output("Shell command output:\n" + shared_output)
if self.auto_test and not self.reflected_message:
test_errors = await self.commands.cmd_test(self.test_cmd)
test_errors = await self.commands.execute("test", self.test_cmd)
if test_errors:
ok = await self.io.confirm_ask("Attempt to fix test errors?")
if ok:
Expand Down
35 changes: 20 additions & 15 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from cecli.io import ConfirmGroup, InputOutput
from cecli.linter import Linter
from cecli.llm import litellm
from cecli.mcp.server import LocalServer
from cecli.mcp import LocalServer
from cecli.models import RETRY_TIMEOUT
from cecli.reasoning_tags import (
REASONING_TAG,
Expand Down Expand Up @@ -138,7 +138,7 @@ class Coder:
chat_language = None
commit_language = None
file_watcher = None
mcp_servers = None
mcp_manager = None
mcp_tools = None
run_one_completed = True
compact_context_completed = True
Expand Down Expand Up @@ -249,8 +249,8 @@ async def create(

if res is not None:
if from_coder:
if from_coder.mcp_servers and kwargs.get("mcp_servers", False):
res.mcp_servers = from_coder.mcp_servers
if from_coder.mcp_manager:
res.mcp_manager = from_coder.mcp_manager
res.mcp_tools = from_coder.mcp_tools

# Transfer TUI app weak reference
Expand Down Expand Up @@ -316,7 +316,7 @@ def __init__(
file_watcher=None,
auto_copy_context=False,
auto_accept_architect=True,
mcp_servers=None,
mcp_manager=None,
enable_context_compaction=False,
context_compaction_max_tokens=None,
context_compaction_summary_tokens=8192,
Expand Down Expand Up @@ -350,7 +350,7 @@ def __init__(
self.args = args

self.num_cache_warming_pings = num_cache_warming_pings
self.mcp_servers = mcp_servers
self.mcp_manager = mcp_manager
self.enable_context_compaction = enable_context_compaction

self.context_compaction_max_tokens = context_compaction_max_tokens
Expand Down Expand Up @@ -1564,7 +1564,7 @@ async def generate(self, user_message, preproc):

def copy_context(self):
if self.auto_copy_context:
self.commands.cmd_copy_context()
self.commands.execute("copy-context", "")

async def get_input(self):
inchat_files = self.get_inchat_relative_files()
Expand Down Expand Up @@ -1684,7 +1684,7 @@ async def check_for_urls(self, inp: str) -> List[str]:
explicit_yes_required=self.args.yes_always_commands,
):
inp += "\n\n"
inp += await self.commands.do_run("web", url, return_content=True)
inp += await self.commands.execute("web", url, return_content=True)
else:
self.rejected_urls.add(url)

Expand Down Expand Up @@ -2438,7 +2438,7 @@ async def send_message(self, inp):
]

if edited and self.auto_test:
test_errors = await self.commands.cmd_test(self.test_cmd)
test_errors = await self.commands.execute("test", self.test_cmd)
self.test_outcome = not test_errors
if test_errors:
ok = await self.io.confirm_ask("Attempt to fix test errors?")
Expand Down Expand Up @@ -2562,7 +2562,7 @@ def _gather_server_tool_calls(self, tool_calls):
and tool_name_from_schema.lower() == tool_call.function.name.lower()
):
# Find the McpServer instance that will be used for communication
for server in self.mcp_servers:
for server in self.mcp_manager:
if server.name == server_name:
if server not in server_tool_calls:
server_tool_calls[server] = []
Expand Down Expand Up @@ -2740,6 +2740,7 @@ async def initialize_mcp_tools(self):
Initialize tools from all configured MCP servers. MCP Servers that fail to be
initialized will not be available to the Coder instance.
"""
# TODO(@gopar): refactor here once we have fully moved over to use the mcp manager
tools = []

async def get_server_tools(server):
Expand All @@ -2750,9 +2751,13 @@ async def get_server_tools(server):
return (server.name, server_tools)

try:
session = await server.connect()
did_connect = await self.mcp_manager.connect_server(server.name)
if not did_connect:
raise Exception("Failed to load tools")

server = self.mcp_manager.get_server(server.name)
server_tools = await experimental_mcp_client.load_mcp_tools(
session=session, format="openai"
session=server.session, format="openai"
)
return (server.name, server_tools)
except Exception as e:
Expand All @@ -2761,11 +2766,11 @@ async def get_server_tools(server):
return None

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

if self.mcp_servers:
if self.mcp_manager:
# Retry initialization in case of CancelledError
max_retries = 3
for i in range(max_retries):
Expand Down Expand Up @@ -3852,7 +3857,7 @@ def show_auto_commit_outcome(self, res):
self.coder_commit_hashes.add(commit_hash)
self.last_coder_commit_message = commit_message
if self.show_diffs:
self.commands.cmd_diff()
self.commands.execute("diff", "")

def show_undo_hint(self):
if not self.commit_before_message:
Expand Down
8 changes: 4 additions & 4 deletions cecli/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def get_commands(self):
commands = [f"/{cmd}" for cmd in registry_commands]
return sorted(commands)

async def do_run(self, cmd_name, args, **kwargs):
async def execute(self, cmd_name, args, **kwargs):
command_class = CommandRegistry.get_command(cmd_name)
if not command_class:
self.io.tool_output(f"Error: Command {cmd_name} not found.")
Expand Down Expand Up @@ -224,17 +224,17 @@ def matching_commands(self, inp):

async def run(self, inp):
if inp.startswith("!"):
return await self.do_run("run", inp[1:])
return await self.execute("run", inp[1:])
res = self.matching_commands(inp)
if res is None:
return
matching_commands, first_word, rest_inp = res
if len(matching_commands) == 1:
command = matching_commands[0][1:]
return await self.do_run(command, rest_inp)
return await self.execute(command, rest_inp)
elif first_word in matching_commands:
command = first_word[1:]
return await self.do_run(command, rest_inp)
return await self.execute(command, rest_inp)
elif len(matching_commands) > 1:
self.io.tool_error(f"Ambiguous command: {', '.join(matching_commands)}")
else:
Expand Down
8 changes: 0 additions & 8 deletions cecli/commands/exit.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@ class ExitCommand(BaseCommand):
@classmethod
async def execute(cls, io, coder, args, **kwargs):
"""Execute the exit command with given parameters."""
for server in coder.mcp_servers:
try:
await server.exit_stack.aclose()
except Exception:
pass

await asyncio.sleep(0)

# Check if running in TUI mode - use graceful exit to restore terminal
if hasattr(io, "request_exit"):
io.request_exit()
Expand Down
74 changes: 73 additions & 1 deletion cecli/commands/history_search.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from typing import List

from cecli.commands.utils.base_command import BaseCommand
Expand All @@ -12,10 +13,20 @@ class HistorySearchCommand(BaseCommand):
@classmethod
async def execute(cls, io, coder, args, **kwargs):
"""Execute the history-search command with given parameters."""
history_lines = io.get_input_history()
# Get history lines based on whether we're in TUI mode or not
if coder.tui and coder.tui():
# In TUI mode, parse the history file directly using our custom parser
history_lines = cls.parse_input_history_file(io.input_history_file)
else:
# In non-TUI mode, use the io.get_input_history() method
history_lines = io.get_input_history()

selected_lines = run_fzf(history_lines, coder=coder)
if selected_lines:
io.set_placeholder("".join(selected_lines))

if coder.tui and coder.tui():
coder.tui().set_input_value("".join(selected_lines))
return format_command_result(
io, "history-search", "Selected history lines and set placeholder"
)
Expand All @@ -38,3 +49,64 @@ def get_help(cls) -> str:
)
help_text += "Selected lines will be pasted into the input prompt for editing.\n"
return help_text

@classmethod
def parse_input_history_file(cls, file_path: str) -> List[str]:
"""Parse the input history file format.

The file format consists of blocks separated by timestamp lines starting with '#'.
Each block has lines starting with '+' for the actual input.

Args:
file_path: Path to the history file

Returns:
List of history entries (strings)
"""
if not file_path or not os.path.exists(file_path):
return []

try:
with open(file_path, "r") as f:
content = f.read()
except (OSError, IOError):
return []

# Parse the file format: blocks separated by timestamp lines starting with '#'
# Each block has lines starting with '+' for the actual input
history = []
current_block = []
in_block = False

for line in content.splitlines():
line = line.rstrip("\n")

if line.startswith("#"):
# This is a timestamp line - start a new block
if current_block:
# Join the current block lines and add to history
block_text = "\n".join(current_block)
history.append(block_text)
current_block = []
in_block = True
# Reset in_block if we encounter another timestamp without any + lines
# This handles consecutive timestamp lines
elif line.startswith("+") and in_block:
# This is an input line in the current block
# Remove the leading '+' and add to current block
# Use [1:] to remove the first character (the '+')
# This preserves any leading spaces that might be part of the input
current_block.append(line[1:])
elif line.strip() == "":
# Empty line - ignore
continue
else:
# Unexpected line format - skip it
continue

# Don't forget the last block
if current_block:
block_text = "\n".join(current_block)
history.append(block_text)

return history
Loading
Loading