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.
Files changed (228) hide show
  1. aider/__init__.py +20 -0
  2. aider/__main__.py +4 -0
  3. aider/_version.py +21 -0
  4. aider/analytics.py +250 -0
  5. aider/args.py +926 -0
  6. aider/args_formatter.py +228 -0
  7. aider/coders/__init__.py +34 -0
  8. aider/coders/architect_coder.py +48 -0
  9. aider/coders/architect_prompts.py +40 -0
  10. aider/coders/ask_coder.py +9 -0
  11. aider/coders/ask_prompts.py +35 -0
  12. aider/coders/base_coder.py +2483 -0
  13. aider/coders/base_prompts.py +60 -0
  14. aider/coders/chat_chunks.py +64 -0
  15. aider/coders/context_coder.py +53 -0
  16. aider/coders/context_prompts.py +75 -0
  17. aider/coders/editblock_coder.py +657 -0
  18. aider/coders/editblock_fenced_coder.py +10 -0
  19. aider/coders/editblock_fenced_prompts.py +143 -0
  20. aider/coders/editblock_func_coder.py +141 -0
  21. aider/coders/editblock_func_prompts.py +27 -0
  22. aider/coders/editblock_prompts.py +174 -0
  23. aider/coders/editor_diff_fenced_coder.py +9 -0
  24. aider/coders/editor_diff_fenced_prompts.py +11 -0
  25. aider/coders/editor_editblock_coder.py +8 -0
  26. aider/coders/editor_editblock_prompts.py +18 -0
  27. aider/coders/editor_whole_coder.py +8 -0
  28. aider/coders/editor_whole_prompts.py +10 -0
  29. aider/coders/help_coder.py +16 -0
  30. aider/coders/help_prompts.py +46 -0
  31. aider/coders/patch_coder.py +706 -0
  32. aider/coders/patch_prompts.py +161 -0
  33. aider/coders/search_replace.py +757 -0
  34. aider/coders/shell.py +37 -0
  35. aider/coders/single_wholefile_func_coder.py +102 -0
  36. aider/coders/single_wholefile_func_prompts.py +27 -0
  37. aider/coders/udiff_coder.py +429 -0
  38. aider/coders/udiff_prompts.py +115 -0
  39. aider/coders/udiff_simple.py +14 -0
  40. aider/coders/udiff_simple_prompts.py +25 -0
  41. aider/coders/wholefile_coder.py +144 -0
  42. aider/coders/wholefile_func_coder.py +134 -0
  43. aider/coders/wholefile_func_prompts.py +27 -0
  44. aider/coders/wholefile_prompts.py +67 -0
  45. aider/commands.py +1665 -0
  46. aider/copypaste.py +72 -0
  47. aider/deprecated.py +126 -0
  48. aider/diffs.py +128 -0
  49. aider/dump.py +29 -0
  50. aider/editor.py +147 -0
  51. aider/exceptions.py +107 -0
  52. aider/format_settings.py +26 -0
  53. aider/gui.py +545 -0
  54. aider/help.py +163 -0
  55. aider/help_pats.py +19 -0
  56. aider/history.py +143 -0
  57. aider/io.py +1175 -0
  58. aider/linter.py +304 -0
  59. aider/llm.py +47 -0
  60. aider/main.py +1267 -0
  61. aider/mdstream.py +243 -0
  62. aider/models.py +1286 -0
  63. aider/onboarding.py +428 -0
  64. aider/openrouter.py +128 -0
  65. aider/prompts.py +64 -0
  66. aider/queries/tree-sitter-language-pack/README.md +7 -0
  67. aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  68. aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  69. aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  70. aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  71. aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  72. aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  73. aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  74. aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  75. aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  76. aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  77. aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  78. aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  79. aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  80. aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  81. aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  82. aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  83. aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  84. aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  85. aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  86. aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  87. aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  88. aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  89. aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  90. aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  91. aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  92. aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  93. aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  94. aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  95. aider/queries/tree-sitter-languages/README.md +23 -0
  96. aider/queries/tree-sitter-languages/c-tags.scm +9 -0
  97. aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  98. aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  99. aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
  100. aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  101. aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  102. aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
  103. aider/queries/tree-sitter-languages/go-tags.scm +30 -0
  104. aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  105. aider/queries/tree-sitter-languages/java-tags.scm +20 -0
  106. aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  107. aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  108. aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  109. aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  110. aider/queries/tree-sitter-languages/php-tags.scm +26 -0
  111. aider/queries/tree-sitter-languages/python-tags.scm +12 -0
  112. aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
  113. aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  114. aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
  115. aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
  116. aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  117. aider/reasoning_tags.py +82 -0
  118. aider/repo.py +623 -0
  119. aider/repomap.py +847 -0
  120. aider/report.py +200 -0
  121. aider/resources/__init__.py +3 -0
  122. aider/resources/model-metadata.json +468 -0
  123. aider/resources/model-settings.yml +1767 -0
  124. aider/run_cmd.py +132 -0
  125. aider/scrape.py +284 -0
  126. aider/sendchat.py +61 -0
  127. aider/special.py +203 -0
  128. aider/urls.py +17 -0
  129. aider/utils.py +338 -0
  130. aider/versioncheck.py +113 -0
  131. aider/voice.py +187 -0
  132. aider/waiting.py +221 -0
  133. aider/watch.py +318 -0
  134. aider/watch_prompts.py +12 -0
  135. aider/website/Gemfile +8 -0
  136. aider/website/_includes/blame.md +162 -0
  137. aider/website/_includes/get-started.md +22 -0
  138. aider/website/_includes/help-tip.md +5 -0
  139. aider/website/_includes/help.md +24 -0
  140. aider/website/_includes/install.md +5 -0
  141. aider/website/_includes/keys.md +4 -0
  142. aider/website/_includes/model-warnings.md +67 -0
  143. aider/website/_includes/multi-line.md +22 -0
  144. aider/website/_includes/python-m-aider.md +5 -0
  145. aider/website/_includes/recording.css +228 -0
  146. aider/website/_includes/recording.md +34 -0
  147. aider/website/_includes/replit-pipx.md +9 -0
  148. aider/website/_includes/works-best.md +1 -0
  149. aider/website/_sass/custom/custom.scss +103 -0
  150. aider/website/docs/config/adv-model-settings.md +1881 -0
  151. aider/website/docs/config/aider_conf.md +527 -0
  152. aider/website/docs/config/api-keys.md +90 -0
  153. aider/website/docs/config/dotenv.md +478 -0
  154. aider/website/docs/config/editor.md +127 -0
  155. aider/website/docs/config/model-aliases.md +103 -0
  156. aider/website/docs/config/options.md +843 -0
  157. aider/website/docs/config/reasoning.md +209 -0
  158. aider/website/docs/config.md +44 -0
  159. aider/website/docs/faq.md +378 -0
  160. aider/website/docs/git.md +76 -0
  161. aider/website/docs/index.md +47 -0
  162. aider/website/docs/install/codespaces.md +39 -0
  163. aider/website/docs/install/docker.md +57 -0
  164. aider/website/docs/install/optional.md +100 -0
  165. aider/website/docs/install/replit.md +8 -0
  166. aider/website/docs/install.md +115 -0
  167. aider/website/docs/languages.md +264 -0
  168. aider/website/docs/legal/contributor-agreement.md +111 -0
  169. aider/website/docs/legal/privacy.md +104 -0
  170. aider/website/docs/llms/anthropic.md +77 -0
  171. aider/website/docs/llms/azure.md +48 -0
  172. aider/website/docs/llms/bedrock.md +132 -0
  173. aider/website/docs/llms/cohere.md +34 -0
  174. aider/website/docs/llms/deepseek.md +32 -0
  175. aider/website/docs/llms/gemini.md +49 -0
  176. aider/website/docs/llms/github.md +105 -0
  177. aider/website/docs/llms/groq.md +36 -0
  178. aider/website/docs/llms/lm-studio.md +39 -0
  179. aider/website/docs/llms/ollama.md +75 -0
  180. aider/website/docs/llms/openai-compat.md +39 -0
  181. aider/website/docs/llms/openai.md +58 -0
  182. aider/website/docs/llms/openrouter.md +78 -0
  183. aider/website/docs/llms/other.md +103 -0
  184. aider/website/docs/llms/vertex.md +50 -0
  185. aider/website/docs/llms/warnings.md +10 -0
  186. aider/website/docs/llms/xai.md +53 -0
  187. aider/website/docs/llms.md +54 -0
  188. aider/website/docs/more/analytics.md +122 -0
  189. aider/website/docs/more/edit-formats.md +116 -0
  190. aider/website/docs/more/infinite-output.md +137 -0
  191. aider/website/docs/more-info.md +8 -0
  192. aider/website/docs/recordings/auto-accept-architect.md +31 -0
  193. aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  194. aider/website/docs/recordings/index.md +21 -0
  195. aider/website/docs/recordings/model-accepts-settings.md +69 -0
  196. aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  197. aider/website/docs/repomap.md +112 -0
  198. aider/website/docs/scripting.md +100 -0
  199. aider/website/docs/troubleshooting/aider-not-found.md +24 -0
  200. aider/website/docs/troubleshooting/edit-errors.md +76 -0
  201. aider/website/docs/troubleshooting/imports.md +62 -0
  202. aider/website/docs/troubleshooting/models-and-keys.md +54 -0
  203. aider/website/docs/troubleshooting/support.md +79 -0
  204. aider/website/docs/troubleshooting/token-limits.md +96 -0
  205. aider/website/docs/troubleshooting/warnings.md +12 -0
  206. aider/website/docs/troubleshooting.md +11 -0
  207. aider/website/docs/usage/browser.md +57 -0
  208. aider/website/docs/usage/caching.md +49 -0
  209. aider/website/docs/usage/commands.md +132 -0
  210. aider/website/docs/usage/conventions.md +119 -0
  211. aider/website/docs/usage/copypaste.md +121 -0
  212. aider/website/docs/usage/images-urls.md +48 -0
  213. aider/website/docs/usage/lint-test.md +118 -0
  214. aider/website/docs/usage/modes.md +211 -0
  215. aider/website/docs/usage/not-code.md +179 -0
  216. aider/website/docs/usage/notifications.md +87 -0
  217. aider/website/docs/usage/tips.md +79 -0
  218. aider/website/docs/usage/tutorials.md +30 -0
  219. aider/website/docs/usage/voice.md +121 -0
  220. aider/website/docs/usage/watch.md +294 -0
  221. aider/website/docs/usage.md +92 -0
  222. aider/website/share/index.md +101 -0
  223. chatmcp_cli-0.1.0.dist-info/METADATA +502 -0
  224. chatmcp_cli-0.1.0.dist-info/RECORD +228 -0
  225. chatmcp_cli-0.1.0.dist-info/WHEEL +5 -0
  226. chatmcp_cli-0.1.0.dist-info/entry_points.txt +3 -0
  227. chatmcp_cli-0.1.0.dist-info/licenses/LICENSE.txt +202 -0
  228. 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)