oricore 1.0.0

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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +199 -0
  3. package/dist/agent/agent/agentManager.d.ts +38 -0
  4. package/dist/agent/agent/builtin/common.d.ts +5 -0
  5. package/dist/agent/agent/builtin/explore.d.ts +5 -0
  6. package/dist/agent/agent/builtin/general-purpose.d.ts +5 -0
  7. package/dist/agent/agent/builtin/index.d.ts +5 -0
  8. package/dist/agent/agent/executor.d.ts +2 -0
  9. package/dist/agent/agent/types.d.ts +98 -0
  10. package/dist/api/engine.d.ts +213 -0
  11. package/dist/communication/index.d.ts +4 -0
  12. package/dist/communication/messageBus.d.ts +71 -0
  13. package/dist/core/at.d.ts +26 -0
  14. package/dist/core/backgroundTaskManager.d.ts +27 -0
  15. package/dist/core/compact.d.ts +9 -0
  16. package/dist/core/config.d.ts +103 -0
  17. package/dist/core/constants.d.ts +32 -0
  18. package/dist/core/context.d.ts +57 -0
  19. package/dist/core/globalData.d.ts +21 -0
  20. package/dist/core/history.d.ts +24 -0
  21. package/dist/core/ide.d.ts +103 -0
  22. package/dist/core/jsonl.d.ts +37 -0
  23. package/dist/core/llmsContext.d.ts +14 -0
  24. package/dist/core/loop.d.ts +82 -0
  25. package/dist/core/message.d.ts +132 -0
  26. package/dist/core/model.d.ts +79 -0
  27. package/dist/core/output-style/builtin/default.d.ts +2 -0
  28. package/dist/core/output-style/builtin/explanatory.d.ts +2 -0
  29. package/dist/core/output-style/builtin/index.d.ts +6 -0
  30. package/dist/core/output-style/builtin/miao.d.ts +2 -0
  31. package/dist/core/output-style/builtin/minimal.d.ts +2 -0
  32. package/dist/core/output-style/types.d.ts +6 -0
  33. package/dist/core/outputFormat.d.ts +29 -0
  34. package/dist/core/outputStyle.d.ts +43 -0
  35. package/dist/core/paths.d.ts +20 -0
  36. package/dist/core/planSystemPrompt.d.ts +5 -0
  37. package/dist/core/plugin.d.ts +138 -0
  38. package/dist/core/project.d.ts +64 -0
  39. package/dist/core/promptCache.d.ts +3 -0
  40. package/dist/core/query.d.ts +14 -0
  41. package/dist/core/rules.d.ts +8 -0
  42. package/dist/core/systemPrompt.d.ts +9 -0
  43. package/dist/core/thinking-config.d.ts +3 -0
  44. package/dist/core/usage.d.ts +14 -0
  45. package/dist/index.d.ts +16 -0
  46. package/dist/index.js +144432 -0
  47. package/dist/mcp/mcp.d.ts +49 -0
  48. package/dist/modes/builtin.d.ts +34 -0
  49. package/dist/modes/index.d.ts +8 -0
  50. package/dist/modes/registry.d.ts +18 -0
  51. package/dist/modes/types.d.ts +51 -0
  52. package/dist/platform/index.d.ts +5 -0
  53. package/dist/platform/node.d.ts +28 -0
  54. package/dist/platform/types.d.ts +41 -0
  55. package/dist/session/session.d.ts +43 -0
  56. package/dist/skill/skill.d.ts +79 -0
  57. package/dist/tools/tool.d.ts +119 -0
  58. package/dist/tools/tools/askUserQuestion.d.ts +48 -0
  59. package/dist/tools/tools/bash.d.ts +43 -0
  60. package/dist/tools/tools/edit.d.ts +9 -0
  61. package/dist/tools/tools/fetch.d.ts +9 -0
  62. package/dist/tools/tools/glob.d.ts +7 -0
  63. package/dist/tools/tools/grep.d.ts +22 -0
  64. package/dist/tools/tools/ls.d.ts +6 -0
  65. package/dist/tools/tools/read.d.ts +9 -0
  66. package/dist/tools/tools/skill.d.ts +7 -0
  67. package/dist/tools/tools/task.d.ts +14 -0
  68. package/dist/tools/tools/todo.d.ts +37 -0
  69. package/dist/tools/tools/write.d.ts +7 -0
  70. package/dist/utils/apiKeyRotation.d.ts +2 -0
  71. package/dist/utils/applyEdit.d.ts +17 -0
  72. package/dist/utils/background-detection.d.ts +2 -0
  73. package/dist/utils/dotenv.d.ts +9 -0
  74. package/dist/utils/env.d.ts +6 -0
  75. package/dist/utils/error.d.ts +11 -0
  76. package/dist/utils/execFileNoThrow.d.ts +8 -0
  77. package/dist/utils/files.d.ts +10 -0
  78. package/dist/utils/git.d.ts +163 -0
  79. package/dist/utils/ide.d.ts +27 -0
  80. package/dist/utils/ignore.d.ts +6 -0
  81. package/dist/utils/isLocal.d.ts +1 -0
  82. package/dist/utils/language.d.ts +9 -0
  83. package/dist/utils/list.d.ts +20 -0
  84. package/dist/utils/mergeSystemMessagesMiddleware.d.ts +2 -0
  85. package/dist/utils/messageNormalization.d.ts +22 -0
  86. package/dist/utils/path.d.ts +34 -0
  87. package/dist/utils/prependSystemMessageMiddleware.d.ts +2 -0
  88. package/dist/utils/project.d.ts +1 -0
  89. package/dist/utils/proxy.d.ts +18 -0
  90. package/dist/utils/randomUUID.d.ts +5 -0
  91. package/dist/utils/renderSessionMarkdown.d.ts +10 -0
  92. package/dist/utils/ripgrep.d.ts +16 -0
  93. package/dist/utils/safeFrontMatter.d.ts +11 -0
  94. package/dist/utils/safeParseJson.d.ts +1 -0
  95. package/dist/utils/safeStringify.d.ts +1 -0
  96. package/dist/utils/sanitizeAIResponse.d.ts +30 -0
  97. package/dist/utils/setTerminalTitle.d.ts +1 -0
  98. package/dist/utils/shell-execution.d.ts +44 -0
  99. package/dist/utils/string.d.ts +8 -0
  100. package/dist/utils/symbols.d.ts +14 -0
  101. package/dist/utils/system-encoding.d.ts +40 -0
  102. package/dist/utils/tokenCounter.d.ts +8 -0
  103. package/dist/utils/username.d.ts +1 -0
  104. package/package.json +106 -0
  105. package/src/agent/agent/agentManager.test.ts +124 -0
  106. package/src/agent/agent/agentManager.ts +372 -0
  107. package/src/agent/agent/builtin/common.ts +20 -0
  108. package/src/agent/agent/builtin/explore.ts +53 -0
  109. package/src/agent/agent/builtin/general-purpose.ts +38 -0
  110. package/src/agent/agent/builtin/index.ts +13 -0
  111. package/src/agent/agent/executor.test.ts +339 -0
  112. package/src/agent/agent/executor.ts +224 -0
  113. package/src/agent/agent/types.ts +119 -0
  114. package/src/api/engine.ts +466 -0
  115. package/src/communication/index.ts +18 -0
  116. package/src/communication/messageBus.ts +393 -0
  117. package/src/core/at.ts +315 -0
  118. package/src/core/backgroundTaskManager.ts +129 -0
  119. package/src/core/compact.ts +95 -0
  120. package/src/core/config.ts +441 -0
  121. package/src/core/constants.ts +82 -0
  122. package/src/core/context.ts +214 -0
  123. package/src/core/globalData.ts +77 -0
  124. package/src/core/history.ts +323 -0
  125. package/src/core/ide.ts +325 -0
  126. package/src/core/jsonl.ts +100 -0
  127. package/src/core/llmsContext.ts +117 -0
  128. package/src/core/loop.ts +638 -0
  129. package/src/core/message.ts +304 -0
  130. package/src/core/model.ts +2198 -0
  131. package/src/core/output-style/builtin/default.ts +9 -0
  132. package/src/core/output-style/builtin/explanatory.ts +22 -0
  133. package/src/core/output-style/builtin/index.ts +19 -0
  134. package/src/core/output-style/builtin/miao.ts +22 -0
  135. package/src/core/output-style/builtin/minimal.ts +8 -0
  136. package/src/core/output-style/types.ts +6 -0
  137. package/src/core/outputFormat.ts +93 -0
  138. package/src/core/outputStyle.ts +255 -0
  139. package/src/core/paths.ts +161 -0
  140. package/src/core/planSystemPrompt.ts +46 -0
  141. package/src/core/plugin.ts +299 -0
  142. package/src/core/project.ts +492 -0
  143. package/src/core/promptCache.ts +32 -0
  144. package/src/core/query.ts +46 -0
  145. package/src/core/rules.ts +56 -0
  146. package/src/core/systemPrompt.ts +176 -0
  147. package/src/core/thinking-config.ts +98 -0
  148. package/src/core/usage.ts +68 -0
  149. package/src/index.ts +39 -0
  150. package/src/mcp/mcp.ts +637 -0
  151. package/src/modes/builtin.ts +305 -0
  152. package/src/modes/index.ts +22 -0
  153. package/src/modes/registry.ts +39 -0
  154. package/src/modes/types.ts +56 -0
  155. package/src/platform/index.ts +6 -0
  156. package/src/platform/node.ts +108 -0
  157. package/src/platform/types.ts +54 -0
  158. package/src/plugins/index.ts +15 -0
  159. package/src/session/session.ts +187 -0
  160. package/src/skill/skill.ts +702 -0
  161. package/src/tools/tool.ts +378 -0
  162. package/src/tools/tools/askUserQuestion.ts +134 -0
  163. package/src/tools/tools/bash.test.ts +425 -0
  164. package/src/tools/tools/bash.ts +999 -0
  165. package/src/tools/tools/edit.ts +86 -0
  166. package/src/tools/tools/fetch.ts +129 -0
  167. package/src/tools/tools/glob.ts +69 -0
  168. package/src/tools/tools/grep.test.ts +194 -0
  169. package/src/tools/tools/grep.ts +358 -0
  170. package/src/tools/tools/ls.ts +51 -0
  171. package/src/tools/tools/read.test.ts +169 -0
  172. package/src/tools/tools/read.ts +284 -0
  173. package/src/tools/tools/skill.ts +73 -0
  174. package/src/tools/tools/task.test.ts +262 -0
  175. package/src/tools/tools/task.ts +284 -0
  176. package/src/tools/tools/todo.ts +269 -0
  177. package/src/tools/tools/write.ts +71 -0
  178. package/src/types.d.ts +18 -0
  179. package/src/utils/apiKeyRotation.test.ts +70 -0
  180. package/src/utils/apiKeyRotation.ts +24 -0
  181. package/src/utils/applyEdit.test.ts +388 -0
  182. package/src/utils/applyEdit.ts +547 -0
  183. package/src/utils/background-detection.test.ts +61 -0
  184. package/src/utils/background-detection.ts +58 -0
  185. package/src/utils/dotenv.ts +26 -0
  186. package/src/utils/env.ts +90 -0
  187. package/src/utils/error.ts +38 -0
  188. package/src/utils/execFileNoThrow.ts +49 -0
  189. package/src/utils/files.ts +93 -0
  190. package/src/utils/git.ts +1152 -0
  191. package/src/utils/ide.ts +279 -0
  192. package/src/utils/ignore.ts +275 -0
  193. package/src/utils/isLocal.ts +6 -0
  194. package/src/utils/language.ts +33 -0
  195. package/src/utils/list.ts +200 -0
  196. package/src/utils/mergeSystemMessagesMiddleware.ts +32 -0
  197. package/src/utils/messageNormalization.test.ts +401 -0
  198. package/src/utils/messageNormalization.ts +168 -0
  199. package/src/utils/path.ts +98 -0
  200. package/src/utils/prependSystemMessageMiddleware.ts +16 -0
  201. package/src/utils/project.ts +32 -0
  202. package/src/utils/proxy.ts +102 -0
  203. package/src/utils/randomUUID.ts +11 -0
  204. package/src/utils/renderSessionMarkdown.ts +175 -0
  205. package/src/utils/ripgrep.ts +189 -0
  206. package/src/utils/safeFrontMatter.test.ts +118 -0
  207. package/src/utils/safeFrontMatter.ts +68 -0
  208. package/src/utils/safeParseJson.ts +7 -0
  209. package/src/utils/safeStringify.ts +10 -0
  210. package/src/utils/sanitizeAIResponse.test.ts +135 -0
  211. package/src/utils/sanitizeAIResponse.ts +55 -0
  212. package/src/utils/setTerminalTitle.ts +7 -0
  213. package/src/utils/shell-execution.test.ts +237 -0
  214. package/src/utils/shell-execution.ts +279 -0
  215. package/src/utils/string.ts +13 -0
  216. package/src/utils/symbols.ts +18 -0
  217. package/src/utils/system-encoding.test.ts +164 -0
  218. package/src/utils/system-encoding.ts +296 -0
  219. package/src/utils/tokenCounter.test.ts +38 -0
  220. package/src/utils/tokenCounter.ts +19 -0
  221. package/src/utils/username.ts +21 -0
