Skip to content

Commit 38dc662

Browse files
authored
Merge pull request #383 from dwash96/v0.95.9
V0.95.9
2 parents 930eeb2 + 091f7f6 commit 38dc662

31 files changed

Lines changed: 1335 additions & 502 deletions

cecli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from packaging import version
22

3-
__version__ = "0.95.8.dev"
3+
__version__ = "0.95.9.dev"
44
safe_version = __version__
55

66
try:

cecli/args.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -757,12 +757,6 @@ def get_parser(default_config_files, git_root):
757757
help="Show release notes on first run of new version (default: None, ask user)",
758758
default=None,
759759
)
760-
group.add_argument(
761-
"--install-main-branch",
762-
action="store_true",
763-
help="Install the latest version from the main branch",
764-
default=False,
765-
)
766760
group.add_argument(
767761
"--upgrade",
768762
"--update",

cecli/coders/agent_coder.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
normalize_vector,
2424
)
2525
from cecli.helpers.skills import SkillsManager
26-
from cecli.mcp.server import LocalServer
26+
from cecli.mcp import LocalServer, McpServerManager
2727
from cecli.repo import ANY_GIT_ERROR
2828
from cecli.tools.utils.registry import ToolRegistry
2929

@@ -209,14 +209,17 @@ async def initialize_mcp_tools(self):
209209
local_tools = self.get_local_tool_schemas()
210210
if not local_tools:
211211
return
212+
212213
local_server_config = {"name": server_name}
213214
local_server = LocalServer(local_server_config)
214-
if not self.mcp_servers:
215-
self.mcp_servers = []
216-
if not any(isinstance(s, LocalServer) for s in self.mcp_servers):
217-
self.mcp_servers.append(local_server)
215+
216+
if not self.mcp_manager:
217+
self.mcp_manager = McpServerManager()
218+
if not self.mcp_manager.get_server(server_name):
219+
await self.mcp_manager.add_server(local_server)
218220
if not self.mcp_tools:
219221
self.mcp_tools = []
222+
220223
if server_name not in [name for name, _ in self.mcp_tools]:
221224
self.mcp_tools.append((local_server.name, local_tools))
222225

