@link-assistant/agent 0.0.9 → 0.0.12

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 (104) hide show
  1. package/EXAMPLES.md +36 -0
  2. package/MODELS.md +72 -24
  3. package/README.md +59 -2
  4. package/TOOLS.md +20 -0
  5. package/package.json +35 -2
  6. package/src/agent/agent.ts +68 -54
  7. package/src/auth/claude-oauth.ts +426 -0
  8. package/src/auth/index.ts +28 -26
  9. package/src/auth/plugins.ts +876 -0
  10. package/src/bun/index.ts +53 -43
  11. package/src/bus/global.ts +5 -5
  12. package/src/bus/index.ts +59 -53
  13. package/src/cli/bootstrap.js +12 -12
  14. package/src/cli/bootstrap.ts +6 -6
  15. package/src/cli/cmd/agent.ts +97 -92
  16. package/src/cli/cmd/auth.ts +469 -0
  17. package/src/cli/cmd/cmd.ts +2 -2
  18. package/src/cli/cmd/export.ts +41 -41
  19. package/src/cli/cmd/mcp.ts +144 -119
  20. package/src/cli/cmd/models.ts +30 -29
  21. package/src/cli/cmd/run.ts +269 -213
  22. package/src/cli/cmd/stats.ts +185 -146
  23. package/src/cli/error.ts +17 -13
  24. package/src/cli/ui.ts +39 -24
  25. package/src/command/index.ts +26 -26
  26. package/src/config/config.ts +528 -288
  27. package/src/config/markdown.ts +15 -15
  28. package/src/file/ripgrep.ts +201 -169
  29. package/src/file/time.ts +21 -18
  30. package/src/file/watcher.ts +51 -42
  31. package/src/file.ts +1 -1
  32. package/src/flag/flag.ts +26 -11
  33. package/src/format/formatter.ts +206 -162
  34. package/src/format/index.ts +61 -61
  35. package/src/global/index.ts +21 -21
  36. package/src/id/id.ts +47 -33
  37. package/src/index.js +346 -199
  38. package/src/json-standard/index.ts +67 -51
  39. package/src/mcp/index.ts +135 -128
  40. package/src/patch/index.ts +336 -267
  41. package/src/project/bootstrap.ts +15 -15
  42. package/src/project/instance.ts +43 -36
  43. package/src/project/project.ts +47 -47
  44. package/src/project/state.ts +37 -33
  45. package/src/provider/models-macro.ts +5 -5
  46. package/src/provider/models.ts +32 -32
  47. package/src/provider/opencode.js +19 -19
  48. package/src/provider/provider.ts +518 -277
  49. package/src/provider/transform.ts +143 -102
  50. package/src/server/project.ts +21 -21
  51. package/src/server/server.ts +111 -105
  52. package/src/session/agent.js +66 -60
  53. package/src/session/compaction.ts +136 -111
  54. package/src/session/index.ts +189 -156
  55. package/src/session/message-v2.ts +312 -268
  56. package/src/session/message.ts +73 -57
  57. package/src/session/processor.ts +180 -166
  58. package/src/session/prompt.ts +678 -533
  59. package/src/session/retry.ts +26 -23
  60. package/src/session/revert.ts +76 -62
  61. package/src/session/status.ts +26 -26
  62. package/src/session/summary.ts +97 -76
  63. package/src/session/system.ts +77 -63
  64. package/src/session/todo.ts +22 -16
  65. package/src/snapshot/index.ts +92 -76
  66. package/src/storage/storage.ts +157 -120
  67. package/src/tool/bash.ts +116 -106
  68. package/src/tool/batch.ts +73 -59
  69. package/src/tool/codesearch.ts +60 -53
  70. package/src/tool/edit.ts +319 -263
  71. package/src/tool/glob.ts +32 -28
  72. package/src/tool/grep.ts +72 -53
  73. package/src/tool/invalid.ts +7 -7
  74. package/src/tool/ls.ts +77 -64
  75. package/src/tool/multiedit.ts +30 -21
  76. package/src/tool/patch.ts +121 -94
  77. package/src/tool/read.ts +140 -122
  78. package/src/tool/registry.ts +38 -38
  79. package/src/tool/task.ts +93 -60
  80. package/src/tool/todo.ts +16 -16
  81. package/src/tool/tool.ts +45 -36
  82. package/src/tool/webfetch.ts +97 -74
  83. package/src/tool/websearch.ts +78 -64
  84. package/src/tool/write.ts +21 -15
  85. package/src/util/binary.ts +27 -19
  86. package/src/util/context.ts +8 -8
  87. package/src/util/defer.ts +7 -5
  88. package/src/util/error.ts +24 -19
  89. package/src/util/eventloop.ts +16 -10
  90. package/src/util/filesystem.ts +37 -33
  91. package/src/util/fn.ts +11 -8
  92. package/src/util/iife.ts +1 -1
  93. package/src/util/keybind.ts +44 -44
  94. package/src/util/lazy.ts +7 -7
  95. package/src/util/locale.ts +20 -16
  96. package/src/util/lock.ts +43 -38
  97. package/src/util/log.ts +95 -85
  98. package/src/util/queue.ts +8 -8
  99. package/src/util/rpc.ts +35 -23
  100. package/src/util/scrap.ts +4 -4
  101. package/src/util/signal.ts +5 -5
  102. package/src/util/timeout.ts +6 -6
  103. package/src/util/token.ts +2 -2
  104. package/src/util/wildcard.ts +38 -27
