chatmcp-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. aider/__init__.py +20 -0
  2. aider/__main__.py +4 -0
  3. aider/_version.py +21 -0
  4. aider/analytics.py +250 -0
  5. aider/args.py +926 -0
  6. aider/args_formatter.py +228 -0
  7. aider/coders/__init__.py +34 -0
  8. aider/coders/architect_coder.py +48 -0
  9. aider/coders/architect_prompts.py +40 -0
  10. aider/coders/ask_coder.py +9 -0
  11. aider/coders/ask_prompts.py +35 -0
  12. aider/coders/base_coder.py +2483 -0
  13. aider/coders/base_prompts.py +60 -0
  14. aider/coders/chat_chunks.py +64 -0
  15. aider/coders/context_coder.py +53 -0
  16. aider/coders/context_prompts.py +75 -0
  17. aider/coders/editblock_coder.py +657 -0
  18. aider/coders/editblock_fenced_coder.py +10 -0
  19. aider/coders/editblock_fenced_prompts.py +143 -0
  20. aider/coders/editblock_func_coder.py +141 -0
  21. aider/coders/editblock_func_prompts.py +27 -0
  22. aider/coders/editblock_prompts.py +174 -0
  23. aider/coders/editor_diff_fenced_coder.py +9 -0
  24. aider/coders/editor_diff_fenced_prompts.py +11 -0
  25. aider/coders/editor_editblock_coder.py +8 -0
  26. aider/coders/editor_editblock_prompts.py +18 -0
  27. aider/coders/editor_whole_coder.py +8 -0
  28. aider/coders/editor_whole_prompts.py +10 -0
  29. aider/coders/help_coder.py +16 -0
  30. aider/coders/help_prompts.py +46 -0
  31. aider/coders/patch_coder.py +706 -0
  32. aider/coders/patch_prompts.py +161 -0
  33. aider/coders/search_replace.py +757 -0
  34. aider/coders/shell.py +37 -0
  35. aider/coders/single_wholefile_func_coder.py +102 -0
  36. aider/coders/single_wholefile_func_prompts.py +27 -0
  37. aider/coders/udiff_coder.py +429 -0
  38. aider/coders/udiff_prompts.py +115 -0
  39. aider/coders/udiff_simple.py +14 -0
  40. aider/coders/udiff_simple_prompts.py +25 -0
  41. aider/coders/wholefile_coder.py +144 -0
  42. aider/coders/wholefile_func_coder.py +134 -0
  43. aider/coders/wholefile_func_prompts.py +27 -0
  44. aider/coders/wholefile_prompts.py +67 -0
  45. aider/commands.py +1665 -0
  46. aider/copypaste.py +72 -0
  47. aider/deprecated.py +126 -0
  48. aider/diffs.py +128 -0
  49. aider/dump.py +29 -0
  50. aider/editor.py +147 -0
  51. aider/exceptions.py +107 -0
  52. aider/format_settings.py +26 -0
  53. aider/gui.py +545 -0
  54. aider/help.py +163 -0
  55. aider/help_pats.py +19 -0
  56. aider/history.py +143 -0
  57. aider/io.py +1175 -0
  58. aider/linter.py +304 -0
  59. aider/llm.py +47 -0
  60. aider/main.py +1267 -0
  61. aider/mdstream.py +243 -0
  62. aider/models.py +1286 -0
  63. aider/onboarding.py +428 -0
  64. aider/openrouter.py +128 -0
  65. aider/prompts.py +64 -0
  66. aider/queries/tree-sitter-language-pack/README.md +7 -0
  67. aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  68. aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  69. aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  70. aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  71. aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  72. aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  73. aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  74. aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  75. aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  76. aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  77. aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  78. aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  79. aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  80. aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  81. aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  82. aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  83. aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  84. aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  85. aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  86. aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  87. aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  88. aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  89. aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  90. aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  91. aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  92. aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  93. aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  94. aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  95. aider/queries/tree-sitter-languages/README.md +23 -0
  96. aider/queries/tree-sitter-languages/c-tags.scm +9 -0
  97. aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  98. aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  99. aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
  100. aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  101. aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  102. aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
  103. aider/queries/tree-sitter-languages/go-tags.scm +30 -0
  104. aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  105. aider/queries/tree-sitter-languages/java-tags.scm +20 -0
  106. aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  107. aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  108. aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  109. aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  110. aider/queries/tree-sitter-languages/php-tags.scm +26 -0
  111. aider/queries/tree-sitter-languages/python-tags.scm +12 -0
  112. aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
  113. aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  114. aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
  115. aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
  116. aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  117. aider/reasoning_tags.py +82 -0
  118. aider/repo.py +623 -0
  119. aider/repomap.py +847 -0
  120. aider/report.py +200 -0
  121. aider/resources/__init__.py +3 -0
  122. aider/resources/model-metadata.json +468 -0
  123. aider/resources/model-settings.yml +1767 -0
  124. aider/run_cmd.py +132 -0
  125. aider/scrape.py +284 -0
  126. aider/sendchat.py +61 -0
  127. aider/special.py +203 -0
  128. aider/urls.py +17 -0
  129. aider/utils.py +338 -0
  130. aider/versioncheck.py +113 -0
  131. aider/voice.py +187 -0
  132. aider/waiting.py +221 -0
  133. aider/watch.py +318 -0
  134. aider/watch_prompts.py +12 -0
  135. aider/website/Gemfile +8 -0
  136. aider/website/_includes/blame.md +162 -0
  137. aider/website/_includes/get-started.md +22 -0
  138. aider/website/_includes/help-tip.md +5 -0
  139. aider/website/_includes/help.md +24 -0
  140. aider/website/_includes/install.md +5 -0
  141. aider/website/_includes/keys.md +4 -0
  142. aider/website/_includes/model-warnings.md +67 -0
  143. aider/website/_includes/multi-line.md +22 -0
  144. aider/website/_includes/python-m-aider.md +5 -0
  145. aider/website/_includes/recording.css +228 -0
  146. aider/website/_includes/recording.md +34 -0
  147. aider/website/_includes/replit-pipx.md +9 -0
  148. aider/website/_includes/works-best.md +1 -0
  149. aider/website/_sass/custom/custom.scss +103 -0
  150. aider/website/docs/config/adv-model-settings.md +1881 -0
  151. aider/website/docs/config/aider_conf.md +527 -0
  152. aider/website/docs/config/api-keys.md +90 -0
  153. aider/website/docs/config/dotenv.md +478 -0
  154. aider/website/docs/config/editor.md +127 -0
  155. aider/website/docs/config/model-aliases.md +103 -0
  156. aider/website/docs/config/options.md +843 -0
  157. aider/website/docs/config/reasoning.md +209 -0
  158. aider/website/docs/config.md +44 -0
  159. aider/website/docs/faq.md +378 -0
  160. aider/website/docs/git.md +76 -0
  161. aider/website/docs/index.md +47 -0
  162. aider/website/docs/install/codespaces.md +39 -0
  163. aider/website/docs/install/docker.md +57 -0
  164. aider/website/docs/install/optional.md +100 -0
  165. aider/website/docs/install/replit.md +8 -0
  166. aider/website/docs/install.md +115 -0
  167. aider/website/docs/languages.md +264 -0
  168. aider/website/docs/legal/contributor-agreement.md +111 -0
  169. aider/website/docs/legal/privacy.md +104 -0
  170. aider/website/docs/llms/anthropic.md +77 -0
  171. aider/website/docs/llms/azure.md +48 -0
  172. aider/website/docs/llms/bedrock.md +132 -0
  173. aider/website/docs/llms/cohere.md +34 -0
  174. aider/website/docs/llms/deepseek.md +32 -0
  175. aider/website/docs/llms/gemini.md +49 -0
  176. aider/website/docs/llms/github.md +105 -0
  177. aider/website/docs/llms/groq.md +36 -0
  178. aider/website/docs/llms/lm-studio.md +39 -0
  179. aider/website/docs/llms/ollama.md +75 -0
  180. aider/website/docs/llms/openai-compat.md +39 -0
  181. aider/website/docs/llms/openai.md +58 -0
  182. aider/website/docs/llms/openrouter.md +78 -0
  183. aider/website/docs/llms/other.md +103 -0
  184. aider/website/docs/llms/vertex.md +50 -0
  185. aider/website/docs/llms/warnings.md +10 -0
  186. aider/website/docs/llms/xai.md +53 -0
  187. aider/website/docs/llms.md +54 -0
  188. aider/website/docs/more/analytics.md +122 -0
  189. aider/website/docs/more/edit-formats.md +116 -0
  190. aider/website/docs/more/infinite-output.md +137 -0
  191. aider/website/docs/more-info.md +8 -0
  192. aider/website/docs/recordings/auto-accept-architect.md +31 -0
  193. aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  194. aider/website/docs/recordings/index.md +21 -0
  195. aider/website/docs/recordings/model-accepts-settings.md +69 -0
  196. aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  197. aider/website/docs/repomap.md +112 -0
  198. aider/website/docs/scripting.md +100 -0
  199. aider/website/docs/troubleshooting/aider-not-found.md +24 -0
  200. aider/website/docs/troubleshooting/edit-errors.md +76 -0
  201. aider/website/docs/troubleshooting/imports.md +62 -0
  202. aider/website/docs/troubleshooting/models-and-keys.md +54 -0
  203. aider/website/docs/troubleshooting/support.md +79 -0
  204. aider/website/docs/troubleshooting/token-limits.md +96 -0
  205. aider/website/docs/troubleshooting/warnings.md +12 -0
  206. aider/website/docs/troubleshooting.md +11 -0
  207. aider/website/docs/usage/browser.md +57 -0
  208. aider/website/docs/usage/caching.md +49 -0
  209. aider/website/docs/usage/commands.md +132 -0
  210. aider/website/docs/usage/conventions.md +119 -0
  211. aider/website/docs/usage/copypaste.md +121 -0
  212. aider/website/docs/usage/images-urls.md +48 -0
  213. aider/website/docs/usage/lint-test.md +118 -0
  214. aider/website/docs/usage/modes.md +211 -0
  215. aider/website/docs/usage/not-code.md +179 -0
  216. aider/website/docs/usage/notifications.md +87 -0
  217. aider/website/docs/usage/tips.md +79 -0
  218. aider/website/docs/usage/tutorials.md +30 -0
  219. aider/website/docs/usage/voice.md +121 -0
  220. aider/website/docs/usage/watch.md +294 -0
  221. aider/website/docs/usage.md +92 -0
  222. aider/website/share/index.md +101 -0
  223. chatmcp_cli-0.1.0.dist-info/METADATA +502 -0
  224. chatmcp_cli-0.1.0.dist-info/RECORD +228 -0
  225. chatmcp_cli-0.1.0.dist-info/WHEEL +5 -0
  226. chatmcp_cli-0.1.0.dist-info/entry_points.txt +3 -0
  227. chatmcp_cli-0.1.0.dist-info/licenses/LICENSE.txt +202 -0
  228. chatmcp_cli-0.1.0.dist-info/top_level.txt +1 -0
