aider-ce 0.87.2.dev9__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.

Potentially problematic release.


This version of aider-ce might be problematic. Click here for more details.

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