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.
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/dist/agent/agent/agentManager.d.ts +38 -0
- package/dist/agent/agent/builtin/common.d.ts +5 -0
- package/dist/agent/agent/builtin/explore.d.ts +5 -0
- package/dist/agent/agent/builtin/general-purpose.d.ts +5 -0
- package/dist/agent/agent/builtin/index.d.ts +5 -0
- package/dist/agent/agent/executor.d.ts +2 -0
- package/dist/agent/agent/types.d.ts +98 -0
- package/dist/api/engine.d.ts +213 -0
- package/dist/communication/index.d.ts +4 -0
- package/dist/communication/messageBus.d.ts +71 -0
- package/dist/core/at.d.ts +26 -0
- package/dist/core/backgroundTaskManager.d.ts +27 -0
- package/dist/core/compact.d.ts +9 -0
- package/dist/core/config.d.ts +103 -0
- package/dist/core/constants.d.ts +32 -0
- package/dist/core/context.d.ts +57 -0
- package/dist/core/globalData.d.ts +21 -0
- package/dist/core/history.d.ts +24 -0
- package/dist/core/ide.d.ts +103 -0
- package/dist/core/jsonl.d.ts +37 -0
- package/dist/core/llmsContext.d.ts +14 -0
- package/dist/core/loop.d.ts +82 -0
- package/dist/core/message.d.ts +132 -0
- package/dist/core/model.d.ts +79 -0
- package/dist/core/output-style/builtin/default.d.ts +2 -0
- package/dist/core/output-style/builtin/explanatory.d.ts +2 -0
- package/dist/core/output-style/builtin/index.d.ts +6 -0
- package/dist/core/output-style/builtin/miao.d.ts +2 -0
- package/dist/core/output-style/builtin/minimal.d.ts +2 -0
- package/dist/core/output-style/types.d.ts +6 -0
- package/dist/core/outputFormat.d.ts +29 -0
- package/dist/core/outputStyle.d.ts +43 -0
- package/dist/core/paths.d.ts +20 -0
- package/dist/core/planSystemPrompt.d.ts +5 -0
- package/dist/core/plugin.d.ts +138 -0
- package/dist/core/project.d.ts +64 -0
- package/dist/core/promptCache.d.ts +3 -0
- package/dist/core/query.d.ts +14 -0
- package/dist/core/rules.d.ts +8 -0
- package/dist/core/systemPrompt.d.ts +9 -0
- package/dist/core/thinking-config.d.ts +3 -0
- package/dist/core/usage.d.ts +14 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +144432 -0
- package/dist/mcp/mcp.d.ts +49 -0
- package/dist/modes/builtin.d.ts +34 -0
- package/dist/modes/index.d.ts +8 -0
- package/dist/modes/registry.d.ts +18 -0
- package/dist/modes/types.d.ts +51 -0
- package/dist/platform/index.d.ts +5 -0
- package/dist/platform/node.d.ts +28 -0
- package/dist/platform/types.d.ts +41 -0
- package/dist/session/session.d.ts +43 -0
- package/dist/skill/skill.d.ts +79 -0
- package/dist/tools/tool.d.ts +119 -0
- package/dist/tools/tools/askUserQuestion.d.ts +48 -0
- package/dist/tools/tools/bash.d.ts +43 -0
- package/dist/tools/tools/edit.d.ts +9 -0
- package/dist/tools/tools/fetch.d.ts +9 -0
- package/dist/tools/tools/glob.d.ts +7 -0
- package/dist/tools/tools/grep.d.ts +22 -0
- package/dist/tools/tools/ls.d.ts +6 -0
- package/dist/tools/tools/read.d.ts +9 -0
- package/dist/tools/tools/skill.d.ts +7 -0
- package/dist/tools/tools/task.d.ts +14 -0
- package/dist/tools/tools/todo.d.ts +37 -0
- package/dist/tools/tools/write.d.ts +7 -0
- package/dist/utils/apiKeyRotation.d.ts +2 -0
- package/dist/utils/applyEdit.d.ts +17 -0
- package/dist/utils/background-detection.d.ts +2 -0
- package/dist/utils/dotenv.d.ts +9 -0
- package/dist/utils/env.d.ts +6 -0
- package/dist/utils/error.d.ts +11 -0
- package/dist/utils/execFileNoThrow.d.ts +8 -0
- package/dist/utils/files.d.ts +10 -0
- package/dist/utils/git.d.ts +163 -0
- package/dist/utils/ide.d.ts +27 -0
- package/dist/utils/ignore.d.ts +6 -0
- package/dist/utils/isLocal.d.ts +1 -0
- package/dist/utils/language.d.ts +9 -0
- package/dist/utils/list.d.ts +20 -0
- package/dist/utils/mergeSystemMessagesMiddleware.d.ts +2 -0
- package/dist/utils/messageNormalization.d.ts +22 -0
- package/dist/utils/path.d.ts +34 -0
- package/dist/utils/prependSystemMessageMiddleware.d.ts +2 -0
- package/dist/utils/project.d.ts +1 -0
- package/dist/utils/proxy.d.ts +18 -0
- package/dist/utils/randomUUID.d.ts +5 -0
- package/dist/utils/renderSessionMarkdown.d.ts +10 -0
- package/dist/utils/ripgrep.d.ts +16 -0
- package/dist/utils/safeFrontMatter.d.ts +11 -0
- package/dist/utils/safeParseJson.d.ts +1 -0
- package/dist/utils/safeStringify.d.ts +1 -0
- package/dist/utils/sanitizeAIResponse.d.ts +30 -0
- package/dist/utils/setTerminalTitle.d.ts +1 -0
- package/dist/utils/shell-execution.d.ts +44 -0
- package/dist/utils/string.d.ts +8 -0
- package/dist/utils/symbols.d.ts +14 -0
- package/dist/utils/system-encoding.d.ts +40 -0
- package/dist/utils/tokenCounter.d.ts +8 -0
- package/dist/utils/username.d.ts +1 -0
- package/package.json +106 -0
- package/src/agent/agent/agentManager.test.ts +124 -0
- package/src/agent/agent/agentManager.ts +372 -0
- package/src/agent/agent/builtin/common.ts +20 -0
- package/src/agent/agent/builtin/explore.ts +53 -0
- package/src/agent/agent/builtin/general-purpose.ts +38 -0
- package/src/agent/agent/builtin/index.ts +13 -0
- package/src/agent/agent/executor.test.ts +339 -0
- package/src/agent/agent/executor.ts +224 -0
- package/src/agent/agent/types.ts +119 -0
- package/src/api/engine.ts +466 -0
- package/src/communication/index.ts +18 -0
- package/src/communication/messageBus.ts +393 -0
- package/src/core/at.ts +315 -0
- package/src/core/backgroundTaskManager.ts +129 -0
- package/src/core/compact.ts +95 -0
- package/src/core/config.ts +441 -0
- package/src/core/constants.ts +82 -0
- package/src/core/context.ts +214 -0
- package/src/core/globalData.ts +77 -0
- package/src/core/history.ts +323 -0
- package/src/core/ide.ts +325 -0
- package/src/core/jsonl.ts +100 -0
- package/src/core/llmsContext.ts +117 -0
- package/src/core/loop.ts +638 -0
- package/src/core/message.ts +304 -0
- package/src/core/model.ts +2198 -0
- package/src/core/output-style/builtin/default.ts +9 -0
- package/src/core/output-style/builtin/explanatory.ts +22 -0
- package/src/core/output-style/builtin/index.ts +19 -0
- package/src/core/output-style/builtin/miao.ts +22 -0
- package/src/core/output-style/builtin/minimal.ts +8 -0
- package/src/core/output-style/types.ts +6 -0
- package/src/core/outputFormat.ts +93 -0
- package/src/core/outputStyle.ts +255 -0
- package/src/core/paths.ts +161 -0
- package/src/core/planSystemPrompt.ts +46 -0
- package/src/core/plugin.ts +299 -0
- package/src/core/project.ts +492 -0
- package/src/core/promptCache.ts +32 -0
- package/src/core/query.ts +46 -0
- package/src/core/rules.ts +56 -0
- package/src/core/systemPrompt.ts +176 -0
- package/src/core/thinking-config.ts +98 -0
- package/src/core/usage.ts +68 -0
- package/src/index.ts +39 -0
- package/src/mcp/mcp.ts +637 -0
- package/src/modes/builtin.ts +305 -0
- package/src/modes/index.ts +22 -0
- package/src/modes/registry.ts +39 -0
- package/src/modes/types.ts +56 -0
- package/src/platform/index.ts +6 -0
- package/src/platform/node.ts +108 -0
- package/src/platform/types.ts +54 -0
- package/src/plugins/index.ts +15 -0
- package/src/session/session.ts +187 -0
- package/src/skill/skill.ts +702 -0
- package/src/tools/tool.ts +378 -0
- package/src/tools/tools/askUserQuestion.ts +134 -0
- package/src/tools/tools/bash.test.ts +425 -0
- package/src/tools/tools/bash.ts +999 -0
- package/src/tools/tools/edit.ts +86 -0
- package/src/tools/tools/fetch.ts +129 -0
- package/src/tools/tools/glob.ts +69 -0
- package/src/tools/tools/grep.test.ts +194 -0
- package/src/tools/tools/grep.ts +358 -0
- package/src/tools/tools/ls.ts +51 -0
- package/src/tools/tools/read.test.ts +169 -0
- package/src/tools/tools/read.ts +284 -0
- package/src/tools/tools/skill.ts +73 -0
- package/src/tools/tools/task.test.ts +262 -0
- package/src/tools/tools/task.ts +284 -0
- package/src/tools/tools/todo.ts +269 -0
- package/src/tools/tools/write.ts +71 -0
- package/src/types.d.ts +18 -0
- package/src/utils/apiKeyRotation.test.ts +70 -0
- package/src/utils/apiKeyRotation.ts +24 -0
- package/src/utils/applyEdit.test.ts +388 -0
- package/src/utils/applyEdit.ts +547 -0
- package/src/utils/background-detection.test.ts +61 -0
- package/src/utils/background-detection.ts +58 -0
- package/src/utils/dotenv.ts +26 -0
- package/src/utils/env.ts +90 -0
- package/src/utils/error.ts +38 -0
- package/src/utils/execFileNoThrow.ts +49 -0
- package/src/utils/files.ts +93 -0
- package/src/utils/git.ts +1152 -0
- package/src/utils/ide.ts +279 -0
- package/src/utils/ignore.ts +275 -0
- package/src/utils/isLocal.ts +6 -0
- package/src/utils/language.ts +33 -0
- package/src/utils/list.ts +200 -0
- package/src/utils/mergeSystemMessagesMiddleware.ts +32 -0
- package/src/utils/messageNormalization.test.ts +401 -0
- package/src/utils/messageNormalization.ts +168 -0
- package/src/utils/path.ts +98 -0
- package/src/utils/prependSystemMessageMiddleware.ts +16 -0
- package/src/utils/project.ts +32 -0
- package/src/utils/proxy.ts +102 -0
- package/src/utils/randomUUID.ts +11 -0
- package/src/utils/renderSessionMarkdown.ts +175 -0
- package/src/utils/ripgrep.ts +189 -0
- package/src/utils/safeFrontMatter.test.ts +118 -0
- package/src/utils/safeFrontMatter.ts +68 -0
- package/src/utils/safeParseJson.ts +7 -0
- package/src/utils/safeStringify.ts +10 -0
- package/src/utils/sanitizeAIResponse.test.ts +135 -0
- package/src/utils/sanitizeAIResponse.ts +55 -0
- package/src/utils/setTerminalTitle.ts +7 -0
- package/src/utils/shell-execution.test.ts +237 -0
- package/src/utils/shell-execution.ts +279 -0
- package/src/utils/string.ts +13 -0
- package/src/utils/symbols.ts +18 -0
- package/src/utils/system-encoding.test.ts +164 -0
- package/src/utils/system-encoding.ts +296 -0
- package/src/utils/tokenCounter.test.ts +38 -0
- package/src/utils/tokenCounter.ts +19 -0
- 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
|
+
}
|