@agi-cli/sdk 0.1.81 → 0.1.83
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/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 -785
- package/src/core/src/tools/builtin/patch.txt +64 -4
- package/src/prompts/src/agents/build.txt +47 -0
- package/src/prompts/src/providers/anthropic.txt +0 -46
- package/src/prompts/src/providers/default.txt +3 -0
- package/src/prompts/src/providers/google.txt +0 -48
- package/src/prompts/src/providers/openai.txt +0 -51
|
@@ -1,759 +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
|
-
// Auto-correct: treat lines without prefix as context (with implicit space)
|
|
355
|
-
// This makes the parser more forgiving for AI-generated patches
|
|
356
|
-
currentHunk.lines.push({ kind: 'context', content: line });
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (!encounteredEnd) {
|
|
361
|
-
throw new Error('Missing "*** End Patch" marker.');
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return operations;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function resolveProjectPath(projectRoot: string, filePath: string): string {
|
|
368
|
-
const fullPath = resolve(projectRoot, filePath);
|
|
369
|
-
const relativePath = relative(projectRoot, fullPath);
|
|
370
|
-
if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
|
|
371
|
-
throw new Error(`Patch path escapes project root: ${filePath}`);
|
|
372
|
-
}
|
|
373
|
-
return fullPath;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function applyAddOperation(
|
|
377
|
-
projectRoot: string,
|
|
378
|
-
operation: PatchAddOperation,
|
|
379
|
-
): Promise<AppliedOperationRecord> {
|
|
380
|
-
const targetPath = resolveProjectPath(projectRoot, operation.filePath);
|
|
381
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
382
|
-
const linesForWrite = [...operation.lines];
|
|
383
|
-
ensureTrailingNewline(linesForWrite);
|
|
384
|
-
await writeFile(targetPath, joinLines(linesForWrite, '\n'), 'utf-8');
|
|
385
|
-
|
|
386
|
-
const hunkLines: PatchHunkLine[] = operation.lines.map((line) => ({
|
|
387
|
-
kind: 'add',
|
|
388
|
-
content: line,
|
|
389
|
-
}));
|
|
390
|
-
|
|
391
|
-
return {
|
|
392
|
-
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) => ({
|
|
393
15
|
filePath: operation.filePath,
|
|
394
|
-
operation,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
oldLines: 0,
|
|
405
|
-
newStart: 1,
|
|
406
|
-
newLines: hunkLines.length,
|
|
407
|
-
additions: hunkLines.length,
|
|
408
|
-
deletions: 0,
|
|
409
|
-
},
|
|
410
|
-
],
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
async function applyDeleteOperation(
|
|
415
|
-
projectRoot: string,
|
|
416
|
-
operation: PatchDeleteOperation,
|
|
417
|
-
): Promise<AppliedOperationRecord> {
|
|
418
|
-
const targetPath = resolveProjectPath(projectRoot, operation.filePath);
|
|
419
|
-
let existingContent = '';
|
|
420
|
-
try {
|
|
421
|
-
existingContent = await readFile(targetPath, 'utf-8');
|
|
422
|
-
} catch (error) {
|
|
423
|
-
if (isErrnoException(error) && error.code === 'ENOENT') {
|
|
424
|
-
throw new Error(`File not found for deletion: ${operation.filePath}`);
|
|
425
|
-
}
|
|
426
|
-
throw error;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const { lines } = splitLines(existingContent);
|
|
430
|
-
await unlink(targetPath);
|
|
431
|
-
|
|
432
|
-
const hunkLines: PatchHunkLine[] = lines.map((line) => ({
|
|
433
|
-
kind: 'remove',
|
|
434
|
-
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
|
+
})),
|
|
435
26
|
}));
|
|
436
|
-
|
|
437
|
-
return {
|
|
438
|
-
kind: 'delete',
|
|
439
|
-
filePath: operation.filePath,
|
|
440
|
-
operation,
|
|
441
|
-
stats: {
|
|
442
|
-
additions: 0,
|
|
443
|
-
deletions: hunkLines.length,
|
|
444
|
-
},
|
|
445
|
-
hunks: [
|
|
446
|
-
{
|
|
447
|
-
header: {},
|
|
448
|
-
lines: hunkLines,
|
|
449
|
-
oldStart: 1,
|
|
450
|
-
oldLines: hunkLines.length,
|
|
451
|
-
newStart: 0,
|
|
452
|
-
newLines: 0,
|
|
453
|
-
additions: 0,
|
|
454
|
-
deletions: hunkLines.length,
|
|
455
|
-
},
|
|
456
|
-
],
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function applyHunksToLines(
|
|
461
|
-
originalLines: string[],
|
|
462
|
-
hunks: PatchHunk[],
|
|
463
|
-
filePath: string,
|
|
464
|
-
useFuzzy: boolean = false,
|
|
465
|
-
): { lines: string[]; applied: AppliedHunkResult[] } {
|
|
466
|
-
const lines = [...originalLines];
|
|
467
|
-
let searchIndex = 0;
|
|
468
|
-
let lineOffset = 0;
|
|
469
|
-
const applied: AppliedHunkResult[] = [];
|
|
470
|
-
|
|
471
|
-
for (const hunk of hunks) {
|
|
472
|
-
const expected: string[] = [];
|
|
473
|
-
const replacement: string[] = [];
|
|
474
|
-
let additions = 0;
|
|
475
|
-
let deletions = 0;
|
|
476
|
-
|
|
477
|
-
for (const line of hunk.lines) {
|
|
478
|
-
if (line.kind !== 'add') {
|
|
479
|
-
expected.push(line.content);
|
|
480
|
-
}
|
|
481
|
-
if (line.kind !== 'remove') {
|
|
482
|
-
replacement.push(line.content);
|
|
483
|
-
}
|
|
484
|
-
if (line.kind === 'add') additions += 1;
|
|
485
|
-
if (line.kind === 'remove') deletions += 1;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const hasExpected = expected.length > 0;
|
|
489
|
-
const hint =
|
|
490
|
-
typeof hunk.header.oldStart === 'number'
|
|
491
|
-
? Math.max(0, hunk.header.oldStart - 1 + lineOffset)
|
|
492
|
-
: searchIndex;
|
|
493
|
-
|
|
494
|
-
let matchIndex = hasExpected
|
|
495
|
-
? findSubsequenceWithFuzzy(
|
|
496
|
-
lines,
|
|
497
|
-
expected,
|
|
498
|
-
Math.max(0, hint - 3),
|
|
499
|
-
useFuzzy,
|
|
500
|
-
)
|
|
501
|
-
: -1;
|
|
502
|
-
|
|
503
|
-
if (hasExpected && matchIndex === -1) {
|
|
504
|
-
matchIndex = findSubsequenceWithFuzzy(lines, expected, 0, useFuzzy);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (matchIndex === -1 && hasExpected && hunk.header.context) {
|
|
508
|
-
const contextIndex = findSubsequence(lines, [hunk.header.context], 0);
|
|
509
|
-
if (contextIndex !== -1) {
|
|
510
|
-
const positionInExpected = expected.indexOf(hunk.header.context);
|
|
511
|
-
matchIndex =
|
|
512
|
-
positionInExpected >= 0
|
|
513
|
-
? Math.max(0, contextIndex - positionInExpected)
|
|
514
|
-
: contextIndex;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (!hasExpected) {
|
|
519
|
-
matchIndex = computeInsertionIndex(lines, hint, hunk.header);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
if (matchIndex === -1) {
|
|
523
|
-
const contextInfo = hunk.header.context
|
|
524
|
-
? ` near context '${hunk.header.context}'`
|
|
525
|
-
: '';
|
|
526
|
-
|
|
527
|
-
// Provide helpful error with nearby context
|
|
528
|
-
const nearbyStart = Math.max(0, hint - 2);
|
|
529
|
-
const nearbyEnd = Math.min(lines.length, hint + 5);
|
|
530
|
-
const nearbyLines = lines.slice(nearbyStart, nearbyEnd);
|
|
531
|
-
const lineNumberInfo =
|
|
532
|
-
nearbyStart > 0 ? ` (around line ${nearbyStart + 1})` : '';
|
|
533
|
-
|
|
534
|
-
let errorMsg = `Failed to apply patch hunk in ${filePath}${contextInfo}.\n`;
|
|
535
|
-
errorMsg += `Expected to find:\n${expected.map((l) => ` ${l}`).join('\n')}\n`;
|
|
536
|
-
errorMsg += `Nearby context${lineNumberInfo}:\n${nearbyLines.map((l, idx) => ` ${nearbyStart + idx + 1}: ${l}`).join('\n')}\n`;
|
|
537
|
-
errorMsg += `Hint: Check for whitespace differences (tabs vs spaces). Try enabling fuzzyMatch option.`;
|
|
538
|
-
|
|
539
|
-
throw new Error(errorMsg);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const deleteCount = hasExpected ? expected.length : 0;
|
|
543
|
-
const originalIndex = matchIndex - lineOffset;
|
|
544
|
-
const normalizedOriginalIndex = Math.min(
|
|
545
|
-
Math.max(0, originalIndex),
|
|
546
|
-
originalLines.length,
|
|
547
|
-
);
|
|
548
|
-
const oldStart = normalizedOriginalIndex + 1;
|
|
549
|
-
const newStart = matchIndex + 1;
|
|
550
|
-
|
|
551
|
-
lines.splice(matchIndex, deleteCount, ...replacement);
|
|
552
|
-
searchIndex = matchIndex + replacement.length;
|
|
553
|
-
lineOffset += replacement.length - deleteCount;
|
|
554
|
-
|
|
555
|
-
applied.push({
|
|
556
|
-
header: { ...hunk.header },
|
|
557
|
-
lines: hunk.lines.map((line) => ({ ...line })),
|
|
558
|
-
oldStart,
|
|
559
|
-
oldLines: deleteCount,
|
|
560
|
-
newStart,
|
|
561
|
-
newLines: replacement.length,
|
|
562
|
-
additions,
|
|
563
|
-
deletions,
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return { lines, applied };
|
|
568
27
|
}
|
|
569
28
|
|
|
570
|
-
function
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
return Math.min(lines.length, Math.max(0, hint));
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
async function applyUpdateOperation(
|
|
596
|
-
projectRoot: string,
|
|
597
|
-
operation: PatchUpdateOperation,
|
|
598
|
-
useFuzzy: boolean = false,
|
|
599
|
-
): Promise<AppliedOperationRecord> {
|
|
600
|
-
const targetPath = resolveProjectPath(projectRoot, operation.filePath);
|
|
601
|
-
let originalContent: string;
|
|
602
|
-
try {
|
|
603
|
-
originalContent = await readFile(targetPath, 'utf-8');
|
|
604
|
-
} catch (error) {
|
|
605
|
-
if (isErrnoException(error) && error.code === 'ENOENT') {
|
|
606
|
-
throw new Error(`File not found: ${operation.filePath}`);
|
|
607
|
-
}
|
|
608
|
-
throw error;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const { lines: originalLines, newline } = splitLines(originalContent);
|
|
612
|
-
const { lines: updatedLines, applied } = applyHunksToLines(
|
|
613
|
-
originalLines,
|
|
614
|
-
operation.hunks,
|
|
615
|
-
operation.filePath,
|
|
616
|
-
useFuzzy,
|
|
617
|
-
);
|
|
618
|
-
ensureTrailingNewline(updatedLines);
|
|
619
|
-
await writeFile(targetPath, joinLines(updatedLines, newline), 'utf-8');
|
|
620
|
-
|
|
621
|
-
const stats = applied.reduce<PatchStats>(
|
|
622
|
-
(acc, hunk) => ({
|
|
623
|
-
additions: acc.additions + hunk.additions,
|
|
624
|
-
deletions: acc.deletions + hunk.deletions,
|
|
625
|
-
}),
|
|
626
|
-
{ additions: 0, deletions: 0 },
|
|
627
|
-
);
|
|
628
|
-
|
|
629
|
-
return {
|
|
630
|
-
kind: 'update',
|
|
631
|
-
filePath: operation.filePath,
|
|
632
|
-
operation,
|
|
633
|
-
stats,
|
|
634
|
-
hunks: applied,
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function summarizeOperations(operations: AppliedOperationRecord[]) {
|
|
639
|
-
const summary = operations.reduce(
|
|
640
|
-
(acc, op) => ({
|
|
641
|
-
files: acc.files + 1,
|
|
642
|
-
additions: acc.additions + op.stats.additions,
|
|
643
|
-
deletions: acc.deletions + op.stats.deletions,
|
|
644
|
-
}),
|
|
645
|
-
{ files: 0, additions: 0, deletions: 0 },
|
|
646
|
-
);
|
|
647
|
-
return {
|
|
648
|
-
files: Math.max(summary.files, operations.length > 0 ? 1 : 0),
|
|
649
|
-
additions: summary.additions,
|
|
650
|
-
deletions: summary.deletions,
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function formatRange(start: number, count: number) {
|
|
655
|
-
const normalizedStart = Math.max(0, start);
|
|
656
|
-
if (count === 0) return `${normalizedStart},0`;
|
|
657
|
-
if (count === 1) return `${normalizedStart}`;
|
|
658
|
-
return `${normalizedStart},${count}`;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function formatHunkHeader(applied: AppliedHunkResult) {
|
|
662
|
-
const oldRange = formatRange(applied.oldStart, applied.oldLines);
|
|
663
|
-
const newRange = formatRange(applied.newStart, applied.newLines);
|
|
664
|
-
const context = applied.header.context?.trim();
|
|
665
|
-
return context
|
|
666
|
-
? `@@ -${oldRange} +${newRange} @@ ${context}`
|
|
667
|
-
: `@@ -${oldRange} +${newRange} @@`;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function serializePatchLine(line: PatchHunkLine): string {
|
|
671
|
-
switch (line.kind) {
|
|
672
|
-
case 'add':
|
|
673
|
-
return `+${line.content}`;
|
|
674
|
-
case 'remove':
|
|
675
|
-
return `-${line.content}`;
|
|
676
|
-
default:
|
|
677
|
-
return ` ${line.content}`;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function formatNormalizedPatch(operations: AppliedOperationRecord[]): string {
|
|
682
|
-
const lines: string[] = [PATCH_BEGIN_MARKER];
|
|
683
|
-
|
|
684
|
-
for (const op of operations) {
|
|
685
|
-
switch (op.kind) {
|
|
686
|
-
case 'add': {
|
|
687
|
-
lines.push(`${PATCH_ADD_PREFIX} ${op.filePath}`);
|
|
688
|
-
for (const hunk of op.hunks) {
|
|
689
|
-
lines.push(formatHunkHeader(hunk));
|
|
690
|
-
for (const line of hunk.lines) {
|
|
691
|
-
lines.push(serializePatchLine(line));
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
break;
|
|
695
|
-
}
|
|
696
|
-
case 'delete': {
|
|
697
|
-
lines.push(`${PATCH_DELETE_PREFIX} ${op.filePath}`);
|
|
698
|
-
for (const hunk of op.hunks) {
|
|
699
|
-
lines.push(formatHunkHeader(hunk));
|
|
700
|
-
for (const line of hunk.lines) {
|
|
701
|
-
lines.push(serializePatchLine(line));
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
break;
|
|
705
|
-
}
|
|
706
|
-
case 'update': {
|
|
707
|
-
lines.push(`${PATCH_UPDATE_PREFIX} ${op.filePath}`);
|
|
708
|
-
const updateOp = op.operation as PatchUpdateOperation;
|
|
709
|
-
for (let i = 0; i < updateOp.hunks.length; i++) {
|
|
710
|
-
const originalHunk = updateOp.hunks[i];
|
|
711
|
-
const appliedHunk = op.hunks[i];
|
|
712
|
-
const header: AppliedHunkResult = {
|
|
713
|
-
...appliedHunk,
|
|
714
|
-
header: {
|
|
715
|
-
...appliedHunk.header,
|
|
716
|
-
context: originalHunk.header.context,
|
|
717
|
-
},
|
|
718
|
-
};
|
|
719
|
-
lines.push(formatHunkHeader(header));
|
|
720
|
-
for (const line of originalHunk.lines) {
|
|
721
|
-
lines.push(serializePatchLine(line));
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
break;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
lines.push(PATCH_END_MARKER);
|
|
730
|
-
return lines.join('\n');
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
async function applyEnvelopedPatch(
|
|
734
|
-
projectRoot: string,
|
|
735
|
-
patch: string,
|
|
736
|
-
useFuzzy: boolean = false,
|
|
737
|
-
) {
|
|
738
|
-
const operations = parseEnvelopedPatch(patch);
|
|
739
|
-
const applied: AppliedOperationRecord[] = [];
|
|
740
|
-
|
|
741
|
-
for (const operation of operations) {
|
|
742
|
-
if (operation.kind === 'add') {
|
|
743
|
-
applied.push(await applyAddOperation(projectRoot, operation));
|
|
744
|
-
} else if (operation.kind === 'delete') {
|
|
745
|
-
applied.push(await applyDeleteOperation(projectRoot, operation));
|
|
746
|
-
} else {
|
|
747
|
-
applied.push(
|
|
748
|
-
await applyUpdateOperation(projectRoot, operation, useFuzzy),
|
|
749
|
-
);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
return {
|
|
754
|
-
operations: applied,
|
|
755
|
-
normalizedPatch: formatNormalizedPatch(applied),
|
|
756
|
-
};
|
|
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
|
+
}));
|
|
757
50
|
}
|
|
758
51
|
|
|
759
52
|
export function buildApplyPatchTool(projectRoot: string): {
|
|
@@ -781,7 +74,8 @@ export function buildApplyPatchTool(projectRoot: string): {
|
|
|
781
74
|
}),
|
|
782
75
|
async execute({
|
|
783
76
|
patch,
|
|
784
|
-
|
|
77
|
+
allowRejects = false,
|
|
78
|
+
fuzzyMatch = true,
|
|
785
79
|
}: {
|
|
786
80
|
patch: string;
|
|
787
81
|
allowRejects?: boolean;
|
|
@@ -791,6 +85,7 @@ export function buildApplyPatchTool(projectRoot: string): {
|
|
|
791
85
|
output: string;
|
|
792
86
|
changes: unknown[];
|
|
793
87
|
artifact: unknown;
|
|
88
|
+
rejected?: unknown[];
|
|
794
89
|
}>
|
|
795
90
|
> {
|
|
796
91
|
if (!patch || patch.trim().length === 0) {
|
|
@@ -805,53 +100,55 @@ export function buildApplyPatchTool(projectRoot: string): {
|
|
|
805
100
|
);
|
|
806
101
|
}
|
|
807
102
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
},
|
|
820
|
-
);
|
|
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
|
+
});
|
|
821
114
|
}
|
|
822
115
|
|
|
823
116
|
try {
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
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
|
+
}
|
|
843
139
|
|
|
844
140
|
return {
|
|
845
141
|
ok: true,
|
|
846
|
-
output: '
|
|
142
|
+
output: output.join('; '),
|
|
847
143
|
changes,
|
|
848
144
|
artifact: {
|
|
849
145
|
kind: 'file_diff',
|
|
850
|
-
patch: normalizedPatch,
|
|
851
|
-
summary,
|
|
146
|
+
patch: result.normalizedPatch,
|
|
147
|
+
summary: result.summary,
|
|
852
148
|
},
|
|
149
|
+
rejected,
|
|
853
150
|
};
|
|
854
|
-
} catch (error
|
|
151
|
+
} catch (error) {
|
|
855
152
|
const errorMessage =
|
|
856
153
|
error instanceof Error ? error.message : String(error);
|
|
857
154
|
return createToolError(
|
|
@@ -865,5 +162,6 @@ export function buildApplyPatchTool(projectRoot: string): {
|
|
|
865
162
|
}
|
|
866
163
|
},
|
|
867
164
|
});
|
|
165
|
+
|
|
868
166
|
return { name: 'apply_patch', tool: applyPatch };
|
|
869
167
|
}
|