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