chatmcp-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aider/__init__.py +20 -0
- aider/__main__.py +4 -0
- aider/_version.py +21 -0
- aider/analytics.py +250 -0
- aider/args.py +926 -0
- aider/args_formatter.py +228 -0
- aider/coders/__init__.py +34 -0
- aider/coders/architect_coder.py +48 -0
- aider/coders/architect_prompts.py +40 -0
- aider/coders/ask_coder.py +9 -0
- aider/coders/ask_prompts.py +35 -0
- aider/coders/base_coder.py +2483 -0
- aider/coders/base_prompts.py +60 -0
- aider/coders/chat_chunks.py +64 -0
- aider/coders/context_coder.py +53 -0
- aider/coders/context_prompts.py +75 -0
- aider/coders/editblock_coder.py +657 -0
- aider/coders/editblock_fenced_coder.py +10 -0
- aider/coders/editblock_fenced_prompts.py +143 -0
- aider/coders/editblock_func_coder.py +141 -0
- aider/coders/editblock_func_prompts.py +27 -0
- aider/coders/editblock_prompts.py +174 -0
- aider/coders/editor_diff_fenced_coder.py +9 -0
- aider/coders/editor_diff_fenced_prompts.py +11 -0
- aider/coders/editor_editblock_coder.py +8 -0
- aider/coders/editor_editblock_prompts.py +18 -0
- aider/coders/editor_whole_coder.py +8 -0
- aider/coders/editor_whole_prompts.py +10 -0
- aider/coders/help_coder.py +16 -0
- aider/coders/help_prompts.py +46 -0
- aider/coders/patch_coder.py +706 -0
- aider/coders/patch_prompts.py +161 -0
- aider/coders/search_replace.py +757 -0
- aider/coders/shell.py +37 -0
- aider/coders/single_wholefile_func_coder.py +102 -0
- aider/coders/single_wholefile_func_prompts.py +27 -0
- aider/coders/udiff_coder.py +429 -0
- aider/coders/udiff_prompts.py +115 -0
- aider/coders/udiff_simple.py +14 -0
- aider/coders/udiff_simple_prompts.py +25 -0
- aider/coders/wholefile_coder.py +144 -0
- aider/coders/wholefile_func_coder.py +134 -0
- aider/coders/wholefile_func_prompts.py +27 -0
- aider/coders/wholefile_prompts.py +67 -0
- aider/commands.py +1665 -0
- aider/copypaste.py +72 -0
- aider/deprecated.py +126 -0
- aider/diffs.py +128 -0
- aider/dump.py +29 -0
- aider/editor.py +147 -0
- aider/exceptions.py +107 -0
- aider/format_settings.py +26 -0
- aider/gui.py +545 -0
- aider/help.py +163 -0
- aider/help_pats.py +19 -0
- aider/history.py +143 -0
- aider/io.py +1175 -0
- aider/linter.py +304 -0
- aider/llm.py +47 -0
- aider/main.py +1267 -0
- aider/mdstream.py +243 -0
- aider/models.py +1286 -0
- aider/onboarding.py +428 -0
- aider/openrouter.py +128 -0
- aider/prompts.py +64 -0
- aider/queries/tree-sitter-language-pack/README.md +7 -0
- aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- aider/queries/tree-sitter-languages/README.md +23 -0
- aider/queries/tree-sitter-languages/c-tags.scm +9 -0
- aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
- aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
- aider/queries/tree-sitter-languages/go-tags.scm +30 -0
- aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- aider/queries/tree-sitter-languages/java-tags.scm +20 -0
- aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- aider/queries/tree-sitter-languages/php-tags.scm +26 -0
- aider/queries/tree-sitter-languages/python-tags.scm +12 -0
- aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
- aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
- aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
- aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- aider/reasoning_tags.py +82 -0
- aider/repo.py +623 -0
- aider/repomap.py +847 -0
- aider/report.py +200 -0
- aider/resources/__init__.py +3 -0
- aider/resources/model-metadata.json +468 -0
- aider/resources/model-settings.yml +1767 -0
- aider/run_cmd.py +132 -0
- aider/scrape.py +284 -0
- aider/sendchat.py +61 -0
- aider/special.py +203 -0
- aider/urls.py +17 -0
- aider/utils.py +338 -0
- aider/versioncheck.py +113 -0
- aider/voice.py +187 -0
- aider/waiting.py +221 -0
- aider/watch.py +318 -0
- aider/watch_prompts.py +12 -0
- aider/website/Gemfile +8 -0
- aider/website/_includes/blame.md +162 -0
- aider/website/_includes/get-started.md +22 -0
- aider/website/_includes/help-tip.md +5 -0
- aider/website/_includes/help.md +24 -0
- aider/website/_includes/install.md +5 -0
- aider/website/_includes/keys.md +4 -0
- aider/website/_includes/model-warnings.md +67 -0
- aider/website/_includes/multi-line.md +22 -0
- aider/website/_includes/python-m-aider.md +5 -0
- aider/website/_includes/recording.css +228 -0
- aider/website/_includes/recording.md +34 -0
- aider/website/_includes/replit-pipx.md +9 -0
- aider/website/_includes/works-best.md +1 -0
- aider/website/_sass/custom/custom.scss +103 -0
- aider/website/docs/config/adv-model-settings.md +1881 -0
- aider/website/docs/config/aider_conf.md +527 -0
- aider/website/docs/config/api-keys.md +90 -0
- aider/website/docs/config/dotenv.md +478 -0
- aider/website/docs/config/editor.md +127 -0
- aider/website/docs/config/model-aliases.md +103 -0
- aider/website/docs/config/options.md +843 -0
- aider/website/docs/config/reasoning.md +209 -0
- aider/website/docs/config.md +44 -0
- aider/website/docs/faq.md +378 -0
- aider/website/docs/git.md +76 -0
- aider/website/docs/index.md +47 -0
- aider/website/docs/install/codespaces.md +39 -0
- aider/website/docs/install/docker.md +57 -0
- aider/website/docs/install/optional.md +100 -0
- aider/website/docs/install/replit.md +8 -0
- aider/website/docs/install.md +115 -0
- aider/website/docs/languages.md +264 -0
- aider/website/docs/legal/contributor-agreement.md +111 -0
- aider/website/docs/legal/privacy.md +104 -0
- aider/website/docs/llms/anthropic.md +77 -0
- aider/website/docs/llms/azure.md +48 -0
- aider/website/docs/llms/bedrock.md +132 -0
- aider/website/docs/llms/cohere.md +34 -0
- aider/website/docs/llms/deepseek.md +32 -0
- aider/website/docs/llms/gemini.md +49 -0
- aider/website/docs/llms/github.md +105 -0
- aider/website/docs/llms/groq.md +36 -0
- aider/website/docs/llms/lm-studio.md +39 -0
- aider/website/docs/llms/ollama.md +75 -0
- aider/website/docs/llms/openai-compat.md +39 -0
- aider/website/docs/llms/openai.md +58 -0
- aider/website/docs/llms/openrouter.md +78 -0
- aider/website/docs/llms/other.md +103 -0
- aider/website/docs/llms/vertex.md +50 -0
- aider/website/docs/llms/warnings.md +10 -0
- aider/website/docs/llms/xai.md +53 -0
- aider/website/docs/llms.md +54 -0
- aider/website/docs/more/analytics.md +122 -0
- aider/website/docs/more/edit-formats.md +116 -0
- aider/website/docs/more/infinite-output.md +137 -0
- aider/website/docs/more-info.md +8 -0
- aider/website/docs/recordings/auto-accept-architect.md +31 -0
- aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
- aider/website/docs/recordings/index.md +21 -0
- aider/website/docs/recordings/model-accepts-settings.md +69 -0
- aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
- aider/website/docs/repomap.md +112 -0
- aider/website/docs/scripting.md +100 -0
- aider/website/docs/troubleshooting/aider-not-found.md +24 -0
- aider/website/docs/troubleshooting/edit-errors.md +76 -0
- aider/website/docs/troubleshooting/imports.md +62 -0
- aider/website/docs/troubleshooting/models-and-keys.md +54 -0
- aider/website/docs/troubleshooting/support.md +79 -0
- aider/website/docs/troubleshooting/token-limits.md +96 -0
- aider/website/docs/troubleshooting/warnings.md +12 -0
- aider/website/docs/troubleshooting.md +11 -0
- aider/website/docs/usage/browser.md +57 -0
- aider/website/docs/usage/caching.md +49 -0
- aider/website/docs/usage/commands.md +132 -0
- aider/website/docs/usage/conventions.md +119 -0
- aider/website/docs/usage/copypaste.md +121 -0
- aider/website/docs/usage/images-urls.md +48 -0
- aider/website/docs/usage/lint-test.md +118 -0
- aider/website/docs/usage/modes.md +211 -0
- aider/website/docs/usage/not-code.md +179 -0
- aider/website/docs/usage/notifications.md +87 -0
- aider/website/docs/usage/tips.md +79 -0
- aider/website/docs/usage/tutorials.md +30 -0
- aider/website/docs/usage/voice.md +121 -0
- aider/website/docs/usage/watch.md +294 -0
- aider/website/docs/usage.md +92 -0
- aider/website/share/index.md +101 -0
- chatmcp_cli-0.1.0.dist-info/METADATA +502 -0
- chatmcp_cli-0.1.0.dist-info/RECORD +228 -0
- chatmcp_cli-0.1.0.dist-info/WHEEL +5 -0
- chatmcp_cli-0.1.0.dist-info/entry_points.txt +3 -0
- chatmcp_cli-0.1.0.dist-info/licenses/LICENSE.txt +202 -0
- chatmcp_cli-0.1.0.dist-info/top_level.txt +1 -0
aider/main.py
ADDED
@@ -0,0 +1,1267 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
import re
|
4
|
+
import sys
|
5
|
+
import threading
|
6
|
+
import traceback
|
7
|
+
import webbrowser
|
8
|
+
from dataclasses import fields
|
9
|
+
from pathlib import Path
|
10
|
+
|
11
|
+
try:
|
12
|
+
import git
|
13
|
+
except ImportError:
|
14
|
+
git = None
|
15
|
+
|
16
|
+
import importlib_resources
|
17
|
+
import shtab
|
18
|
+
from dotenv import load_dotenv
|
19
|
+
from prompt_toolkit.enums import EditingMode
|
20
|
+
|
21
|
+
from aider import __version__, models, urls, utils
|
22
|
+
from aider.analytics import Analytics
|
23
|
+
from aider.args import get_parser
|
24
|
+
from aider.coders import Coder
|
25
|
+
from aider.coders.base_coder import UnknownEditFormat
|
26
|
+
from aider.commands import Commands, SwitchCoder
|
27
|
+
from aider.copypaste import ClipboardWatcher
|
28
|
+
from aider.deprecated import handle_deprecated_model_args
|
29
|
+
from aider.format_settings import format_settings, scrub_sensitive_info
|
30
|
+
from aider.history import ChatSummary
|
31
|
+
from aider.io import InputOutput
|
32
|
+
from aider.llm import litellm # noqa: F401; properly init litellm on launch
|
33
|
+
from aider.models import ModelSettings
|
34
|
+
from aider.onboarding import offer_openrouter_oauth, select_default_model
|
35
|
+
from aider.repo import ANY_GIT_ERROR, GitRepo
|
36
|
+
from aider.report import report_uncaught_exceptions
|
37
|
+
from aider.versioncheck import check_version, install_from_main_branch, install_upgrade
|
38
|
+
from aider.watch import FileWatcher
|
39
|
+
|
40
|
+
from .dump import dump # noqa: F401
|
41
|
+
|
42
|
+
|
43
|
+
def check_config_files_for_yes(config_files):
|
44
|
+
found = False
|
45
|
+
for config_file in config_files:
|
46
|
+
if Path(config_file).exists():
|
47
|
+
try:
|
48
|
+
with open(config_file, "r") as f:
|
49
|
+
for line in f:
|
50
|
+
if line.strip().startswith("yes:"):
|
51
|
+
print("Configuration error detected.")
|
52
|
+
print(f"The file {config_file} contains a line starting with 'yes:'")
|
53
|
+
print("Please replace 'yes:' with 'yes-always:' in this file.")
|
54
|
+
found = True
|
55
|
+
except Exception:
|
56
|
+
pass
|
57
|
+
return found
|
58
|
+
|
59
|
+
|
60
|
+
def get_git_root():
|
61
|
+
"""Try and guess the git repo, since the conf.yml can be at the repo root"""
|
62
|
+
try:
|
63
|
+
repo = git.Repo(search_parent_directories=True)
|
64
|
+
return repo.working_tree_dir
|
65
|
+
except (git.InvalidGitRepositoryError, FileNotFoundError):
|
66
|
+
return None
|
67
|
+
|
68
|
+
|
69
|
+
def guessed_wrong_repo(io, git_root, fnames, git_dname):
|
70
|
+
"""After we parse the args, we can determine the real repo. Did we guess wrong?"""
|
71
|
+
|
72
|
+
try:
|
73
|
+
check_repo = Path(GitRepo(io, fnames, git_dname).root).resolve()
|
74
|
+
except (OSError,) + ANY_GIT_ERROR:
|
75
|
+
return
|
76
|
+
|
77
|
+
# we had no guess, rely on the "true" repo result
|
78
|
+
if not git_root:
|
79
|
+
return str(check_repo)
|
80
|
+
|
81
|
+
git_root = Path(git_root).resolve()
|
82
|
+
if check_repo == git_root:
|
83
|
+
return
|
84
|
+
|
85
|
+
return str(check_repo)
|
86
|
+
|
87
|
+
|
88
|
+
def make_new_repo(git_root, io):
|
89
|
+
try:
|
90
|
+
repo = git.Repo.init(git_root)
|
91
|
+
check_gitignore(git_root, io, False)
|
92
|
+
except ANY_GIT_ERROR as err: # issue #1233
|
93
|
+
io.tool_error(f"Unable to create git repo in {git_root}")
|
94
|
+
io.tool_output(str(err))
|
95
|
+
return
|
96
|
+
|
97
|
+
io.tool_output(f"Git repository created in {git_root}")
|
98
|
+
return repo
|
99
|
+
|
100
|
+
|
101
|
+
def setup_git(git_root, io):
|
102
|
+
if git is None:
|
103
|
+
return
|
104
|
+
|
105
|
+
try:
|
106
|
+
cwd = Path.cwd()
|
107
|
+
except OSError:
|
108
|
+
cwd = None
|
109
|
+
|
110
|
+
repo = None
|
111
|
+
|
112
|
+
if git_root:
|
113
|
+
try:
|
114
|
+
repo = git.Repo(git_root)
|
115
|
+
except ANY_GIT_ERROR:
|
116
|
+
pass
|
117
|
+
elif cwd == Path.home():
|
118
|
+
io.tool_warning(
|
119
|
+
"You should probably run aider in your project's directory, not your home dir."
|
120
|
+
)
|
121
|
+
return
|
122
|
+
elif cwd and io.confirm_ask(
|
123
|
+
"No git repo found, create one to track aider's changes (recommended)?"
|
124
|
+
):
|
125
|
+
git_root = str(cwd.resolve())
|
126
|
+
repo = make_new_repo(git_root, io)
|
127
|
+
|
128
|
+
if not repo:
|
129
|
+
return
|
130
|
+
|
131
|
+
try:
|
132
|
+
user_name = repo.git.config("--get", "user.name") or None
|
133
|
+
except git.exc.GitCommandError:
|
134
|
+
user_name = None
|
135
|
+
|
136
|
+
try:
|
137
|
+
user_email = repo.git.config("--get", "user.email") or None
|
138
|
+
except git.exc.GitCommandError:
|
139
|
+
user_email = None
|
140
|
+
|
141
|
+
if user_name and user_email:
|
142
|
+
return repo.working_tree_dir
|
143
|
+
|
144
|
+
with repo.config_writer() as git_config:
|
145
|
+
if not user_name:
|
146
|
+
git_config.set_value("user", "name", "Your Name")
|
147
|
+
io.tool_warning('Update git name with: git config user.name "Your Name"')
|
148
|
+
if not user_email:
|
149
|
+
git_config.set_value("user", "email", "you@example.com")
|
150
|
+
io.tool_warning('Update git email with: git config user.email "you@example.com"')
|
151
|
+
|
152
|
+
return repo.working_tree_dir
|
153
|
+
|
154
|
+
|
155
|
+
def check_gitignore(git_root, io, ask=True):
|
156
|
+
if not git_root:
|
157
|
+
return
|
158
|
+
|
159
|
+
try:
|
160
|
+
repo = git.Repo(git_root)
|
161
|
+
patterns_to_add = []
|
162
|
+
|
163
|
+
if not repo.ignored(".aider"):
|
164
|
+
patterns_to_add.append(".aider*")
|
165
|
+
|
166
|
+
env_path = Path(git_root) / ".env"
|
167
|
+
if env_path.exists() and not repo.ignored(".env"):
|
168
|
+
patterns_to_add.append(".env")
|
169
|
+
|
170
|
+
if not patterns_to_add:
|
171
|
+
return
|
172
|
+
|
173
|
+
gitignore_file = Path(git_root) / ".gitignore"
|
174
|
+
if gitignore_file.exists():
|
175
|
+
try:
|
176
|
+
content = io.read_text(gitignore_file)
|
177
|
+
if content is None:
|
178
|
+
return
|
179
|
+
if not content.endswith("\n"):
|
180
|
+
content += "\n"
|
181
|
+
except OSError as e:
|
182
|
+
io.tool_error(f"Error when trying to read {gitignore_file}: {e}")
|
183
|
+
return
|
184
|
+
else:
|
185
|
+
content = ""
|
186
|
+
except ANY_GIT_ERROR:
|
187
|
+
return
|
188
|
+
|
189
|
+
if ask:
|
190
|
+
io.tool_output("You can skip this check with --no-gitignore")
|
191
|
+
if not io.confirm_ask(f"Add {', '.join(patterns_to_add)} to .gitignore (recommended)?"):
|
192
|
+
return
|
193
|
+
|
194
|
+
content += "\n".join(patterns_to_add) + "\n"
|
195
|
+
|
196
|
+
try:
|
197
|
+
io.write_text(gitignore_file, content)
|
198
|
+
io.tool_output(f"Added {', '.join(patterns_to_add)} to .gitignore")
|
199
|
+
except OSError as e:
|
200
|
+
io.tool_error(f"Error when trying to write to {gitignore_file}: {e}")
|
201
|
+
io.tool_output(
|
202
|
+
"Try running with appropriate permissions or manually add these patterns to .gitignore:"
|
203
|
+
)
|
204
|
+
for pattern in patterns_to_add:
|
205
|
+
io.tool_output(f" {pattern}")
|
206
|
+
|
207
|
+
|
208
|
+
def check_streamlit_install(io):
|
209
|
+
return utils.check_pip_install_extra(
|
210
|
+
io,
|
211
|
+
"streamlit",
|
212
|
+
"You need to install the aider browser feature",
|
213
|
+
["aider-chat[browser]"],
|
214
|
+
)
|
215
|
+
|
216
|
+
|
217
|
+
def write_streamlit_credentials():
|
218
|
+
from streamlit.file_util import get_streamlit_file_path
|
219
|
+
|
220
|
+
# See https://github.com/Aider-AI/aider/issues/772
|
221
|
+
|
222
|
+
credential_path = Path(get_streamlit_file_path()) / "credentials.toml"
|
223
|
+
if not os.path.exists(credential_path):
|
224
|
+
empty_creds = '[general]\nemail = ""\n'
|
225
|
+
|
226
|
+
os.makedirs(os.path.dirname(credential_path), exist_ok=True)
|
227
|
+
with open(credential_path, "w") as f:
|
228
|
+
f.write(empty_creds)
|
229
|
+
else:
|
230
|
+
print("Streamlit credentials already exist.")
|
231
|
+
|
232
|
+
|
233
|
+
def launch_gui(args):
|
234
|
+
from streamlit.web import cli
|
235
|
+
|
236
|
+
from aider import gui
|
237
|
+
|
238
|
+
print()
|
239
|
+
print("CONTROL-C to exit...")
|
240
|
+
|
241
|
+
# Necessary so streamlit does not prompt the user for an email address.
|
242
|
+
write_streamlit_credentials()
|
243
|
+
|
244
|
+
target = gui.__file__
|
245
|
+
|
246
|
+
st_args = ["run", target]
|
247
|
+
|
248
|
+
st_args += [
|
249
|
+
"--browser.gatherUsageStats=false",
|
250
|
+
"--runner.magicEnabled=false",
|
251
|
+
"--server.runOnSave=false",
|
252
|
+
]
|
253
|
+
|
254
|
+
# https://github.com/Aider-AI/aider/issues/2193
|
255
|
+
is_dev = "-dev" in str(__version__)
|
256
|
+
|
257
|
+
if is_dev:
|
258
|
+
print("Watching for file changes.")
|
259
|
+
else:
|
260
|
+
st_args += [
|
261
|
+
"--global.developmentMode=false",
|
262
|
+
"--server.fileWatcherType=none",
|
263
|
+
"--client.toolbarMode=viewer", # minimal?
|
264
|
+
]
|
265
|
+
|
266
|
+
st_args += ["--"] + args
|
267
|
+
|
268
|
+
cli.main(st_args)
|
269
|
+
|
270
|
+
# from click.testing import CliRunner
|
271
|
+
# runner = CliRunner()
|
272
|
+
# from streamlit.web import bootstrap
|
273
|
+
# bootstrap.load_config_options(flag_options={})
|
274
|
+
# cli.main_run(target, args)
|
275
|
+
# sys.argv = ['streamlit', 'run', '--'] + args
|
276
|
+
|
277
|
+
|
278
|
+
def parse_lint_cmds(lint_cmds, io):
|
279
|
+
err = False
|
280
|
+
res = dict()
|
281
|
+
for lint_cmd in lint_cmds:
|
282
|
+
if re.match(r"^[a-z]+:.*", lint_cmd):
|
283
|
+
pieces = lint_cmd.split(":")
|
284
|
+
lang = pieces[0]
|
285
|
+
cmd = lint_cmd[len(lang) + 1 :]
|
286
|
+
lang = lang.strip()
|
287
|
+
else:
|
288
|
+
lang = None
|
289
|
+
cmd = lint_cmd
|
290
|
+
|
291
|
+
cmd = cmd.strip()
|
292
|
+
|
293
|
+
if cmd:
|
294
|
+
res[lang] = cmd
|
295
|
+
else:
|
296
|
+
io.tool_error(f'Unable to parse --lint-cmd "{lint_cmd}"')
|
297
|
+
io.tool_output('The arg should be "language: cmd --args ..."')
|
298
|
+
io.tool_output('For example: --lint-cmd "python: flake8 --select=E9"')
|
299
|
+
err = True
|
300
|
+
if err:
|
301
|
+
return
|
302
|
+
return res
|
303
|
+
|
304
|
+
|
305
|
+
def generate_search_path_list(default_file, git_root, command_line_file):
|
306
|
+
files = []
|
307
|
+
files.append(Path.home() / default_file) # homedir
|
308
|
+
if git_root:
|
309
|
+
files.append(Path(git_root) / default_file) # git root
|
310
|
+
files.append(default_file)
|
311
|
+
if command_line_file:
|
312
|
+
files.append(command_line_file)
|
313
|
+
|
314
|
+
resolved_files = []
|
315
|
+
for fn in files:
|
316
|
+
try:
|
317
|
+
resolved_files.append(Path(fn).resolve())
|
318
|
+
except OSError:
|
319
|
+
pass
|
320
|
+
|
321
|
+
files = resolved_files
|
322
|
+
files.reverse()
|
323
|
+
uniq = []
|
324
|
+
for fn in files:
|
325
|
+
if fn not in uniq:
|
326
|
+
uniq.append(fn)
|
327
|
+
uniq.reverse()
|
328
|
+
files = uniq
|
329
|
+
files = list(map(str, files))
|
330
|
+
files = list(dict.fromkeys(files))
|
331
|
+
|
332
|
+
return files
|
333
|
+
|
334
|
+
|
335
|
+
def register_models(git_root, model_settings_fname, io, verbose=False):
|
336
|
+
model_settings_files = generate_search_path_list(
|
337
|
+
".aider.model.settings.yml", git_root, model_settings_fname
|
338
|
+
)
|
339
|
+
|
340
|
+
try:
|
341
|
+
files_loaded = models.register_models(model_settings_files)
|
342
|
+
if len(files_loaded) > 0:
|
343
|
+
if verbose:
|
344
|
+
io.tool_output("Loaded model settings from:")
|
345
|
+
for file_loaded in files_loaded:
|
346
|
+
io.tool_output(f" - {file_loaded}") # noqa: E221
|
347
|
+
elif verbose:
|
348
|
+
io.tool_output("No model settings files loaded")
|
349
|
+
except Exception as e:
|
350
|
+
io.tool_error(f"Error loading aider model settings: {e}")
|
351
|
+
return 1
|
352
|
+
|
353
|
+
if verbose:
|
354
|
+
io.tool_output("Searched for model settings files:")
|
355
|
+
for file in model_settings_files:
|
356
|
+
io.tool_output(f" - {file}")
|
357
|
+
|
358
|
+
return None
|
359
|
+
|
360
|
+
|
361
|
+
def load_dotenv_files(git_root, dotenv_fname, encoding="utf-8"):
|
362
|
+
# Standard .env file search path
|
363
|
+
dotenv_files = generate_search_path_list(
|
364
|
+
".env",
|
365
|
+
git_root,
|
366
|
+
dotenv_fname,
|
367
|
+
)
|
368
|
+
|
369
|
+
# Explicitly add the OAuth keys file to the beginning of the list
|
370
|
+
oauth_keys_file = Path.home() / ".aider" / "oauth-keys.env"
|
371
|
+
if oauth_keys_file.exists():
|
372
|
+
# Insert at the beginning so it's loaded first (and potentially overridden)
|
373
|
+
dotenv_files.insert(0, str(oauth_keys_file.resolve()))
|
374
|
+
# Remove duplicates if it somehow got included by generate_search_path_list
|
375
|
+
dotenv_files = list(dict.fromkeys(dotenv_files))
|
376
|
+
|
377
|
+
loaded = []
|
378
|
+
for fname in dotenv_files:
|
379
|
+
try:
|
380
|
+
if Path(fname).exists():
|
381
|
+
load_dotenv(fname, override=True, encoding=encoding)
|
382
|
+
loaded.append(fname)
|
383
|
+
except OSError as e:
|
384
|
+
print(f"OSError loading {fname}: {e}")
|
385
|
+
except Exception as e:
|
386
|
+
print(f"Error loading {fname}: {e}")
|
387
|
+
return loaded
|
388
|
+
|
389
|
+
|
390
|
+
def register_litellm_models(git_root, model_metadata_fname, io, verbose=False):
|
391
|
+
model_metadata_files = []
|
392
|
+
|
393
|
+
# Add the resource file path
|
394
|
+
resource_metadata = importlib_resources.files("aider.resources").joinpath("model-metadata.json")
|
395
|
+
model_metadata_files.append(str(resource_metadata))
|
396
|
+
|
397
|
+
model_metadata_files += generate_search_path_list(
|
398
|
+
".aider.model.metadata.json", git_root, model_metadata_fname
|
399
|
+
)
|
400
|
+
|
401
|
+
try:
|
402
|
+
model_metadata_files_loaded = models.register_litellm_models(model_metadata_files)
|
403
|
+
if len(model_metadata_files_loaded) > 0 and verbose:
|
404
|
+
io.tool_output("Loaded model metadata from:")
|
405
|
+
for model_metadata_file in model_metadata_files_loaded:
|
406
|
+
io.tool_output(f" - {model_metadata_file}") # noqa: E221
|
407
|
+
except Exception as e:
|
408
|
+
io.tool_error(f"Error loading model metadata models: {e}")
|
409
|
+
return 1
|
410
|
+
|
411
|
+
|
412
|
+
def sanity_check_repo(repo, io):
|
413
|
+
if not repo:
|
414
|
+
return True
|
415
|
+
|
416
|
+
if not repo.repo.working_tree_dir:
|
417
|
+
io.tool_error("The git repo does not seem to have a working tree?")
|
418
|
+
return False
|
419
|
+
|
420
|
+
bad_ver = False
|
421
|
+
try:
|
422
|
+
repo.get_tracked_files()
|
423
|
+
if not repo.git_repo_error:
|
424
|
+
return True
|
425
|
+
error_msg = str(repo.git_repo_error)
|
426
|
+
except UnicodeDecodeError as exc:
|
427
|
+
error_msg = (
|
428
|
+
"Failed to read the Git repository. This issue is likely caused by a path encoded "
|
429
|
+
f'in a format different from the expected encoding "{sys.getfilesystemencoding()}".\n'
|
430
|
+
f"Internal error: {str(exc)}"
|
431
|
+
)
|
432
|
+
except ANY_GIT_ERROR as exc:
|
433
|
+
error_msg = str(exc)
|
434
|
+
bad_ver = "version in (1, 2)" in error_msg
|
435
|
+
except AssertionError as exc:
|
436
|
+
error_msg = str(exc)
|
437
|
+
bad_ver = True
|
438
|
+
|
439
|
+
if bad_ver:
|
440
|
+
io.tool_error("Aider only works with git repos with version number 1 or 2.")
|
441
|
+
io.tool_output("You may be able to convert your repo: git update-index --index-version=2")
|
442
|
+
io.tool_output("Or run aider --no-git to proceed without using git.")
|
443
|
+
io.offer_url(urls.git_index_version, "Open documentation url for more info?")
|
444
|
+
return False
|
445
|
+
|
446
|
+
io.tool_error("Unable to read git repository, it may be corrupt?")
|
447
|
+
io.tool_output(error_msg)
|
448
|
+
return False
|
449
|
+
|
450
|
+
|
451
|
+
def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False):
|
452
|
+
report_uncaught_exceptions()
|
453
|
+
|
454
|
+
if argv is None:
|
455
|
+
argv = sys.argv[1:]
|
456
|
+
|
457
|
+
if git is None:
|
458
|
+
git_root = None
|
459
|
+
elif force_git_root:
|
460
|
+
git_root = force_git_root
|
461
|
+
else:
|
462
|
+
git_root = get_git_root()
|
463
|
+
|
464
|
+
conf_fname = Path(".aider.conf.yml")
|
465
|
+
|
466
|
+
default_config_files = []
|
467
|
+
try:
|
468
|
+
default_config_files += [conf_fname.resolve()] # CWD
|
469
|
+
except OSError:
|
470
|
+
pass
|
471
|
+
|
472
|
+
if git_root:
|
473
|
+
git_conf = Path(git_root) / conf_fname # git root
|
474
|
+
if git_conf not in default_config_files:
|
475
|
+
default_config_files.append(git_conf)
|
476
|
+
default_config_files.append(Path.home() / conf_fname) # homedir
|
477
|
+
default_config_files = list(map(str, default_config_files))
|
478
|
+
|
479
|
+
parser = get_parser(default_config_files, git_root)
|
480
|
+
try:
|
481
|
+
args, unknown = parser.parse_known_args(argv)
|
482
|
+
except AttributeError as e:
|
483
|
+
if all(word in str(e) for word in ["bool", "object", "has", "no", "attribute", "strip"]):
|
484
|
+
if check_config_files_for_yes(default_config_files):
|
485
|
+
return 1
|
486
|
+
raise e
|
487
|
+
|
488
|
+
if args.verbose:
|
489
|
+
print("Config files search order, if no --config:")
|
490
|
+
for file in default_config_files:
|
491
|
+
exists = "(exists)" if Path(file).exists() else ""
|
492
|
+
print(f" - {file} {exists}")
|
493
|
+
|
494
|
+
default_config_files.reverse()
|
495
|
+
|
496
|
+
parser = get_parser(default_config_files, git_root)
|
497
|
+
|
498
|
+
args, unknown = parser.parse_known_args(argv)
|
499
|
+
|
500
|
+
# Load the .env file specified in the arguments
|
501
|
+
loaded_dotenvs = load_dotenv_files(git_root, args.env_file, args.encoding)
|
502
|
+
|
503
|
+
# Parse again to include any arguments that might have been defined in .env
|
504
|
+
args = parser.parse_args(argv)
|
505
|
+
|
506
|
+
if args.shell_completions:
|
507
|
+
# Ensure parser.prog is set for shtab, though it should be by default
|
508
|
+
parser.prog = "aider"
|
509
|
+
print(shtab.complete(parser, shell=args.shell_completions))
|
510
|
+
sys.exit(0)
|
511
|
+
|
512
|
+
if git is None:
|
513
|
+
args.git = False
|
514
|
+
|
515
|
+
if args.analytics_disable:
|
516
|
+
analytics = Analytics(permanently_disable=True)
|
517
|
+
print("Analytics have been permanently disabled.")
|
518
|
+
|
519
|
+
if not args.verify_ssl:
|
520
|
+
import httpx
|
521
|
+
|
522
|
+
os.environ["SSL_VERIFY"] = ""
|
523
|
+
litellm._load_litellm()
|
524
|
+
litellm._lazy_module.client_session = httpx.Client(verify=False)
|
525
|
+
litellm._lazy_module.aclient_session = httpx.AsyncClient(verify=False)
|
526
|
+
# Set verify_ssl on the model_info_manager
|
527
|
+
models.model_info_manager.set_verify_ssl(False)
|
528
|
+
|
529
|
+
if args.timeout:
|
530
|
+
models.request_timeout = args.timeout
|
531
|
+
|
532
|
+
if args.dark_mode:
|
533
|
+
args.user_input_color = "#32FF32"
|
534
|
+
args.tool_error_color = "#FF3333"
|
535
|
+
args.tool_warning_color = "#FFFF00"
|
536
|
+
args.assistant_output_color = "#00FFFF"
|
537
|
+
args.code_theme = "monokai"
|
538
|
+
|
539
|
+
if args.light_mode:
|
540
|
+
args.user_input_color = "green"
|
541
|
+
args.tool_error_color = "red"
|
542
|
+
args.tool_warning_color = "#FFA500"
|
543
|
+
args.assistant_output_color = "blue"
|
544
|
+
args.code_theme = "default"
|
545
|
+
|
546
|
+
if return_coder and args.yes_always is None:
|
547
|
+
args.yes_always = True
|
548
|
+
|
549
|
+
editing_mode = EditingMode.VI if args.vim else EditingMode.EMACS
|
550
|
+
|
551
|
+
def get_io(pretty):
|
552
|
+
return InputOutput(
|
553
|
+
pretty,
|
554
|
+
args.yes_always,
|
555
|
+
args.input_history_file,
|
556
|
+
args.chat_history_file,
|
557
|
+
input=input,
|
558
|
+
output=output,
|
559
|
+
user_input_color=args.user_input_color,
|
560
|
+
tool_output_color=args.tool_output_color,
|
561
|
+
tool_warning_color=args.tool_warning_color,
|
562
|
+
tool_error_color=args.tool_error_color,
|
563
|
+
completion_menu_color=args.completion_menu_color,
|
564
|
+
completion_menu_bg_color=args.completion_menu_bg_color,
|
565
|
+
completion_menu_current_color=args.completion_menu_current_color,
|
566
|
+
completion_menu_current_bg_color=args.completion_menu_current_bg_color,
|
567
|
+
assistant_output_color=args.assistant_output_color,
|
568
|
+
code_theme=args.code_theme,
|
569
|
+
dry_run=args.dry_run,
|
570
|
+
encoding=args.encoding,
|
571
|
+
line_endings=args.line_endings,
|
572
|
+
llm_history_file=args.llm_history_file,
|
573
|
+
editingmode=editing_mode,
|
574
|
+
fancy_input=args.fancy_input,
|
575
|
+
multiline_mode=args.multiline,
|
576
|
+
notifications=args.notifications,
|
577
|
+
notifications_command=args.notifications_command,
|
578
|
+
)
|
579
|
+
|
580
|
+
io = get_io(args.pretty)
|
581
|
+
try:
|
582
|
+
io.rule()
|
583
|
+
except UnicodeEncodeError as err:
|
584
|
+
if not io.pretty:
|
585
|
+
raise err
|
586
|
+
io = get_io(False)
|
587
|
+
io.tool_warning("Terminal does not support pretty output (UnicodeDecodeError)")
|
588
|
+
|
589
|
+
# Process any environment variables set via --set-env
|
590
|
+
if args.set_env:
|
591
|
+
for env_setting in args.set_env:
|
592
|
+
try:
|
593
|
+
name, value = env_setting.split("=", 1)
|
594
|
+
os.environ[name.strip()] = value.strip()
|
595
|
+
except ValueError:
|
596
|
+
io.tool_error(f"Invalid --set-env format: {env_setting}")
|
597
|
+
io.tool_output("Format should be: ENV_VAR_NAME=value")
|
598
|
+
return 1
|
599
|
+
|
600
|
+
# Process any API keys set via --api-key
|
601
|
+
if args.api_key:
|
602
|
+
for api_setting in args.api_key:
|
603
|
+
try:
|
604
|
+
provider, key = api_setting.split("=", 1)
|
605
|
+
env_var = f"{provider.strip().upper()}_API_KEY"
|
606
|
+
os.environ[env_var] = key.strip()
|
607
|
+
except ValueError:
|
608
|
+
io.tool_error(f"Invalid --api-key format: {api_setting}")
|
609
|
+
io.tool_output("Format should be: provider=key")
|
610
|
+
return 1
|
611
|
+
|
612
|
+
if args.anthropic_api_key:
|
613
|
+
os.environ["ANTHROPIC_API_KEY"] = args.anthropic_api_key
|
614
|
+
|
615
|
+
if args.openai_api_key:
|
616
|
+
os.environ["OPENAI_API_KEY"] = args.openai_api_key
|
617
|
+
|
618
|
+
# Handle deprecated model shortcut args
|
619
|
+
handle_deprecated_model_args(args, io)
|
620
|
+
if args.openai_api_base:
|
621
|
+
os.environ["OPENAI_API_BASE"] = args.openai_api_base
|
622
|
+
if args.openai_api_version:
|
623
|
+
io.tool_warning(
|
624
|
+
"--openai-api-version is deprecated, use --set-env OPENAI_API_VERSION=<value>"
|
625
|
+
)
|
626
|
+
os.environ["OPENAI_API_VERSION"] = args.openai_api_version
|
627
|
+
if args.openai_api_type:
|
628
|
+
io.tool_warning("--openai-api-type is deprecated, use --set-env OPENAI_API_TYPE=<value>")
|
629
|
+
os.environ["OPENAI_API_TYPE"] = args.openai_api_type
|
630
|
+
if args.openai_organization_id:
|
631
|
+
io.tool_warning(
|
632
|
+
"--openai-organization-id is deprecated, use --set-env OPENAI_ORGANIZATION=<value>"
|
633
|
+
)
|
634
|
+
os.environ["OPENAI_ORGANIZATION"] = args.openai_organization_id
|
635
|
+
|
636
|
+
analytics = Analytics(logfile=args.analytics_log, permanently_disable=args.analytics_disable)
|
637
|
+
if args.analytics is not False:
|
638
|
+
if analytics.need_to_ask(args.analytics):
|
639
|
+
io.tool_output(
|
640
|
+
"Aider respects your privacy and never collects your code, chat messages, keys or"
|
641
|
+
" personal info."
|
642
|
+
)
|
643
|
+
io.tool_output(f"For more info: {urls.analytics}")
|
644
|
+
disable = not io.confirm_ask(
|
645
|
+
"Allow collection of anonymous analytics to help improve aider?"
|
646
|
+
)
|
647
|
+
|
648
|
+
analytics.asked_opt_in = True
|
649
|
+
if disable:
|
650
|
+
analytics.disable(permanently=True)
|
651
|
+
io.tool_output("Analytics have been permanently disabled.")
|
652
|
+
|
653
|
+
analytics.save_data()
|
654
|
+
io.tool_output()
|
655
|
+
|
656
|
+
# This is a no-op if the user has opted out
|
657
|
+
analytics.enable()
|
658
|
+
|
659
|
+
analytics.event("launched")
|
660
|
+
|
661
|
+
if args.gui and not return_coder:
|
662
|
+
if not check_streamlit_install(io):
|
663
|
+
analytics.event("exit", reason="Streamlit not installed")
|
664
|
+
return
|
665
|
+
analytics.event("gui session")
|
666
|
+
launch_gui(argv)
|
667
|
+
analytics.event("exit", reason="GUI session ended")
|
668
|
+
return
|
669
|
+
|
670
|
+
if args.verbose:
|
671
|
+
for fname in loaded_dotenvs:
|
672
|
+
io.tool_output(f"Loaded {fname}")
|
673
|
+
|
674
|
+
all_files = args.files + (args.file or [])
|
675
|
+
fnames = [str(Path(fn).resolve()) for fn in all_files]
|
676
|
+
read_only_fnames = []
|
677
|
+
for fn in args.read or []:
|
678
|
+
path = Path(fn).expanduser().resolve()
|
679
|
+
if path.is_dir():
|
680
|
+
read_only_fnames.extend(str(f) for f in path.rglob("*") if f.is_file())
|
681
|
+
else:
|
682
|
+
read_only_fnames.append(str(path))
|
683
|
+
|
684
|
+
if len(all_files) > 1:
|
685
|
+
good = True
|
686
|
+
for fname in all_files:
|
687
|
+
if Path(fname).is_dir():
|
688
|
+
io.tool_error(f"{fname} is a directory, not provided alone.")
|
689
|
+
good = False
|
690
|
+
if not good:
|
691
|
+
io.tool_output(
|
692
|
+
"Provide either a single directory of a git repo, or a list of one or more files."
|
693
|
+
)
|
694
|
+
analytics.event("exit", reason="Invalid directory input")
|
695
|
+
return 1
|
696
|
+
|
697
|
+
git_dname = None
|
698
|
+
if len(all_files) == 1:
|
699
|
+
if Path(all_files[0]).is_dir():
|
700
|
+
if args.git:
|
701
|
+
git_dname = str(Path(all_files[0]).resolve())
|
702
|
+
fnames = []
|
703
|
+
else:
|
704
|
+
io.tool_error(f"{all_files[0]} is a directory, but --no-git selected.")
|
705
|
+
analytics.event("exit", reason="Directory with --no-git")
|
706
|
+
return 1
|
707
|
+
|
708
|
+
# We can't know the git repo for sure until after parsing the args.
|
709
|
+
# If we guessed wrong, reparse because that changes things like
|
710
|
+
# the location of the config.yml and history files.
|
711
|
+
if args.git and not force_git_root and git is not None:
|
712
|
+
right_repo_root = guessed_wrong_repo(io, git_root, fnames, git_dname)
|
713
|
+
if right_repo_root:
|
714
|
+
analytics.event("exit", reason="Recursing with correct repo")
|
715
|
+
return main(argv, input, output, right_repo_root, return_coder=return_coder)
|
716
|
+
|
717
|
+
if args.just_check_update:
|
718
|
+
update_available = check_version(io, just_check=True, verbose=args.verbose)
|
719
|
+
analytics.event("exit", reason="Just checking update")
|
720
|
+
return 0 if not update_available else 1
|
721
|
+
|
722
|
+
if args.install_main_branch:
|
723
|
+
success = install_from_main_branch(io)
|
724
|
+
analytics.event("exit", reason="Installed main branch")
|
725
|
+
return 0 if success else 1
|
726
|
+
|
727
|
+
if args.upgrade:
|
728
|
+
success = install_upgrade(io)
|
729
|
+
analytics.event("exit", reason="Upgrade completed")
|
730
|
+
return 0 if success else 1
|
731
|
+
|
732
|
+
if args.check_update:
|
733
|
+
check_version(io, verbose=args.verbose)
|
734
|
+
|
735
|
+
if args.git:
|
736
|
+
git_root = setup_git(git_root, io)
|
737
|
+
if args.gitignore:
|
738
|
+
check_gitignore(git_root, io)
|
739
|
+
|
740
|
+
if args.verbose:
|
741
|
+
show = format_settings(parser, args)
|
742
|
+
io.tool_output(show)
|
743
|
+
|
744
|
+
cmd_line = " ".join(sys.argv)
|
745
|
+
cmd_line = scrub_sensitive_info(args, cmd_line)
|
746
|
+
io.tool_output(cmd_line, log_only=True)
|
747
|
+
|
748
|
+
is_first_run = is_first_run_of_new_version(io, verbose=args.verbose)
|
749
|
+
check_and_load_imports(io, is_first_run, verbose=args.verbose)
|
750
|
+
|
751
|
+
register_models(git_root, args.model_settings_file, io, verbose=args.verbose)
|
752
|
+
register_litellm_models(git_root, args.model_metadata_file, io, verbose=args.verbose)
|
753
|
+
|
754
|
+
if args.list_models:
|
755
|
+
models.print_matching_models(io, args.list_models)
|
756
|
+
analytics.event("exit", reason="Listed models")
|
757
|
+
return 0
|
758
|
+
|
759
|
+
# Process any command line aliases
|
760
|
+
if args.alias:
|
761
|
+
for alias_def in args.alias:
|
762
|
+
# Split on first colon only
|
763
|
+
parts = alias_def.split(":", 1)
|
764
|
+
if len(parts) != 2:
|
765
|
+
io.tool_error(f"Invalid alias format: {alias_def}")
|
766
|
+
io.tool_output("Format should be: alias:model-name")
|
767
|
+
analytics.event("exit", reason="Invalid alias format error")
|
768
|
+
return 1
|
769
|
+
alias, model = parts
|
770
|
+
models.MODEL_ALIASES[alias.strip()] = model.strip()
|
771
|
+
|
772
|
+
selected_model_name = select_default_model(args, io, analytics)
|
773
|
+
if not selected_model_name:
|
774
|
+
# Error message and analytics event are handled within select_default_model
|
775
|
+
# It might have already offered OAuth if no model/keys were found.
|
776
|
+
# If it failed here, we exit.
|
777
|
+
return 1
|
778
|
+
args.model = selected_model_name # Update args with the selected model
|
779
|
+
|
780
|
+
# Check if an OpenRouter model was selected/specified but the key is missing
|
781
|
+
if args.model.startswith("openrouter/") and not os.environ.get("OPENROUTER_API_KEY"):
|
782
|
+
io.tool_warning(
|
783
|
+
f"The specified model '{args.model}' requires an OpenRouter API key, which was not"
|
784
|
+
" found."
|
785
|
+
)
|
786
|
+
# Attempt OAuth flow because the specific model needs it
|
787
|
+
if offer_openrouter_oauth(io, analytics):
|
788
|
+
# OAuth succeeded, the key should now be in os.environ.
|
789
|
+
# Check if the key is now present after the flow.
|
790
|
+
if os.environ.get("OPENROUTER_API_KEY"):
|
791
|
+
io.tool_output(
|
792
|
+
"OpenRouter successfully connected."
|
793
|
+
) # Inform user connection worked
|
794
|
+
else:
|
795
|
+
# This case should ideally not happen if offer_openrouter_oauth succeeded
|
796
|
+
# but check defensively.
|
797
|
+
io.tool_error(
|
798
|
+
"OpenRouter authentication seemed successful, but the key is still missing."
|
799
|
+
)
|
800
|
+
analytics.event(
|
801
|
+
"exit",
|
802
|
+
reason="OpenRouter key missing after successful OAuth for specified model",
|
803
|
+
)
|
804
|
+
return 1
|
805
|
+
else:
|
806
|
+
# OAuth failed or was declined by the user
|
807
|
+
io.tool_error(
|
808
|
+
f"Unable to proceed without an OpenRouter API key for model '{args.model}'."
|
809
|
+
)
|
810
|
+
io.offer_url(urls.models_and_keys, "Open documentation URL for more info?")
|
811
|
+
analytics.event(
|
812
|
+
"exit",
|
813
|
+
reason="OpenRouter key missing for specified model and OAuth failed/declined",
|
814
|
+
)
|
815
|
+
return 1
|
816
|
+
|
817
|
+
main_model = models.Model(
|
818
|
+
args.model,
|
819
|
+
weak_model=args.weak_model,
|
820
|
+
editor_model=args.editor_model,
|
821
|
+
editor_edit_format=args.editor_edit_format,
|
822
|
+
verbose=args.verbose,
|
823
|
+
)
|
824
|
+
|
825
|
+
# Check if deprecated remove_reasoning is set
|
826
|
+
if main_model.remove_reasoning is not None:
|
827
|
+
io.tool_warning(
|
828
|
+
"Model setting 'remove_reasoning' is deprecated, please use 'reasoning_tag' instead."
|
829
|
+
)
|
830
|
+
|
831
|
+
# Set reasoning effort and thinking tokens if specified
|
832
|
+
if args.reasoning_effort is not None:
|
833
|
+
# Apply if check is disabled or model explicitly supports it
|
834
|
+
if not args.check_model_accepts_settings or (
|
835
|
+
main_model.accepts_settings and "reasoning_effort" in main_model.accepts_settings
|
836
|
+
):
|
837
|
+
main_model.set_reasoning_effort(args.reasoning_effort)
|
838
|
+
|
839
|
+
if args.thinking_tokens is not None:
|
840
|
+
# Apply if check is disabled or model explicitly supports it
|
841
|
+
if not args.check_model_accepts_settings or (
|
842
|
+
main_model.accepts_settings and "thinking_tokens" in main_model.accepts_settings
|
843
|
+
):
|
844
|
+
main_model.set_thinking_tokens(args.thinking_tokens)
|
845
|
+
|
846
|
+
# Show warnings about unsupported settings that are being ignored
|
847
|
+
if args.check_model_accepts_settings:
|
848
|
+
settings_to_check = [
|
849
|
+
{"arg": args.reasoning_effort, "name": "reasoning_effort"},
|
850
|
+
{"arg": args.thinking_tokens, "name": "thinking_tokens"},
|
851
|
+
]
|
852
|
+
|
853
|
+
for setting in settings_to_check:
|
854
|
+
if setting["arg"] is not None and (
|
855
|
+
not main_model.accepts_settings
|
856
|
+
or setting["name"] not in main_model.accepts_settings
|
857
|
+
):
|
858
|
+
io.tool_warning(
|
859
|
+
f"Warning: {main_model.name} does not support '{setting['name']}', ignoring."
|
860
|
+
)
|
861
|
+
io.tool_output(
|
862
|
+
f"Use --no-check-model-accepts-settings to force the '{setting['name']}'"
|
863
|
+
" setting."
|
864
|
+
)
|
865
|
+
|
866
|
+
if args.copy_paste and args.edit_format is None:
|
867
|
+
if main_model.edit_format in ("diff", "whole", "diff-fenced"):
|
868
|
+
main_model.edit_format = "editor-" + main_model.edit_format
|
869
|
+
|
870
|
+
if args.verbose:
|
871
|
+
io.tool_output("Model metadata:")
|
872
|
+
io.tool_output(json.dumps(main_model.info, indent=4))
|
873
|
+
|
874
|
+
io.tool_output("Model settings:")
|
875
|
+
for attr in sorted(fields(ModelSettings), key=lambda x: x.name):
|
876
|
+
val = getattr(main_model, attr.name)
|
877
|
+
val = json.dumps(val, indent=4)
|
878
|
+
io.tool_output(f"{attr.name}: {val}")
|
879
|
+
|
880
|
+
lint_cmds = parse_lint_cmds(args.lint_cmd, io)
|
881
|
+
if lint_cmds is None:
|
882
|
+
analytics.event("exit", reason="Invalid lint command format")
|
883
|
+
return 1
|
884
|
+
|
885
|
+
if args.show_model_warnings:
|
886
|
+
problem = models.sanity_check_models(io, main_model)
|
887
|
+
if problem:
|
888
|
+
analytics.event("model warning", main_model=main_model)
|
889
|
+
io.tool_output("You can skip this check with --no-show-model-warnings")
|
890
|
+
|
891
|
+
try:
|
892
|
+
io.offer_url(urls.model_warnings, "Open documentation url for more info?")
|
893
|
+
io.tool_output()
|
894
|
+
except KeyboardInterrupt:
|
895
|
+
analytics.event("exit", reason="Keyboard interrupt during model warnings")
|
896
|
+
return 1
|
897
|
+
|
898
|
+
repo = None
|
899
|
+
if args.git:
|
900
|
+
try:
|
901
|
+
repo = GitRepo(
|
902
|
+
io,
|
903
|
+
fnames,
|
904
|
+
git_dname,
|
905
|
+
args.aiderignore,
|
906
|
+
models=main_model.commit_message_models(),
|
907
|
+
attribute_author=args.attribute_author,
|
908
|
+
attribute_committer=args.attribute_committer,
|
909
|
+
attribute_commit_message_author=args.attribute_commit_message_author,
|
910
|
+
attribute_commit_message_committer=args.attribute_commit_message_committer,
|
911
|
+
commit_prompt=args.commit_prompt,
|
912
|
+
subtree_only=args.subtree_only,
|
913
|
+
git_commit_verify=args.git_commit_verify,
|
914
|
+
attribute_co_authored_by=args.attribute_co_authored_by, # Pass the arg
|
915
|
+
)
|
916
|
+
except FileNotFoundError:
|
917
|
+
pass
|
918
|
+
|
919
|
+
if not args.skip_sanity_check_repo:
|
920
|
+
if not sanity_check_repo(repo, io):
|
921
|
+
analytics.event("exit", reason="Repository sanity check failed")
|
922
|
+
return 1
|
923
|
+
|
924
|
+
if repo:
|
925
|
+
analytics.event("repo", num_files=len(repo.get_tracked_files()))
|
926
|
+
else:
|
927
|
+
analytics.event("no-repo")
|
928
|
+
|
929
|
+
commands = Commands(
|
930
|
+
io,
|
931
|
+
None,
|
932
|
+
voice_language=args.voice_language,
|
933
|
+
voice_input_device=args.voice_input_device,
|
934
|
+
voice_format=args.voice_format,
|
935
|
+
verify_ssl=args.verify_ssl,
|
936
|
+
args=args,
|
937
|
+
parser=parser,
|
938
|
+
verbose=args.verbose,
|
939
|
+
editor=args.editor,
|
940
|
+
original_read_only_fnames=read_only_fnames,
|
941
|
+
)
|
942
|
+
|
943
|
+
summarizer = ChatSummary(
|
944
|
+
[main_model.weak_model, main_model],
|
945
|
+
args.max_chat_history_tokens or main_model.max_chat_history_tokens,
|
946
|
+
)
|
947
|
+
|
948
|
+
if args.cache_prompts and args.map_refresh == "auto":
|
949
|
+
args.map_refresh = "files"
|
950
|
+
|
951
|
+
if not main_model.streaming:
|
952
|
+
if args.stream:
|
953
|
+
io.tool_warning(
|
954
|
+
f"Warning: Streaming is not supported by {main_model.name}. Disabling streaming."
|
955
|
+
)
|
956
|
+
args.stream = False
|
957
|
+
|
958
|
+
if args.map_tokens is None:
|
959
|
+
map_tokens = main_model.get_repo_map_tokens()
|
960
|
+
else:
|
961
|
+
map_tokens = args.map_tokens
|
962
|
+
|
963
|
+
# Track auto-commits configuration
|
964
|
+
analytics.event("auto_commits", enabled=bool(args.auto_commits))
|
965
|
+
|
966
|
+
try:
|
967
|
+
coder = Coder.create(
|
968
|
+
main_model=main_model,
|
969
|
+
edit_format=args.edit_format,
|
970
|
+
io=io,
|
971
|
+
repo=repo,
|
972
|
+
fnames=fnames,
|
973
|
+
read_only_fnames=read_only_fnames,
|
974
|
+
show_diffs=args.show_diffs,
|
975
|
+
auto_commits=args.auto_commits,
|
976
|
+
dirty_commits=args.dirty_commits,
|
977
|
+
dry_run=args.dry_run,
|
978
|
+
map_tokens=map_tokens,
|
979
|
+
verbose=args.verbose,
|
980
|
+
stream=args.stream,
|
981
|
+
use_git=args.git,
|
982
|
+
restore_chat_history=args.restore_chat_history,
|
983
|
+
auto_lint=args.auto_lint,
|
984
|
+
auto_test=args.auto_test,
|
985
|
+
lint_cmds=lint_cmds,
|
986
|
+
test_cmd=args.test_cmd,
|
987
|
+
commands=commands,
|
988
|
+
summarizer=summarizer,
|
989
|
+
analytics=analytics,
|
990
|
+
map_refresh=args.map_refresh,
|
991
|
+
cache_prompts=args.cache_prompts,
|
992
|
+
map_mul_no_files=args.map_multiplier_no_files,
|
993
|
+
num_cache_warming_pings=args.cache_keepalive_pings,
|
994
|
+
suggest_shell_commands=args.suggest_shell_commands,
|
995
|
+
chat_language=args.chat_language,
|
996
|
+
commit_language=args.commit_language,
|
997
|
+
detect_urls=args.detect_urls,
|
998
|
+
auto_copy_context=args.copy_paste,
|
999
|
+
auto_accept_architect=args.auto_accept_architect,
|
1000
|
+
)
|
1001
|
+
except UnknownEditFormat as err:
|
1002
|
+
io.tool_error(str(err))
|
1003
|
+
io.offer_url(urls.edit_formats, "Open documentation about edit formats?")
|
1004
|
+
analytics.event("exit", reason="Unknown edit format")
|
1005
|
+
return 1
|
1006
|
+
except ValueError as err:
|
1007
|
+
io.tool_error(str(err))
|
1008
|
+
analytics.event("exit", reason="ValueError during coder creation")
|
1009
|
+
return 1
|
1010
|
+
|
1011
|
+
if return_coder:
|
1012
|
+
analytics.event("exit", reason="Returning coder object")
|
1013
|
+
return coder
|
1014
|
+
|
1015
|
+
ignores = []
|
1016
|
+
if git_root:
|
1017
|
+
ignores.append(str(Path(git_root) / ".gitignore"))
|
1018
|
+
if args.aiderignore:
|
1019
|
+
ignores.append(args.aiderignore)
|
1020
|
+
|
1021
|
+
if args.watch_files:
|
1022
|
+
file_watcher = FileWatcher(
|
1023
|
+
coder,
|
1024
|
+
gitignores=ignores,
|
1025
|
+
verbose=args.verbose,
|
1026
|
+
analytics=analytics,
|
1027
|
+
root=str(Path.cwd()) if args.subtree_only else None,
|
1028
|
+
)
|
1029
|
+
coder.file_watcher = file_watcher
|
1030
|
+
|
1031
|
+
if args.copy_paste:
|
1032
|
+
analytics.event("copy-paste mode")
|
1033
|
+
ClipboardWatcher(coder.io, verbose=args.verbose)
|
1034
|
+
|
1035
|
+
coder.show_announcements()
|
1036
|
+
|
1037
|
+
if args.show_prompts:
|
1038
|
+
coder.cur_messages += [
|
1039
|
+
dict(role="user", content="Hello!"),
|
1040
|
+
]
|
1041
|
+
messages = coder.format_messages().all_messages()
|
1042
|
+
utils.show_messages(messages)
|
1043
|
+
analytics.event("exit", reason="Showed prompts")
|
1044
|
+
return
|
1045
|
+
|
1046
|
+
if args.lint:
|
1047
|
+
coder.commands.cmd_lint(fnames=fnames)
|
1048
|
+
|
1049
|
+
if args.test:
|
1050
|
+
if not args.test_cmd:
|
1051
|
+
io.tool_error("No --test-cmd provided.")
|
1052
|
+
analytics.event("exit", reason="No test command provided")
|
1053
|
+
return 1
|
1054
|
+
coder.commands.cmd_test(args.test_cmd)
|
1055
|
+
if io.placeholder:
|
1056
|
+
coder.run(io.placeholder)
|
1057
|
+
|
1058
|
+
if args.commit:
|
1059
|
+
if args.dry_run:
|
1060
|
+
io.tool_output("Dry run enabled, skipping commit.")
|
1061
|
+
else:
|
1062
|
+
coder.commands.cmd_commit()
|
1063
|
+
|
1064
|
+
if args.lint or args.test or args.commit:
|
1065
|
+
analytics.event("exit", reason="Completed lint/test/commit")
|
1066
|
+
return
|
1067
|
+
|
1068
|
+
if args.show_repo_map:
|
1069
|
+
repo_map = coder.get_repo_map()
|
1070
|
+
if repo_map:
|
1071
|
+
io.tool_output(repo_map)
|
1072
|
+
analytics.event("exit", reason="Showed repo map")
|
1073
|
+
return
|
1074
|
+
|
1075
|
+
if args.apply:
|
1076
|
+
content = io.read_text(args.apply)
|
1077
|
+
if content is None:
|
1078
|
+
analytics.event("exit", reason="Failed to read apply content")
|
1079
|
+
return
|
1080
|
+
coder.partial_response_content = content
|
1081
|
+
# For testing #2879
|
1082
|
+
# from aider.coders.base_coder import all_fences
|
1083
|
+
# coder.fence = all_fences[1]
|
1084
|
+
coder.apply_updates()
|
1085
|
+
analytics.event("exit", reason="Applied updates")
|
1086
|
+
return
|
1087
|
+
|
1088
|
+
if args.apply_clipboard_edits:
|
1089
|
+
args.edit_format = main_model.editor_edit_format
|
1090
|
+
args.message = "/paste"
|
1091
|
+
|
1092
|
+
if args.show_release_notes is True:
|
1093
|
+
io.tool_output(f"Opening release notes: {urls.release_notes}")
|
1094
|
+
io.tool_output()
|
1095
|
+
webbrowser.open(urls.release_notes)
|
1096
|
+
elif args.show_release_notes is None and is_first_run:
|
1097
|
+
io.tool_output()
|
1098
|
+
io.offer_url(
|
1099
|
+
urls.release_notes,
|
1100
|
+
"Would you like to see what's new in this version?",
|
1101
|
+
allow_never=False,
|
1102
|
+
)
|
1103
|
+
|
1104
|
+
if git_root and Path.cwd().resolve() != Path(git_root).resolve():
|
1105
|
+
io.tool_warning(
|
1106
|
+
"Note: in-chat filenames are always relative to the git working dir, not the current"
|
1107
|
+
" working dir."
|
1108
|
+
)
|
1109
|
+
|
1110
|
+
io.tool_output(f"Cur working dir: {Path.cwd()}")
|
1111
|
+
io.tool_output(f"Git working dir: {git_root}")
|
1112
|
+
|
1113
|
+
if args.stream and args.cache_prompts:
|
1114
|
+
io.tool_warning("Cost estimates may be inaccurate when using streaming and caching.")
|
1115
|
+
|
1116
|
+
if args.load:
|
1117
|
+
commands.cmd_load(args.load)
|
1118
|
+
|
1119
|
+
if args.message:
|
1120
|
+
io.add_to_input_history(args.message)
|
1121
|
+
io.tool_output()
|
1122
|
+
try:
|
1123
|
+
coder.run(with_message=args.message)
|
1124
|
+
except SwitchCoder:
|
1125
|
+
pass
|
1126
|
+
analytics.event("exit", reason="Completed --message")
|
1127
|
+
return
|
1128
|
+
|
1129
|
+
if args.message_file:
|
1130
|
+
try:
|
1131
|
+
message_from_file = io.read_text(args.message_file)
|
1132
|
+
io.tool_output()
|
1133
|
+
coder.run(with_message=message_from_file)
|
1134
|
+
except FileNotFoundError:
|
1135
|
+
io.tool_error(f"Message file not found: {args.message_file}")
|
1136
|
+
analytics.event("exit", reason="Message file not found")
|
1137
|
+
return 1
|
1138
|
+
except IOError as e:
|
1139
|
+
io.tool_error(f"Error reading message file: {e}")
|
1140
|
+
analytics.event("exit", reason="Message file IO error")
|
1141
|
+
return 1
|
1142
|
+
|
1143
|
+
analytics.event("exit", reason="Completed --message-file")
|
1144
|
+
return
|
1145
|
+
|
1146
|
+
if args.exit:
|
1147
|
+
analytics.event("exit", reason="Exit flag set")
|
1148
|
+
return
|
1149
|
+
|
1150
|
+
analytics.event("cli session", main_model=main_model, edit_format=main_model.edit_format)
|
1151
|
+
|
1152
|
+
while True:
|
1153
|
+
try:
|
1154
|
+
coder.ok_to_warm_cache = bool(args.cache_keepalive_pings)
|
1155
|
+
coder.run()
|
1156
|
+
analytics.event("exit", reason="Completed main CLI coder.run")
|
1157
|
+
return
|
1158
|
+
except SwitchCoder as switch:
|
1159
|
+
coder.ok_to_warm_cache = False
|
1160
|
+
|
1161
|
+
# Set the placeholder if provided
|
1162
|
+
if hasattr(switch, "placeholder") and switch.placeholder is not None:
|
1163
|
+
io.placeholder = switch.placeholder
|
1164
|
+
|
1165
|
+
kwargs = dict(io=io, from_coder=coder)
|
1166
|
+
kwargs.update(switch.kwargs)
|
1167
|
+
if "show_announcements" in kwargs:
|
1168
|
+
del kwargs["show_announcements"]
|
1169
|
+
|
1170
|
+
coder = Coder.create(**kwargs)
|
1171
|
+
|
1172
|
+
if switch.kwargs.get("show_announcements") is not False:
|
1173
|
+
coder.show_announcements()
|
1174
|
+
|
1175
|
+
|
1176
|
+
def is_first_run_of_new_version(io, verbose=False):
|
1177
|
+
"""Check if this is the first run of a new version/executable combination"""
|
1178
|
+
installs_file = Path.home() / ".aider" / "installs.json"
|
1179
|
+
key = (__version__, sys.executable)
|
1180
|
+
|
1181
|
+
# Never show notes for .dev versions
|
1182
|
+
if ".dev" in __version__:
|
1183
|
+
return False
|
1184
|
+
|
1185
|
+
if verbose:
|
1186
|
+
io.tool_output(
|
1187
|
+
f"Checking imports for version {__version__} and executable {sys.executable}"
|
1188
|
+
)
|
1189
|
+
io.tool_output(f"Installs file: {installs_file}")
|
1190
|
+
|
1191
|
+
try:
|
1192
|
+
if installs_file.exists():
|
1193
|
+
with open(installs_file, "r") as f:
|
1194
|
+
installs = json.load(f)
|
1195
|
+
if verbose:
|
1196
|
+
io.tool_output("Installs file exists and loaded")
|
1197
|
+
else:
|
1198
|
+
installs = {}
|
1199
|
+
if verbose:
|
1200
|
+
io.tool_output("Installs file does not exist, creating new dictionary")
|
1201
|
+
|
1202
|
+
is_first_run = str(key) not in installs
|
1203
|
+
|
1204
|
+
if is_first_run:
|
1205
|
+
installs[str(key)] = True
|
1206
|
+
installs_file.parent.mkdir(parents=True, exist_ok=True)
|
1207
|
+
with open(installs_file, "w") as f:
|
1208
|
+
json.dump(installs, f, indent=4)
|
1209
|
+
|
1210
|
+
return is_first_run
|
1211
|
+
|
1212
|
+
except Exception as e:
|
1213
|
+
io.tool_warning(f"Error checking version: {e}")
|
1214
|
+
if verbose:
|
1215
|
+
io.tool_output(f"Full exception details: {traceback.format_exc()}")
|
1216
|
+
return True # Safer to assume it's a first run if we hit an error
|
1217
|
+
|
1218
|
+
|
1219
|
+
def check_and_load_imports(io, is_first_run, verbose=False):
|
1220
|
+
try:
|
1221
|
+
if is_first_run:
|
1222
|
+
if verbose:
|
1223
|
+
io.tool_output(
|
1224
|
+
"First run for this version and executable, loading imports synchronously"
|
1225
|
+
)
|
1226
|
+
try:
|
1227
|
+
load_slow_imports(swallow=False)
|
1228
|
+
except Exception as err:
|
1229
|
+
io.tool_error(str(err))
|
1230
|
+
io.tool_output("Error loading required imports. Did you install aider properly?")
|
1231
|
+
io.offer_url(urls.install_properly, "Open documentation url for more info?")
|
1232
|
+
sys.exit(1)
|
1233
|
+
|
1234
|
+
if verbose:
|
1235
|
+
io.tool_output("Imports loaded and installs file updated")
|
1236
|
+
else:
|
1237
|
+
if verbose:
|
1238
|
+
io.tool_output("Not first run, loading imports in background thread")
|
1239
|
+
thread = threading.Thread(target=load_slow_imports)
|
1240
|
+
thread.daemon = True
|
1241
|
+
thread.start()
|
1242
|
+
|
1243
|
+
except Exception as e:
|
1244
|
+
io.tool_warning(f"Error in loading imports: {e}")
|
1245
|
+
if verbose:
|
1246
|
+
io.tool_output(f"Full exception details: {traceback.format_exc()}")
|
1247
|
+
|
1248
|
+
|
1249
|
+
def load_slow_imports(swallow=True):
|
1250
|
+
# These imports are deferred in various ways to
|
1251
|
+
# improve startup time.
|
1252
|
+
# This func is called either synchronously or in a thread
|
1253
|
+
# depending on whether it's been run before for this version and executable.
|
1254
|
+
|
1255
|
+
try:
|
1256
|
+
import httpx # noqa: F401
|
1257
|
+
import litellm # noqa: F401
|
1258
|
+
import networkx # noqa: F401
|
1259
|
+
import numpy # noqa: F401
|
1260
|
+
except Exception as e:
|
1261
|
+
if not swallow:
|
1262
|
+
raise e
|
1263
|
+
|
1264
|
+
|
1265
|
+
if __name__ == "__main__":
|
1266
|
+
status = main()
|
1267
|
+
sys.exit(status)
|