headlesscode 1.0.2

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 (232) hide show
  1. package/ATTRIBUTION.md +53 -0
  2. package/CODE_OF_CONDUCT.md +130 -0
  3. package/CONTRIBUTING.md +107 -0
  4. package/LICENSE +202 -0
  5. package/README.md +486 -0
  6. package/SECURITY.md +211 -0
  7. package/bin/headlesscode.mjs +83 -0
  8. package/package.json +63 -0
  9. package/shared/prompts/review-mode-prompt-short.md +93 -0
  10. package/shared/prompts/review-mode-prompt.md +281 -0
  11. package/shared/rules-code/rules.md +22 -0
  12. package/shared/stacks/cpp/rules.md +30 -0
  13. package/shared/stacks/fastapi/rules.md +30 -0
  14. package/shared/stacks/javascript/rules.md +37 -0
  15. package/shared/stacks/postgresql/rules.md +31 -0
  16. package/shared/stacks/python/rules.md +35 -0
  17. package/shared/stacks/react/rules.md +11 -0
  18. package/shared/stacks/typescript/rules.md +10 -0
  19. package/src/budget/budget.ts +221 -0
  20. package/src/budget/concurrency.ts +126 -0
  21. package/src/budget/cost.ts +309 -0
  22. package/src/budget/index.ts +8 -0
  23. package/src/checkpoints/cli.ts +256 -0
  24. package/src/checkpoints/service.ts +227 -0
  25. package/src/cli.ts +1535 -0
  26. package/src/cloud/docker-provider.ts +334 -0
  27. package/src/cloud/provider.ts +300 -0
  28. package/src/codeintel/call-graph.ts +78 -0
  29. package/src/codeintel/find-references.ts +123 -0
  30. package/src/codeintel/go-to-definition.ts +193 -0
  31. package/src/codeintel/handlers.ts +190 -0
  32. package/src/codeintel/import-graph.ts +173 -0
  33. package/src/codeintel/outline.ts +180 -0
  34. package/src/codeintel/position.ts +77 -0
  35. package/src/codeintel/program.ts +350 -0
  36. package/src/codeintel/rename-symbol.ts +213 -0
  37. package/src/codeintel/tools.ts +280 -0
  38. package/src/codemap/build.ts +135 -0
  39. package/src/codemap/cli.ts +190 -0
  40. package/src/codemap/extract.ts +339 -0
  41. package/src/codemap/files.ts +236 -0
  42. package/src/codemap/fingerprint.ts +65 -0
  43. package/src/codemap/flows.ts +62 -0
  44. package/src/codemap/html.ts +451 -0
  45. package/src/codemap/lock.ts +80 -0
  46. package/src/codemap/types.ts +101 -0
  47. package/src/codesearch/airunner-embedder.ts +185 -0
  48. package/src/codesearch/chunk.ts +339 -0
  49. package/src/codesearch/cli.ts +223 -0
  50. package/src/codesearch/embedder.ts +332 -0
  51. package/src/codesearch/files.ts +280 -0
  52. package/src/codesearch/index.ts +469 -0
  53. package/src/codesearch/ollama-embedder.ts +205 -0
  54. package/src/codesearch/search.ts +141 -0
  55. package/src/codesearch/types.ts +100 -0
  56. package/src/config/mode-models.ts +218 -0
  57. package/src/dashboard/aggregate.ts +364 -0
  58. package/src/dashboard/chat-thread.ts +141 -0
  59. package/src/dashboard/checkpoints.ts +124 -0
  60. package/src/dashboard/cli.ts +193 -0
  61. package/src/dashboard/codemap.ts +44 -0
  62. package/src/dashboard/files.ts +121 -0
  63. package/src/dashboard/page.ts +2803 -0
  64. package/src/dashboard/self-improvement-metrics.ts +282 -0
  65. package/src/dashboard/server.ts +1103 -0
  66. package/src/dashboard/session-launch.ts +310 -0
  67. package/src/dashboard/timeline.ts +273 -0
  68. package/src/dashboard/tool-exec.ts +107 -0
  69. package/src/dashboard/trend-cli.ts +141 -0
  70. package/src/dashboard/trend.ts +413 -0
  71. package/src/decision-proxy/cli.ts +261 -0
  72. package/src/decision-proxy/proxy.ts +569 -0
  73. package/src/deploy/gate-cli.ts +147 -0
  74. package/src/deploy/gate.ts +254 -0
  75. package/src/engine/condense.ts +512 -0
  76. package/src/engine/events.ts +428 -0
  77. package/src/engine/handoff.ts +71 -0
  78. package/src/engine/lazy-tools.ts +160 -0
  79. package/src/engine/local-explore.ts +653 -0
  80. package/src/engine/logger.ts +96 -0
  81. package/src/engine/loop.ts +5517 -0
  82. package/src/engine/parser.ts +347 -0
  83. package/src/engine/prompt.ts +860 -0
  84. package/src/engine/reports.ts +47 -0
  85. package/src/engine/stacks.ts +448 -0
  86. package/src/engine/types.ts +291 -0
  87. package/src/engine/usage.ts +186 -0
  88. package/src/github/app-auth.ts +161 -0
  89. package/src/github/cli.ts +448 -0
  90. package/src/github/installations.ts +133 -0
  91. package/src/github/pr.ts +321 -0
  92. package/src/github/provision.ts +118 -0
  93. package/src/github/push.ts +122 -0
  94. package/src/index-util.ts +50 -0
  95. package/src/index.ts +81 -0
  96. package/src/init/cli.ts +248 -0
  97. package/src/init/gitignore.ts +74 -0
  98. package/src/llm/ollama.ts +308 -0
  99. package/src/llm/openrouter.ts +868 -0
  100. package/src/llm/preflight.ts +367 -0
  101. package/src/llm/transcript-capture.ts +84 -0
  102. package/src/memory/embed.ts +110 -0
  103. package/src/memory/index.ts +22 -0
  104. package/src/memory/local.ts +259 -0
  105. package/src/memory/summarizer.ts +283 -0
  106. package/src/memory/types.ts +153 -0
  107. package/src/memory/uwuchat.ts +157 -0
  108. package/src/migrate/cli.ts +115 -0
  109. package/src/orchestrator/analyze-cli.ts +104 -0
  110. package/src/orchestrator/auto-split.ts +206 -0
  111. package/src/orchestrator/cleanup.ts +1003 -0
  112. package/src/orchestrator/cli.ts +3571 -0
  113. package/src/orchestrator/cost-estimate.ts +564 -0
  114. package/src/orchestrator/cost-history-cli.ts +242 -0
  115. package/src/orchestrator/cost-history.ts +397 -0
  116. package/src/orchestrator/git-sync.ts +250 -0
  117. package/src/orchestrator/index.ts +153 -0
  118. package/src/orchestrator/log-analysis.ts +0 -0
  119. package/src/orchestrator/merge-check.ts +108 -0
  120. package/src/orchestrator/pipeline.ts +411 -0
  121. package/src/orchestrator/resume.ts +1940 -0
  122. package/src/orchestrator/reviewer.ts +503 -0
  123. package/src/orchestrator/split.ts +296 -0
  124. package/src/orchestrator/state.ts +542 -0
  125. package/src/orchestrator/status.ts +697 -0
  126. package/src/orchestrator/verification-gate.ts +134 -0
  127. package/src/orchestrator/watch.ts +898 -0
  128. package/src/permissions/commands.ts +1083 -0
  129. package/src/permissions/config.ts +241 -0
  130. package/src/permissions/index.ts +12 -0
  131. package/src/permissions/protected-files.ts +96 -0
  132. package/src/permissions/store-protection.ts +272 -0
  133. package/src/project-store.ts +648 -0
  134. package/src/projects/cli.ts +382 -0
  135. package/src/qa/qa.ts +487 -0
  136. package/src/tools/browser/handler.ts +346 -0
  137. package/src/tools/browser/service.ts +406 -0
  138. package/src/tools/browser/smoke.ts +78 -0
  139. package/src/tools/browser/tool.ts +99 -0
  140. package/src/tools/executor.ts +2575 -0
  141. package/src/tools/language-detect.ts +183 -0
  142. package/src/tools/output-summarizer.ts +369 -0
  143. package/src/tools/run-tests.ts +302 -0
  144. package/src/tools/set-indentation-tool.ts +49 -0
  145. package/src/tools/test-selection.ts +160 -0
  146. package/src/vendor/tests/smoke.ts +103 -0
  147. package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
  148. package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
  149. package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
  150. package/src/vendor/zoo-code/shim/os-name.ts +18 -0
  151. package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
  152. package/src/vendor/zoo-code/shim/vscode.ts +76 -0
  153. package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
  154. package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
  155. package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
  156. package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
  157. package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
  158. package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
  159. package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
  160. package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
  161. package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
  162. package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
  163. package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
  164. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
  165. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
  166. package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
  167. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
  168. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
  169. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
  170. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
  171. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
  172. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
  173. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
  174. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
  175. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
  176. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
  177. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
  178. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
  179. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
  180. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
  181. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
  182. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
  183. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
  184. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
  185. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
  186. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
  187. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
  188. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
  189. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
  190. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
  191. package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
  192. package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
  193. package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
  194. package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
  195. package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
  196. package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
  197. package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
  198. package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
  199. package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
  200. package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
  201. package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
  202. package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
  203. package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
  204. package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
  205. package/src/vendor/zoo-code/src/shared/language.ts +43 -0
  206. package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
  207. package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
  208. package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
  209. package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
  210. package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
  211. package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
  212. package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
  213. package/src/vendor/zoo-code/src/utils/object.ts +18 -0
  214. package/src/vendor/zoo-code/src/utils/path.ts +94 -0
  215. package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
  216. package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
  217. package/src/vendor/zoo-code/types/global-settings.ts +19 -0
  218. package/src/vendor/zoo-code/types/index.ts +22 -0
  219. package/src/vendor/zoo-code/types/message.ts +375 -0
  220. package/src/vendor/zoo-code/types/mode.ts +241 -0
  221. package/src/vendor/zoo-code/types/todo.ts +19 -0
  222. package/src/vendor/zoo-code/types/tool-params.ts +116 -0
  223. package/src/vendor/zoo-code/types/tool.ts +67 -0
  224. package/src/vendor/zoo-code/types/vscode.ts +84 -0
  225. package/src/vision/describe.ts +242 -0
  226. package/src/vision/tool.ts +91 -0
  227. package/src/watcher/cli.ts +369 -0
  228. package/src/watcher/github.ts +304 -0
  229. package/src/watcher/index.ts +59 -0
  230. package/src/watcher/state.ts +254 -0
  231. package/src/watcher/watch.ts +562 -0
  232. package/tsconfig.json +18 -0
