@agi-cli/sdk 0.1.80 → 0.1.82
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 +2 -1
- package/src/core/src/tools/builtin/patch/apply.ts +562 -0
- package/src/core/src/tools/builtin/patch/constants.ts +5 -0
- package/src/core/src/tools/builtin/patch/normalize.ts +31 -0
- package/src/core/src/tools/builtin/patch/parse-enveloped.ts +209 -0
- package/src/core/src/tools/builtin/patch/parse-unified.ts +231 -0
- package/src/core/src/tools/builtin/patch/parse.ts +28 -0
- package/src/core/src/tools/builtin/patch/text.ts +23 -0
- package/src/core/src/tools/builtin/patch/types.ts +82 -0
- package/src/core/src/tools/builtin/patch.ts +83 -783
- package/src/core/src/tools/builtin/patch.txt +100 -10
- package/src/prompts/src/agents/build.txt +47 -0
- package/src/prompts/src/providers/anthropic.txt +0 -31
- package/src/prompts/src/providers/default.txt +20 -2
- package/src/prompts/src/providers/google.txt +0 -33
- package/src/prompts/src/providers/openai.txt +0 -36
|
@@ -1,757 +1,52 @@
|
|
|
1
1
|
import { tool, type Tool } from 'ai';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
|
4
|
-
import { dirname, resolve, relative, isAbsolute } from 'node:path';
|
|
5
3
|
import DESCRIPTION from './patch.txt' with { type: 'text' };
|
|
6
4
|
import { createToolError, type ToolResponse } from '../error.ts';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface PatchUpdateOperation {
|
|
20
|
-
kind: 'update';
|
|
21
|
-
filePath: string;
|
|
22
|
-
hunks: PatchHunk[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type ParsedPatchOperation =
|
|
26
|
-
| PatchAddOperation
|
|
27
|
-
| PatchDeleteOperation
|
|
28
|
-
| PatchUpdateOperation;
|
|
29
|
-
|
|
30
|
-
type PatchLineKind = 'context' | 'add' | 'remove';
|
|
31
|
-
|
|
32
|
-
interface PatchHunkLine {
|
|
33
|
-
kind: PatchLineKind;
|
|
34
|
-
content: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface PatchHunkHeader {
|
|
38
|
-
oldStart?: number;
|
|
39
|
-
oldLines?: number;
|
|
40
|
-
newStart?: number;
|
|
41
|
-
newLines?: number;
|
|
42
|
-
context?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface PatchHunk {
|
|
46
|
-
header: PatchHunkHeader;
|
|
47
|
-
lines: PatchHunkLine[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface PatchStats {
|
|
51
|
-
additions: number;
|
|
52
|
-
deletions: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface AppliedHunkResult {
|
|
56
|
-
header: PatchHunkHeader;
|
|
57
|
-
lines: PatchHunkLine[];
|
|
58
|
-
oldStart: number;
|
|
59
|
-
oldLines: number;
|
|
60
|
-
newStart: number;
|
|
61
|
-
newLines: number;
|
|
62
|
-
additions: number;
|
|
63
|
-
deletions: number;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface AppliedOperationRecord {
|
|
67
|
-
kind: 'add' | 'delete' | 'update';
|
|
68
|
-
filePath: string;
|
|
69
|
-
operation: ParsedPatchOperation;
|
|
70
|
-
stats: PatchStats;
|
|
71
|
-
hunks: AppliedHunkResult[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const PATCH_BEGIN_MARKER = '*** Begin Patch';
|
|
75
|
-
const PATCH_END_MARKER = '*** End Patch';
|
|
76
|
-
const PATCH_ADD_PREFIX = '*** Add File:';
|
|
77
|
-
const PATCH_UPDATE_PREFIX = '*** Update File:';
|
|
78
|
-
const PATCH_DELETE_PREFIX = '*** Delete File:';
|
|
79
|
-
|
|
80
|
-
function isErrnoException(value: unknown): value is NodeJS.ErrnoException {
|
|
81
|
-
return (
|
|
82
|
-
value instanceof Error &&
|
|
83
|
-
typeof (value as NodeJS.ErrnoException).code === 'string'
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function splitLines(value: string): { lines: string[]; newline: string } {
|
|
88
|
-
const newline = value.includes('\r\n') ? '\r\n' : '\n';
|
|
89
|
-
const normalized = newline === '\n' ? value : value.replace(/\r\n/g, '\n');
|
|
90
|
-
const parts = normalized.split('\n');
|
|
91
|
-
|
|
92
|
-
if (parts.length > 0 && parts[parts.length - 1] === '') {
|
|
93
|
-
parts.pop();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return { lines: parts, newline };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function joinLines(lines: string[], newline: string): string {
|
|
100
|
-
const base = lines.join('\n');
|
|
101
|
-
return newline === '\n' ? base : base.replace(/\n/g, newline);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function ensureTrailingNewline(lines: string[]) {
|
|
105
|
-
if (lines.length === 0 || lines[lines.length - 1] !== '') {
|
|
106
|
-
lines.push('');
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Normalize whitespace for fuzzy matching.
|
|
112
|
-
* Converts tabs to spaces and trims leading/trailing whitespace.
|
|
113
|
-
*/
|
|
114
|
-
function normalizeWhitespace(line: string): string {
|
|
115
|
-
return line.replace(/\t/g, ' ').trim();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Find subsequence with optional whitespace normalization for fuzzy matching.
|
|
120
|
-
* Falls back to normalized matching if exact match fails.
|
|
121
|
-
*/
|
|
122
|
-
function findSubsequenceWithFuzzy(
|
|
123
|
-
lines: string[],
|
|
124
|
-
pattern: string[],
|
|
125
|
-
startIndex: number,
|
|
126
|
-
useFuzzy: boolean,
|
|
127
|
-
): number {
|
|
128
|
-
// Try exact match first
|
|
129
|
-
const exactMatch = findSubsequence(lines, pattern, startIndex);
|
|
130
|
-
if (exactMatch !== -1) return exactMatch;
|
|
131
|
-
|
|
132
|
-
// If fuzzy matching is enabled and exact match failed, try normalized matching
|
|
133
|
-
if (useFuzzy && pattern.length > 0) {
|
|
134
|
-
const normalizedLines = lines.map(normalizeWhitespace);
|
|
135
|
-
const normalizedPattern = pattern.map(normalizeWhitespace);
|
|
136
|
-
|
|
137
|
-
const start = Math.max(0, startIndex);
|
|
138
|
-
for (let i = start; i <= lines.length - pattern.length; i++) {
|
|
139
|
-
let matches = true;
|
|
140
|
-
for (let j = 0; j < pattern.length; j++) {
|
|
141
|
-
if (normalizedLines[i + j] !== normalizedPattern[j]) {
|
|
142
|
-
matches = false;
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (matches) return i;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return -1;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function findSubsequence(
|
|
154
|
-
lines: string[],
|
|
155
|
-
pattern: string[],
|
|
156
|
-
startIndex: number,
|
|
157
|
-
): number {
|
|
158
|
-
if (pattern.length === 0) return -1;
|
|
159
|
-
const start = Math.max(0, startIndex);
|
|
160
|
-
for (let i = start; i <= lines.length - pattern.length; i++) {
|
|
161
|
-
let matches = true;
|
|
162
|
-
for (let j = 0; j < pattern.length; j++) {
|
|
163
|
-
if (lines[i + j] !== pattern[j]) {
|
|
164
|
-
matches = false;
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (matches) return i;
|
|
169
|
-
}
|
|
170
|
-
return -1;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function parseDirectivePath(line: string, prefix: string): string {
|
|
174
|
-
const filePath = line.slice(prefix.length).trim();
|
|
175
|
-
if (!filePath) {
|
|
176
|
-
throw new Error(`Missing file path for directive: ${line}`);
|
|
177
|
-
}
|
|
178
|
-
if (filePath.startsWith('/') || isAbsolute(filePath)) {
|
|
179
|
-
throw new Error('Patch file paths must be relative to the project root.');
|
|
180
|
-
}
|
|
181
|
-
return filePath;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function parseHunkHeader(raw: string): PatchHunkHeader {
|
|
185
|
-
const numericMatch = raw.match(
|
|
186
|
-
/^@@\s*-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s*@@(?:\s*(.*))?$/,
|
|
187
|
-
);
|
|
188
|
-
if (numericMatch) {
|
|
189
|
-
const [, oldStart, oldCount, newStart, newCount, context] = numericMatch;
|
|
190
|
-
return {
|
|
191
|
-
oldStart: Number.parseInt(oldStart, 10),
|
|
192
|
-
oldLines: oldCount ? Number.parseInt(oldCount, 10) : undefined,
|
|
193
|
-
newStart: Number.parseInt(newStart, 10),
|
|
194
|
-
newLines: newCount ? Number.parseInt(newCount, 10) : undefined,
|
|
195
|
-
context: context?.trim() || undefined,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const context = raw.replace(/^@@/, '').trim();
|
|
200
|
-
return context ? { context } : {};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function parseEnvelopedPatch(patch: string): ParsedPatchOperation[] {
|
|
204
|
-
const normalized = patch.replace(/\r\n/g, '\n');
|
|
205
|
-
const lines = normalized.split('\n');
|
|
206
|
-
const operations: ParsedPatchOperation[] = [];
|
|
207
|
-
|
|
208
|
-
type Builder =
|
|
209
|
-
| (PatchAddOperation & { kind: 'add' })
|
|
210
|
-
| (PatchDeleteOperation & { kind: 'delete' })
|
|
211
|
-
| (PatchUpdateOperation & {
|
|
212
|
-
kind: 'update';
|
|
213
|
-
currentHunk: PatchHunk | null;
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
let builder: Builder | null = null;
|
|
217
|
-
let insidePatch = false;
|
|
218
|
-
let encounteredEnd = false;
|
|
219
|
-
|
|
220
|
-
const flushBuilder = () => {
|
|
221
|
-
if (!builder) return;
|
|
222
|
-
if (builder.kind === 'update') {
|
|
223
|
-
if (builder.currentHunk && builder.currentHunk.lines.length === 0) {
|
|
224
|
-
builder.hunks.pop();
|
|
225
|
-
}
|
|
226
|
-
if (builder.hunks.length === 0) {
|
|
227
|
-
throw new Error(
|
|
228
|
-
`Update for ${builder.filePath} does not contain any diff hunks.`,
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
operations.push({
|
|
232
|
-
kind: 'update',
|
|
233
|
-
filePath: builder.filePath,
|
|
234
|
-
hunks: builder.hunks.map((hunk) => ({
|
|
235
|
-
header: { ...hunk.header },
|
|
236
|
-
lines: hunk.lines.map((line) => ({ ...line })),
|
|
237
|
-
})),
|
|
238
|
-
});
|
|
239
|
-
} else if (builder.kind === 'add') {
|
|
240
|
-
operations.push({
|
|
241
|
-
kind: 'add',
|
|
242
|
-
filePath: builder.filePath,
|
|
243
|
-
lines: [...builder.lines],
|
|
244
|
-
});
|
|
245
|
-
} else {
|
|
246
|
-
operations.push({ kind: 'delete', filePath: builder.filePath });
|
|
247
|
-
}
|
|
248
|
-
builder = null;
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
for (let i = 0; i < lines.length; i++) {
|
|
252
|
-
const line = lines[i];
|
|
253
|
-
if (!insidePatch) {
|
|
254
|
-
if (line.trim() === '') continue;
|
|
255
|
-
if (line.startsWith(PATCH_BEGIN_MARKER)) {
|
|
256
|
-
insidePatch = true;
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
throw new Error(
|
|
260
|
-
'Patch must start with "*** Begin Patch" and use the enveloped patch format.',
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (line.startsWith(PATCH_BEGIN_MARKER)) {
|
|
265
|
-
throw new Error('Nested "*** Begin Patch" markers are not supported.');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (line.startsWith(PATCH_END_MARKER)) {
|
|
269
|
-
flushBuilder();
|
|
270
|
-
encounteredEnd = true;
|
|
271
|
-
const remaining = lines.slice(i + 1).find((rest) => rest.trim() !== '');
|
|
272
|
-
if (remaining) {
|
|
273
|
-
throw new Error(
|
|
274
|
-
'Unexpected content found after "*** End Patch" marker.',
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (line.startsWith(PATCH_ADD_PREFIX)) {
|
|
281
|
-
flushBuilder();
|
|
282
|
-
builder = {
|
|
283
|
-
kind: 'add',
|
|
284
|
-
filePath: parseDirectivePath(line, PATCH_ADD_PREFIX),
|
|
285
|
-
lines: [],
|
|
286
|
-
};
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (line.startsWith(PATCH_UPDATE_PREFIX)) {
|
|
291
|
-
flushBuilder();
|
|
292
|
-
builder = {
|
|
293
|
-
kind: 'update',
|
|
294
|
-
filePath: parseDirectivePath(line, PATCH_UPDATE_PREFIX),
|
|
295
|
-
hunks: [],
|
|
296
|
-
currentHunk: null,
|
|
297
|
-
};
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (line.startsWith(PATCH_DELETE_PREFIX)) {
|
|
302
|
-
flushBuilder();
|
|
303
|
-
builder = {
|
|
304
|
-
kind: 'delete',
|
|
305
|
-
filePath: parseDirectivePath(line, PATCH_DELETE_PREFIX),
|
|
306
|
-
};
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (!builder) {
|
|
311
|
-
if (line.trim() === '') {
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
throw new Error(`Unexpected content in patch: "${line}"`);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (builder.kind === 'add') {
|
|
318
|
-
const content = line.startsWith('+') ? line.slice(1) : line;
|
|
319
|
-
builder.lines.push(content);
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (builder.kind === 'delete') {
|
|
324
|
-
if (line.trim() !== '') {
|
|
325
|
-
throw new Error(
|
|
326
|
-
`Delete directive for ${builder.filePath} should not contain additional lines.`,
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (line.startsWith('@@')) {
|
|
333
|
-
const hunk: PatchHunk = { header: parseHunkHeader(line), lines: [] };
|
|
334
|
-
builder.hunks.push(hunk);
|
|
335
|
-
builder.currentHunk = hunk;
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (!builder.currentHunk) {
|
|
340
|
-
const fallbackHunk: PatchHunk = { header: {}, lines: [] };
|
|
341
|
-
builder.hunks.push(fallbackHunk);
|
|
342
|
-
builder.currentHunk = fallbackHunk;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const currentHunk = builder.currentHunk;
|
|
346
|
-
const prefix = line[0];
|
|
347
|
-
if (prefix === '+') {
|
|
348
|
-
currentHunk.lines.push({ kind: 'add', content: line.slice(1) });
|
|
349
|
-
} else if (prefix === '-') {
|
|
350
|
-
currentHunk.lines.push({ kind: 'remove', content: line.slice(1) });
|
|
351
|
-
} else if (prefix === ' ') {
|
|
352
|
-
currentHunk.lines.push({ kind: 'context', content: line.slice(1) });
|
|
353
|
-
} else {
|
|
354
|
-
throw new Error(`Unrecognized patch line: "${line}"`);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (!encounteredEnd) {
|
|
359
|
-
throw new Error('Missing "*** End Patch" marker.');
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return operations;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function resolveProjectPath(projectRoot: string, filePath: string): string {
|
|
366
|
-
const fullPath = resolve(projectRoot, filePath);
|
|
367
|
-
const relativePath = relative(projectRoot, fullPath);
|
|
368
|
-
if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
|
|
369
|
-
throw new Error(`Patch path escapes project root: ${filePath}`);
|
|
370
|
-
}
|
|
371
|
-
return fullPath;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async function applyAddOperation(
|
|
375
|
-
projectRoot: string,
|
|
376
|
-
operation: PatchAddOperation,
|
|
377
|
-
): Promise<AppliedOperationRecord> {
|
|
378
|
-
const targetPath = resolveProjectPath(projectRoot, operation.filePath);
|
|
379
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
380
|
-
const linesForWrite = [...operation.lines];
|
|
381
|
-
ensureTrailingNewline(linesForWrite);
|
|
382
|
-
await writeFile(targetPath, joinLines(linesForWrite, '\n'), 'utf-8');
|
|
383
|
-
|
|
384
|
-
const hunkLines: PatchHunkLine[] = operation.lines.map((line) => ({
|
|
385
|
-
kind: 'add',
|
|
386
|
-
content: line,
|
|
387
|
-
}));
|
|
388
|
-
|
|
389
|
-
return {
|
|
390
|
-
kind: 'add',
|
|
5
|
+
import { applyPatchOperations } from './patch/apply.ts';
|
|
6
|
+
import { parsePatchInput } from './patch/parse.ts';
|
|
7
|
+
import type {
|
|
8
|
+
AppliedPatchOperation,
|
|
9
|
+
PatchOperation,
|
|
10
|
+
RejectedPatch,
|
|
11
|
+
} from './patch/types.ts';
|
|
12
|
+
|
|
13
|
+
function serializeChanges(operations: AppliedPatchOperation[]) {
|
|
14
|
+
return operations.map((operation) => ({
|
|
391
15
|
filePath: operation.filePath,
|
|
392
|
-
operation,
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
oldLines: 0,
|
|
403
|
-
newStart: 1,
|
|
404
|
-
newLines: hunkLines.length,
|
|
405
|
-
additions: hunkLines.length,
|
|
406
|
-
deletions: 0,
|
|
407
|
-
},
|
|
408
|
-
],
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
async function applyDeleteOperation(
|
|
413
|
-
projectRoot: string,
|
|
414
|
-
operation: PatchDeleteOperation,
|
|
415
|
-
): Promise<AppliedOperationRecord> {
|
|
416
|
-
const targetPath = resolveProjectPath(projectRoot, operation.filePath);
|
|
417
|
-
let existingContent = '';
|
|
418
|
-
try {
|
|
419
|
-
existingContent = await readFile(targetPath, 'utf-8');
|
|
420
|
-
} catch (error) {
|
|
421
|
-
if (isErrnoException(error) && error.code === 'ENOENT') {
|
|
422
|
-
throw new Error(`File not found for deletion: ${operation.filePath}`);
|
|
423
|
-
}
|
|
424
|
-
throw error;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const { lines } = splitLines(existingContent);
|
|
428
|
-
await unlink(targetPath);
|
|
429
|
-
|
|
430
|
-
const hunkLines: PatchHunkLine[] = lines.map((line) => ({
|
|
431
|
-
kind: 'remove',
|
|
432
|
-
content: line,
|
|
16
|
+
kind: operation.kind,
|
|
17
|
+
hunks: operation.hunks.map((hunk) => ({
|
|
18
|
+
oldStart: hunk.oldStart,
|
|
19
|
+
oldLines: hunk.oldLines,
|
|
20
|
+
newStart: hunk.newStart,
|
|
21
|
+
newLines: hunk.newLines,
|
|
22
|
+
additions: hunk.additions,
|
|
23
|
+
deletions: hunk.deletions,
|
|
24
|
+
context: hunk.header.context,
|
|
25
|
+
})),
|
|
433
26
|
}));
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
kind: 'delete',
|
|
437
|
-
filePath: operation.filePath,
|
|
438
|
-
operation,
|
|
439
|
-
stats: {
|
|
440
|
-
additions: 0,
|
|
441
|
-
deletions: hunkLines.length,
|
|
442
|
-
},
|
|
443
|
-
hunks: [
|
|
444
|
-
{
|
|
445
|
-
header: {},
|
|
446
|
-
lines: hunkLines,
|
|
447
|
-
oldStart: 1,
|
|
448
|
-
oldLines: hunkLines.length,
|
|
449
|
-
newStart: 0,
|
|
450
|
-
newLines: 0,
|
|
451
|
-
additions: 0,
|
|
452
|
-
deletions: hunkLines.length,
|
|
453
|
-
},
|
|
454
|
-
],
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function applyHunksToLines(
|
|
459
|
-
originalLines: string[],
|
|
460
|
-
hunks: PatchHunk[],
|
|
461
|
-
filePath: string,
|
|
462
|
-
useFuzzy: boolean = false,
|
|
463
|
-
): { lines: string[]; applied: AppliedHunkResult[] } {
|
|
464
|
-
const lines = [...originalLines];
|
|
465
|
-
let searchIndex = 0;
|
|
466
|
-
let lineOffset = 0;
|
|
467
|
-
const applied: AppliedHunkResult[] = [];
|
|
468
|
-
|
|
469
|
-
for (const hunk of hunks) {
|
|
470
|
-
const expected: string[] = [];
|
|
471
|
-
const replacement: string[] = [];
|
|
472
|
-
let additions = 0;
|
|
473
|
-
let deletions = 0;
|
|
474
|
-
|
|
475
|
-
for (const line of hunk.lines) {
|
|
476
|
-
if (line.kind !== 'add') {
|
|
477
|
-
expected.push(line.content);
|
|
478
|
-
}
|
|
479
|
-
if (line.kind !== 'remove') {
|
|
480
|
-
replacement.push(line.content);
|
|
481
|
-
}
|
|
482
|
-
if (line.kind === 'add') additions += 1;
|
|
483
|
-
if (line.kind === 'remove') deletions += 1;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const hasExpected = expected.length > 0;
|
|
487
|
-
const hint =
|
|
488
|
-
typeof hunk.header.oldStart === 'number'
|
|
489
|
-
? Math.max(0, hunk.header.oldStart - 1 + lineOffset)
|
|
490
|
-
: searchIndex;
|
|
491
|
-
|
|
492
|
-
let matchIndex = hasExpected
|
|
493
|
-
? findSubsequenceWithFuzzy(
|
|
494
|
-
lines,
|
|
495
|
-
expected,
|
|
496
|
-
Math.max(0, hint - 3),
|
|
497
|
-
useFuzzy,
|
|
498
|
-
)
|
|
499
|
-
: -1;
|
|
500
|
-
|
|
501
|
-
if (hasExpected && matchIndex === -1) {
|
|
502
|
-
matchIndex = findSubsequenceWithFuzzy(lines, expected, 0, useFuzzy);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if (matchIndex === -1 && hasExpected && hunk.header.context) {
|
|
506
|
-
const contextIndex = findSubsequence(lines, [hunk.header.context], 0);
|
|
507
|
-
if (contextIndex !== -1) {
|
|
508
|
-
const positionInExpected = expected.indexOf(hunk.header.context);
|
|
509
|
-
matchIndex =
|
|
510
|
-
positionInExpected >= 0
|
|
511
|
-
? Math.max(0, contextIndex - positionInExpected)
|
|
512
|
-
: contextIndex;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (!hasExpected) {
|
|
517
|
-
matchIndex = computeInsertionIndex(lines, hint, hunk.header);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if (matchIndex === -1) {
|
|
521
|
-
const contextInfo = hunk.header.context
|
|
522
|
-
? ` near context '${hunk.header.context}'`
|
|
523
|
-
: '';
|
|
524
|
-
|
|
525
|
-
// Provide helpful error with nearby context
|
|
526
|
-
const nearbyStart = Math.max(0, hint - 2);
|
|
527
|
-
const nearbyEnd = Math.min(lines.length, hint + 5);
|
|
528
|
-
const nearbyLines = lines.slice(nearbyStart, nearbyEnd);
|
|
529
|
-
const lineNumberInfo =
|
|
530
|
-
nearbyStart > 0 ? ` (around line ${nearbyStart + 1})` : '';
|
|
531
|
-
|
|
532
|
-
let errorMsg = `Failed to apply patch hunk in ${filePath}${contextInfo}.\n`;
|
|
533
|
-
errorMsg += `Expected to find:\n${expected.map((l) => ` ${l}`).join('\n')}\n`;
|
|
534
|
-
errorMsg += `Nearby context${lineNumberInfo}:\n${nearbyLines.map((l, idx) => ` ${nearbyStart + idx + 1}: ${l}`).join('\n')}\n`;
|
|
535
|
-
errorMsg += `Hint: Check for whitespace differences (tabs vs spaces). Try enabling fuzzyMatch option.`;
|
|
536
|
-
|
|
537
|
-
throw new Error(errorMsg);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const deleteCount = hasExpected ? expected.length : 0;
|
|
541
|
-
const originalIndex = matchIndex - lineOffset;
|
|
542
|
-
const normalizedOriginalIndex = Math.min(
|
|
543
|
-
Math.max(0, originalIndex),
|
|
544
|
-
originalLines.length,
|
|
545
|
-
);
|
|
546
|
-
const oldStart = normalizedOriginalIndex + 1;
|
|
547
|
-
const newStart = matchIndex + 1;
|
|
548
|
-
|
|
549
|
-
lines.splice(matchIndex, deleteCount, ...replacement);
|
|
550
|
-
searchIndex = matchIndex + replacement.length;
|
|
551
|
-
lineOffset += replacement.length - deleteCount;
|
|
552
|
-
|
|
553
|
-
applied.push({
|
|
554
|
-
header: { ...hunk.header },
|
|
555
|
-
lines: hunk.lines.map((line) => ({ ...line })),
|
|
556
|
-
oldStart,
|
|
557
|
-
oldLines: deleteCount,
|
|
558
|
-
newStart,
|
|
559
|
-
newLines: replacement.length,
|
|
560
|
-
additions,
|
|
561
|
-
deletions,
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
return { lines, applied };
|
|
566
27
|
}
|
|
567
28
|
|
|
568
|
-
function
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
return Math.min(lines.length, Math.max(0, hint));
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
async function applyUpdateOperation(
|
|
594
|
-
projectRoot: string,
|
|
595
|
-
operation: PatchUpdateOperation,
|
|
596
|
-
useFuzzy: boolean = false,
|
|
597
|
-
): Promise<AppliedOperationRecord> {
|
|
598
|
-
const targetPath = resolveProjectPath(projectRoot, operation.filePath);
|
|
599
|
-
let originalContent: string;
|
|
600
|
-
try {
|
|
601
|
-
originalContent = await readFile(targetPath, 'utf-8');
|
|
602
|
-
} catch (error) {
|
|
603
|
-
if (isErrnoException(error) && error.code === 'ENOENT') {
|
|
604
|
-
throw new Error(`File not found: ${operation.filePath}`);
|
|
605
|
-
}
|
|
606
|
-
throw error;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const { lines: originalLines, newline } = splitLines(originalContent);
|
|
610
|
-
const { lines: updatedLines, applied } = applyHunksToLines(
|
|
611
|
-
originalLines,
|
|
612
|
-
operation.hunks,
|
|
613
|
-
operation.filePath,
|
|
614
|
-
useFuzzy,
|
|
615
|
-
);
|
|
616
|
-
ensureTrailingNewline(updatedLines);
|
|
617
|
-
await writeFile(targetPath, joinLines(updatedLines, newline), 'utf-8');
|
|
618
|
-
|
|
619
|
-
const stats = applied.reduce<PatchStats>(
|
|
620
|
-
(acc, hunk) => ({
|
|
621
|
-
additions: acc.additions + hunk.additions,
|
|
622
|
-
deletions: acc.deletions + hunk.deletions,
|
|
623
|
-
}),
|
|
624
|
-
{ additions: 0, deletions: 0 },
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
return {
|
|
628
|
-
kind: 'update',
|
|
629
|
-
filePath: operation.filePath,
|
|
630
|
-
operation,
|
|
631
|
-
stats,
|
|
632
|
-
hunks: applied,
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function summarizeOperations(operations: AppliedOperationRecord[]) {
|
|
637
|
-
const summary = operations.reduce(
|
|
638
|
-
(acc, op) => ({
|
|
639
|
-
files: acc.files + 1,
|
|
640
|
-
additions: acc.additions + op.stats.additions,
|
|
641
|
-
deletions: acc.deletions + op.stats.deletions,
|
|
642
|
-
}),
|
|
643
|
-
{ files: 0, additions: 0, deletions: 0 },
|
|
644
|
-
);
|
|
645
|
-
return {
|
|
646
|
-
files: Math.max(summary.files, operations.length > 0 ? 1 : 0),
|
|
647
|
-
additions: summary.additions,
|
|
648
|
-
deletions: summary.deletions,
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function formatRange(start: number, count: number) {
|
|
653
|
-
const normalizedStart = Math.max(0, start);
|
|
654
|
-
if (count === 0) return `${normalizedStart},0`;
|
|
655
|
-
if (count === 1) return `${normalizedStart}`;
|
|
656
|
-
return `${normalizedStart},${count}`;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
function formatHunkHeader(applied: AppliedHunkResult) {
|
|
660
|
-
const oldRange = formatRange(applied.oldStart, applied.oldLines);
|
|
661
|
-
const newRange = formatRange(applied.newStart, applied.newLines);
|
|
662
|
-
const context = applied.header.context?.trim();
|
|
663
|
-
return context
|
|
664
|
-
? `@@ -${oldRange} +${newRange} @@ ${context}`
|
|
665
|
-
: `@@ -${oldRange} +${newRange} @@`;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function serializePatchLine(line: PatchHunkLine): string {
|
|
669
|
-
switch (line.kind) {
|
|
670
|
-
case 'add':
|
|
671
|
-
return `+${line.content}`;
|
|
672
|
-
case 'remove':
|
|
673
|
-
return `-${line.content}`;
|
|
674
|
-
default:
|
|
675
|
-
return ` ${line.content}`;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function formatNormalizedPatch(operations: AppliedOperationRecord[]): string {
|
|
680
|
-
const lines: string[] = [PATCH_BEGIN_MARKER];
|
|
681
|
-
|
|
682
|
-
for (const op of operations) {
|
|
683
|
-
switch (op.kind) {
|
|
684
|
-
case 'add': {
|
|
685
|
-
lines.push(`${PATCH_ADD_PREFIX} ${op.filePath}`);
|
|
686
|
-
for (const hunk of op.hunks) {
|
|
687
|
-
lines.push(formatHunkHeader(hunk));
|
|
688
|
-
for (const line of hunk.lines) {
|
|
689
|
-
lines.push(serializePatchLine(line));
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
break;
|
|
693
|
-
}
|
|
694
|
-
case 'delete': {
|
|
695
|
-
lines.push(`${PATCH_DELETE_PREFIX} ${op.filePath}`);
|
|
696
|
-
for (const hunk of op.hunks) {
|
|
697
|
-
lines.push(formatHunkHeader(hunk));
|
|
698
|
-
for (const line of hunk.lines) {
|
|
699
|
-
lines.push(serializePatchLine(line));
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
break;
|
|
703
|
-
}
|
|
704
|
-
case 'update': {
|
|
705
|
-
lines.push(`${PATCH_UPDATE_PREFIX} ${op.filePath}`);
|
|
706
|
-
const updateOp = op.operation as PatchUpdateOperation;
|
|
707
|
-
for (let i = 0; i < updateOp.hunks.length; i++) {
|
|
708
|
-
const originalHunk = updateOp.hunks[i];
|
|
709
|
-
const appliedHunk = op.hunks[i];
|
|
710
|
-
const header: AppliedHunkResult = {
|
|
711
|
-
...appliedHunk,
|
|
712
|
-
header: {
|
|
713
|
-
...appliedHunk.header,
|
|
714
|
-
context: originalHunk.header.context,
|
|
715
|
-
},
|
|
716
|
-
};
|
|
717
|
-
lines.push(formatHunkHeader(header));
|
|
718
|
-
for (const line of originalHunk.lines) {
|
|
719
|
-
lines.push(serializePatchLine(line));
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
break;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
lines.push(PATCH_END_MARKER);
|
|
728
|
-
return lines.join('\n');
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
async function applyEnvelopedPatch(
|
|
732
|
-
projectRoot: string,
|
|
733
|
-
patch: string,
|
|
734
|
-
useFuzzy: boolean = false,
|
|
735
|
-
) {
|
|
736
|
-
const operations = parseEnvelopedPatch(patch);
|
|
737
|
-
const applied: AppliedOperationRecord[] = [];
|
|
738
|
-
|
|
739
|
-
for (const operation of operations) {
|
|
740
|
-
if (operation.kind === 'add') {
|
|
741
|
-
applied.push(await applyAddOperation(projectRoot, operation));
|
|
742
|
-
} else if (operation.kind === 'delete') {
|
|
743
|
-
applied.push(await applyDeleteOperation(projectRoot, operation));
|
|
744
|
-
} else {
|
|
745
|
-
applied.push(
|
|
746
|
-
await applyUpdateOperation(projectRoot, operation, useFuzzy),
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
return {
|
|
752
|
-
operations: applied,
|
|
753
|
-
normalizedPatch: formatNormalizedPatch(applied),
|
|
754
|
-
};
|
|
29
|
+
function serializeRejected(rejected: RejectedPatch[]) {
|
|
30
|
+
if (rejected.length === 0) return undefined;
|
|
31
|
+
return rejected.map((item) => ({
|
|
32
|
+
filePath: item.filePath,
|
|
33
|
+
kind: item.kind,
|
|
34
|
+
reason: item.reason,
|
|
35
|
+
hunks:
|
|
36
|
+
item.operation.kind === 'update'
|
|
37
|
+
? item.operation.hunks.map((hunk) => ({
|
|
38
|
+
oldStart: hunk.header.oldStart,
|
|
39
|
+
oldLines: hunk.header.oldLines,
|
|
40
|
+
newStart: hunk.header.newStart,
|
|
41
|
+
newLines: hunk.header.newLines,
|
|
42
|
+
context: hunk.header.context,
|
|
43
|
+
lines: hunk.lines.map((line) => ({
|
|
44
|
+
kind: line.kind,
|
|
45
|
+
content: line.content,
|
|
46
|
+
})),
|
|
47
|
+
}))
|
|
48
|
+
: undefined,
|
|
49
|
+
}));
|
|
755
50
|
}
|
|
756
51
|
|
|
757
52
|
export function buildApplyPatchTool(projectRoot: string): {
|
|
@@ -779,7 +74,8 @@ export function buildApplyPatchTool(projectRoot: string): {
|
|
|
779
74
|
}),
|
|
780
75
|
async execute({
|
|
781
76
|
patch,
|
|
782
|
-
|
|
77
|
+
allowRejects = false,
|
|
78
|
+
fuzzyMatch = true,
|
|
783
79
|
}: {
|
|
784
80
|
patch: string;
|
|
785
81
|
allowRejects?: boolean;
|
|
@@ -789,6 +85,7 @@ export function buildApplyPatchTool(projectRoot: string): {
|
|
|
789
85
|
output: string;
|
|
790
86
|
changes: unknown[];
|
|
791
87
|
artifact: unknown;
|
|
88
|
+
rejected?: unknown[];
|
|
792
89
|
}>
|
|
793
90
|
> {
|
|
794
91
|
if (!patch || patch.trim().length === 0) {
|
|
@@ -803,53 +100,55 @@ export function buildApplyPatchTool(projectRoot: string): {
|
|
|
803
100
|
);
|
|
804
101
|
}
|
|
805
102
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
},
|
|
818
|
-
);
|
|
103
|
+
let operations: PatchOperation[];
|
|
104
|
+
try {
|
|
105
|
+
const parsed = parsePatchInput(patch);
|
|
106
|
+
operations = parsed.operations;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
return createToolError(message, 'validation', {
|
|
110
|
+
parameter: 'patch',
|
|
111
|
+
suggestion:
|
|
112
|
+
'Provide patch content using the enveloped format (*** Begin Patch ... *** End Patch) or standard unified diff format (---/+++ headers).',
|
|
113
|
+
});
|
|
819
114
|
}
|
|
820
115
|
|
|
821
116
|
try {
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
}
|
|
117
|
+
const result = await applyPatchOperations(projectRoot, operations, {
|
|
118
|
+
useFuzzy: fuzzyMatch,
|
|
119
|
+
allowRejects,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const changes = serializeChanges(result.operations);
|
|
123
|
+
const rejected = serializeRejected(result.rejected);
|
|
124
|
+
|
|
125
|
+
const output: string[] = [];
|
|
126
|
+
if (result.operations.length > 0) {
|
|
127
|
+
output.push(
|
|
128
|
+
`Applied ${result.operations.length} operation${result.operations.length === 1 ? '' : 's'}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (allowRejects && result.rejected.length > 0) {
|
|
132
|
+
output.push(
|
|
133
|
+
`Skipped ${result.rejected.length} operation${result.rejected.length === 1 ? '' : 's'} due to mismatches`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (output.length === 0) {
|
|
137
|
+
output.push('No changes applied');
|
|
138
|
+
}
|
|
841
139
|
|
|
842
140
|
return {
|
|
843
141
|
ok: true,
|
|
844
|
-
output: '
|
|
142
|
+
output: output.join('; '),
|
|
845
143
|
changes,
|
|
846
144
|
artifact: {
|
|
847
145
|
kind: 'file_diff',
|
|
848
|
-
patch: normalizedPatch,
|
|
849
|
-
summary,
|
|
146
|
+
patch: result.normalizedPatch,
|
|
147
|
+
summary: result.summary,
|
|
850
148
|
},
|
|
149
|
+
rejected,
|
|
851
150
|
};
|
|
852
|
-
} catch (error
|
|
151
|
+
} catch (error) {
|
|
853
152
|
const errorMessage =
|
|
854
153
|
error instanceof Error ? error.message : String(error);
|
|
855
154
|
return createToolError(
|
|
@@ -863,5 +162,6 @@ export function buildApplyPatchTool(projectRoot: string): {
|
|
|
863
162
|
}
|
|
864
163
|
},
|
|
865
164
|
});
|
|
165
|
+
|
|
866
166
|
return { name: 'apply_patch', tool: applyPatch };
|
|
867
167
|
}
|