@@ -257,9 +260,7 @@ async def _execute_local_tool_calls(self, tool_calls_list):
257260
t.get("function", {}).get("name") == norm_tool_name
258261
for t in server_tools
259262
):
260-
server = next(
261-
(s for s in self.mcp_servers if s.name == server_name), None
262-
)
263+
server = self.mcp_manager.get_server(server_name)
263264
if server:
264265
for params in parsed_args_list:
265266
tasks.append(
@@ -955,7 +956,7 @@ async def _execute_tool_with_registry(self, norm_tool_name, params):
955956
if self.mcp_tools:
956957
for server_name, server_tools in self.mcp_tools:
957958
if any(t.get("function", {}).get("name") == norm_tool_name for t in server_tools):
958-
server = next((s for s in self.mcp_servers if s.name == server_name), None)
959+
server = self.mcp_manager.get_server(server_name)
959960
if server:
960961
return await self._execute_mcp_tool(server, norm_tool_name, params)
961962
else:
@@ -1499,7 +1500,7 @@ async def _apply_edits_from_response(self):
14991500
if shared_output:
15001501
self.io.tool_output("Shell command output:\n" + shared_output)
15011502
if self.auto_test and not self.reflected_message:
1502-
test_errors = await self.commands.cmd_test(self.test_cmd)
1503+
test_errors = await self.commands.execute("test", self.test_cmd)
15031504
if test_errors:
15041505
ok = await self.io.confirm_ask("Attempt to fix test errors?")
15051506
if ok:

cecli/coders/base_coder.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
from cecli.io import ConfirmGroup, InputOutput
4545
from cecli.linter import Linter
4646
from cecli.llm import litellm
47-
from cecli.mcp.server import LocalServer
47+
from cecli.mcp import LocalServer
4848
from cecli.models import RETRY_TIMEOUT
4949
from cecli.reasoning_tags import (
5050
REASONING_TAG,
@@ -138,7 +138,7 @@ class Coder:
138138
chat_language = None
139139
commit_language = None
140140
file_watcher = None
141-
mcp_servers = None
141+
mcp_manager = None
142142
mcp_tools = None
143143
run_one_completed = True
144144
compact_context_completed = True
@@ -249,8 +249,8 @@ async def create(
249249

250250
if res is not None:
251251
if from_coder:
252-
if from_coder.mcp_servers and kwargs.get("mcp_servers", False):
253-
res.mcp_servers = from_coder.mcp_servers
252+
if from_coder.mcp_manager:
253+
res.mcp_manager = from_coder.mcp_manager
254254
res.mcp_tools = from_coder.mcp_tools
255255

256256
# Transfer TUI app weak reference
@@ -316,7 +316,7 @@ def __init__(
316316
file_watcher=None,
317317
auto_copy_context=False,
318318
auto_accept_architect=True,
319-
mcp_servers=None,
319+
mcp_manager=None,
320320
enable_context_compaction=False,
321321
context_compaction_max_tokens=None,
322322
context_compaction_summary_tokens=8192,
@@ -350,7 +350,7 @@ def __init__(
350350
self.args = args
351351

352352
self.num_cache_warming_pings = num_cache_warming_pings
353-
self.mcp_servers = mcp_servers
353+
self.mcp_manager = mcp_manager
354354
self.enable_context_compaction = enable_context_compaction
355355

356356
self.context_compaction_max_tokens = context_compaction_max_tokens
@@ -1564,7 +1564,7 @@ async def generate(self, user_message, preproc):
15641564

15651565
def copy_context(self):
15661566
if self.auto_copy_context:
1567-
self.commands.cmd_copy_context()
1567+
self.commands.execute("copy-context", "")
15681568

15691569
async def get_input(self):
15701570
inchat_files = self.get_inchat_relative_files()
@@ -1684,7 +1684,7 @@ async def check_for_urls(self, inp: str) -> List[str]:
16841684
explicit_yes_required=self.args.yes_always_commands,
16851685
):
16861686
inp += "\n\n"
1687-
inp += await self.commands.do_run("web", url, return_content=True)
1687+
inp += await self.commands.execute("web", url, return_content=True)
16881688
else:
16891689
self.rejected_urls.add(url)
16901690

@@ -2438,7 +2438,7 @@ async def send_message(self, inp):
24382438
]
24392439

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

27452746
async def get_server_tools(server):
@@ -2750,9 +2751,13 @@ async def get_server_tools(server):
27502751
return (server.name, server_tools)
27512752

27522753
try:
2753-
session = await server.connect()
2754+
did_connect = await self.mcp_manager.connect_server(server.name)
2755+
if not did_connect:
2756+
raise Exception("Failed to load tools")
2757+
2758+
server = self.mcp_manager.get_server(server.name)
27542759
server_tools = await experimental_mcp_client.load_mcp_tools(
2755-
session=session, format="openai"
2760+
session=server.session, format="openai"
27562761
)
27572762
return (server.name, server_tools)
27582763
except Exception as e:
@@ -2761,11 +2766,11 @@ async def get_server_tools(server):
27612766
return None
27622767

27632768
async def get_all_server_tools():
2764-
tasks = [get_server_tools(server) for server in self.mcp_servers]
2769+
tasks = [get_server_tools(server) for server in self.mcp_manager]
27652770
results = await asyncio.gather(*tasks)
27662771
return [result for result in results if result is not None]
27672772

2768-
if self.mcp_servers:
2773+
if self.mcp_manager:
27692774
# Retry initialization in case of CancelledError
27702775
max_retries = 3
27712776
for i in range(max_retries):
@@ -3852,7 +3857,7 @@ def show_auto_commit_outcome(self, res):
38523857
self.coder_commit_hashes.add(commit_hash)
38533858
self.last_coder_commit_message = commit_message
38543859
if self.show_diffs:
3855-
self.commands.cmd_diff()
3860+
self.commands.execute("diff", "")
38563861

38573862
def show_undo_hint(self):
38583863
if not self.commit_before_message:

cecli/commands/core.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def get_commands(self):
178178
commands = [f"/{cmd}" for cmd in registry_commands]
179179
return sorted(commands)
180180

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

225225
async def run(self, inp):
226226
if inp.startswith("!"):
227-
return await self.do_run("run", inp[1:])
227+
return await self.execute("run", inp[1:])
228228
res = self.matching_commands(inp)
229229
if res is None:
230230
return
231231
matching_commands, first_word, rest_inp = res
232232
if len(matching_commands) == 1:
233233
command = matching_commands[0][1:]
234-
return await self.do_run(command, rest_inp)
234+
return await self.execute(command, rest_inp)
235235
elif first_word in matching_commands:
236236
command = first_word[1:]
237-
return await self.do_run(command, rest_inp)
237+
return await self.execute(command, rest_inp)
238238
elif len(matching_commands) > 1:
239239
self.io.tool_error(f"Ambiguous command: {', '.join(matching_commands)}")
240240
else:

cecli/commands/exit.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,6 @@ class ExitCommand(BaseCommand):
1414
@classmethod
1515
async def execute(cls, io, coder, args, **kwargs):
1616
"""Execute the exit command with given parameters."""
17-
for server in coder.mcp_servers:
18-
try:
19-
await server.exit_stack.aclose()
20-
except Exception:
21-
pass
22-
23-
await asyncio.sleep(0)
24-
2517
# Check if running in TUI mode - use graceful exit to restore terminal
2618
if hasattr(io, "request_exit"):
2719
io.request_exit()

cecli/commands/history_search.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from typing import List
23

34
from cecli.commands.utils.base_command import BaseCommand
@@ -12,10 +13,20 @@ class HistorySearchCommand(BaseCommand):
1213
@classmethod
1314
async def execute(cls, io, coder, args, **kwargs):
1415
"""Execute the history-search command with given parameters."""
15-
history_lines = io.get_input_history()
16+
# Get history lines based on whether we're in TUI mode or not
17+
if coder.tui and coder.tui():
18+
# In TUI mode, parse the history file directly using our custom parser
19+
history_lines = cls.parse_input_history_file(io.input_history_file)
20+
else:
21+
# In non-TUI mode, use the io.get_input_history() method
22+
history_lines = io.get_input_history()
23+
1624
selected_lines = run_fzf(history_lines, coder=coder)
1725
if selected_lines:
1826
io.set_placeholder("".join(selected_lines))
27+
28+
if coder.tui and coder.tui():
29+
coder.tui().set_input_value("".join(selected_lines))
1930
return format_command_result(
2031
io, "history-search", "Selected history lines and set placeholder"
2132
)
@@ -38,3 +49,64 @@ def get_help(cls) -> str:
3849
)
3950
help_text += "Selected lines will be pasted into the input prompt for editing.\n"
4051
return help_text
52+
53+
@classmethod
54+
def parse_input_history_file(cls, file_path: str) -> List[str]:
55+
"""Parse the input history file format.
56+
57+
The file format consists of blocks separated by timestamp lines starting with '#'.
58+
Each block has lines starting with '+' for the actual input.
59+
60+
Args:
61+
file_path: Path to the history file
62+
63+
Returns:
64+
List of history entries (strings)
65+
"""
66+
if not file_path or not os.path.exists(file_path):
67+
return []
68+
69+
try:
70+
with open(file_path, "r") as f:
71+
content = f.read()
72+
except (OSError, IOError):
73+
return []
74+
75+
# Parse the file format: blocks separated by timestamp lines starting with '#'
76+
# Each block has lines starting with '+' for the actual input
77+
history = []
78+
current_block = []
79+
in_block = False
80+
81+
for line in content.splitlines():
82+
line = line.rstrip("\n")
83+
84+
if line.startswith("#"):
85+
# This is a timestamp line - start a new block
86+
if current_block:
87+
# Join the current block lines and add to history
88+
block_text = "\n".join(current_block)
89+
history.append(block_text)
90+
current_block = []
91+
in_block = True
92+
# Reset in_block if we encounter another timestamp without any + lines
93+
# This handles consecutive timestamp lines
94+
elif line.startswith("+") and in_block:
95+
# This is an input line in the current block
96+
# Remove the leading '+' and add to current block
97+
# Use [1:] to remove the first character (the '+')
98+
# This preserves any leading spaces that might be part of the input
99+
current_block.append(line[1:])
100+
elif line.strip() == "":
101+
# Empty line - ignore
102+
continue
103+
else:
104+
# Unexpected line format - skip it
105+
continue
106+
107+
# Don't forget the last block
108+
if current_block:
109+
block_text = "\n".join(current_block)
110+
history.append(block_text)
111+
112+
return history

0 commit comments

Comments
 (0)