Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3de0907
[gh-316] Add OAuth flow to MCP servers
gopar Dec 26, 2025
c898ddb
[gh-316] Reuse port we used to sign client up
gopar Dec 30, 2025
b3d501c
Update context_manager.py
BecoKo Jan 2, 2026
4fc6652
Suppress warning for the right Local MCP server name in base_coder.py
BecoKo Jan 2, 2026
da0ff27
Fix: Environment variables prefix
BecoKo Jan 2, 2026
2778086
Move commands.py to core.py under commands module
gopar Jan 2, 2026
d7e119b
feat: display per-1M token pricing for models in search output
chrisnestrud Jan 2, 2026
248e868
Rename SwitchCoder to SwitchCoderSignal to better detail what is happ…
gopar Jan 2, 2026
411f34a
Bump Version
dwash96 Jan 2, 2026
9ccccd9
Merge branch 'main' into gh-316-add-oauth-mcp-flow
gopar Jan 2, 2026
e0c7938
[gh-316] Fix remnants of aider vs cecli
gopar Jan 2, 2026
f68e9cf
test: add comprehensive print_matching_models pricing tests
chrisnestrud Jan 2, 2026
a810ca0
[gh-316] No need to store time it was saved. MCP sdk should do this
gopar Jan 2, 2026
2825a14
[gh-316] Fix argument positions leftover from merge conflict
gopar Jan 2, 2026
d19d0c9
Merge pull request #334 from gopar/gh-316-add-oauth-mcp-flow
dwash96 Jan 3, 2026
16c8c41
Merge pull request #356 from BecoKo/BecoKo-patch-3
dwash96 Jan 3, 2026
5c7034d
Merge pull request #357 from gopar/rename-switch-coder-exception
dwash96 Jan 3, 2026
c6bb413
Merge pull request #355 from BecoKo/BecoKo-patch-2
dwash96 Jan 3, 2026
97dbea0
Merge pull request #354 from BecoKo/BecoKo-patch-1
dwash96 Jan 3, 2026
c4bcaee
Merge pull request #359 from chrisnestrud/feature/show-prices
dwash96 Jan 3, 2026
5ecf894
Fix formatting
dwash96 Jan 3, 2026
fbc4679
Update MCP dependency version for oauth changes
dwash96 Jan 3, 2026
ad33f45
Fix pypi version check name
dwash96 Jan 3, 2026
7a4c1fb
Update env variable prefix part 2 to "CECLI_" and not "CECLI"
dwash96 Jan 3, 2026
3067b68
Update documentation for command line variables
dwash96 Jan 3, 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
8 changes: 5 additions & 3 deletions benchmark/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
# Cache for commit-hash -> version lookup
_VERSION_CACHE = {}

BENCHMARK_DNAME = Path(os.environ.get("CECLIBENCHMARK_DIR", "tmp.benchmarks"))
BENCHMARK_DNAME = Path(os.environ.get("CECLI_BENCHMARK_DIR", "tmp.benchmarks"))
EXERCISES_DIR_DEFAULT = "cecli-cat"
RESULTS_DIR_DEFAULT = "cat-results"

