@ottocode/sdk 0.1.173

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 (125) hide show
  1. package/README.md +338 -0
  2. package/package.json +128 -0
  3. package/src/agent/types.ts +19 -0
  4. package/src/auth/src/copilot-oauth.ts +190 -0
  5. package/src/auth/src/index.ts +100 -0
  6. package/src/auth/src/oauth.ts +234 -0
  7. package/src/auth/src/openai-oauth.ts +394 -0
  8. package/src/auth/src/wallet.ts +51 -0
  9. package/src/browser.ts +32 -0
  10. package/src/config/src/index.ts +110 -0
  11. package/src/config/src/manager.ts +181 -0
  12. package/src/config/src/paths.ts +98 -0
  13. package/src/core/src/errors.ts +102 -0
  14. package/src/core/src/index.ts +108 -0
  15. package/src/core/src/providers/resolver.ts +244 -0
  16. package/src/core/src/streaming/artifacts.ts +41 -0
  17. package/src/core/src/terminals/bun-pty.ts +13 -0
  18. package/src/core/src/terminals/circular-buffer.ts +30 -0
  19. package/src/core/src/terminals/ensure-bun-pty.ts +70 -0
  20. package/src/core/src/terminals/index.ts +8 -0
  21. package/src/core/src/terminals/manager.ts +158 -0
  22. package/src/core/src/terminals/rust-libs.ts +30 -0
  23. package/src/core/src/terminals/terminal.ts +132 -0
  24. package/src/core/src/tools/bin-manager.ts +250 -0
  25. package/src/core/src/tools/builtin/bash.ts +155 -0
  26. package/src/core/src/tools/builtin/bash.txt +7 -0
  27. package/src/core/src/tools/builtin/file-cache.ts +39 -0
  28. package/src/core/src/tools/builtin/finish.ts +12 -0
  29. package/src/core/src/tools/builtin/finish.txt +10 -0
  30. package/src/core/src/tools/builtin/fs/cd.ts +19 -0
  31. package/src/core/src/tools/builtin/fs/cd.txt +5 -0
  32. package/src/core/src/tools/builtin/fs/index.ts +20 -0
  33. package/src/core/src/tools/builtin/fs/ls.ts +72 -0
  34. package/src/core/src/tools/builtin/fs/ls.txt +8 -0
  35. package/src/core/src/tools/builtin/fs/pwd.ts +17 -0
  36. package/src/core/src/tools/builtin/fs/pwd.txt +5 -0
  37. package/src/core/src/tools/builtin/fs/read.ts +119 -0
  38. package/src/core/src/tools/builtin/fs/read.txt +8 -0
  39. package/src/core/src/tools/builtin/fs/tree.ts +149 -0
  40. package/src/core/src/tools/builtin/fs/tree.txt +11 -0
  41. package/src/core/src/tools/builtin/fs/util.ts +95 -0
  42. package/src/core/src/tools/builtin/fs/write.ts +106 -0
  43. package/src/core/src/tools/builtin/fs/write.txt +11 -0
  44. package/src/core/src/tools/builtin/git.commit.txt +6 -0
  45. package/src/core/src/tools/builtin/git.diff.txt +5 -0
  46. package/src/core/src/tools/builtin/git.status.txt +5 -0
  47. package/src/core/src/tools/builtin/git.ts +151 -0
  48. package/src/core/src/tools/builtin/glob.ts +128 -0
  49. package/src/core/src/tools/builtin/glob.txt +10 -0
  50. package/src/core/src/tools/builtin/grep.ts +136 -0
  51. package/src/core/src/tools/builtin/grep.txt +9 -0
  52. package/src/core/src/tools/builtin/ignore.ts +45 -0
  53. package/src/core/src/tools/builtin/patch/apply.ts +546 -0
  54. package/src/core/src/tools/builtin/patch/constants.ts +5 -0
  55. package/src/core/src/tools/builtin/patch/normalize.ts +31 -0
  56. package/src/core/src/tools/builtin/patch/parse-enveloped.ts +209 -0
  57. package/src/core/src/tools/builtin/patch/parse-unified.ts +231 -0
  58. package/src/core/src/tools/builtin/patch/parse.ts +28 -0
  59. package/src/core/src/tools/builtin/patch/text.ts +23 -0
  60. package/src/core/src/tools/builtin/patch/types.ts +82 -0
  61. package/src/core/src/tools/builtin/patch.ts +167 -0
  62. package/src/core/src/tools/builtin/patch.txt +207 -0
  63. package/src/core/src/tools/builtin/progress.ts +55 -0
  64. package/src/core/src/tools/builtin/progress.txt +7 -0
  65. package/src/core/src/tools/builtin/ripgrep.ts +125 -0
  66. package/src/core/src/tools/builtin/ripgrep.txt +7 -0
  67. package/src/core/src/tools/builtin/terminal.ts +300 -0
  68. package/src/core/src/tools/builtin/terminal.txt +93 -0
  69. package/src/core/src/tools/builtin/todos.ts +66 -0
  70. package/src/core/src/tools/builtin/todos.txt +7 -0
  71. package/src/core/src/tools/builtin/websearch.ts +250 -0
  72. package/src/core/src/tools/builtin/websearch.txt +12 -0
  73. package/src/core/src/tools/error.ts +67 -0
  74. package/src/core/src/tools/loader.ts +421 -0
  75. package/src/core/src/types/index.ts +11 -0
  76. package/src/core/src/types/types.ts +4 -0
  77. package/src/core/src/utils/ansi.ts +27 -0
  78. package/src/core/src/utils/debug.ts +40 -0
  79. package/src/core/src/utils/logger.ts +150 -0
  80. package/src/index.ts +313 -0
  81. package/src/prompts/src/agents/build.txt +89 -0
  82. package/src/prompts/src/agents/general.txt +15 -0
  83. package/src/prompts/src/agents/plan.txt +10 -0
  84. package/src/prompts/src/agents/research.txt +50 -0
  85. package/src/prompts/src/base.txt +24 -0
  86. package/src/prompts/src/debug.ts +104 -0
  87. package/src/prompts/src/index.ts +1 -0
  88. package/src/prompts/src/modes/oneshot.txt +9 -0
  89. package/src/prompts/src/providers/anthropic.txt +247 -0
  90. package/src/prompts/src/providers/anthropicSpoof.txt +1 -0
  91. package/src/prompts/src/providers/default.txt +466 -0
  92. package/src/prompts/src/providers/google.txt +230 -0
  93. package/src/prompts/src/providers/moonshot.txt +24 -0
  94. package/src/prompts/src/providers/openai.txt +414 -0
  95. package/src/prompts/src/providers.ts +143 -0
  96. package/src/providers/src/anthropic-caching.ts +202 -0
  97. package/src/providers/src/anthropic-oauth-client.ts +157 -0
  98. package/src/providers/src/authorization.ts +17 -0
  99. package/src/providers/src/catalog-manual.ts +135 -0
  100. package/src/providers/src/catalog-merged.ts +9 -0
  101. package/src/providers/src/catalog.ts +8329 -0
  102. package/src/providers/src/copilot-client.ts +39 -0
  103. package/src/providers/src/env.ts +31 -0
  104. package/src/providers/src/google-client.ts +16 -0
  105. package/src/providers/src/index.ts +75 -0
  106. package/src/providers/src/moonshot-client.ts +25 -0
  107. package/src/providers/src/oauth-models.ts +39 -0
  108. package/src/providers/src/openai-oauth-client.ts +108 -0
  109. package/src/providers/src/opencode-client.ts +64 -0
  110. package/src/providers/src/openrouter-client.ts +31 -0
  111. package/src/providers/src/pricing.ts +178 -0
  112. package/src/providers/src/setu-client.ts +643 -0
  113. package/src/providers/src/utils.ts +210 -0
  114. package/src/providers/src/validate.ts +39 -0
  115. package/src/providers/src/zai-client.ts +47 -0
  116. package/src/skills/index.ts +34 -0
  117. package/src/skills/loader.ts +152 -0
  118. package/src/skills/parser.ts +108 -0
  119. package/src/skills/tool.ts +87 -0
  120. package/src/skills/types.ts +41 -0
  121. package/src/skills/validator.ts +110 -0
  122. package/src/types/src/auth.ts +33 -0
  123. package/src/types/src/config.ts +36 -0
  124. package/src/types/src/index.ts +20 -0
  125. package/src/types/src/provider.ts +71 -0