aider/onboarding.py ADDED
@@ -0,0 +1,428 @@
1
+ import base64
2
+ import hashlib
3
+ import http.server
4
+ import os
5
+ import secrets
6
+ import socketserver
7
+ import threading
8
+ import time
9
+ import webbrowser
10
+ from urllib.parse import parse_qs, urlparse
11
+
12
+ import requests
13
+
14
+ from aider import urls
15
+ from aider.io import InputOutput
16
+
17
+
18
+ def check_openrouter_tier(api_key):
19
+ """
20
+ Checks if the user is on a free tier for OpenRouter.
21
+
22
+ Args:
23
+ api_key: The OpenRouter API key to check.
24
+
25
+ Returns:
26
+ A boolean indicating if the user is on a free tier (True) or paid tier (False).
27
+ Returns True if the check fails.
28
+ """
29
+ try:
30
+ response = requests.get(
31
+ "https://openrouter.ai/api/v1/auth/key",
32
+ headers={"Authorization": f"Bearer {api_key}"},
33
+ timeout=5, # Add a reasonable timeout
34
+ )
35
+ response.raise_for_status()
36
+ data = response.json()
37
+ # According to the documentation, 'is_free_tier' will be true if the user has never paid
38
+ return data.get("data", {}).get("is_free_tier", True) # Default to True if not found
39
+ except Exception:
40
+ # If there's any error, we'll default to assuming free tier
41
+ return True
42
+
43
+
44
+ def try_to_select_default_model():
45
+ """
46
+ Attempts to select a default model based on available API keys.
47
+ Checks OpenRouter tier status to select appropriate model.
48
+
49
+ Returns:
50
+ The name of the selected model, or None if no suitable default is found.
51
+ """
52
+ # Special handling for OpenRouter
53
+ openrouter_key = os.environ.get("OPENROUTER_API_KEY")
54
+ if openrouter_key:
55
+ # Check if the user is on a free tier
56
+ is_free_tier = check_openrouter_tier(openrouter_key)
57
+ if is_free_tier:
58
+ return "openrouter/deepseek/deepseek-r1:free"
59
+ else:
60
+ return "openrouter/anthropic/claude-sonnet-4"
61
+
62
+ # Select model based on other available API keys
63
+ model_key_pairs = [
64
+ ("ANTHROPIC_API_KEY", "sonnet"),
65
+ ("DEEPSEEK_API_KEY", "deepseek"),
66
+ ("OPENAI_API_KEY", "gpt-4o"),
67
+ ("GEMINI_API_KEY", "gemini/gemini-2.5-pro-exp-03-25"),
68
+ ("VERTEXAI_PROJECT", "vertex_ai/gemini-2.5-pro-exp-03-25"),
69
+ ]
70
+
71
+ for env_key, model_name in model_key_pairs:
72
+ api_key_value = os.environ.get(env_key)
73
+ if api_key_value:
74
+ return model_name
75
+
76
+ return None
77
+
78
+
79
+ def offer_openrouter_oauth(io, analytics):
80
+ """
81
+ Offers OpenRouter OAuth flow to the user if no API keys are found.
82
+
83
+ Args:
84
+ io: The InputOutput object for user interaction.
85
+ analytics: The Analytics object for tracking events.
86
+
87
+ Returns:
88
+ True if authentication was successful, False otherwise.
89
+ """
90
+ # No API keys found - Offer OpenRouter OAuth
91
+ io.tool_output("OpenRouter provides free and paid access to many LLMs.")
92
+ # Use confirm_ask which handles non-interactive cases
93
+ if io.confirm_ask(
94
+ "Login to OpenRouter or create a free account?",
95
+ default="y",
96
+ ):
97
+ analytics.event("oauth_flow_initiated", provider="openrouter")
98
+ openrouter_key = start_openrouter_oauth_flow(io, analytics)
99
+ if openrouter_key:
100
+ # Successfully got key via OAuth, use the default OpenRouter model
101
+ # Ensure OPENROUTER_API_KEY is now set in the environment for later use
102
+ os.environ["OPENROUTER_API_KEY"] = openrouter_key
103
+ # Track OAuth success leading to model selection
104
+ analytics.event("oauth_flow_success")
105
+ return True
106
+
107
+ # OAuth failed or was cancelled by user implicitly (e.g., closing browser)
108
+ # Error messages are handled within start_openrouter_oauth_flow
109
+ analytics.event("oauth_flow_failure")
110
+ io.tool_error("OpenRouter authentication did not complete successfully.")
111
+ # Fall through to the final error message
112
+
113
+ return False
114
+
115
+
116
+ def select_default_model(args, io, analytics):
117
+ """
118
+ Selects a default model based on available API keys if no model is specified.
119
+ Offers OAuth flow for OpenRouter if no keys are found.
120
+
121
+ Args:
122
+ args: The command line arguments object.
123
+ io: The InputOutput object for user interaction.
124
+ analytics: The Analytics object for tracking events.
125
+
126
+ Returns:
127
+ The name of the selected model, or None if no suitable default is found.
128
+ """
129
+ if args.model:
130
+ return args.model # Model already specified
131
+
132
+ model = try_to_select_default_model()
133
+ if model:
134
+ io.tool_warning(f"Using {model} model with API key from environment.")
135
+ analytics.event("auto_model_selection", model=model)
136
+ return model
137
+
138
+ no_model_msg = "No LLM model was specified and no API keys were provided."
139
+ io.tool_warning(no_model_msg)
140
+
141
+ # Try OAuth if no model was detected
142
+ offer_openrouter_oauth(io, analytics)
143
+
144
+ # Check again after potential OAuth success
145
+ model = try_to_select_default_model()
146
+ if model:
147
+ return model
148
+
149
+ io.offer_url(urls.models_and_keys, "Open documentation URL for more info?")
150
+
151
+
152
+ # Helper function to find an available port
153
+ def find_available_port(start_port=8484, end_port=8584):
154
+ for port in range(start_port, end_port + 1):
155
+ try:
156
+ # Check if the port is available by trying to bind to it
157
+ with socketserver.TCPServer(("localhost", port), None):
158
+ return port
159
+ except OSError:
160
+ # Port is likely already in use
161
+ continue
162
+ return None
163
+
164
+
165
+ # PKCE code generation
166
+ def generate_pkce_codes():
167
+ code_verifier = secrets.token_urlsafe(64)
168
+ hasher = hashlib.sha256()
169
+ hasher.update(code_verifier.encode("utf-8"))
170
+ code_challenge = base64.urlsafe_b64encode(hasher.digest()).rstrip(b"=").decode("utf-8")
171
+ return code_verifier, code_challenge
172
+
173
+
174
+ # Function to exchange the authorization code for an API key
175
+ def exchange_code_for_key(code, code_verifier, io):
176
+ try:
177
+ response = requests.post(
178
+ "https://openrouter.ai/api/v1/auth/keys",
179
+ headers={"Content-Type": "application/json"},
180
+ json={
181
+ "code": code,
182
+ "code_verifier": code_verifier,
183
+ "code_challenge_method": "S256",
184
+ },
185
+ timeout=30, # Add a timeout
186
+ )
187
+ response.raise_for_status() # Raise exception for bad status codes (4xx or 5xx)
188
+ data = response.json()
189
+ api_key = data.get("key")
190
+ if not api_key:
191
+ io.tool_error("Error: 'key' not found in OpenRouter response.")
192
+ io.tool_error(f"Response: {response.text}")
193
+ return None
194
+ return api_key
195
+ except requests.exceptions.Timeout:
196
+ io.tool_error("Error: Request to OpenRouter timed out during code exchange.")
197
+ return None
198
+ except requests.exceptions.HTTPError as e:
199
+ io.tool_error(
200
+ "Error exchanging code for OpenRouter key:"
201
+ f" {e.response.status_code} {e.response.reason}"
202
+ )
203
+ io.tool_error(f"Response: {e.response.text}")
204
+ return None
205
+ except requests.exceptions.RequestException as e:
206
+ io.tool_error(f"Error exchanging code for OpenRouter key: {e}")
207
+ return None
208
+ except Exception as e:
209
+ io.tool_error(f"Unexpected error during code exchange: {e}")
210
+ return None
211
+
212
+
213
+ # Function to start the OAuth flow
214
+ def start_openrouter_oauth_flow(io, analytics):
215
+ """Initiates the OpenRouter OAuth PKCE flow using a local server."""
216
+
217
+ port = find_available_port()
218
+ if not port:
219
+ io.tool_error("Could not find an available port between 8484 and 8584.")
220
+ io.tool_error("Please ensure a port in this range is free, or configure manually.")
221
+ return None
222
+
223
+ callback_url = f"http://localhost:{port}/callback/aider"
224
+ auth_code = None
225
+ server_error = None
226
+ server_started = threading.Event()
227
+ shutdown_server = threading.Event()
228
+
229
+ class OAuthCallbackHandler(http.server.SimpleHTTPRequestHandler):
230
+ def do_GET(self):
231
+ nonlocal auth_code
232
+ parsed_path = urlparse(self.path)
233
+ if parsed_path.path == "/callback/aider":
234
+ query_params = parse_qs(parsed_path.query)
235
+ if "code" in query_params:
236
+ auth_code = query_params["code"][0]
237
+ self.send_response(200)
238
+ self.send_header("Content-type", "text/html")
239
+ self.end_headers()
240
+ self.wfile.write(
241
+ b"<html><body><h1>Success!</h1>"
242
+ b"<p>Aider has received the authentication code. "
243
+ b"You can close this browser tab.</p></body></html>"
244
+ )
245
+ # Signal the main thread to shut down the server
246
+ # Signal the main thread to shut down the server
247
+ shutdown_server.set()
248
+ else:
249
+ # Redirect to aider website if 'code' is missing (e.g., user visited manually)
250
+ self.send_response(302) # Found (temporary redirect)
251
+ self.send_header("Location", urls.website)
252
+ self.end_headers()
253
+ # No need to set server_error, just redirect.
254
+ # Do NOT shut down the server here; wait for timeout or success.
255
+ else:
256
+ # Redirect anything else (e.g., favicon.ico) to the main website as well
257
+ self.send_response(302)
258
+ self.send_header("Location", urls.website)
259
+ self.end_headers()
260
+ self.wfile.write(b"Not Found")
261
+
262
+ def log_message(self, format, *args):
263
+ # Suppress server logging to keep terminal clean
264
+ pass
265
+
266
+ def run_server():
267
+ nonlocal server_error
268
+ try:
269
+ with socketserver.TCPServer(("localhost", port), OAuthCallbackHandler) as httpd:
270
+ io.tool_output(f"Temporary server listening on {callback_url}", log_only=True)
271
+ server_started.set() # Signal that the server is ready
272
+ # Wait until shutdown is requested or timeout occurs (handled by main thread)
273
+ while not shutdown_server.is_set():
274
+ httpd.handle_request() # Handle one request at a time
275
+ # Add a small sleep to prevent busy-waiting if needed,
276
+ # though handle_request should block appropriately.
277
+ time.sleep(0.1)
278
+ io.tool_output("Shutting down temporary server.", log_only=True)
279
+ except Exception as e:
280
+ server_error = f"Failed to start or run temporary server: {e}"
281
+ server_started.set() # Signal even if failed, error will be checked
282
+ shutdown_server.set() # Ensure shutdown logic proceeds
283
+
284
+ server_thread = threading.Thread(target=run_server, daemon=True)
285
+ server_thread.start()
286
+
287
+ # Wait briefly for the server to start, or for an error
288
+ if not server_started.wait(timeout=5):
289
+ io.tool_error("Temporary authentication server failed to start in time.")
290
+ shutdown_server.set() # Ensure thread exits if it eventually starts
291
+ server_thread.join(timeout=1)
292
+ return None
293
+
294
+ # Check if server failed during startup
295
+ if server_error:
296
+ io.tool_error(server_error)
297
+ shutdown_server.set() # Ensure thread exits
298
+ server_thread.join(timeout=1)
299
+ return None
300
+
301
+ # Generate codes and URL
302
+ code_verifier, code_challenge = generate_pkce_codes()
303
+ auth_url_base = "https://openrouter.ai/auth"
304
+ auth_params = {
305
+ "callback_url": callback_url,
306
+ "code_challenge": code_challenge,
307
+ "code_challenge_method": "S256",
308
+ }
309
+ auth_url = f"{auth_url_base}?{'&'.join(f'{k}={v}' for k, v in auth_params.items())}"
310
+
311
+ io.tool_output("\nPlease open this URL in your browser to connect Aider with OpenRouter:")
312
+ io.tool_output()
313
+ print(auth_url)
314
+
315
+ MINUTES = 5
316
+ io.tool_output(f"\nWaiting up to {MINUTES} minutes for you to finish in the browser...")
317
+ io.tool_output("Use Control-C to interrupt.")
318
+
319
+ try:
320
+ webbrowser.open(auth_url)
321
+ except Exception:
322
+ pass
323
+
324
+ # Wait for the callback to set the auth_code or for timeout/error
325
+ interrupted = False
326
+ try:
327
+ shutdown_server.wait(timeout=MINUTES * 60) # Convert minutes to seconds
328
+ except KeyboardInterrupt:
329
+ io.tool_warning("\nOAuth flow interrupted.")
330
+ analytics.event("oauth_flow_failed", provider="openrouter", reason="user_interrupt")
331
+ interrupted = True
332
+ # Ensure the server thread is signaled to shut down
333
+ shutdown_server.set()
334
+
335
+ # Join the server thread to ensure it's cleaned up
336
+ server_thread.join(timeout=1)
337
+
338
+ if interrupted:
339
+ return None # Return None if interrupted by user
340
+
341
+ if server_error:
342
+ io.tool_error(f"Authentication failed: {server_error}")
343
+ analytics.event("oauth_flow_failed", provider="openrouter", reason=server_error)
344
+ return None
345
+
346
+ if not auth_code:
347
+ io.tool_error("Authentication with OpenRouter failed.")
348
+ analytics.event("oauth_flow_failed", provider="openrouter")
349
+ return None
350
+
351
+ io.tool_output("Completing authentication...")
352
+ analytics.event("oauth_flow_code_received", provider="openrouter")
353
+
354
+ # Exchange code for key
355
+ api_key = exchange_code_for_key(auth_code, code_verifier, io)
356
+
357
+ if api_key:
358
+ # Set env var for the current session immediately
359
+ os.environ["OPENROUTER_API_KEY"] = api_key
360
+
361
+ # Save the key to the oauth-keys.env file
362
+ try:
363
+ config_dir = os.path.expanduser("~/.aider")
364
+ os.makedirs(config_dir, exist_ok=True)
365
+ key_file = os.path.join(config_dir, "oauth-keys.env")
366
+ with open(key_file, "a", encoding="utf-8") as f:
367
+ f.write(f'OPENROUTER_API_KEY="{api_key}"\n')
368
+
369
+ io.tool_warning("Aider will load the OpenRouter key automatically in future sessions.")
370
+ io.tool_output()
371
+
372
+ analytics.event("oauth_flow_success", provider="openrouter")
373
+ return api_key
374
+ except Exception as e:
375
+ io.tool_error(f"Successfully obtained key, but failed to save it to file: {e}")
376
+ io.tool_warning("Set OPENROUTER_API_KEY environment variable for this session only.")
377
+ # Still return the key for the current session even if saving failed
378
+ analytics.event("oauth_flow_save_failed", provider="openrouter", reason=str(e))
379
+ return api_key
380
+ else:
381
+ io.tool_error("Authentication with OpenRouter failed.")
382
+ analytics.event("oauth_flow_failed", provider="openrouter", reason="code_exchange_failed")
383
+ return None
384
+
385
+
386
+ # Dummy Analytics class for testing
387
+ class DummyAnalytics:
388
+ def event(self, *args, **kwargs):
389
+ # print(f"Analytics Event: {args} {kwargs}") # Optional: print events
390
+ pass
391
+
392
+
393
+ def main():
394
+ """Main function to test the OpenRouter OAuth flow."""
395
+ print("Starting OpenRouter OAuth flow test...")
396
+
397
+ # Use a real IO object for interaction
398
+ io = InputOutput(
399
+ pretty=True,
400
+ yes=False,
401
+ input_history_file=None,
402
+ chat_history_file=None,
403
+ tool_output_color="BLUE",
404
+ tool_error_color="RED",
405
+ )
406
+ # Use a dummy analytics object
407
+ analytics = DummyAnalytics()
408
+
409
+ # Ensure OPENROUTER_API_KEY is not set, to trigger the flow naturally
410
+ # (though start_openrouter_oauth_flow doesn't check this itself)
411
+ if "OPENROUTER_API_KEY" in os.environ:
412
+ print("Warning: OPENROUTER_API_KEY is already set in environment.")
413
+ # del os.environ["OPENROUTER_API_KEY"] # Optionally unset it for testing
414
+
415
+ api_key = start_openrouter_oauth_flow(io, analytics)
416
+
417
+ if api_key:
418
+ print("\nOAuth flow completed successfully!")
419
+ print(f"Obtained API Key (first 5 chars): {api_key[:5]}...")
420
+ # Be careful printing the key, even partially
421
+ else:
422
+ print("\nOAuth flow failed or was cancelled.")
423
+
424
+ print("\nOpenRouter OAuth flow test finished.")
425
+
426
+
427
+ if __name__ == "__main__":
428
+ main()
aider/openrouter.py ADDED
@@ -0,0 +1,128 @@
1
+ """
2
+ OpenRouter model metadata caching and lookup.
3
+
4
+ This module keeps a local cached copy of the OpenRouter model list
5
+ (downloaded from ``https://openrouter.ai/api/v1/models``) and exposes a
6
+ helper class that returns metadata for a given model in a format compatible
7
+ with litellm’s ``get_model_info``.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import time
13
+ from pathlib import Path
14
+ from typing import Dict
15
+
16
+ import requests
17
+
18
+
19
+ def _cost_per_token(val: str | None) -> float | None:
20
+ """Convert a price string (USD per token) to a float."""
21
+ if val in (None, "", "0"):
22
+ return 0.0 if val == "0" else None
23
+ try:
24
+ return float(val)
25
+ except Exception: # noqa: BLE001
26
+ return None
27
+
28
+
29
+ class OpenRouterModelManager:
30
+ MODELS_URL = "https://openrouter.ai/api/v1/models"
31
+ CACHE_TTL = 60 * 60 * 24 # 24 h
32
+
33
+ def __init__(self) -> None:
34
+ self.cache_dir = Path.home() / ".aider" / "caches"
35
+ self.cache_file = self.cache_dir / "openrouter_models.json"
36
+ self.content: Dict | None = None
37
+ self.verify_ssl: bool = True
38
+ self._cache_loaded = False
39
+
40
+ # ------------------------------------------------------------------ #
41
+ # Public API #
42
+ # ------------------------------------------------------------------ #
43
+ def set_verify_ssl(self, verify_ssl: bool) -> None:
44
+ """Enable/disable SSL verification for API requests."""
45
+ self.verify_ssl = verify_ssl
46
+
47
+ def get_model_info(self, model: str) -> Dict:
48
+ """
49
+ Return metadata for *model* or an empty ``dict`` when unknown.
50
+
51
+ ``model`` should use the aider naming convention, e.g.
52
+ ``openrouter/nousresearch/deephermes-3-mistral-24b-preview:free``.
53
+ """
54
+ self._ensure_content()
55
+ if not self.content or "data" not in self.content:
56
+ return {}
57
+
58
+ route = self._strip_prefix(model)
59
+
60
+ # Consider both the exact id and id without any “:suffix”.
61
+ candidates = {route}
62
+ if ":" in route:
63
+ candidates.add(route.split(":", 1)[0])
64
+
65
+ record = next((item for item in self.content["data"] if item.get("id") in candidates), None)
66
+ if not record:
67
+ return {}
68
+
69
+ context_len = (
70
+ record.get("top_provider", {}).get("context_length")
71
+ or record.get("context_length")
72
+ or None
73
+ )
74
+
75
+ pricing = record.get("pricing", {})
76
+ return {
77
+ "max_input_tokens": context_len,
78
+ "max_tokens": context_len,
79
+ "max_output_tokens": context_len,
80
+ "input_cost_per_token": _cost_per_token(pricing.get("prompt")),
81
+ "output_cost_per_token": _cost_per_token(pricing.get("completion")),
82
+ "litellm_provider": "openrouter",
83
+ }
84
+
85
+ # ------------------------------------------------------------------ #
86
+ # Internal helpers #
87
+ # ------------------------------------------------------------------ #
88
+ def _strip_prefix(self, model: str) -> str:
89
+ return model[len("openrouter/") :] if model.startswith("openrouter/") else model
90
+
91
+ def _ensure_content(self) -> None:
92
+ self._load_cache()
93
+ if not self.content:
94
+ self._update_cache()
95
+
96
+ def _load_cache(self) -> None:
97
+ if self._cache_loaded:
98
+ return
99
+ try:
100
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
101
+ if self.cache_file.exists():
102
+ cache_age = time.time() - self.cache_file.stat().st_mtime
103
+ if cache_age < self.CACHE_TTL:
104
+ try:
105
+ self.content = json.loads(self.cache_file.read_text())
106
+ except json.JSONDecodeError:
107
+ self.content = None
108
+ except OSError:
109
+ # Cache directory might be unwritable; ignore.
110
+ pass
111
+
112
+ self._cache_loaded = True
113
+
114
+ def _update_cache(self) -> None:
115
+ try:
116
+ response = requests.get(self.MODELS_URL, timeout=10, verify=self.verify_ssl)
117
+ if response.status_code == 200:
118
+ self.content = response.json()
119
+ try:
120
+ self.cache_file.write_text(json.dumps(self.content, indent=2))
121
+ except OSError:
122
+ pass # Non-fatal if we can’t write the cache
123
+ except Exception as ex: # noqa: BLE001
124
+ print(f"Failed to fetch OpenRouter model list: {ex}")
125
+ try:
126
+ self.cache_file.write_text("{}")
127
+ except OSError:
128
+ pass
aider/prompts.py ADDED
@@ -0,0 +1,64 @@
1
+ # flake8: noqa: E501
2
+
3
+
4
+ # COMMIT
5
+
6
+ # Conventional Commits text adapted from:
7
+ # https://www.conventionalcommits.org/en/v1.0.0/#summary
8
+ commit_system = """You are an expert software engineer that generates concise, \
9
+ one-line Git commit messages based on the provided diffs.
10
+ Review the provided context and diffs which are about to be committed to a git repo.
11
+ Review the diffs carefully.
12
+ Generate a one-line commit message for those changes.
13
+ The commit message should be structured as follows: <type>: <description>
14
+ Use these for <type>: fix, feat, build, chore, ci, docs, style, refactor, perf, test
15
+
16
+ Ensure the commit message:{language_instruction}
17
+ - Starts with the appropriate prefix.
18
+ - Is in the imperative mood (e.g., \"add feature\" not \"added feature\" or \"adding feature\").
19
+ - Does not exceed 72 characters.
20
+
21
+ Reply only with the one-line commit message, without any additional text, explanations, or line breaks.
22
+
23
+ Reply only with the one-line commit message, without any additional text, explanations, \
24
+ or line breaks.
25
+ """
26
+
27
+ # COMMANDS
28
+ undo_command_reply = (
29
+ "I did `git reset --hard HEAD~1` to discard the last edits. Please wait for further"
30
+ " instructions before attempting that change again. Feel free to ask relevant questions about"
31
+ " why the changes were reverted."
32
+ )
33
+
34
+ added_files = (
35
+ "I added these files to the chat: {fnames}\nLet me know if there are others we should add."
36
+ )
37
+
38
+
39
+ run_output = """I ran this command:
40
+
41
+ {command}
42
+
43
+ And got this output:
44
+
45
+ {output}
46
+ """
47
+
48
+ # CHAT HISTORY
49
+ summarize = """*Briefly* summarize this partial conversation about programming.
50
+ Include less detail about older parts and more detail about the most recent messages.
51
+ Start a new paragraph every time the topic changes!
52
+
53
+ This is only part of a longer conversation so *DO NOT* conclude the summary with language like "Finally, ...". Because the conversation continues after the summary.
54
+ The summary *MUST* include the function names, libraries, packages that are being discussed.
55
+ The summary *MUST* include the filenames that are being referenced by the assistant inside the ```...``` fenced code blocks!
56
+ The summaries *MUST NOT* include ```...``` fenced code blocks!
57
+
58
+ Phrase the summary with the USER in first person, telling the ASSISTANT about the conversation.
59
+ Write *as* the user.
60
+ The user should refer to the assistant as *you*.
61
+ Start the summary with "I asked you...".
62
+ """
63
+
64
+ summary_prefix = "I spoke to you previously about a number of things.\n"
@@ -0,0 +1,7 @@
1
+ These scm files are all adapted from the github repositories listed here:
2
+
3
+ https://github.com/Goldziher/tree-sitter-language-pack/blob/main/sources/language_definitions.json
4
+
5
+ See this URL for information on the licenses of each repo:
6
+
7
+ https://github.com/Goldziher/tree-sitter-language-pack/
@@ -0,0 +1,5 @@
1
+ (function_declarator
2
+ declarator: (identifier) @name.definition.function) @definition.function
3
+
4
+ (call_expression
5
+ function: (identifier) @name.reference.call) @reference.call
@@ -0,0 +1,9 @@
1
+ (struct_specifier name: (type_identifier) @name.definition.class body:(_)) @definition.class
2
+
3
+ (declaration type: (union_specifier name: (type_identifier) @name.definition.class)) @definition.class
4
+
5
+ (function_declarator declarator: (identifier) @name.definition.function) @definition.function
6
+
7
+ (type_definition declarator: (type_identifier) @name.definition.type) @definition.type
8
+
9
+ (enum_specifier name: (type_identifier) @name.definition.type) @definition.type
@@ -0,0 +1,16 @@
1
+ ; Definitions
2
+ (intent_def
3
+ (intent) @name.definition.intent) @definition.intent
4
+
5
+ (slot_def
6
+ (slot) @name.definition.slot) @definition.slot
7
+
8
+ (alias_def
9
+ (alias) @name.definition.alias) @definition.alias
10
+
11
+ ; References
12
+ (slot_ref
13
+ (slot) @name.reference.slot) @reference.slot
14
+
15
+ (alias_ref
16
+ (alias) @name.reference.alias) @reference.alias