@agi-cli/sdk 0.1.77 → 0.1.79
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/package.json +1 -1
- package/src/core/src/tools/builtin/fs/write.txt +4 -2
- package/src/core/src/tools/builtin/patch.ts +700 -264
- package/src/core/src/tools/builtin/patch.txt +103 -6
- package/src/prompts/src/providers/anthropic.txt +15 -6
- package/src/prompts/src/providers/default.txt +17 -8
- package/src/prompts/src/providers/google.txt +15 -6
- package/src/prompts/src/providers/openai.txt +17 -7
package/package.json
CHANGED
|
@@ -4,5 +4,7 @@
|
|
|
4
4
|
- Returns a compact patch artifact summarizing the change
|
|
5
5
|
|
|
6
6
|
Usage tips:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
7
|
+
- Only use for creating new files or completely replacing file content
|
|
8
|
+
- NEVER use for partial/targeted edits - use apply_patch or edit instead
|
|
9
|
+
- Using write for partial edits wastes output tokens and risks hallucinating unchanged parts
|
|
10
|
+
- Prefer idempotent writes by providing the full intended content when you do use write
|
|
@@ -1,184 +1,688 @@
|
|
|
1
1
|
import { tool, type Tool } from 'ai';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
|
4
|
+
import { dirname, resolve, relative, isAbsolute } from 'node:path';
|
|
6
5
|
import DESCRIPTION from './patch.txt' with { type: 'text' };
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
interface PatchAddOperation {
|
|
8
|
+
kind: 'add';
|
|
9
|
+
filePath: string;
|
|
10
|
+
lines: string[];
|
|
11
|
+
}
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
13
|
+
interface PatchDeleteOperation {
|
|
14
|
+
kind: 'delete';
|
|
15
|
+
filePath: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PatchUpdateOperation {
|
|
19
|
+
kind: 'update';
|
|
20
|
+
filePath: string;
|
|
21
|
+
hunks: PatchHunk[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ParsedPatchOperation =
|
|
25
|
+
| PatchAddOperation
|
|
26
|
+
| PatchDeleteOperation
|
|
27
|
+
| PatchUpdateOperation;
|
|
28
|
+
|
|
29
|
+
type PatchLineKind = 'context' | 'add' | 'remove';
|
|
30
|
+
|
|
31
|
+
interface PatchHunkLine {
|
|
32
|
+
kind: PatchLineKind;
|
|
33
|
+
content: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PatchHunkHeader {
|
|
37
|
+
oldStart?: number;
|
|
38
|
+
oldLines?: number;
|
|
39
|
+
newStart?: number;
|
|
40
|
+
newLines?: number;
|
|
41
|
+
context?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface PatchHunk {
|
|
45
|
+
header: PatchHunkHeader;
|
|
46
|
+
lines: PatchHunkLine[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface PatchStats {
|
|
50
|
+
additions: number;
|
|
51
|
+
deletions: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface AppliedHunkResult {
|
|
55
|
+
header: PatchHunkHeader;
|
|
56
|
+
lines: PatchHunkLine[];
|
|
57
|
+
oldStart: number;
|
|
58
|
+
oldLines: number;
|
|
59
|
+
newStart: number;
|
|
60
|
+
newLines: number;
|
|
61
|
+
additions: number;
|
|
62
|
+
deletions: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface AppliedOperationRecord {
|
|
66
|
+
kind: 'add' | 'delete' | 'update';
|
|
67
|
+
filePath: string;
|
|
68
|
+
operation: ParsedPatchOperation;
|
|
69
|
+
stats: PatchStats;
|
|
70
|
+
hunks: AppliedHunkResult[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const PATCH_BEGIN_MARKER = '*** Begin Patch';
|
|
74
|
+
const PATCH_END_MARKER = '*** End Patch';
|
|
75
|
+
const PATCH_ADD_PREFIX = '*** Add File:';
|
|
76
|
+
const PATCH_UPDATE_PREFIX = '*** Update File:';
|
|
77
|
+
const PATCH_DELETE_PREFIX = '*** Delete File:';
|
|
78
|
+
|
|
79
|
+
function isErrnoException(value: unknown): value is NodeJS.ErrnoException {
|
|
80
|
+
return (
|
|
81
|
+
value instanceof Error &&
|
|
82
|
+
typeof (value as NodeJS.ErrnoException).code === 'string'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function splitLines(value: string): { lines: string[]; newline: string } {
|
|
87
|
+
const newline = value.includes('\r\n') ? '\r\n' : '\n';
|
|
88
|
+
const normalized = newline === '\n' ? value : value.replace(/\r\n/g, '\n');
|
|
89
|
+
const parts = normalized.split('\n');
|
|
90
|
+
|
|
91
|
+
if (parts.length > 0 && parts[parts.length - 1] === '') {
|
|
92
|
+
parts.pop();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { lines: parts, newline };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function joinLines(lines: string[], newline: string): string {
|
|
99
|
+
const base = lines.join('\n');
|
|
100
|
+
return newline === '\n' ? base : base.replace(/\n/g, newline);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ensureTrailingNewline(lines: string[]) {
|
|
104
|
+
if (lines.length === 0 || lines[lines.length - 1] !== '') {
|
|
105
|
+
lines.push('');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function findSubsequence(
|
|
110
|
+
lines: string[],
|
|
111
|
+
pattern: string[],
|
|
112
|
+
startIndex: number,
|
|
113
|
+
): number {
|
|
114
|
+
if (pattern.length === 0) return -1;
|
|
115
|
+
const start = Math.max(0, startIndex);
|
|
116
|
+
for (let i = start; i <= lines.length - pattern.length; i++) {
|
|
117
|
+
let matches = true;
|
|
118
|
+
for (let j = 0; j < pattern.length; j++) {
|
|
119
|
+
if (lines[i + j] !== pattern[j]) {
|
|
120
|
+
matches = false;
|
|
121
|
+
break;
|
|
32
122
|
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
123
|
+
}
|
|
124
|
+
if (matches) return i;
|
|
125
|
+
}
|
|
126
|
+
return -1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseDirectivePath(line: string, prefix: string): string {
|
|
130
|
+
const filePath = line.slice(prefix.length).trim();
|
|
131
|
+
if (!filePath) {
|
|
132
|
+
throw new Error(`Missing file path for directive: ${line}`);
|
|
133
|
+
}
|
|
134
|
+
if (filePath.startsWith('/') || isAbsolute(filePath)) {
|
|
135
|
+
throw new Error('Patch file paths must be relative to the project root.');
|
|
136
|
+
}
|
|
137
|
+
return filePath;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseHunkHeader(raw: string): PatchHunkHeader {
|
|
141
|
+
const numericMatch = raw.match(
|
|
142
|
+
/^@@\s*-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s*@@(?:\s*(.*))?$/,
|
|
143
|
+
);
|
|
144
|
+
if (numericMatch) {
|
|
145
|
+
const [, oldStart, oldCount, newStart, newCount, context] = numericMatch;
|
|
146
|
+
return {
|
|
147
|
+
oldStart: Number.parseInt(oldStart, 10),
|
|
148
|
+
oldLines: oldCount ? Number.parseInt(oldCount, 10) : undefined,
|
|
149
|
+
newStart: Number.parseInt(newStart, 10),
|
|
150
|
+
newLines: newCount ? Number.parseInt(newCount, 10) : undefined,
|
|
151
|
+
context: context?.trim() || undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const context = raw.replace(/^@@/, '').trim();
|
|
156
|
+
return context ? { context } : {};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseEnvelopedPatch(patch: string): ParsedPatchOperation[] {
|
|
160
|
+
const normalized = patch.replace(/\r\n/g, '\n');
|
|
161
|
+
const lines = normalized.split('\n');
|
|
162
|
+
const operations: ParsedPatchOperation[] = [];
|
|
163
|
+
|
|
164
|
+
type Builder =
|
|
165
|
+
| (PatchAddOperation & { kind: 'add' })
|
|
166
|
+
| (PatchDeleteOperation & { kind: 'delete' })
|
|
167
|
+
| (PatchUpdateOperation & {
|
|
168
|
+
kind: 'update';
|
|
169
|
+
currentHunk: PatchHunk | null;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
let builder: Builder | null = null;
|
|
173
|
+
let insidePatch = false;
|
|
174
|
+
let encounteredEnd = false;
|
|
175
|
+
|
|
176
|
+
const flushBuilder = () => {
|
|
177
|
+
if (!builder) return;
|
|
178
|
+
if (builder.kind === 'update') {
|
|
179
|
+
if (builder.currentHunk && builder.currentHunk.lines.length === 0) {
|
|
180
|
+
builder.hunks.pop();
|
|
46
181
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
182
|
+
if (builder.hunks.length === 0) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Update for ${builder.filePath} does not contain any diff hunks.`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
operations.push({
|
|
188
|
+
kind: 'update',
|
|
189
|
+
filePath: builder.filePath,
|
|
190
|
+
hunks: builder.hunks.map((hunk) => ({
|
|
191
|
+
header: { ...hunk.header },
|
|
192
|
+
lines: hunk.lines.map((line) => ({ ...line })),
|
|
193
|
+
})),
|
|
194
|
+
});
|
|
195
|
+
} else if (builder.kind === 'add') {
|
|
196
|
+
operations.push({
|
|
197
|
+
kind: 'add',
|
|
198
|
+
filePath: builder.filePath,
|
|
199
|
+
lines: [...builder.lines],
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
operations.push({ kind: 'delete', filePath: builder.filePath });
|
|
203
|
+
}
|
|
204
|
+
builder = null;
|
|
205
|
+
};
|
|
56
206
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Try exact match first
|
|
75
|
-
if (existingContent.includes(oldText)) {
|
|
76
|
-
newContent = existingContent.replace(oldText, newText);
|
|
77
|
-
} else {
|
|
78
|
-
// Try normalizing whitespace for more flexible matching
|
|
79
|
-
const normalizeWhitespace = (s: string) =>
|
|
80
|
-
s.replace(/\s+/g, ' ').trim();
|
|
81
|
-
const normalizedOld = normalizeWhitespace(oldText);
|
|
82
|
-
const normalizedExisting = normalizeWhitespace(existingContent);
|
|
83
|
-
|
|
84
|
-
if (normalizedExisting.includes(normalizedOld)) {
|
|
85
|
-
// Find the actual text in the file by matching normalized version
|
|
86
|
-
const lines = existingContent.split('\n');
|
|
87
|
-
let found = false;
|
|
88
|
-
|
|
89
|
-
for (let i = 0; i < lines.length; i++) {
|
|
90
|
-
const candidate = lines
|
|
91
|
-
.slice(i, i + oldLines.length)
|
|
92
|
-
.join('\n');
|
|
93
|
-
if (normalizeWhitespace(candidate) === normalizedOld) {
|
|
94
|
-
// Replace this section
|
|
95
|
-
const before = lines.slice(0, i).join('\n');
|
|
96
|
-
const after = lines.slice(i + oldLines.length).join('\n');
|
|
97
|
-
newContent =
|
|
98
|
-
before +
|
|
99
|
-
(before ? '\n' : '') +
|
|
100
|
-
newText +
|
|
101
|
-
(after ? '\n' : '') +
|
|
102
|
-
after;
|
|
103
|
-
found = true;
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!found) {
|
|
109
|
-
const preview = oldText.substring(0, 100);
|
|
110
|
-
return {
|
|
111
|
-
ok: false,
|
|
112
|
-
error: `Cannot find exact location to replace in ${currentFile}. Looking for: "${preview}${oldText.length > 100 ? '...' : ''}"`,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
// Can't find even with normalized whitespace
|
|
117
|
-
const preview = oldText.substring(0, 100);
|
|
118
|
-
const filePreview = existingContent.substring(0, 200);
|
|
119
|
-
return {
|
|
120
|
-
ok: false,
|
|
121
|
-
error: `Cannot find content to replace in ${currentFile}.\nLooking for: "${preview}${oldText.length > 100 ? '...' : ''}"\nFile contains: "${filePreview}${existingContent.length > 200 ? '...' : ''}"`,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
} else if (newLines.length > 0) {
|
|
126
|
-
// Just appending new lines
|
|
127
|
-
newContent =
|
|
128
|
-
existingContent +
|
|
129
|
-
(existingContent.endsWith('\n') ? '' : '\n') +
|
|
130
|
-
newLines.join('\n');
|
|
131
|
-
}
|
|
207
|
+
for (let i = 0; i < lines.length; i++) {
|
|
208
|
+
const line = lines[i];
|
|
209
|
+
if (!insidePatch) {
|
|
210
|
+
if (line.trim() === '') continue;
|
|
211
|
+
if (line.startsWith(PATCH_BEGIN_MARKER)) {
|
|
212
|
+
insidePatch = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
throw new Error(
|
|
216
|
+
'Patch must start with "*** Begin Patch" and use the enveloped patch format.',
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (line.startsWith(PATCH_BEGIN_MARKER)) {
|
|
221
|
+
throw new Error('Nested "*** Begin Patch" markers are not supported.');
|
|
222
|
+
}
|
|
132
223
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
224
|
+
if (line.startsWith(PATCH_END_MARKER)) {
|
|
225
|
+
flushBuilder();
|
|
226
|
+
encounteredEnd = true;
|
|
227
|
+
const remaining = lines.slice(i + 1).find((rest) => rest.trim() !== '');
|
|
228
|
+
if (remaining) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
'Unexpected content found after "*** End Patch" marker.',
|
|
231
|
+
);
|
|
139
232
|
}
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (line.startsWith(PATCH_ADD_PREFIX)) {
|
|
237
|
+
flushBuilder();
|
|
238
|
+
builder = {
|
|
239
|
+
kind: 'add',
|
|
240
|
+
filePath: parseDirectivePath(line, PATCH_ADD_PREFIX),
|
|
241
|
+
lines: [],
|
|
242
|
+
};
|
|
243
|
+
continue;
|
|
140
244
|
}
|
|
141
|
-
return { ok: true };
|
|
142
|
-
}
|
|
143
245
|
|
|
144
|
-
|
|
145
|
-
|
|
246
|
+
if (line.startsWith(PATCH_UPDATE_PREFIX)) {
|
|
247
|
+
flushBuilder();
|
|
248
|
+
builder = {
|
|
249
|
+
kind: 'update',
|
|
250
|
+
filePath: parseDirectivePath(line, PATCH_UPDATE_PREFIX),
|
|
251
|
+
hunks: [],
|
|
252
|
+
currentHunk: null,
|
|
253
|
+
};
|
|
146
254
|
continue;
|
|
147
255
|
}
|
|
148
256
|
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (line.
|
|
160
|
-
|
|
161
|
-
operation = 'add';
|
|
162
|
-
} else if (line.startsWith('*** Update File:')) {
|
|
163
|
-
currentFile = line.replace('*** Update File:', '').trim();
|
|
164
|
-
operation = 'update';
|
|
165
|
-
} else if (line.startsWith('*** Delete File:')) {
|
|
166
|
-
currentFile = line.replace('*** Delete File:', '').trim();
|
|
167
|
-
operation = 'delete';
|
|
257
|
+
if (line.startsWith(PATCH_DELETE_PREFIX)) {
|
|
258
|
+
flushBuilder();
|
|
259
|
+
builder = {
|
|
260
|
+
kind: 'delete',
|
|
261
|
+
filePath: parseDirectivePath(line, PATCH_DELETE_PREFIX),
|
|
262
|
+
};
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!builder) {
|
|
267
|
+
if (line.trim() === '') {
|
|
268
|
+
continue;
|
|
168
269
|
}
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
270
|
+
throw new Error(`Unexpected content in patch: "${line}"`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (builder.kind === 'add') {
|
|
274
|
+
const content = line.startsWith('+') ? line.slice(1) : line;
|
|
275
|
+
builder.lines.push(content);
|
|
276
|
+
continue;
|
|
176
277
|
}
|
|
278
|
+
|
|
279
|
+
if (builder.kind === 'delete') {
|
|
280
|
+
if (line.trim() !== '') {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`Delete directive for ${builder.filePath} should not contain additional lines.`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (line.startsWith('@@')) {
|
|
289
|
+
const hunk: PatchHunk = { header: parseHunkHeader(line), lines: [] };
|
|
290
|
+
builder.hunks.push(hunk);
|
|
291
|
+
builder.currentHunk = hunk;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!builder.currentHunk) {
|
|
296
|
+
const fallbackHunk: PatchHunk = { header: {}, lines: [] };
|
|
297
|
+
builder.hunks.push(fallbackHunk);
|
|
298
|
+
builder.currentHunk = fallbackHunk;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const currentHunk = builder.currentHunk;
|
|
302
|
+
const prefix = line[0];
|
|
303
|
+
if (prefix === '+') {
|
|
304
|
+
currentHunk.lines.push({ kind: 'add', content: line.slice(1) });
|
|
305
|
+
} else if (prefix === '-') {
|
|
306
|
+
currentHunk.lines.push({ kind: 'remove', content: line.slice(1) });
|
|
307
|
+
} else if (prefix === ' ') {
|
|
308
|
+
currentHunk.lines.push({ kind: 'context', content: line.slice(1) });
|
|
309
|
+
} else {
|
|
310
|
+
throw new Error(`Unrecognized patch line: "${line}"`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!encounteredEnd) {
|
|
315
|
+
throw new Error('Missing "*** End Patch" marker.');
|
|
177
316
|
}
|
|
178
317
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
318
|
+
return operations;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function resolveProjectPath(projectRoot: string, filePath: string): string {
|
|
322
|
+
const fullPath = resolve(projectRoot, filePath);
|
|
323
|
+
const relativePath = relative(projectRoot, fullPath);
|
|
324
|
+
if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
|
|
325
|
+
throw new Error(`Patch path escapes project root: ${filePath}`);
|
|
326
|
+
}
|
|
327
|
+
return fullPath;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function applyAddOperation(
|
|
331
|
+
projectRoot: string,
|
|
332
|
+
operation: PatchAddOperation,
|
|
333
|
+
): Promise<AppliedOperationRecord> {
|
|
334
|
+
const targetPath = resolveProjectPath(projectRoot, operation.filePath);
|
|
335
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
336
|
+
const linesForWrite = [...operation.lines];
|
|
337
|
+
ensureTrailingNewline(linesForWrite);
|
|
338
|
+
await writeFile(targetPath, joinLines(linesForWrite, '\n'), 'utf-8');
|
|
339
|
+
|
|
340
|
+
const hunkLines: PatchHunkLine[] = operation.lines.map((line) => ({
|
|
341
|
+
kind: 'add',
|
|
342
|
+
content: line,
|
|
343
|
+
}));
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
kind: 'add',
|
|
347
|
+
filePath: operation.filePath,
|
|
348
|
+
operation,
|
|
349
|
+
stats: {
|
|
350
|
+
additions: hunkLines.length,
|
|
351
|
+
deletions: 0,
|
|
352
|
+
},
|
|
353
|
+
hunks: [
|
|
354
|
+
{
|
|
355
|
+
header: {},
|
|
356
|
+
lines: hunkLines,
|
|
357
|
+
oldStart: 0,
|
|
358
|
+
oldLines: 0,
|
|
359
|
+
newStart: 1,
|
|
360
|
+
newLines: hunkLines.length,
|
|
361
|
+
additions: hunkLines.length,
|
|
362
|
+
deletions: 0,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function applyDeleteOperation(
|
|
369
|
+
projectRoot: string,
|
|
370
|
+
operation: PatchDeleteOperation,
|
|
371
|
+
): Promise<AppliedOperationRecord> {
|
|
372
|
+
const targetPath = resolveProjectPath(projectRoot, operation.filePath);
|
|
373
|
+
let existingContent = '';
|
|
374
|
+
try {
|
|
375
|
+
existingContent = await readFile(targetPath, 'utf-8');
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (isErrnoException(error) && error.code === 'ENOENT') {
|
|
378
|
+
throw new Error(`File not found for deletion: ${operation.filePath}`);
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const { lines } = splitLines(existingContent);
|
|
384
|
+
await unlink(targetPath);
|
|
385
|
+
|
|
386
|
+
const hunkLines: PatchHunkLine[] = lines.map((line) => ({
|
|
387
|
+
kind: 'remove',
|
|
388
|
+
content: line,
|
|
389
|
+
}));
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
kind: 'delete',
|
|
393
|
+
filePath: operation.filePath,
|
|
394
|
+
operation,
|
|
395
|
+
stats: {
|
|
396
|
+
additions: 0,
|
|
397
|
+
deletions: hunkLines.length,
|
|
398
|
+
},
|
|
399
|
+
hunks: [
|
|
400
|
+
{
|
|
401
|
+
header: {},
|
|
402
|
+
lines: hunkLines,
|
|
403
|
+
oldStart: 1,
|
|
404
|
+
oldLines: hunkLines.length,
|
|
405
|
+
newStart: 0,
|
|
406
|
+
newLines: 0,
|
|
407
|
+
additions: 0,
|
|
408
|
+
deletions: hunkLines.length,
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function applyHunksToLines(
|
|
415
|
+
originalLines: string[],
|
|
416
|
+
hunks: PatchHunk[],
|
|
417
|
+
filePath: string,
|
|
418
|
+
): { lines: string[]; applied: AppliedHunkResult[] } {
|
|
419
|
+
const lines = [...originalLines];
|
|
420
|
+
let searchIndex = 0;
|
|
421
|
+
let lineOffset = 0;
|
|
422
|
+
const applied: AppliedHunkResult[] = [];
|
|
423
|
+
|
|
424
|
+
for (const hunk of hunks) {
|
|
425
|
+
const expected: string[] = [];
|
|
426
|
+
const replacement: string[] = [];
|
|
427
|
+
let additions = 0;
|
|
428
|
+
let deletions = 0;
|
|
429
|
+
|
|
430
|
+
for (const line of hunk.lines) {
|
|
431
|
+
if (line.kind !== 'add') {
|
|
432
|
+
expected.push(line.content);
|
|
433
|
+
}
|
|
434
|
+
if (line.kind !== 'remove') {
|
|
435
|
+
replacement.push(line.content);
|
|
436
|
+
}
|
|
437
|
+
if (line.kind === 'add') additions += 1;
|
|
438
|
+
if (line.kind === 'remove') deletions += 1;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const hasExpected = expected.length > 0;
|
|
442
|
+
const hint =
|
|
443
|
+
typeof hunk.header.oldStart === 'number'
|
|
444
|
+
? Math.max(0, hunk.header.oldStart - 1 + lineOffset)
|
|
445
|
+
: searchIndex;
|
|
446
|
+
|
|
447
|
+
let matchIndex = hasExpected
|
|
448
|
+
? findSubsequence(lines, expected, Math.max(0, hint - 3))
|
|
449
|
+
: -1;
|
|
450
|
+
|
|
451
|
+
if (hasExpected && matchIndex === -1) {
|
|
452
|
+
matchIndex = findSubsequence(lines, expected, 0);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (matchIndex === -1 && hasExpected && hunk.header.context) {
|
|
456
|
+
const contextIndex = findSubsequence(lines, [hunk.header.context], 0);
|
|
457
|
+
if (contextIndex !== -1) {
|
|
458
|
+
const positionInExpected = expected.indexOf(hunk.header.context);
|
|
459
|
+
matchIndex =
|
|
460
|
+
positionInExpected >= 0
|
|
461
|
+
? Math.max(0, contextIndex - positionInExpected)
|
|
462
|
+
: contextIndex;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!hasExpected) {
|
|
467
|
+
matchIndex = computeInsertionIndex(lines, hint, hunk.header);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (matchIndex === -1) {
|
|
471
|
+
const contextInfo = hunk.header.context
|
|
472
|
+
? ` near context '${hunk.header.context}'`
|
|
473
|
+
: '';
|
|
474
|
+
throw new Error(
|
|
475
|
+
`Failed to apply patch hunk in ${filePath}${contextInfo}.`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const deleteCount = hasExpected ? expected.length : 0;
|
|
480
|
+
const originalIndex = matchIndex - lineOffset;
|
|
481
|
+
const normalizedOriginalIndex = Math.min(
|
|
482
|
+
Math.max(0, originalIndex),
|
|
483
|
+
originalLines.length,
|
|
484
|
+
);
|
|
485
|
+
const oldStart = normalizedOriginalIndex + 1;
|
|
486
|
+
const newStart = matchIndex + 1;
|
|
487
|
+
|
|
488
|
+
lines.splice(matchIndex, deleteCount, ...replacement);
|
|
489
|
+
searchIndex = matchIndex + replacement.length;
|
|
490
|
+
lineOffset += replacement.length - deleteCount;
|
|
491
|
+
|
|
492
|
+
applied.push({
|
|
493
|
+
header: { ...hunk.header },
|
|
494
|
+
lines: hunk.lines.map((line) => ({ ...line })),
|
|
495
|
+
oldStart,
|
|
496
|
+
oldLines: deleteCount,
|
|
497
|
+
newStart,
|
|
498
|
+
newLines: replacement.length,
|
|
499
|
+
additions,
|
|
500
|
+
deletions,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return { lines, applied };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function computeInsertionIndex(
|
|
508
|
+
lines: string[],
|
|
509
|
+
hint: number,
|
|
510
|
+
header: PatchHunkHeader,
|
|
511
|
+
): number {
|
|
512
|
+
if (header.context) {
|
|
513
|
+
const contextIndex = findSubsequence(lines, [header.context], 0);
|
|
514
|
+
if (contextIndex !== -1) {
|
|
515
|
+
return contextIndex + 1;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (typeof header.oldStart === 'number') {
|
|
520
|
+
const zeroBased = Math.max(0, header.oldStart - 1);
|
|
521
|
+
return Math.min(lines.length, zeroBased);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (typeof header.newStart === 'number') {
|
|
525
|
+
const zeroBased = Math.max(0, header.newStart - 1);
|
|
526
|
+
return Math.min(lines.length, zeroBased);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return Math.min(lines.length, Math.max(0, hint));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function applyUpdateOperation(
|
|
533
|
+
projectRoot: string,
|
|
534
|
+
operation: PatchUpdateOperation,
|
|
535
|
+
): Promise<AppliedOperationRecord> {
|
|
536
|
+
const targetPath = resolveProjectPath(projectRoot, operation.filePath);
|
|
537
|
+
let originalContent: string;
|
|
538
|
+
try {
|
|
539
|
+
originalContent = await readFile(targetPath, 'utf-8');
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (isErrnoException(error) && error.code === 'ENOENT') {
|
|
542
|
+
throw new Error(`File not found: ${operation.filePath}`);
|
|
543
|
+
}
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const { lines: originalLines, newline } = splitLines(originalContent);
|
|
548
|
+
const { lines: updatedLines, applied } = applyHunksToLines(
|
|
549
|
+
originalLines,
|
|
550
|
+
operation.hunks,
|
|
551
|
+
operation.filePath,
|
|
552
|
+
);
|
|
553
|
+
ensureTrailingNewline(updatedLines);
|
|
554
|
+
await writeFile(targetPath, joinLines(updatedLines, newline), 'utf-8');
|
|
555
|
+
|
|
556
|
+
const stats = applied.reduce<PatchStats>(
|
|
557
|
+
(acc, hunk) => ({
|
|
558
|
+
additions: acc.additions + hunk.additions,
|
|
559
|
+
deletions: acc.deletions + hunk.deletions,
|
|
560
|
+
}),
|
|
561
|
+
{ additions: 0, deletions: 0 },
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
kind: 'update',
|
|
566
|
+
filePath: operation.filePath,
|
|
567
|
+
operation,
|
|
568
|
+
stats,
|
|
569
|
+
hunks: applied,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function summarizeOperations(operations: AppliedOperationRecord[]) {
|
|
574
|
+
const summary = operations.reduce(
|
|
575
|
+
(acc, op) => ({
|
|
576
|
+
files: acc.files + 1,
|
|
577
|
+
additions: acc.additions + op.stats.additions,
|
|
578
|
+
deletions: acc.deletions + op.stats.deletions,
|
|
579
|
+
}),
|
|
580
|
+
{ files: 0, additions: 0, deletions: 0 },
|
|
581
|
+
);
|
|
582
|
+
return {
|
|
583
|
+
files: Math.max(summary.files, operations.length > 0 ? 1 : 0),
|
|
584
|
+
additions: summary.additions,
|
|
585
|
+
deletions: summary.deletions,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function formatRange(start: number, count: number) {
|
|
590
|
+
const normalizedStart = Math.max(0, start);
|
|
591
|
+
if (count === 0) return `${normalizedStart},0`;
|
|
592
|
+
if (count === 1) return `${normalizedStart}`;
|
|
593
|
+
return `${normalizedStart},${count}`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function formatHunkHeader(applied: AppliedHunkResult) {
|
|
597
|
+
const oldRange = formatRange(applied.oldStart, applied.oldLines);
|
|
598
|
+
const newRange = formatRange(applied.newStart, applied.newLines);
|
|
599
|
+
const context = applied.header.context?.trim();
|
|
600
|
+
return context
|
|
601
|
+
? `@@ -${oldRange} +${newRange} @@ ${context}`
|
|
602
|
+
: `@@ -${oldRange} +${newRange} @@`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function serializePatchLine(line: PatchHunkLine): string {
|
|
606
|
+
switch (line.kind) {
|
|
607
|
+
case 'add':
|
|
608
|
+
return `+${line.content}`;
|
|
609
|
+
case 'remove':
|
|
610
|
+
return `-${line.content}`;
|
|
611
|
+
default:
|
|
612
|
+
return ` ${line.content}`;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function formatNormalizedPatch(operations: AppliedOperationRecord[]): string {
|
|
617
|
+
const lines: string[] = [PATCH_BEGIN_MARKER];
|
|
618
|
+
|
|
619
|
+
for (const op of operations) {
|
|
620
|
+
switch (op.kind) {
|
|
621
|
+
case 'add': {
|
|
622
|
+
lines.push(`${PATCH_ADD_PREFIX} ${op.filePath}`);
|
|
623
|
+
for (const hunk of op.hunks) {
|
|
624
|
+
lines.push(formatHunkHeader(hunk));
|
|
625
|
+
for (const line of hunk.lines) {
|
|
626
|
+
lines.push(serializePatchLine(line));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case 'delete': {
|
|
632
|
+
lines.push(`${PATCH_DELETE_PREFIX} ${op.filePath}`);
|
|
633
|
+
for (const hunk of op.hunks) {
|
|
634
|
+
lines.push(formatHunkHeader(hunk));
|
|
635
|
+
for (const line of hunk.lines) {
|
|
636
|
+
lines.push(serializePatchLine(line));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
case 'update': {
|
|
642
|
+
lines.push(`${PATCH_UPDATE_PREFIX} ${op.filePath}`);
|
|
643
|
+
const updateOp = op.operation as PatchUpdateOperation;
|
|
644
|
+
for (let i = 0; i < updateOp.hunks.length; i++) {
|
|
645
|
+
const originalHunk = updateOp.hunks[i];
|
|
646
|
+
const appliedHunk = op.hunks[i];
|
|
647
|
+
const header: AppliedHunkResult = {
|
|
648
|
+
...appliedHunk,
|
|
649
|
+
header: {
|
|
650
|
+
...appliedHunk.header,
|
|
651
|
+
context: originalHunk.header.context,
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
lines.push(formatHunkHeader(header));
|
|
655
|
+
for (const line of originalHunk.lines) {
|
|
656
|
+
lines.push(serializePatchLine(line));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
lines.push(PATCH_END_MARKER);
|
|
665
|
+
return lines.join('\n');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function applyEnvelopedPatch(projectRoot: string, patch: string) {
|
|
669
|
+
const operations = parseEnvelopedPatch(patch);
|
|
670
|
+
const applied: AppliedOperationRecord[] = [];
|
|
671
|
+
|
|
672
|
+
for (const operation of operations) {
|
|
673
|
+
if (operation.kind === 'add') {
|
|
674
|
+
applied.push(await applyAddOperation(projectRoot, operation));
|
|
675
|
+
} else if (operation.kind === 'delete') {
|
|
676
|
+
applied.push(await applyDeleteOperation(projectRoot, operation));
|
|
677
|
+
} else {
|
|
678
|
+
applied.push(await applyUpdateOperation(projectRoot, operation));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
operations: applied,
|
|
684
|
+
normalizedPatch: formatNormalizedPatch(applied),
|
|
685
|
+
};
|
|
182
686
|
}
|
|
183
687
|
|
|
184
688
|
export function buildApplyPatchTool(projectRoot: string): {
|
|
@@ -197,114 +701,46 @@ export function buildApplyPatchTool(projectRoot: string): {
|
|
|
197
701
|
'Allow hunks to be rejected without failing the whole operation',
|
|
198
702
|
),
|
|
199
703
|
}),
|
|
200
|
-
async execute({
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const isEnveloped =
|
|
209
|
-
patch.includes('*** Begin Patch') ||
|
|
210
|
-
patch.includes('*** Add File:') ||
|
|
211
|
-
patch.includes('*** Update File:');
|
|
212
|
-
|
|
213
|
-
if (isEnveloped) {
|
|
214
|
-
// Handle enveloped patches directly
|
|
215
|
-
const result = await applyEnvelopedPatch(projectRoot, patch);
|
|
216
|
-
const summary = summarizePatch(patch);
|
|
217
|
-
if (result.ok) {
|
|
218
|
-
return {
|
|
219
|
-
ok: true,
|
|
220
|
-
output: 'Applied enveloped patch',
|
|
221
|
-
artifact: { kind: 'file_diff', patch, summary },
|
|
222
|
-
} as const;
|
|
223
|
-
} else {
|
|
224
|
-
return {
|
|
225
|
-
ok: false,
|
|
226
|
-
error: result.error || 'Failed to apply enveloped patch',
|
|
227
|
-
artifact: { kind: 'file_diff', patch, summary },
|
|
228
|
-
} as const;
|
|
229
|
-
}
|
|
704
|
+
async execute({ patch }: { patch: string; allowRejects?: boolean }) {
|
|
705
|
+
if (
|
|
706
|
+
!patch.includes(PATCH_BEGIN_MARKER) ||
|
|
707
|
+
!patch.includes(PATCH_END_MARKER)
|
|
708
|
+
) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
'Only enveloped patch format is supported. Patch must start with "*** Begin Patch" and contain "*** Add File:", "*** Update File:", or "*** Delete File:" directives.',
|
|
711
|
+
);
|
|
230
712
|
}
|
|
231
713
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
});
|
|
251
|
-
// Check if any files were actually modified
|
|
252
|
-
try {
|
|
253
|
-
const { stdout: statusOut } = await execAsync(
|
|
254
|
-
`git -C "${projectRoot}" status --porcelain`,
|
|
255
|
-
);
|
|
256
|
-
if (statusOut && statusOut.trim().length > 0) {
|
|
257
|
-
return {
|
|
258
|
-
ok: true,
|
|
259
|
-
output: stdout.trim(),
|
|
260
|
-
artifact: { kind: 'file_diff', patch, summary },
|
|
261
|
-
} as const;
|
|
262
|
-
}
|
|
263
|
-
} catch {}
|
|
264
|
-
} catch (error: unknown) {
|
|
265
|
-
const err = error as { stderr?: string; message?: string };
|
|
266
|
-
lastError = err.stderr || err.message || 'git apply failed';
|
|
267
|
-
}
|
|
268
|
-
}
|
|
714
|
+
const { operations, normalizedPatch } = await applyEnvelopedPatch(
|
|
715
|
+
projectRoot,
|
|
716
|
+
patch,
|
|
717
|
+
);
|
|
718
|
+
const summary = summarizeOperations(operations);
|
|
719
|
+
const changes = operations.map((operation) => ({
|
|
720
|
+
filePath: operation.filePath,
|
|
721
|
+
kind: operation.kind,
|
|
722
|
+
hunks: operation.hunks.map((hunk) => ({
|
|
723
|
+
oldStart: hunk.oldStart,
|
|
724
|
+
oldLines: hunk.oldLines,
|
|
725
|
+
newStart: hunk.newStart,
|
|
726
|
+
newLines: hunk.newLines,
|
|
727
|
+
additions: hunk.additions,
|
|
728
|
+
deletions: hunk.deletions,
|
|
729
|
+
context: hunk.header.context,
|
|
730
|
+
})),
|
|
731
|
+
}));
|
|
269
732
|
|
|
270
|
-
// Final check if files were modified anyway
|
|
271
|
-
try {
|
|
272
|
-
const { stdout: statusOut } = await execAsync(
|
|
273
|
-
`git -C "${projectRoot}" status --porcelain`,
|
|
274
|
-
);
|
|
275
|
-
if (statusOut && statusOut.trim().length > 0) {
|
|
276
|
-
return {
|
|
277
|
-
ok: true,
|
|
278
|
-
output: 'Patch applied with warnings',
|
|
279
|
-
artifact: { kind: 'file_diff', patch, summary },
|
|
280
|
-
} as const;
|
|
281
|
-
}
|
|
282
|
-
} catch {}
|
|
283
|
-
|
|
284
|
-
// If both attempts fail and no files changed, return error with more context
|
|
285
|
-
const errorDetails = lastError.includes('patch does not apply')
|
|
286
|
-
? 'The patch cannot be applied because the target content has changed or does not match. The file may have been modified since the patch was created.'
|
|
287
|
-
: lastError ||
|
|
288
|
-
'git apply failed (tried -p1 and -p0) — ensure paths match project root';
|
|
289
733
|
return {
|
|
290
|
-
ok:
|
|
291
|
-
|
|
292
|
-
|
|
734
|
+
ok: true,
|
|
735
|
+
output: 'Applied enveloped patch',
|
|
736
|
+
changes,
|
|
737
|
+
artifact: {
|
|
738
|
+
kind: 'file_diff',
|
|
739
|
+
patch: normalizedPatch,
|
|
740
|
+
summary,
|
|
741
|
+
},
|
|
293
742
|
} as const;
|
|
294
743
|
},
|
|
295
744
|
});
|
|
296
745
|
return { name: 'apply_patch', tool: applyPatch };
|
|
297
746
|
}
|
|
298
|
-
|
|
299
|
-
function summarizePatch(patch: string) {
|
|
300
|
-
const lines = String(patch || '').split('\n');
|
|
301
|
-
let files = 0;
|
|
302
|
-
let additions = 0;
|
|
303
|
-
let deletions = 0;
|
|
304
|
-
for (const l of lines) {
|
|
305
|
-
if (/^\*\*\*\s+(Add|Update|Delete) File:/.test(l)) files += 1;
|
|
306
|
-
else if (l.startsWith('+') && !l.startsWith('+++')) additions += 1;
|
|
307
|
-
else if (l.startsWith('-') && !l.startsWith('---')) deletions += 1;
|
|
308
|
-
}
|
|
309
|
-
return { files, additions, deletions };
|
|
310
|
-
}
|
|
@@ -1,7 +1,104 @@
|
|
|
1
|
-
|
|
2
|
-
- Uses `git apply` under the hood; tries `-p1` then `-p0`
|
|
3
|
-
- Returns an artifact summary and any output from `git apply`
|
|
1
|
+
Apply a patch to modify one or more files using the enveloped patch format.
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
**RECOMMENDED: Use apply_patch for targeted file edits to avoid rewriting entire files and wasting tokens.**
|
|
4
|
+
|
|
5
|
+
Use `apply_patch` only when:
|
|
6
|
+
- You want to make targeted edits to specific lines (primary use case)
|
|
7
|
+
- You want to make multiple related changes across different files in a single operation
|
|
8
|
+
- You need to add/delete entire files along with modifications
|
|
9
|
+
- You have JUST read the file and are confident the content hasn't changed
|
|
10
|
+
|
|
11
|
+
**IMPORTANT: Patches require EXACT line matches. If the file content has changed since you last read it, the patch will fail.**
|
|
12
|
+
|
|
13
|
+
**Alternative: Use the `edit` tool if you need fuzzy matching or structured operations.**
|
|
14
|
+
|
|
15
|
+
## Patch Format
|
|
16
|
+
|
|
17
|
+
All patches must be wrapped in markers and use explicit file directives:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
*** Begin Patch
|
|
21
|
+
*** Add File: path/to/file.txt
|
|
22
|
+
+line 1
|
|
23
|
+
+line 2
|
|
24
|
+
*** Update File: path/to/other.txt
|
|
25
|
+
-old line
|
|
26
|
+
+new line
|
|
27
|
+
*** Delete File: path/to/delete.txt
|
|
28
|
+
*** End Patch
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## File Operations
|
|
32
|
+
|
|
33
|
+
### Add a new file:
|
|
34
|
+
```
|
|
35
|
+
*** Begin Patch
|
|
36
|
+
*** Add File: src/hello.ts
|
|
37
|
+
+export function hello() {
|
|
38
|
+
+ console.log("Hello!");
|
|
39
|
+
+}
|
|
40
|
+
*** End Patch
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Update an existing file (simple replacement):
|
|
44
|
+
```
|
|
45
|
+
*** Begin Patch
|
|
46
|
+
*** Update File: src/config.ts
|
|
47
|
+
-const PORT = 3000;
|
|
48
|
+
+const PORT = 8080;
|
|
49
|
+
*** End Patch
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**CRITICAL**: The `-` lines must match EXACTLY what's in the file, character-for-character. If you're not 100% certain, use the `edit` tool instead.
|
|
53
|
+
|
|
54
|
+
### Update with context (recommended for precision):
|
|
55
|
+
```
|
|
56
|
+
*** Begin Patch
|
|
57
|
+
*** Update File: src/app.ts
|
|
58
|
+
@@ function main()
|
|
59
|
+
function main() {
|
|
60
|
+
- console.log("old");
|
|
61
|
+
+ console.log("new");
|
|
62
|
+
}
|
|
63
|
+
*** End Patch
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The `@@ context line` helps locate the exact position, but the `-` lines must still match exactly.
|
|
67
|
+
|
|
68
|
+
### Delete a file:
|
|
69
|
+
```
|
|
70
|
+
*** Begin Patch
|
|
71
|
+
*** Delete File: old/unused.ts
|
|
72
|
+
*** End Patch
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Multiple operations in one patch:
|
|
76
|
+
```
|
|
77
|
+
*** Begin Patch
|
|
78
|
+
*** Add File: new.txt
|
|
79
|
+
+New content
|
|
80
|
+
*** Update File: existing.txt
|
|
81
|
+
-old
|
|
82
|
+
+new
|
|
83
|
+
*** Delete File: obsolete.txt
|
|
84
|
+
*** End Patch
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Line Prefixes
|
|
88
|
+
|
|
89
|
+
- Lines starting with `+` are added
|
|
90
|
+
- Lines starting with `-` are removed
|
|
91
|
+
- Lines starting with ` ` (space) are context (kept unchanged)
|
|
92
|
+
- Lines starting with `@@` provide context for finding the location
|
|
93
|
+
|
|
94
|
+
## Common Errors
|
|
95
|
+
|
|
96
|
+
**"Failed to find expected lines"**: The file content doesn't match your patch. The file may have changed, or you may have mistyped the lines. Solution: Use the `edit` tool instead.
|
|
97
|
+
|
|
98
|
+
## Important Notes
|
|
99
|
+
|
|
100
|
+
- **Patches are fragile**: Any mismatch in whitespace, indentation, or content will cause failure
|
|
101
|
+
- **Use `edit` for reliability**: The `edit` tool can make targeted changes without requiring exact matches
|
|
102
|
+
- All file paths are relative to the project root
|
|
103
|
+
- The patch format does NOT support standard unified diff format (no `---`/`+++` headers)
|
|
104
|
+
- Always wrap patches with `*** Begin Patch` and `*** End Patch`
|
|
@@ -74,7 +74,7 @@ When making changes to files, first understand the file's code conventions. Mimi
|
|
|
74
74
|
- IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked
|
|
75
75
|
|
|
76
76
|
## Tool Selection Guidelines
|
|
77
|
-
- For file editing, prefer `
|
|
77
|
+
- For file editing, prefer `apply_patch` for targeted edits to avoid rewriting entire files and wasting tokens
|
|
78
78
|
- When you need to make multiple edits to the same file, combine them in a single `edit` call with multiple ops
|
|
79
79
|
- Use `ripgrep` over `grep` for faster searching across large codebases
|
|
80
80
|
- Always use `glob` first to discover files before reading them, unless you know exact paths
|
|
@@ -82,19 +82,28 @@ When making changes to files, first understand the file's code conventions. Mimi
|
|
|
82
82
|
|
|
83
83
|
## File Editing Best Practices
|
|
84
84
|
|
|
85
|
-
**Using the `
|
|
85
|
+
**Using the `apply_patch` Tool** (Recommended):
|
|
86
|
+
- Primary choice for targeted file edits - avoids rewriting entire files
|
|
87
|
+
- Only requires the specific lines you want to change
|
|
88
|
+
- Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
|
|
89
|
+
- Only use when you have a complete unified diff ready
|
|
90
|
+
- Ensure patch is based on current file content (not stale)
|
|
91
|
+
- If patch fails, read the file first to see current state before retrying
|
|
92
|
+
|
|
93
|
+
**Using the `edit` Tool** (Alternative):
|
|
86
94
|
- Specify the file path and a list of operations
|
|
87
95
|
- Operations are applied sequentially to the latest file state
|
|
88
96
|
- Operation types: `replace`, `insert-before`, `insert-after`, `delete`
|
|
89
97
|
- Each operation includes: `old` (text to match/find) and `new` (replacement/insertion)
|
|
90
98
|
- When making multiple changes to a file, use ONE `edit` call with multiple ops
|
|
91
99
|
|
|
92
|
-
**Using the `
|
|
93
|
-
- Only use when
|
|
94
|
-
-
|
|
95
|
-
-
|
|
100
|
+
**Using the `write` Tool** (Last Resort):
|
|
101
|
+
- Only use when creating new files or completely replacing file content
|
|
102
|
+
- NEVER use for targeted edits - it rewrites the entire file
|
|
103
|
+
- Wastes output tokens and risks hallucinating unchanged parts
|
|
96
104
|
|
|
97
105
|
**Never**:
|
|
106
|
+
- Use `write` for partial file edits (use `apply_patch` or `edit` instead)
|
|
98
107
|
- Make multiple separate `edit` or `apply_patch` calls for the same file
|
|
99
108
|
- Assume file content remains unchanged between operations
|
|
100
109
|
- Use `bash` with `sed`/`awk` for programmatic file editing (use `edit` instead)
|
|
@@ -22,8 +22,8 @@ You have access to a rich set of specialized tools optimized for coding tasks:
|
|
|
22
22
|
**File Reading & Editing**:
|
|
23
23
|
- `read`: Read file contents (supports line ranges)
|
|
24
24
|
- `write`: Write complete file contents
|
|
25
|
-
- `
|
|
26
|
-
- `
|
|
25
|
+
- `apply_patch`: Apply unified diff patches (RECOMMENDED for targeted edits)
|
|
26
|
+
- `edit`: Structured file editing with operations (alternative editing method)
|
|
27
27
|
|
|
28
28
|
**Version Control**:
|
|
29
29
|
- `git_status`, `git_diff`, `git_log`, `git_show`, `git_commit`
|
|
@@ -37,7 +37,7 @@ You have access to a rich set of specialized tools optimized for coding tasks:
|
|
|
37
37
|
### Tool Usage Best Practices:
|
|
38
38
|
|
|
39
39
|
1. **Batch Independent Operations**: Make all independent tool calls in one turn
|
|
40
|
-
2. **File Editing**: Prefer `
|
|
40
|
+
2. **File Editing**: Prefer `apply_patch` for targeted edits to avoid rewriting entire files
|
|
41
41
|
3. **Combine Edits**: When editing the same file multiple times, use ONE `edit` call with multiple ops
|
|
42
42
|
4. **Search First**: Use `glob` to find files before reading them
|
|
43
43
|
5. **Progress Updates**: Call `progress_update` at major milestones (planning, discovering, writing, verifying)
|
|
@@ -46,19 +46,28 @@ You have access to a rich set of specialized tools optimized for coding tasks:
|
|
|
46
46
|
|
|
47
47
|
## File Editing Best Practices
|
|
48
48
|
|
|
49
|
-
**Using the `
|
|
49
|
+
**Using the `apply_patch` Tool** (Recommended):
|
|
50
|
+
- Primary choice for targeted file edits - avoids rewriting entire files
|
|
51
|
+
- Only requires the specific lines you want to change
|
|
52
|
+
- Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
|
|
53
|
+
- Only use when you have a complete unified diff ready
|
|
54
|
+
- Ensure patch is based on current file content (not stale)
|
|
55
|
+
- If patch fails, read the file first to see current state before retrying
|
|
56
|
+
|
|
57
|
+
**Using the `edit` Tool** (Alternative):
|
|
50
58
|
- Specify the file path and a list of operations
|
|
51
59
|
- Operations are applied sequentially to the latest file state
|
|
52
60
|
- Operation types: `replace`, `insert-before`, `insert-after`, `delete`
|
|
53
61
|
- Each operation includes: `old` (text to match/find) and `new` (replacement/insertion)
|
|
54
62
|
- When making multiple changes to a file, use ONE `edit` call with multiple ops
|
|
55
63
|
|
|
56
|
-
**Using the `
|
|
57
|
-
- Only use when
|
|
58
|
-
-
|
|
59
|
-
-
|
|
64
|
+
**Using the `write` Tool** (Last Resort):
|
|
65
|
+
- Only use when creating new files or completely replacing file content
|
|
66
|
+
- NEVER use for targeted edits - it rewrites the entire file
|
|
67
|
+
- Wastes output tokens and risks hallucinating unchanged parts
|
|
60
68
|
|
|
61
69
|
**Never**:
|
|
70
|
+
- Use `write` for partial file edits (use `apply_patch` or `edit` instead)
|
|
62
71
|
- Make multiple separate `edit` or `apply_patch` calls for the same file
|
|
63
72
|
- Assume file content remains unchanged between operations
|
|
64
73
|
- Use `bash` with `sed`/`awk` for programmatic file editing (use `edit` instead)
|
|
@@ -18,7 +18,7 @@ You are opencode, an interactive CLI agent specializing in software engineering
|
|
|
18
18
|
Your primary tools for coding tasks are:
|
|
19
19
|
- **Discovery**: `glob` (find files), `ripgrep` (search content), `tree` (directory structure)
|
|
20
20
|
- **Reading**: `read` (individual files), `ls` (directory listing)
|
|
21
|
-
- **Editing**: `
|
|
21
|
+
- **Editing**: `apply_patch` (recommended - targeted edits), `edit` (alternative - structured ops)
|
|
22
22
|
- **Execution**: `bash` (run commands), `git_*` tools (version control)
|
|
23
23
|
- **Planning**: `update_plan` (create and track task plans)
|
|
24
24
|
- **Progress**: `progress_update` (inform user of current phase)
|
|
@@ -28,19 +28,28 @@ call with multiple ops. Each separate `edit` operation re-reads the file fresh.
|
|
|
28
28
|
|
|
29
29
|
## File Editing Best Practices
|
|
30
30
|
|
|
31
|
-
**Using the `
|
|
31
|
+
**Using the `apply_patch` Tool** (Recommended):
|
|
32
|
+
- Primary choice for targeted file edits - avoids rewriting entire files
|
|
33
|
+
- Only requires the specific lines you want to change
|
|
34
|
+
- Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
|
|
35
|
+
- Only use when you have a complete unified diff ready
|
|
36
|
+
- Ensure patch is based on current file content (not stale)
|
|
37
|
+
- If patch fails, read the file first to see current state before retrying
|
|
38
|
+
|
|
39
|
+
**Using the `edit` Tool** (Alternative):
|
|
32
40
|
- Specify the file path and a list of operations
|
|
33
41
|
- Operations are applied sequentially to the latest file state
|
|
34
42
|
- Operation types: `replace`, `insert-before`, `insert-after`, `delete`
|
|
35
43
|
- Each operation includes: `old` (text to match/find) and `new` (replacement/insertion)
|
|
36
44
|
- When making multiple changes to a file, use ONE `edit` call with multiple ops
|
|
37
45
|
|
|
38
|
-
**Using the `
|
|
39
|
-
- Only use when
|
|
40
|
-
-
|
|
41
|
-
-
|
|
46
|
+
**Using the `write` Tool** (Last Resort):
|
|
47
|
+
- Only use when creating new files or completely replacing file content
|
|
48
|
+
- NEVER use for targeted edits - it rewrites the entire file
|
|
49
|
+
- Wastes output tokens and risks hallucinating unchanged parts
|
|
42
50
|
|
|
43
51
|
**Never**:
|
|
52
|
+
- Use `write` for partial file edits (use `apply_patch` or `edit` instead)
|
|
44
53
|
- Make multiple separate `edit` or `apply_patch` calls for the same file
|
|
45
54
|
- Assume file content remains unchanged between operations
|
|
46
55
|
- Use `bash` with `sed`/`awk` for programmatic file editing (use `edit` instead)
|
|
@@ -54,8 +54,9 @@ Before making tool calls, send a brief preamble to the user explaining what you'
|
|
|
54
54
|
Your toolset includes specialized file editing and search tools. Follow these guidelines:
|
|
55
55
|
|
|
56
56
|
**File Editing**:
|
|
57
|
-
- Prefer `
|
|
58
|
-
- Use `
|
|
57
|
+
- Prefer `apply_patch` tool for targeted edits - avoids rewriting entire files
|
|
58
|
+
- Use `edit` tool as alternative when you need structured operations
|
|
59
|
+
- NEVER use `write` for partial edits - it rewrites the entire file and wastes tokens
|
|
59
60
|
- When editing the same file multiple times, batch operations in a single `edit` call
|
|
60
61
|
- Each separate `edit` operation re-reads the file, so ops within one call are sequential
|
|
61
62
|
|
|
@@ -71,19 +72,28 @@ Your toolset includes specialized file editing and search tools. Follow these gu
|
|
|
71
72
|
|
|
72
73
|
## File Editing Best Practices
|
|
73
74
|
|
|
74
|
-
**Using the `
|
|
75
|
+
**Using the `apply_patch` Tool** (Recommended):
|
|
76
|
+
- Primary choice for targeted file edits - avoids rewriting entire files
|
|
77
|
+
- Only requires the specific lines you want to change
|
|
78
|
+
- Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
|
|
79
|
+
- Only use when you have a complete unified diff ready
|
|
80
|
+
- Ensure patch is based on current file content (not stale)
|
|
81
|
+
- If patch fails, read the file first to see current state before retrying
|
|
82
|
+
|
|
83
|
+
**Using the `edit` Tool** (Alternative):
|
|
75
84
|
- Specify the file path and a list of operations
|
|
76
85
|
- Operations are applied sequentially to the latest file state
|
|
77
86
|
- Operation types: `replace`, `insert-before`, `insert-after`, `delete`
|
|
78
87
|
- Each operation includes: `old` (text to match/find) and `new` (replacement/insertion)
|
|
79
88
|
- When making multiple changes to a file, use ONE `edit` call with multiple ops
|
|
80
89
|
|
|
81
|
-
**Using the `
|
|
82
|
-
- Only use when
|
|
83
|
-
-
|
|
84
|
-
-
|
|
90
|
+
**Using the `write` Tool** (Last Resort):
|
|
91
|
+
- Only use when creating new files or completely replacing file content
|
|
92
|
+
- NEVER use for targeted edits - it rewrites the entire file
|
|
93
|
+
- Wastes output tokens and risks hallucinating unchanged parts
|
|
85
94
|
|
|
86
95
|
**Never**:
|
|
96
|
+
- Use `write` for partial file edits (use `apply_patch` or `edit` instead)
|
|
87
97
|
- Make multiple separate `edit` or `apply_patch` calls for the same file
|
|
88
98
|
- Assume file content remains unchanged between operations
|
|
89
99
|
- Use `bash` with `sed`/`awk` for programmatic file editing (use `edit` instead)
|