@@ -0,0 +1,546 @@
1
+ import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
2
+ import { dirname, isAbsolute, relative, resolve } from 'node:path';
3
+
4
+ import { NORMALIZATION_LEVELS, normalizeWhitespace } from './normalize.ts';
5
+ import {
6
+ PATCH_ADD_PREFIX,
7
+ PATCH_DELETE_PREFIX,
8
+ PATCH_UPDATE_PREFIX,
9
+ PATCH_BEGIN_MARKER,
10
+ PATCH_END_MARKER,
11
+ } from './constants.ts';
12
+ import type {
13
+ AppliedPatchHunk,
14
+ AppliedPatchOperation,
15
+ PatchAddOperation,
16
+ PatchApplicationResult,
17
+ PatchDeleteOperation,
18
+ PatchHunk,
19
+ PatchHunkLine,
20
+ PatchOperation,
21
+ PatchSummary,
22
+ PatchUpdateOperation,
23
+ RejectedPatch,
24
+ } from './types.ts';
25
+ import { ensureTrailingNewline, joinLines, splitLines } from './text.ts';
26
+
27
+ export function resolveProjectPath(
28
+ projectRoot: string,
29
+ filePath: string,
30
+ ): string {
31
+ const fullPath = resolve(projectRoot, filePath);
32
+ const relativePath = relative(projectRoot, fullPath);
33
+ if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
34
+ throw new Error(`Patch path escapes project root: ${filePath}`);
35
+ }
36
+ return fullPath;
37
+ }
38
+
39
+ function makeAppliedRecord(
40
+ kind: AppliedPatchOperation['kind'],
41
+ filePath: string,
42
+ hunks: AppliedPatchHunk[],
43
+ ): AppliedPatchOperation {
44
+ const stats = hunks.reduce(
45
+ (acc, hunk) => ({
46
+ additions: acc.additions + hunk.additions,
47
+ deletions: acc.deletions + hunk.deletions,
48
+ }),
49
+ { additions: 0, deletions: 0 },
50
+ );
51
+ return {
52
+ kind,
53
+ filePath,
54
+ stats,
55
+ hunks,
56
+ };
57
+ }
58
+
59
+ function makeSummary(operations: AppliedPatchOperation[]): PatchSummary {
60
+ return operations.reduce<PatchSummary>(
61
+ (acc, op) => ({
62
+ files: acc.files + 1,
63
+ additions: acc.additions + op.stats.additions,
64
+ deletions: acc.deletions + op.stats.deletions,
65
+ }),
66
+ { files: 0, additions: 0, deletions: 0 },
67
+ );
68
+ }
69
+
70
+ function serializePatchLine(line: PatchHunkLine): string {
71
+ switch (line.kind) {
72
+ case 'add':
73
+ return `+${line.content}`;
74
+ case 'remove':
75
+ return `-${line.content}`;
76
+ default:
77
+ return ` ${line.content}`;
78
+ }
79
+ }
80
+
81
+ function formatRange(start: number, count: number) {
82
+ const normalizedStart = Math.max(0, start);
83
+ if (count === 0) return `${normalizedStart},0`;
84
+ if (count === 1) return `${normalizedStart}`;
85
+ return `${normalizedStart},${count}`;
86
+ }
87
+
88
+ function formatHunkHeader(hunk: AppliedPatchHunk) {
89
+ const oldRange = formatRange(hunk.oldStart, hunk.oldLines);
90
+ const newRange = formatRange(hunk.newStart, hunk.newLines);
91
+ const context = hunk.header.context?.trim();
92
+ return context
93
+ ? `@@ -${oldRange} +${newRange} @@ ${context}`
94
+ : `@@ -${oldRange} +${newRange} @@`;
95
+ }
96
+
97
+ function formatNormalizedPatch(operations: AppliedPatchOperation[]): string {
98
+ const lines: string[] = [PATCH_BEGIN_MARKER];
99
+ for (const op of operations) {
100
+ switch (op.kind) {
101
+ case 'add':
102
+ lines.push(`${PATCH_ADD_PREFIX} ${op.filePath}`);
103
+ break;
104
+ case 'delete':
105
+ lines.push(`${PATCH_DELETE_PREFIX} ${op.filePath}`);
106
+ break;
107
+ case 'update':
108
+ lines.push(`${PATCH_UPDATE_PREFIX} ${op.filePath}`);
109
+ break;
110
+ }
111
+
112
+ if (op.kind === 'add' || op.kind === 'delete') {
113
+ for (const hunk of op.hunks) {
114
+ lines.push(formatHunkHeader(hunk));
115
+ for (const line of hunk.lines) {
116
+ lines.push(serializePatchLine(line));
117
+ }
118
+ }
119
+ continue;
120
+ }
121
+
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
+ }
129
+ lines.push(PATCH_END_MARKER);
130
+ return lines.join('\n');
131
+ }
132
+
133
+ function findLineIndex(
134
+ lines: string[],
135
+ pattern: string,
136
+ start: number,
137
+ useFuzzy: boolean,
138
+ ): number {
139
+ for (let i = Math.max(0, start); i < lines.length; i++) {
140
+ if (lines[i] === pattern) return i;
141
+ if (!useFuzzy) continue;
142
+ for (const level of NORMALIZATION_LEVELS.slice(1)) {
143
+ if (
144
+ normalizeWhitespace(lines[i], level) ===
145
+ normalizeWhitespace(pattern, level)
146
+ ) {
147
+ return i;
148
+ }
149
+ }
150
+ }
151
+ return -1;
152
+ }
153
+
154
+ function findSubsequence(
155
+ lines: string[],
156
+ pattern: string[],
157
+ startIndex: number,
158
+ useFuzzy: boolean,
159
+ ): number {
160
+ if (pattern.length === 0) return -1;
161
+ const start = Math.max(0, startIndex);
162
+ for (let i = start; i <= lines.length - pattern.length; i++) {
163
+ let matches = true;
164
+ for (let j = 0; j < pattern.length; j++) {
165
+ const line = lines[i + j];
166
+ const target = pattern[j];
167
+ if (line === target) continue;
168
+ if (!useFuzzy) {
169
+ matches = false;
170
+ break;
171
+ }
172
+ let matched = false;
173
+ for (const level of NORMALIZATION_LEVELS.slice(1)) {
174
+ if (
175
+ normalizeWhitespace(line, level) ===
176
+ normalizeWhitespace(target, level)
177
+ ) {
178
+ matched = true;
179
+ break;
180
+ }
181
+ }
182
+ if (!matched) {
183
+ matches = false;
184
+ break;
185
+ }
186
+ }
187
+ if (matches) return i;
188
+ }
189
+ return -1;
190
+ }
191
+
192
+ function computeInsertionIndex(
193
+ lines: string[],
194
+ header: PatchHunk['header'],
195
+ hint: number,
196
+ ): number {
197
+ if (header.context) {
198
+ const contextIndex = findLineIndex(lines, header.context, 0, true);
199
+ if (contextIndex !== -1) return contextIndex + 1;
200
+ }
201
+
202
+ if (typeof header.oldStart === 'number') {
203
+ const zeroBased = Math.max(0, header.oldStart - 1);
204
+ return Math.min(lines.length, zeroBased);
205
+ }
206
+
207
+ if (typeof header.newStart === 'number') {
208
+ const zeroBased = Math.max(0, header.newStart - 1);
209
+ return Math.min(lines.length, zeroBased);
210
+ }
211
+
212
+ return Math.min(lines.length, Math.max(0, hint));
213
+ }
214
+
215
+ function lineExists(
216
+ lines: string[],
217
+ target: string,
218
+ useFuzzy: boolean,
219
+ ): boolean {
220
+ return findLineIndex(lines, target, 0, useFuzzy) !== -1;
221
+ }
222
+
223
+ function isHunkAlreadyApplied(
224
+ lines: string[],
225
+ hunk: PatchHunk,
226
+ useFuzzy: boolean,
227
+ ): boolean {
228
+ const replacement = hunk.lines
229
+ .filter((line) => line.kind !== 'remove')
230
+ .map((line) => line.content);
231
+
232
+ if (replacement.length > 0) {
233
+ return findSubsequence(lines, replacement, 0, useFuzzy) !== -1;
234
+ }
235
+
236
+ const removals = hunk.lines.filter((line) => line.kind === 'remove');
237
+ const _additions = hunk.lines
238
+ .filter((line) => line.kind === 'add')
239
+ .map((line) => line.content);
240
+ const _contextLines = hunk.lines
241
+ .filter((line) => line.kind === 'context')
242
+ .map((line) => line.content);
243
+ if (removals.length === 0) return false;
244
+ return removals.every((line) => !lineExists(lines, line.content, useFuzzy));
245
+ }
246
+
247
+ function applyHunkToLines(
248
+ lines: string[],
249
+ originalLines: string[],
250
+ hunk: PatchHunk,
251
+ hint: number,
252
+ useFuzzy: boolean,
253
+ ): AppliedPatchHunk | null {
254
+ const expected = hunk.lines
255
+ .filter((line) => line.kind !== 'add')
256
+ .map((line) => line.content);
257
+ const replacement = hunk.lines
258
+ .filter((line) => line.kind !== 'remove')
259
+ .map((line) => line.content);
260
+
261
+ const removals = hunk.lines.filter((line) => line.kind === 'remove');
262
+ const additions = hunk.lines
263
+ .filter((line) => line.kind === 'add')
264
+ .map((line) => line.content);
265
+ const contextLines = hunk.lines
266
+ .filter((line) => line.kind === 'context')
267
+ .map((line) => line.content);
268
+
269
+ const hasExpected = expected.length > 0;
270
+ const initialHint =
271
+ typeof hunk.header.oldStart === 'number'
272
+ ? Math.max(0, hunk.header.oldStart - 1)
273
+ : hint;
274
+
275
+ let matchIndex = hasExpected
276
+ ? findSubsequence(lines, expected, Math.max(0, initialHint - 3), useFuzzy)
277
+ : -1;
278
+
279
+ if (hasExpected && matchIndex === -1) {
280
+ matchIndex = findSubsequence(lines, expected, 0, useFuzzy);
281
+ }
282
+
283
+ if (matchIndex === -1 && removals.length > 0) {
284
+ const expectedWithoutMissingRemovals = expected.filter((line) =>
285
+ lineExists(lines, line, useFuzzy),
286
+ );
287
+ if (expectedWithoutMissingRemovals.length > 0) {
288
+ matchIndex = findSubsequence(
289
+ lines,
290
+ expectedWithoutMissingRemovals,
291
+ Math.max(0, initialHint - 3),
292
+ useFuzzy,
293
+ );
294
+ if (matchIndex === -1) {
295
+ matchIndex = findSubsequence(
296
+ lines,
297
+ expectedWithoutMissingRemovals,
298
+ 0,
299
+ useFuzzy,
300
+ );
301
+ }
302
+ }
303
+ }
304
+
305
+ if (matchIndex === -1 && isHunkAlreadyApplied(lines, hunk, useFuzzy)) {
306
+ const skipStart =
307
+ initialHint >= 0 && initialHint < lines.length ? initialHint + 1 : 1;
308
+ return {
309
+ header: { ...hunk.header },
310
+ lines: hunk.lines.map((line) => ({ ...line })),
311
+ oldStart: skipStart,
312
+ oldLines: 0,
313
+ newStart: skipStart,
314
+ newLines: replacement.length,
315
+ additions: hunk.lines.filter((l) => l.kind === 'add').length,
316
+ deletions: hunk.lines.filter((l) => l.kind === 'remove').length,
317
+ };
318
+ }
319
+
320
+ if (matchIndex === -1 && !hasExpected) {
321
+ matchIndex = computeInsertionIndex(lines, hunk.header, initialHint);
322
+ }
323
+
324
+ if (matchIndex === -1) {
325
+ const contextInfo = hunk.header.context
326
+ ? ` near context '${hunk.header.context}'`
327
+ : '';
328
+
329
+ if (additions.length > 0) {
330
+ const anchorContext =
331
+ contextLines.length > 0
332
+ ? contextLines[contextLines.length - 1]
333
+ : undefined;
334
+ const anchorIndex =
335
+ anchorContext !== undefined
336
+ ? findLineIndex(lines, anchorContext, 0, useFuzzy)
337
+ : -1;
338
+
339
+ const insertionIndex =
340
+ anchorIndex !== -1
341
+ ? anchorIndex + 1
342
+ : computeInsertionIndex(lines, hunk.header, initialHint);
343
+
344
+ if (
345
+ findSubsequence(
346
+ lines,
347
+ additions,
348
+ Math.max(0, insertionIndex - additions.length),
349
+ useFuzzy,
350
+ ) !== -1
351
+ ) {
352
+ const skipStart =
353
+ insertionIndex >= 0 && insertionIndex < lines.length
354
+ ? insertionIndex + 1
355
+ : lines.length + 1;
356
+ return {
357
+ header: { ...hunk.header },
358
+ lines: hunk.lines.map((line) => ({ ...line })),
359
+ oldStart: skipStart,
360
+ oldLines: 0,
361
+ newStart: skipStart,
362
+ newLines: additions.length,
363
+ additions: additions.length,
364
+ deletions: 0,
365
+ };
366
+ }
367
+ }
368
+
369
+ let errorMsg = `Failed to apply patch hunk${contextInfo}.`;
370
+ if (expected.length > 0) {
371
+ errorMsg += `\nExpected to find:\n${expected
372
+ .map((l) => ` ${l}`)
373
+ .join('\n')}`;
374
+ }
375
+ if (removals.length > 0) {
376
+ const missing = removals
377
+ .filter((line) => !lineExists(lines, line.content, useFuzzy))
378
+ .map((line) => line.content);
379
+ if (missing.length === removals.length) {
380
+ errorMsg +=
381
+ '\nAll removal lines already absent; consider reading the file again to capture current state.';
382
+ }
383
+ }
384
+ throw new Error(errorMsg);
385
+ }
386
+
387
+ const deleteCount = hasExpected ? expected.length : 0;
388
+ const originalIndex = matchIndex;
389
+ const oldStart = Math.min(
390
+ originalLines.length,
391
+ Math.max(0, originalIndex) + 1,
392
+ );
393
+ const newStart = matchIndex + 1;
394
+
395
+ lines.splice(matchIndex, deleteCount, ...replacement);
396
+
397
+ return {
398
+ header: { ...hunk.header },
399
+ lines: hunk.lines.map((line) => ({ ...line })),
400
+ oldStart,
401
+ oldLines: deleteCount,
402
+ newStart,
403
+ newLines: replacement.length,
404
+ additions: hunk.lines.filter((l) => l.kind === 'add').length,
405
+ deletions: hunk.lines.filter((l) => l.kind === 'remove').length,
406
+ };
407
+ }
408
+
409
+ async function applyAddOperation(
410
+ projectRoot: string,
411
+ operation: PatchAddOperation,
412
+ ): Promise<AppliedPatchOperation> {
413
+ const target = resolveProjectPath(projectRoot, operation.filePath);
414
+ await mkdir(dirname(target), { recursive: true });
415
+ const lines = [...operation.lines];
416
+ ensureTrailingNewline(lines);
417
+ await writeFile(target, joinLines(lines, '\n'), 'utf-8');
418
+
419
+ const appliedHunk: AppliedPatchHunk = {
420
+ header: {},
421
+ lines: operation.lines.map((line) => ({ kind: 'add', content: line })),
422
+ oldStart: 0,
423
+ oldLines: 0,
424
+ newStart: 1,
425
+ newLines: lines.length,
426
+ additions: lines.length,
427
+ deletions: 0,
428
+ };
429
+
430
+ return makeAppliedRecord('add', operation.filePath, [appliedHunk]);
431
+ }
432
+
433
+ async function applyDeleteOperation(
434
+ projectRoot: string,
435
+ operation: PatchDeleteOperation,
436
+ ): Promise<AppliedPatchOperation> {
437
+ const target = resolveProjectPath(projectRoot, operation.filePath);
438
+ let existing = '';
439
+ try {
440
+ existing = await readFile(target, 'utf-8');
441
+ } catch (error) {
442
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
443
+ throw new Error(`File not found for deletion: ${operation.filePath}`);
444
+ }
445
+ throw error;
446
+ }
447
+
448
+ const { lines } = splitLines(existing);
449
+ await unlink(target);
450
+
451
+ const appliedHunk: AppliedPatchHunk = {
452
+ header: {},
453
+ lines: lines.map((line) => ({ kind: 'remove', content: line })),
454
+ oldStart: 1,
455
+ oldLines: lines.length,
456
+ newStart: 0,
457
+ newLines: 0,
458
+ additions: 0,
459
+ deletions: lines.length,
460
+ };
461
+
462
+ return makeAppliedRecord('delete', operation.filePath, [appliedHunk]);
463
+ }
464
+
465
+ async function applyUpdateOperation(
466
+ projectRoot: string,
467
+ operation: PatchUpdateOperation,
468
+ useFuzzy: boolean,
469
+ ): Promise<AppliedPatchOperation> {
470
+ const target = resolveProjectPath(projectRoot, operation.filePath);
471
+ let original: string;
472
+ try {
473
+ original = await readFile(target, 'utf-8');
474
+ } catch (error) {
475
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
476
+ throw new Error(`File not found: ${operation.filePath}`);
477
+ }
478
+ throw error;
479
+ }
480
+
481
+ const { lines: originalLines, newline } = splitLines(original);
482
+ const workingLines = [...originalLines];
483
+ const appliedHunks: AppliedPatchHunk[] = [];
484
+ let hint = 0;
485
+
486
+ for (const hunk of operation.hunks) {
487
+ const applied = applyHunkToLines(
488
+ workingLines,
489
+ originalLines,
490
+ hunk,
491
+ hint,
492
+ useFuzzy,
493
+ );
494
+ if (!applied) continue;
495
+ appliedHunks.push(applied);
496
+ hint = applied.newStart + applied.newLines - 1;
497
+ }
498
+
499
+ ensureTrailingNewline(workingLines);
500
+ await writeFile(target, joinLines(workingLines, newline), 'utf-8');
501
+
502
+ return makeAppliedRecord('update', operation.filePath, appliedHunks);
503
+ }
504
+
505
+ export async function applyPatchOperations(
506
+ projectRoot: string,
507
+ operations: PatchOperation[],
508
+ options: { useFuzzy: boolean; allowRejects: boolean },
509
+ ): Promise<PatchApplicationResult> {
510
+ const applied: AppliedPatchOperation[] = [];
511
+ const rejected: RejectedPatch[] = [];
512
+
513
+ for (const operation of operations) {
514
+ try {
515
+ if (operation.kind === 'add') {
516
+ applied.push(await applyAddOperation(projectRoot, operation));
517
+ } else if (operation.kind === 'delete') {
518
+ applied.push(await applyDeleteOperation(projectRoot, operation));
519
+ } else {
520
+ applied.push(
521
+ await applyUpdateOperation(projectRoot, operation, options.useFuzzy),
522
+ );
523
+ }
524
+ } catch (error) {
525
+ if (options.allowRejects) {
526
+ rejected.push({
527
+ kind: operation.kind,
528
+ filePath: operation.filePath,
529
+ reason: error instanceof Error ? error.message : String(error),
530
+ operation,
531
+ });
532
+ continue;
533
+ }
534
+ throw error;
535
+ }
536
+ }
537
+
538
+ const summary = makeSummary(applied);
539
+
540
+ return {
541
+ operations: applied,
542
+ summary,
543
+ normalizedPatch: formatNormalizedPatch(applied),
544
+ rejected,
545
+ };
546
+ }
@@ -0,0 +1,5 @@
1
+ export const PATCH_BEGIN_MARKER = '*** Begin Patch';
2
+ export const PATCH_END_MARKER = '*** End Patch';
3
+ export const PATCH_ADD_PREFIX = '*** Add File:';
4
+ export const PATCH_UPDATE_PREFIX = '*** Update File:';
5
+ export const PATCH_DELETE_PREFIX = '*** Delete File:';
@@ -0,0 +1,31 @@
1
+ enum NormalizationLevel {
2
+ EXACT = 'exact',
3
+ TABS_ONLY = 'tabs',
4
+ WHITESPACE = 'whitespace',
5
+ AGGRESSIVE = 'aggressive',
6
+ }
7
+
8
+ export function normalizeWhitespace(
9
+ line: string,
10
+ level: NormalizationLevel,
11
+ ): string {
12
+ switch (level) {
13
+ case NormalizationLevel.EXACT:
14
+ return line;
15
+ case NormalizationLevel.TABS_ONLY:
16
+ return line.replace(/\t/g, ' ');
17
+ case NormalizationLevel.WHITESPACE:
18
+ return line.replace(/\t/g, ' ').replace(/\s+$/, '');
19
+ case NormalizationLevel.AGGRESSIVE:
20
+ return line.replace(/\t/g, ' ').trim();
21
+ default:
22
+ return line;
23
+ }
24
+ }
25
+
26
+ export const NORMALIZATION_LEVELS: NormalizationLevel[] = [
27
+ NormalizationLevel.EXACT,
28
+ NormalizationLevel.TABS_ONLY,
29
+ NormalizationLevel.WHITESPACE,
30
+ NormalizationLevel.AGGRESSIVE,
31
+ ];