@@ -0,0 +1,670 @@
1
+ import { distance } from "fastest-levenshtein"
2
+
3
+ import { ToolProgressStatus, DEFAULT_DIFF_FUZZY_THRESHOLD } from "../../../../types"
4
+
5
+ import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text"
6
+ import { ToolUse, DiffStrategy, DiffResult } from "../../../shared/tools"
7
+ import { normalizeString } from "../../../utils/text-normalization"
8
+
9
+ const BUFFER_LINES = 40 // Number of extra context lines to show before and after matches
10
+
11
+ function getSimilarity(original: string, search: string): number {
12
+ // Empty searches are no longer supported
13
+ if (search === "") {
14
+ return 0
15
+ }
16
+
17
+ // Use the normalizeString utility to handle smart quotes and other special characters
18
+ const normalizedOriginal = normalizeString(original)
19
+ const normalizedSearch = normalizeString(search)
20
+
21
+ if (normalizedOriginal === normalizedSearch) {
22
+ return 1
23
+ }
24
+
25
+ // Calculate Levenshtein distance using fastest-levenshtein's distance function
26
+ const dist = distance(normalizedOriginal, normalizedSearch)
27
+
28
+ // Calculate similarity ratio (0 to 1, where 1 is an exact match)
29
+ const maxLength = Math.max(normalizedOriginal.length, normalizedSearch.length)
30
+ return 1 - dist / maxLength
31
+ }
32
+
33
+ /**
34
+ * Performs a "middle-out" search of `lines` (between [startIndex, endIndex]) to find
35
+ * the slice that is most similar to `searchChunk`. Returns the best score, index, and matched text.
36
+ */
37
+ function fuzzySearch(lines: string[], searchChunk: string, startIndex: number, endIndex: number) {
38
+ let bestScore = 0
39
+ let bestMatchIndex = -1
40
+ let bestMatchContent = ""
41
+ const searchLen = searchChunk.split(/\r?\n/).length
42
+
43
+ // Middle-out from the midpoint
44
+ const midPoint = Math.floor((startIndex + endIndex) / 2)
45
+ let leftIndex = midPoint
46
+ let rightIndex = midPoint + 1
47
+
48
+ while (leftIndex >= startIndex || rightIndex <= endIndex - searchLen) {
49
+ if (leftIndex >= startIndex) {
50
+ const originalChunk = lines.slice(leftIndex, leftIndex + searchLen).join("\n")
51
+ const similarity = getSimilarity(originalChunk, searchChunk)
52
+ if (similarity > bestScore) {
53
+ bestScore = similarity
54
+ bestMatchIndex = leftIndex
55
+ bestMatchContent = originalChunk
56
+ }
57
+ leftIndex--
58
+ }
59
+
60
+ if (rightIndex <= endIndex - searchLen) {
61
+ const originalChunk = lines.slice(rightIndex, rightIndex + searchLen).join("\n")
62
+ const similarity = getSimilarity(originalChunk, searchChunk)
63
+ if (similarity > bestScore) {
64
+ bestScore = similarity
65
+ bestMatchIndex = rightIndex
66
+ bestMatchContent = originalChunk
67
+ }
68
+ rightIndex++
69
+ }
70
+ }
71
+
72
+ return { bestScore, bestMatchIndex, bestMatchContent }
73
+ }
74
+
75
+ export class MultiSearchReplaceDiffStrategy implements DiffStrategy {
76
+ private fuzzyThreshold: number
77
+ private bufferLines: number
78
+
79
+ getName(): string {
80
+ return "MultiSearchReplace"
81
+ }
82
+
83
+ constructor(fuzzyThreshold?: number, bufferLines?: number) {
84
+ // Use provided threshold or default to exact matching (1.0)
85
+ // A value of 0.9 means 90% similarity is required for a match,
86
+ // but the default remains 1.0 (exact match). Users can opt in
87
+ // to relaxed matching via diffFuzzyThreshold in settings.
88
+ // Clamp the threshold to [0.5, 1.0] as a defence-in-depth guard.
89
+ const thresholdVal = fuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD
90
+ this.fuzzyThreshold = Math.max(0.5, Math.min(1.0, thresholdVal))
91
+ this.bufferLines = bufferLines ?? BUFFER_LINES
92
+ }
93
+
94
+ private unescapeMarkers(content: string): string {
95
+ return content
96
+ .replace(/^\\<<<<<<</gm, "<<<<<<<")
97
+ .replace(/^\\=======/gm, "=======")
98
+ .replace(/^\\>>>>>>>/gm, ">>>>>>>")
99
+ .replace(/^\\-------/gm, "-------")
100
+ .replace(/^\\:end_line:/gm, ":end_line:")
101
+ .replace(/^\\:start_line:/gm, ":start_line:")
102
+ }
103
+
104
+ private validateMarkerSequencing(diffContent: string): { success: boolean; error?: string } {
105
+ enum State {
106
+ START,
107
+ AFTER_SEARCH,
108
+ AFTER_SEPARATOR,
109
+ }
110
+ const state = { current: State.START, line: 0 }
111
+
112
+ // Pattern allows optional '>' after SEARCH to handle AI-generated diffs
113
+ // (e.g., Sonnet 4 sometimes adds an extra '>')
114
+ const SEARCH_PATTERN = /^<<<<<<< SEARCH>?$/
115
+ const SEARCH = SEARCH_PATTERN.source.replace(/[\^$]/g, "") // Remove regex anchors for display
116
+ const SEP = "======="
117
+ const REPLACE = ">>>>>>> REPLACE"
118
+ const SEARCH_PREFIX = "<<<<<<<"
119
+ const REPLACE_PREFIX = ">>>>>>>"
120
+
121
+ const reportMergeConflictError = (found: string, _expected: string) => ({
122
+ success: false,
123
+ error:
124
+ `ERROR: Special marker '${found}' found in your diff content at line ${state.line}:\n` +
125
+ "\n" +
126
+ `When removing merge conflict markers like '${found}' from files, you MUST escape them\n` +
127
+ "in your SEARCH section by prepending a backslash (\\) at the beginning of the line:\n" +
128
+ "\n" +
129
+ "CORRECT FORMAT:\n\n" +
130
+ "<<<<<<< SEARCH\n" +
131
+ "content before\n" +
132
+ `\\${found} <-- Note the backslash here in this example\n` +
133
+ "content after\n" +
134
+ "=======\n" +
135
+ "replacement content\n" +
136
+ ">>>>>>> REPLACE\n" +
137
+ "\n" +
138
+ "Without escaping, the system confuses your content with diff syntax markers.\n" +
139
+ "You may use multiple diff blocks in a single diff request, but ANY of ONLY the following separators that occur within SEARCH or REPLACE content must be escaped, as follows:\n" +
140
+ `\\${SEARCH}\n` +
141
+ `\\${SEP}\n` +
142
+ `\\${REPLACE}\n`,
143
+ })
144
+
145
+ const reportInvalidDiffError = (found: string, expected: string) => ({
146
+ success: false,
147
+ error:
148
+ `ERROR: Diff block is malformed: marker '${found}' found in your diff content at line ${state.line}. Expected: ${expected}\n` +
149
+ "\n" +
150
+ "CORRECT FORMAT:\n\n" +
151
+ "<<<<<<< SEARCH\n" +
152
+ ":start_line: (required) The line number of original content where the search block starts.\n" +
153
+ "-------\n" +
154
+ "[exact content to find including whitespace]\n" +
155
+ "=======\n" +
156
+ "[new content to replace with]\n" +
157
+ ">>>>>>> REPLACE\n",
158
+ })
159
+
160
+ const reportLineMarkerInReplaceError = (marker: string) => ({
161
+ success: false,
162
+ error:
163
+ `ERROR: Invalid line marker '${marker}' found in REPLACE section at line ${state.line}\n` +
164
+ "\n" +
165
+ "Line markers (:start_line: and :end_line:) are only allowed in SEARCH sections.\n" +
166
+ "\n" +
167
+ "CORRECT FORMAT:\n" +
168
+ "<<<<<<< SEARCH\n" +
169
+ ":start_line:5\n" +
170
+ "content to find\n" +
171
+ "=======\n" +
172
+ "replacement content\n" +
173
+ ">>>>>>> REPLACE\n" +
174
+ "\n" +
175
+ "INCORRECT FORMAT:\n" +
176
+ "<<<<<<< SEARCH\n" +
177
+ "content to find\n" +
178
+ "=======\n" +
179
+ ":start_line:5 <-- Invalid location\n" +
180
+ "replacement content\n" +
181
+ ">>>>>>> REPLACE\n",
182
+ })
183
+
184
+ const lines = diffContent.split("\n")
185
+ const searchCount = lines.filter((l) => SEARCH_PATTERN.test(l.trim())).length
186
+ const sepCount = lines.filter((l) => l.trim() === SEP).length
187
+ const replaceCount = lines.filter((l) => l.trim() === REPLACE).length
188
+
189
+ const likelyBadStructure = searchCount !== replaceCount || sepCount < searchCount
190
+
191
+ for (const line of diffContent.split("\n")) {
192
+ state.line++
193
+ const marker = line.trim()
194
+
195
+ // Check for line markers in REPLACE sections (but allow escaped ones)
196
+ if (state.current === State.AFTER_SEPARATOR) {
197
+ if (marker.startsWith(":start_line:") && !line.trim().startsWith("\\:start_line:")) {
198
+ return reportLineMarkerInReplaceError(":start_line:")
199
+ }
200
+ if (marker.startsWith(":end_line:") && !line.trim().startsWith("\\:end_line:")) {
201
+ return reportLineMarkerInReplaceError(":end_line:")
202
+ }
203
+ }
204
+
205
+ switch (state.current) {
206
+ case State.START:
207
+ if (marker === SEP)
208
+ return likelyBadStructure
209
+ ? reportInvalidDiffError(SEP, SEARCH)
210
+ : reportMergeConflictError(SEP, SEARCH)
211
+ if (marker === REPLACE) return reportInvalidDiffError(REPLACE, SEARCH)
212
+ if (marker.startsWith(REPLACE_PREFIX)) return reportMergeConflictError(marker, SEARCH)
213
+ if (SEARCH_PATTERN.test(marker)) state.current = State.AFTER_SEARCH
214
+ else if (marker.startsWith(SEARCH_PREFIX)) return reportMergeConflictError(marker, SEARCH)
215
+ break
216
+
217
+ case State.AFTER_SEARCH:
218
+ if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH_PATTERN.source, SEP)
219
+ if (marker.startsWith(SEARCH_PREFIX)) return reportMergeConflictError(marker, SEARCH)
220
+ if (marker === REPLACE) return reportInvalidDiffError(REPLACE, SEP)
221
+ if (marker.startsWith(REPLACE_PREFIX)) return reportMergeConflictError(marker, SEARCH)
222
+ if (marker === SEP) state.current = State.AFTER_SEPARATOR
223
+ break
224
+
225
+ case State.AFTER_SEPARATOR:
226
+ if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH_PATTERN.source, REPLACE)
227
+ if (marker.startsWith(SEARCH_PREFIX)) return reportMergeConflictError(marker, REPLACE)
228
+ if (marker === SEP)
229
+ return likelyBadStructure
230
+ ? reportInvalidDiffError(SEP, REPLACE)
231
+ : reportMergeConflictError(SEP, REPLACE)
232
+ if (marker === REPLACE) state.current = State.START
233
+ else if (marker.startsWith(REPLACE_PREFIX)) return reportMergeConflictError(marker, REPLACE)
234
+ break
235
+ }
236
+ }
237
+
238
+ return state.current === State.START
239
+ ? { success: true }
240
+ : {
241
+ success: false,
242
+ error: `ERROR: Unexpected end of sequence: Expected '${
243
+ state.current === State.AFTER_SEARCH ? "=======" : ">>>>>>> REPLACE"
244
+ }' was not found.`,
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Repairs truncated diffs (common with Grok) by adding missing ======= and >>>>>>> REPLACE markers.
250
+ * When the model's output gets cut off mid-stream, the diff may end after SEARCH content
251
+ * without the separator or closing marker. This method detects that pattern and appends
252
+ * the missing markers so the diff can still be parsed and applied.
253
+ */
254
+ private repairTruncatedDiff(diffContent: string): string {
255
+ // Only repair if the diff has at least one SEARCH marker
256
+ if (!/(?<!\\)<<<<<<< SEARCH/.test(diffContent)) {
257
+ return diffContent
258
+ }
259
+
260
+ // Split into blocks based on SEARCH markers
261
+ const blocks = diffContent.split(/(?=(?<!\\)<<<<<<< SEARCH)/)
262
+
263
+ let repaired = ""
264
+
265
+ for (let i = 0; i < blocks.length; i++) {
266
+ const block = blocks[i]
267
+
268
+ if (block.trim() === "") {
269
+ continue
270
+ }
271
+
272
+ // Skip prefix blocks that don't contain a SEARCH marker
273
+ // (e.g., the filename line before the first <<<<<<< SEARCH)
274
+ if (!/(?<!\\)<<<<<<< SEARCH/.test(block)) {
275
+ repaired += block
276
+ continue
277
+ }
278
+
279
+ // Check if this block is complete (has both ======= and >>>>>>> REPLACE)
280
+ const hasSeparator = /(?<=\n)(?<!\\)=======\s*\n/.test(block)
281
+ const hasCloser = /(?<=\n)(?<!\\)>>>>>>> REPLACE(?=\n|$)/.test(block)
282
+
283
+ if (hasSeparator && hasCloser) {
284
+ // Block is complete — emit verbatim (keeps its own trailing separator)
285
+ repaired += block
286
+ continue
287
+ }
288
+
289
+ // Block needs repair. Build a clean block ending at >>>>>>> REPLACE, then
290
+ // re-add an inter-block separator if more (non-empty) blocks follow, so the
291
+ // appended closer never gets glued to the next "<<<<<<< SEARCH".
292
+ const isLast = blocks.slice(i + 1).every((b) => b.trim() === "")
293
+ const separator = isLast ? "" : "\n\n"
294
+
295
+ if (hasSeparator && !hasCloser) {
296
+ // Has ======= but missing >>>>>>> REPLACE — append closing marker
297
+ const body = block.replace(/\s+$/, "")
298
+ repaired += body + "\n>>>>>>> REPLACE" + separator
299
+ } else if (hasCloser && !hasSeparator) {
300
+ // Has >>>>>>> REPLACE but missing the ======= separator. Don't synthesize a
301
+ // second closer; splice the separator in right before the existing closer so
302
+ // everything above it becomes the SEARCH section.
303
+ const body = block.replace(/\s+$/, "")
304
+ repaired += body.replace(/(\n)(>>>>>>> REPLACE)(?=\n|$)/, "$1=======\n$2") + separator
305
+ } else {
306
+ // Missing both ======= and >>>>>>> REPLACE.
307
+ const searchMatch = block.match(/^<<<<<<< SEARCH\n?([\s\S]*)$/)
308
+ let content = (searchMatch?.[1] ?? "").replace(/\s+$/, "")
309
+
310
+ // Peel off any leading Grok header directives (:start_line:, :end_line:, -------)
311
+ // so the "first line is SEARCH" heuristic sees real content, not metadata. The
312
+ // directives are preserved as a header on the SEARCH section.
313
+ let header = ""
314
+ const directiveLine = /^(?::start_line:\s*\d+|:end_line:\s*\d+|-------)\s*$/
315
+ let nlIdx: number
316
+ while ((nlIdx = content.indexOf("\n")) !== -1 && directiveLine.test(content.slice(0, nlIdx))) {
317
+ header += content.slice(0, nlIdx + 1)
318
+ content = content.slice(nlIdx + 1)
319
+ }
320
+
321
+ const firstNewlineIdx = content.indexOf("\n")
322
+ if (firstNewlineIdx !== -1) {
323
+ // First line is SEARCH content, rest is REPLACE content
324
+ const searchContent = content.substring(0, firstNewlineIdx)
325
+ const replaceContent = content.substring(firstNewlineIdx + 1)
326
+ repaired +=
327
+ "<<<<<<< SEARCH\n" +
328
+ header +
329
+ searchContent +
330
+ "\n=======\n" +
331
+ replaceContent +
332
+ "\n>>>>>>> REPLACE" +
333
+ separator
334
+ } else if (header) {
335
+ // Only a directive header plus a single content line: that line is the SEARCH
336
+ // target (the user pinned it with start_line); the REPLACE section is empty.
337
+ repaired += "<<<<<<< SEARCH\n" + header + content + "\n=======\n\n>>>>>>> REPLACE" + separator
338
+ } else {
339
+ // Single line — treat as empty SEARCH with content as REPLACE
340
+ repaired += "<<<<<<< SEARCH\n=======\n" + content + "\n>>>>>>> REPLACE" + separator
341
+ }
342
+ }
343
+ }
344
+
345
+ return repaired || diffContent
346
+ }
347
+
348
+ async applyDiff(
349
+ originalContent: string,
350
+ diffContent: string,
351
+ _paramStartLine?: number,
352
+ _paramEndLine?: number,
353
+ ): Promise<DiffResult> {
354
+ // Repair truncated diffs before validation (common with Grok and other models
355
+ // whose output gets cut off mid-stream, leaving missing ======= and >>>>>>> REPLACE markers)
356
+ const repairedDiff = this.repairTruncatedDiff(diffContent)
357
+
358
+ const validseq = this.validateMarkerSequencing(repairedDiff)
359
+ if (!validseq.success) {
360
+ return {
361
+ success: false,
362
+ error: validseq.error!,
363
+ }
364
+ }
365
+
366
+ /*
367
+ Regex parts:
368
+
369
+ 1. (?:^|\n)
370
+ Ensures the first marker starts at the beginning of the file or right after a newline.
371
+
372
+ 2. (?<!\\)<<<<<<< SEARCH\s*\n
373
+ Matches the line "<<<<<<< SEARCH" (ignoring any trailing spaces) – the negative lookbehind makes sure it isn't escaped.
374
+
375
+ 3. ((?:\:start_line:\s*(\d+)\s*\n))?
376
+ Optionally matches a ":start_line:" line. The outer capturing group is group 1 and the inner (\d+) is group 2.
377
+
378
+ 4. ((?:\:end_line:\s*(\d+)\s*\n))?
379
+ Optionally matches a ":end_line:" line. Group 3 is the whole match and group 4 is the digits.
380
+
381
+ 5. ((?<!\\)-------\s*\n)?
382
+ Optionally matches the "-------" marker line (group 5).
383
+
384
+ 6. ([\s\S]*?)(?:\n)?
385
+ Non‐greedy match for the "search content" (group 6) up to the next marker.
386
+
387
+ 7. (?:(?<=\n)(?<!\\)=======\s*\n)
388
+ Matches the "=======" marker on its own line.
389
+
390
+ 8. ([\s\S]*?)(?:\n)?
391
+ Non‐greedy match for the "replace content" (group 7).
392
+
393
+ 9. (?:(?<=\n)(?<!\\)>>>>>>> REPLACE)(?=\n|$)
394
+ Matches the final ">>>>>>> REPLACE" marker on its own line (and requires a following newline or the end of file).
395
+ */
396
+
397
+ const matches = [
398
+ ...repairedDiff.matchAll(
399
+ /(?:^|\n)(?<!\\)<<<<<<< SEARCH>?\s*\n((?:\:start_line:\s*(\d+)\s*\n))?((?:\:end_line:\s*(\d+)\s*\n))?((?<!\\)-------\s*\n)?([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)=======\s*\n)([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)>>>>>>> REPLACE)(?=\n|$)/g,
400
+ ),
401
+ ]
402
+
403
+ if (matches.length === 0) {
404
+ return {
405
+ success: false,
406
+ error: `Invalid diff format - missing required sections\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n:start_line: start line\\n-------\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include start_line/SEARCH/=======/REPLACE sections with correct markers on new lines`,
407
+ }
408
+ }
409
+ // Detect line ending from original content
410
+ const lineEnding = originalContent.includes("\r\n") ? "\r\n" : "\n"
411
+ let resultLines = originalContent.split(/\r?\n/)
412
+ let delta = 0
413
+ const diffResults: DiffResult[] = []
414
+ let appliedCount = 0
415
+ const replacements = matches
416
+ .map((match) => ({
417
+ startLine: Number(match[2] ?? 0),
418
+ searchContent: match[6],
419
+ replaceContent: match[7],
420
+ }))
421
+ .sort((a, b) => a.startLine - b.startLine)
422
+
423
+ for (const replacement of replacements) {
424
+ let { searchContent, replaceContent } = replacement
425
+ let startLine = replacement.startLine + (replacement.startLine === 0 ? 0 : delta)
426
+
427
+ // First unescape any escaped markers in the content
428
+ searchContent = this.unescapeMarkers(searchContent)
429
+ replaceContent = this.unescapeMarkers(replaceContent)
430
+
431
+ // Strip line numbers from search and replace content if every line starts with a line number
432
+ const hasAllLineNumbers =
433
+ (everyLineHasLineNumbers(searchContent) && everyLineHasLineNumbers(replaceContent)) ||
434
+ (everyLineHasLineNumbers(searchContent) && replaceContent.trim() === "")
435
+
436
+ if (hasAllLineNumbers && startLine === 0) {
437
+ startLine = parseInt(searchContent.split("\n")[0].split("|")[0])
438
+ }
439
+
440
+ if (hasAllLineNumbers) {
441
+ searchContent = stripLineNumbers(searchContent)
442
+ replaceContent = stripLineNumbers(replaceContent)
443
+ }
444
+
445
+ // Validate that search and replace content are not identical
446
+ if (searchContent === replaceContent) {
447
+ diffResults.push({
448
+ success: false,
449
+ error:
450
+ `Search and replace content are identical - no changes would be made\n\n` +
451
+ `Debug Info:\n` +
452
+ `- Search and replace must be different to make changes\n` +
453
+ `- Use read_file to verify the content you want to change`,
454
+ })
455
+ continue
456
+ }
457
+
458
+ // Split content into lines, handling both \n and \r\n
459
+ let searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
460
+ let replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
461
+
462
+ // Validate that search content is not empty
463
+ if (searchLines.length === 0) {
464
+ diffResults.push({
465
+ success: false,
466
+ error: `Empty search content is not allowed\n\nDebug Info:\n- Search content cannot be empty\n- For insertions, provide a specific line using :start_line: and include content to search for\n- For example, match a single line to insert before/after it`,
467
+ })
468
+ continue
469
+ }
470
+
471
+ const endLine = replacement.startLine + searchLines.length - 1
472
+
473
+ // Initialize search variables
474
+ let matchIndex = -1
475
+ let bestMatchScore = 0
476
+ let bestMatchContent = ""
477
+ const searchChunk = searchLines.join("\n")
478
+
479
+ // Determine search bounds
480
+ let searchStartIndex = 0
481
+ let searchEndIndex = resultLines.length
482
+
483
+ // Validate and handle line range if provided
484
+ if (startLine) {
485
+ // Convert to 0-based index
486
+ const exactStartIndex = startLine - 1
487
+ const searchLen = searchLines.length
488
+ const exactEndIndex = exactStartIndex + searchLen - 1
489
+
490
+ // Try exact match first
491
+ const originalChunk = resultLines.slice(exactStartIndex, exactEndIndex + 1).join("\n")
492
+ const similarity = getSimilarity(originalChunk, searchChunk)
493
+ if (similarity >= this.fuzzyThreshold) {
494
+ matchIndex = exactStartIndex
495
+ bestMatchScore = similarity
496
+ bestMatchContent = originalChunk
497
+ } else {
498
+ // Set bounds for buffered search
499
+ searchStartIndex = Math.max(0, startLine - (this.bufferLines + 1))
500
+ searchEndIndex = Math.min(resultLines.length, startLine + searchLines.length + this.bufferLines)
501
+ }
502
+ }
503
+
504
+ // If no match found yet, try middle-out search within bounds
505
+ if (matchIndex === -1) {
506
+ const {
507
+ bestScore,
508
+ bestMatchIndex,
509
+ bestMatchContent: midContent,
510
+ } = fuzzySearch(resultLines, searchChunk, searchStartIndex, searchEndIndex)
511
+ matchIndex = bestMatchIndex
512
+ bestMatchScore = bestScore
513
+ bestMatchContent = midContent
514
+ }
515
+
516
+ // Try aggressive line number stripping as a fallback if regular matching fails
517
+ if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
518
+ // Strip both search and replace content once (simultaneously)
519
+ const aggressiveSearchContent = stripLineNumbers(searchContent, true)
520
+ const aggressiveReplaceContent = stripLineNumbers(replaceContent, true)
521
+
522
+ const aggressiveSearchLines = aggressiveSearchContent ? aggressiveSearchContent.split(/\r?\n/) : []
523
+ const aggressiveSearchChunk = aggressiveSearchLines.join("\n")
524
+
525
+ // Try middle-out search again with aggressive stripped content (respecting the same search bounds)
526
+ const {
527
+ bestScore,
528
+ bestMatchIndex,
529
+ bestMatchContent: aggContent,
530
+ } = fuzzySearch(resultLines, aggressiveSearchChunk, searchStartIndex, searchEndIndex)
531
+ if (bestMatchIndex !== -1 && bestScore >= this.fuzzyThreshold) {
532
+ matchIndex = bestMatchIndex
533
+ bestMatchScore = bestScore
534
+ bestMatchContent = aggContent
535
+ // Replace the original search/replace with their stripped versions
536
+ searchContent = aggressiveSearchContent
537
+ replaceContent = aggressiveReplaceContent
538
+ searchLines = aggressiveSearchLines
539
+ replaceLines = replaceContent ? replaceContent.split(/\r?\n/) : []
540
+ } else {
541
+ // No match found with either method
542
+ const originalContentSection =
543
+ startLine && endLine
544
+ ? `\n\nOriginal Content:\n${addLineNumbers(
545
+ resultLines
546
+ .slice(
547
+ Math.max(0, startLine - 1 - this.bufferLines),
548
+ Math.min(resultLines.length, endLine + this.bufferLines),
549
+ )
550
+ .join("\n"),
551
+ Math.max(1, startLine - this.bufferLines),
552
+ )}`
553
+ : `\n\nOriginal Content:\n${addLineNumbers(
554
+ resultLines
555
+ .slice(
556
+ Math.max(0, (matchIndex >= 0 ? matchIndex : 0) - this.bufferLines),
557
+ Math.min(
558
+ resultLines.length,
559
+ (matchIndex >= 0 ? matchIndex : 0) +
560
+ searchLines.length +
561
+ this.bufferLines,
562
+ ),
563
+ )
564
+ .join("\n"),
565
+ Math.max(1, (matchIndex >= 0 ? matchIndex : 0) - this.bufferLines + 1),
566
+ )}`
567
+
568
+ const bestMatchSection = bestMatchContent
569
+ ? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
570
+ : `\n\nBest Match Found:\n(no match)`
571
+
572
+ const lineRange = startLine ? ` at line: ${startLine}` : ""
573
+
574
+ const levenDist = bestMatchContent
575
+ ? distance(normalizeString(searchChunk), normalizeString(bestMatchContent))
576
+ : -1
577
+
578
+ diffResults.push({
579
+ success: false,
580
+ error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine ? `starting at line ${startLine}` : "start to end"}\n- Levenshtein Distance: ${levenDist >= 0 ? `${levenDist} characters` : "N/A"}\n- Search Length: ${searchChunk.length} characters\n- Best Match Length: ${bestMatchContent ? bestMatchContent.length : 0} characters\n- Tried both standard and aggressive line number stripping\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`,
581
+ })
582
+ continue
583
+ }
584
+ }
585
+
586
+ // Get the matched lines from the original content
587
+ const matchedLines = resultLines.slice(matchIndex, matchIndex + searchLines.length)
588
+
589
+ // Get the exact indentation (preserving tabs/spaces) of each line
590
+ const originalIndents = matchedLines.map((line) => {
591
+ const match = line.match(/^[\t ]*/)
592
+ return match ? match[0] : ""
593
+ })
594
+
595
+ // Get the exact indentation of each line in the search block
596
+ const searchIndents = searchLines.map((line) => {
597
+ const match = line.match(/^[\t ]*/)
598
+ return match ? match[0] : ""
599
+ })
600
+
601
+ // Apply the replacement while preserving exact indentation
602
+ const indentedReplaceLines = replaceLines.map((line) => {
603
+ // Get the matched line's exact indentation
604
+ const matchedIndent = originalIndents[0] || ""
605
+
606
+ // Get the current line's indentation relative to the search content
607
+ const currentIndentMatch = line.match(/^[\t ]*/)
608
+ const currentIndent = currentIndentMatch ? currentIndentMatch[0] : ""
609
+ const searchBaseIndent = searchIndents[0] || ""
610
+
611
+ // Calculate the relative indentation level
612
+ const searchBaseLevel = searchBaseIndent.length
613
+ const currentLevel = currentIndent.length
614
+ const relativeLevel = currentLevel - searchBaseLevel
615
+
616
+ // If relative level is negative, remove indentation from matched indent
617
+ // If positive, add to matched indent
618
+ const finalIndent =
619
+ relativeLevel < 0
620
+ ? matchedIndent.slice(0, Math.max(0, matchedIndent.length + relativeLevel))
621
+ : matchedIndent + currentIndent.slice(searchBaseLevel)
622
+
623
+ return finalIndent + line.trim()
624
+ })
625
+
626
+ // Construct the final content
627
+ const beforeMatch = resultLines.slice(0, matchIndex)
628
+ const afterMatch = resultLines.slice(matchIndex + searchLines.length)
629
+ resultLines = [...beforeMatch, ...indentedReplaceLines, ...afterMatch]
630
+ delta = delta - matchedLines.length + replaceLines.length
631
+ appliedCount++
632
+ }
633
+ const finalContent = resultLines.join(lineEnding)
634
+ if (appliedCount === 0) {
635
+ return {
636
+ success: false,
637
+ failParts: diffResults,
638
+ }
639
+ }
640
+ return {
641
+ success: true,
642
+ content: finalContent,
643
+ failParts: diffResults,
644
+ }
645
+ }
646
+
647
+ getProgressStatus(toolUse: ToolUse, result?: DiffResult): ToolProgressStatus {
648
+ const diffContent = toolUse.params.diff
649
+ if (diffContent) {
650
+ const icon = "diff-multiple"
651
+ if (toolUse.partial) {
652
+ if (Math.floor(diffContent.length / 10) % 10 === 0) {
653
+ const searchBlockCount = (diffContent.match(/SEARCH/g) || []).length
654
+ return { icon, text: `${searchBlockCount}` }
655
+ }
656
+ } else if (result) {
657
+ const searchBlockCount = (diffContent.match(/SEARCH/g) || []).length
658
+ if (result.failParts?.length) {
659
+ return {
660
+ icon,
661
+ text: `${searchBlockCount - result.failParts.length}/${searchBlockCount}`,
662
+ }
663
+ } else {
664
+ return { icon, text: `${searchBlockCount}` }
665
+ }
666
+ }
667
+ }
668
+ return {}
669
+ }
670
+ }