diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index abe35a1c0ee..004aed0b6b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,149 +7,60 @@ contribute. ## Bug Reports and Feature Requests -Please submit bug reports and feature requests as GitHub issues. This -helps us to keep track of them and discuss potential solutions or -enhancements. - -## LLM Benchmark Results - -Contributions of -[LLM benchmark results](https://aider.chat/docs/leaderboards/) -are welcome! -See the -[benchmark README](https://github.com/Aider-AI/aider/blob/main/benchmark/README.md) -for information on running aider's code editing benchmarks. -Submit results by opening a PR with edits to the -[benchmark results data files](https://github.com/Aider-AI/aider/blob/main/aider/website/_data/). +Please submit bug reports as GitHub issues. We would prefer feature requests +be brought directly to the [Discord Chat](https://discord.gg/AX9ZEA7nJn). +This should be faster for triaging feature requests and scoping hem out. ## Pull Requests We appreciate your pull requests. For small changes, feel free to submit a PR directly. If you are considering a large or significant -change, please discuss it in a GitHub issue before submitting the +change, please discuss it in the [Discord Chat](https://discord.gg/AX9ZEA7nJn) before submitting the PR. This will save both you and the maintainers time, and it helps to ensure that your contributions can be integrated smoothly. -## Licensing - -Before contributing a PR, please review our -[Individual Contributor License Agreement](https://aider.chat/docs/legal/contributor-agreement.html). -All contributors will be asked to complete the agreement as part of the PR process. - -## Setting up a Development Environment - -### Clone the Repository - -``` -git clone https://github.com/Aider-AI/aider.git -cd aider -``` - -### Create a Virtual Environment - -It is recommended to create a virtual environment outside of the repository to keep your development environment isolated. - -#### Using `venv` (Python 3.9 and later) - -``` -python -m venv /path/to/venv -``` - -### Activate the Virtual Environment - -#### On Windows - -``` -/path/to/venv/Scripts/activate -``` - -#### On Unix or macOS - -``` -source /path/to/venv/bin/activate -``` - -### Install the Project in Editable Mode - -This step allows you to make changes to the source code and have them take effect immediately without reinstalling the package. - -``` -pip install -e . -``` - -### Install the Project Dependencies - -``` -pip install -r requirements.txt -``` - -For development, at least install the development dependencies: +## Setting up the Development Environment -``` -pip install -r requirements/requirements-dev.txt -``` - -Consider installing other optional dependencies from the `requirements/` directory, if your development work needs them. +```bash +# Clone the repository +git clone https://github.com/dwash96/aider-ce.git +cd aider-ce -Note that these dependency files are generated by `./scripts/pip-compile.sh` and then committed. See [Managing Dependencies](#managing-dependencies). +# Make a venv +python3 -m venv venv +source venv/bin/activate -### Install Pre-commit Hooks (Optional) +# Install UV because it's superior +pip install uv -The project uses pre-commit hooks for code formatting and linting. If you want to install and use these hooks, run: +# Build Project +uv pip install -e . -``` +# Add tool chain +uv install pre-commit pre-commit install -``` - -This will automatically run the pre-commit hooks when you commit changes to the repository. -Now you should have a fully functional development environment for the Aider project. You can start making changes, running tests, and contributing to the project. +# Run Program +aider-ce -### Handy Opinionated Setup Commands for MacOS / Linux +# OR! -Here's an example of following the setup instructions above, for your copy/paste pleasure if your system works the same. Start in the project directory. +cecli ``` -python3 -m venv ../aider_venv \ - && source ../aider_venv/bin/activate \ - && pip3 install -e . \ - && pip3 install -r requirements.txt \ - && pip3 install -r requirements/requirements-dev.txt -``` - -### Running Tests - -Just run `pytest`. ### Building the Docker Image The project includes a `Dockerfile` for building a Docker image. You can build the image by running: ``` -docker build -t aider -f docker/Dockerfile . -``` - -### Building the Documentation - -The project's documentation is built using Jekyll and hosted on GitHub Pages. To build the documentation locally, follow these steps: +docker build -t aider-ce -f docker/Dockerfile . -1. Install Ruby and Bundler (if not already installed). -2. Navigate to the `aider/website` directory. -3. Install the required gems: - ``` - bundle install - ``` -4. Build the documentation: - ``` - bundle exec jekyll build - ``` -5. Preview the website while editing (optional): - ``` - bundle exec jekyll serve - ``` +# OR! -The built documentation will be available in the `aider/website/_site` directory. +docker build -t cecli -f docker/Dockerfile . +``` ## Coding Standards @@ -161,10 +72,6 @@ Aider supports Python versions 3.9, 3.10, 3.11, and 3.12. When contributing code The project follows the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide for Python code, with a maximum line length of 100 characters. Additionally, the project uses [isort](https://pycqa.github.io/isort/) and [Black](https://black.readthedocs.io/en/stable/) for sorting imports and code formatting, respectively. Please install the pre-commit hooks to automatically format your code before committing changes. -### No Type Hints - -The project does not use type hints. - ### Testing The project uses [pytest](https://docs.pytest.org/en/latest/) for running unit tests. The test files are located in the `aider/tests` directory and follow the naming convention `test_*.py`. @@ -226,16 +133,23 @@ You can also pass one argument to `pip-compile.sh`, which will flow through to ` ./scripts/pip-compile.sh --upgrade ``` -### Pre-commit Hooks - -The project uses [pre-commit](https://pre-commit.com/) hooks to automatically format code, lint, and run other checks before committing changes. After cloning the repository, run the following command to set up the pre-commit hooks: +### Building the Documentation -``` -pre-commit install -``` +The project's documentation is built using Jekyll and hosted on GitHub Pages. To build the documentation locally, follow these steps: -pre-commit will then run automatically on each `git commit` command. You can use the following command line to run pre-commit manually: +1. Install Ruby and Bundler (if not already installed). +2. Navigate to the `aider/website` directory. +3. Install the required gems: + ``` + bundle install + ``` +4. Build the documentation: + ``` + bundle exec jekyll build + ``` +5. Preview the website while editing (optional): + ``` + bundle exec jekyll serve + ``` -``` -pre-commit run --all-files -``` +The built documentation will be available in the `aider/website/_site` directory. diff --git a/README.md b/README.md index 2406d0259ca..9e0b097610e 100644 --- a/README.md +++ b/README.md @@ -68,26 +68,23 @@ cache-prompts: true check-update: true debug: false enable-context-compaction: true +context-compaction-max-tokens: 64000 env-file: .aider.env multiline: true preserve-todo-list: true show-model-warnings: true +use-enhanced-map: true watch-files: false -agent-config: | - { - "large_file_token_threshold": 12500, - "skip_cli_confirmations": false - } -mcp-servers: | - { - "mcpServers": - { - "context7":{ - "transport":"http", - "url":"https://mcp.context7.com/mcp" - } - } - } + +agent-config: + large_file_token_threshold: 12500 + skip_cli_confirmations: false + +mcp-servers: + mcpServers: + context7: + transport: http + url: https://mcp.context7.com/mcp ``` Use the adjacent .aider.env file to store model api keys as environment variables, e.g: diff --git a/aider/__init__.py b/aider/__init__.py index 2c8d3aa69ce..00ed12f582a 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.89.3.dev" +__version__ = "0.89.4.dev" safe_version = __version__ try: diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 6e6f758af8b..da8246edfb4 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -3001,8 +3001,8 @@ async def show_send_output_stream(self, completion): await asyncio.sleep(0.1) # Yield control and wait briefly if isinstance(chunk, str): - text = chunk - received_content = True + self.io.tool_error(chunk) + continue else: if len(chunk.choices) == 0: continue diff --git a/aider/coders/chat_chunks.py b/aider/coders/chat_chunks.py index ce2b547c152..eda14e48f47 100644 --- a/aider/coders/chat_chunks.py +++ b/aider/coders/chat_chunks.py @@ -46,40 +46,49 @@ def all_messages(self): ) def add_cache_control_headers(self): - if self.examples: - self.add_cache_control(self.examples) + # Limit to 4 cacheable blocks to appease Anthropic's limits on chunk caching + if self.format_list("readonly_files"): + self.add_cache_control(self.format_list("readonly_files")) + elif self.format_list("static"): + self.add_cache_control(self.format_list("static")) + elif self.format_list("examples"): + self.add_cache_control(self.format_list("examples")) else: - self.add_cache_control(self.system) + self.add_cache_control(self.format_list("system")) # The files form a cacheable block. # The block starts with readonly_files and ends with chat_files. # So we mark the end of chat_files. - self.add_cache_control(self.chat_files) + # self.add_cache_control(self.add_cache_control(self.format_list("chat_files")) # The repo map is its own cacheable block. - self.add_cache_control(self.repo) + if self.format_list("repo"): + self.add_cache_control(self.format_list("repo")) + elif self.format_list("chat_files"): + self.add_cache_control(self.format_list("chat_files")) # The history is ephemeral on its own. - self.add_cache_control(self.done) + self.add_cache_control(self.add_cache_control(self.format_list("cur")), penultimate=True) # Per this: https://github.com/BerriAI/litellm/issues/10226 # The first and second to last messages are cache optimal # Since caches are also written to incrementally and you need # the past and current states to properly append and gain # efficiencies/savings in cache writing - def add_cache_control(self, messages): - if not messages or len(messages) < 2: + def add_cache_control(self, messages, penultimate=False): + if not messages: return - content = messages[-2]["content"] - if type(content) is str: - content = dict( - type="text", - text=content, - ) - content["cache_control"] = {"type": "ephemeral"} + if penultimate and len(messages) < 2: + content = messages[-2]["content"] + if type(content) is str: + content = dict( + type="text", + text=content, + ) + content["cache_control"] = {"type": "ephemeral"} - messages[-2]["content"] = [content] + messages[-2]["content"] = [content] content = messages[-1]["content"] if type(content) is str: diff --git a/aider/main.py b/aider/main.py index 0ac1bbd54ff..148b254dbee 100644 --- a/aider/main.py +++ b/aider/main.py @@ -44,6 +44,44 @@ from .dump import dump # noqa: F401 +def convert_yaml_to_json_string(value): + """ + Convert YAML dict/list values to JSON strings for compatibility. + + configargparse.YAMLConfigFileParser converts YAML to Python objects, + but some arguments expect JSON strings. This function handles: + - Direct dict/list objects + - String representations of dicts/lists (Python literals) + - Already JSON strings (passed through unchanged) + + Args: + value: The value to convert + + Returns: + str: JSON string if value is a dict/list, otherwise the original value + """ + if value is None: + return None + + if isinstance(value, (dict, list)): + return json.dumps(value) + + if isinstance(value, str): + # configargparse might convert dict to string representation + # Try to parse it as a Python literal + try: + import ast + + parsed = ast.literal_eval(value) + if isinstance(parsed, (dict, list)): + return json.dumps(parsed) + except (SyntaxError, ValueError): + # If it's not a Python literal, assume it's already JSON + pass + + return value + + def check_config_files_for_yes(config_files): found = False for config_file in config_files: @@ -509,6 +547,15 @@ async def main_async(argv=None, input=None, output=None, force_git_root=None, re args = parser.parse_args(argv) set_args_error_data(args) + # Convert YAML dict arguments to JSON strings for compatibility + # configargparse.YAMLConfigFileParser converts YAML to Python objects, + # but some arguments expect JSON strings + if hasattr(args, "agent_config") and args.agent_config is not None: + args.agent_config = convert_yaml_to_json_string(args.agent_config) + + if hasattr(args, "mcp_servers") and args.mcp_servers is not None: + args.mcp_servers = convert_yaml_to_json_string(args.mcp_servers) + if args.debug: global log_file os.makedirs(".aider/logs/", exist_ok=True) diff --git a/aider/models.py b/aider/models.py index 082e4300f67..ac2b9925c37 100644 --- a/aider/models.py +++ b/aider/models.py @@ -901,6 +901,12 @@ def is_deepseek(self): return return True + def is_anthropic(self): + name = self.name.lower() + if "claude" not in name: + return + return True + def is_ollama(self): return self.name.startswith("ollama/") or self.name.startswith("ollama_chat/") @@ -977,21 +983,26 @@ async def send_completion( dump(kwargs) kwargs["messages"] = messages - # Per this: https://github.com/BerriAI/litellm/issues/10226 - # The first and second to last messages are cache optimal - # Since caches are also written to incrementally and you need - # the past and current states to properly append and gain - # efficiencies/savings in cache writing - kwargs["cache_control_injection_points"] = [ - { - "location": "message", - "index": -1, - }, - { - "location": "message", - "index": -2, - }, - ] + if not self.is_anthropic(): + # Per this: https://github.com/BerriAI/litellm/issues/10226 + # The first and second to last messages are cache optimal + # Since caches are also written to incrementally and you need + # the past and current states to properly append and gain + # efficiencies/savings in cache writing + kwargs["cache_control_injection_points"] = [ + { + "location": "message", + "role": "system", + }, + { + "location": "message", + "index": -1, + }, + { + "location": "message", + "index": -2, + }, + ] # Are we using github copilot? if "GITHUB_COPILOT_TOKEN" in os.environ or self.name.startswith("github_copilot/"): diff --git a/aider/repo.py b/aider/repo.py index 0e7664af120..7baebfddef0 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -154,7 +154,7 @@ async def commit(self, fnames=None, context=None, message=None, aider_edits=Fals Key Concepts: - Author: The person who originally wrote the code changes. - Committer: The person who last applied the commit to the repository. - - aider_edits=True: Changes were generated by Aider (LLM). + - aider_edits=True: Changes were generated by Aider-CE (LLM). - aider_edits=False: Commit is user-driven (e.g., /commit manually staged changes). - Explicit Setting: A flag (--attribute-...) is set to True or False via command line or config file. @@ -162,10 +162,10 @@ async def commit(self, fnames=None, context=None, message=None, aider_edits=Fals interpreted as True unless overridden by other logic. Flags: - - --attribute-author: Modify Author name to "User Name (aider)". - - --attribute-committer: Modify Committer name to "User Name (aider)". + - --attribute-author: Modify Author name to "User Name (aider-ce)". + - --attribute-committer: Modify Committer name to "User Name (aider-ce)". - --attribute-co-authored-by: Add - "Co-authored-by: aider () " trailer to commit message. + "Co-authored-by: aider-ce ()" trailer to commit message. Behavior Summary: @@ -184,17 +184,17 @@ async def commit(self, fnames=None, context=None, message=None, aider_edits=Fals 2. When aider_edits = False (User Changes): - --attribute-co-authored-by is IGNORED (trailer never added). - Author name is NEVER modified (--attribute-author ignored). - - Committer name IS modified by default (implicit True, as Aider runs `git commit`). + - Committer name IS modified by default (implicit True, as Aider-CE runs `git commit`). - EXCEPTION: If --attribute-committer is EXPLICITLY False, the name is NOT modified. Resulting Scenarios: - - Standard AI edit (defaults): Co-authored-by=False -> Author=You(aider), - Committer=You(aider) + - Standard AI edit (defaults): Co-authored-by=False -> Author=You(aider-ce), + Committer=You(aider-ce) - AI edit with Co-authored-by (default): Co-authored-by=True -> Author=You, Committer=You, Trailer added - AI edit with Co-authored-by + Explicit Author: Co-authored-by=True, - --attribute-author -> Author=You(aider), Committer=You, Trailer added - - User commit (defaults): aider_edits=False -> Author=You, Committer=You(aider) + --attribute-author -> Author=You(aider-ce), Committer=You, Trailer added + - User commit (defaults): aider_edits=False -> Author=You, Committer=You(aider-ce) - User commit with explicit no-committer: aider_edits=False, --no-attribute-committer -> Author=You, Committer=You """ @@ -249,10 +249,10 @@ async def commit(self, fnames=None, context=None, message=None, aider_edits=Fals model_name = "unknown-model" if coder and hasattr(coder, "main_model") and coder.main_model.name: model_name = coder.main_model.name - commit_message_trailer = f"\n\nCo-authored-by: aider ({model_name}) " + commit_message_trailer = f"\n\nCo-authored-by: aider-ce ({model_name})" # Determine if author/committer names should be modified - # Author modification applies only to aider edits. + # Author modification applies only to aider-ce edits. # It's used if effective_author is True AND # (co-authored-by is False OR author was explicitly set). use_attribute_author = ( @@ -261,7 +261,7 @@ async def commit(self, fnames=None, context=None, message=None, aider_edits=Fals # Committer modification applies regardless of aider_edits (based on tests). # It's used if effective_committer is True AND - # (it's not an aider edit with co-authored-by OR committer was explicitly set). + # (it's not an aider-ce edit with co-authored-by OR committer was explicitly set). use_attribute_committer = effective_committer and ( not (aider_edits and attribute_co_authored_by) or committer_explicit ) @@ -270,7 +270,7 @@ async def commit(self, fnames=None, context=None, message=None, aider_edits=Fals commit_message = "(no commit message provided)" if prefix_commit_message: - commit_message = "aider: " + commit_message + commit_message = "aider-ce: " + commit_message full_commit_message = commit_message + commit_message_trailer @@ -291,7 +291,7 @@ async def commit(self, fnames=None, context=None, message=None, aider_edits=Fals original_user_name = self.repo.git.config("--get", "user.name") original_committer_name_env = os.environ.get("GIT_COMMITTER_NAME") original_author_name_env = os.environ.get("GIT_AUTHOR_NAME") - committer_name = f"{original_user_name} (aider)" + committer_name = f"{original_user_name} (aider-ce)" try: # Use context managers to handle environment variables diff --git a/aider/tools/grep.py b/aider/tools/grep.py index 0686c6d8b3f..88eec568ac9 100644 --- a/aider/tools/grep.py +++ b/aider/tools/grep.py @@ -167,7 +167,7 @@ def execute( cmd_args.append("--exclude-dir=.git") # Add pattern and directory path - cmd_args.extend([pattern, str(search_dir_path)]) + cmd_args.extend(["--", pattern, str(search_dir_path)]) # Convert list to command string for run_cmd_subprocess command_string = oslex.join(cmd_args) diff --git a/pytest.ini b/pytest.ini index d079bd78f43..6b4c1804927 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,6 +4,7 @@ addopts = -p no:warnings asyncio_mode = auto testpaths = tests/basic + tests/tools tests/help tests/browser tests/scrape diff --git a/tests/tools/test_grep.py b/tests/tools/test_grep.py new file mode 100644 index 00000000000..207301ebb21 --- /dev/null +++ b/tests/tools/test_grep.py @@ -0,0 +1,51 @@ +import shutil +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +from aider.tools import grep + + +@pytest.mark.skipif(shutil.which("rg") is None, reason="rg is required") +@pytest.mark.parametrize( + "search_term", + [ + "--pattern", + "--pat tern", + "-pattern", + "--", + "-- -test", + ], +) +def test_dash_prefixed_pattern_is_searched_literally(search_term, tmp_path, monkeypatch): + sample = tmp_path / "example.txt" + sample.write_text(f"flag {search_term} should be found\n") + + coder = SimpleNamespace( + repo=SimpleNamespace(root=str(tmp_path)), + io=SimpleNamespace( + tool_error=Mock(), + tool_output=Mock(), + tool_warning=Mock(), + ), + verbose=False, + root=str(tmp_path), + ) + + monkeypatch.setattr(grep.Tool, "_find_search_tool", lambda: ("rg", shutil.which("rg"))) + + result = grep.Tool.execute( + coder, + pattern=search_term, + file_pattern="*.txt", + directory=".", + use_regex=False, + case_insensitive=False, + context_before=0, + context_after=0, + ) + + assert "Found matches" in result + assert search_term in result + coder.io.tool_error.assert_not_called()