superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,22 @@
1
+ """
2
+ SuperQode Agent - Minimal, Transparent Agent Loop.
3
+
4
+ Design Philosophy:
5
+ - MINIMAL HARNESS: No heavy system prompts, no opinionated formatting
6
+ - TRANSPARENT: What you see is what the model gets
7
+ - FAIR TESTING: Compare models on equal footing
8
+ - SIMPLE LOOP: prompt → model → tools → repeat
9
+
10
+ This is NOT trying to be the best coding agent.
11
+ This IS trying to be the fairest way to test model coding capabilities.
12
+ """
13
+
14
+ from .loop import AgentLoop, AgentConfig
15
+ from .system_prompts import SystemPromptLevel, get_system_prompt
16
+
17
+ __all__ = [
18
+ "AgentLoop",
19
+ "AgentConfig",
20
+ "SystemPromptLevel",
21
+ "get_system_prompt",
22
+ ]
@@ -0,0 +1,334 @@
1
+ """
2
+ Edit Strategies - Fallback matching for edit operations.
3
+
4
+ When exact string match fails, these strategies are tried in order to find
5
+ a suitable match (e.g., whitespace differences, indentation, line trimming).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Generator, Tuple
11
+ import re
12
+
13
+ # Similarity thresholds for block anchor fallback matching
14
+ SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0
15
+ MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3
16
+
17
+
18
+ def _levenshtein(a: str, b: str) -> int:
19
+ """Levenshtein distance between two strings."""
20
+ if not a or not b:
21
+ return max(len(a), len(b))
22
+ # Build matrix
23
+ matrix = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
24
+ for i in range(len(a) + 1):
25
+ matrix[i][0] = i
26
+ for j in range(len(b) + 1):
27
+ matrix[0][j] = j
28
+ for i in range(1, len(a) + 1):
29
+ for j in range(1, len(b) + 1):
30
+ cost = 0 if a[i - 1] == b[j - 1] else 1
31
+ matrix[i][j] = min(
32
+ matrix[i - 1][j] + 1,
33
+ matrix[i][j - 1] + 1,
34
+ matrix[i - 1][j - 1] + cost,
35
+ )
36
+ return matrix[len(a)][len(b)]
37
+
38
+
39
+ def _simple_replacer(content: str, find: str) -> Generator[str, None, None]:
40
+ """Exact match only."""
41
+ if find in content:
42
+ yield find
43
+
44
+
45
+ def _line_trimmed_replacer(content: str, find: str) -> Generator[str, None, None]:
46
+ """Match when each line matches after trimming whitespace."""
47
+ original_lines = content.split("\n")
48
+ search_lines = find.split("\n")
49
+ if search_lines and search_lines[-1] == "":
50
+ search_lines.pop()
51
+ for i in range(len(original_lines) - len(search_lines) + 1):
52
+ matches = True
53
+ for j in range(len(search_lines)):
54
+ if original_lines[i + j].strip() != search_lines[j].strip():
55
+ matches = False
56
+ break
57
+ if matches:
58
+ match_start = sum(len(original_lines[k]) + 1 for k in range(i))
59
+ match_end = match_start
60
+ for k in range(len(search_lines)):
61
+ match_end += len(original_lines[i + k])
62
+ if k < len(search_lines) - 1:
63
+ match_end += 1
64
+ yield content[match_start:match_end]
65
+
66
+
67
+ def _block_anchor_replacer(content: str, find: str) -> Generator[str, None, None]:
68
+ """Match by first/last line anchors with Levenshtein similarity for middle lines."""
69
+ original_lines = content.split("\n")
70
+ search_lines = find.split("\n")
71
+ if len(search_lines) < 3:
72
+ return
73
+ if search_lines and search_lines[-1] == "":
74
+ search_lines.pop()
75
+ first_line_search = search_lines[0].strip()
76
+ last_line_search = search_lines[-1].strip()
77
+ search_block_size = len(search_lines)
78
+ candidates = []
79
+ for i in range(len(original_lines)):
80
+ if original_lines[i].strip() != first_line_search:
81
+ continue
82
+ for j in range(i + 2, len(original_lines)):
83
+ if original_lines[j].strip() == last_line_search:
84
+ candidates.append((i, j))
85
+ break
86
+ if not candidates:
87
+ return
88
+ if len(candidates) == 1:
89
+ start_line, end_line = candidates[0]
90
+ actual_block_size = end_line - start_line + 1
91
+ lines_to_check = min(search_block_size - 2, actual_block_size - 2)
92
+ similarity = 0.0
93
+ if lines_to_check > 0:
94
+ for j in range(1, min(search_block_size - 1, actual_block_size - 1)):
95
+ orig = original_lines[start_line + j].strip()
96
+ search = search_lines[j].strip()
97
+ max_len = max(len(orig), len(search))
98
+ if max_len == 0:
99
+ continue
100
+ dist = _levenshtein(orig, search)
101
+ similarity += (1 - dist / max_len) / lines_to_check
102
+ else:
103
+ similarity = 1.0
104
+ if similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD:
105
+ match_start = sum(len(original_lines[k]) + 1 for k in range(start_line))
106
+ match_end = match_start
107
+ for k in range(start_line, end_line + 1):
108
+ match_end += len(original_lines[k])
109
+ if k < end_line:
110
+ match_end += 1
111
+ yield content[match_start:match_end]
112
+ return
113
+ best_match = None
114
+ max_similarity = -1.0
115
+ for start_line, end_line in candidates:
116
+ actual_block_size = end_line - start_line + 1
117
+ lines_to_check = min(search_block_size - 2, actual_block_size - 2)
118
+ similarity = 0.0
119
+ if lines_to_check > 0:
120
+ for j in range(1, min(search_block_size - 1, actual_block_size - 1)):
121
+ orig = original_lines[start_line + j].strip()
122
+ search = search_lines[j].strip()
123
+ max_len = max(len(orig), len(search))
124
+ if max_len == 0:
125
+ continue
126
+ dist = _levenshtein(orig, search)
127
+ similarity += 1 - dist / max_len
128
+ similarity /= lines_to_check
129
+ else:
130
+ similarity = 1.0
131
+ if similarity > max_similarity:
132
+ max_similarity = similarity
133
+ best_match = (start_line, end_line)
134
+ if max_similarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD and best_match:
135
+ start_line, end_line = best_match
136
+ match_start = sum(len(original_lines[k]) + 1 for k in range(start_line))
137
+ match_end = match_start
138
+ for k in range(start_line, end_line + 1):
139
+ match_end += len(original_lines[k])
140
+ if k < end_line:
141
+ match_end += 1
142
+ yield content[match_start:match_end]
143
+
144
+
145
+ def _whitespace_normalized_replacer(content: str, find: str) -> Generator[str, None, None]:
146
+ """Normalize all whitespace to single spaces for matching."""
147
+
148
+ def normalize(s: str) -> str:
149
+ return re.sub(r"\s+", " ", s).strip()
150
+
151
+ normalized_find = normalize(find)
152
+ lines = content.split("\n")
153
+ for i, line in enumerate(lines):
154
+ if normalize(line) == normalized_find:
155
+ yield line
156
+ else:
157
+ if normalized_find in normalize(line):
158
+ words = find.strip().split()
159
+ if words:
160
+ pattern = re.escape(words[0])
161
+ for w in words[1:]:
162
+ pattern += r"\s+" + re.escape(w)
163
+ m = re.search(pattern, line)
164
+ if m:
165
+ yield m.group(0)
166
+ find_lines = find.split("\n")
167
+ if len(find_lines) > 1:
168
+ for i in range(len(lines) - len(find_lines) + 1):
169
+ block = "\n".join(lines[i : i + len(find_lines)])
170
+ if normalize(block) == normalized_find:
171
+ yield block
172
+
173
+
174
+ def _indentation_flexible_replacer(content: str, find: str) -> Generator[str, None, None]:
175
+ """Ignore indentation differences by removing minimum indent."""
176
+
177
+ def remove_indentation(text: str) -> str:
178
+ lines = text.split("\n")
179
+ non_empty = [line for line in lines if line.strip()]
180
+ if not non_empty:
181
+ return text
182
+ min_indent = min(
183
+ len(m.group(1)) if (m := re.match(r"^(\s*)", line)) else 0 for line in non_empty
184
+ )
185
+ return "\n".join(line if not line.strip() else line[min_indent:] for line in lines)
186
+
187
+ normalized_find = remove_indentation(find)
188
+ content_lines = content.split("\n")
189
+ find_lines = find.split("\n")
190
+ for i in range(len(content_lines) - len(find_lines) + 1):
191
+ block = "\n".join(content_lines[i : i + len(find_lines)])
192
+ if remove_indentation(block) == normalized_find:
193
+ yield block
194
+
195
+
196
+ def _escape_normalized_replacer(content: str, find: str) -> Generator[str, None, None]:
197
+ """Unescape \\n, \\t, etc. in the find string for matching."""
198
+
199
+ def unescape(s: str) -> str:
200
+ return re.sub(
201
+ r"\\(n|t|r|'|\"|`|\\|\n|\$)",
202
+ lambda m: {
203
+ "n": "\n",
204
+ "t": "\t",
205
+ "r": "\r",
206
+ "'": "'",
207
+ '"': '"',
208
+ "`": "`",
209
+ "\\": "\\",
210
+ "\n": "\n",
211
+ "$": "$",
212
+ }.get(m.group(1), m.group(0)),
213
+ s,
214
+ )
215
+
216
+ unescaped = unescape(find)
217
+ if unescaped in content:
218
+ yield unescaped
219
+ lines = content.split("\n")
220
+ find_lines = unescape(find).split("\n")
221
+ for i in range(len(lines) - len(find_lines) + 1):
222
+ block = "\n".join(lines[i : i + len(find_lines)])
223
+ if unescape(block) == unescaped:
224
+ yield block
225
+
226
+
227
+ def _trimmed_boundary_replacer(content: str, find: str) -> Generator[str, None, None]:
228
+ """Try matching with trimmed find (leading/trailing whitespace removed)."""
229
+ trimmed = find.strip()
230
+ if trimmed == find:
231
+ return
232
+ if trimmed in content:
233
+ yield trimmed
234
+ lines = content.split("\n")
235
+ find_lines = find.split("\n")
236
+ for i in range(len(lines) - len(find_lines) + 1):
237
+ block = "\n".join(lines[i : i + len(find_lines)])
238
+ if block.strip() == trimmed:
239
+ yield block
240
+
241
+
242
+ def _context_aware_replacer(content: str, find: str) -> Generator[str, None, None]:
243
+ """Match by first/last line anchors; require ~50% of middle lines to match."""
244
+ find_lines = find.split("\n")
245
+ if len(find_lines) < 3:
246
+ return
247
+ if find_lines and find_lines[-1] == "":
248
+ find_lines.pop()
249
+ first_line = find_lines[0].strip()
250
+ last_line = find_lines[-1].strip()
251
+ content_lines = content.split("\n")
252
+ for i in range(len(content_lines)):
253
+ if content_lines[i].strip() != first_line:
254
+ continue
255
+ for j in range(i + 2, len(content_lines)):
256
+ if content_lines[j].strip() == last_line:
257
+ block_lines = content_lines[i : j + 1]
258
+ block = "\n".join(block_lines)
259
+ if len(block_lines) == len(find_lines):
260
+ matching = 0
261
+ total = 0
262
+ for k in range(1, len(block_lines) - 1):
263
+ bl = block_lines[k].strip()
264
+ fl = find_lines[k].strip()
265
+ if bl or fl:
266
+ total += 1
267
+ if bl == fl:
268
+ matching += 1
269
+ if total == 0 or matching / total >= 0.5:
270
+ yield block
271
+ return
272
+ break
273
+
274
+
275
+ def _multi_occurrence_replacer(content: str, find: str) -> Generator[str, None, None]:
276
+ """Yield each exact occurrence (for replace_all handling)."""
277
+ start = 0
278
+ while True:
279
+ idx = content.find(find, start)
280
+ if idx == -1:
281
+ break
282
+ yield find
283
+ start = idx + len(find)
284
+
285
+
286
+ REPLACERS = [
287
+ _simple_replacer,
288
+ _line_trimmed_replacer,
289
+ _block_anchor_replacer,
290
+ _whitespace_normalized_replacer,
291
+ _indentation_flexible_replacer,
292
+ _escape_normalized_replacer,
293
+ _trimmed_boundary_replacer,
294
+ _context_aware_replacer,
295
+ _multi_occurrence_replacer,
296
+ ]
297
+
298
+
299
+ def replace_with_strategies(
300
+ content: str, old_string: str, new_string: str, replace_all: bool = False
301
+ ) -> Tuple[str, int]:
302
+ """
303
+ Replace old_string with new_string in content, trying multiple matching
304
+ strategies when exact match fails.
305
+
306
+ Returns:
307
+ Tuple of (new_content, replaced_count).
308
+
309
+ Raises:
310
+ ValueError: if old_string == new_string
311
+ ValueError: if old_string not found with any strategy
312
+ ValueError: if multiple matches and not replace_all
313
+ """
314
+ if old_string == new_string:
315
+ raise ValueError("old_string and new_string must be different")
316
+ for replacer in REPLACERS:
317
+ for search in replacer(content, old_string):
318
+ idx = content.find(search)
319
+ if idx == -1:
320
+ continue
321
+ if replace_all:
322
+ count = content.count(search)
323
+ new_content = content.replace(search, new_string)
324
+ return (new_content, count)
325
+ last_idx = content.rfind(search)
326
+ if idx != last_idx:
327
+ count = content.count(search)
328
+ raise ValueError(
329
+ f"Found {count} occurrences of old_string. Provide more surrounding "
330
+ "lines in old_string to identify the correct match, or use replace_all=true."
331
+ )
332
+ new_content = content[:idx] + new_string + content[idx + len(search) :]
333
+ return (new_content, 1)
334
+ raise ValueError("old_string not found in content")