@ottocode/sdk 0.1.265 → 0.1.266

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.
Files changed (28) hide show
  1. package/package.json +2 -2
  2. package/src/core/src/providers/resolver.ts +29 -70
  3. package/src/core/src/tools/bin-manager/cache.ts +13 -0
  4. package/src/core/src/tools/bin-manager/filesystem.ts +32 -0
  5. package/src/core/src/tools/bin-manager/paths.ts +36 -0
  6. package/src/core/src/tools/bin-manager/vendor.ts +80 -0
  7. package/src/core/src/tools/bin-manager.ts +14 -140
  8. package/src/core/src/tools/builtin/patch/apply-hunk.ts +308 -0
  9. package/src/core/src/tools/builtin/patch/apply-report.ts +99 -0
  10. package/src/core/src/tools/builtin/patch/apply.ts +6 -663
  11. package/src/core/src/tools/builtin/patch/hunk-header.ts +17 -0
  12. package/src/core/src/tools/builtin/patch/indentation.ts +160 -0
  13. package/src/core/src/tools/builtin/patch/matching.ts +58 -0
  14. package/src/core/src/tools/builtin/patch/parse-enveloped.ts +10 -72
  15. package/src/core/src/tools/builtin/patch/parse-unified.ts +15 -105
  16. package/src/core/src/tools/builtin/patch/replace-builder.ts +64 -0
  17. package/src/core/src/tools/builtin/patch/unified-state.ts +86 -0
  18. package/src/core/src/tools/builtin/websearch-strategies.ts +197 -0
  19. package/src/core/src/tools/builtin/websearch.ts +9 -187
  20. package/src/core/src/tools/loader.ts +6 -49
  21. package/src/core/src/tools/plugin-discovery.ts +86 -0
  22. package/src/core/src/utils/logger/format.ts +50 -0
  23. package/src/core/src/utils/logger/sinks.ts +61 -0
  24. package/src/core/src/utils/logger.ts +2 -119
  25. package/src/index.ts +2 -0
  26. package/src/providers/src/index.ts +4 -0
  27. package/src/providers/src/model-resolution.ts +21 -0
  28. package/src/providers/src/zai-client.ts +5 -2
@@ -1,37 +1,23 @@
1
1
  import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
2
2
  import { dirname, isAbsolute, relative, resolve } from 'node:path';
3
3
 
4
- import {
5
- NORMALIZATION_LEVELS,
6
- normalizeWhitespace,
7
- computeIndentDelta,
8
- applyIndentDelta,
9
- getLeadingWhitespace,
10
- detectIndentStyle,
11
- expandWhitespace,
12
- inferTabSizeFromPairs,
13
- } from './normalize.ts';
14
- import {
15
- PATCH_ADD_PREFIX,
16
- PATCH_DELETE_PREFIX,
17
- PATCH_UPDATE_PREFIX,
18
- PATCH_BEGIN_MARKER,
19
- PATCH_END_MARKER,
20
- } from './constants.ts';
4
+ import { applyHunkToLines } from './apply-hunk.ts';
21
5
  import type {
22
6
  AppliedPatchHunk,
23
7
  AppliedPatchOperation,
24
8
  PatchAddOperation,
25
9
  PatchApplicationResult,
26
10
  PatchDeleteOperation,
27
- PatchHunk,
28
- PatchHunkLine,
29
11
  PatchOperation,
30
- PatchSummary,
31
12
  PatchUpdateOperation,
32
13
  RejectedPatch,
33
14
  } from './types.ts';
34
15
  import { ensureTrailingNewline, joinLines, splitLines } from './text.ts';
16
+ import {
17
+ formatNormalizedPatch,
18
+ makeAppliedRecord,
19
+ makeSummary,
20
+ } from './apply-report.ts';
35
21
 
