import configparser import os import sys from pathlib import Path import git import litellm from dotenv import load_dotenv from streamlit.web import cli from aider import __version__, models from aider.args import get_parser from aider.coders import Coder from aider.commands import SwitchModel from aider.io import InputOutput from aider.repo import GitRepo from aider.versioncheck import check_version from .dump import dump # noqa: F401 litellm.suppress_debug_info = True os.environ["OR_SITE_URL"] = "http://aider.chat" os.environ["OR_APP_NAME"] = "Aider" def get_git_root(): """Try and guess the git repo, since the conf.yml can be at the repo root""" try: repo = git.Repo(search_parent_directories=True) return repo.working_tree_dir except git.InvalidGitRepositoryError: return None def guessed_wrong_repo(io, git_root, fnames, git_dname): """After we parse the args, we can determine the real repo. Did we guess wrong?""" try: check_repo = Path(GitRepo(io, fnames, git_dname).root).resolve() except FileNotFoundError: return # we had no guess, rely on the "true" repo result if not git_root: return str(check_repo) git_root = Path(git_root).resolve() if check_repo == git_root: return return str(check_repo) def setup_git(git_root, io): repo = None if git_root: repo = git.Repo(git_root) elif io.confirm_ask("No git repo found, create one to track GPT's changes (recommended)?"): git_root = str(Path.cwd().resolve()) repo = git.Repo.init(git_root) io.tool_output("Git repository created in the current working directory.") check_gitignore(git_root, io, False) if not repo: return user_name = None user_email = None with repo.config_reader() as config: try: user_name = config.get_value("user", "name", None) except configparser.NoSectionError: pass try: user_email = config.get_value("user", "email", None) except configparser.NoSectionError: pass if user_name and user_email: return repo.working_tree_dir with repo.config_writer() as git_config: if not user_name: git_config.set_value("user", "name", "Your Name") io.tool_error('Update git name with: git config user.name "Your Name"') if not user_email: git_config.set_value("user", "email", "you@example.com") io.tool_error('Update git email with: git config user.email "you@example.com"') return repo.working_tree_dir def check_gitignore(git_root, io, ask=True): if not git_root: return try: repo = git.Repo(git_root) if repo.ignored(".aider"): return except git.exc.InvalidGitRepositoryError: pass pat = ".aider*" gitignore_file = Path(git_root) / ".gitignore" if gitignore_file.exists(): content = io.read_text(gitignore_file) if content is None: return if pat in content.splitlines(): return else: content = "" if ask and not io.confirm_ask(f"Add {pat} to .gitignore (recommended)?"): return if content and not content.endswith("\n"): content += "\n" content += pat + "\n" io.write_text(gitignore_file, content) io.tool_output(f"Added {pat} to .gitignore") def format_settings(parser, args): show = scrub_sensitive_info(args, parser.format_values()) show += "\n" show += "Option settings:\n" for arg, val in sorted(vars(args).items()): if val: val = scrub_sensitive_info(args, str(val)) show += f" - {arg}: {val}\n" return show def scrub_sensitive_info(args, text): # Replace sensitive information with placeholder if text and args.openai_api_key: text = text.replace(args.openai_api_key, "***") if text and args.anthropic_api_key: text = text.replace(args.anthropic_api_key, "***") return text def launch_gui(args): from aider import gui print() print("CONTROL-C to exit...") target = gui.__file__ st_args = ["run", target] st_args += [ "--browser.gatherUsageStats=false", "--runner.magicEnabled=false", "--server.runOnSave=false", ] if "-dev" in __version__: print("Watching for file changes.") else: st_args += [ "--global.developmentMode=false", "--server.fileWatcherType=none", "--client.toolbarMode=viewer", # minimal? ] st_args += ["--"] + args cli.main(st_args) # from click.testing import CliRunner # runner = CliRunner() # from streamlit.web import bootstrap # bootstrap.load_config_options(flag_options={}) # cli.main_run(target, args) # sys.argv = ['streamlit', 'run', '--'] + args def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False): if argv is None: argv = sys.argv[1:] if force_git_root: git_root = force_git_root else: git_root = get_git_root() conf_fname = Path(".aider.conf.yml") default_config_files = [conf_fname.resolve()] # CWD if git_root: git_conf = Path(git_root) / conf_fname # git root if git_conf not in default_config_files: default_config_files.append(git_conf) default_config_files.append(Path.home() / conf_fname) # homedir default_config_files = list(map(str, default_config_files)) parser = get_parser(default_config_files, git_root) args = parser.parse_args(argv) if args.gui and not return_coder: launch_gui(argv) return if args.dark_mode: args.user_input_color = "#32FF32" args.tool_error_color = "#FF3333" args.assistant_output_color = "#00FFFF" args.code_theme = "monokai" if args.light_mode: args.user_input_color = "green" args.tool_error_color = "red" args.assistant_output_color = "blue" args.code_theme = "default" if return_coder and args.yes is None: args.yes = True io = InputOutput( args.pretty, args.yes, args.input_history_file, args.chat_history_file, input=input, output=output, user_input_color=args.user_input_color, tool_output_color=args.tool_output_color, tool_error_color=args.tool_error_color, dry_run=args.dry_run, encoding=args.encoding, ) fnames = [str(Path(fn).resolve()) for fn in args.files] if len(args.files) > 1: good = True for fname in args.files: if Path(fname).is_dir(): io.tool_error(f"{fname} is a directory, not provided alone.") good = False if not good: io.tool_error( "Provide either a single directory of a git repo, or a list of one or more files." ) return 1 git_dname = None if len(args.files) == 1: if Path(args.files[0]).is_dir(): if args.git: git_dname = str(Path(args.files[0]).resolve()) fnames = [] else: io.tool_error(f"{args.files[0]} is a directory, but --no-git selected.") return 1 # We can't know the git repo for sure until after parsing the args. # If we guessed wrong, reparse because that changes things like # the location of the config.yml and history files. if args.git and not force_git_root: right_repo_root = guessed_wrong_repo(io, git_root, fnames, git_dname) if right_repo_root: return main(argv, input, output, right_repo_root, return_coder=return_coder) if not args.skip_check_update: check_version(io.tool_error) if args.check_update: update_available = check_version(lambda msg: None) return 0 if not update_available else 1 if args.models: models.print_matching_models(io, args.models) return 0 if args.git: git_root = setup_git(git_root, io) if args.gitignore: check_gitignore(git_root, io) if args.verbose: show = format_settings(parser, args) io.tool_output(show) cmd_line = " ".join(sys.argv) cmd_line = scrub_sensitive_info(args, cmd_line) io.tool_output(cmd_line, log_only=True) if args.env_file: load_dotenv(args.env_file) if args.anthropic_api_key: os.environ["ANTHROPIC_API_KEY"] = args.anthropic_api_key if args.openai_api_key: os.environ["OPENAI_API_KEY"] = args.openai_api_key if args.openai_api_base: os.environ["OPENAI_API_BASE"] = args.openai_api_base if args.openai_api_version: os.environ["OPENAI_API_VERSION"] = args.openai_api_version if args.openai_api_type: os.environ["OPENAI_API_TYPE"] = args.openai_api_type if args.openai_organization_id: os.environ["OPENAI_ORGANIZATION"] = args.openai_organization_id main_model = models.Model(args.model, weak_model=args.weak_model) if args.show_model_warnings: models.sanity_check_models(io, main_model) try: coder = Coder.create( main_model=main_model, edit_format=args.edit_format, io=io, ## fnames=fnames, git_dname=git_dname, pretty=args.pretty, show_diffs=args.show_diffs, auto_commits=args.auto_commits, dirty_commits=args.dirty_commits, dry_run=args.dry_run, map_tokens=args.map_tokens, verbose=args.verbose, assistant_output_color=args.assistant_output_color, code_theme=args.code_theme, stream=args.stream, use_git=args.git, voice_language=args.voice_language, aider_ignore_file=args.aiderignore, ) except ValueError as err: io.tool_error(str(err)) return 1 if return_coder: return coder coder.show_announcements() if args.commit: coder.commands.cmd_commit("") return if args.show_repo_map: repo_map = coder.get_repo_map() if repo_map: io.tool_output(repo_map) return if args.apply: content = io.read_text(args.apply) if content is None: return coder.partial_response_content = content coder.apply_updates() return if "VSCODE_GIT_IPC_HANDLE" in os.environ: args.pretty = False io.tool_output("VSCode terminal detected, pretty output has been disabled.") io.tool_output("Use /help to see in-chat commands, run with --help to see cmd line args") if git_root and Path.cwd().resolve() != Path(git_root).resolve(): io.tool_error( "Note: in-chat filenames are always relative to the git working dir, not the current" " working dir." ) io.tool_error(f"Cur working dir: {Path.cwd()}") io.tool_error(f"Git working dir: {git_root}") if args.message: io.add_to_input_history(args.message) io.tool_output() coder.run(with_message=args.message) return if args.message_file: try: message_from_file = io.read_text(args.message_file) io.tool_output() coder.run(with_message=message_from_file) except FileNotFoundError: io.tool_error(f"Message file not found: {args.message_file}") return 1 except IOError as e: io.tool_error(f"Error reading message file: {e}") return 1 return while True: try: coder.run() return except SwitchModel as switch: coder = Coder.create(main_model=switch.model, io=io, from_coder=coder) coder.show_announcements() if __name__ == "__main__": status = main() sys.exit(status)