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
@@ -0,0 +1,657 @@
1
+ import difflib
2
+ import math
3
+ import re
4
+ import sys
5
+ from difflib import SequenceMatcher
6
+ from pathlib import Path
7
+
8
+ from aider import utils
9
+
10
+ from ..dump import dump # noqa: F401
11
+ from .base_coder import Coder
12
+ from .editblock_prompts import EditBlockPrompts
13
+
14
+
15
+ class EditBlockCoder(Coder):
16
+ """A coder that uses search/replace blocks for code modifications."""
17
+
18
+ edit_format = "diff"
19
+ gpt_prompts = EditBlockPrompts()
20
+
21
+ def get_edits(self):
22
+ content = self.partial_response_content
23
+
24
+ # might raise ValueError for malformed ORIG/UPD blocks
25
+ edits = list(
26
+ find_original_update_blocks(
27
+ content,
28
+ self.fence,
29
+ self.get_inchat_relative_files(),
30
+ )
31
+ )
32
+
33
+ self.shell_commands += [edit[1] for edit in edits if edit[0] is None]
34
+ edits = [edit for edit in edits if edit[0] is not None]
35
+
36
+ return edits
37
+
38
+ def apply_edits_dry_run(self, edits):
39
+ return self.apply_edits(edits, dry_run=True)
40
+
41
+ def apply_edits(self, edits, dry_run=False):
42
+ failed = []
43
+ passed = []
44
+ updated_edits = []
45
+
46
+ for edit in edits:
47
+ path, original, updated = edit
48
+ full_path = self.abs_root_path(path)
49
+ new_content = None
50
+
51
+ if Path(full_path).exists():
52
+ content = self.io.read_text(full_path)
53
+ new_content = do_replace(full_path, content, original, updated, self.fence)
54
+
55
+ # If the edit failed, and
56
+ # this is not a "create a new file" with an empty original...
57
+ # https://github.com/Aider-AI/aider/issues/2258
58
+ if not new_content and original.strip():
59
+ # try patching any of the other files in the chat
60
+ for full_path in self.abs_fnames:
61
+ content = self.io.read_text(full_path)
62
+ new_content = do_replace(full_path, content, original, updated, self.fence)
63
+ if new_content:
64
+ path = self.get_rel_fname(full_path)
65
+ break
66
+
67
+ updated_edits.append((path, original, updated))
68
+
69
+ if new_content:
70
+ if not dry_run:
71
+ self.io.write_text(full_path, new_content)
72
+ passed.append(edit)
73
+ else:
74
+ failed.append(edit)
75
+
76
+ if dry_run:
77
+ return updated_edits
78
+
79
+ if not failed:
80
+ return
81
+
82
+ blocks = "block" if len(failed) == 1 else "blocks"
83
+
84
+ res = f"# {len(failed)} SEARCH/REPLACE {blocks} failed to match!\n"
85
+ for edit in failed:
86
+ path, original, updated = edit
87
+
88
+ full_path = self.abs_root_path(path)
89
+ content = self.io.read_text(full_path)
90
+
91
+ res += f"""
92
+ ## SearchReplaceNoExactMatch: This SEARCH block failed to exactly match lines in {path}
93
+ <<<<<<< SEARCH
94
+ {original}=======
95
+ {updated}>>>>>>> REPLACE
96
+
97
+ """
98
+ did_you_mean = find_similar_lines(original, content)
99
+ if did_you_mean:
100
+ res += f"""Did you mean to match some of these actual lines from {path}?
101
+
102
+ {self.fence[0]}
103
+ {did_you_mean}
104
+ {self.fence[1]}
105
+
106
+ """
107
+
108
+ if updated in content and updated:
109
+ res += f"""Are you sure you need this SEARCH/REPLACE block?
110
+ The REPLACE lines are already in {path}!
111
+
112
+ """
113
+ res += (
114
+ "The SEARCH section must exactly match an existing block of lines including all white"
115
+ " space, comments, indentation, docstrings, etc\n"
116
+ )
117
+ if passed:
118
+ pblocks = "block" if len(passed) == 1 else "blocks"
119
+ res += f"""
120
+ # The other {len(passed)} SEARCH/REPLACE {pblocks} were applied successfully.
121
+ Don't re-send them.
122
+ Just reply with fixed versions of the {blocks} above that failed to match.
123
+ """
124
+ raise ValueError(res)
125
+
126
+
127
+ def prep(content):
128
+ if content and not content.endswith("\n"):
129
+ content += "\n"
130
+ lines = content.splitlines(keepends=True)
131
+ return content, lines
132
+
133
+
134
+ def perfect_or_whitespace(whole_lines, part_lines, replace_lines):
135
+ # Try for a perfect match
136
+ res = perfect_replace(whole_lines, part_lines, replace_lines)
137
+ if res:
138
+ return res
139
+
140
+ # Try being flexible about leading whitespace
141
+ res = replace_part_with_missing_leading_whitespace(whole_lines, part_lines, replace_lines)
142
+ if res:
143
+ return res
144
+
145
+
146
+ def perfect_replace(whole_lines, part_lines, replace_lines):
147
+ part_tup = tuple(part_lines)
148
+ part_len = len(part_lines)
149
+
150
+ for i in range(len(whole_lines) - part_len + 1):
151
+ whole_tup = tuple(whole_lines[i : i + part_len])
152
+ if part_tup == whole_tup:
153
+ res = whole_lines[:i] + replace_lines + whole_lines[i + part_len :]
154
+ return "".join(res)
155
+
156
+
157
+ def replace_most_similar_chunk(whole, part, replace):
158
+ """Best efforts to find the `part` lines in `whole` and replace them with `replace`"""
159
+
160
+ whole, whole_lines = prep(whole)
161
+ part, part_lines = prep(part)
162
+ replace, replace_lines = prep(replace)
163
+
164
+ res = perfect_or_whitespace(whole_lines, part_lines, replace_lines)
165
+ if res:
166
+ return res
167
+
168
+ # drop leading empty line, GPT sometimes adds them spuriously (issue #25)
169
+ if len(part_lines) > 2 and not part_lines[0].strip():
170
+ skip_blank_line_part_lines = part_lines[1:]
171
+ res = perfect_or_whitespace(whole_lines, skip_blank_line_part_lines, replace_lines)
172
+ if res:
173
+ return res
174
+
175
+ # Try to handle when it elides code with ...
176
+ try:
177
+ res = try_dotdotdots(whole, part, replace)
178
+ if res:
179
+ return res
180
+ except ValueError:
181
+ pass
182
+
183
+ return
184
+ # Try fuzzy matching
185
+ res = replace_closest_edit_distance(whole_lines, part, part_lines, replace_lines)
186
+ if res:
187
+ return res
188
+
189
+
190
+ def try_dotdotdots(whole, part, replace):
191
+ """
192
+ See if the edit block has ... lines.
193
+ If not, return none.
194
+
195
+ If yes, try and do a perfect edit with the ... chunks.
196
+ If there's a mismatch or otherwise imperfect edit, raise ValueError.
197
+
198
+ If perfect edit succeeds, return the updated whole.
199
+ """
200
+
201
+ dots_re = re.compile(r"(^\s*\.\.\.\n)", re.MULTILINE | re.DOTALL)
202
+
203
+ part_pieces = re.split(dots_re, part)
204
+ replace_pieces = re.split(dots_re, replace)
205
+
206
+ if len(part_pieces) != len(replace_pieces):
207
+ raise ValueError("Unpaired ... in SEARCH/REPLACE block")
208
+
209
+ if len(part_pieces) == 1:
210
+ # no dots in this edit block, just return None
211
+ return
212
+
213
+ # Compare odd strings in part_pieces and replace_pieces
214
+ all_dots_match = all(part_pieces[i] == replace_pieces[i] for i in range(1, len(part_pieces), 2))
215
+
216
+ if not all_dots_match:
217
+ raise ValueError("Unmatched ... in SEARCH/REPLACE block")
218
+
219
+ part_pieces = [part_pieces[i] for i in range(0, len(part_pieces), 2)]
220
+ replace_pieces = [replace_pieces[i] for i in range(0, len(replace_pieces), 2)]
221
+
222
+ pairs = zip(part_pieces, replace_pieces)
223
+ for part, replace in pairs:
224
+ if not part and not replace:
225
+ continue
226
+
227
+ if not part and replace:
228
+ if not whole.endswith("\n"):
229
+ whole += "\n"
230
+ whole += replace
231
+ continue
232
+
233
+ if whole.count(part) == 0:
234
+ raise ValueError
235
+ if whole.count(part) > 1:
236
+ raise ValueError
237
+
238
+ whole = whole.replace(part, replace, 1)
239
+
240
+ return whole
241
+
242
+
243
+ def replace_part_with_missing_leading_whitespace(whole_lines, part_lines, replace_lines):
244
+ # GPT often messes up leading whitespace.
245
+ # It usually does it uniformly across the ORIG and UPD blocks.
246
+ # Either omitting all leading whitespace, or including only some of it.
247
+
248
+ # Outdent everything in part_lines and replace_lines by the max fixed amount possible
249
+ leading = [len(p) - len(p.lstrip()) for p in part_lines if p.strip()] + [
250
+ len(p) - len(p.lstrip()) for p in replace_lines if p.strip()
251
+ ]
252
+
253
+ if leading and min(leading):
254
+ num_leading = min(leading)
255
+ part_lines = [p[num_leading:] if p.strip() else p for p in part_lines]
256
+ replace_lines = [p[num_leading:] if p.strip() else p for p in replace_lines]
257
+
258
+ # can we find an exact match not including the leading whitespace
259
+ num_part_lines = len(part_lines)
260
+
261
+ for i in range(len(whole_lines) - num_part_lines + 1):
262
+ add_leading = match_but_for_leading_whitespace(
263
+ whole_lines[i : i + num_part_lines], part_lines
264
+ )
265
+
266
+ if add_leading is None:
267
+ continue
268
+
269
+ replace_lines = [add_leading + rline if rline.strip() else rline for rline in replace_lines]
270
+ whole_lines = whole_lines[:i] + replace_lines + whole_lines[i + num_part_lines :]
271
+ return "".join(whole_lines)
272
+
273
+ return None
274
+
275
+
276
+ def match_but_for_leading_whitespace(whole_lines, part_lines):
277
+ num = len(whole_lines)
278
+
279
+ # does the non-whitespace all agree?
280
+ if not all(whole_lines[i].lstrip() == part_lines[i].lstrip() for i in range(num)):
281
+ return
282
+
283
+ # are they all offset the same?
284
+ add = set(
285
+ whole_lines[i][: len(whole_lines[i]) - len(part_lines[i])]
286
+ for i in range(num)
287
+ if whole_lines[i].strip()
288
+ )
289
+
290
+ if len(add) != 1:
291
+ return
292
+
293
+ return add.pop()
294
+
295
+
296
+ def replace_closest_edit_distance(whole_lines, part, part_lines, replace_lines):
297
+ similarity_thresh = 0.8
298
+
299
+ max_similarity = 0
300
+ most_similar_chunk_start = -1
301
+ most_similar_chunk_end = -1
302
+
303
+ scale = 0.1
304
+ min_len = math.floor(len(part_lines) * (1 - scale))
305
+ max_len = math.ceil(len(part_lines) * (1 + scale))
306
+
307
+ for length in range(min_len, max_len):
308
+ for i in range(len(whole_lines) - length + 1):
309
+ chunk = whole_lines[i : i + length]
310
+ chunk = "".join(chunk)
311
+
312
+ similarity = SequenceMatcher(None, chunk, part).ratio()
313
+
314
+ if similarity > max_similarity and similarity:
315
+ max_similarity = similarity
316
+ most_similar_chunk_start = i
317
+ most_similar_chunk_end = i + length
318
+
319
+ if max_similarity < similarity_thresh:
320
+ return
321
+
322
+ modified_whole = (
323
+ whole_lines[:most_similar_chunk_start]
324
+ + replace_lines
325
+ + whole_lines[most_similar_chunk_end:]
326
+ )
327
+ modified_whole = "".join(modified_whole)
328
+
329
+ return modified_whole
330
+
331
+
332
+ DEFAULT_FENCE = ("`" * 3, "`" * 3)
333
+
334
+
335
+ def strip_quoted_wrapping(res, fname=None, fence=DEFAULT_FENCE):
336
+ """
337
+ Given an input string which may have extra "wrapping" around it, remove the wrapping.
338
+ For example:
339
+
340
+ filename.ext
341
+ ```
342
+ We just want this content
343
+ Not the filename and triple quotes
344
+ ```
345
+ """
346
+ if not res:
347
+ return res
348
+
349
+ res = res.splitlines()
350
+
351
+ if fname and res[0].strip().endswith(Path(fname).name):
352
+ res = res[1:]
353
+
354
+ if res[0].startswith(fence[0]) and res[-1].startswith(fence[1]):
355
+ res = res[1:-1]
356
+
357
+ res = "\n".join(res)
358
+ if res and res[-1] != "\n":
359
+ res += "\n"
360
+
361
+ return res
362
+
363
+
364
+ def do_replace(fname, content, before_text, after_text, fence=None):
365
+ before_text = strip_quoted_wrapping(before_text, fname, fence)
366
+ after_text = strip_quoted_wrapping(after_text, fname, fence)
367
+ fname = Path(fname)
368
+
369
+ # does it want to make a new file?
370
+ if not fname.exists() and not before_text.strip():
371
+ fname.touch()
372
+ content = ""
373
+
374
+ if content is None:
375
+ return
376
+
377
+ if not before_text.strip():
378
+ # append to existing file, or start a new file
379
+ new_content = content + after_text
380
+ else:
381
+ new_content = replace_most_similar_chunk(content, before_text, after_text)
382
+
383
+ return new_content
384
+
385
+
386
+ HEAD = r"^<{5,9} SEARCH\s*$"
387
+ DIVIDER = r"^={5,9}\s*$"
388
+ UPDATED = r"^>{5,9} REPLACE\s*$"
389
+
390
+ HEAD_ERR = "<<<<<<< SEARCH"
391
+ DIVIDER_ERR = "======="
392
+ UPDATED_ERR = ">>>>>>> REPLACE"
393
+
394
+ separators = "|".join([HEAD, DIVIDER, UPDATED])
395
+
396
+ split_re = re.compile(r"^((?:" + separators + r")[ ]*\n)", re.MULTILINE | re.DOTALL)
397
+
398
+
399
+ missing_filename_err = (
400
+ "Bad/missing filename. The filename must be alone on the line before the opening fence"
401
+ " {fence[0]}"
402
+ )
403
+
404
+ # Always be willing to treat triple-backticks as a fence when searching for filenames
405
+ triple_backticks = "`" * 3
406
+
407
+
408
+ def strip_filename(filename, fence):
409
+ filename = filename.strip()
410
+
411
+ if filename == "...":
412
+ return
413
+
414
+ start_fence = fence[0]
415
+ if filename.startswith(start_fence):
416
+ candidate = filename[len(start_fence) :]
417
+ if candidate and ("." in candidate or "/" in candidate):
418
+ return candidate
419
+ return
420
+
421
+ if filename.startswith(triple_backticks):
422
+ candidate = filename[len(triple_backticks) :]
423
+ if candidate and ("." in candidate or "/" in candidate):
424
+ return candidate
425
+ return
426
+
427
+ filename = filename.rstrip(":")
428
+ filename = filename.lstrip("#")
429
+ filename = filename.strip()
430
+ filename = filename.strip("`")
431
+ filename = filename.strip("*")
432
+
433
+ # https://github.com/Aider-AI/aider/issues/1158
434
+ # filename = filename.replace("\\_", "_")
435
+
436
+ return filename
437
+
438
+
439
+ def find_original_update_blocks(content, fence=DEFAULT_FENCE, valid_fnames=None):
440
+ lines = content.splitlines(keepends=True)
441
+ i = 0
442
+ current_filename = None
443
+
444
+ head_pattern = re.compile(HEAD)
445
+ divider_pattern = re.compile(DIVIDER)
446
+ updated_pattern = re.compile(UPDATED)
447
+
448
+ while i < len(lines):
449
+ line = lines[i]
450
+
451
+ # Check for shell code blocks
452
+ shell_starts = [
453
+ "```bash",
454
+ "```sh",
455
+ "```shell",
456
+ "```cmd",
457
+ "```batch",
458
+ "```powershell",
459
+ "```ps1",
460
+ "```zsh",
461
+ "```fish",
462
+ "```ksh",
463
+ "```csh",
464
+ "```tcsh",
465
+ ]
466
+
467
+ # Check if the next line or the one after that is an editblock
468
+ next_is_editblock = (
469
+ i + 1 < len(lines)
470
+ and head_pattern.match(lines[i + 1].strip())
471
+ or i + 2 < len(lines)
472
+ and head_pattern.match(lines[i + 2].strip())
473
+ )
474
+
475
+ if any(line.strip().startswith(start) for start in shell_starts) and not next_is_editblock:
476
+ shell_content = []
477
+ i += 1
478
+ while i < len(lines) and not lines[i].strip().startswith("```"):
479
+ shell_content.append(lines[i])
480
+ i += 1
481
+ if i < len(lines) and lines[i].strip().startswith("```"):
482
+ i += 1 # Skip the closing ```
483
+
484
+ yield None, "".join(shell_content)
485
+ continue
486
+
487
+ # Check for SEARCH/REPLACE blocks
488
+ if head_pattern.match(line.strip()):
489
+ try:
490
+ # if next line after HEAD exists and is DIVIDER, it's a new file
491
+ if i + 1 < len(lines) and divider_pattern.match(lines[i + 1].strip()):
492
+ filename = find_filename(lines[max(0, i - 3) : i], fence, None)
493
+ else:
494
+ filename = find_filename(lines[max(0, i - 3) : i], fence, valid_fnames)
495
+
496
+ if not filename:
497
+ if current_filename:
498
+ filename = current_filename
499
+ else:
500
+ raise ValueError(missing_filename_err.format(fence=fence))
501
+
502
+ current_filename = filename
503
+
504
+ original_text = []
505
+ i += 1
506
+ while i < len(lines) and not divider_pattern.match(lines[i].strip()):
507
+ original_text.append(lines[i])
508
+ i += 1
509
+
510
+ if i >= len(lines) or not divider_pattern.match(lines[i].strip()):
511
+ raise ValueError(f"Expected `{DIVIDER_ERR}`")
512
+
513
+ updated_text = []
514
+ i += 1
515
+ while i < len(lines) and not (
516
+ updated_pattern.match(lines[i].strip())
517
+ or divider_pattern.match(lines[i].strip())
518
+ ):
519
+ updated_text.append(lines[i])
520
+ i += 1
521
+
522
+ if i >= len(lines) or not (
523
+ updated_pattern.match(lines[i].strip())
524
+ or divider_pattern.match(lines[i].strip())
525
+ ):
526
+ raise ValueError(f"Expected `{UPDATED_ERR}` or `{DIVIDER_ERR}`")
527
+
528
+ yield filename, "".join(original_text), "".join(updated_text)
529
+
530
+ except ValueError as e:
531
+ processed = "".join(lines[: i + 1])
532
+ err = e.args[0]
533
+ raise ValueError(f"{processed}\n^^^ {err}")
534
+
535
+ i += 1
536
+
537
+
538
+ def find_filename(lines, fence, valid_fnames):
539
+ """
540
+ Deepseek Coder v2 has been doing this:
541
+
542
+
543
+ ```python
544
+ word_count.py
545
+ ```
546
+ ```python
547
+ <<<<<<< SEARCH
548
+ ...
549
+
550
+ This is a more flexible search back for filenames.
551
+ """
552
+
553
+ if valid_fnames is None:
554
+ valid_fnames = []
555
+
556
+ # Go back through the 3 preceding lines
557
+ lines.reverse()
558
+ lines = lines[:3]
559
+
560
+ filenames = []
561
+ for line in lines:
562
+ # If we find a filename, done
563
+ filename = strip_filename(line, fence)
564
+ if filename:
565
+ filenames.append(filename)
566
+
567
+ # Only continue as long as we keep seeing fences
568
+ if not line.startswith(fence[0]) and not line.startswith(triple_backticks):
569
+ break
570
+
571
+ if not filenames:
572
+ return
573
+
574
+ # pick the *best* filename found
575
+
576
+ # Check for exact match first
577
+ for fname in filenames:
578
+ if fname in valid_fnames:
579
+ return fname
580
+
581
+ # Check for partial match (basename match)
582
+ for fname in filenames:
583
+ for vfn in valid_fnames:
584
+ if fname == Path(vfn).name:
585
+ return vfn
586
+
587
+ # Perform fuzzy matching with valid_fnames
588
+ for fname in filenames:
589
+ close_matches = difflib.get_close_matches(fname, valid_fnames, n=1, cutoff=0.8)
590
+ if len(close_matches) == 1:
591
+ return close_matches[0]
592
+
593
+ # If no fuzzy match, look for a file w/extension
594
+ for fname in filenames:
595
+ if "." in fname:
596
+ return fname
597
+
598
+ if filenames:
599
+ return filenames[0]
600
+
601
+
602
+ def find_similar_lines(search_lines, content_lines, threshold=0.6):
603
+ search_lines = search_lines.splitlines()
604
+ content_lines = content_lines.splitlines()
605
+
606
+ best_ratio = 0
607
+ best_match = None
608
+
609
+ for i in range(len(content_lines) - len(search_lines) + 1):
610
+ chunk = content_lines[i : i + len(search_lines)]
611
+ ratio = SequenceMatcher(None, search_lines, chunk).ratio()
612
+ if ratio > best_ratio:
613
+ best_ratio = ratio
614
+ best_match = chunk
615
+ best_match_i = i
616
+
617
+ if best_ratio < threshold:
618
+ return ""
619
+
620
+ if best_match[0] == search_lines[0] and best_match[-1] == search_lines[-1]:
621
+ return "\n".join(best_match)
622
+
623
+ N = 5
624
+ best_match_end = min(len(content_lines), best_match_i + len(search_lines) + N)
625
+ best_match_i = max(0, best_match_i - N)
626
+
627
+ best = content_lines[best_match_i:best_match_end]
628
+ return "\n".join(best)
629
+
630
+
631
+ def main():
632
+ history_md = Path(sys.argv[1]).read_text()
633
+ if not history_md:
634
+ return
635
+
636
+ messages = utils.split_chat_history_markdown(history_md)
637
+
638
+ for msg in messages:
639
+ msg = msg["content"]
640
+ edits = list(find_original_update_blocks(msg))
641
+
642
+ for fname, before, after in edits:
643
+ # Compute diff
644
+ diff = difflib.unified_diff(
645
+ before.splitlines(keepends=True),
646
+ after.splitlines(keepends=True),
647
+ fromfile="before",
648
+ tofile="after",
649
+ )
650
+ diff = "".join(diff)
651
+ dump(before)
652
+ dump(after)
653
+ dump(diff)
654
+
655
+
656
+ if __name__ == "__main__":
657
+ main()
@@ -0,0 +1,10 @@
1
+ from ..dump import dump # noqa: F401
2
+ from .editblock_coder import EditBlockCoder
3
+ from .editblock_fenced_prompts import EditBlockFencedPrompts
4
+
5
+
6
+ class EditBlockFencedCoder(EditBlockCoder):
7
+ """A coder that uses fenced search/replace blocks for code modifications."""
8
+
9
+ edit_format = "diff-fenced"
10
+ gpt_prompts = EditBlockFencedPrompts()