@@ -0,0 +1,547 @@
1
+ import * as Diff from 'diff';
2
+ import { readFileSync } from 'fs';
3
+ import { isAbsolute, resolve } from 'pathe';
4
+
5
+ export interface Edit {
6
+ old_string: string;
7
+ new_string: string;
8
+ replace_all?: boolean;
9
+ }
10
+
11
+ export interface Hunk {
12
+ oldStart: number;
13
+ oldLines: number;
14
+ newStart: number;
15
+ newLines: number;
16
+ lines: string[];
17
+ }
18
+
19
+ /**
20
+ * Calculate Levenshtein distance between two strings
21
+ * Used for similarity calculation in BlockAnchorReplacer
22
+ */
23
+ function levenshtein(a: string, b: string): number {
24
+ const matrix: number[][] = [];
25
+
26
+ // Initialize matrix
27
+ for (let i = 0; i <= b.length; i++) {
28
+ matrix[i] = [i];
29
+ }
30
+ for (let j = 0; j <= a.length; j++) {
31
+ matrix[0][j] = j;
32
+ }
33
+
34
+ // Fill matrix
35
+ for (let i = 1; i <= b.length; i++) {
36
+ for (let j = 1; j <= a.length; j++) {
37
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
38
+ matrix[i][j] = matrix[i - 1][j - 1];
39
+ } else {
40
+ matrix[i][j] = Math.min(
41
+ matrix[i - 1][j - 1] + 1, // substitution
42
+ matrix[i][j - 1] + 1, // insertion
43
+ matrix[i - 1][j] + 1, // deletion
44
+ );
45
+ }
46
+ }
47
+ }
48
+
49
+ return matrix[b.length][a.length];
50
+ }
51
+
52
+ /**
53
+ * Try line-trimmed matching: ignore leading/trailing whitespace on each line
54
+ * Solves indentation difference issues
55
+ * Returns null if multiple matches found to avoid ambiguity
56
+ */
57
+ function tryLineTrimmedMatch(content: string, oldStr: string): string | null {
58
+ const contentLines = content.split('\n');
59
+ const searchLines = oldStr.split('\n');
60
+ const matches: string[] = [];
61
+
62
+ for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
63
+ let isMatch = true;
64
+ for (let j = 0; j < searchLines.length; j++) {
65
+ if (contentLines[i + j].trim() !== searchLines[j].trim()) {
66
+ isMatch = false;
67
+ break;
68
+ }
69
+ }
70
+
71
+ if (isMatch) {
72
+ const matchedLines = contentLines.slice(i, i + searchLines.length);
73
+ matches.push(matchedLines.join('\n'));
74
+ }
75
+ }
76
+
77
+ if (matches.length === 1) {
78
+ return matches[0];
79
+ }
80
+
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * Try block anchor matching: use first/last lines as anchors + Levenshtein similarity
86
+ * Solves code blocks with slight modifications in middle lines
87
+ * Requires at least 3 lines
88
+ *
89
+ * Similarity thresholds:
90
+ * - Single candidate: 0.0 (more lenient)
91
+ * - Multiple candidates: 0.3 (more strict)
92
+ */
93
+ function tryBlockAnchorMatch(content: string, oldStr: string): string | null {
94
+ const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.5;
95
+ const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3;
96
+
97
+ const contentLines = content.split('\n');
98
+ const searchLines = oldStr.split('\n');
99
+
100
+ // Require at least 3 lines for this strategy
101
+ if (searchLines.length < 3) {
102
+ return null;
103
+ }
104
+
105
+ const firstLine = searchLines[0].trim();
106
+ const lastLine = searchLines[searchLines.length - 1].trim();
107
+
108
+ // Collect all candidates where first and last lines match
109
+ const candidates: Array<{ startLine: number; endLine: number }> = [];
110
+
111
+ for (let i = 0; i < contentLines.length; i++) {
112
+ if (contentLines[i].trim() !== firstLine) continue;
113
+
114
+ for (let j = i + 2; j < contentLines.length; j++) {
115
+ if (contentLines[j].trim() === lastLine) {
116
+ candidates.push({ startLine: i, endLine: j });
117
+ }
118
+ }
119
+ }
120
+
121
+ if (candidates.length === 0) {
122
+ return null;
123
+ }
124
+
125
+ // Helper function to calculate similarity for a candidate
126
+ const calculateSimilarity = (candidate: {
127
+ startLine: number;
128
+ endLine: number;
129
+ }): number => {
130
+ const blockSize = candidate.endLine - candidate.startLine + 1;
131
+ const middleLines = Math.min(blockSize - 2, searchLines.length - 2);
132
+
133
+ if (middleLines <= 0) return 1.0; // Only first and last lines, already matched
134
+
135
+ let totalSimilarity = 0;
136
+
137
+ for (let k = 1; k <= middleLines; k++) {
138
+ const contentLine = contentLines[candidate.startLine + k];
139
+ const searchLine = searchLines[k];
140
+ const maxLen = Math.max(contentLine.length, searchLine.length);
141
+
142
+ if (maxLen === 0) {
143
+ totalSimilarity += 1.0;
144
+ } else {
145
+ const distance = levenshtein(contentLine, searchLine);
146
+ totalSimilarity += 1 - distance / maxLen;
147
+ }
148
+ }
149
+
150
+ return totalSimilarity / middleLines;
151
+ };
152
+
153
+ // Single candidate scenario - use more lenient threshold
154
+ if (candidates.length === 1) {
155
+ const candidate = candidates[0];
156
+ const similarity = calculateSimilarity(candidate);
157
+
158
+ if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
159
+ const matchedLines = contentLines.slice(
160
+ candidate.startLine,
161
+ candidate.endLine + 1,
162
+ );
163
+ return matchedLines.join('\n');
164
+ }
165
+
166
+ return null;
167
+ }
168
+
169
+ // Multiple candidates scenario - find best match above threshold
170
+ let bestMatch: string | null = null;
171
+ let maxSimilarity = -1;
172
+
173
+ for (const candidate of candidates) {
174
+ const similarity = calculateSimilarity(candidate);
175
+
176
+ if (
177
+ similarity > maxSimilarity &&
178
+ similarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD
179
+ ) {
180
+ maxSimilarity = similarity;
181
+ const matchedLines = contentLines.slice(
182
+ candidate.startLine,
183
+ candidate.endLine + 1,
184
+ );
185
+ bestMatch = matchedLines.join('\n');
186
+ }
187
+ }
188
+
189
+ return bestMatch;
190
+ }
191
+
192
+ /**
193
+ * Try whitespace-normalized matching: replace all consecutive whitespace with single space
194
+ * Solves extra spaces and tab mixing issues
195
+ * Returns null if multiple matches found to avoid ambiguity
196
+ */
197
+ function tryWhitespaceNormalizedMatch(
198
+ content: string,
199
+ oldStr: string,
200
+ ): string | null {
201
+ const normalize = (text: string) => text.replace(/\s+/g, ' ').trim();
202
+ const normalizedOld = normalize(oldStr);
203
+ const lines = content.split('\n');
204
+ const matches: string[] = [];
205
+
206
+ // Single-line matching
207
+ for (const line of lines) {
208
+ if (normalize(line) === normalizedOld) {
209
+ matches.push(line);
210
+ }
211
+ }
212
+
213
+ if (matches.length === 1) {
214
+ return matches[0];
215
+ }
216
+
217
+ // Multi-line matching
218
+ const oldLines = oldStr.split('\n');
219
+ if (oldLines.length > 1) {
220
+ const multiLineMatches: string[] = [];
221
+ for (let i = 0; i <= lines.length - oldLines.length; i++) {
222
+ const block = lines.slice(i, i + oldLines.length).join('\n');
223
+ if (normalize(block) === normalizedOld) {
224
+ multiLineMatches.push(block);
225
+ }
226
+ }
227
+ if (multiLineMatches.length === 1) {
228
+ return multiLineMatches[0];
229
+ }
230
+ }
231
+
232
+ return null;
233
+ }
234
+
235
+ /**
236
+ * Unescape string (handle LLM over-escaping issues)
237
+ * Reference: unescapeStringForGeminiBug in edit-logic-analysis.md
238
+ */
239
+ function unescapeStringForGeminiBug(inputString: string): string {
240
+ return inputString.replace(
241
+ /\\+(n|t|r|'|"|`|\\|\n)/g,
242
+ (match, capturedChar) => {
243
+ switch (capturedChar) {
244
+ case 'n':
245
+ return '\n';
246
+ case 't':
247
+ return '\t';
248
+ case 'r':
249
+ return '\r';
250
+ case "'":
251
+ return "'";
252
+ case '"':
253
+ return '"';
254
+ case '`':
255
+ return '`';
256
+ case '\\':
257
+ return '\\';
258
+ case '\n':
259
+ return '\n';
260
+ default:
261
+ return match;
262
+ }
263
+ },
264
+ );
265
+ }
266
+
267
+ /**
268
+ * Try escape-normalized matching: handle over-escaped strings
269
+ * Solves LLM-generated \\n, \\t escape issues
270
+ */
271
+ function tryEscapeNormalizedMatch(
272
+ content: string,
273
+ oldStr: string,
274
+ ): string | null {
275
+ const unescaped = unescapeStringForGeminiBug(oldStr);
276
+
277
+ // Direct matching
278
+ if (content.includes(unescaped)) {
279
+ return unescaped;
280
+ }
281
+
282
+ // Multi-line block matching
283
+ const lines = content.split('\n');
284
+ const unescapedLines = unescaped.split('\n');
285
+
286
+ if (unescapedLines.length > 1) {
287
+ for (let i = 0; i <= lines.length - unescapedLines.length; i++) {
288
+ const block = lines.slice(i, i + unescapedLines.length).join('\n');
289
+ if (unescapeStringForGeminiBug(block) === unescaped) {
290
+ return block;
291
+ }
292
+ }
293
+ }
294
+
295
+ return null;
296
+ }
297
+
298
+ /**
299
+ * Remove common indentation from text
300
+ */
301
+ function removeCommonIndentation(text: string): string {
302
+ const lines = text.split('\n');
303
+ const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
304
+
305
+ if (nonEmptyLines.length === 0) return text;
306
+
307
+ // Find minimum indentation
308
+ const minIndent = Math.min(
309
+ ...nonEmptyLines.map((line) => {
310
+ const match = line.match(/^(\s*)/);
311
+ return match ? match[1].length : 0;
312
+ }),
313
+ );
314
+
315
+ // Remove minimum common indentation
316
+ return lines
317
+ .map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
318
+ .join('\n');
319
+ }
320
+
321
+ /**
322
+ * Try indentation-flexible matching: ignore overall indentation level differences
323
+ * Solves code block movement to different indentation levels
324
+ * Returns null if multiple matches found to avoid ambiguity
325
+ */
326
+ function tryIndentationFlexibleMatch(
327
+ content: string,
328
+ oldStr: string,
329
+ ): string | null {
330
+ const normalizedSearch = removeCommonIndentation(oldStr);
331
+ const contentLines = content.split('\n');
332
+ const searchLines = oldStr.split('\n');
333
+ const matches: string[] = [];
334
+
335
+ for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
336
+ const block = contentLines.slice(i, i + searchLines.length).join('\n');
337
+ if (removeCommonIndentation(block) === normalizedSearch) {
338
+ matches.push(block);
339
+ }
340
+ }
341
+
342
+ if (matches.length === 1) {
343
+ return matches[0];
344
+ }
345
+
346
+ return null;
347
+ }
348
+
349
+ /**
350
+ * Apply string replacement using multiple strategies to improve match success rate
351
+ * Strategies are tried in priority order, using the first successful one
352
+ *
353
+ * Strategy chain (in priority order):
354
+ * 1. Exact match
355
+ * 2. Line-trimmed match (ignoring indentation)
356
+ * 3. Block anchor match (using first/last lines + similarity)
357
+ * 4. Whitespace-normalized match (handling extra spaces)
358
+ * 5. Escape-normalized match (handling over-escaping)
359
+ * 6. Indentation-flexible match (ignoring base indentation level)
360
+ *
361
+ * Note: If a strategy finds multiple matches and replace_all=false,
362
+ * it will continue to the next more precise strategy (but current implementation
363
+ * performs replacement directly, as subsequent strategies typically find the same
364
+ * or more matches)
365
+ */
366
+ function applyStringReplace(
367
+ content: string,
368
+ oldStr: string,
369
+ newStr: string,
370
+ replaceAll = false,
371
+ ): { result: string; matchIndex: number } {
372
+ const performReplace = (
373
+ text: string,
374
+ search: string,
375
+ replace: string,
376
+ matchIdx: number,
377
+ ): { result: string; matchIndex: number } => {
378
+ if (replaceAll) {
379
+ const escapedSearch = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
380
+ return {
381
+ result: text.replace(new RegExp(escapedSearch, 'g'), () => replace),
382
+ matchIndex: matchIdx,
383
+ };
384
+ }
385
+ return {
386
+ result: text.replace(search, () => replace),
387
+ matchIndex: matchIdx,
388
+ };
389
+ };
390
+
391
+ // Strategy 1: Exact match
392
+ if (content.includes(oldStr)) {
393
+ const matchIndex = content.indexOf(oldStr);
394
+ if (newStr !== '') {
395
+ return performReplace(content, oldStr, newStr, matchIndex);
396
+ }
397
+
398
+ const hasTrailingNewline =
399
+ !oldStr.endsWith('\n') && content.includes(`${oldStr}\n`);
400
+
401
+ return hasTrailingNewline
402
+ ? performReplace(content, `${oldStr}\n`, newStr, matchIndex)
403
+ : performReplace(content, oldStr, newStr, matchIndex);
404
+ }
405
+
406
+ // Strategy 2: Line-trimmed match
407
+ const lineTrimmedMatch = tryLineTrimmedMatch(content, oldStr);
408
+ if (lineTrimmedMatch) {
409
+ const matchIndex = content.indexOf(lineTrimmedMatch);
410
+ return performReplace(content, lineTrimmedMatch, newStr, matchIndex);
411
+ }
412
+
413
+ // Strategy 3: Block anchor match (first/last lines + similarity)
414
+ const blockAnchorMatch = tryBlockAnchorMatch(content, oldStr);
415
+ if (blockAnchorMatch) {
416
+ const matchIndex = content.indexOf(blockAnchorMatch);
417
+ return performReplace(content, blockAnchorMatch, newStr, matchIndex);
418
+ }
419
+
420
+ // Strategy 4: Whitespace-normalized match
421
+ const whitespaceMatch = tryWhitespaceNormalizedMatch(content, oldStr);
422
+ if (whitespaceMatch) {
423
+ const matchIndex = content.indexOf(whitespaceMatch);
424
+ return performReplace(content, whitespaceMatch, newStr, matchIndex);
425
+ }
426
+
427
+ // Strategy 5: Escape-normalized match
428
+ const escapeMatch = tryEscapeNormalizedMatch(content, oldStr);
429
+ if (escapeMatch) {
430
+ const matchIndex = content.indexOf(escapeMatch);
431
+ return performReplace(content, escapeMatch, newStr, matchIndex);
432
+ }
433
+
434
+ // Strategy 6: Indentation-flexible match
435
+ const indentMatch = tryIndentationFlexibleMatch(content, oldStr);
436
+ if (indentMatch) {
437
+ const matchIndex = content.indexOf(indentMatch);
438
+ return performReplace(content, indentMatch, newStr, matchIndex);
439
+ }
440
+
441
+ // All strategies failed
442
+ const truncatedOldStr =
443
+ oldStr.length > 200 ? `${oldStr.substring(0, 200)}...` : oldStr;
444
+
445
+ throw new Error(
446
+ `The string to be replaced was not found in the file. Please ensure the 'old_string' matches the file content exactly, including indentation and whitespace.\nTarget string (first 200 chars): ${truncatedOldStr}`,
447
+ );
448
+ }
449
+
450
+ export function applyEdits(
451
+ cwd: string,
452
+ filePath: string,
453
+ edits: Edit[],
454
+ ): { patch: any; updatedFile: string; startLineNumber: number } {
455
+ const fullFilePath = isAbsolute(filePath) ? filePath : resolve(cwd, filePath);
456
+
457
+ let fileContents = '';
458
+ try {
459
+ fileContents = readFileSync(fullFilePath, 'utf-8');
460
+ // Normalize line endings: CRLF → LF
461
+ fileContents = fileContents.replace(/\r\n/g, '\n');
462
+ } catch (error: any) {
463
+ if (
464
+ error.code === 'ENOENT' &&
465
+ edits.length === 1 &&
466
+ edits[0].old_string === ''
467
+ ) {
468
+ fileContents = '';
469
+ } else {
470
+ throw error;
471
+ }
472
+ }
473
+
474
+ let currentContent = fileContents;
475
+ const newStringsHistory: string[] = [];
476
+ let firstMatchLineNumber = 1;
477
+
478
+ for (const edit of edits) {
479
+ const { old_string, new_string, replace_all } = edit;
480
+
481
+ if (old_string === undefined || old_string === null) {
482
+ throw new Error(
483
+ `old_string is required and cannot be undefined or null when editing file: ${filePath}`,
484
+ );
485
+ }
486
+
487
+ const oldStrCheck = old_string.replace(/\n+$/, '');
488
+ for (const historyStr of newStringsHistory) {
489
+ if (oldStrCheck !== '' && historyStr.includes(oldStrCheck)) {
490
+ throw new Error(
491
+ `Cannot edit file: old_string is a substring of a new_string from a previous edit.\nOld string: ${old_string}`,
492
+ );
493
+ }
494
+ }
495
+
496
+ const previousContent = currentContent;
497
+
498
+ if (old_string === '') {
499
+ currentContent = new_string;
500
+ firstMatchLineNumber = 1;
501
+ } else {
502
+ const { result, matchIndex } = applyStringReplace(
503
+ currentContent,
504
+ old_string,
505
+ new_string,
506
+ replace_all,
507
+ );
508
+ currentContent = result;
509
+ if (firstMatchLineNumber === 1 && matchIndex >= 0) {
510
+ const textBeforeMatch = previousContent.substring(0, matchIndex);
511
+ firstMatchLineNumber = textBeforeMatch.split('\n').length;
512
+ }
513
+ }
514
+
515
+ if (currentContent === previousContent) {
516
+ if (old_string === new_string && old_string !== '') {
517
+ throw new Error(
518
+ 'No changes to make: old_string and new_string are exactly the same.',
519
+ );
520
+ }
521
+ throw new Error(
522
+ `String not found in file. Failed to apply edit.\nString: ${old_string}`,
523
+ );
524
+ }
525
+
526
+ newStringsHistory.push(new_string);
527
+ }
528
+
529
+ if (currentContent === fileContents && edits.length > 0) {
530
+ throw new Error(
531
+ 'Original and edited file match exactly. Failed to apply edit.',
532
+ );
533
+ }
534
+
535
+ const patch = Diff.structuredPatch(
536
+ filePath,
537
+ filePath,
538
+ fileContents,
539
+ currentContent,
540
+ );
541
+
542
+ return {
543
+ patch,
544
+ updatedFile: currentContent,
545
+ startLineNumber: firstMatchLineNumber,
546
+ };
547
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { getCommandRoot, shouldRunInBackground } from './background-detection';
3
+
4
+ describe('background-detection', () => {
5
+ test('should extract command root', () => {
6
+ expect(getCommandRoot('npm run dev')).toBe('npm');
7
+ expect(getCommandRoot('pnpm install')).toBe('pnpm');
8
+ expect(getCommandRoot('/usr/bin/node script.js')).toBe('node');
9
+ });
10
+
11
+ test('should return true when user explicitly requests background after threshold', () => {
12
+ expect(shouldRunInBackground('any command', 4000, true, false, true)).toBe(
13
+ true,
14
+ );
15
+ });
16
+
17
+ test('should return false when user requests background but before threshold', () => {
18
+ expect(shouldRunInBackground('any command', 1000, true, false, true)).toBe(
19
+ false,
20
+ );
21
+ });
22
+
23
+ test('should return false for short running commands', () => {
24
+ expect(shouldRunInBackground('npm run dev', 1000, true, false, false)).toBe(
25
+ false,
26
+ );
27
+ });
28
+
29
+ test('should return false without output', () => {
30
+ expect(
31
+ shouldRunInBackground('npm run dev', 3000, false, false, false),
32
+ ).toBe(false);
33
+ });
34
+
35
+ test('should return false for non-dev commands', () => {
36
+ expect(shouldRunInBackground('echo hello', 3000, true, false, false)).toBe(
37
+ false,
38
+ );
39
+ });
40
+
41
+ test('should return true for dev commands with output after 2s', () => {
42
+ expect(shouldRunInBackground('npm run dev', 4500, true, false, false)).toBe(
43
+ true,
44
+ );
45
+ expect(shouldRunInBackground('pnpm dev', 4500, true, false, false)).toBe(
46
+ true,
47
+ );
48
+ expect(shouldRunInBackground('yarn start', 4500, true, false, false)).toBe(
49
+ true,
50
+ );
51
+ });
52
+
53
+ test('should return false when command is completed regardless of other conditions', () => {
54
+ expect(shouldRunInBackground('npm run dev', 5000, true, true, false)).toBe(
55
+ false,
56
+ );
57
+ expect(shouldRunInBackground('any command', 5000, true, true, true)).toBe(
58
+ false,
59
+ );
60
+ });
61
+ });
@@ -0,0 +1,58 @@
1
+ import { BACKGROUND_THRESHOLD_MS } from '../core/constants';
2
+
3
+ const DEV_COMMANDS = [
4
+ 'npm',
5
+ 'pnpm',
6
+ 'yarn',
7
+ 'node',
8
+ 'python',
9
+ 'python3',
10
+ 'go',
11
+ 'cargo',
12
+ 'make',
13
+ 'docker',
14
+ 'webpack',
15
+ 'vite',
16
+ 'jest',
17
+ 'pytest',
18
+ ];
19
+
20
+ export function getCommandRoot(command: string): string | undefined {
21
+ return command
22
+ .trim()
23
+ .replace(/[{}()]/g, '')
24
+ .split(/[\s;&|]+/)[0]
25
+ ?.split(/[\/\\]/)
26
+ .pop();
27
+ }
28
+
29
+ export function shouldRunInBackground(
30
+ command: string,
31
+ elapsedMs: number,
32
+ hasOutput: boolean,
33
+ isCommandCompleted: boolean,
34
+ userRequested?: boolean,
35
+ ): boolean {
36
+ // If command is completed, never move to background
37
+ if (isCommandCompleted) {
38
+ return false;
39
+ }
40
+
41
+ // Basic condition checks
42
+ if (elapsedMs < BACKGROUND_THRESHOLD_MS || !hasOutput) {
43
+ return false;
44
+ }
45
+
46
+ // User explicitly requested background execution
47
+ if (userRequested) {
48
+ return true;
49
+ }
50
+
51
+ // Check if it's a development command
52
+ const commandRoot = getCommandRoot(command);
53
+ if (!commandRoot) {
54
+ return false;
55
+ }
56
+
57
+ return DEV_COMMANDS.includes(commandRoot.toLowerCase());
58
+ }
@@ -0,0 +1,26 @@
1
+ import dotenv from 'dotenv';
2
+ import fs from 'fs';
3
+ import path from 'pathe';
4
+
5
+ /**
6
+ * Load .env file from the specified directory
7
+ * Searches for .env files in the following order:
8
+ * 1. .env.local
9
+ * 2. .env
10
+ *
11
+ * @param cwd - Current working directory
12
+ */
13
+ export function loadEnvFile(cwd: string): void {
14
+ const envLocalPath = path.join(cwd, '.env.local');
15
+ const envPath = path.join(cwd, '.env');
16
+
17
+ // Load .env.local first (highest priority for local overrides)
18
+ if (fs.existsSync(envLocalPath)) {
19
+ dotenv.config({ path: envLocalPath });
20
+ }
21
+
22
+ // Load .env (will be overridden by .env.local if both exist)
23
+ if (fs.existsSync(envPath)) {
24
+ dotenv.config({ path: envPath });
25
+ }
26
+ }