36
22
  export function resolveProjectPath(
37
23
  projectRoot: string,
@@ -45,649 +31,6 @@ export function resolveProjectPath(
45
31
  return fullPath;
46
32
  }
47
33
 
48
- function makeAppliedRecord(
49
- kind: AppliedPatchOperation['kind'],
50
- filePath: string,
51
- hunks: AppliedPatchHunk[],
52
- ): AppliedPatchOperation {
53
- const stats = hunks.reduce(
54
- (acc, hunk) => ({
55
- additions: acc.additions + hunk.additions,
56
- deletions: acc.deletions + hunk.deletions,
57
- }),
58
- { additions: 0, deletions: 0 },
59
- );
60
- return {
61
- kind,
62
- filePath,
63
- stats,
64
- hunks,
65
- };
66
- }
67
-
68
- function makeSummary(operations: AppliedPatchOperation[]): PatchSummary {
69
- return operations.reduce<PatchSummary>(
70
- (acc, op) => ({
71
- files: acc.files + 1,
72
- additions: acc.additions + op.stats.additions,
73
- deletions: acc.deletions + op.stats.deletions,
74
- }),
75
- { files: 0, additions: 0, deletions: 0 },
76
- );
77
- }
78
-
79
- function serializePatchLine(line: PatchHunkLine): string {
80
- switch (line.kind) {
81
- case 'add':
82
- return `+${line.content}`;
83
- case 'remove':
84
- return `-${line.content}`;
85
- default:
86
- return ` ${line.content}`;
87
- }
88
- }
89
-
90
- function formatRange(start: number, count: number) {
91
- const normalizedStart = Math.max(0, start);
92
- if (count === 0) return `${normalizedStart},0`;
93
- if (count === 1) return `${normalizedStart}`;
94
- return `${normalizedStart},${count}`;
95
- }
96
-
97
- function formatHunkHeader(hunk: AppliedPatchHunk) {
98
- const oldRange = formatRange(hunk.oldStart, hunk.oldLines);
99
- const newRange = formatRange(hunk.newStart, hunk.newLines);
100
- const context = hunk.header.context?.trim();
101
- return context
102
- ? `@@ -${oldRange} +${newRange} @@ ${context}`
103
- : `@@ -${oldRange} +${newRange} @@`;
104
- }
105
-
106
- function formatNormalizedPatch(operations: AppliedPatchOperation[]): string {
107
- const lines: string[] = [PATCH_BEGIN_MARKER];
108
- for (const op of operations) {
109
- switch (op.kind) {
110
- case 'add':
111
- lines.push(`${PATCH_ADD_PREFIX} ${op.filePath}`);
112
- break;
113
- case 'delete':
114
- lines.push(`${PATCH_DELETE_PREFIX} ${op.filePath}`);
115
- break;
116
- case 'update':
117
- lines.push(`${PATCH_UPDATE_PREFIX} ${op.filePath}`);
118
- break;
119
- }
120
-
121
- if (op.kind === 'add' || op.kind === 'delete') {
122
- for (const hunk of op.hunks) {
123
- lines.push(formatHunkHeader(hunk));
124
- for (const line of hunk.lines) {
125
- lines.push(serializePatchLine(line));
126
- }
127
- }
128
- continue;
129
- }
130
-
131
- for (const hunk of op.hunks) {
132
- lines.push(formatHunkHeader(hunk));
133
- for (const line of hunk.lines) {
134
- lines.push(serializePatchLine(line));
135
- }
136
- }
137
- }
138
- lines.push(PATCH_END_MARKER);
139
- return lines.join('\n');
140
- }
141
-
142
- function findLineIndex(
143
- lines: string[],
144
- pattern: string,
145
- start: number,
146
- useFuzzy: boolean,
147
- ): number {
148
- for (let i = Math.max(0, start); i < lines.length; i++) {
149
- if (lines[i] === pattern) return i;
150
- if (!useFuzzy) continue;
151
- for (const level of NORMALIZATION_LEVELS.slice(1)) {
152
- if (
153
- normalizeWhitespace(lines[i], level) ===
154
- normalizeWhitespace(pattern, level)
155
- ) {
156
- return i;
157
- }
158
- }
159
- }
160
- return -1;
161
- }
162
-
163
- function findSubsequence(
164
- lines: string[],
165
- pattern: string[],
166
- startIndex: number,
167
- useFuzzy: boolean,
168
- ): number {
169
- if (pattern.length === 0) return -1;
170
- const start = Math.max(0, startIndex);
171
- for (let i = start; i <= lines.length - pattern.length; i++) {
172
- let matches = true;
173
- for (let j = 0; j < pattern.length; j++) {
174
- const line = lines[i + j];
175
- const target = pattern[j];
176
- if (line === target) continue;
177
- if (!useFuzzy) {
178
- matches = false;
179
- break;
180
- }
181
- let matched = false;
182
- for (const level of NORMALIZATION_LEVELS.slice(1)) {
183
- if (
184
- normalizeWhitespace(line, level) ===
185
- normalizeWhitespace(target, level)
186
- ) {
187
- matched = true;
188
- break;
189
- }
190
- }
191
- if (!matched) {
192
- matches = false;
193
- break;
194
- }
195
- }
196
- if (matches) return i;
197
- }
198
- return -1;
199
- }
200
-
201
- function computeInsertionIndex(
202
- lines: string[],
203
- header: PatchHunk['header'],
204
- hint: number,
205
- ): number {
206
- if (header.context) {
207
- const contextIndex = findLineIndex(lines, header.context, 0, true);
208
- if (contextIndex !== -1) return contextIndex + 1;
209
- }
210
-
211
- if (typeof header.oldStart === 'number') {
212
- const zeroBased = Math.max(0, header.oldStart - 1);
213
- return Math.min(lines.length, zeroBased);
214
- }
215
-
216
- if (typeof header.newStart === 'number') {
217
- const zeroBased = Math.max(0, header.newStart - 1);
218
- return Math.min(lines.length, zeroBased);
219
- }
220
-
221
- return Math.min(lines.length, Math.max(0, hint));
222
- }
223
-
224
- function lineExists(
225
- lines: string[],
226
- target: string,
227
- useFuzzy: boolean,
228
- ): boolean {
229
- return findLineIndex(lines, target, 0, useFuzzy) !== -1;
230
- }
231
-
232
- function isHunkAlreadyApplied(
233
- lines: string[],
234
- hunk: PatchHunk,
235
- useFuzzy: boolean,
236
- ): boolean {
237
- const replacement = hunk.lines
238
- .filter((line) => line.kind !== 'remove')
239
- .map((line) => line.content);
240
-
241
- if (replacement.length > 0) {
242
- return findSubsequence(lines, replacement, 0, useFuzzy) !== -1;
243
- }
244
-
245
- const removals = hunk.lines.filter((line) => line.kind === 'remove');
246
- const _additions = hunk.lines
247
- .filter((line) => line.kind === 'add')
248
- .map((line) => line.content);
249
- const _contextLines = hunk.lines
250
- .filter((line) => line.kind === 'context')
251
- .map((line) => line.content);
252
- if (removals.length === 0) return false;
253
- return removals.every((line) => !lineExists(lines, line.content, useFuzzy));
254
- }
255
-
256
- function adjustReplacementIndentation(
257
- hunk: PatchHunk,
258
- matchedFileLines: string[],
259
- allFileLines?: string[],
260
- ): string[] {
261
- const result: string[] = [];
262
- let expectedIdx = 0;
263
- let lastDelta = 0;
264
- let lastFileIndentExpanded = 0;
265
- let lastPatchIndentExpanded = 0;
266
- let hasDelta = false;
267
- let hasStyleMismatch = false;
268
- let fileIndentChar: 'tab' | 'space' = 'space';
269
- const deltas: number[] = [];
270
- let hasAddStyleMismatch = false;
271
- let fileIndentDetected = false;
272
-
273
- for (const fl of matchedFileLines) {
274
- const ws = getLeadingWhitespace(fl);
275
- if (ws.length > 0) {
276
- fileIndentChar = detectIndentStyle(ws);
277
- fileIndentDetected = true;
278
- break;
279
- }
280
- }
281
-
282
- if (!fileIndentDetected && allFileLines) {
283
- for (const fl of allFileLines) {
284
- const ws = getLeadingWhitespace(fl);
285
- if (ws.length > 0) {
286
- fileIndentChar = detectIndentStyle(ws);
287
- fileIndentDetected = true;
288
- break;
289
- }
290
- }
291
- }
292
-
293
- const patchContextLines = hunk.lines
294
- .filter((l) => l.kind === 'context' || l.kind === 'remove')
295
- .map((l) => l.content);
296
- const tabSize = inferTabSizeFromPairs(patchContextLines, matchedFileLines);
297
-
298
- let tempIdx = 0;
299
- for (const line of hunk.lines) {
300
- if (line.kind === 'context' || line.kind === 'remove') {
301
- const fileLine = matchedFileLines[tempIdx];
302
- if (fileLine !== undefined) {
303
- const d = computeIndentDelta(line.content, fileLine, tabSize);
304
- if (d !== 0) deltas.push(d);
305
- }
306
- tempIdx++;
307
- }
308
- }
309
- const sortedDeltas = [...deltas].sort((a, b) => a - b);
310
- const medianDelta =
311
- sortedDeltas.length > 0
312
- ? sortedDeltas[Math.floor(sortedDeltas.length / 2)]
313
- : 0;
314
-
315
- for (const line of hunk.lines) {
316
- if (line.kind === 'add' && line.content.trim() !== '') {
317
- const ws = getLeadingWhitespace(line.content);
318
- if (ws.length > 0 && detectIndentStyle(ws) !== fileIndentChar) {
319
- hasAddStyleMismatch = true;
320
- break;
321
- }
322
- }
323
- }
324
-
325
- for (const line of hunk.lines) {
326
- if (line.kind === 'context') {
327
- const fileLine = matchedFileLines[expectedIdx];
328
- if (fileLine !== undefined) {
329
- lastDelta = computeIndentDelta(line.content, fileLine, tabSize);
330
- lastFileIndentExpanded = expandWhitespace(
331
- getLeadingWhitespace(fileLine),
332
- tabSize,
333
- );
334
- lastPatchIndentExpanded = expandWhitespace(
335
- getLeadingWhitespace(line.content),
336
- tabSize,
337
- );
338
- if (lastDelta !== 0) hasDelta = true;
339
- if (
340
- detectIndentStyle(getLeadingWhitespace(fileLine)) !==
341
- detectIndentStyle(getLeadingWhitespace(line.content)) &&
342
- getLeadingWhitespace(fileLine).length > 0
343
- ) {
344
- hasStyleMismatch = true;
345
- }
346
- result.push(fileLine);
347
- } else {
348
- result.push(line.content);
349
- }
350
- expectedIdx++;
351
- } else if (line.kind === 'remove') {
352
- const fileLine = matchedFileLines[expectedIdx];
353
- if (fileLine !== undefined) {
354
- lastDelta = computeIndentDelta(line.content, fileLine, tabSize);
355
- lastFileIndentExpanded = expandWhitespace(
356
- getLeadingWhitespace(fileLine),
357
- tabSize,
358
- );
359
- lastPatchIndentExpanded = expandWhitespace(
360
- getLeadingWhitespace(line.content),
361
- tabSize,
362
- );
363
- if (lastDelta !== 0) hasDelta = true;
364
- if (
365
- detectIndentStyle(getLeadingWhitespace(fileLine)) !==
366
- detectIndentStyle(getLeadingWhitespace(line.content)) &&
367
- getLeadingWhitespace(fileLine).length > 0
368
- ) {
369
- hasStyleMismatch = true;
370
- }
371
- }
372
- expectedIdx++;
373
- } else if (line.kind === 'add') {
374
- const addIndent = expandWhitespace(
375
- getLeadingWhitespace(line.content),
376
- tabSize,
377
- );
378
- const addWs = getLeadingWhitespace(line.content);
379
- const addStyle =
380
- addWs.length > 0 ? detectIndentStyle(addWs) : fileIndentChar;
381
- const styleMismatch =
382
- addStyle !== fileIndentChar && line.content.trim() !== '';
383
- if (styleMismatch) {
384
- const relativeOffset = addIndent - lastPatchIndentExpanded;
385
- const targetIndent = lastFileIndentExpanded + relativeOffset;
386
- const actualDelta = targetIndent - addIndent;
387
- result.push(
388
- applyIndentDelta(line.content, actualDelta, fileIndentChar, tabSize),
389
- );
390
- } else if (Math.abs(medianDelta) > tabSize) {
391
- result.push(
392
- applyIndentDelta(line.content, medianDelta, fileIndentChar, tabSize),
393
- );
394
- } else {
395
- result.push(line.content);
396
- }
397
- }
398
- }
399
-
400
- if (!hasDelta && !hasStyleMismatch && !hasAddStyleMismatch) {
401
- return hunk.lines.filter((l) => l.kind !== 'remove').map((l) => l.content);
402
- }
403
-
404
- return result;
405
- }
406
-
407
- function applyHunkToLines(
408
- lines: string[],
409
- originalLines: string[],
410
- hunk: PatchHunk,
411
- hint: number,
412
- useFuzzy: boolean,
413
- ): AppliedPatchHunk | null {
414
- const expected = hunk.lines
415
- .filter((line) => line.kind !== 'add')
416
- .map((line) => line.content);
417
- const replacement = hunk.lines
418
- .filter((line) => line.kind !== 'remove')
419
- .map((line) => line.content);
420
-
421
- const removals = hunk.lines.filter((line) => line.kind === 'remove');
422
- const additions = hunk.lines
423
- .filter((line) => line.kind === 'add')
424
- .map((line) => line.content);
425
- const contextLines = hunk.lines
426
- .filter((line) => line.kind === 'context')
427
- .map((line) => line.content);
428
-
429
- const hasExpected = expected.length > 0;
430
- const initialHint =
431
- typeof hunk.header.oldStart === 'number'
432
- ? Math.max(0, hunk.header.oldStart - 1)
433
- : hint;
434
-
435
- let matchIndex = hasExpected
436
- ? findSubsequence(lines, expected, Math.max(0, initialHint - 3), useFuzzy)
437
- : -1;
438
- let matchedExpected = expected;
439
-
440
- if (hasExpected && matchIndex === -1) {
441
- matchIndex = findSubsequence(lines, expected, 0, useFuzzy);
442
- }
443
-
444
- if (matchIndex === -1 && removals.length > 0) {
445
- const allContextPresent = contextLines.every((line) =>
446
- lineExists(lines, line, useFuzzy),
447
- );
448
- if (!allContextPresent) {
449
- matchIndex = -1;
450
- } else {
451
- const expectedWithoutMissingRemovals = hunk.lines
452
- .filter((line) => {
453
- if (line.kind === 'add') return false;
454
- if (line.kind === 'remove') {
455
- return lineExists(lines, line.content, useFuzzy);
456
- }
457
- return true;
458
- })
459
- .map((line) => line.content);
460
- const includedRemovalCount = hunk.lines.filter(
461
- (line) =>
462
- line.kind === 'remove' && lineExists(lines, line.content, useFuzzy),
463
- ).length;
464
- const minRequired = Math.max(contextLines.length, 2);
465
- if (
466
- includedRemovalCount > 0 &&
467
- expectedWithoutMissingRemovals.length >= minRequired
468
- ) {
469
- matchIndex = findSubsequence(
470
- lines,
471
- expectedWithoutMissingRemovals,
472
- Math.max(0, initialHint - 3),
473
- useFuzzy,
474
- );
475
- if (matchIndex === -1) {
476
- matchIndex = findSubsequence(
477
- lines,
478
- expectedWithoutMissingRemovals,
479
- 0,
480
- useFuzzy,
481
- );
482
- }
483
- if (matchIndex !== -1) {
484
- matchedExpected = expectedWithoutMissingRemovals;
485
- }
486
- }
487
- }
488
- }
489
-
490
- if (
491
- matchIndex === -1 &&
492
- useFuzzy &&
493
- removals.length >= 2 &&
494
- contextLines.length === 0
495
- ) {
496
- const firstRemoval = removals[0].content;
497
- const lastRemoval = removals[removals.length - 1].content;
498
- const firstIdx = findLineIndex(lines, firstRemoval, 0, true);
499
- if (firstIdx !== -1) {
500
- const rangeEnd = firstIdx + expected.length - 1;
501
- if (rangeEnd < lines.length) {
502
- const lastInRange = lines[rangeEnd];
503
- let lastMatches = lastInRange === lastRemoval;
504
- if (!lastMatches) {
505
- for (const level of NORMALIZATION_LEVELS.slice(1)) {
506
- if (
507
- normalizeWhitespace(lastInRange, level) ===
508
- normalizeWhitespace(lastRemoval, level)
509
- ) {
510
- lastMatches = true;
511
- break;
512
- }
513
- }
514
- }
515
- if (lastMatches) {
516
- let matchCount = 0;
517
- for (let k = 0; k < expected.length; k++) {
518
- const fileLine = lines[firstIdx + k];
519
- const expLine = expected[k];
520
- if (fileLine === expLine) {
521
- matchCount++;
522
- continue;
523
- }
524
- for (const level of NORMALIZATION_LEVELS.slice(1)) {
525
- if (
526
- normalizeWhitespace(fileLine, level) ===
527
- normalizeWhitespace(expLine, level)
528
- ) {
529
- matchCount++;
530
- break;
531
- }
532
- }
533
- }
534
- const matchRatio = matchCount / expected.length;
535
- if (matchRatio >= 0.5) {
536
- matchIndex = firstIdx;
537
- matchedExpected = lines.slice(firstIdx, firstIdx + expected.length);
538
- }
539
- }
540
- }
541
- }
542
- }
543
-
544
- if (matchIndex === -1 && isHunkAlreadyApplied(lines, hunk, useFuzzy)) {
545
- const skipStart =
546
- initialHint >= 0 && initialHint < lines.length ? initialHint + 1 : 1;
547
- return {
548
- header: { ...hunk.header },
549
- lines: hunk.lines.map((line) => ({ ...line })),
550
- oldStart: skipStart,
551
- oldLines: 0,
552
- newStart: skipStart,
553
- newLines: replacement.length,
554
- additions: hunk.lines.filter((l) => l.kind === 'add').length,
555
- deletions: hunk.lines.filter((l) => l.kind === 'remove').length,
556
- };
557
- }
558
-
559
- if (matchIndex === -1 && !hasExpected) {
560
- matchIndex = computeInsertionIndex(lines, hunk.header, initialHint);
561
- }
562
-
563
- if (matchIndex === -1) {
564
- const contextInfo = hunk.header.context
565
- ? ` near context '${hunk.header.context}'`
566
- : '';
567
-
568
- if (additions.length > 0) {
569
- const hasRemovals = removals.length > 0;
570
- let anchorIndex = -1;
571
- if (!hasRemovals && contextLines.length > 0) {
572
- const anchorContext = contextLines[contextLines.length - 1];
573
- anchorIndex = findLineIndex(lines, anchorContext, 0, useFuzzy);
574
- } else if (!hasRemovals) {
575
- anchorIndex = -1;
576
- }
577
-
578
- const insertionIndex =
579
- anchorIndex !== -1
580
- ? anchorIndex + 1
581
- : computeInsertionIndex(lines, hunk.header, initialHint);
582
-
583
- if (
584
- findSubsequence(
585
- lines,
586
- additions,
587
- Math.max(0, insertionIndex - additions.length),
588
- useFuzzy,
589
- ) !== -1
590
- ) {
591
- const skipStart =
592
- insertionIndex >= 0 && insertionIndex < lines.length
593
- ? insertionIndex + 1
594
- : lines.length + 1;
595
- return {
596
- header: { ...hunk.header },
597
- lines: hunk.lines.map((line) => ({ ...line })),
598
- oldStart: skipStart,
599
- oldLines: 0,
600
- newStart: skipStart,
601
- newLines: additions.length,
602
- additions: additions.length,
603
- deletions: 0,
604
- };
605
- }
606
- }
607
-
608
- let errorMsg = `Failed to apply patch hunk${contextInfo}.`;
609
- if (expected.length > 0) {
610
- errorMsg += `\nExpected to find:\n${expected
611
- .map((l) => ` ${l}`)
612
- .join('\n')}`;
613
- }
614
- if (removals.length > 0) {
615
- const missing = removals
616
- .filter((line) => !lineExists(lines, line.content, useFuzzy))
617
- .map((line) => line.content);
618
- if (missing.length === removals.length) {
619
- errorMsg +=
620
- '\nAll removal lines already absent; consider reading the file again to capture current state.';
621
- }
622
- }
623
- throw new Error(errorMsg);
624
- }
625
-
626
- const deleteCount = hasExpected ? matchedExpected.length : 0;
627
- const originalIndex = matchIndex;
628
- const oldStart = Math.min(
629
- originalLines.length,
630
- Math.max(0, originalIndex) + 1,
631
- );
632
- const newStart = matchIndex + 1;
633
-
634
- const adjustedReplacement =
635
- useFuzzy && hasExpected && matchedExpected.length === expected.length
636
- ? adjustReplacementIndentation(
637
- hunk,
638
- lines.slice(matchIndex, matchIndex + matchedExpected.length),
639
- originalLines,
640
- )
641
- : replacement;
642
-
643
- const targetSlice = lines.slice(
644
- matchIndex,
645
- matchIndex + adjustedReplacement.length,
646
- );
647
- if (
648
- adjustedReplacement.length > 0 &&
649
- adjustedReplacement.length === targetSlice.length &&
650
- adjustedReplacement.every((line, i) => {
651
- if (line === targetSlice[i]) return true;
652
- if (!useFuzzy) return false;
653
- for (const level of NORMALIZATION_LEVELS.slice(1)) {
654
- if (
655
- normalizeWhitespace(line, level) ===
656
- normalizeWhitespace(targetSlice[i], level)
657
- ) {
658
- return true;
659
- }
660
- }
661
- return false;
662
- })
663
- ) {
664
- const skipStart = matchIndex + 1;
665
- return {
666
- header: { ...hunk.header },
667
- lines: hunk.lines.map((line) => ({ ...line })),
668
- oldStart: skipStart,
669
- oldLines: 0,
670
- newStart: skipStart,
671
- newLines: adjustedReplacement.length,
672
- additions: 0,
673
- deletions: 0,
674
- };
675
- }
676
-
677
- lines.splice(matchIndex, deleteCount, ...adjustedReplacement);
678
-
679
- return {
680
- header: { ...hunk.header },
681
- lines: hunk.lines.map((line) => ({ ...line })),
682
- oldStart,
683
- oldLines: deleteCount,
684
- newStart,
685
- newLines: adjustedReplacement.length,
686
- additions: hunk.lines.filter((l) => l.kind === 'add').length,
687
- deletions: hunk.lines.filter((l) => l.kind === 'remove').length,
688
- };
689
- }
690
-
691
34
  async function applyAddOperation(
692
35
  projectRoot: string,
693
36
  operation: PatchAddOperation,
@@ -0,0 +1,17 @@
1
+ export function parseHunkHeader(raw: string) {
2
+ const match = raw.match(
3
+ /^@@\s*-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s*@@(?:\s*(.*))?$/,
4
+ );
5
+ if (match) {
6
+ const [, oldStart, oldCount, newStart, newCount, context] = match;
7
+ return {
8
+ oldStart: Number.parseInt(oldStart, 10),
9
+ oldLines: oldCount ? Number.parseInt(oldCount, 10) : undefined,
10
+ newStart: Number.parseInt(newStart, 10),
11
+ newLines: newCount ? Number.parseInt(newCount, 10) : undefined,
12
+ context: context?.trim() || undefined,
13
+ };
14
+ }
15
+ const context = raw.replace(/^@@/, '').trim();
16
+ return context ? { context } : {};
17
+ }