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/voice.py ADDED
@@ -0,0 +1,205 @@
1
+ import math
2
+ import os
3
+ import queue
4
+ import tempfile
5
+ import time
6
+ import warnings
7
+
8
+ from prompt_toolkit.shortcuts import prompt
9
+
10
+ from aider.llm import litellm
11
+
12
+ from .dump import dump # noqa: F401
13
+
14
+ warnings.filterwarnings(
15
+ "ignore", message="Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work"
16
+ )
17
+ warnings.filterwarnings("ignore", category=SyntaxWarning)
18
+
19
+
20
+ try:
21
+ from pydub import AudioSegment # noqa
22
+ from pydub.exceptions import CouldntDecodeError, CouldntEncodeError # noqa
23
+
24
+ PYDUB_AVAILABLE = True
25
+ except (ModuleNotFoundError, ImportError) as e:
26
+ if "audioop" in str(e) or "pyaudioop" in str(e):
27
+ # Handle missing audioop/pyaudioop dependency gracefully
28
+ PYDUB_AVAILABLE = False
29
+ AudioSegment = None
30
+ CouldntDecodeError = Exception
31
+ CouldntEncodeError = Exception
32
+ else:
33
+ raise
34
+
35
+ try:
36
+ import soundfile as sf
37
+ except (OSError, ModuleNotFoundError):
38
+ sf = None
39
+
40
+
41
+ class SoundDeviceError(Exception):
42
+ pass
43
+
44
+
45
+ class Voice:
46
+ max_rms = 0
47
+ min_rms = 1e5
48
+ pct = 0
49
+
50
+ threshold = 0.15
51
+
52
+ def __init__(self, audio_format="wav", device_name=None):
53
+ if sf is None:
54
+ raise SoundDeviceError
55
+ try:
56
+ print("Initializing sound device...")
57
+ import sounddevice as sd
58
+
59
+ self.sd = sd
60
+
61
+ devices = sd.query_devices()
62
+
63
+ if device_name:
64
+ # Find the device with matching name
65
+ device_id = None
66
+ for i, device in enumerate(devices):
67
+ if device_name in device["name"]:
68
+ device_id = i
69
+ break
70
+ if device_id is None:
71
+ available_inputs = [d["name"] for d in devices if d["max_input_channels"] > 0]
72
+ raise ValueError(
73
+ f"Device '{device_name}' not found. Available input devices:"
74
+ f" {available_inputs}"
75
+ )
76
+
77
+ print(f"Using input device: {device_name} (ID: {device_id})")
78
+
79
+ self.device_id = device_id
80
+ else:
81
+ self.device_id = None
82
+
83
+ except (OSError, ModuleNotFoundError):
84
+ raise SoundDeviceError
85
+ if audio_format not in ["wav", "mp3", "webm"]:
86
+ raise ValueError(f"Unsupported audio format: {audio_format}")
87
+ self.audio_format = audio_format
88
+
89
+ def callback(self, indata, frames, time, status):
90
+ """This is called (from a separate thread) for each audio block."""
91
+ import numpy as np
92
+
93
+ rms = np.sqrt(np.mean(indata**2))
94
+ self.max_rms = max(self.max_rms, rms)
95
+ self.min_rms = min(self.min_rms, rms)
96
+
97
+ rng = self.max_rms - self.min_rms
98
+ if rng > 0.001:
99
+ self.pct = (rms - self.min_rms) / rng
100
+ else:
101
+ self.pct = 0.5
102
+
103
+ self.q.put(indata.copy())
104
+
105
+ def get_prompt(self):
106
+ num = 10
107
+ if math.isnan(self.pct) or self.pct < self.threshold:
108
+ cnt = 0
109
+ else:
110
+ cnt = int(self.pct * 10)
111
+
112
+ bar = "░" * cnt + "█" * (num - cnt)
113
+ bar = bar[:num]
114
+
115
+ dur = time.time() - self.start_time
116
+ return f"Recording, press ENTER when done... {dur:.1f}sec {bar}"
117
+
118
+ def record_and_transcribe(self, history=None, language=None):
119
+ try:
120
+ return self.raw_record_and_transcribe(history, language)
121
+ except KeyboardInterrupt:
122
+ return
123
+ except SoundDeviceError as e:
124
+ print(f"Error: {e}")
125
+ print("Please ensure you have a working audio input device connected and try again.")
126
+ return
127
+
128
+ def raw_record_and_transcribe(self, history, language):
129
+ self.q = queue.Queue()
130
+
131
+ temp_wav = tempfile.mktemp(suffix=".wav")
132
+
133
+ try:
134
+ sample_rate = int(self.sd.query_devices(self.device_id, "input")["default_samplerate"])
135
+ except (TypeError, ValueError):
136
+ sample_rate = 16000 # fallback to 16kHz if unable to query device
137
+ except self.sd.PortAudioError:
138
+ raise SoundDeviceError(
139
+ "No audio input device detected. Please check your audio settings and try again."
140
+ )
141
+
142
+ self.start_time = time.time()
143
+
144
+ try:
145
+ with self.sd.InputStream(
146
+ samplerate=sample_rate, channels=1, callback=self.callback, device=self.device_id
147
+ ):
148
+ prompt(self.get_prompt, refresh_interval=0.1)
149
+ except self.sd.PortAudioError as err:
150
+ raise SoundDeviceError(f"Error accessing audio input device: {err}")
151
+
152
+ with sf.SoundFile(temp_wav, mode="x", samplerate=sample_rate, channels=1) as file:
153
+ while not self.q.empty():
154
+ file.write(self.q.get())
155
+
156
+ use_audio_format = self.audio_format
157
+
158
+ # Check file size and offer to convert to mp3 if too large
159
+ file_size = os.path.getsize(temp_wav)
160
+ if file_size > 24.9 * 1024 * 1024 and self.audio_format == "wav":
161
+ print("\nWarning: {temp_wav} is too large, switching to mp3 format.")
162
+ use_audio_format = "mp3"
163
+
164
+ filename = temp_wav
165
+ if use_audio_format != "wav":
166
+ try:
167
+ if not PYDUB_AVAILABLE:
168
+ print(
169
+ f"Warning: pydub not available, cannot convert to {use_audio_format}. Using"
170
+ " original WAV file."
171
+ )
172
+ else:
173
+ new_filename = tempfile.mktemp(suffix=f".{use_audio_format}")
174
+ audio = AudioSegment.from_wav(temp_wav)
175
+ audio.export(new_filename, format=use_audio_format)
176
+ os.remove(temp_wav)
177
+ filename = new_filename
178
+ except (CouldntDecodeError, CouldntEncodeError) as e:
179
+ print(f"Error converting audio: {e}")
180
+ except (OSError, FileNotFoundError) as e:
181
+ print(f"File system error during conversion: {e}")
182
+ except Exception as e:
183
+ print(f"Unexpected error during audio conversion: {e}")
184
+
185
+ with open(filename, "rb") as fh:
186
+ try:
187
+ transcript = litellm.transcription(
188
+ model="whisper-1", file=fh, prompt=history, language=language
189
+ )
190
+ except Exception as err:
191
+ print(f"Unable to transcribe {filename}: {err}")
192
+ return
193
+
194
+ if filename != temp_wav:
195
+ os.remove(filename)
196
+
197
+ text = transcript.text
198
+ return text
199
+
200
+
201
+ if __name__ == "__main__":
202
+ api_key = os.getenv("OPENAI_API_KEY")
203
+ if not api_key:
204
+ raise ValueError("Please set the OPENAI_API_KEY environment variable.")
205
+ print(Voice().record_and_transcribe())
aider/waiting.py ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ A simple wrapper for rich.status to provide a spinner.
5
+ """
6
+
7
+ from rich.console import Console
8
+
9
+
10
+ class Spinner:
11
+ """A wrapper around rich.status.Status for displaying a spinner."""
12
+
13
+ def __init__(self, text: str = "Waiting..."):
14
+ self.text = text
15
+ self.console = Console()
16
+ self.status = None
17
+
18
+ def step(self, message=None):
19
+ """Start the spinner or update its text."""
20
+ if self.status is None:
21
+ self.status = self.console.status(self.text, spinner="dots2")
22
+ self.status.start()
23
+ elif message:
24
+ self.status.update(message)
25
+
26
+ def end(self):
27
+ """Stop the spinner."""
28
+ if self.status:
29
+ self.status.stop()
30
+ self.status = None
31
+
32
+ # Allow use as a context-manager
33
+ def __enter__(self):
34
+ self.step()
35
+ return self
36
+
37
+ def __exit__(self, exc_type, exc_val, exc_tb):
38
+ self.end()
aider/watch.py ADDED
@@ -0,0 +1,318 @@
1
+ import re
2
+ import threading
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from grep_ast import TreeContext
7
+ from pathspec import PathSpec
8
+ from pathspec.patterns import GitWildMatchPattern
9
+ from watchfiles import watch
10
+
11
+ from aider.dump import dump # noqa
12
+ from aider.watch_prompts import watch_ask_prompt, watch_code_prompt
13
+
14
+
15
+ def load_gitignores(gitignore_paths: list[Path]) -> Optional[PathSpec]:
16
+ """Load and parse multiple .gitignore files into a single PathSpec"""
17
+ if not gitignore_paths:
18
+ return None
19
+
20
+ patterns = [
21
+ ".aider*",
22
+ ".git",
23
+ # Common editor backup/temp files
24
+ "*~", # Emacs/vim backup
25
+ "*.bak", # Generic backup
26
+ "*.swp", # Vim swap
27
+ "*.swo", # Vim swap
28
+ "\\#*\\#", # Emacs auto-save
29
+ ".#*", # Emacs lock files
30
+ "*.tmp", # Generic temp files
31
+ "*.temp", # Generic temp files
32
+ "*.orig", # Merge conflict originals
33
+ "*.pyc", # Python bytecode
34
+ "__pycache__/", # Python cache dir
35
+ ".DS_Store", # macOS metadata
36
+ "Thumbs.db", # Windows thumbnail cache
37
+ "*.svg",
38
+ "*.pdf",
39
+ # IDE files
40
+ ".idea/", # JetBrains IDEs
41
+ ".vscode/", # VS Code
42
+ "*.sublime-*", # Sublime Text
43
+ ".project", # Eclipse
44
+ ".settings/", # Eclipse
45
+ "*.code-workspace", # VS Code workspace
46
+ # Environment files
47
+ ".env", # Environment variables
48
+ ".venv/", # Python virtual environments
49
+ "node_modules/", # Node.js dependencies
50
+ "vendor/", # Various dependencies
51
+ # Logs and caches
52
+ "*.log", # Log files
53
+ ".cache/", # Cache directories
54
+ ".pytest_cache/", # Python test cache
55
+ "coverage/", # Code coverage reports
56
+ ] # Always ignore
57
+ for path in gitignore_paths:
58
+ if path.exists():
59
+ with open(path) as f:
60
+ patterns.extend(f.readlines())
61
+
62
+ return PathSpec.from_lines(GitWildMatchPattern, patterns) if patterns else None
63
+
64
+
65
+ class FileWatcher:
66
+ """Watches source files for changes and AI comments"""
67
+
68
+ # Compiled regex pattern for AI comments
69
+ ai_comment_pattern = re.compile(
70
+ r"(?:#|//|--|;+) *(ai\b.*|ai\b.*|.*\bai[?!]?) *$", re.IGNORECASE
71
+ )
72
+
73
+ def __init__(self, coder, gitignores=None, verbose=False, analytics=None, root=None):
74
+ self.coder = coder
75
+ self.io = coder.io
76
+ self.root = Path(root) if root else Path(coder.root)
77
+ self.verbose = verbose
78
+ self.analytics = analytics
79
+ self.stop_event = None
80
+ self.watcher_thread = None
81
+ self.changed_files = set()
82
+ self.gitignores = gitignores
83
+
84
+ self.gitignore_spec = load_gitignores(
85
+ [Path(g) for g in self.gitignores] if self.gitignores else []
86
+ )
87
+
88
+ coder.io.file_watcher = self
89
+
90
+ def filter_func(self, change_type, path):
91
+ """Filter function for the file watcher"""
92
+ path_obj = Path(path)
93
+ path_abs = path_obj.absolute()
94
+
95
+ if not path_abs.is_relative_to(self.root.absolute()):
96
+ return False
97
+
98
+ rel_path = path_abs.relative_to(self.root)
99
+ if self.verbose:
100
+ print("Changed", rel_path)
101
+
102
+ if self.gitignore_spec and self.gitignore_spec.match_file(
103
+ rel_path.as_posix() + ("/" if path_abs.is_dir() else "")
104
+ ):
105
+ return False
106
+
107
+ # Check file size before reading content
108
+ if path_abs.is_file() and path_abs.stat().st_size > 1 * 1024 * 1024: # 1MB limit
109
+ return False
110
+
111
+ if self.verbose:
112
+ print("Checking", rel_path)
113
+
114
+ # Check if file contains AI markers
115
+ try:
116
+ comments, _, _ = self.get_ai_comments(str(path_abs))
117
+ return bool(comments)
118
+ except Exception:
119
+ return
120
+
121
+ def get_roots_to_watch(self):
122
+ """Determine which root paths to watch based on gitignore rules"""
123
+ if self.gitignore_spec:
124
+ roots = [
125
+ str(path)
126
+ for path in self.root.iterdir()
127
+ if not self.gitignore_spec.match_file(
128
+ path.relative_to(self.root).as_posix() + ("/" if path.is_dir() else "")
129
+ )
130
+ ]
131
+ # Fallback to watching root if all top-level items are filtered out
132
+ return roots if roots else [str(self.root)]
133
+ return [str(self.root)]
134
+
135
+ def handle_changes(self, changes):
136
+ """Process the detected changes and update state"""
137
+ if not changes:
138
+ return False
139
+
140
+ changed_files = {str(Path(change[1])) for change in changes}
141
+ self.changed_files.update(changed_files)
142
+ self.io.interrupt_input()
143
+ return True
144
+
145
+ def watch_files(self):
146
+ """Watch for file changes and process them"""
147
+ try:
148
+ roots_to_watch = self.get_roots_to_watch()
149
+
150
+ for changes in watch(
151
+ *roots_to_watch,
152
+ watch_filter=self.filter_func,
153
+ stop_event=self.stop_event,
154
+ ignore_permission_denied=True,
155
+ ):
156
+ if self.handle_changes(changes):
157
+ return
158
+
159
+ except Exception as e:
160
+ if self.verbose:
161
+ dump(f"File watcher error: {e}")
162
+ raise e
163
+
164
+ def start(self):
165
+ """Start watching for file changes"""
166
+ self.stop_event = threading.Event()
167
+ self.changed_files = set()
168
+
169
+ self.watcher_thread = threading.Thread(target=self.watch_files, daemon=True)
170
+ self.watcher_thread.start()
171
+
172
+ def stop(self):
173
+ """Stop watching for file changes"""
174
+ if self.stop_event:
175
+ self.stop_event.set()
176
+ if self.watcher_thread:
177
+ self.watcher_thread.join()
178
+ self.watcher_thread = None
179
+ self.stop_event = None
180
+
181
+ def process_changes(self):
182
+ """Get any detected file changes"""
183
+
184
+ has_action = None
185
+ added = False
186
+ for fname in self.changed_files:
187
+ _, _, action = self.get_ai_comments(fname)
188
+ if action in ("!", "?"):
189
+ has_action = action
190
+
191
+ if fname in self.coder.abs_fnames:
192
+ continue
193
+ if self.analytics:
194
+ self.analytics.event("ai-comments file-add")
195
+ self.coder.abs_fnames.add(fname)
196
+ rel_fname = self.coder.get_rel_fname(fname)
197
+ if not added:
198
+ self.io.tool_output()
199
+ added = True
200
+ self.io.tool_output(f"Added {rel_fname} to the chat")
201
+
202
+ if not has_action:
203
+ if added:
204
+ self.io.tool_output(
205
+ "End your comment with AI! to request changes or AI? to ask questions"
206
+ )
207
+ return ""
208
+
209
+ if self.analytics:
210
+ self.analytics.event("ai-comments execute")
211
+ self.io.tool_output("Processing your request...")
212
+
213
+ if has_action == "!":
214
+ res = watch_code_prompt
215
+ elif has_action == "?":
216
+ res = watch_ask_prompt
217
+
218
+ # Refresh all AI comments from tracked files
219
+ for fname in self.coder.abs_fnames:
220
+ line_nums, comments, _action = self.get_ai_comments(fname)
221
+ if not line_nums:
222
+ continue
223
+
224
+ code = self.io.read_text(fname)
225
+ if not code:
226
+ continue
227
+
228
+ rel_fname = self.coder.get_rel_fname(fname)
229
+ res += f"\n{rel_fname}:\n"
230
+
231
+ # Convert comment line numbers to line indices (0-based)
232
+ lois = [ln - 1 for ln, _ in zip(line_nums, comments) if ln > 0]
233
+
234
+ try:
235
+ context = TreeContext(
236
+ rel_fname,
237
+ code,
238
+ color=False,
239
+ line_number=False,
240
+ child_context=False,
241
+ last_line=False,
242
+ margin=0,
243
+ mark_lois=True,
244
+ loi_pad=3,
245
+ show_top_of_file_parent_scope=False,
246
+ )
247
+ context.lines_of_interest = set()
248
+ context.add_lines_of_interest(lois)
249
+ context.add_context()
250
+ res += context.format()
251
+ except ValueError:
252
+ for ln, comment in zip(line_nums, comments):
253
+ res += f" Line {ln}: {comment}\n"
254
+
255
+ return res
256
+
257
+ def get_ai_comments(self, filepath):
258
+ """Extract AI comment line numbers, comments and action status from a file"""
259
+ line_nums = []
260
+ comments = []
261
+ has_action = None # None, "!" or "?"
262
+ content = self.io.read_text(filepath, silent=True)
263
+ if not content:
264
+ return None, None, None
265
+
266
+ for i, line in enumerate(content.splitlines(), 1):
267
+ if match := self.ai_comment_pattern.search(line):
268
+ comment = match.group(0).strip()
269
+ if comment:
270
+ line_nums.append(i)
271
+ comments.append(comment)
272
+ comment = comment.lower()
273
+ comment = comment.lstrip("/#-;") # Added semicolon for Lisp comments
274
+ comment = comment.strip()
275
+ if comment.startswith("ai!") or comment.endswith("ai!"):
276
+ has_action = "!"
277
+ elif comment.startswith("ai?") or comment.endswith("ai?"):
278
+ has_action = "?"
279
+ if not line_nums:
280
+ return None, None, None
281
+ return line_nums, comments, has_action
282
+
283
+
284
+ def main():
285
+ """Example usage of the file watcher"""
286
+ import argparse
287
+
288
+ parser = argparse.ArgumentParser(description="Watch source files for changes")
289
+ parser.add_argument("directory", help="Directory to watch")
290
+ parser.add_argument(
291
+ "--gitignore",
292
+ action="append",
293
+ help="Path to .gitignore file (can be specified multiple times)",
294
+ )
295
+ args = parser.parse_args()
296
+
297
+ directory = args.directory
298
+ print(f"Watching source files in {directory}...")
299
+
300
+ # Example ignore function that ignores files with "test" in the name
301
+ def ignore_test_files(path):
302
+ return "test" in path.name.lower()
303
+
304
+ watcher = FileWatcher(directory, gitignores=args.gitignore)
305
+ try:
306
+ watcher.start()
307
+ while True:
308
+ if changes := watcher.get_changes():
309
+ for file in sorted(changes.keys()):
310
+ print(file)
311
+ watcher.changed_files = None
312
+ except KeyboardInterrupt:
313
+ print("\nStopped watching files")
314
+ watcher.stop()
315
+
316
+
317
+ if __name__ == "__main__":
318
+ main()
aider/watch_prompts.py ADDED
@@ -0,0 +1,12 @@
1
+ watch_code_prompt = """
2
+ I've written your instructions in comments in the code and marked them with "ai"
3
+ You can see the "AI" comments shown below (marked with █).
4
+ Find them in the code files I've shared with you, and follow their instructions.
5
+
6
+ After completing those instructions, also be sure to remove all the "AI" comments from the code too.
7
+ """
8
+
9
+ watch_ask_prompt = """/ask
10
+ Find the "AI" comments below (marked with █) in the code files I've shared with you.
11
+ They contain my questions that I need you to answer and other instructions for you.
12
+ """
aider/website/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+ gem 'jekyll'
3
+ gem "just-the-docs", "0.8.2"
4
+ gem 'jekyll-redirect-from'
5
+ gem 'jekyll-sitemap'
6
+ gem "webrick"
7
+ gem 'github-pages', group: :jekyll_plugins
8
+ gem "html-proofer"