@jsonstudio/llms 0.6.753 → 0.6.802
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/conversion/compat/actions/apply-patch-fixer.d.ts +1 -0
- package/dist/conversion/compat/actions/apply-patch-fixer.js +30 -0
- package/dist/conversion/compat/actions/apply-patch-format-fixer.d.ts +1 -0
- package/dist/conversion/compat/actions/apply-patch-format-fixer.js +233 -0
- package/dist/conversion/compat/actions/index.d.ts +2 -0
- package/dist/conversion/compat/actions/index.js +2 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +15 -15
- package/dist/conversion/compat/profiles/chat-glm.json +194 -194
- package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
- package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/dist/conversion/compat/profiles/responses-output2choices-test.json +10 -9
- package/dist/conversion/hub/pipeline/context-limit.d.ts +13 -0
- package/dist/conversion/hub/pipeline/context-limit.js +55 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +35 -0
- package/dist/conversion/shared/bridge-message-utils.d.ts +1 -0
- package/dist/conversion/shared/bridge-message-utils.js +7 -0
- package/dist/conversion/shared/bridge-policies.js +8 -8
- package/dist/conversion/shared/snapshot-hooks.js +54 -1
- package/dist/conversion/shared/tool-governor.js +18 -23
- package/dist/filters/special/response-tool-arguments-stringify.js +3 -22
- package/dist/router/virtual-router/engine-selection.js +49 -4
- package/dist/router/virtual-router/engine.d.ts +5 -0
- package/dist/router/virtual-router/engine.js +21 -0
- package/dist/tools/apply-patch/regression-capturer.d.ts +12 -0
- package/dist/tools/apply-patch/regression-capturer.js +112 -0
- package/dist/tools/apply-patch/structured.d.ts +20 -0
- package/dist/tools/apply-patch/structured.js +441 -0
- package/dist/tools/apply-patch/validator.d.ts +8 -0
- package/dist/tools/apply-patch/validator.js +616 -0
- package/dist/tools/apply-patch-structured.d.ts +1 -20
- package/dist/tools/apply-patch-structured.js +1 -277
- package/dist/tools/args-json.d.ts +1 -0
- package/dist/tools/args-json.js +175 -0
- package/dist/tools/exec-command/normalize.d.ts +17 -0
- package/dist/tools/exec-command/normalize.js +112 -0
- package/dist/tools/exec-command/regression-capturer.d.ts +11 -0
- package/dist/tools/exec-command/regression-capturer.js +144 -0
- package/dist/tools/exec-command/validator.d.ts +6 -0
- package/dist/tools/exec-command/validator.js +22 -0
- package/dist/tools/patch-args-normalizer.d.ts +15 -0
- package/dist/tools/patch-args-normalizer.js +472 -0
- package/dist/tools/patch-regression-capturer.d.ts +1 -0
- package/dist/tools/patch-regression-capturer.js +1 -0
- package/dist/tools/tool-registry.js +36 -541
- package/package.json +1 -1
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { buildStructuredPatch, isStructuredApplyPatchPayload, StructuredApplyPatchError } from './structured.js';
|
|
2
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
const asString = (value) => {
|
|
4
|
+
if (typeof value === 'string' && value.trim().length > 0)
|
|
5
|
+
return value;
|
|
6
|
+
return null;
|
|
7
|
+
};
|
|
8
|
+
const stripCodeFences = (text) => {
|
|
9
|
+
const trimmed = text.trim();
|
|
10
|
+
// Only treat the entire payload as fenced when it *starts* with a code fence.
|
|
11
|
+
// Patch bodies (especially added Markdown files) may legitimately contain ``` blocks;
|
|
12
|
+
// we must not strip those.
|
|
13
|
+
if (!trimmed.startsWith('```'))
|
|
14
|
+
return text;
|
|
15
|
+
const fenceRe = /^```(?:diff|patch|apply_patch|text|json)?[ \t]*\n([\s\S]*?)\n```/gmi;
|
|
16
|
+
const candidates = [];
|
|
17
|
+
let match = null;
|
|
18
|
+
while ((match = fenceRe.exec(trimmed))) {
|
|
19
|
+
if (match[1])
|
|
20
|
+
candidates.push(match[1].trim());
|
|
21
|
+
}
|
|
22
|
+
if (!candidates.length)
|
|
23
|
+
return text;
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
if (candidate.includes('*** Begin Patch') ||
|
|
26
|
+
candidate.includes('*** Update File:') ||
|
|
27
|
+
candidate.includes('diff --git')) {
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return candidates[0] ?? text;
|
|
32
|
+
};
|
|
33
|
+
export const looksLikePatch = (text) => {
|
|
34
|
+
if (!text)
|
|
35
|
+
return false;
|
|
36
|
+
const t = text.trim();
|
|
37
|
+
if (!t)
|
|
38
|
+
return false;
|
|
39
|
+
return (t.includes('*** Begin Patch') ||
|
|
40
|
+
t.includes('*** Update File:') ||
|
|
41
|
+
t.includes('*** Add File:') ||
|
|
42
|
+
t.includes('*** Delete File:') ||
|
|
43
|
+
t.includes('diff --git') ||
|
|
44
|
+
/^(?:@@|\+\+\+\s|---\s)/m.test(t));
|
|
45
|
+
};
|
|
46
|
+
const decodeEscapedNewlinesIfNeeded = (value) => {
|
|
47
|
+
try {
|
|
48
|
+
if (!value)
|
|
49
|
+
return value;
|
|
50
|
+
if (value.includes('\n'))
|
|
51
|
+
return value;
|
|
52
|
+
if (!value.includes('\\n'))
|
|
53
|
+
return value;
|
|
54
|
+
return value.replace(/\\n/g, '\n');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const stripConflictMarkers = (text) => {
|
|
61
|
+
try {
|
|
62
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
if (line.startsWith('<<<<<<<') || line.startsWith('=======') || line.startsWith('>>>>>>>')) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
out.push(line);
|
|
69
|
+
}
|
|
70
|
+
return out.join('\n');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return text;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const convertGitDiffToApplyPatch = (text) => {
|
|
77
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
78
|
+
const files = [];
|
|
79
|
+
let current = null;
|
|
80
|
+
let sawDiff = false;
|
|
81
|
+
const flush = () => {
|
|
82
|
+
if (!current)
|
|
83
|
+
return;
|
|
84
|
+
if (current.path && current.kind === 'delete') {
|
|
85
|
+
files.push(current);
|
|
86
|
+
current = null;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (current.path && current.lines.length) {
|
|
90
|
+
files.push(current);
|
|
91
|
+
}
|
|
92
|
+
current = null;
|
|
93
|
+
};
|
|
94
|
+
const extractPath = (value) => {
|
|
95
|
+
const v = String(value || '').trim();
|
|
96
|
+
if (!v)
|
|
97
|
+
return '';
|
|
98
|
+
const m = v.match(/^(?:a\/|b\/)?(.+)$/);
|
|
99
|
+
return (m && m[1] ? m[1] : v).trim();
|
|
100
|
+
};
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
const diffMatch = line.match(/^diff --git\s+a\/(.+?)\s+b\/(.+)$/);
|
|
103
|
+
if (diffMatch) {
|
|
104
|
+
sawDiff = true;
|
|
105
|
+
flush();
|
|
106
|
+
current = { path: extractPath(diffMatch[2]), kind: 'update', lines: [] };
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!current)
|
|
110
|
+
continue;
|
|
111
|
+
const delMatch = line.match(/^deleted file mode\s+/);
|
|
112
|
+
const newMatch = line.match(/^new file mode\s+/);
|
|
113
|
+
if (delMatch) {
|
|
114
|
+
current.kind = 'delete';
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (newMatch) {
|
|
118
|
+
current.kind = 'add';
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (line.startsWith('--- ') || line.startsWith('+++ ') || line.startsWith('index ')) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (line.startsWith('@@')) {
|
|
125
|
+
current.lines.push(line);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
|
|
129
|
+
current.lines.push(line);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
flush();
|
|
134
|
+
if (!sawDiff || files.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
const out = ['*** Begin Patch'];
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
if (file.kind === 'delete') {
|
|
139
|
+
out.push(`*** Delete File: ${file.path}`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (file.kind === 'add') {
|
|
143
|
+
out.push(`*** Add File: ${file.path}`);
|
|
144
|
+
for (const l of file.lines) {
|
|
145
|
+
if (l.startsWith('+'))
|
|
146
|
+
out.push(l);
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
out.push(`*** Update File: ${file.path}`);
|
|
151
|
+
out.push(...file.lines);
|
|
152
|
+
}
|
|
153
|
+
out.push('*** End Patch');
|
|
154
|
+
return out.join('\n');
|
|
155
|
+
};
|
|
156
|
+
export const normalizeApplyPatchText = (raw) => {
|
|
157
|
+
if (!raw)
|
|
158
|
+
return raw;
|
|
159
|
+
let text = raw.replace(/\r\n/g, '\n');
|
|
160
|
+
text = decodeEscapedNewlinesIfNeeded(text);
|
|
161
|
+
text = stripCodeFences(text);
|
|
162
|
+
text = text.trim();
|
|
163
|
+
if (!text)
|
|
164
|
+
return raw;
|
|
165
|
+
text = stripConflictMarkers(text);
|
|
166
|
+
if (!text.includes('*** Begin Patch') && text.includes('diff --git')) {
|
|
167
|
+
const converted = convertGitDiffToApplyPatch(text);
|
|
168
|
+
if (converted)
|
|
169
|
+
text = converted;
|
|
170
|
+
}
|
|
171
|
+
else if (!text.includes('*** Begin Patch') && !text.includes('diff --git')) {
|
|
172
|
+
const minusMatch = text.match(/^---\s+(.*)$/m);
|
|
173
|
+
const plusMatch = text.match(/^\+\+\+\s+(.*)$/m);
|
|
174
|
+
if (minusMatch && plusMatch) {
|
|
175
|
+
const rawPlus = plusMatch[1] || '';
|
|
176
|
+
const pathMatch = rawPlus.match(/^(?:b\/)?(.+)$/);
|
|
177
|
+
const filePath = (pathMatch && pathMatch[1] ? pathMatch[1] : rawPlus).trim();
|
|
178
|
+
if (filePath) {
|
|
179
|
+
const synthetic = `diff --git a/${filePath} b/${filePath}\n${text}`;
|
|
180
|
+
const converted = convertGitDiffToApplyPatch(synthetic);
|
|
181
|
+
if (converted)
|
|
182
|
+
text = converted;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (text.includes('*** Add File:')) {
|
|
187
|
+
text = text.replace(/\*\*\* Create File:/g, '*** Add File:');
|
|
188
|
+
}
|
|
189
|
+
let hasBegin = text.includes('*** Begin Patch');
|
|
190
|
+
const hasEnd = text.includes('*** End Patch');
|
|
191
|
+
if (hasBegin && !hasEnd) {
|
|
192
|
+
text = `${text}\n*** End Patch`;
|
|
193
|
+
}
|
|
194
|
+
if (!hasBegin && /^\*\*\* (Add|Update|Delete) File:/m.test(text)) {
|
|
195
|
+
text = `*** Begin Patch\n${text}\n*** End Patch`;
|
|
196
|
+
hasBegin = true;
|
|
197
|
+
}
|
|
198
|
+
if (!text.includes('*** Begin Patch')) {
|
|
199
|
+
return text;
|
|
200
|
+
}
|
|
201
|
+
const beginIndex = text.indexOf('*** Begin Patch');
|
|
202
|
+
if (beginIndex > 0) {
|
|
203
|
+
text = text.slice(beginIndex);
|
|
204
|
+
}
|
|
205
|
+
const endMarker = '*** End Patch';
|
|
206
|
+
const firstEndIndex = text.indexOf(endMarker);
|
|
207
|
+
const concatSignatures = [
|
|
208
|
+
`${endMarker}","input":"*** Begin Patch`,
|
|
209
|
+
`${endMarker}","patch":"*** Begin Patch`,
|
|
210
|
+
`${endMarker}\\",\\"input\\":\\"*** Begin Patch`,
|
|
211
|
+
`${endMarker}\\",\\"patch\\":\\"*** Begin Patch`
|
|
212
|
+
];
|
|
213
|
+
const hasConcatenationSignal = concatSignatures.some((sig) => text.includes(sig));
|
|
214
|
+
if (hasConcatenationSignal && firstEndIndex >= 0) {
|
|
215
|
+
text = text.slice(0, firstEndIndex + endMarker.length);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
const lastEndIndex = text.lastIndexOf(endMarker);
|
|
219
|
+
if (lastEndIndex >= 0) {
|
|
220
|
+
const afterEnd = text.slice(lastEndIndex + endMarker.length);
|
|
221
|
+
if (afterEnd.trim().length > 0) {
|
|
222
|
+
text = text.slice(0, lastEndIndex + endMarker.length);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Fix missing prefix lines in Update File sections: treat as context (" ").
|
|
227
|
+
const lines = text.split('\n');
|
|
228
|
+
const output = [];
|
|
229
|
+
let inUpdateSection = false;
|
|
230
|
+
let afterUpdateHeader = false;
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
if (line.startsWith('*** Begin Patch')) {
|
|
233
|
+
output.push(line);
|
|
234
|
+
inUpdateSection = false;
|
|
235
|
+
afterUpdateHeader = false;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (line.startsWith('*** End Patch')) {
|
|
239
|
+
output.push(line);
|
|
240
|
+
inUpdateSection = false;
|
|
241
|
+
afterUpdateHeader = false;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (line.startsWith('*** Update File:')) {
|
|
245
|
+
output.push(line);
|
|
246
|
+
inUpdateSection = true;
|
|
247
|
+
afterUpdateHeader = true;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (line.startsWith('*** Add File:') || line.startsWith('*** Delete File:')) {
|
|
251
|
+
output.push(line);
|
|
252
|
+
inUpdateSection = false;
|
|
253
|
+
afterUpdateHeader = false;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (inUpdateSection) {
|
|
257
|
+
if (afterUpdateHeader && line.trim() === '') {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
afterUpdateHeader = false;
|
|
261
|
+
if (line.startsWith('@@') || line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
|
|
262
|
+
output.push(line);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
output.push(` ${line}`);
|
|
266
|
+
}
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
output.push(line);
|
|
270
|
+
}
|
|
271
|
+
return output.join('\n');
|
|
272
|
+
};
|
|
273
|
+
const buildSingleChangePayload = (record) => {
|
|
274
|
+
const kindRaw = asString(record.kind);
|
|
275
|
+
if (!kindRaw)
|
|
276
|
+
return undefined;
|
|
277
|
+
const change = {
|
|
278
|
+
kind: kindRaw.toLowerCase(),
|
|
279
|
+
lines: record.lines ?? record.text ?? record.body,
|
|
280
|
+
target: asString(record.target) ?? undefined,
|
|
281
|
+
anchor: asString(record.anchor) ?? undefined
|
|
282
|
+
};
|
|
283
|
+
if (typeof record.use_anchor_indent === 'boolean') {
|
|
284
|
+
change.use_anchor_indent = record.use_anchor_indent;
|
|
285
|
+
}
|
|
286
|
+
const changeFile = asString(record.file);
|
|
287
|
+
if (changeFile) {
|
|
288
|
+
change.file = changeFile;
|
|
289
|
+
}
|
|
290
|
+
return { file: changeFile, changes: [change] };
|
|
291
|
+
};
|
|
292
|
+
const tryParseJson = (value) => {
|
|
293
|
+
if (typeof value !== 'string')
|
|
294
|
+
return undefined;
|
|
295
|
+
const trimmed = value.trim();
|
|
296
|
+
if (!trimmed)
|
|
297
|
+
return undefined;
|
|
298
|
+
if (!(trimmed.startsWith('{') || trimmed.startsWith('[')))
|
|
299
|
+
return undefined;
|
|
300
|
+
try {
|
|
301
|
+
return JSON.parse(trimmed);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const escapeUnescapedQuotesInJsonStrings = (input) => {
|
|
308
|
+
// Best-effort: when JSON is almost valid but contains unescaped `"` inside string values
|
|
309
|
+
// (e.g. JSX snippets like className="..."), escape quotes that are not followed by a
|
|
310
|
+
// valid JSON token delimiter. Deterministic; does not attempt to fix structural issues.
|
|
311
|
+
let out = '';
|
|
312
|
+
let inString = false;
|
|
313
|
+
let escaped = false;
|
|
314
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
315
|
+
const ch = input[i] ?? '';
|
|
316
|
+
if (!inString) {
|
|
317
|
+
if (ch === '"') {
|
|
318
|
+
inString = true;
|
|
319
|
+
escaped = false;
|
|
320
|
+
}
|
|
321
|
+
out += ch;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (escaped) {
|
|
325
|
+
out += ch;
|
|
326
|
+
escaped = false;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (ch === '\\') {
|
|
330
|
+
out += ch;
|
|
331
|
+
escaped = true;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (ch === '"') {
|
|
335
|
+
let j = i + 1;
|
|
336
|
+
while (j < input.length && /\s/.test(input[j] ?? ''))
|
|
337
|
+
j += 1;
|
|
338
|
+
const next = j < input.length ? input[j] : '';
|
|
339
|
+
if (next === '' || next === ':' || next === ',' || next === '}' || next === ']') {
|
|
340
|
+
inString = false;
|
|
341
|
+
out += ch;
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
out += '\\"';
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
out += ch;
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
};
|
|
352
|
+
const balanceJsonContainers = (input) => {
|
|
353
|
+
// Best-effort bracket/brace balancing for JSON-like strings.
|
|
354
|
+
// Only operates outside string literals. When encountering a closing token that doesn't
|
|
355
|
+
// match the current stack top, inserts the missing closer(s) to recover.
|
|
356
|
+
let out = '';
|
|
357
|
+
let inString = false;
|
|
358
|
+
let escaped = false;
|
|
359
|
+
const stack = [];
|
|
360
|
+
const closeFor = (open) => (open === '{' ? '}' : ']');
|
|
361
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
362
|
+
const ch = input[i] ?? '';
|
|
363
|
+
if (inString) {
|
|
364
|
+
out += ch;
|
|
365
|
+
if (escaped) {
|
|
366
|
+
escaped = false;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (ch === '\\') {
|
|
370
|
+
escaped = true;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (ch === '"') {
|
|
374
|
+
inString = false;
|
|
375
|
+
}
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (ch === '"') {
|
|
379
|
+
inString = true;
|
|
380
|
+
out += ch;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (ch === '{' || ch === '[') {
|
|
384
|
+
stack.push(ch);
|
|
385
|
+
out += ch;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (ch === '}' || ch === ']') {
|
|
389
|
+
const expectedOpen = ch === '}' ? '{' : '[';
|
|
390
|
+
while (stack.length && stack[stack.length - 1] !== expectedOpen) {
|
|
391
|
+
const open = stack.pop();
|
|
392
|
+
out += closeFor(open);
|
|
393
|
+
}
|
|
394
|
+
if (stack.length && stack[stack.length - 1] === expectedOpen) {
|
|
395
|
+
stack.pop();
|
|
396
|
+
}
|
|
397
|
+
out += ch;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
out += ch;
|
|
401
|
+
}
|
|
402
|
+
while (stack.length) {
|
|
403
|
+
const open = stack.pop();
|
|
404
|
+
out += closeFor(open);
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
};
|
|
408
|
+
const tryParseJsonLoose = (value) => {
|
|
409
|
+
const parsed = tryParseJson(value);
|
|
410
|
+
if (parsed !== undefined)
|
|
411
|
+
return parsed;
|
|
412
|
+
if (typeof value !== 'string')
|
|
413
|
+
return undefined;
|
|
414
|
+
const trimmed = value.trim();
|
|
415
|
+
if (!trimmed)
|
|
416
|
+
return undefined;
|
|
417
|
+
if (!(trimmed.startsWith('{') || trimmed.startsWith('[')))
|
|
418
|
+
return undefined;
|
|
419
|
+
let repaired = escapeUnescapedQuotesInJsonStrings(trimmed);
|
|
420
|
+
repaired = balanceJsonContainers(repaired);
|
|
421
|
+
if (!repaired || repaired === trimmed)
|
|
422
|
+
return undefined;
|
|
423
|
+
try {
|
|
424
|
+
return JSON.parse(repaired);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const coerceChangesArray = (value) => {
|
|
431
|
+
const parsed = tryParseJson(value);
|
|
432
|
+
if (!parsed)
|
|
433
|
+
return undefined;
|
|
434
|
+
if (Array.isArray(parsed)) {
|
|
435
|
+
const items = parsed.filter((entry) => entry && typeof entry === 'object');
|
|
436
|
+
if (!items.length)
|
|
437
|
+
return undefined;
|
|
438
|
+
// Ensure at least one entry looks like a structured change.
|
|
439
|
+
if (!items.some((c) => typeof c.kind === 'string' && String(c.kind).trim()))
|
|
440
|
+
return undefined;
|
|
441
|
+
return items;
|
|
442
|
+
}
|
|
443
|
+
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.changes)) {
|
|
444
|
+
const items = parsed.changes.filter((entry) => entry && typeof entry === 'object');
|
|
445
|
+
if (!items.length)
|
|
446
|
+
return undefined;
|
|
447
|
+
if (!items.some((c) => typeof c.kind === 'string' && String(c.kind).trim()))
|
|
448
|
+
return undefined;
|
|
449
|
+
return items;
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
};
|
|
453
|
+
const coerceStructuredPayload = (record) => {
|
|
454
|
+
if (isStructuredApplyPatchPayload(record)) {
|
|
455
|
+
return record;
|
|
456
|
+
}
|
|
457
|
+
if (Array.isArray(record.changes) && record.changes.length === 0) {
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
// Common shape error: { file, instructions: "[{...},{...}]" } where instructions contains JSON changes.
|
|
461
|
+
if (!Array.isArray(record.changes)) {
|
|
462
|
+
const changesFromInstructions = coerceChangesArray(record.instructions);
|
|
463
|
+
if (changesFromInstructions) {
|
|
464
|
+
return {
|
|
465
|
+
...(typeof record.file === 'string' ? { file: record.file } : {}),
|
|
466
|
+
changes: changesFromInstructions
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
// Another common shape: changes is a JSON string.
|
|
470
|
+
const changesFromString = coerceChangesArray(record.changes);
|
|
471
|
+
if (changesFromString) {
|
|
472
|
+
return {
|
|
473
|
+
...(typeof record.file === 'string' ? { file: record.file } : {}),
|
|
474
|
+
changes: changesFromString
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const editsFromString = coerceChangesArray(record.edits ?? record.operations ?? record.ops);
|
|
478
|
+
if (editsFromString) {
|
|
479
|
+
return {
|
|
480
|
+
...(typeof record.file === 'string' ? { file: record.file } : {}),
|
|
481
|
+
changes: editsFromString
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const single = buildSingleChangePayload(record);
|
|
486
|
+
if (single)
|
|
487
|
+
return single;
|
|
488
|
+
return undefined;
|
|
489
|
+
};
|
|
490
|
+
const toJson = (value) => {
|
|
491
|
+
try {
|
|
492
|
+
return JSON.stringify(value ?? {});
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return '{}';
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
export function validateApplyPatchArgs(argsString, rawArgs) {
|
|
499
|
+
const rawTrimmed = typeof argsString === 'string' ? argsString.trim() : '';
|
|
500
|
+
const looksJsonContainer = rawTrimmed.startsWith('{') || rawTrimmed.startsWith('[');
|
|
501
|
+
// Raw patch text without JSON wrapper
|
|
502
|
+
if (!looksJsonContainer && looksLikePatch(rawTrimmed)) {
|
|
503
|
+
const patchText = normalizeApplyPatchText(rawTrimmed);
|
|
504
|
+
return { ok: true, normalizedArgs: toJson({ patch: patchText, input: patchText }) };
|
|
505
|
+
}
|
|
506
|
+
const extractFromRecord = (rec) => {
|
|
507
|
+
// Special case: argsString claims to be JSON but isn't parseable and raw text looks like a patch.
|
|
508
|
+
if (looksJsonContainer && Object.keys(rec).length === 0 && looksLikePatch(rawTrimmed)) {
|
|
509
|
+
return { patchText: normalizeApplyPatchText(rawTrimmed) };
|
|
510
|
+
}
|
|
511
|
+
const patchField = asString(rec.patch);
|
|
512
|
+
if (patchField && looksLikePatch(patchField)) {
|
|
513
|
+
return { patchText: normalizeApplyPatchText(patchField) };
|
|
514
|
+
}
|
|
515
|
+
const diffField = asString(rec.diff) ?? asString(rec.patchText) ?? asString(rec.body);
|
|
516
|
+
if (diffField && looksLikePatch(diffField)) {
|
|
517
|
+
return { patchText: normalizeApplyPatchText(diffField) };
|
|
518
|
+
}
|
|
519
|
+
const inputField = asString(rec.input);
|
|
520
|
+
if (inputField && looksLikePatch(inputField)) {
|
|
521
|
+
return { patchText: normalizeApplyPatchText(inputField) };
|
|
522
|
+
}
|
|
523
|
+
// Common shape: patch text stored under `instructions` (e.g. "*** Update File: ...").
|
|
524
|
+
const instructionsField = asString(rec.instructions);
|
|
525
|
+
if (instructionsField && looksLikePatch(instructionsField)) {
|
|
526
|
+
return { patchText: normalizeApplyPatchText(instructionsField) };
|
|
527
|
+
}
|
|
528
|
+
// Common wrapper shape (seen in codex samples): { _raw: "{...json...}" }.
|
|
529
|
+
// `_raw` may contain either patch text or a JSON-encoded structured payload.
|
|
530
|
+
const rawEnvelope = asString(rec._raw);
|
|
531
|
+
if (rawEnvelope) {
|
|
532
|
+
const trimmed = rawEnvelope.trim();
|
|
533
|
+
if (looksLikePatch(trimmed)) {
|
|
534
|
+
return { patchText: normalizeApplyPatchText(trimmed) };
|
|
535
|
+
}
|
|
536
|
+
const parsed = tryParseJsonLoose(trimmed);
|
|
537
|
+
if (parsed && isRecord(parsed)) {
|
|
538
|
+
return extractFromRecord(parsed);
|
|
539
|
+
}
|
|
540
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
541
|
+
const changesArray = parsed.filter((entry) => isRecord(entry));
|
|
542
|
+
if (changesArray.length && changesArray.some((c) => typeof c.kind === 'string')) {
|
|
543
|
+
const payload = { changes: changesArray };
|
|
544
|
+
try {
|
|
545
|
+
return { patchText: buildStructuredPatch(payload) };
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
if (!(error instanceof StructuredApplyPatchError))
|
|
549
|
+
throw error;
|
|
550
|
+
return { failureReason: error.reason || 'structured_apply_patch_error' };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const payload = coerceStructuredPayload(rec);
|
|
556
|
+
if (payload) {
|
|
557
|
+
try {
|
|
558
|
+
return { patchText: buildStructuredPatch(payload) };
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
if (!(error instanceof StructuredApplyPatchError))
|
|
562
|
+
throw error;
|
|
563
|
+
return { failureReason: error.reason || 'structured_apply_patch_error' };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return {};
|
|
567
|
+
};
|
|
568
|
+
let patchText;
|
|
569
|
+
let failureReason;
|
|
570
|
+
if (isRecord(rawArgs)) {
|
|
571
|
+
const res = extractFromRecord(rawArgs);
|
|
572
|
+
patchText = res.patchText;
|
|
573
|
+
failureReason = res.failureReason;
|
|
574
|
+
}
|
|
575
|
+
else if (Array.isArray(rawArgs) && rawArgs.length > 0) {
|
|
576
|
+
// compatibility: arguments may be either:
|
|
577
|
+
// - a single-element array [{ file, changes: [...] }]
|
|
578
|
+
// - a raw changes array [{ kind, ... }, ...]
|
|
579
|
+
const first = rawArgs.find((entry) => isRecord(entry));
|
|
580
|
+
if (first && Array.isArray(first.changes)) {
|
|
581
|
+
const res = extractFromRecord(first);
|
|
582
|
+
patchText = res.patchText;
|
|
583
|
+
failureReason = res.failureReason;
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
const changesArray = rawArgs.filter((entry) => isRecord(entry));
|
|
587
|
+
if (changesArray.length && changesArray.some((c) => typeof c.kind === 'string')) {
|
|
588
|
+
const payload = { changes: changesArray };
|
|
589
|
+
try {
|
|
590
|
+
patchText = buildStructuredPatch(payload);
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
if (!(error instanceof StructuredApplyPatchError))
|
|
594
|
+
throw error;
|
|
595
|
+
failureReason = error.reason || 'structured_apply_patch_error';
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
else if (typeof rawArgs === 'string' && looksLikePatch(rawArgs)) {
|
|
601
|
+
patchText = normalizeApplyPatchText(rawArgs);
|
|
602
|
+
}
|
|
603
|
+
if (!patchText) {
|
|
604
|
+
if (failureReason)
|
|
605
|
+
return { ok: false, reason: failureReason };
|
|
606
|
+
if (looksJsonContainer &&
|
|
607
|
+
isRecord(rawArgs) &&
|
|
608
|
+
Object.keys(rawArgs).length === 0 &&
|
|
609
|
+
rawTrimmed.length > 0 &&
|
|
610
|
+
!looksLikePatch(rawTrimmed)) {
|
|
611
|
+
return { ok: false, reason: 'invalid_json' };
|
|
612
|
+
}
|
|
613
|
+
return { ok: false, reason: 'missing_changes' };
|
|
614
|
+
}
|
|
615
|
+
return { ok: true, normalizedArgs: toJson({ patch: patchText, input: patchText }) };
|
|
616
|
+
}
|
|
@@ -1,20 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
export interface StructuredApplyPatchChange {
|
|
3
|
-
file?: string;
|
|
4
|
-
kind: StructuredApplyPatchKind | string;
|
|
5
|
-
anchor?: string;
|
|
6
|
-
target?: string;
|
|
7
|
-
lines?: string[] | string;
|
|
8
|
-
use_anchor_indent?: boolean;
|
|
9
|
-
}
|
|
10
|
-
export interface StructuredApplyPatchPayload extends Record<string, unknown> {
|
|
11
|
-
instructions?: string;
|
|
12
|
-
file?: string;
|
|
13
|
-
changes: StructuredApplyPatchChange[];
|
|
14
|
-
}
|
|
15
|
-
export declare class StructuredApplyPatchError extends Error {
|
|
16
|
-
reason: string;
|
|
17
|
-
constructor(reason: string, message: string);
|
|
18
|
-
}
|
|
19
|
-
export declare function buildStructuredPatch(payload: StructuredApplyPatchPayload): string;
|
|
20
|
-
export declare function isStructuredApplyPatchPayload(candidate: unknown): candidate is StructuredApplyPatchPayload;
|
|
1
|
+
export * from './apply-patch/structured.js';
|