@@ -1,147 +1,166 @@
1
- import z from "zod"
2
- import * as path from "path"
3
- import * as fs from "fs/promises"
4
- import { Log } from "../util/log"
1
+ import z from 'zod';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs/promises';
4
+ import { Log } from '../util/log';
5
5
 
6
6
  export namespace Patch {
7
- const log = Log.create({ service: "patch" })
7
+ const log = Log.create({ service: 'patch' });
8
8
 
9
9
  // Schema definitions
10
10
  export const PatchSchema = z.object({
11
- patchText: z.string().describe("The full patch text that describes all changes to be made"),
12
- })
11
+ patchText: z
12
+ .string()
13
+ .describe('The full patch text that describes all changes to be made'),
14
+ });
13
15
 
14
- export type PatchParams = z.infer<typeof PatchSchema>
16
+ export type PatchParams = z.infer<typeof PatchSchema>;
15
17
 
16
18
  // Core types matching the Rust implementation
17
19
  export interface ApplyPatchArgs {
18
- patch: string
19
- hunks: Hunk[]
20
- workdir?: string
20
+ patch: string;
21
+ hunks: Hunk[];
22
+ workdir?: string;
21
23
  }
22
24
 
23
25
  export type Hunk =
24
- | { type: "add"; path: string; contents: string }
25
- | { type: "delete"; path: string }
26
- | { type: "update"; path: string; move_path?: string; chunks: UpdateFileChunk[] }
26
+ | { type: 'add'; path: string; contents: string }
27
+ | { type: 'delete'; path: string }
28
+ | {
29
+ type: 'update';
30
+ path: string;
31
+ move_path?: string;
32
+ chunks: UpdateFileChunk[];
33
+ };
27
34
 
28
35
  export interface UpdateFileChunk {
29
- old_lines: string[]
30
- new_lines: string[]
31
- change_context?: string
32
- is_end_of_file?: boolean
36
+ old_lines: string[];
37
+ new_lines: string[];
38
+ change_context?: string;
39
+ is_end_of_file?: boolean;
33
40
  }
34
41
 
35
42
  export interface ApplyPatchAction {
36
- changes: Map<string, ApplyPatchFileChange>
37
- patch: string
38
- cwd: string
43
+ changes: Map<string, ApplyPatchFileChange>;
44
+ patch: string;
45
+ cwd: string;
39
46
  }
40
47
 
41
48
  export type ApplyPatchFileChange =
42
- | { type: "add"; content: string }
43
- | { type: "delete"; content: string }
44
- | { type: "update"; unified_diff: string; move_path?: string; new_content: string }
49
+ | { type: 'add'; content: string }
50
+ | { type: 'delete'; content: string }
51
+ | {
52
+ type: 'update';
53
+ unified_diff: string;
54
+ move_path?: string;
55
+ new_content: string;
56
+ };
45
57
 
46
58
  export interface AffectedPaths {
47
- added: string[]
48
- modified: string[]
49
- deleted: string[]
59
+ added: string[];
60
+ modified: string[];
61
+ deleted: string[];
50
62
  }
51
63
 
52
64
  export enum ApplyPatchError {
53
- ParseError = "ParseError",
54
- IoError = "IoError",
55
- ComputeReplacements = "ComputeReplacements",
56
- ImplicitInvocation = "ImplicitInvocation",
65
+ ParseError = 'ParseError',
66
+ IoError = 'IoError',
67
+ ComputeReplacements = 'ComputeReplacements',
68
+ ImplicitInvocation = 'ImplicitInvocation',
57
69
  }
58
70
 
59
71
  export enum MaybeApplyPatch {
60
- Body = "Body",
61
- ShellParseError = "ShellParseError",
62
- PatchParseError = "PatchParseError",
63
- NotApplyPatch = "NotApplyPatch",
72
+ Body = 'Body',
73
+ ShellParseError = 'ShellParseError',
74
+ PatchParseError = 'PatchParseError',
75
+ NotApplyPatch = 'NotApplyPatch',
64
76
  }
65
77
 
66
78
  export enum MaybeApplyPatchVerified {
67
- Body = "Body",
68
- ShellParseError = "ShellParseError",
69
- CorrectnessError = "CorrectnessError",
70
- NotApplyPatch = "NotApplyPatch",
79
+ Body = 'Body',
80
+ ShellParseError = 'ShellParseError',
81
+ CorrectnessError = 'CorrectnessError',
82
+ NotApplyPatch = 'NotApplyPatch',
71
83
  }
72
84
 
73
85
  // Parser implementation
