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/waiting.py ADDED
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ Thread-based, killable spinner utility.
5
+
6
+ Use it like:
7
+
8
+ from aider.waiting import WaitingSpinner
9
+
10
+ spinner = WaitingSpinner("Waiting for LLM")
11
+ spinner.start()
12
+ ... # long task
13
+ spinner.stop()
14
+ """
15
+
16
+ import sys
17
+ import threading
18
+ import time
19
+
20
+ from rich.console import Console
21
+
22
+
23
+ class Spinner:
24
+ """
25
+ Minimal spinner that scans a single marker back and forth across a line.
26
+
27
+ The animation is pre-rendered into a list of frames. If the terminal
28
+ cannot display unicode the frames are converted to plain ASCII.
29
+ """
30
+
31
+ last_frame_idx = 0 # Class variable to store the last frame index
32
+
33
+ def __init__(self, text: str, width: int = 7):
34
+ self.text = text
35
+ self.start_time = time.time()
36
+ self.last_update = 0.0
37
+ self.visible = False
38
+ self.is_tty = sys.stdout.isatty()
39
+ self.console = Console()
40
+
41
+ # Pre-render the animation frames using pure ASCII so they will
42
+ # always display, even on very limited terminals.
43
+ ascii_frames = [
44
+ "#= ", # C1 C2 space(8)
45
+ "=# ", # C2 C1 space(8)
46
+ " =# ", # space(1) C2 C1 space(7)
47
+ " =# ", # space(2) C2 C1 space(6)
48
+ " =# ", # space(3) C2 C1 space(5)
49
+ " =# ", # space(4) C2 C1 space(4)
50
+ " =# ", # space(5) C2 C1 space(3)
51
+ " =# ", # space(6) C2 C1 space(2)
52
+ " =# ", # space(7) C2 C1 space(1)
53
+ " =#", # space(8) C2 C1
54
+ " #=", # space(8) C1 C2
55
+ " #= ", # space(7) C1 C2 space(1)
56
+ " #= ", # space(6) C1 C2 space(2)
57
+ " #= ", # space(5) C1 C2 space(3)
58
+ " #= ", # space(4) C1 C2 space(4)
59
+ " #= ", # space(3) C1 C2 space(5)
60
+ " #= ", # space(2) C1 C2 space(6)
61
+ " #= ", # space(1) C1 C2 space(7)
62
+ ]
63
+
64
+ self.unicode_palette = "░█"
65
+ xlate_from, xlate_to = ("=#", self.unicode_palette)
66
+
67
+ # If unicode is supported, swap the ASCII chars for nicer glyphs.
68
+ if self._supports_unicode():
69
+ translation_table = str.maketrans(xlate_from, xlate_to)
70
+ frames = [f.translate(translation_table) for f in ascii_frames]
71
+ self.scan_char = xlate_to[xlate_from.find("#")]
72
+ else:
73
+ frames = ascii_frames
74
+ self.scan_char = "#"
75
+
76
+ # Bounce the scanner back and forth.
77
+ self.frames = frames
78
+ self.frame_idx = Spinner.last_frame_idx # Initialize from class variable
79
+ self.width = len(frames[0]) - 2 # number of chars between the brackets
80
+ self.animation_len = len(frames[0])
81
+ self.last_display_len = 0 # Length of the last spinner line (frame + text)
82
+
83
+ def _supports_unicode(self) -> bool:
84
+ if not self.is_tty:
85
+ return False
86
+ try:
87
+ out = self.unicode_palette
88
+ out += "\b" * len(self.unicode_palette)
89
+ out += " " * len(self.unicode_palette)
90
+ out += "\b" * len(self.unicode_palette)
91
+ sys.stdout.write(out)
92
+ sys.stdout.flush()
93
+ return True
94
+ except UnicodeEncodeError:
95
+ return False
96
+ except Exception:
97
+ return False
98
+
99
+ def _next_frame(self) -> str:
100
+ frame = self.frames[self.frame_idx]
101
+ self.frame_idx = (self.frame_idx + 1) % len(self.frames)
102
+ Spinner.last_frame_idx = self.frame_idx # Update class variable
103
+ return frame
104
+
105
+ def step(self, text: str = None) -> None:
106
+ if text is not None:
107
+ self.text = text
108
+
109
+ if not self.is_tty:
110
+ return
111
+
112
+ now = time.time()
113
+ if not self.visible and now - self.start_time >= 0.5:
114
+ self.visible = True
115
+ self.last_update = 0.0
116
+ if self.is_tty:
117
+ self.console.show_cursor(False)
118
+
119
+ if not self.visible or now - self.last_update < 0.1:
120
+ return
121
+
122
+ self.last_update = now
123
+ frame_str = self._next_frame()
124
+
125
+ # Determine the maximum width for the spinner line
126
+ # Subtract 2 as requested, to leave a margin or prevent cursor wrapping issues
127
+ max_spinner_width = self.console.width - 2
128
+ if max_spinner_width < 0: # Handle extremely narrow terminals
129
+ max_spinner_width = 0
130
+
131
+ current_text_payload = f" {self.text}"
132
+ line_to_display = f"{frame_str}{current_text_payload}"
133
+
134
+ # Truncate the line if it's too long for the console width
135
+ if len(line_to_display) > max_spinner_width:
136
+ line_to_display = line_to_display[:max_spinner_width]
137
+
138
+ len_line_to_display = len(line_to_display)
139
+
140
+ # Calculate padding to clear any remnants from a longer previous line
141
+ padding_to_clear = " " * max(0, self.last_display_len - len_line_to_display)
142
+
143
+ # Write the spinner frame, text, and any necessary clearing spaces
144
+ sys.stdout.write(f"\r{line_to_display}{padding_to_clear}")
145
+ self.last_display_len = len_line_to_display
146
+
147
+ # Calculate number of backspaces to position cursor at the scanner character
148
+ scan_char_abs_pos = frame_str.find(self.scan_char)
149
+
150
+ # Total characters written to the line (frame + text + padding)
151
+ total_chars_written_on_line = len_line_to_display + len(padding_to_clear)
152
+
153
+ # num_backspaces will be non-positive if scan_char_abs_pos is beyond
154
+ # total_chars_written_on_line (e.g., if the scan char itself was truncated).
155
+ # (e.g., if the scan char itself was truncated).
156
+ # In such cases, (effectively) 0 backspaces are written,
157
+ # and the cursor stays at the end of the line.
158
+ num_backspaces = total_chars_written_on_line - scan_char_abs_pos
159
+ sys.stdout.write("\b" * num_backspaces)
160
+ sys.stdout.flush()
161
+
162
+ def end(self) -> None:
163
+ if self.visible and self.is_tty:
164
+ clear_len = self.last_display_len # Use the length of the last displayed content
165
+ sys.stdout.write("\r" + " " * clear_len + "\r")
166
+ sys.stdout.flush()
167
+ self.console.show_cursor(True)
168
+ self.visible = False
169
+
170
+
171
+ class WaitingSpinner:
172
+ """Background spinner that can be started/stopped safely."""
173
+
174
+ def __init__(self, text: str = "Waiting for LLM", delay: float = 0.15):
175
+ self.spinner = Spinner(text)
176
+ self.delay = delay
177
+ self._stop_event = threading.Event()
178
+ self._thread = threading.Thread(target=self._spin, daemon=True)
179
+
180
+ def _spin(self):
181
+ while not self._stop_event.is_set():
182
+ self.spinner.step()
183
+ time.sleep(self.delay)
184
+ self.spinner.end()
185
+
186
+ def start(self):
187
+ """Start the spinner in a background thread."""
188
+ if not self._thread.is_alive():
189
+ self._thread.start()
190
+
191
+ def stop(self):
192
+ """Request the spinner to stop and wait briefly for the thread to exit."""
193
+ self._stop_event.set()
194
+ if self._thread.is_alive():
195
+ self._thread.join(timeout=self.delay)
196
+ self.spinner.end()
197
+
198
+ # Allow use as a context-manager
199
+ def __enter__(self):
200
+ self.start()
201
+ return self
202
+
203
+ def __exit__(self, exc_type, exc_val, exc_tb):
204
+ self.stop()
205
+
206
+
207
+ def main():
208
+ spinner = Spinner("Running spinner...")
209
+ try:
210
+ for _ in range(100):
211
+ time.sleep(0.15)
212
+ spinner.step()
213
+ print("Success!")
214
+ except KeyboardInterrupt:
215
+ print("\nInterrupted by user.")
216
+ finally:
217
+ spinner.end()
218
+
219
+
220
+ if __name__ == "__main__":
221
+ main()
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"