Expand Down Expand Up @@ -209,9 +209,11 @@ def main(
return 1
results_dir = resolved_results_dir

if not dry and "CECLIDOCKER" not in os.environ:
if not dry and "CECLI_DOCKER" not in os.environ:
logger.warning("Warning: Benchmarking runs unvetted code. Run in a docker container.")
logger.warning("Set CECLIDOCKER in the environment to by-pass this check at your own risk.")
logger.warning(
"Set CECLI_DOCKER in the environment to by-pass this check at your own risk."
)
return

# Check dirs exist
Expand Down
4 changes: 2 additions & 2 deletions benchmark/benchmark_classic.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
# Cache for commit-hash -> version lookup
_VERSION_CACHE = {}

BENCHMARK_DNAME = Path(os.environ.get("CECLIBENCHMARK_DIR", "tmp.benchmarks"))
BENCHMARK_DNAME = Path(os.environ.get("CECLI_BENCHMARK_DIR", "tmp.benchmarks"))

EXERCISES_DIR_DEFAULT = "polyglot-benchmark"

Expand Down Expand Up @@ -267,7 +267,7 @@ def main(
if repo.is_dirty():
commit_hash += "-dirty"

if "CECLIDOCKER" not in os.environ:
if "CECLI_DOCKER" not in os.environ:
print("Warning: benchmarking runs unvetted code from GPT, run in a docker container")
return

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.92.0.dev"
__version__ = "0.92.1.dev"
safe_version = __version__

try:
Expand Down
4 changes: 2 additions & 2 deletions cecli/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def get_parser(default_config_files, git_root):
add_config_file_help=True,
default_config_files=default_config_files,
config_file_parser_class=configargparse.YAMLConfigFileParser,
auto_env_var_prefix="CECLI",
auto_env_var_prefix="CECLI_",
)
# List of valid edit formats for argparse validation & shtab completion.
# Dynamically gather them from the registered coder classes so the list
Expand Down Expand Up @@ -567,7 +567,7 @@ def get_parser(default_config_files, git_root):

group.add_argument(
"--cecli-ignore",
metavar="CECLIIGNORE",
metavar="CECLI_IGNORE",
type=lambda path_str: resolve_cecli_ignore_path(path_str, git_root),
default=default_cecli_ignore_file,
help="Specify the cecli ignore file (default: .cecli.ignore in git root)",
Expand Down
4 changes: 2 additions & 2 deletions cecli/coders/architect_coder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import asyncio

from ..commands import SwitchCoder
from ..commands import SwitchCoderSignal
from .ask_coder import AskCoder
from .base_coder import Coder

Expand Down Expand Up @@ -60,4 +60,4 @@ async def reply_completed(self):
except Exception as e:
self.io.tool_error(e)

raise SwitchCoder(main_model=self.main_model, edit_format="architect")
raise SwitchCoderSignal(main_model=self.main_model, edit_format="architect")
14 changes: 7 additions & 7 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

import cecli.prompts.utils.system as prompts
from cecli import __version__, models, urls, utils
from cecli.commands import Commands, SwitchCoder
from cecli.commands import Commands, SwitchCoderSignal
from cecli.exceptions import LiteLLMExceptions
from cecli.helpers import coroutines
from cecli.helpers.profiler import TokenProfiler
Expand Down Expand Up @@ -1370,7 +1370,7 @@ async def _run_parallel(self, with_message=None, preproc=True):
if task.exception():
raise task.exception()

except (SwitchCoder, SystemExit):
except (SwitchCoderSignal, SystemExit):
# Re-raise SwitchCoder to be handled by outer try block
raise
except KeyboardInterrupt:
Expand Down Expand Up @@ -1461,7 +1461,7 @@ async def input_task(self, preproc):
self.io.set_placeholder("")
self.keyboard_interrupt()
await self.io.stop_task_streams()
except (SwitchCoder, SystemExit):
except (SwitchCoderSignal, SystemExit):
raise
except Exception as e:
if self.verbose or self.args.debug:
Expand Down Expand Up @@ -1496,7 +1496,7 @@ async def output_task(self, preproc):
if self.io.output_task.done():
exception = self.io.output_task.exception()
if exception:
if isinstance(exception, SwitchCoder):
if isinstance(exception, SwitchCoderSignal):
await self.io.output_task
raise exception

Expand All @@ -1519,7 +1519,7 @@ async def output_task(self, preproc):
self.io.stop_spinner()
self.keyboard_interrupt()
await self.io.stop_task_streams()
except (SwitchCoder, SystemExit):
except (SwitchCoderSignal, SystemExit):
raise
except Exception as e:
if self.verbose or self.args.debug:
Expand Down Expand Up @@ -2138,7 +2138,7 @@ def warm_cache(self, chunks):
return

delay = 5 * 60 - 5
delay = float(os.environ.get("CECLICACHE_KEEPALIVE_DELAY", delay))
delay = float(os.environ.get("CECLI_CACHE_KEEPALIVE_DELAY", delay))
self.next_cache_warm = time.time() + delay
self.warming_pings_left = self.num_cache_warming_pings
self.cache_warming_chunks = chunks
Expand Down Expand Up @@ -2741,7 +2741,7 @@ async def get_server_tools(server):
)
return (server.name, server_tools)
except Exception as e:
if server.name != "unnamed-server" and server.name != "local_tools":
if server.name != "unnamed-server" and server.name != "Local":
self.io.tool_warning(f"Error initializing MCP server {server.name}: {e}")
return None

Expand Down
51 changes: 2 additions & 49 deletions cecli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
BaseCommand pattern for modular, testable command execution.
"""

import sys
import traceback
from pathlib import Path

from .add import AddCommand
from .agent import AgentCommand
from .architect import ArchitectCommand
Expand All @@ -22,6 +18,7 @@
from .context_management import ContextManagementCommand
from .copy import CopyCommand
from .copy_context import CopyContextCommand
from .core import Commands, SwitchCoderSignal
from .diff import DiffCommand

# Import and register commands
Expand Down Expand Up @@ -127,50 +124,6 @@
CommandRegistry.register(LoadSkillCommand)
CommandRegistry.register(RemoveSkillCommand)

# Import SwitchCoder and Commands directly from commands.py
# We need to handle the circular import carefully

# Add parent directory to path to import commands.py directly
parent_dir = str(Path(__file__).parent.parent)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)

# Import the commands module directly
try:
import importlib.util

spec = importlib.util.spec_from_file_location(
"cecli.commands_module", Path(__file__).parent.parent / "commands.py"
)
commands_module = importlib.util.module_from_spec(spec)
sys.modules["cecli.commands_module"] = commands_module
spec.loader.exec_module(commands_module)

# Get the classes from the module
Commands = getattr(commands_module, "Commands", None)
SwitchCoder = getattr(commands_module, "SwitchCoder", None)

if Commands is None or SwitchCoder is None:
raise ImportError("Commands or SwitchCoder not found in commands.py")

except Exception as e:
# Print the error for debugging
print(f"Error importing commands.py: {e}")
traceback.print_exc()

# Fallback: define simple placeholder classes
class SwitchCoder(Exception):
def __init__(self, placeholder=None, **kwargs):
self.kwargs = kwargs
self.placeholder = placeholder

class Commands:
"""Placeholder for Commands class defined in original commands.py"""

def __init__(self, *args, **kwargs):
# Accept any arguments but do nothing
pass


__all__ = [
"BaseCommand",
Expand Down Expand Up @@ -234,6 +187,6 @@ def __init__(self, *args, **kwargs):
"CommandPrefixCommand",
"LoadSkillCommand",
"RemoveSkillCommand",
"SwitchCoder",
"SwitchCoderSignal",
"Commands",
]
4 changes: 2 additions & 2 deletions cecli/commands/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@ async def execute(cls, io, coder, args, **kwargs):
map_tokens = 0
map_mul_no_files = 1

from cecli.commands import SwitchCoder
from cecli.commands import SwitchCoderSignal

raise SwitchCoder(
raise SwitchCoderSignal(
edit_format=coder.edit_format,
summarize_from_coder=False,
from_coder=coder,
Expand Down
22 changes: 19 additions & 3 deletions cecli/commands.py → cecli/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,32 @@
import sys
from pathlib import Path

from cecli.commands.utils.registry import CommandRegistry
from cecli.helpers.file_searcher import handle_core_files
from cecli.repo import ANY_GIT_ERROR

from .commands.utils.registry import CommandRegistry

class SwitchCoderSignal(BaseException):
"""
Signal to switch the current Coder instance to a new configuration.

This is NOT an error - it's a control flow signal used to propagate
coder switching requests up through the async call stack. It carries
the kwargs needed to create a new Coder instance.

Note: Inherits from BaseException (like KeyboardInterrupt and SystemExit)
to avoid being caught by generic `except Exception` handlers, making the
non-error nature of this signal explicit.

Attributes:
kwargs: Configuration dict passed to Coder.create() for the new instance
placeholder: Optional placeholder text for the input prompt
"""

class SwitchCoder(Exception):
def __init__(self, placeholder=None, **kwargs):
self.kwargs = kwargs
self.placeholder = placeholder
super().__init__()


class Commands:
Expand Down Expand Up @@ -119,7 +135,7 @@ async def do_run(self, cmd_name, args, **kwargs):
except ANY_GIT_ERROR as err:
self.io.tool_error(f"Unable to complete {cmd_name}: {err}")
return
except SwitchCoder as e:
except SwitchCoderSignal as e:
raise e
except Exception as e:
self.io.tool_error(f"Error executing command {cmd_name}: {str(e)}")
Expand Down
8 changes: 4 additions & 4 deletions cecli/commands/drop.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,18 @@ async def execute(cls, io, coder, args, **kwargs):
return format_command_result(io, "drop", "Removed files from chat")

finally:
# This mimics the SwitchCoder behavior in the original cmd_drop
# This mimics the SwitchCoderSignal behavior in the original cmd_drop
if coder.repo_map:
map_tokens = coder.repo_map.max_map_tokens
map_mul_no_files = coder.repo_map.map_mul_no_files
else:
map_tokens = 0
map_mul_no_files = 1

# Raise SwitchCoder to trigger coder recreation
from . import SwitchCoder
# Raise SwitchCoderSignal to trigger coder recreation
from . import SwitchCoderSignal

raise SwitchCoder(
raise SwitchCoderSignal(
edit_format=coder.edit_format,
summarize_from_coder=False,
from_coder=coder,
Expand Down
4 changes: 2 additions & 2 deletions cecli/commands/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ async def execute(cls, io, coder, args, **kwargs):
map_tokens = 0
map_mul_no_files = 1

from cecli.commands import SwitchCoder
from cecli.commands import SwitchCoderSignal

raise SwitchCoder(
raise SwitchCoderSignal(
edit_format=coder.edit_format,
summarize_from_coder=False,
from_coder=help_coder,
Expand Down
6 changes: 3 additions & 3 deletions cecli/commands/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ async def execute(cls, io, coder, args, **kwargs):
try:
await commands_instance.run(cmd)
except Exception as e:
# Handle SwitchCoder exception specifically
if type(e).__name__ == "SwitchCoder":
# SwitchCoder is raised when switching between coder types (e.g., /architect, /ask).
# Handle SwitchCoderSignal exception specifically
if type(e).__name__ == "SwitchCoderSignal":
# SwitchCoderSignal is raised when switching between coder types (e.g., /architect, /ask).
# This is expected behavior, not an error. But this gets in the way when running `/load` so we
# ignore it and continue processing remaining commands.
should_raise_at_end = e
Expand Down
16 changes: 9 additions & 7 deletions cecli/commands/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,25 @@ async def execute(cls, io, coder, args, **kwargs):
coder.coder_commit_hashes = temp_coder.coder_commit_hashes

# Restore the original model configuration
from cecli.commands import SwitchCoder
from cecli.commands import SwitchCoderSignal

raise SwitchCoder(main_model=original_main_model, edit_format=original_edit_format)
raise SwitchCoderSignal(
main_model=original_main_model, edit_format=original_edit_format
)
except Exception as e:
# If there's an error, still restore the original model
if not isinstance(e, SwitchCoder):
if not isinstance(e, SwitchCoderSignal):
io.tool_error(str(e))
raise SwitchCoder(
raise SwitchCoderSignal(
main_model=original_main_model, edit_format=original_edit_format
)
else:
# Re-raise SwitchCoder if that's what was thrown
# Re-raise SwitchCoderSignal if that's what was thrown
raise
else:
from cecli.commands import SwitchCoder
from cecli.commands import SwitchCoderSignal

raise SwitchCoder(main_model=model, edit_format=new_edit_format)
raise SwitchCoderSignal(main_model=model, edit_format=new_edit_format)

@classmethod
def get_completions(cls, io, coder, args) -> List[str]:
Expand Down
6 changes: 3 additions & 3 deletions cecli/commands/reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ async def execute(cls, io, coder, args, **kwargs):
map_tokens = 0
map_mul_no_files = 1

# Raise SwitchCoder to trigger coder recreation
from . import SwitchCoder
# Raise SwitchCoderSignal to trigger coder recreation
from . import SwitchCoderSignal

raise SwitchCoder(
raise SwitchCoderSignal(
edit_format=coder.edit_format,
summarize_from_coder=False,
from_coder=coder,
Expand Down
8 changes: 4 additions & 4 deletions cecli/commands/utils/base_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ async def _generic_chat_command(cls, io, coder, args, edit_format, placeholder=N
"""
if not args.strip():
# Switch to the corresponding chat mode
from cecli.commands import SwitchCoder
from cecli.commands import SwitchCoderSignal

raise SwitchCoder(edit_format=edit_format)
raise SwitchCoderSignal(edit_format=edit_format)

from cecli.coders.base_coder import Coder

Expand All @@ -121,9 +121,9 @@ async def _generic_chat_command(cls, io, coder, args, edit_format, placeholder=N
await new_coder.generate(user_message=user_msg, preproc=False)
coder.coder_commit_hashes = new_coder.coder_commit_hashes

from cecli.commands import SwitchCoder
from cecli.commands import SwitchCoderSignal

raise SwitchCoder(
raise SwitchCoderSignal(
main_model=original_main_model,
edit_format=original_edit_format,
done_messages=new_coder.done_messages,
Expand Down
Loading
Loading