aider/aider/main.py

1068 lines
35 KiB
Python

import configparser
import json
import os
import re
import sys
import threading
import traceback
import webbrowser
from dataclasses import fields
from pathlib import Path
import git
import importlib_resources
from dotenv import load_dotenv
from prompt_toolkit.enums import EditingMode
from aider import __version__, models, urls, utils
from aider.analytics import Analytics
from aider.args import get_parser
from aider.coders import Coder
from aider.coders.base_coder import UnknownEditFormat
from aider.commands import Commands, SwitchCoder
from aider.copypaste import ClipboardWatcher
from aider.format_settings import format_settings, scrub_sensitive_info
from aider.history import ChatSummary
from aider.io import InputOutput
from aider.llm import litellm # noqa: F401; properly init litellm on launch
from aider.models import ModelSettings
from aider.repo import ANY_GIT_ERROR, GitRepo
from aider.report import report_uncaught_exceptions
from aider.versioncheck import check_version, install_from_main_branch, install_upgrade
from aider.watch import FileWatcher
from .dump import dump # noqa: F401
def check_config_files_for_yes(config_files):
found = False
for config_file in config_files:
if Path(config_file).exists():
try:
with open(config_file, "r") as f:
for line in f:
if line.strip().startswith("yes:"):
print("Configuration error detected.")
print(f"The file {config_file} contains a line starting with 'yes:'")
print("Please replace 'yes:' with 'yes-always:' in this file.")
found = True
except Exception:
pass
return found
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, FileNotFoundError):
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 (OSError,) + ANY_GIT_ERROR:
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 make_new_repo(git_root, io):
try:
repo = git.Repo.init(git_root)
check_gitignore(git_root, io, False)
except ANY_GIT_ERROR as err: # issue #1233
io.tool_error(f"Unable to create git repo in {git_root}")
io.tool_output(str(err))
return
io.tool_output(f"Git repository created in {git_root}")
return repo
def setup_git(git_root, io):
try:
cwd = Path.cwd()
except OSError:
cwd = None
repo = None
if git_root:
try:
repo = git.Repo(git_root)
except ANY_GIT_ERROR:
pass
elif cwd == Path.home():
io.tool_warning("You should probably run aider in a directory, not your home dir.")
return
elif cwd and io.confirm_ask(
"No git repo found, create one to track aider's changes (recommended)?"
):
git_root = str(cwd.resolve())
repo = make_new_repo(git_root, io)
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, configparser.NoOptionError):
pass
try:
user_email = config.get_value("user", "email", None)
except (configparser.NoSectionError, configparser.NoOptionError):
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_warning('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_warning('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") and repo.ignored(".env"):
return
except ANY_GIT_ERROR:
pass
patterns = [".aider*", ".env"]
patterns_to_add = []
gitignore_file = Path(git_root) / ".gitignore"
if gitignore_file.exists():
try:
content = io.read_text(gitignore_file)
if content is None:
return
existing_lines = content.splitlines()
for pat in patterns:
if pat not in existing_lines:
patterns_to_add.append(pat)
except OSError as e:
io.tool_error(f"Error when trying to read {gitignore_file}: {e}")
return
else:
content = ""
patterns_to_add = patterns
if not patterns_to_add:
return
if ask and not io.confirm_ask(f"Add {', '.join(patterns_to_add)} to .gitignore (recommended)?"):
return
if content and not content.endswith("\n"):
content += "\n"
content += "\n".join(patterns_to_add) + "\n"
try:
io.write_text(gitignore_file, content)
io.tool_output(f"Added {', '.join(patterns_to_add)} to .gitignore")
except OSError as e:
io.tool_error(f"Error when trying to write to {gitignore_file}: {e}")
io.tool_output(
"Try running with appropriate permissions or manually add these patterns to .gitignore:"
)
for pattern in patterns_to_add:
io.tool_output(f" {pattern}")
def check_streamlit_install(io):
return utils.check_pip_install_extra(
io,
"streamlit",
"You need to install the aider browser feature",
["aider-chat[browser]"],
)
def launch_gui(args):
from streamlit.web import cli
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",
]
# https://github.com/Aider-AI/aider/issues/2193
is_dev = "-dev" in str(__version__)
if is_dev:
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 parse_lint_cmds(lint_cmds, io):
err = False
res = dict()
for lint_cmd in lint_cmds:
if re.match(r"^[a-z]+:.*", lint_cmd):
pieces = lint_cmd.split(":")
lang = pieces[0]
cmd = lint_cmd[len(lang) + 1 :]
lang = lang.strip()
else:
lang = None
cmd = lint_cmd
cmd = cmd.strip()
if cmd:
res[lang] = cmd
else:
io.tool_error(f'Unable to parse --lint-cmd "{lint_cmd}"')
io.tool_output('The arg should be "language: cmd --args ..."')
io.tool_output('For example: --lint-cmd "python: flake8 --select=E9"')
err = True
if err:
return
return res
def generate_search_path_list(default_file, git_root, command_line_file):
files = []
files.append(Path.home() / default_file) # homedir
if git_root:
files.append(Path(git_root) / default_file) # git root
files.append(default_file)
if command_line_file:
files.append(command_line_file)
resolved_files = []
for fn in files:
try:
resolved_files.append(Path(fn).resolve())
except OSError:
pass
files = resolved_files
files.reverse()
uniq = []
for fn in files:
if fn not in uniq:
uniq.append(fn)
uniq.reverse()
files = uniq
files = list(map(str, files))
files = list(dict.fromkeys(files))
return files
def register_models(git_root, model_settings_fname, io, verbose=False):
model_settings_files = generate_search_path_list(
".aider.model.settings.yml", git_root, model_settings_fname
)
try:
files_loaded = models.register_models(model_settings_files)
if len(files_loaded) > 0:
if verbose:
io.tool_output("Loaded model settings from:")
for file_loaded in files_loaded:
io.tool_output(f" - {file_loaded}") # noqa: E221
elif verbose:
io.tool_output("No model settings files loaded")
except Exception as e:
io.tool_error(f"Error loading aider model settings: {e}")
return 1
if verbose:
io.tool_output("Searched for model settings files:")
for file in model_settings_files:
io.tool_output(f" - {file}")
return None
def load_dotenv_files(git_root, dotenv_fname, encoding="utf-8"):
dotenv_files = generate_search_path_list(
".env",
git_root,
dotenv_fname,
)
loaded = []
for fname in dotenv_files:
try:
if Path(fname).exists():
load_dotenv(fname, override=True, encoding=encoding)
loaded.append(fname)
except OSError as e:
print(f"OSError loading {fname}: {e}")
except Exception as e:
print(f"Error loading {fname}: {e}")
return loaded
def register_litellm_models(git_root, model_metadata_fname, io, verbose=False):
model_metatdata_files = []
# Add the resource file path
resource_metadata = importlib_resources.files("aider.resources").joinpath("model-metadata.json")
model_metatdata_files.append(str(resource_metadata))
model_metatdata_files += generate_search_path_list(
".aider.model.metadata.json", git_root, model_metadata_fname
)
try:
model_metadata_files_loaded = models.register_litellm_models(model_metatdata_files)
if len(model_metadata_files_loaded) > 0 and verbose:
io.tool_output("Loaded model metadata from:")
for model_metadata_file in model_metadata_files_loaded:
io.tool_output(f" - {model_metadata_file}") # noqa: E221
except Exception as e:
io.tool_error(f"Error loading model metadata models: {e}")
return 1
def sanity_check_repo(repo, io):
if not repo:
return True
if not repo.repo.working_tree_dir:
io.tool_error("The git repo does not seem to have a working tree?")
return False
bad_ver = False
try:
repo.get_tracked_files()
if not repo.git_repo_error:
return True
error_msg = str(repo.git_repo_error)
except ANY_GIT_ERROR as exc:
error_msg = str(exc)
bad_ver = "version in (1, 2)" in error_msg
except AssertionError as exc:
error_msg = str(exc)
bad_ver = True
if bad_ver:
io.tool_error("Aider only works with git repos with version number 1 or 2.")
io.tool_output("You may be able to convert your repo: git update-index --index-version=2")
io.tool_output("Or run aider --no-git to proceed without using git.")
io.offer_url(urls.git_index_version, "Open documentation url for more info?")
return False
io.tool_error("Unable to read git repository, it may be corrupt?")
io.tool_output(error_msg)
return False
def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False):
report_uncaught_exceptions()
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 = []
try:
default_config_files += [conf_fname.resolve()] # CWD
except OSError:
pass
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)
try:
args, unknown = parser.parse_known_args(argv)
except AttributeError as e:
if all(word in str(e) for word in ["bool", "object", "has", "no", "attribute", "strip"]):
if check_config_files_for_yes(default_config_files):
return 1
raise e
if args.verbose:
print("Config files search order, if no --config:")
for file in default_config_files:
exists = "(exists)" if Path(file).exists() else ""
print(f" - {file} {exists}")
default_config_files.reverse()
parser = get_parser(default_config_files, git_root)
args, unknown = parser.parse_known_args(argv)
# Load the .env file specified in the arguments
loaded_dotenvs = load_dotenv_files(git_root, args.env_file, args.encoding)
# Parse again to include any arguments that might have been defined in .env
args = parser.parse_args(argv)
if args.analytics_disable:
analytics = Analytics(permanently_disable=True)
print("Analytics have been permanently disabled.")
if not args.verify_ssl:
import httpx
os.environ["SSL_VERIFY"] = ""
litellm._load_litellm()
litellm._lazy_module.client_session = httpx.Client(verify=False)
litellm._lazy_module.aclient_session = httpx.AsyncClient(verify=False)
if args.timeout:
litellm._load_litellm()
litellm._lazy_module.request_timeout = args.timeout
if args.dark_mode:
args.user_input_color = "#32FF32"
args.tool_error_color = "#FF3333"
args.tool_warning_color = "#FFFF00"
args.assistant_output_color = "#00FFFF"
args.code_theme = "monokai"
if args.light_mode:
args.user_input_color = "green"
args.tool_error_color = "red"
args.tool_warning_color = "#FFA500"
args.assistant_output_color = "blue"
args.code_theme = "default"
if return_coder and args.yes_always is None:
args.yes_always = True
editing_mode = EditingMode.VI if args.vim else EditingMode.EMACS
def get_io(pretty):
return InputOutput(
pretty,
args.yes_always,
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_warning_color=args.tool_warning_color,
tool_error_color=args.tool_error_color,
completion_menu_color=args.completion_menu_color,
completion_menu_bg_color=args.completion_menu_bg_color,
completion_menu_current_color=args.completion_menu_current_color,
completion_menu_current_bg_color=args.completion_menu_current_bg_color,
assistant_output_color=args.assistant_output_color,
code_theme=args.code_theme,
dry_run=args.dry_run,
encoding=args.encoding,
llm_history_file=args.llm_history_file,
editingmode=editing_mode,
fancy_input=args.fancy_input,
)
io = get_io(args.pretty)
try:
io.rule()
except UnicodeEncodeError as err:
if not io.pretty:
raise err
io = get_io(False)
io.tool_warning("Terminal does not support pretty output (UnicodeDecodeError)")
# Process any environment variables set via --set-env
if args.set_env:
for env_setting in args.set_env:
try:
name, value = env_setting.split("=", 1)
os.environ[name.strip()] = value.strip()
except ValueError:
io.tool_error(f"Invalid --set-env format: {env_setting}")
io.tool_output("Format should be: ENV_VAR_NAME=value")
analytics.event("exit", reason="Invalid env var format")
return 1
analytics = Analytics(logfile=args.analytics_log, permanently_disable=args.analytics_disable)
if args.analytics is not False:
if analytics.need_to_ask(args.analytics):
io.tool_output(
"Aider respects your privacy and never collects your code, chat messages, keys or"
" personal info."
)
io.tool_output(f"For more info: {urls.analytics}")
disable = not io.confirm_ask(
"Allow collection of anonymous analytics to help improve aider?"
)
analytics.asked_opt_in = True
if disable:
analytics.disable(permanently=True)
io.tool_output("Analytics have been permanently disabled.")
analytics.save_data()
io.tool_output()
# This is a no-op if the user has opted out
analytics.enable()
analytics.event("launched")
if args.gui and not return_coder:
if not check_streamlit_install(io):
analytics.event("exit", reason="Streamlit not installed")
return
analytics.event("gui session")
launch_gui(argv)
analytics.event("exit", reason="GUI session ended")
return
if args.verbose:
for fname in loaded_dotenvs:
io.tool_output(f"Loaded {fname}")
all_files = args.files + (args.file or [])
fnames = [str(Path(fn).resolve()) for fn in all_files]
read_only_fnames = []
for fn in args.read or []:
path = Path(fn).expanduser().resolve()
if path.is_dir():
read_only_fnames.extend(str(f) for f in path.rglob("*") if f.is_file())
else:
read_only_fnames.append(str(path))
if len(all_files) > 1:
good = True
for fname in all_files:
if Path(fname).is_dir():
io.tool_error(f"{fname} is a directory, not provided alone.")
good = False
if not good:
io.tool_output(
"Provide either a single directory of a git repo, or a list of one or more files."
)
analytics.event("exit", reason="Invalid directory input")
return 1
git_dname = None
if len(all_files) == 1:
if Path(all_files[0]).is_dir():
if args.git:
git_dname = str(Path(all_files[0]).resolve())
fnames = []
else:
io.tool_error(f"{all_files[0]} is a directory, but --no-git selected.")
analytics.event("exit", reason="Directory with --no-git")
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:
analytics.event("exit", reason="Recursing with correct repo")
return main(argv, input, output, right_repo_root, return_coder=return_coder)
if args.just_check_update:
update_available = check_version(io, just_check=True, verbose=args.verbose)
analytics.event("exit", reason="Just checking update")
return 0 if not update_available else 1
if args.install_main_branch:
success = install_from_main_branch(io)
analytics.event("exit", reason="Installed main branch")
return 0 if success else 1
if args.upgrade:
success = install_upgrade(io)
analytics.event("exit", reason="Upgrade completed")
return 0 if success else 1
if args.check_update:
check_version(io, verbose=args.verbose)
if args.list_models:
models.print_matching_models(io, args.list_models)
analytics.event("exit", reason="Listed 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)
is_first_run = is_first_run_of_new_version(io, verbose=args.verbose)
check_and_load_imports(io, is_first_run, verbose=args.verbose)
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
register_models(git_root, args.model_settings_file, io, verbose=args.verbose)
register_litellm_models(git_root, args.model_metadata_file, io, verbose=args.verbose)
# Process any command line aliases
if args.alias:
for alias_def in args.alias:
# Split on first colon only
parts = alias_def.split(":", 1)
if len(parts) != 2:
io.tool_error(f"Invalid alias format: {alias_def}")
io.tool_output("Format should be: alias:model-name")
analytics.event("exit", reason="Invalid alias format error")
return 1
alias, model = parts
models.MODEL_ALIASES[alias.strip()] = model.strip()
if not args.model:
args.model = "gpt-4o-2024-08-06"
if os.environ.get("ANTHROPIC_API_KEY"):
args.model = "claude-3-5-sonnet-20241022"
main_model = models.Model(
args.model,
weak_model=args.weak_model,
editor_model=args.editor_model,
editor_edit_format=args.editor_edit_format,
)
if args.copy_paste and args.edit_format is None:
if main_model.edit_format in ("diff", "whole"):
main_model.edit_format = "editor-" + main_model.edit_format
if args.verbose:
io.tool_output("Model metadata:")
io.tool_output(json.dumps(main_model.info, indent=4))
io.tool_output("Model settings:")
for attr in sorted(fields(ModelSettings), key=lambda x: x.name):
val = getattr(main_model, attr.name)
val = json.dumps(val, indent=4)
io.tool_output(f"{attr.name}: {val}")
lint_cmds = parse_lint_cmds(args.lint_cmd, io)
if lint_cmds is None:
analytics.event("exit", reason="Invalid lint command format")
return 1
if args.show_model_warnings:
problem = models.sanity_check_models(io, main_model)
if problem:
analytics.event("model warning", main_model=main_model)
io.tool_output("You can skip this check with --no-show-model-warnings")
try:
io.offer_url(urls.model_warnings, "Open documentation url for more info?")
io.tool_output()
except KeyboardInterrupt:
analytics.event("exit", reason="Keyboard interrupt during model warnings")
return 1
repo = None
if args.git:
try:
repo = GitRepo(
io,
fnames,
git_dname,
args.aiderignore,
models=main_model.commit_message_models(),
attribute_author=args.attribute_author,
attribute_committer=args.attribute_committer,
attribute_commit_message_author=args.attribute_commit_message_author,
attribute_commit_message_committer=args.attribute_commit_message_committer,
commit_prompt=args.commit_prompt,
subtree_only=args.subtree_only,
)
except FileNotFoundError:
pass
if not args.skip_sanity_check_repo:
if not sanity_check_repo(repo, io):
analytics.event("exit", reason="Repository sanity check failed")
return 1
if repo:
analytics.event("repo", num_files=len(repo.get_tracked_files()))
else:
analytics.event("no-repo")
commands = Commands(
io,
None,
verify_ssl=args.verify_ssl,
args=args,
parser=parser,
verbose=args.verbose,
editor=args.editor,
)
summarizer = ChatSummary(
[main_model.weak_model, main_model],
args.max_chat_history_tokens or main_model.max_chat_history_tokens,
)
if args.cache_prompts and args.map_refresh == "auto":
args.map_refresh = "files"
if not main_model.streaming:
if args.stream:
io.tool_warning(
f"Warning: Streaming is not supported by {main_model.name}. Disabling streaming."
)
args.stream = False
try:
coder = Coder.create(
main_model=main_model,
edit_format=args.edit_format,
io=io,
repo=repo,
fnames=fnames,
read_only_fnames=read_only_fnames,
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,
stream=args.stream,
use_git=args.git,
restore_chat_history=args.restore_chat_history,
auto_lint=args.auto_lint,
auto_test=args.auto_test,
lint_cmds=lint_cmds,
test_cmd=args.test_cmd,
commands=commands,
summarizer=summarizer,
analytics=analytics,
map_refresh=args.map_refresh,
cache_prompts=args.cache_prompts,
map_mul_no_files=args.map_multiplier_no_files,
num_cache_warming_pings=args.cache_keepalive_pings,
suggest_shell_commands=args.suggest_shell_commands,
chat_language=args.chat_language,
detect_urls=args.detect_urls,
auto_copy_context=args.copy_paste,
)
except UnknownEditFormat as err:
io.tool_error(str(err))
io.offer_url(urls.edit_formats, "Open documentation about edit formats?")
analytics.event("exit", reason="Unknown edit format")
return 1
except ValueError as err:
io.tool_error(str(err))
analytics.event("exit", reason="ValueError during coder creation")
return 1
if return_coder:
analytics.event("exit", reason="Returning coder object")
return coder
ignores = []
if git_root:
ignores.append(str(Path(git_root) / ".gitignore"))
if args.aiderignore:
ignores.append(args.aiderignore)
if args.watch_files:
file_watcher = FileWatcher(coder, gitignores=ignores, verbose=args.verbose)
coder.file_watcher = file_watcher
if args.copy_paste:
ClipboardWatcher(coder.io, verbose=args.verbose)
coder.show_announcements()
if args.show_prompts:
coder.cur_messages += [
dict(role="user", content="Hello!"),
]
messages = coder.format_messages().all_messages()
utils.show_messages(messages)
analytics.event("exit", reason="Showed prompts")
return
if args.lint:
coder.commands.cmd_lint(fnames=fnames)
if args.test:
if not args.test_cmd:
io.tool_error("No --test-cmd provided.")
analytics.event("exit", reason="No test command provided")
return 1
coder.commands.cmd_test(args.test_cmd)
if io.placeholder:
coder.run(io.placeholder)
if args.commit:
if args.dry_run:
io.tool_output("Dry run enabled, skipping commit.")
else:
coder.commands.cmd_commit()
if args.lint or args.test or args.commit:
analytics.event("exit", reason="Completed lint/test/commit")
return
if args.show_repo_map:
repo_map = coder.get_repo_map()
if repo_map:
io.tool_output(repo_map)
analytics.event("exit", reason="Showed repo map")
return
if args.apply:
content = io.read_text(args.apply)
if content is None:
analytics.event("exit", reason="Failed to read apply content")
return
coder.partial_response_content = content
coder.apply_updates()
analytics.event("exit", reason="Applied updates")
return
if args.apply_clipboard_edits:
args.edit_format = main_model.editor_edit_format
args.message = "/paste"
if args.show_release_notes is True:
io.tool_output(f"Opening release notes: {urls.release_notes}")
io.tool_output()
webbrowser.open(urls.release_notes)
elif args.show_release_notes is None and is_first_run:
io.tool_output()
io.offer_url(
urls.release_notes,
"Would you like to see what's new in this version?",
allow_never=False,
)
if git_root and Path.cwd().resolve() != Path(git_root).resolve():
io.tool_warning(
"Note: in-chat filenames are always relative to the git working dir, not the current"
" working dir."
)
io.tool_output(f"Cur working dir: {Path.cwd()}")
io.tool_output(f"Git working dir: {git_root}")
if args.load:
commands.cmd_load(args.load)
if args.message:
io.add_to_input_history(args.message)
io.tool_output()
try:
coder.run(with_message=args.message)
except SwitchCoder:
pass
analytics.event("exit", reason="Completed --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}")
analytics.event("exit", reason="Message file not found")
return 1
except IOError as e:
io.tool_error(f"Error reading message file: {e}")
analytics.event("exit", reason="Message file IO error")
return 1
analytics.event("exit", reason="Completed --message-file")
return
if args.exit:
analytics.event("exit", reason="Exit flag set")
return
analytics.event("cli session", main_model=main_model, edit_format=main_model.edit_format)
while True:
try:
coder.run()
analytics.event("exit", reason="Completed main CLI coder.run")
return
except SwitchCoder as switch:
kwargs = dict(io=io, from_coder=coder)
kwargs.update(switch.kwargs)
if "show_announcements" in kwargs:
del kwargs["show_announcements"]
coder = Coder.create(**kwargs)
if switch.kwargs.get("show_announcements") is not False:
coder.show_announcements()
def is_first_run_of_new_version(io, verbose=False):
"""Check if this is the first run of a new version/executable combination"""
installs_file = Path.home() / ".aider" / "installs.json"
key = (__version__, sys.executable)
# Never show notes for .dev versions
if ".dev" in __version__:
return False
if verbose:
io.tool_output(
f"Checking imports for version {__version__} and executable {sys.executable}"
)
io.tool_output(f"Installs file: {installs_file}")
try:
if installs_file.exists():
with open(installs_file, "r") as f:
installs = json.load(f)
if verbose:
io.tool_output("Installs file exists and loaded")
else:
installs = {}
if verbose:
io.tool_output("Installs file does not exist, creating new dictionary")
is_first_run = str(key) not in installs
if is_first_run:
installs[str(key)] = True
installs_file.parent.mkdir(parents=True, exist_ok=True)
with open(installs_file, "w") as f:
json.dump(installs, f, indent=4)
return is_first_run
except Exception as e:
io.tool_warning(f"Error checking version: {e}")
if verbose:
io.tool_output(f"Full exception details: {traceback.format_exc()}")
return True # Safer to assume it's a first run if we hit an error
def check_and_load_imports(io, is_first_run, verbose=False):
try:
if is_first_run:
if verbose:
io.tool_output(
"First run for this version and executable, loading imports synchronously"
)
try:
load_slow_imports(swallow=False)
except Exception as err:
io.tool_error(str(err))
io.tool_output("Error loading required imports. Did you install aider properly?")
io.offer_url(urls.install_properly, "Open documentation url for more info?")
sys.exit(1)
if verbose:
io.tool_output("Imports loaded and installs file updated")
else:
if verbose:
io.tool_output("Not first run, loading imports in background thread")
thread = threading.Thread(target=load_slow_imports)
thread.daemon = True
thread.start()
except Exception as e:
io.tool_warning(f"Error in loading imports: {e}")
if verbose:
io.tool_output(f"Full exception details: {traceback.format_exc()}")
def load_slow_imports(swallow=True):
# These imports are deferred in various ways to
# improve startup time.
# This func is called either synchronously or in a thread
# depending on whether it's been run before for this version and executable.
try:
import httpx # noqa: F401
import litellm # noqa: F401
import networkx # noqa: F401
import numpy # noqa: F401
except Exception as e:
if not swallow:
raise e
if __name__ == "__main__":
status = main()
sys.exit(status)