Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
465a93d
feat: Implement MCP connection keepalive tests
Jun 6, 2026
d634458
feat: implement MCP connection keepalive logic
Jun 7, 2026
d519a06
cli-37: merged
Jun 14, 2026
529d14d
cli-37: update
Jun 15, 2026
77b8384
cli-37: merged
Jun 15, 2026
9a35546
refactor: Update tests for MCP keepalive resilience and logging
Jun 17, 2026
44a38fc
Merge branch 'main' of github.com-personal:cecli-dev/cecli into cli-3…
Jun 20, 2026
ebe6f66
Merge branch 'v0.100.9' of github.com-personal:cecli-dev/cecli into c…
Jun 20, 2026
0bfe84d
fix: remove unused imports and variables in tests
Jun 20, 2026
e5ed51f
fix: Remove unused imports and variables from tests
Jun 20, 2026
20f8452
fix: Remove unused imports from test files
Jun 20, 2026
c91b806
fix: Update mcp.md with keepalive config example and explanation
Jun 20, 2026
c82d214
fix: Set default keepalive interval to 300 seconds
Jun 20, 2026
aa3c093
fix: Remove unused imports and variables from tests
Jun 20, 2026
1ec76ee
Rename EditText and ReadRange into EditFile and ReadFile
Jul 3, 2026
0e2a013
Since we enforce one match, let it fully strip content
Jul 3, 2026
9e33d7c
Add tests for message construction
Jul 3, 2026
8666685
Add snapshot content on first diff
Jul 3, 2026
66648eb
Don't pre-suppose a diff is always going to be provided
Jul 3, 2026
369daf6
Overhaul grep tool
Jul 3, 2026
2e0f277
De-garble edit_files failure output
Jul 3, 2026
8d41ac6
Update ls.py
Jul 3, 2026
7152a06
refactor: Check vision support against active model
Jul 6, 2026
d26d8ca
Merge pull request #598 from szmania/cli-53-agent-model-vision-support
dwash96 Jul 7, 2026
be8fe1a
#596: All notification settings to all trigger notification sub syste…
Jul 7, 2026
f801c53
Allow for global `~/.cecli/conf.yml` file as lowest precedence config
Jul 7, 2026
221e9f4
Add default location for subagents and skills
Jul 7, 2026
51463af
Add default .env file location and fix precedence for skills
Jul 7, 2026
649f4e8
Update documentation for default
Jul 7, 2026
f731bf7
Allow adding openai api compatible model providers in config
Jul 8, 2026
df5c5f5
Merge remote-tracking branch 'szmania/cli-37-mcp-connection-keepalive…
Jul 8, 2026
f0e80c6
Add HookHelpers class to make python hooks a bit easier to orchestrate
Jul 8, 2026
7c6e408
Fix MCP keep alive tests
Jul 8, 2026
9db0b5a
Fix awkward spacing on /reset and /clear
Jul 8, 2026
715e8cb
Add colors to file diff output
Jul 8, 2026
727669a
Update read_file description
Jul 8, 2026
5787c05
Update file tools
Jul 8, 2026
7a65689
Fix insert test
Jul 8, 2026
9e201c2
Fix MCP test
Jul 10, 2026
7df52d6
Update hashpos to use latin lower case chars but as an optimized pref…
Jul 10, 2026
d108312
Update compaction to be token and ration based. Single method handles…
Jul 10, 2026
142412d
Use full messages dict to trigger observations so they are more dense
Jul 10, 2026
ec8a992
Make observations more descriptive
Jul 10, 2026
0e516ff
Allow multiline matches for models mis targetting edits in EditFile tool
Jul 10, 2026
8a9ea90
#600: Only compact on commands that trigger LLM calls
Jul 10, 2026
67636c5
Be more stringent about edit_file content id format
Jul 10, 2026
166fce6
Update agent mode prompts with follow content ID examples
Jul 11, 2026
0a9f0b4
Split upper and lowercase chars for more single characters to use
Jul 11, 2026
2489057
Fix HashPos parser regex
Jul 11, 2026
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
11 changes: 10 additions & 1 deletion cecli/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ def get_parser(default_config_files, git_root):
"Specify a file with model tag overrides (e.g., gpt-4o:high -> reasoning_effort: high)"
),
).complete = shtab.FILE
group.add_argument(
"--model-providers",
metavar="MODEL_PROVIDERS_JSON",
help=(
"Specify custom OpenAI-compatible model providers as a JSON/YAML string (e.g.,"
' \'{"my-provider": {"api_base": "https://...", "api_key_env": ["MY_KEY"]}}\')'
),
default=None,
)
group.add_argument(
"--reasoning-effort",
type=str,
Expand Down Expand Up @@ -1113,7 +1122,7 @@ def get_parser(default_config_files, git_root):
group.add_argument(
"--notification-bell",
action=argparse.BooleanOptionalAction,
default=True,
default=False,
help=(
"Allow notification commands to produce an audible bell. When enabled, command"
" output is not suppressed so terminal bell escape sequences can ring through"
Expand Down
2 changes: 1 addition & 1 deletion cecli/change_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def track_change(

Parameters:
- file_path: Path to the file that was changed
- change_type: Type of change (e.g., 'edittext', 'insertlines')
- change_type: Type of change (e.g., 'editfile', 'insertlines')
- original_content: Original content before the change
- new_content: New content after the change
- metadata: Additional information about the change (line numbers, positions, etc.)
Expand Down
9 changes: 5 additions & 4 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ def __init__(self, *args, **kwargs):
"commandinteractive",
"explorecode",
"ls",
"readrange",
"readfile",
"grep",
"thinking",
"updatetodolist",
}
self.write_tools = {
"command",
"commandinteractive",
"edittext",
"editfile",
"undochange",
}
self.edit_allowed = True
Expand Down Expand Up @@ -165,6 +165,7 @@ def _get_agent_config(self):
config["command_timeout"] = nested.getter(config, "command_timeout", 30)
config["allowed_commands"] = nested.getter(config, "allowed_commands", [])
config["hot_reload"] = nested.getter(config, "hot_reload", False)
config["diff_colors"] = nested.getter(config, "diff_colors", True)
config["allow_nested_delegation"] = nested.getter(config, "allow_nested_delegation", False)

config["tools_paths"] = nested.getter(config, ["tools_paths", "tool_paths"], [])
Expand Down Expand Up @@ -828,7 +829,7 @@ async def gather_and_await():
if lint_errors:
lint_errors = lint_errors.replace(
"# Fix any linting errors below, if possible.",
"# Fix any linting errors below, if possible and then continue with your task.",
"# Fix the linting errors below, and then continue with your task.",
1,
)
ConversationService.get_manager(self).add_message(
Expand Down Expand Up @@ -1138,7 +1139,7 @@ def _generate_tool_context(self, repetitive_tools):
context_parts.append("\n\n")
context_parts.append("## File Editing Tools Disabled")
context_parts.append(
"File editing tools are currently disabled. Use `ReadRange` to determine the"
"File editing tools are currently disabled. Use `ReadFile` to determine the"
" current content ID prefixes needed to perform an edit and activate them when"
" you are ready to edit a file."
)
Expand Down
57 changes: 44 additions & 13 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1733,7 +1733,27 @@ async def generate(self, user_message, preproc):
# Compacting is wasteful since /clear will clear everything
# and /exit will exit the application
stripped = user_message.strip()
if stripped not in ("/clear", "/reset", "/exit", "/quit"):
is_command = self.commands.is_command(stripped)
is_allowed_command = False

if is_command:
res = self.commands.matching_commands(user_message)
if res is not None:
matching_commands, first_word, rest_inp = res
if len(matching_commands) == 1:
command = matching_commands[0]
splits = (rest_inp or "").split()
split_map = {
"/agent": 1,
"/architect": 1,
"/ask": 1,
"/code": 1,
"/model": 2,
}
if command in split_map and len(splits) >= split_map.get(command, 0):
is_allowed_command = True

if not is_command or is_allowed_command:
self.compact_context_completed = False
await self.compact_context_if_needed()
self.compact_context_completed = True
Expand Down Expand Up @@ -1977,27 +1997,34 @@ async def compact_context_if_needed(self, force=False, message=""):
done_messages = manager.get_messages_dict(MessageTag.DONE)
cur_messages = manager.get_messages_dict(MessageTag.CUR)
diff_messages = manager.get_messages_dict(MessageTag.DIFFS)
file_context_messages = manager.get_messages_dict(MessageTag.FILE_CONTEXTS)
all_messages = manager.get_messages_dict()

# Exclude first cur_message since that's the user's initial input
done_tokens = self.summarizer.count_tokens(done_messages)
cur_tokens = self.summarizer.count_tokens(cur_messages[1:] if len(cur_messages) > 1 else [])
diff_tokens = self.summarizer.count_tokens(diff_messages)
file_context_tokens = self.summarizer.count_tokens(file_context_messages)
all_tokens = self.summarizer.count_tokens(all_messages)

combined_tokens = done_tokens + cur_tokens + diff_tokens
message_tokens = done_tokens + cur_tokens
file_tokens = diff_tokens + file_context_tokens
combined_tokens = done_tokens + cur_tokens + diff_tokens + file_context_tokens

self.context_compaction_current_ratio = all_tokens / self.context_compaction_max_tokens

if force or (
all_tokens >= self.context_compaction_max_tokens * 0.9
and ConversationService.get_chunks(self).last_clear_count > 20
and file_tokens / max(message_tokens, 1) > 2
):
manager.clear_tag(MessageTag.LINT, ratio=0.33)
manager.clear_tag(MessageTag.DIFFS, ratio=0.33)
manager.clear_tag(MessageTag.FILE_CONTEXTS, ratio=0.33)
ConversationService.get_files(self).clear_file_cache()
ConversationService.get_chunks(self).flush_removals()
ConversationService.get_chunks(self).reset_clear_count()
ObservationService.get_instance(self).reset_index()

if not force and combined_tokens < self.context_compaction_max_tokens:
return
Expand Down Expand Up @@ -2031,9 +2058,9 @@ async def summarize_and_update(messages, tag):
manager.clear_tag(tag)

if tag == MessageTag.DONE:
manager.add_message({"role": "user", "content": text}, tag=tag)
manager.add_message(
{
manager.queue_message(message_dict={"role": "user", "content": text}, tag=tag)
manager.queue_message(
message_dict={
"role": "assistant",
"content": (
"Ok, I will use this summary and the observations as context for"
Expand All @@ -2044,21 +2071,22 @@ async def summarize_and_update(messages, tag):
)
else:
if self.last_user_message:
manager.add_message(
{"role": "user", "content": self.last_user_message}, tag=tag
manager.queue_message(
message_dict={"role": "user", "content": self.last_user_message},
tag=tag,
)

manager.add_message(
{
manager.queue_message(
message_dict={
"role": "assistant",
"content": "Ok. I am awaiting your summary of our goals to proceed.",
},
tag=tag,
force=True,
)

manager.add_message(
{
manager.queue_message(
message_dict={
"role": "user",
"content": (
"Here is a summary of our current goals and historical"
Expand All @@ -2068,8 +2096,8 @@ async def summarize_and_update(messages, tag):
tag=tag,
)

manager.add_message(
{
manager.queue_message(
message_dict={
"role": "assistant",
"content": (
"Ok, I will use this summary and proceed with our task. I will"
Expand Down Expand Up @@ -2102,6 +2130,9 @@ async def summarize_and_update(messages, tag):
manager.clear_tag(MessageTag.FILE_CONTEXTS)
ConversationService.get_files(self).clear_file_cache()
ConversationService.get_chunks(self).flush_removals()
ConversationService.get_chunks(self).reset_clear_count()
ObservationService.get_instance(self).reset_index()
self.format_chat_chunks()

except Exception as e:
self.io.tool_warning(f"Context compaction failed: {e}")
Expand Down
5 changes: 3 additions & 2 deletions cecli/commands/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,11 @@ async def execute(cls, io, coder, args, **kwargs):
else:
io.tool_error(f"Cannot add {matched_file} as it's not part of the repository")
else:
if is_image_file(matched_file) and not coder.main_model.info.get("supports_vision"):
active_model = coder.get_active_model()
if is_image_file(matched_file) and not active_model.info.get("supports_vision"):
io.tool_error(
f"Cannot add image file {matched_file} as the"
f" {coder.main_model.name} does not support images."
f" {active_model.name} does not support images."
)
continue
content = io.read_text(abs_file_path)
Expand Down
5 changes: 2 additions & 3 deletions cecli/commands/clear.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ async def execute(cls, io, coder, args, **kwargs):

# Clear TUI output if available
if coder.tui and coder.tui():
coder.tui().action_clear_output()
coder.tui().call_later(coder.tui().action_clear_output)

io.tool_output("All chat history cleared.")
return format_command_result(io, "clear", "Cleared chat history")
return format_command_result(io, "clear", "All chat history cleared.")

@classmethod
def get_completions(cls, io, coder, args) -> List[str]:
Expand Down
5 changes: 3 additions & 2 deletions cecli/commands/read_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,11 @@ def _add_read_only_file(
source_mode="read-only",
target_mode="read-only",
):
if is_image_file(original_name) and not coder.main_model.info.get("supports_vision"):
active_model = coder.get_active_model()
if is_image_file(original_name) and not active_model.info.get("supports_vision"):
io.tool_error(
f"Cannot add image file {original_name} as the"
f" {coder.main_model.name} does not support images."
f" {active_model.name} does not support images."
)
return

Expand Down
5 changes: 3 additions & 2 deletions cecli/commands/read_only_stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,11 @@ def _add_read_only_file(
source_mode="read-only",
target_mode="read-only",
):
if is_image_file(original_name) and not coder.main_model.info.get("supports_vision"):
active_model = coder.get_active_model()
if is_image_file(original_name) and not active_model.info.get("supports_vision"):
io.tool_error(
f"Cannot add image file {original_name} as the"
f" {coder.main_model.name} does not support images."
f" {active_model.name} does not support images."
)
return

Expand Down
2 changes: 1 addition & 1 deletion cecli/commands/reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async def execute(cls, io, coder, args, **kwargs):

# Clear TUI output if available
if coder.tui and coder.tui():
coder.tui().action_clear_output()
coder.tui().call_later(coder.tui().action_clear_output)
else:
io.tool_output("All files dropped and chat history cleared.")

Expand Down
7 changes: 6 additions & 1 deletion cecli/helpers/agents/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,13 @@ def build_registry(cls, paths: List[str]) -> None:

from .config import parse_subagent_file

# Always check the default sub-agents directory in the user's home
default_dir = str(Path.home() / ".cecli" / "subagents")
if default_dir not in paths:
paths = [default_dir] + list(paths)

for directory in paths:
dir_path = Path(directory)
dir_path = Path(directory).expanduser()
if not dir_path.is_dir():
continue
for md_file in sorted(dir_path.glob("*.md")):
Expand Down
1 change: 1 addition & 0 deletions cecli/helpers/conversation/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ def generate_diff(self, fname: str) -> Optional[str]:
)

if not snapshot_content:
self.add_file(abs_fname, content=current_content)
return None

# Generate diff between snapshot and current content using hashline helper
Expand Down
Loading
Loading