74
86
  function parsePatchHeader(
75
87
  lines: string[],
76
- startIdx: number,
88
+ startIdx: number
77
89
  ): { filePath: string; movePath?: string; nextIdx: number } | null {
78
- const line = lines[startIdx]
90
+ const line = lines[startIdx];
79
91
 
80
- if (line.startsWith("*** Add File:")) {
81
- const filePath = line.split(":", 2)[1]?.trim()
82
- return filePath ? { filePath, nextIdx: startIdx + 1 } : null
92
+ if (line.startsWith('*** Add File:')) {
93
+ const filePath = line.split(':', 2)[1]?.trim();
94
+ return filePath ? { filePath, nextIdx: startIdx + 1 } : null;
83
95
  }
84
96
 
85
- if (line.startsWith("*** Delete File:")) {
86
- const filePath = line.split(":", 2)[1]?.trim()
87
- return filePath ? { filePath, nextIdx: startIdx + 1 } : null
97
+ if (line.startsWith('*** Delete File:')) {
98
+ const filePath = line.split(':', 2)[1]?.trim();
99
+ return filePath ? { filePath, nextIdx: startIdx + 1 } : null;
88
100
  }
89
101
 
90
- if (line.startsWith("*** Update File:")) {
91
- const filePath = line.split(":", 2)[1]?.trim()
92
- let movePath: string | undefined
93
- let nextIdx = startIdx + 1
102
+ if (line.startsWith('*** Update File:')) {
103
+ const filePath = line.split(':', 2)[1]?.trim();
104
+ let movePath: string | undefined;
105
+ let nextIdx = startIdx + 1;
94
106
 
95
107
  // Check for move directive
96
- if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) {
97
- movePath = lines[nextIdx].split(":", 2)[1]?.trim()
98
- nextIdx++
108
+ if (nextIdx < lines.length && lines[nextIdx].startsWith('*** Move to:')) {
109
+ movePath = lines[nextIdx].split(':', 2)[1]?.trim();
110
+ nextIdx++;
99
111
  }
100
112
 
101
- return filePath ? { filePath, movePath, nextIdx } : null
113
+ return filePath ? { filePath, movePath, nextIdx } : null;
102
114
  }
103
115
 
104
- return null
116
+ return null;
105
117
  }
106
118
 
107
- function parseUpdateFileChunks(lines: string[], startIdx: number): { chunks: UpdateFileChunk[]; nextIdx: number } {
108
- const chunks: UpdateFileChunk[] = []
109
- let i = startIdx
119
+ function parseUpdateFileChunks(
120
+ lines: string[],
121
+ startIdx: number
122
+ ): { chunks: UpdateFileChunk[]; nextIdx: number } {
123
+ const chunks: UpdateFileChunk[] = [];
124
+ let i = startIdx;
110
125
 
111
- while (i < lines.length && !lines[i].startsWith("***")) {
112
- if (lines[i].startsWith("@@")) {
126
+ while (i < lines.length && !lines[i].startsWith('***')) {
127
+ if (lines[i].startsWith('@@')) {
113
128
  // Parse context line
114
- const contextLine = lines[i].substring(2).trim()
115
- i++
129
+ const contextLine = lines[i].substring(2).trim();
130
+ i++;
116
131
 
117
- const oldLines: string[] = []
118
- const newLines: string[] = []
119
- let isEndOfFile = false
132
+ const oldLines: string[] = [];
133
+ const newLines: string[] = [];
134
+ let isEndOfFile = false;
120
135
 
121
136
  // Parse change lines
122
- while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("***")) {
123
- const changeLine = lines[i]
124
-
125
- if (changeLine === "*** End of File") {
126
- isEndOfFile = true
127
- i++
128
- break
137
+ while (
138
+ i < lines.length &&
139
+ !lines[i].startsWith('@@') &&
140
+ !lines[i].startsWith('***')
141
+ ) {
142
+ const changeLine = lines[i];
143
+
144
+ if (changeLine === '*** End of File') {
145
+ isEndOfFile = true;
146
+ i++;
147
+ break;
129
148
  }
130
149
 
131
- if (changeLine.startsWith(" ")) {
150
+ if (changeLine.startsWith(' ')) {
132
151
  // Keep line - appears in both old and new
133
- const content = changeLine.substring(1)
134
- oldLines.push(content)
135
- newLines.push(content)
136
- } else if (changeLine.startsWith("-")) {
152
+ const content = changeLine.substring(1);
153
+ oldLines.push(content);
154
+ newLines.push(content);
155
+ } else if (changeLine.startsWith('-')) {
137
156
  // Remove line - only in old
138
- oldLines.push(changeLine.substring(1))
139
- } else if (changeLine.startsWith("+")) {
157
+ oldLines.push(changeLine.substring(1));
158
+ } else if (changeLine.startsWith('+')) {
140
159
  // Add line - only in new
141
- newLines.push(changeLine.substring(1))
160
+ newLines.push(changeLine.substring(1));
142
161
  }
143
162
 
144
- i++
163
+ i++;
145
164
  }
146
165
 
147
166
  chunks.push({
@@ -149,380 +168,419 @@ export namespace Patch {
149
168
  new_lines: newLines,
150
169
  change_context: contextLine || undefined,
151
170
  is_end_of_file: isEndOfFile || undefined,
152
- })
171
+ });
153
172
  } else {
154
- i++
173
+ i++;
155
174
  }
156
175
  }
157
176
 
158
- return { chunks, nextIdx: i }
177
+ return { chunks, nextIdx: i };
159
178
  }
160
179
 
161
- function parseAddFileContent(lines: string[], startIdx: number): { content: string; nextIdx: number } {
162
- let content = ""
163
- let i = startIdx
164
-
165
- while (i < lines.length && !lines[i].startsWith("***")) {
166
- if (lines[i].startsWith("+")) {
167
- content += lines[i].substring(1) + "\n"
180
+ function parseAddFileContent(
181
+ lines: string[],
182
+ startIdx: number
183
+ ): { content: string; nextIdx: number } {
184
+ let content = '';
185
+ let i = startIdx;
186
+
187
+ while (i < lines.length && !lines[i].startsWith('***')) {
188
+ if (lines[i].startsWith('+')) {
189
+ content += lines[i].substring(1) + '\n';
168
190
  }
169
- i++
191
+ i++;
170
192
  }
171
193
 
172
194
  // Remove trailing newline
173
- if (content.endsWith("\n")) {
174
- content = content.slice(0, -1)
195
+ if (content.endsWith('\n')) {
196
+ content = content.slice(0, -1);
175
197
  }
176
198
 
177
- return { content, nextIdx: i }
199
+ return { content, nextIdx: i };
178
200
  }
179
201
 
180
202
  export function parsePatch(patchText: string): { hunks: Hunk[] } {
181
- const lines = patchText.split("\n")
182
- const hunks: Hunk[] = []
183
- let i = 0
203
+ const lines = patchText.split('\n');
204
+ const hunks: Hunk[] = [];
205
+ let i = 0;
184
206
 
185
207
  // Look for Begin/End patch markers
186
- const beginMarker = "*** Begin Patch"
187
- const endMarker = "*** End Patch"
208
+ const beginMarker = '*** Begin Patch';
209
+ const endMarker = '*** End Patch';
188
210
 
189
- const beginIdx = lines.findIndex((line) => line.trim() === beginMarker)
190
- const endIdx = lines.findIndex((line) => line.trim() === endMarker)
211
+ const beginIdx = lines.findIndex((line) => line.trim() === beginMarker);
212
+ const endIdx = lines.findIndex((line) => line.trim() === endMarker);
191
213
 
192
214
  if (beginIdx === -1 || endIdx === -1 || beginIdx >= endIdx) {
193
- throw new Error("Invalid patch format: missing Begin/End markers")
215
+ throw new Error('Invalid patch format: missing Begin/End markers');
194
216
  }
195
217
 
196
218
  // Parse content between markers
197
- i = beginIdx + 1
219
+ i = beginIdx + 1;
198
220
 
199
221
  while (i < endIdx) {
200
- const header = parsePatchHeader(lines, i)
222
+ const header = parsePatchHeader(lines, i);
201
223
  if (!header) {
202
- i++
203
- continue
224
+ i++;
225
+ continue;
204
226
  }
205
227
 
206
- if (lines[i].startsWith("*** Add File:")) {
207
- const { content, nextIdx } = parseAddFileContent(lines, header.nextIdx)
228
+ if (lines[i].startsWith('*** Add File:')) {
229
+ const { content, nextIdx } = parseAddFileContent(lines, header.nextIdx);
208
230
  hunks.push({
209
- type: "add",
231
+ type: 'add',
210
232
  path: header.filePath,
211
233
  contents: content,
212
- })
213
- i = nextIdx
214
- } else if (lines[i].startsWith("*** Delete File:")) {
234
+ });
235
+ i = nextIdx;
236
+ } else if (lines[i].startsWith('*** Delete File:')) {
215
237
  hunks.push({
216
- type: "delete",
238
+ type: 'delete',
217
239
  path: header.filePath,
218
- })
219
- i = header.nextIdx
220
- } else if (lines[i].startsWith("*** Update File:")) {
221
- const { chunks, nextIdx } = parseUpdateFileChunks(lines, header.nextIdx)
240
+ });
241
+ i = header.nextIdx;
242
+ } else if (lines[i].startsWith('*** Update File:')) {
243
+ const { chunks, nextIdx } = parseUpdateFileChunks(
244
+ lines,
245
+ header.nextIdx
246
+ );
222
247
  hunks.push({
223
- type: "update",
248
+ type: 'update',
224
249
  path: header.filePath,
225
250
  move_path: header.movePath,
226
251
  chunks,
227
- })
228
- i = nextIdx
252
+ });
253
+ i = nextIdx;
229
254
  } else {
230
- i++
255
+ i++;
231
256
  }
232
257
  }
233
258
 
234
- return { hunks }
259
+ return { hunks };
235
260
  }
236
261
 
237
262
  // Apply patch functionality
238
263
  export function maybeParseApplyPatch(
239
- argv: string[],
264
+ argv: string[]
240
265
  ):
241
266
  | { type: MaybeApplyPatch.Body; args: ApplyPatchArgs }
242
267
  | { type: MaybeApplyPatch.PatchParseError; error: Error }
243
268
  | { type: MaybeApplyPatch.NotApplyPatch } {
244
- const APPLY_PATCH_COMMANDS = ["apply_patch", "applypatch"]
269
+ const APPLY_PATCH_COMMANDS = ['apply_patch', 'applypatch'];
245
270
 
246
271
  // Direct invocation: apply_patch <patch>
247
272
  if (argv.length === 2 && APPLY_PATCH_COMMANDS.includes(argv[0])) {
248
273
  try {
249
- const { hunks } = parsePatch(argv[1])
274
+ const { hunks } = parsePatch(argv[1]);
250
275
  return {
251
276
  type: MaybeApplyPatch.Body,
252
277
  args: {
253
278
  patch: argv[1],
254
279
  hunks,
255
280
  },
256
- }
281
+ };
257
282
  } catch (error) {
258
283
  return {
259
284
  type: MaybeApplyPatch.PatchParseError,
260
285
  error: error as Error,
261
- }
286
+ };
262
287
  }
263
288
  }
264
289
 
265
290
  // Bash heredoc form: bash -lc 'apply_patch <<"EOF" ...'
266
- if (argv.length === 3 && argv[0] === "bash" && argv[1] === "-lc") {
291
+ if (argv.length === 3 && argv[0] === 'bash' && argv[1] === '-lc') {
267
292
  // Simple extraction - in real implementation would need proper bash parsing
268
- const script = argv[2]
269
- const heredocMatch = script.match(/apply_patch\s*<<['"](\w+)['"]\s*\n([\s\S]*?)\n\1/)
293
+ const script = argv[2];
294
+ const heredocMatch = script.match(
295
+ /apply_patch\s*<<['"](\w+)['"]\s*\n([\s\S]*?)\n\1/
296
+ );
270
297
 
271
298
  if (heredocMatch) {
272
- const patchContent = heredocMatch[2]
299
+ const patchContent = heredocMatch[2];
273
300
  try {
274
- const { hunks } = parsePatch(patchContent)
301
+ const { hunks } = parsePatch(patchContent);
275
302
  return {
276
303
  type: MaybeApplyPatch.Body,
277
304
  args: {
278
305
  patch: patchContent,
279
306
  hunks,
280
307
  },
281
- }
308
+ };
282
309
  } catch (error) {
283
310
  return {
284
311
  type: MaybeApplyPatch.PatchParseError,
285
312
  error: error as Error,
286
- }
313
+ };
287
314
  }
288
315
  }
289
316
  }
290
317
 
291
- return { type: MaybeApplyPatch.NotApplyPatch }
318
+ return { type: MaybeApplyPatch.NotApplyPatch };
292
319
  }
293
320
 
294
321
  // File content manipulation
295
322
  interface ApplyPatchFileUpdate {
296
- unified_diff: string
297
- content: string
323
+ unified_diff: string;
324
+ content: string;
298
325
  }
299
326
 
300
- export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFileChunk[]): ApplyPatchFileUpdate {
327
+ export function deriveNewContentsFromChunks(
328
+ filePath: string,
329
+ chunks: UpdateFileChunk[]
330
+ ): ApplyPatchFileUpdate {
301
331
  // Read original file content
302
- let originalContent: string
332
+ let originalContent: string;
303
333
  try {
304
- originalContent = require("fs").readFileSync(filePath, "utf-8")
334
+ originalContent = require('fs').readFileSync(filePath, 'utf-8');
305
335
  } catch (error) {
306
- throw new Error(`Failed to read file ${filePath}: ${error}`)
336
+ throw new Error(`Failed to read file ${filePath}: ${error}`);
307
337
  }
308
338
 
309
- let originalLines = originalContent.split("\n")
339
+ let originalLines = originalContent.split('\n');
310
340
 
311
341
  // Drop trailing empty element for consistent line counting
312
- if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
313
- originalLines.pop()
342
+ if (
343
+ originalLines.length > 0 &&
344
+ originalLines[originalLines.length - 1] === ''
345
+ ) {
346
+ originalLines.pop();
314
347
  }
315
348
 
316
- const replacements = computeReplacements(originalLines, filePath, chunks)
317
- let newLines = applyReplacements(originalLines, replacements)
349
+ const replacements = computeReplacements(originalLines, filePath, chunks);
350
+ let newLines = applyReplacements(originalLines, replacements);
318
351
 
319
352
  // Ensure trailing newline
320
- if (newLines.length === 0 || newLines[newLines.length - 1] !== "") {
321
- newLines.push("")
353
+ if (newLines.length === 0 || newLines[newLines.length - 1] !== '') {
354
+ newLines.push('');
322
355
  }
323
356
 
324
- const newContent = newLines.join("\n")
357
+ const newContent = newLines.join('\n');
325
358
 
326
359
  // Generate unified diff
327
- const unifiedDiff = generateUnifiedDiff(originalContent, newContent)
360
+ const unifiedDiff = generateUnifiedDiff(originalContent, newContent);
328
361
 
329
362
  return {
330
363
  unified_diff: unifiedDiff,
331
364
  content: newContent,
332
- }
365
+ };
333
366
  }
334
367
 
335
368
  function computeReplacements(
336
369
  originalLines: string[],
337
370
  filePath: string,
338
- chunks: UpdateFileChunk[],
371
+ chunks: UpdateFileChunk[]
339
372
  ): Array<[number, number, string[]]> {
340
- const replacements: Array<[number, number, string[]]> = []
341
- let lineIndex = 0
373
+ const replacements: Array<[number, number, string[]]> = [];
374
+ let lineIndex = 0;
342
375
 
343
376
  for (const chunk of chunks) {
344
377
  // Handle context-based seeking
345
378
  if (chunk.change_context) {
346
- const contextIdx = seekSequence(originalLines, [chunk.change_context], lineIndex)
379
+ const contextIdx = seekSequence(
380
+ originalLines,
381
+ [chunk.change_context],
382
+ lineIndex
383
+ );
347
384
  if (contextIdx === -1) {
348
- throw new Error(`Failed to find context '${chunk.change_context}' in ${filePath}`)
385
+ throw new Error(
386
+ `Failed to find context '${chunk.change_context}' in ${filePath}`
387
+ );
349
388
  }
350
- lineIndex = contextIdx + 1
389
+ lineIndex = contextIdx + 1;
351
390
  }
352
391
 
353
392
  // Handle pure addition (no old lines)
354
393
  if (chunk.old_lines.length === 0) {
355
394
  const insertionIdx =
356
- originalLines.length > 0 && originalLines[originalLines.length - 1] === ""
395
+ originalLines.length > 0 &&
396
+ originalLines[originalLines.length - 1] === ''
357
397
  ? originalLines.length - 1
358
- : originalLines.length
359
- replacements.push([insertionIdx, 0, chunk.new_lines])
360
- continue
398
+ : originalLines.length;
399
+ replacements.push([insertionIdx, 0, chunk.new_lines]);
400
+ continue;
361
401
  }
362
402
 
363
403
  // Try to match old lines in the file
364
- let pattern = chunk.old_lines
365
- let newSlice = chunk.new_lines
366
- let found = seekSequence(originalLines, pattern, lineIndex)
404
+ let pattern = chunk.old_lines;
405
+ let newSlice = chunk.new_lines;
406
+ let found = seekSequence(originalLines, pattern, lineIndex);
367
407
 
368
408
  // Retry without trailing empty line if not found
369
- if (found === -1 && pattern.length > 0 && pattern[pattern.length - 1] === "") {
370
- pattern = pattern.slice(0, -1)
371
- if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") {
372
- newSlice = newSlice.slice(0, -1)
409
+ if (
410
+ found === -1 &&
411
+ pattern.length > 0 &&
412
+ pattern[pattern.length - 1] === ''
413
+ ) {
414
+ pattern = pattern.slice(0, -1);
415
+ if (newSlice.length > 0 && newSlice[newSlice.length - 1] === '') {
416
+ newSlice = newSlice.slice(0, -1);
373
417
  }
374
- found = seekSequence(originalLines, pattern, lineIndex)
418
+ found = seekSequence(originalLines, pattern, lineIndex);
375
419
  }
376
420
 
377
421
  if (found !== -1) {
378
- replacements.push([found, pattern.length, newSlice])
379
- lineIndex = found + pattern.length
422
+ replacements.push([found, pattern.length, newSlice]);
423
+ lineIndex = found + pattern.length;
380
424
  } else {
381
- throw new Error(`Failed to find expected lines in ${filePath}:\n${chunk.old_lines.join("\n")}`)
425
+ throw new Error(
426
+ `Failed to find expected lines in ${filePath}:\n${chunk.old_lines.join('\n')}`
427
+ );
382
428
  }
383
429
  }
384
430
 
385
431
  // Sort replacements by index to apply in order
386
- replacements.sort((a, b) => a[0] - b[0])
432
+ replacements.sort((a, b) => a[0] - b[0]);
387
433
 
388
- return replacements
434
+ return replacements;
389
435
  }
390
436
 
391
- function applyReplacements(lines: string[], replacements: Array<[number, number, string[]]>): string[] {
437
+ function applyReplacements(
438
+ lines: string[],
439
+ replacements: Array<[number, number, string[]]>
440
+ ): string[] {
392
441
  // Apply replacements in reverse order to avoid index shifting
393
- const result = [...lines]
442
+ const result = [...lines];
394
443
 
395
444
  for (let i = replacements.length - 1; i >= 0; i--) {
396
- const [startIdx, oldLen, newSegment] = replacements[i]
445
+ const [startIdx, oldLen, newSegment] = replacements[i];
397
446
 
398
447
  // Remove old lines
399
- result.splice(startIdx, oldLen)
448
+ result.splice(startIdx, oldLen);
400
449
 
401
450
  // Insert new lines
402
451
  for (let j = 0; j < newSegment.length; j++) {
403
- result.splice(startIdx + j, 0, newSegment[j])
452
+ result.splice(startIdx + j, 0, newSegment[j]);
404
453
  }
405
454
  }
406
455
 
407
- return result
456
+ return result;
408
457
  }
409
458
 
410
- function seekSequence(lines: string[], pattern: string[], startIndex: number): number {
411
- if (pattern.length === 0) return -1
459
+ function seekSequence(
460
+ lines: string[],
461
+ pattern: string[],
462
+ startIndex: number
463
+ ): number {
464
+ if (pattern.length === 0) return -1;
412
465
 
413
466
  // Simple substring search implementation
414
467
  for (let i = startIndex; i <= lines.length - pattern.length; i++) {
415
- let matches = true
468
+ let matches = true;
416
469
 
417
470
  for (let j = 0; j < pattern.length; j++) {
418
471
  if (lines[i + j] !== pattern[j]) {
419
- matches = false
420
- break
472
+ matches = false;
473
+ break;
421
474
  }
422
475
  }
423
476
 
424
477
  if (matches) {
425
- return i
478
+ return i;
426
479
  }
427
480
  }
428
481
 
429
- return -1
482
+ return -1;
430
483
  }
431
484
 
432
485
  function generateUnifiedDiff(oldContent: string, newContent: string): string {
433
- const oldLines = oldContent.split("\n")
434
- const newLines = newContent.split("\n")
486
+ const oldLines = oldContent.split('\n');
487
+ const newLines = newContent.split('\n');
435
488
 
436
489
  // Simple diff generation - in a real implementation you'd use a proper diff algorithm
437
- let diff = "@@ -1 +1 @@\n"
490
+ let diff = '@@ -1 +1 @@\n';
438
491
 
439
492
  // Find changes (simplified approach)
440
- const maxLen = Math.max(oldLines.length, newLines.length)
441
- let hasChanges = false
493
+ const maxLen = Math.max(oldLines.length, newLines.length);
494
+ let hasChanges = false;
442
495
 
443
496
  for (let i = 0; i < maxLen; i++) {
444
- const oldLine = oldLines[i] || ""
445
- const newLine = newLines[i] || ""
497
+ const oldLine = oldLines[i] || '';
498
+ const newLine = newLines[i] || '';
446
499
 
447
500
  if (oldLine !== newLine) {
448
- if (oldLine) diff += `-${oldLine}\n`
449
- if (newLine) diff += `+${newLine}\n`
450
- hasChanges = true
501
+ if (oldLine) diff += `-${oldLine}\n`;
502
+ if (newLine) diff += `+${newLine}\n`;
503
+ hasChanges = true;
451
504
  } else if (oldLine) {
452
- diff += ` ${oldLine}\n`
505
+ diff += ` ${oldLine}\n`;
453
506
  }
454
507
  }
455
508
 
456
- return hasChanges ? diff : ""
509
+ return hasChanges ? diff : '';
457
510
  }
458
511
 
459
512
  // Apply hunks to filesystem
460
- export async function applyHunksToFiles(hunks: Hunk[]): Promise<AffectedPaths> {
513
+ export async function applyHunksToFiles(
514
+ hunks: Hunk[]
515
+ ): Promise<AffectedPaths> {
461
516
  if (hunks.length === 0) {
462
- throw new Error("No files were modified.")
517
+ throw new Error('No files were modified.');
463
518
  }
464
519
 
465
- const added: string[] = []
466
- const modified: string[] = []
467
- const deleted: string[] = []
520
+ const added: string[] = [];
521
+ const modified: string[] = [];
522
+ const deleted: string[] = [];
468
523
 
469
524
  for (const hunk of hunks) {
470
525
  switch (hunk.type) {
471
- case "add":
526
+ case 'add':
472
527
  // Create parent directories
473
- const addDir = path.dirname(hunk.path)
474
- if (addDir !== "." && addDir !== "/") {
475
- await fs.mkdir(addDir, { recursive: true })
528
+ const addDir = path.dirname(hunk.path);
529
+ if (addDir !== '.' && addDir !== '/') {
530
+ await fs.mkdir(addDir, { recursive: true });
476
531
  }
477
532
 
478
- await fs.writeFile(hunk.path, hunk.contents, "utf-8")
479
- added.push(hunk.path)
480
- log.info(`Added file: ${hunk.path}`)
481
- break
533
+ await fs.writeFile(hunk.path, hunk.contents, 'utf-8');
534
+ added.push(hunk.path);
535
+ log.info(`Added file: ${hunk.path}`);
536
+ break;
482
537
 
483
- case "delete":
484
- await fs.unlink(hunk.path)
485
- deleted.push(hunk.path)
486
- log.info(`Deleted file: ${hunk.path}`)
487
- break
538
+ case 'delete':
539
+ await fs.unlink(hunk.path);
540
+ deleted.push(hunk.path);
541
+ log.info(`Deleted file: ${hunk.path}`);
542
+ break;
488
543
 
489
- case "update":
490
- const fileUpdate = deriveNewContentsFromChunks(hunk.path, hunk.chunks)
544
+ case 'update':
545
+ const fileUpdate = deriveNewContentsFromChunks(
546
+ hunk.path,
547
+ hunk.chunks
548
+ );
491
549
 
492
550
  if (hunk.move_path) {
493
551
  // Handle file move
494
- const moveDir = path.dirname(hunk.move_path)
495
- if (moveDir !== "." && moveDir !== "/") {
496
- await fs.mkdir(moveDir, { recursive: true })
552
+ const moveDir = path.dirname(hunk.move_path);
553
+ if (moveDir !== '.' && moveDir !== '/') {
554
+ await fs.mkdir(moveDir, { recursive: true });
497
555
  }
498
556
 
499
- await fs.writeFile(hunk.move_path, fileUpdate.content, "utf-8")
500
- await fs.unlink(hunk.path)
501
- modified.push(hunk.move_path)
502
- log.info(`Moved file: ${hunk.path} -> ${hunk.move_path}`)
557
+ await fs.writeFile(hunk.move_path, fileUpdate.content, 'utf-8');
558
+ await fs.unlink(hunk.path);
559
+ modified.push(hunk.move_path);
560
+ log.info(`Moved file: ${hunk.path} -> ${hunk.move_path}`);
503
561
  } else {
504
562
  // Regular update
505
- await fs.writeFile(hunk.path, fileUpdate.content, "utf-8")
506
- modified.push(hunk.path)
507
- log.info(`Updated file: ${hunk.path}`)
563
+ await fs.writeFile(hunk.path, fileUpdate.content, 'utf-8');
564
+ modified.push(hunk.path);
565
+ log.info(`Updated file: ${hunk.path}`);
508
566
  }
509
- break
567
+ break;
510
568
  }
511
569
  }
512
570
 
513
- return { added, modified, deleted }
571
+ return { added, modified, deleted };
514
572
  }
515
573
 
516
574
  // Main patch application function
517
575
  export async function applyPatch(patchText: string): Promise<AffectedPaths> {
518
- const { hunks } = parsePatch(patchText)
519
- return applyHunksToFiles(hunks)
576
+ const { hunks } = parsePatch(patchText);
577
+ return applyHunksToFiles(hunks);
520
578
  }
521
579
 
522
580
  // Async version of maybeParseApplyPatchVerified
523
581
  export async function maybeParseApplyPatchVerified(
524
582
  argv: string[],
525
- cwd: string,
583
+ cwd: string
526
584
  ): Promise<
527
585
  | { type: MaybeApplyPatchVerified.Body; action: ApplyPatchAction }
528
586
  | { type: MaybeApplyPatchVerified.CorrectnessError; error: Error }
@@ -531,72 +589,83 @@ export namespace Patch {
531
589
  // Detect implicit patch invocation (raw patch without apply_patch command)
532
590
  if (argv.length === 1) {
533
591
  try {
534
- parsePatch(argv[0])
592
+ parsePatch(argv[0]);
535
593
  return {
536
594
  type: MaybeApplyPatchVerified.CorrectnessError,
537
595
  error: new Error(ApplyPatchError.ImplicitInvocation),
538
- }
596
+ };
539
597
  } catch {
540
598
  // Not a patch, continue
541
599
  }
542
600
  }
543
601
 
544
- const result = maybeParseApplyPatch(argv)
602
+ const result = maybeParseApplyPatch(argv);
545
603
 
546
604
  switch (result.type) {
547
605
  case MaybeApplyPatch.Body:
548
- const { args } = result
549
- const effectiveCwd = args.workdir ? path.resolve(cwd, args.workdir) : cwd
550
- const changes = new Map<string, ApplyPatchFileChange>()
606
+ const { args } = result;
607
+ const effectiveCwd = args.workdir
608
+ ? path.resolve(cwd, args.workdir)
609
+ : cwd;
610
+ const changes = new Map<string, ApplyPatchFileChange>();
551
611
 
552
612
  for (const hunk of args.hunks) {
553
613
  const resolvedPath = path.resolve(
554
614
  effectiveCwd,
555
- hunk.type === "update" && hunk.move_path ? hunk.move_path : hunk.path,
556
- )
615
+ hunk.type === 'update' && hunk.move_path
616
+ ? hunk.move_path
617
+ : hunk.path
618
+ );
557
619
 
558
620
  switch (hunk.type) {
559
- case "add":
621
+ case 'add':
560
622
  changes.set(resolvedPath, {
561
- type: "add",
623
+ type: 'add',
562
624
  content: hunk.contents,
563
- })
564
- break
625
+ });
626
+ break;
565
627
 
566
- case "delete":
628
+ case 'delete':
567
629
  // For delete, we need to read the current content
568
- const deletePath = path.resolve(effectiveCwd, hunk.path)
630
+ const deletePath = path.resolve(effectiveCwd, hunk.path);
569
631
  try {
570
- const content = await fs.readFile(deletePath, "utf-8")
632
+ const content = await fs.readFile(deletePath, 'utf-8');
571
633
  changes.set(resolvedPath, {
572
- type: "delete",
634
+ type: 'delete',
573
635
  content,
574
- })
636
+ });
575
637
  } catch (error) {
576
638
  return {
577
639
  type: MaybeApplyPatchVerified.CorrectnessError,
578
- error: new Error(`Failed to read file for deletion: ${deletePath}`),
579
- }
640
+ error: new Error(
641
+ `Failed to read file for deletion: ${deletePath}`
642
+ ),
643
+ };
580
644
  }
581
- break
645
+ break;
582
646
 
583
- case "update":
584
- const updatePath = path.resolve(effectiveCwd, hunk.path)
647
+ case 'update':
648
+ const updatePath = path.resolve(effectiveCwd, hunk.path);
585
649
  try {
586
- const fileUpdate = deriveNewContentsFromChunks(updatePath, hunk.chunks)
650
+ const fileUpdate = deriveNewContentsFromChunks(
651
+ updatePath,
652
+ hunk.chunks
653
+ );
587
654
  changes.set(resolvedPath, {
588
- type: "update",
655
+ type: 'update',
589
656
  unified_diff: fileUpdate.unified_diff,
590
- move_path: hunk.move_path ? path.resolve(effectiveCwd, hunk.move_path) : undefined,
657
+ move_path: hunk.move_path
658
+ ? path.resolve(effectiveCwd, hunk.move_path)
659
+ : undefined,
591
660
  new_content: fileUpdate.content,
592
- })
661
+ });
593
662
  } catch (error) {
594
663
  return {
595
664
  type: MaybeApplyPatchVerified.CorrectnessError,
596
665
  error: error as Error,
597
- }
666
+ };
598
667
  }
599
- break
668
+ break;
600
669
  }
601
670
  }
602
671
 
@@ -607,16 +676,16 @@ export namespace Patch {
607
676
  patch: args.patch,
608
677
  cwd: effectiveCwd,
609
678
  },
610
- }
679
+ };
611
680
 
612
681
  case MaybeApplyPatch.PatchParseError:
613
682
  return {
614
683
  type: MaybeApplyPatchVerified.CorrectnessError,
615
684
  error: result.error,
616
- }
685
+ };
617
686
 
618
687
  case MaybeApplyPatch.NotApplyPatch:
619
- return { type: MaybeApplyPatchVerified.NotApplyPatch }
688
+ return { type: MaybeApplyPatchVerified.NotApplyPatch };
620
689
  }
621
690
  }
622
691
  }