@myagentroam/agent 0.10.16 → 0.10.18
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.
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type PatchHunk = {
|
|
2
|
+
changeContext?: string;
|
|
3
|
+
oldLines: string[];
|
|
4
|
+
patchLines: Array<{
|
|
5
|
+
kind: 'context' | 'addition' | 'deletion';
|
|
6
|
+
value: string;
|
|
7
|
+
}>;
|
|
8
|
+
addedLines: number;
|
|
9
|
+
deletedLines: number;
|
|
10
|
+
anchor: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type PatchChange = {
|
|
13
|
+
kind: 'add';
|
|
14
|
+
path: string;
|
|
15
|
+
lines: string[];
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'delete';
|
|
18
|
+
path: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'update';
|
|
21
|
+
path: string;
|
|
22
|
+
targetPath?: string;
|
|
23
|
+
hunks: PatchHunk[];
|
|
24
|
+
};
|
|
25
|
+
export declare function parsePatch(patch: string): PatchChange[];
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { MarAgentError } from '../../error.js';
|
|
3
|
+
import { TOOL_EXECUTION_LIMITS } from '../execution-limits.js';
|
|
4
|
+
const beginMarker = '*** Begin Patch';
|
|
5
|
+
const endMarker = '*** End Patch';
|
|
6
|
+
const addHeader = '*** Add File: ';
|
|
7
|
+
const updateHeader = '*** Update File: ';
|
|
8
|
+
const deleteHeader = '*** Delete File: ';
|
|
9
|
+
const moveHeader = '*** Move to: ';
|
|
10
|
+
const syntaxTokens = [
|
|
11
|
+
`${beginMarker} ***`,
|
|
12
|
+
`${endMarker} ***`,
|
|
13
|
+
addHeader,
|
|
14
|
+
updateHeader,
|
|
15
|
+
deleteHeader,
|
|
16
|
+
moveHeader,
|
|
17
|
+
'@@ ',
|
|
18
|
+
'+',
|
|
19
|
+
'-',
|
|
20
|
+
' '
|
|
21
|
+
];
|
|
22
|
+
export function parsePatch(patch) {
|
|
23
|
+
if (typeof patch !== 'string')
|
|
24
|
+
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Patch must be a string.');
|
|
25
|
+
if (Buffer.byteLength(patch) > TOOL_EXECUTION_LIMITS.patchMaxBytes)
|
|
26
|
+
throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Patch exceeds the input byte limit.');
|
|
27
|
+
return new FreeformPatchParser(patch).parse();
|
|
28
|
+
}
|
|
29
|
+
class FreeformPatchParser {
|
|
30
|
+
#lines;
|
|
31
|
+
#lineOffsets = [];
|
|
32
|
+
#claimed = new Set();
|
|
33
|
+
#index = 1;
|
|
34
|
+
constructor(patch) {
|
|
35
|
+
const sourceLines = patch.split('\n');
|
|
36
|
+
let offset = 0;
|
|
37
|
+
this.#lines = sourceLines.map((line, index) => {
|
|
38
|
+
this.#lineOffsets.push(offset);
|
|
39
|
+
offset += line.length + 1;
|
|
40
|
+
return index < sourceLines.length - 1 && line.endsWith('\r') ? line.slice(0, -1) : line;
|
|
41
|
+
});
|
|
42
|
+
if (patch.endsWith('\n'))
|
|
43
|
+
this.#lines.pop();
|
|
44
|
+
}
|
|
45
|
+
parse() {
|
|
46
|
+
for (const [index, line] of this.#lines.entries()) {
|
|
47
|
+
const column = line.indexOf('\r');
|
|
48
|
+
if (column !== -1)
|
|
49
|
+
this.#fail('Patch contains unsupported bare carriage returns.', index, column);
|
|
50
|
+
}
|
|
51
|
+
this.#marker(0, beginMarker, 'first');
|
|
52
|
+
this.#marker(this.#lines.length - 1, endMarker, 'last');
|
|
53
|
+
if (this.#lines.length < 3)
|
|
54
|
+
this.#fail('Patch must contain at least one operation.', 1);
|
|
55
|
+
const changes = [];
|
|
56
|
+
while (this.#index < this.#lines.length - 1) {
|
|
57
|
+
const headerIndex = this.#index++;
|
|
58
|
+
const header = this.#lines[headerIndex];
|
|
59
|
+
if (header.startsWith(addHeader)) {
|
|
60
|
+
const path = this.#path(headerIndex, addHeader);
|
|
61
|
+
const lines = [];
|
|
62
|
+
while (this.#hasBodyLine()) {
|
|
63
|
+
const index = this.#index++;
|
|
64
|
+
const line = this.#lines[index];
|
|
65
|
+
if (!line.startsWith('+'))
|
|
66
|
+
this.#fail('Added file lines must start with +.', index);
|
|
67
|
+
lines.push(line.slice(1));
|
|
68
|
+
}
|
|
69
|
+
changes.push({ kind: 'add', path, lines });
|
|
70
|
+
}
|
|
71
|
+
else if (header.startsWith(deleteHeader)) {
|
|
72
|
+
changes.push({ kind: 'delete', path: this.#path(headerIndex, deleteHeader) });
|
|
73
|
+
}
|
|
74
|
+
else if (header.startsWith(updateHeader)) {
|
|
75
|
+
const path = this.#path(headerIndex, updateHeader);
|
|
76
|
+
let targetPath;
|
|
77
|
+
if (this.#lines[this.#index]?.startsWith(moveHeader)) {
|
|
78
|
+
targetPath = this.#path(this.#index++, moveHeader);
|
|
79
|
+
}
|
|
80
|
+
const hunks = [];
|
|
81
|
+
while (this.#index < this.#lines.length - 1 && this.#lines[this.#index].startsWith('@@'))
|
|
82
|
+
hunks.push(this.#hunk());
|
|
83
|
+
if (!hunks.length && !targetPath)
|
|
84
|
+
this.#fail('Update operations must contain hunks or move a file.', headerIndex);
|
|
85
|
+
changes.push({ kind: 'update', path, ...(targetPath ? { targetPath } : {}), hunks });
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.#fail("Expected an operation header: '*** Add File: <path>', '*** Update File: <path>', or '*** Delete File: <path>'.", headerIndex, Math.max(...[addHeader, updateHeader, deleteHeader].map((prefix) => commonPrefixLength(header, prefix))));
|
|
89
|
+
}
|
|
90
|
+
if (changes.length > TOOL_EXECUTION_LIMITS.patchMaxFiles)
|
|
91
|
+
throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Patch exceeds the file limit.');
|
|
92
|
+
}
|
|
93
|
+
if (!changes.some((change) => change.kind !== 'update' || change.targetPath || change.hunks.some((hunk) => !hunk.anchor)))
|
|
94
|
+
this.#fail('Patch must change or move at least one file.', 1);
|
|
95
|
+
return changes;
|
|
96
|
+
}
|
|
97
|
+
#marker(index, marker, position) {
|
|
98
|
+
const line = this.#lines[index];
|
|
99
|
+
const trimmed = line.trim();
|
|
100
|
+
if (trimmed === marker || trimmed === `${marker} ***`)
|
|
101
|
+
return;
|
|
102
|
+
const leading = line.length - line.trimStart().length;
|
|
103
|
+
const expected = trimmed.startsWith(`${marker} ***`) ? `${marker} ***` : marker;
|
|
104
|
+
this.#fail(`Expected ${position} line to be '${marker}' (optional trailing ' ***').`, index, leading + commonPrefixLength(trimmed, expected));
|
|
105
|
+
}
|
|
106
|
+
#path(index, prefix) {
|
|
107
|
+
const path = this.#lines[index].slice(prefix.length);
|
|
108
|
+
if (!path || path.trim() !== path) {
|
|
109
|
+
const column = path.trimStart() !== path ? prefix.length : prefix.length + path.trimEnd().length;
|
|
110
|
+
this.#fail('Patch paths must be non-empty and unpadded.', index, column);
|
|
111
|
+
}
|
|
112
|
+
const key = isAbsolute(path) ? resolve(path) : resolve('/', path);
|
|
113
|
+
if (this.#claimed.has(key))
|
|
114
|
+
this.#fail('Patch contains duplicate or overlapping paths.', index, prefix.length);
|
|
115
|
+
this.#claimed.add(key);
|
|
116
|
+
return path;
|
|
117
|
+
}
|
|
118
|
+
#hasBodyLine() {
|
|
119
|
+
return this.#index < this.#lines.length - 1 && !this.#lines[this.#index].startsWith('*** ');
|
|
120
|
+
}
|
|
121
|
+
#hunk() {
|
|
122
|
+
const index = this.#index++;
|
|
123
|
+
const header = this.#lines[index];
|
|
124
|
+
let changeContext;
|
|
125
|
+
if (header !== '@@') {
|
|
126
|
+
if (!header.startsWith('@@ '))
|
|
127
|
+
this.#fail('Patch change context headers must use @@ or @@ <context>.', index, 2);
|
|
128
|
+
changeContext = header.slice(3);
|
|
129
|
+
if (!changeContext.trim())
|
|
130
|
+
this.#fail('Patch change context must be non-empty.', index, 3);
|
|
131
|
+
}
|
|
132
|
+
const oldLines = [];
|
|
133
|
+
const patchLines = [];
|
|
134
|
+
let addedLines = 0;
|
|
135
|
+
let deletedLines = 0;
|
|
136
|
+
while (this.#hasBodyLine() && !this.#lines[this.#index].startsWith('@@')) {
|
|
137
|
+
const lineIndex = this.#index++;
|
|
138
|
+
const line = this.#lines[lineIndex];
|
|
139
|
+
const value = line.slice(1);
|
|
140
|
+
if (line.startsWith(' ')) {
|
|
141
|
+
oldLines.push(value);
|
|
142
|
+
patchLines.push({ kind: 'context', value });
|
|
143
|
+
}
|
|
144
|
+
else if (line.startsWith('+')) {
|
|
145
|
+
patchLines.push({ kind: 'addition', value });
|
|
146
|
+
addedLines++;
|
|
147
|
+
}
|
|
148
|
+
else if (line.startsWith('-')) {
|
|
149
|
+
oldLines.push(value);
|
|
150
|
+
patchLines.push({ kind: 'deletion', value });
|
|
151
|
+
deletedLines++;
|
|
152
|
+
}
|
|
153
|
+
else
|
|
154
|
+
this.#fail('Patch hunk lines must start with space, +, or -.', lineIndex);
|
|
155
|
+
}
|
|
156
|
+
if (!oldLines.length)
|
|
157
|
+
this.#fail('Insertion hunks require context.', index);
|
|
158
|
+
return {
|
|
159
|
+
...(changeContext === undefined ? {} : { changeContext }),
|
|
160
|
+
oldLines,
|
|
161
|
+
patchLines,
|
|
162
|
+
addedLines,
|
|
163
|
+
deletedLines,
|
|
164
|
+
anchor: !addedLines && !deletedLines
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
#fail(message, index, characterOffset = 0) {
|
|
168
|
+
const source = this.#lines[index];
|
|
169
|
+
const line = index + 1;
|
|
170
|
+
const column = characterCount(source.slice(0, characterOffset)) + 1;
|
|
171
|
+
const offset = this.#lineOffsets[index] + characterOffset;
|
|
172
|
+
const preview = syntaxPreview(source, characterOffset);
|
|
173
|
+
const gutter = String(line);
|
|
174
|
+
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', `Patch syntax error at line ${line}, column ${column}: ${message}\n${gutter} | ${preview.text}\n${' '.repeat(gutter.length)} | ${' '.repeat(preview.caret)}^`, { details: { line, column, offset } });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function commonPrefixLength(actual, expected) {
|
|
178
|
+
let index = 0;
|
|
179
|
+
while (index < actual.length && index < expected.length && actual[index] === expected[index])
|
|
180
|
+
index++;
|
|
181
|
+
return index;
|
|
182
|
+
}
|
|
183
|
+
function characterCount(value) {
|
|
184
|
+
let count = 0;
|
|
185
|
+
for (const _character of value)
|
|
186
|
+
count++;
|
|
187
|
+
return count;
|
|
188
|
+
}
|
|
189
|
+
function syntaxPreview(source, characterOffset) {
|
|
190
|
+
// Only grammar tokens and one offending character are public; paths and file bodies are not.
|
|
191
|
+
const trimmed = source.trimStart();
|
|
192
|
+
const leading = source.length - trimmed.length;
|
|
193
|
+
const tokenLength = Math.max(...syntaxTokens.map((token) => commonPrefixLength(trimmed, token)));
|
|
194
|
+
const token = trimmed.slice(0, tokenLength);
|
|
195
|
+
if (characterOffset < leading) {
|
|
196
|
+
const before = characterOffset > 40 ? '… ' : displaySyntax(source.slice(0, characterOffset));
|
|
197
|
+
return {
|
|
198
|
+
text: `${before}${displayCharacter(source[characterOffset])}<content omitted>`,
|
|
199
|
+
caret: before.length
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const prefix = leading > 40 ? '… ' : displaySyntax(source.slice(0, leading));
|
|
203
|
+
const syntaxEnd = leading + tokenLength;
|
|
204
|
+
if (characterOffset < syntaxEnd) {
|
|
205
|
+
return {
|
|
206
|
+
text: `${prefix}${token}${source.length > syntaxEnd ? '<content omitted>' : ''}`,
|
|
207
|
+
caret: prefix.length + characterOffset - leading
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const gap = characterOffset > syntaxEnd ? '<content omitted>' : '';
|
|
211
|
+
const character = source.slice(characterOffset)[Symbol.iterator]().next().value;
|
|
212
|
+
const before = `${prefix}${token}${gap}`;
|
|
213
|
+
return {
|
|
214
|
+
text: `${before}${character === undefined ? '<end of line>' : displayCharacter(character)}${character !== undefined && source.length > characterOffset + character.length ? '<content omitted>' : ''}`,
|
|
215
|
+
caret: before.length
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function displayCharacter(character) {
|
|
219
|
+
const point = character.codePointAt(0);
|
|
220
|
+
if (point >= 0x7f)
|
|
221
|
+
return `\\u{${point.toString(16)}}`;
|
|
222
|
+
return JSON.stringify(character).slice(1, -1);
|
|
223
|
+
}
|
|
224
|
+
function displaySyntax(value) {
|
|
225
|
+
return [...value]
|
|
226
|
+
.map((character) => (character === '\t' ? ' ' : displayCharacter(character)))
|
|
227
|
+
.join('');
|
|
228
|
+
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { chmod, lstat, mkdir, readFile, rename, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname,
|
|
3
|
+
import { dirname, parse, resolve } from 'node:path';
|
|
4
4
|
import { TextDecoder } from 'node:util';
|
|
5
5
|
import { MarAgentError } from '../../error.js';
|
|
6
6
|
import { TOOL_EXECUTION_LIMITS } from '../execution-limits.js';
|
|
7
7
|
import { captureFileChange, reserveFileChangePaths } from '../result-extensions.js';
|
|
8
|
+
import { parsePatch } from './apply-patch-parser.js';
|
|
8
9
|
export const applyPatchToolDefinition = {
|
|
9
10
|
name: 'apply_patch',
|
|
10
11
|
allowedCallers: ['direct', 'programmatic'],
|
|
11
12
|
parallelSafety: 'serial',
|
|
12
|
-
description: `Preferred for precise, reviewable local edits. Do not batch all task files merely to reduce tool calls. Group only a few tightly coupled edits whose combined patch is quick to generate and review; split broad or independent work into successive bounded patches before composing them. Submit a raw *** Begin Patch / *** End Patch patch. Supports Add File, Update File, Delete File, and Move to. Add File may replace an existing regular file, and Move to may replace an existing regular destination; duplicate operations on the same resolved path remain invalid. Update hunks accept @@ or @@ <change context>. Context-only hunks, including trailing hunks, and unchanged file sections are validated in order but are not written or reported as edits. The patch must include a change or move. Context is searched forward with Codex-style exact, whitespace-tolerant, then common Unicode punctuation-tolerant matching. Syntax and resolved paths are validated before writing, then file units are committed in order. Each file unit is atomic, and a Move keeps its source and destination atomic. On MAR_AGENT_PATCH_PARTIAL, earlier files remain applied; the error lists applied, failed, and pending paths, so retry only failed and pending paths. Existing file permissions, line endings, and final-newline state are preserved. Returns changed file paths and operations with per-file and total line counts when all units succeed. In Code Mode, the Promise resolves on success and rejects with code/message on failure; the returned envelope has data.files and data.totals, not an ok or success flag. Oversized patches are rejected without advertising defensive limits as recommended patch sizes. A first-unit MAR_AGENT_PATCH_CONFLICT applies nothing; hunk match conflicts identify the affected path and hunk so you can reread only that file before retrying.`,
|
|
13
|
+
description: `Preferred for precise, reviewable local edits. Do not batch all task files merely to reduce tool calls. Group only a few tightly coupled edits whose combined patch is quick to generate and review; split broad or independent work into successive bounded patches before composing them. Submit a raw *** Begin Patch / *** End Patch patch, each marker on its own line. Surrounding marker whitespace and an optional trailing *** are accepted; incomplete markers or extra text are rejected. Add File content lines, including blank lines, must start with +. Syntax errors include line/column and a bounded caret diagnostic with file content omitted. Supports Add File, Update File, Delete File, and Move to. Add File may replace an existing regular file, and Move to may replace an existing regular destination; duplicate operations on the same resolved path remain invalid. Update hunks accept @@ or @@ <change context>. Context-only hunks, including trailing hunks, and unchanged file sections are validated in order but are not written or reported as edits. The patch must include a change or move. Context is searched forward with Codex-style exact, whitespace-tolerant, then common Unicode punctuation-tolerant matching. Syntax and resolved paths are validated before writing, then file units are committed in order. Each file unit is atomic, and a Move keeps its source and destination atomic. On MAR_AGENT_PATCH_PARTIAL, earlier files remain applied; the error lists applied, failed, and pending paths, so retry only failed and pending paths. Existing file permissions, line endings, and final-newline state are preserved. Returns changed file paths and operations with per-file and total line counts when all units succeed. In Code Mode, the Promise resolves on success and rejects with code/message on failure; the returned envelope has data.files and data.totals, not an ok or success flag. Oversized patches are rejected without advertising defensive limits as recommended patch sizes. A first-unit MAR_AGENT_PATCH_CONFLICT applies nothing; hunk match conflicts identify the affected path and hunk so you can reread only that file before retrying.`,
|
|
13
14
|
inputMode: 'freeform',
|
|
14
15
|
outputSchema: {
|
|
15
16
|
type: 'object',
|
|
@@ -306,121 +307,6 @@ export class ApplyPatchTool {
|
|
|
306
307
|
await captureFileChange(mutation.path, mutation.original, mutation.content);
|
|
307
308
|
}
|
|
308
309
|
}
|
|
309
|
-
function parsePatch(patch) {
|
|
310
|
-
if (typeof patch !== 'string')
|
|
311
|
-
invalid('Patch must be a string.');
|
|
312
|
-
if (Buffer.byteLength(patch) > TOOL_EXECUTION_LIMITS.patchMaxBytes)
|
|
313
|
-
throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Patch exceeds the input byte limit.');
|
|
314
|
-
const normalized = patch.replaceAll('\r\n', '\n');
|
|
315
|
-
if (normalized.includes('\r'))
|
|
316
|
-
invalid('Patch contains unsupported bare carriage returns.');
|
|
317
|
-
const value = normalized.endsWith('\n') ? normalized.slice(0, -1) : normalized;
|
|
318
|
-
if (!value.startsWith('*** Begin Patch\n') || !value.endsWith('\n*** End Patch'))
|
|
319
|
-
invalid('Patch envelope is invalid.');
|
|
320
|
-
const body = value.slice('*** Begin Patch\n'.length, -'\n*** End Patch'.length);
|
|
321
|
-
if (!body)
|
|
322
|
-
invalid('Patch must contain at least one operation.');
|
|
323
|
-
const lines = body.split('\n');
|
|
324
|
-
const changes = [];
|
|
325
|
-
const claimed = new Set();
|
|
326
|
-
let index = 0;
|
|
327
|
-
const claim = (path) => {
|
|
328
|
-
if (!path || path.trim() !== path)
|
|
329
|
-
invalid('Patch paths must be non-empty and unpadded.');
|
|
330
|
-
const key = isAbsolute(path) ? resolve(path) : resolve('/', path);
|
|
331
|
-
if (claimed.has(key))
|
|
332
|
-
invalid('Patch contains duplicate or overlapping paths.');
|
|
333
|
-
claimed.add(key);
|
|
334
|
-
};
|
|
335
|
-
while (index < lines.length) {
|
|
336
|
-
const header = lines[index++];
|
|
337
|
-
if (header.startsWith('*** Add File: ')) {
|
|
338
|
-
const path = header.slice('*** Add File: '.length);
|
|
339
|
-
claim(path);
|
|
340
|
-
const content = [];
|
|
341
|
-
while (index < lines.length && !lines[index].startsWith('*** ')) {
|
|
342
|
-
const line = lines[index++];
|
|
343
|
-
if (!line.startsWith('+'))
|
|
344
|
-
invalid('Added file lines must start with +.');
|
|
345
|
-
content.push(line.slice(1));
|
|
346
|
-
}
|
|
347
|
-
changes.push({ kind: 'add', path, lines: content });
|
|
348
|
-
}
|
|
349
|
-
else if (header.startsWith('*** Delete File: ')) {
|
|
350
|
-
const path = header.slice('*** Delete File: '.length);
|
|
351
|
-
claim(path);
|
|
352
|
-
changes.push({ kind: 'delete', path });
|
|
353
|
-
}
|
|
354
|
-
else if (header.startsWith('*** Update File: ')) {
|
|
355
|
-
const path = header.slice('*** Update File: '.length);
|
|
356
|
-
claim(path);
|
|
357
|
-
let targetPath;
|
|
358
|
-
if (lines[index]?.startsWith('*** Move to: ')) {
|
|
359
|
-
targetPath = lines[index++].slice('*** Move to: '.length);
|
|
360
|
-
claim(targetPath);
|
|
361
|
-
}
|
|
362
|
-
const hunks = [];
|
|
363
|
-
while (index < lines.length && lines[index].startsWith('@@')) {
|
|
364
|
-
const hunkHeader = lines[index++];
|
|
365
|
-
let changeContext;
|
|
366
|
-
if (hunkHeader !== '@@') {
|
|
367
|
-
if (!hunkHeader.startsWith('@@ '))
|
|
368
|
-
invalid('Patch change context headers must use @@ or @@ <context>.');
|
|
369
|
-
changeContext = hunkHeader.slice(3);
|
|
370
|
-
if (!changeContext.trim())
|
|
371
|
-
invalid('Patch change context must be non-empty.');
|
|
372
|
-
}
|
|
373
|
-
const oldLines = [];
|
|
374
|
-
const patchLines = [];
|
|
375
|
-
let addedLines = 0;
|
|
376
|
-
let deletedLines = 0;
|
|
377
|
-
while (index < lines.length &&
|
|
378
|
-
!lines[index].startsWith('@@') &&
|
|
379
|
-
!lines[index].startsWith('*** ')) {
|
|
380
|
-
const line = lines[index++];
|
|
381
|
-
if (line.startsWith(' ')) {
|
|
382
|
-
const value = line.slice(1);
|
|
383
|
-
oldLines.push(value);
|
|
384
|
-
patchLines.push({ kind: 'context', value });
|
|
385
|
-
}
|
|
386
|
-
else if (line.startsWith('+')) {
|
|
387
|
-
const value = line.slice(1);
|
|
388
|
-
patchLines.push({ kind: 'addition', value });
|
|
389
|
-
addedLines++;
|
|
390
|
-
}
|
|
391
|
-
else if (line.startsWith('-')) {
|
|
392
|
-
const value = line.slice(1);
|
|
393
|
-
oldLines.push(value);
|
|
394
|
-
patchLines.push({ kind: 'deletion', value });
|
|
395
|
-
deletedLines++;
|
|
396
|
-
}
|
|
397
|
-
else
|
|
398
|
-
invalid('Patch hunk lines must start with space, +, or -.');
|
|
399
|
-
}
|
|
400
|
-
if (!oldLines.length)
|
|
401
|
-
invalid('Insertion hunks require context.');
|
|
402
|
-
hunks.push({
|
|
403
|
-
...(changeContext === undefined ? {} : { changeContext }),
|
|
404
|
-
oldLines,
|
|
405
|
-
patchLines,
|
|
406
|
-
addedLines,
|
|
407
|
-
deletedLines,
|
|
408
|
-
anchor: !addedLines && !deletedLines
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
if (!hunks.length && !targetPath)
|
|
412
|
-
invalid('Update operations must contain hunks or move a file.');
|
|
413
|
-
changes.push({ kind: 'update', path, ...(targetPath ? { targetPath } : {}), hunks });
|
|
414
|
-
}
|
|
415
|
-
else
|
|
416
|
-
invalid('Patch operation is unsupported.');
|
|
417
|
-
if (changes.length > TOOL_EXECUTION_LIMITS.patchMaxFiles)
|
|
418
|
-
throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Patch exceeds the file limit.');
|
|
419
|
-
}
|
|
420
|
-
if (!changes.some((change) => change.kind !== 'update' || change.targetPath || change.hunks.some((hunk) => !hunk.anchor)))
|
|
421
|
-
invalid('Patch must change or move at least one file.');
|
|
422
|
-
return changes;
|
|
423
|
-
}
|
|
424
310
|
function applyHunks(original, hunks, path) {
|
|
425
311
|
let text;
|
|
426
312
|
try {
|