@aexol/spectral 0.9.81 → 0.9.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/dist/agent/index.js +2 -2
- package/dist/memory/tools/compact-context.d.ts.map +1 -1
- package/dist/memory/tools/compact-context.js +7 -4
- package/dist/sdk/agent-core/harness/agent-harness.js +1 -1
- package/dist/sdk/agent-core/harness/types.d.ts +2 -0
- package/dist/sdk/agent-core/harness/types.d.ts.map +1 -1
- package/dist/sdk/ai/providers/openai-completions.d.ts.map +1 -1
- package/dist/sdk/ai/providers/openai-completions.js +435 -92
- package/dist/sdk/ai/types.d.ts +4 -0
- package/dist/sdk/ai/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/agent-session.d.ts +1 -1
- package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/agent-session.js +1 -1
- package/dist/sdk/coding-agent/core/extensions/runner.d.ts +3 -1
- package/dist/sdk/coding-agent/core/extensions/runner.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/runner.js +6 -2
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +2 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/sdk.d.ts +2 -2
- package/dist/sdk/coding-agent/core/sdk.js +2 -2
- package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.js +22 -4
- package/dist/sdk/coding-agent/core/tools/apply-patch.d.ts +27 -0
- package/dist/sdk/coding-agent/core/tools/apply-patch.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/tools/apply-patch.js +353 -0
- package/dist/sdk/coding-agent/core/tools/edit-diff.d.ts +1 -6
- package/dist/sdk/coding-agent/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/edit-diff.js +179 -100
- package/dist/sdk/coding-agent/core/tools/edit.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/edit.js +9 -5
- package/dist/sdk/coding-agent/core/tools/index.d.ts +4 -1
- package/dist/sdk/coding-agent/core/tools/index.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/index.js +11 -0
- package/dist/sdk/coding-agent/index.d.ts +1 -1
- package/dist/sdk/coding-agent/index.d.ts.map +1 -1
- package/dist/sdk/coding-agent/index.js +1 -1
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +274 -54
- package/dist/server/handlers/queue.d.ts +3 -11
- package/dist/server/handlers/queue.d.ts.map +1 -1
- package/dist/server/handlers/queue.js +48 -24
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +43 -9
- package/dist/server/storage.d.ts +2 -1
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +16 -8
- package/dist/server/wire.d.ts +11 -1
- package/dist/server/wire.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { constants } from "fs";
|
|
2
|
+
import { access as fsAccess, mkdir as fsMkdir, readFile as fsReadFile, rm as fsRm, writeFile as fsWriteFile } from "fs/promises";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { detectLineEnding, generateDiffString, generateUnifiedPatch, normalizeForFuzzyMatch, normalizeToLF, restoreLineEndings, stripBom, } from "./edit-diff.js";
|
|
6
|
+
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
7
|
+
import { resolveToCwd } from "./path-utils.js";
|
|
8
|
+
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
9
|
+
const applyPatchSchema = Type.Object({
|
|
10
|
+
patchText: Type.String({ description: "The full patch text that describes all changes to be made." }),
|
|
11
|
+
}, { additionalProperties: false });
|
|
12
|
+
const defaultApplyPatchOperations = {
|
|
13
|
+
readFile: (filePath) => fsReadFile(filePath),
|
|
14
|
+
writeFile: (filePath, content) => fsWriteFile(filePath, content, "utf-8"),
|
|
15
|
+
removeFile: (filePath) => fsRm(filePath, { force: false }),
|
|
16
|
+
mkdir: (filePath) => fsMkdir(filePath, { recursive: true }).then(() => { }),
|
|
17
|
+
access: (filePath) => fsAccess(filePath, constants.R_OK | constants.W_OK),
|
|
18
|
+
};
|
|
19
|
+
function stripHeredoc(input) {
|
|
20
|
+
const heredocMatch = input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/);
|
|
21
|
+
return heredocMatch ? heredocMatch[2] : input;
|
|
22
|
+
}
|
|
23
|
+
function parsePatchHeader(lines, startIndex) {
|
|
24
|
+
const line = lines[startIndex];
|
|
25
|
+
if (line.startsWith("*** Add File:")) {
|
|
26
|
+
const filePath = line.slice("*** Add File:".length).trim();
|
|
27
|
+
return filePath ? { path: filePath, nextIndex: startIndex + 1 } : null;
|
|
28
|
+
}
|
|
29
|
+
if (line.startsWith("*** Delete File:")) {
|
|
30
|
+
const filePath = line.slice("*** Delete File:".length).trim();
|
|
31
|
+
return filePath ? { path: filePath, nextIndex: startIndex + 1 } : null;
|
|
32
|
+
}
|
|
33
|
+
if (line.startsWith("*** Update File:")) {
|
|
34
|
+
const filePath = line.slice("*** Update File:".length).trim();
|
|
35
|
+
let movePath;
|
|
36
|
+
let nextIndex = startIndex + 1;
|
|
37
|
+
if (lines[nextIndex]?.startsWith("*** Move to:")) {
|
|
38
|
+
movePath = lines[nextIndex].slice("*** Move to:".length).trim();
|
|
39
|
+
nextIndex++;
|
|
40
|
+
}
|
|
41
|
+
return filePath ? { path: filePath, movePath, nextIndex } : null;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function parseAddFileContent(lines, startIndex) {
|
|
46
|
+
let content = "";
|
|
47
|
+
let index = startIndex;
|
|
48
|
+
while (index < lines.length && !lines[index].startsWith("***")) {
|
|
49
|
+
if (lines[index].startsWith("+"))
|
|
50
|
+
content += `${lines[index].slice(1)}\n`;
|
|
51
|
+
index++;
|
|
52
|
+
}
|
|
53
|
+
return { content: content.endsWith("\n") ? content.slice(0, -1) : content, nextIndex: index };
|
|
54
|
+
}
|
|
55
|
+
function parseUpdateChunks(lines, startIndex) {
|
|
56
|
+
const chunks = [];
|
|
57
|
+
let index = startIndex;
|
|
58
|
+
while (index < lines.length && !lines[index].startsWith("***")) {
|
|
59
|
+
if (!lines[index].startsWith("@@")) {
|
|
60
|
+
index++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const header = parseUpdateHeader(lines[index]);
|
|
64
|
+
index++;
|
|
65
|
+
const oldLines = [];
|
|
66
|
+
const newLines = [];
|
|
67
|
+
let isEndOfFile = false;
|
|
68
|
+
while (index < lines.length && !lines[index].startsWith("@@") && !lines[index].startsWith("***")) {
|
|
69
|
+
const line = lines[index];
|
|
70
|
+
if (line === "*** End of File") {
|
|
71
|
+
isEndOfFile = true;
|
|
72
|
+
index++;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (line.startsWith(" ")) {
|
|
76
|
+
oldLines.push(line.slice(1));
|
|
77
|
+
newLines.push(line.slice(1));
|
|
78
|
+
}
|
|
79
|
+
else if (line.startsWith("-")) {
|
|
80
|
+
oldLines.push(line.slice(1));
|
|
81
|
+
}
|
|
82
|
+
else if (line.startsWith("+")) {
|
|
83
|
+
newLines.push(line.slice(1));
|
|
84
|
+
}
|
|
85
|
+
index++;
|
|
86
|
+
}
|
|
87
|
+
chunks.push({
|
|
88
|
+
oldLines,
|
|
89
|
+
newLines,
|
|
90
|
+
changeContext: header.changeContext,
|
|
91
|
+
oldStartLine: header.oldStartLine,
|
|
92
|
+
isEndOfFile: isEndOfFile || undefined,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return { chunks, nextIndex: index };
|
|
96
|
+
}
|
|
97
|
+
function parseUpdateHeader(line) {
|
|
98
|
+
const body = line.slice(2).trim();
|
|
99
|
+
if (!body)
|
|
100
|
+
return {};
|
|
101
|
+
const unifiedMatch = body.match(/^-([0-9]+)(?:,[0-9]+)?\s+\+[0-9]+(?:,[0-9]+)?\s*@@\s*(.*)$/);
|
|
102
|
+
if (!unifiedMatch)
|
|
103
|
+
return { changeContext: body };
|
|
104
|
+
const oldStartLine = Number.parseInt(unifiedMatch[1], 10);
|
|
105
|
+
const trailingContext = unifiedMatch[2]?.trim() || undefined;
|
|
106
|
+
return { oldStartLine: Number.isFinite(oldStartLine) ? oldStartLine : undefined, changeContext: trailingContext };
|
|
107
|
+
}
|
|
108
|
+
function parsePatch(patchText) {
|
|
109
|
+
const normalizedPatchText = patchText.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
110
|
+
const lines = stripHeredoc(normalizedPatchText.trim()).split("\n");
|
|
111
|
+
const beginIndex = lines.findIndex((line) => line.trim() === "*** Begin Patch");
|
|
112
|
+
const endIndex = lines.findIndex((line) => line.trim() === "*** End Patch");
|
|
113
|
+
if (beginIndex === -1 || endIndex === -1 || beginIndex >= endIndex) {
|
|
114
|
+
throw new Error("Invalid patch format: missing Begin/End markers.");
|
|
115
|
+
}
|
|
116
|
+
const hunks = [];
|
|
117
|
+
let index = beginIndex + 1;
|
|
118
|
+
while (index < endIndex) {
|
|
119
|
+
const header = parsePatchHeader(lines, index);
|
|
120
|
+
if (!header) {
|
|
121
|
+
index++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (lines[index].startsWith("*** Add File:")) {
|
|
125
|
+
const { content, nextIndex } = parseAddFileContent(lines, header.nextIndex);
|
|
126
|
+
hunks.push({ type: "add", path: header.path, contents: content });
|
|
127
|
+
index = nextIndex;
|
|
128
|
+
}
|
|
129
|
+
else if (lines[index].startsWith("*** Delete File:")) {
|
|
130
|
+
hunks.push({ type: "delete", path: header.path });
|
|
131
|
+
index = header.nextIndex;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const { chunks, nextIndex } = parseUpdateChunks(lines, header.nextIndex);
|
|
135
|
+
hunks.push({ type: "update", path: header.path, movePath: header.movePath, chunks });
|
|
136
|
+
index = nextIndex;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return hunks;
|
|
140
|
+
}
|
|
141
|
+
function compareNormalized(a, b) {
|
|
142
|
+
return normalizeForFuzzyMatch(a.trim()) === normalizeForFuzzyMatch(b.trim());
|
|
143
|
+
}
|
|
144
|
+
function tryMatch(lines, pattern, startIndex, compare, eof = false) {
|
|
145
|
+
if (pattern.length === 0)
|
|
146
|
+
return -1;
|
|
147
|
+
if (eof) {
|
|
148
|
+
const fromEnd = lines.length - pattern.length;
|
|
149
|
+
if (fromEnd >= startIndex && pattern.every((line, offset) => compare(lines[fromEnd + offset], line)))
|
|
150
|
+
return fromEnd;
|
|
151
|
+
}
|
|
152
|
+
for (let index = startIndex; index <= lines.length - pattern.length; index++) {
|
|
153
|
+
if (pattern.every((line, offset) => compare(lines[index + offset], line)))
|
|
154
|
+
return index;
|
|
155
|
+
}
|
|
156
|
+
return -1;
|
|
157
|
+
}
|
|
158
|
+
function seekSequence(lines, pattern, startIndex, eof = false) {
|
|
159
|
+
return [
|
|
160
|
+
(a, b) => a === b,
|
|
161
|
+
(a, b) => a.trimEnd() === b.trimEnd(),
|
|
162
|
+
(a, b) => a.trim() === b.trim(),
|
|
163
|
+
compareNormalized,
|
|
164
|
+
].reduce((found, compare) => found !== -1 ? found : tryMatch(lines, pattern, startIndex, compare, eof), -1);
|
|
165
|
+
}
|
|
166
|
+
function seekSequenceNearHint(lines, pattern, startIndex, hintLine, eof = false) {
|
|
167
|
+
if (!hintLine || hintLine < 1 || pattern.length === 0)
|
|
168
|
+
return seekSequence(lines, pattern, startIndex, eof);
|
|
169
|
+
const hintedIndex = Math.max(startIndex, Math.min(lines.length, hintLine - 1));
|
|
170
|
+
for (const radius of [0, 1, 3, 8, 20]) {
|
|
171
|
+
const windowStart = Math.max(startIndex, hintedIndex - radius);
|
|
172
|
+
const windowEnd = Math.min(lines.length - pattern.length, hintedIndex + radius);
|
|
173
|
+
if (windowEnd < windowStart)
|
|
174
|
+
continue;
|
|
175
|
+
for (const compare of [
|
|
176
|
+
(a, b) => a === b,
|
|
177
|
+
(a, b) => a.trimEnd() === b.trimEnd(),
|
|
178
|
+
(a, b) => a.trim() === b.trim(),
|
|
179
|
+
compareNormalized,
|
|
180
|
+
]) {
|
|
181
|
+
for (let index = windowStart; index <= windowEnd; index++) {
|
|
182
|
+
if (pattern.every((line, offset) => compare(lines[index + offset], line)))
|
|
183
|
+
return index;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return seekSequence(lines, pattern, startIndex, eof);
|
|
188
|
+
}
|
|
189
|
+
function computeReplacements(originalLines, filePath, chunks) {
|
|
190
|
+
const replacements = [];
|
|
191
|
+
let lineIndex = 0;
|
|
192
|
+
for (const chunk of chunks) {
|
|
193
|
+
if (chunk.changeContext) {
|
|
194
|
+
const contextIndex = seekSequence(originalLines, [chunk.changeContext], lineIndex);
|
|
195
|
+
if (contextIndex === -1)
|
|
196
|
+
throw new Error(`Failed to find context '${chunk.changeContext}' in ${filePath}.`);
|
|
197
|
+
lineIndex = contextIndex + 1;
|
|
198
|
+
}
|
|
199
|
+
if (chunk.oldLines.length === 0) {
|
|
200
|
+
const insertionIndex = originalLines.at(-1) === "" ? originalLines.length - 1 : originalLines.length;
|
|
201
|
+
replacements.push([insertionIndex, 0, chunk.newLines]);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
let oldLines = chunk.oldLines;
|
|
205
|
+
let newLines = chunk.newLines;
|
|
206
|
+
let found = seekSequenceNearHint(originalLines, oldLines, lineIndex, chunk.oldStartLine, chunk.isEndOfFile);
|
|
207
|
+
if (found === -1 && oldLines.at(-1) === "") {
|
|
208
|
+
oldLines = oldLines.slice(0, -1);
|
|
209
|
+
newLines = newLines.at(-1) === "" ? newLines.slice(0, -1) : newLines;
|
|
210
|
+
found = seekSequenceNearHint(originalLines, oldLines, lineIndex, chunk.oldStartLine, chunk.isEndOfFile);
|
|
211
|
+
}
|
|
212
|
+
if (found === -1) {
|
|
213
|
+
throw new Error(`Failed to find expected lines in ${filePath}:\n${chunk.oldLines.join("\n")}`);
|
|
214
|
+
}
|
|
215
|
+
replacements.push([found, oldLines.length, newLines]);
|
|
216
|
+
lineIndex = found + oldLines.length;
|
|
217
|
+
}
|
|
218
|
+
return replacements.sort((a, b) => a[0] - b[0]);
|
|
219
|
+
}
|
|
220
|
+
function applyReplacements(lines, replacements) {
|
|
221
|
+
const result = [...lines];
|
|
222
|
+
for (let index = replacements.length - 1; index >= 0; index--) {
|
|
223
|
+
const [start, oldLength, newLines] = replacements[index];
|
|
224
|
+
result.splice(start, oldLength, ...newLines);
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
function deriveUpdatedContent(filePath, rawContent, chunks) {
|
|
229
|
+
const { bom, text } = stripBom(rawContent);
|
|
230
|
+
const lineEnding = detectLineEnding(text);
|
|
231
|
+
const normalized = normalizeToLF(text);
|
|
232
|
+
const originalLines = normalized.split("\n");
|
|
233
|
+
if (originalLines.at(-1) === "")
|
|
234
|
+
originalLines.pop();
|
|
235
|
+
const replacements = computeReplacements(originalLines, filePath, chunks);
|
|
236
|
+
const newLines = applyReplacements(originalLines, replacements);
|
|
237
|
+
if (newLines.length === 0 || newLines.at(-1) !== "")
|
|
238
|
+
newLines.push("");
|
|
239
|
+
return { bom, oldContent: normalized, newContent: newLines.join("\n"), lineEnding };
|
|
240
|
+
}
|
|
241
|
+
export function createApplyPatchToolDefinition(cwd, options) {
|
|
242
|
+
const ops = options?.operations ?? defaultApplyPatchOperations;
|
|
243
|
+
return {
|
|
244
|
+
name: "apply_patch",
|
|
245
|
+
label: "Apply Patch",
|
|
246
|
+
description: "Apply a file-oriented patch with explicit Add File, Update File, and Delete File sections.",
|
|
247
|
+
promptSnippet: "Apply multi-file patches using a structured patch envelope with Add/Update/Delete file sections.",
|
|
248
|
+
promptGuidelines: [
|
|
249
|
+
"Use apply_patch for manual code edits, especially with OpenAI/GPT/Codex models. It is safer than exact oldText replacement because each hunk carries explicit context and changed lines.",
|
|
250
|
+
"Before calling apply_patch, read the target file and build hunks from the current contents. Do not guess indentation, omitted blank lines, comments, or surrounding context.",
|
|
251
|
+
"Patch format: start with *** Begin Patch, include one or more *** Add File / *** Update File / *** Delete File sections, and finish with *** End Patch.",
|
|
252
|
+
"For Update File sections, use @@ context headers when helpful, prefix unchanged lines with a space, removed lines with -, and added lines with +.",
|
|
253
|
+
"Keep each Update File hunk small and local. If a patch fails to match, re-read the file and retry with a smaller hunk copied from the current file instead of reusing stale context.",
|
|
254
|
+
],
|
|
255
|
+
parameters: applyPatchSchema,
|
|
256
|
+
async execute(_toolCallId, input, signal) {
|
|
257
|
+
if (!input.patchText?.trim())
|
|
258
|
+
throw new Error("patchText is required.");
|
|
259
|
+
const hunks = parsePatch(input.patchText);
|
|
260
|
+
if (hunks.length === 0)
|
|
261
|
+
throw new Error("apply_patch verification failed: no hunks found.");
|
|
262
|
+
const changes = [];
|
|
263
|
+
for (const hunk of hunks) {
|
|
264
|
+
if (signal?.aborted)
|
|
265
|
+
throw new Error("Operation aborted");
|
|
266
|
+
const absolutePath = resolveToCwd(hunk.path, cwd);
|
|
267
|
+
if (hunk.type === "add") {
|
|
268
|
+
const existing = await ops.readFile(absolutePath).then(() => true, () => false);
|
|
269
|
+
if (existing)
|
|
270
|
+
throw new Error(`Could not add file: ${hunk.path}. File already exists.`);
|
|
271
|
+
const { bom, text } = stripBom(normalizeToLF(hunk.contents));
|
|
272
|
+
changes.push({
|
|
273
|
+
path: hunk.path,
|
|
274
|
+
absolutePath,
|
|
275
|
+
oldContent: "",
|
|
276
|
+
newContent: text.endsWith("\n") ? text : `${text}\n`,
|
|
277
|
+
type: "add",
|
|
278
|
+
bom,
|
|
279
|
+
lineEnding: "\n",
|
|
280
|
+
});
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
await ops.access(absolutePath).catch((error) => {
|
|
284
|
+
const errorMessage = error instanceof Error && "code" in error ? `Error code: ${error.code}` : String(error);
|
|
285
|
+
throw new Error(`Could not patch file: ${hunk.path}. ${errorMessage}.`);
|
|
286
|
+
});
|
|
287
|
+
const rawContent = (await ops.readFile(absolutePath)).toString("utf-8");
|
|
288
|
+
if (hunk.type === "delete") {
|
|
289
|
+
const { bom, text } = stripBom(rawContent);
|
|
290
|
+
changes.push({
|
|
291
|
+
path: hunk.path,
|
|
292
|
+
absolutePath,
|
|
293
|
+
oldContent: normalizeToLF(text),
|
|
294
|
+
newContent: "",
|
|
295
|
+
type: "delete",
|
|
296
|
+
bom,
|
|
297
|
+
lineEnding: detectLineEnding(text),
|
|
298
|
+
});
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const updated = deriveUpdatedContent(hunk.path, rawContent, hunk.chunks);
|
|
302
|
+
const absoluteMovePath = hunk.movePath ? resolveToCwd(hunk.movePath, cwd) : undefined;
|
|
303
|
+
changes.push({
|
|
304
|
+
path: hunk.path,
|
|
305
|
+
absolutePath,
|
|
306
|
+
oldContent: updated.oldContent,
|
|
307
|
+
newContent: updated.newContent,
|
|
308
|
+
type: "update",
|
|
309
|
+
movePath: hunk.movePath,
|
|
310
|
+
absoluteMovePath,
|
|
311
|
+
bom: updated.bom,
|
|
312
|
+
lineEnding: updated.lineEnding,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
const mutatedFiles = changes.map((change) => change.absoluteMovePath ?? change.absolutePath);
|
|
316
|
+
return withFileMutationQueue(mutatedFiles.join("\0"), async () => {
|
|
317
|
+
let combinedDiff = "";
|
|
318
|
+
let combinedPatch = "";
|
|
319
|
+
let firstChangedLine;
|
|
320
|
+
for (const change of changes) {
|
|
321
|
+
if (signal?.aborted)
|
|
322
|
+
throw new Error("Operation aborted");
|
|
323
|
+
if (change.oldContent === change.newContent && !change.movePath) {
|
|
324
|
+
throw new Error(`No changes made to ${change.path}. The patch produced identical content.`);
|
|
325
|
+
}
|
|
326
|
+
if (change.type === "delete") {
|
|
327
|
+
await ops.removeFile(change.absolutePath);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
const targetPath = change.absoluteMovePath ?? change.absolutePath;
|
|
331
|
+
await ops.mkdir(dirname(targetPath));
|
|
332
|
+
await ops.writeFile(targetPath, change.bom + restoreLineEndings(change.newContent, change.lineEnding));
|
|
333
|
+
if (change.movePath)
|
|
334
|
+
await ops.removeFile(change.absolutePath);
|
|
335
|
+
}
|
|
336
|
+
const displayPath = change.movePath ? `${change.path} => ${change.movePath}` : change.path;
|
|
337
|
+
const diffResult = generateDiffString(change.oldContent, change.newContent);
|
|
338
|
+
combinedDiff += `${displayPath}\n${diffResult.diff}\n`;
|
|
339
|
+
combinedPatch += `${generateUnifiedPatch(displayPath, change.oldContent, change.newContent)}\n`;
|
|
340
|
+
firstChangedLine ??= diffResult.firstChangedLine;
|
|
341
|
+
}
|
|
342
|
+
const modifiedFiles = changes.map((change) => change.movePath ?? change.path);
|
|
343
|
+
return {
|
|
344
|
+
content: [{ type: "text", text: `Successfully applied patch to ${modifiedFiles.length} file(s).` }],
|
|
345
|
+
details: { diff: combinedDiff.trim(), patch: combinedPatch.trim(), firstChangedLine, modifiedFiles },
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
export function createApplyPatchTool(cwd, options) {
|
|
352
|
+
return wrapToolDefinition(createApplyPatchToolDefinition(cwd, options));
|
|
353
|
+
}
|
|
@@ -18,7 +18,7 @@ export interface FuzzyMatchResult {
|
|
|
18
18
|
found: boolean;
|
|
19
19
|
/** The index where the match starts in the original content */
|
|
20
20
|
index: number;
|
|
21
|
-
/** Length of the matched text */
|
|
21
|
+
/** Length of the matched text in the original content */
|
|
22
22
|
matchLength: number;
|
|
23
23
|
/** Whether fuzzy matching was used (false = exact match) */
|
|
24
24
|
usedFuzzyMatch: boolean;
|
|
@@ -33,11 +33,6 @@ export interface AppliedEditsResult {
|
|
|
33
33
|
baseContent: string;
|
|
34
34
|
newContent: string;
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Find oldText in content, trying exact match first, then tolerant whitespace matching.
|
|
38
|
-
* Fuzzy matches still return indexes into the original content, so replacements preserve
|
|
39
|
-
* untouched text outside the matched span.
|
|
40
|
-
*/
|
|
41
36
|
export declare function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult;
|
|
42
37
|
/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */
|
|
43
38
|
export declare function stripBom(content: string): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-diff.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE9E;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqB3D;
|
|
1
|
+
{"version":3,"file":"edit-diff.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE9E;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqB3D;AAED,MAAM,WAAW,gBAAgB;IAChC,gCAAgC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;IACxB,8DAA8D;IAC9D,qBAAqB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,IAAI;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CAChB;AAwBD,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACnB;AA8KD,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAkBhF;AA6CD,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAEvE;AA0CD;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC5C,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,IAAI,EAAE,EACb,IAAI,EAAE,MAAM,GACV,kBAAkB,CA0DpB;AAED,yCAAyC;AACzC,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,SAAI,GAAG,MAAM,CAKnH;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,YAAY,SAAI,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAuHxD;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACrC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,IAAI,EAAE,EACb,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,cAAc,GAAG,aAAa,CAAC,CAyBzC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,cAAc,GAAG,aAAa,CAAC,CAEzC"}
|