@link-assistant/agent 0.0.8 → 0.0.11

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 +80 -1
  2. package/MODELS.md +72 -24
  3. package/README.md +95 -2
  4. package/TOOLS.md +20 -0
  5. package/package.json +36 -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 +468 -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 +210 -53
  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 +78 -0
  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 +554 -332
  38. package/src/json-standard/index.ts +173 -0
  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
package/src/tool/edit.ts CHANGED
@@ -3,81 +3,108 @@
3
3
  // https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
4
4
  // https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
5
5
 
6
- import z from "zod"
7
- import * as path from "path"
8
- import { Tool } from "./tool"
9
- import { createTwoFilesPatch, diffLines } from "diff"
10
- import DESCRIPTION from "./edit.txt"
11
- import { File } from "../file"
12
- import { Bus } from "../bus"
13
- import { FileTime } from "../file/time"
14
- import { Instance } from "../project/instance"
15
- import { Snapshot } from "../snapshot"
6
+ import z from 'zod';
7
+ import * as path from 'path';
8
+ import { Tool } from './tool';
9
+ import { createTwoFilesPatch, diffLines } from 'diff';
10
+ import DESCRIPTION from './edit.txt';
11
+ import { File } from '../file';
12
+ import { Bus } from '../bus';
13
+ import { FileTime } from '../file/time';
14
+ import { Instance } from '../project/instance';
15
+ import { Snapshot } from '../snapshot';
16
16
 
17
17
  function normalizeLineEndings(text: string): string {
18
- return text.replaceAll("\r\n", "\n")
18
+ return text.replaceAll('\r\n', '\n');
19
19
  }
20
20
 
21
- export const EditTool = Tool.define("edit", {
21
+ export const EditTool = Tool.define('edit', {
22
22
  description: DESCRIPTION,
23
23
  parameters: z.object({
24
- filePath: z.string().describe("The absolute path to the file to modify"),
25
- oldString: z.string().describe("The text to replace"),
26
- newString: z.string().describe("The text to replace it with (must be different from oldString)"),
27
- replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
24
+ filePath: z.string().describe('The absolute path to the file to modify'),
25
+ oldString: z.string().describe('The text to replace'),
26
+ newString: z
27
+ .string()
28
+ .describe(
29
+ 'The text to replace it with (must be different from oldString)'
30
+ ),
31
+ replaceAll: z
32
+ .boolean()
33
+ .optional()
34
+ .describe('Replace all occurrences of oldString (default false)'),
28
35
  }),
29
36
  async execute(params, ctx) {
30
37
  if (!params.filePath) {
31
- throw new Error("filePath is required")
38
+ throw new Error('filePath is required');
32
39
  }
33
40
 
34
41
  if (params.oldString === params.newString) {
35
- throw new Error("oldString and newString must be different")
42
+ throw new Error('oldString and newString must be different');
36
43
  }
37
44
 
38
45
  // No restrictions - unrestricted file editing
39
- const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
46
+ const filePath = path.isAbsolute(params.filePath)
47
+ ? params.filePath
48
+ : path.join(Instance.directory, params.filePath);
40
49
 
41
- let diff = ""
42
- let contentOld = ""
43
- let contentNew = ""
50
+ let diff = '';
51
+ let contentOld = '';
52
+ let contentNew = '';
44
53
  await (async () => {
45
- if (params.oldString === "") {
46
- contentNew = params.newString
47
- diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
48
- await Bun.write(filePath, params.newString)
54
+ if (params.oldString === '') {
55
+ contentNew = params.newString;
56
+ diff = trimDiff(
57
+ createTwoFilesPatch(filePath, filePath, contentOld, contentNew)
58
+ );
59
+ await Bun.write(filePath, params.newString);
49
60
  await Bus.publish(File.Event.Edited, {
50
61
  file: filePath,
51
- })
52
- return
62
+ });
63
+ return;
53
64
  }
54
65
 
55
- const file = Bun.file(filePath)
56
- const stats = await file.stat().catch(() => {})
57
- if (!stats) throw new Error(`File ${filePath} not found`)
58
- if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
59
- await FileTime.assert(ctx.sessionID, filePath)
60
- contentOld = await file.text()
61
- contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
66
+ const file = Bun.file(filePath);
67
+ const stats = await file.stat().catch(() => {});
68
+ if (!stats) throw new Error(`File ${filePath} not found`);
69
+ if (stats.isDirectory())
70
+ throw new Error(`Path is a directory, not a file: ${filePath}`);
71
+ await FileTime.assert(ctx.sessionID, filePath);
72
+ contentOld = await file.text();
73
+ contentNew = replace(
74
+ contentOld,
75
+ params.oldString,
76
+ params.newString,
77
+ params.replaceAll
78
+ );
62
79
 
63
80
  diff = trimDiff(
64
- createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)),
65
- )
66
-
67
- await file.write(contentNew)
81
+ createTwoFilesPatch(
82
+ filePath,
83
+ filePath,
84
+ normalizeLineEndings(contentOld),
85
+ normalizeLineEndings(contentNew)
86
+ )
87
+ );
88
+
89
+ await file.write(contentNew);
68
90
  await Bus.publish(File.Event.Edited, {
69
91
  file: filePath,
70
- })
71
- contentNew = await file.text()
92
+ });
93
+ contentNew = await file.text();
72
94
  diff = trimDiff(
73
- createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)),
74
- )
75
- })()
95
+ createTwoFilesPatch(
96
+ filePath,
97
+ filePath,
98
+ normalizeLineEndings(contentOld),
99
+ normalizeLineEndings(contentNew)
100
+ )
101
+ );
102
+ })();
76
103
 
77
- FileTime.read(ctx.sessionID, filePath)
104
+ FileTime.read(ctx.sessionID, filePath);
78
105
 
79
- let output = ""
80
- const diagnostics = {}
106
+ let output = '';
107
+ const diagnostics = {};
81
108
 
82
109
  const filediff: Snapshot.FileDiff = {
83
110
  file: filePath,
@@ -85,10 +112,10 @@ export const EditTool = Tool.define("edit", {
85
112
  after: contentNew,
86
113
  additions: 0,
87
114
  deletions: 0,
88
- }
115
+ };
89
116
  for (const change of diffLines(contentOld, contentNew)) {
90
- if (change.added) filediff.additions += change.count || 0
91
- if (change.removed) filediff.deletions += change.count || 0
117
+ if (change.added) filediff.additions += change.count || 0;
118
+ if (change.removed) filediff.deletions += change.count || 0;
92
119
  }
93
120
 
94
121
  return {
@@ -99,239 +126,254 @@ export const EditTool = Tool.define("edit", {
99
126
  },
100
127
  title: `${path.relative(Instance.worktree, filePath)}`,
101
128
  output,
102
- }
129
+ };
103
130
  },
104
- })
131
+ });
105
132
 
106
- export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
133
+ export type Replacer = (
134
+ content: string,
135
+ find: string
136
+ ) => Generator<string, void, unknown>;
107
137
 
108
138
  // Similarity thresholds for block anchor fallback matching
109
- const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0
110
- const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3
139
+ const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0;
140
+ const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3;
111
141
 
112
142
  /**
113
143
  * Levenshtein distance algorithm implementation
114
144
  */
115
145
  function levenshtein(a: string, b: string): number {
116
146
  // Handle empty strings
117
- if (a === "" || b === "") {
118
- return Math.max(a.length, b.length)
147
+ if (a === '' || b === '') {
148
+ return Math.max(a.length, b.length);
119
149
  }
120
150
  const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
121
- Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
122
- )
151
+ Array.from({ length: b.length + 1 }, (_, j) =>
152
+ i === 0 ? j : j === 0 ? i : 0
153
+ )
154
+ );
123
155
 
124
156
  for (let i = 1; i <= a.length; i++) {
125
157
  for (let j = 1; j <= b.length; j++) {
126
- const cost = a[i - 1] === b[j - 1] ? 0 : 1
127
- matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost)
158
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
159
+ matrix[i][j] = Math.min(
160
+ matrix[i - 1][j] + 1,
161
+ matrix[i][j - 1] + 1,
162
+ matrix[i - 1][j - 1] + cost
163
+ );
128
164
  }
129
165
  }
130
- return matrix[a.length][b.length]
166
+ return matrix[a.length][b.length];
131
167
  }
132
168
 
133
169
  export const SimpleReplacer: Replacer = function* (_content, find) {
134
- yield find
135
- }
170
+ yield find;
171
+ };
136
172
 
137
173
  export const LineTrimmedReplacer: Replacer = function* (content, find) {
138
- const originalLines = content.split("\n")
139
- const searchLines = find.split("\n")
174
+ const originalLines = content.split('\n');
175
+ const searchLines = find.split('\n');
140
176
 
141
- if (searchLines[searchLines.length - 1] === "") {
142
- searchLines.pop()
177
+ if (searchLines[searchLines.length - 1] === '') {
178
+ searchLines.pop();
143
179
  }
144
180
 
145
181
  for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
146
- let matches = true
182
+ let matches = true;
147
183
 
148
184
  for (let j = 0; j < searchLines.length; j++) {
149
- const originalTrimmed = originalLines[i + j].trim()
150
- const searchTrimmed = searchLines[j].trim()
185
+ const originalTrimmed = originalLines[i + j].trim();
186
+ const searchTrimmed = searchLines[j].trim();
151
187
 
152
188
  if (originalTrimmed !== searchTrimmed) {
153
- matches = false
154
- break
189
+ matches = false;
190
+ break;
155
191
  }
156
192
  }
157
193
 
158
194
  if (matches) {
159
- let matchStartIndex = 0
195
+ let matchStartIndex = 0;
160
196
  for (let k = 0; k < i; k++) {
161
- matchStartIndex += originalLines[k].length + 1
197
+ matchStartIndex += originalLines[k].length + 1;
162
198
  }
163
199
 
164
- let matchEndIndex = matchStartIndex
200
+ let matchEndIndex = matchStartIndex;
165
201
  for (let k = 0; k < searchLines.length; k++) {
166
- matchEndIndex += originalLines[i + k].length
202
+ matchEndIndex += originalLines[i + k].length;
167
203
  if (k < searchLines.length - 1) {
168
- matchEndIndex += 1 // Add newline character except for the last line
204
+ matchEndIndex += 1; // Add newline character except for the last line
169
205
  }
170
206
  }
171
207
 
172
- yield content.substring(matchStartIndex, matchEndIndex)
208
+ yield content.substring(matchStartIndex, matchEndIndex);
173
209
  }
174
210
  }
175
- }
211
+ };
176
212
 
177
213
  export const BlockAnchorReplacer: Replacer = function* (content, find) {
178
- const originalLines = content.split("\n")
179
- const searchLines = find.split("\n")
214
+ const originalLines = content.split('\n');
215
+ const searchLines = find.split('\n');
180
216
 
181
217
  if (searchLines.length < 3) {
182
- return
218
+ return;
183
219
  }
184
220
 
185
- if (searchLines[searchLines.length - 1] === "") {
186
- searchLines.pop()
221
+ if (searchLines[searchLines.length - 1] === '') {
222
+ searchLines.pop();
187
223
  }
188
224
 
189
- const firstLineSearch = searchLines[0].trim()
190
- const lastLineSearch = searchLines[searchLines.length - 1].trim()
191
- const searchBlockSize = searchLines.length
225
+ const firstLineSearch = searchLines[0].trim();
226
+ const lastLineSearch = searchLines[searchLines.length - 1].trim();
227
+ const searchBlockSize = searchLines.length;
192
228
 
193
229
  // Collect all candidate positions where both anchors match
194
- const candidates: Array<{ startLine: number; endLine: number }> = []
230
+ const candidates: Array<{ startLine: number; endLine: number }> = [];
195
231
  for (let i = 0; i < originalLines.length; i++) {
196
232
  if (originalLines[i].trim() !== firstLineSearch) {
197
- continue
233
+ continue;
198
234
  }
199
235
 
200
236
  // Look for the matching last line after this first line
201
237
  for (let j = i + 2; j < originalLines.length; j++) {
202
238
  if (originalLines[j].trim() === lastLineSearch) {
203
- candidates.push({ startLine: i, endLine: j })
204
- break // Only match the first occurrence of the last line
239
+ candidates.push({ startLine: i, endLine: j });
240
+ break; // Only match the first occurrence of the last line
205
241
  }
206
242
  }
207
243
  }
208
244
 
209
245
  // Return immediately if no candidates
210
246
  if (candidates.length === 0) {
211
- return
247
+ return;
212
248
  }
213
249
 
214
250
  // Handle single candidate scenario (using relaxed threshold)
215
251
  if (candidates.length === 1) {
216
- const { startLine, endLine } = candidates[0]
217
- const actualBlockSize = endLine - startLine + 1
252
+ const { startLine, endLine } = candidates[0];
253
+ const actualBlockSize = endLine - startLine + 1;
218
254
 
219
- let similarity = 0
220
- let linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2) // Middle lines only
255
+ let similarity = 0;
256
+ let linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2); // Middle lines only
221
257
 
222
258
  if (linesToCheck > 0) {
223
259
  for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
224
- const originalLine = originalLines[startLine + j].trim()
225
- const searchLine = searchLines[j].trim()
226
- const maxLen = Math.max(originalLine.length, searchLine.length)
260
+ const originalLine = originalLines[startLine + j].trim();
261
+ const searchLine = searchLines[j].trim();
262
+ const maxLen = Math.max(originalLine.length, searchLine.length);
227
263
  if (maxLen === 0) {
228
- continue
264
+ continue;
229
265
  }
230
- const distance = levenshtein(originalLine, searchLine)
231
- similarity += (1 - distance / maxLen) / linesToCheck
266
+ const distance = levenshtein(originalLine, searchLine);
267
+ similarity += (1 - distance / maxLen) / linesToCheck;
232
268
 
233
269
  // Exit early when threshold is reached
234
270
  if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
235
- break
271
+ break;
236
272
  }
237
273
  }
238
274
  } else {
239
275
  // No middle lines to compare, just accept based on anchors
240
- similarity = 1.0
276
+ similarity = 1.0;
241
277
  }
242
278
 
243
279
  if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
244
- let matchStartIndex = 0
280
+ let matchStartIndex = 0;
245
281
  for (let k = 0; k < startLine; k++) {
246
- matchStartIndex += originalLines[k].length + 1
282
+ matchStartIndex += originalLines[k].length + 1;
247
283
  }
248
- let matchEndIndex = matchStartIndex
284
+ let matchEndIndex = matchStartIndex;
249
285
  for (let k = startLine; k <= endLine; k++) {
250
- matchEndIndex += originalLines[k].length
286
+ matchEndIndex += originalLines[k].length;
251
287
  if (k < endLine) {
252
- matchEndIndex += 1 // Add newline character except for the last line
288
+ matchEndIndex += 1; // Add newline character except for the last line
253
289
  }
254
290
  }
255
- yield content.substring(matchStartIndex, matchEndIndex)
291
+ yield content.substring(matchStartIndex, matchEndIndex);
256
292
  }
257
- return
293
+ return;
258
294
  }
259
295
 
260
296
  // Calculate similarity for multiple candidates
261
- let bestMatch: { startLine: number; endLine: number } | null = null
262
- let maxSimilarity = -1
297
+ let bestMatch: { startLine: number; endLine: number } | null = null;
298
+ let maxSimilarity = -1;
263
299
 
264
300
  for (const candidate of candidates) {
265
- const { startLine, endLine } = candidate
266
- const actualBlockSize = endLine - startLine + 1
301
+ const { startLine, endLine } = candidate;
302
+ const actualBlockSize = endLine - startLine + 1;
267
303
 
268
- let similarity = 0
269
- let linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2) // Middle lines only
304
+ let similarity = 0;
305
+ let linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2); // Middle lines only
270
306
 
271
307
  if (linesToCheck > 0) {
272
308
  for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
273
- const originalLine = originalLines[startLine + j].trim()
274
- const searchLine = searchLines[j].trim()
275
- const maxLen = Math.max(originalLine.length, searchLine.length)
309
+ const originalLine = originalLines[startLine + j].trim();
310
+ const searchLine = searchLines[j].trim();
311
+ const maxLen = Math.max(originalLine.length, searchLine.length);
276
312
  if (maxLen === 0) {
277
- continue
313
+ continue;
278
314
  }
279
- const distance = levenshtein(originalLine, searchLine)
280
- similarity += 1 - distance / maxLen
315
+ const distance = levenshtein(originalLine, searchLine);
316
+ similarity += 1 - distance / maxLen;
281
317
  }
282
- similarity /= linesToCheck // Average similarity
318
+ similarity /= linesToCheck; // Average similarity
283
319
  } else {
284
320
  // No middle lines to compare, just accept based on anchors
285
- similarity = 1.0
321
+ similarity = 1.0;
286
322
  }
287
323
 
288
324
  if (similarity > maxSimilarity) {
289
- maxSimilarity = similarity
290
- bestMatch = candidate
325
+ maxSimilarity = similarity;
326
+ bestMatch = candidate;
291
327
  }
292
328
  }
293
329
 
294
330
  // Threshold judgment
295
331
  if (maxSimilarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD && bestMatch) {
296
- const { startLine, endLine } = bestMatch
297
- let matchStartIndex = 0
332
+ const { startLine, endLine } = bestMatch;
333
+ let matchStartIndex = 0;
298
334
  for (let k = 0; k < startLine; k++) {
299
- matchStartIndex += originalLines[k].length + 1
335
+ matchStartIndex += originalLines[k].length + 1;
300
336
  }
301
- let matchEndIndex = matchStartIndex
337
+ let matchEndIndex = matchStartIndex;
302
338
  for (let k = startLine; k <= endLine; k++) {
303
- matchEndIndex += originalLines[k].length
339
+ matchEndIndex += originalLines[k].length;
304
340
  if (k < endLine) {
305
- matchEndIndex += 1
341
+ matchEndIndex += 1;
306
342
  }
307
343
  }
308
- yield content.substring(matchStartIndex, matchEndIndex)
344
+ yield content.substring(matchStartIndex, matchEndIndex);
309
345
  }
310
- }
346
+ };
311
347
 
312
- export const WhitespaceNormalizedReplacer: Replacer = function* (content, find) {
313
- const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
314
- const normalizedFind = normalizeWhitespace(find)
348
+ export const WhitespaceNormalizedReplacer: Replacer = function* (
349
+ content,
350
+ find
351
+ ) {
352
+ const normalizeWhitespace = (text: string) =>
353
+ text.replace(/\s+/g, ' ').trim();
354
+ const normalizedFind = normalizeWhitespace(find);
315
355
 
316
356
  // Handle single line matches
317
- const lines = content.split("\n")
357
+ const lines = content.split('\n');
318
358
  for (let i = 0; i < lines.length; i++) {
319
- const line = lines[i]
359
+ const line = lines[i];
320
360
  if (normalizeWhitespace(line) === normalizedFind) {
321
- yield line
361
+ yield line;
322
362
  } else {
323
363
  // Only check for substring matches if the full line doesn't match
324
- const normalizedLine = normalizeWhitespace(line)
364
+ const normalizedLine = normalizeWhitespace(line);
325
365
  if (normalizedLine.includes(normalizedFind)) {
326
366
  // Find the actual substring in the original line that matches
327
- const words = find.trim().split(/\s+/)
367
+ const words = find.trim().split(/\s+/);
328
368
  if (words.length > 0) {
329
- const pattern = words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+")
369
+ const pattern = words
370
+ .map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
371
+ .join('\\s+');
330
372
  try {
331
- const regex = new RegExp(pattern)
332
- const match = line.match(regex)
373
+ const regex = new RegExp(pattern);
374
+ const match = line.match(regex);
333
375
  if (match) {
334
- yield match[0]
376
+ yield match[0];
335
377
  }
336
378
  } catch (e) {
337
379
  // Invalid regex pattern, skip
@@ -342,234 +384,244 @@ export const WhitespaceNormalizedReplacer: Replacer = function* (content, find)
342
384
  }
343
385
 
344
386
  // Handle multi-line matches
345
- const findLines = find.split("\n")
387
+ const findLines = find.split('\n');
346
388
  if (findLines.length > 1) {
347
389
  for (let i = 0; i <= lines.length - findLines.length; i++) {
348
- const block = lines.slice(i, i + findLines.length)
349
- if (normalizeWhitespace(block.join("\n")) === normalizedFind) {
350
- yield block.join("\n")
390
+ const block = lines.slice(i, i + findLines.length);
391
+ if (normalizeWhitespace(block.join('\n')) === normalizedFind) {
392
+ yield block.join('\n');
351
393
  }
352
394
  }
353
395
  }
354
- }
396
+ };
355
397
 
356
398
  export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
357
399
  const removeIndentation = (text: string) => {
358
- const lines = text.split("\n")
359
- const nonEmptyLines = lines.filter((line) => line.trim().length > 0)
360
- if (nonEmptyLines.length === 0) return text
400
+ const lines = text.split('\n');
401
+ const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
402
+ if (nonEmptyLines.length === 0) return text;
361
403
 
362
404
  const minIndent = Math.min(
363
405
  ...nonEmptyLines.map((line) => {
364
- const match = line.match(/^(\s*)/)
365
- return match ? match[1].length : 0
366
- }),
367
- )
406
+ const match = line.match(/^(\s*)/);
407
+ return match ? match[1].length : 0;
408
+ })
409
+ );
368
410
 
369
- return lines.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent))).join("\n")
370
- }
411
+ return lines
412
+ .map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
413
+ .join('\n');
414
+ };
371
415
 
372
- const normalizedFind = removeIndentation(find)
373
- const contentLines = content.split("\n")
374
- const findLines = find.split("\n")
416
+ const normalizedFind = removeIndentation(find);
417
+ const contentLines = content.split('\n');
418
+ const findLines = find.split('\n');
375
419
 
376
420
  for (let i = 0; i <= contentLines.length - findLines.length; i++) {
377
- const block = contentLines.slice(i, i + findLines.length).join("\n")
421
+ const block = contentLines.slice(i, i + findLines.length).join('\n');
378
422
  if (removeIndentation(block) === normalizedFind) {
379
- yield block
423
+ yield block;
380
424
  }
381
425
  }
382
- }
426
+ };
383
427
 
384
428
  export const EscapeNormalizedReplacer: Replacer = function* (content, find) {
385
429
  const unescapeString = (str: string): string => {
386
430
  return str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, capturedChar) => {
387
431
  switch (capturedChar) {
388
- case "n":
389
- return "\n"
390
- case "t":
391
- return "\t"
392
- case "r":
393
- return "\r"
432
+ case 'n':
433
+ return '\n';
434
+ case 't':
435
+ return '\t';
436
+ case 'r':
437
+ return '\r';
394
438
  case "'":
395
- return "'"
439
+ return "'";
396
440
  case '"':
397
- return '"'
398
- case "`":
399
- return "`"
400
- case "\\":
401
- return "\\"
402
- case "\n":
403
- return "\n"
404
- case "$":
405
- return "$"
441
+ return '"';
442
+ case '`':
443
+ return '`';
444
+ case '\\':
445
+ return '\\';
446
+ case '\n':
447
+ return '\n';
448
+ case '$':
449
+ return '$';
406
450
  default:
407
- return match
451
+ return match;
408
452
  }
409
- })
410
- }
453
+ });
454
+ };
411
455
 
412
- const unescapedFind = unescapeString(find)
456
+ const unescapedFind = unescapeString(find);
413
457
 
414
458
  // Try direct match with unescaped find string
415
459
  if (content.includes(unescapedFind)) {
416
- yield unescapedFind
460
+ yield unescapedFind;
417
461
  }
418
462
 
419
463
  // Also try finding escaped versions in content that match unescaped find
420
- const lines = content.split("\n")
421
- const findLines = unescapedFind.split("\n")
464
+ const lines = content.split('\n');
465
+ const findLines = unescapedFind.split('\n');
422
466
 
423
467
  for (let i = 0; i <= lines.length - findLines.length; i++) {
424
- const block = lines.slice(i, i + findLines.length).join("\n")
425
- const unescapedBlock = unescapeString(block)
468
+ const block = lines.slice(i, i + findLines.length).join('\n');
469
+ const unescapedBlock = unescapeString(block);
426
470
 
427
471
  if (unescapedBlock === unescapedFind) {
428
- yield block
472
+ yield block;
429
473
  }
430
474
  }
431
- }
475
+ };
432
476
 
433
477
  export const MultiOccurrenceReplacer: Replacer = function* (content, find) {
434
478
  // This replacer yields all exact matches, allowing the replace function
435
479
  // to handle multiple occurrences based on replaceAll parameter
436
- let startIndex = 0
480
+ let startIndex = 0;
437
481
 
438
482
  while (true) {
439
- const index = content.indexOf(find, startIndex)
440
- if (index === -1) break
483
+ const index = content.indexOf(find, startIndex);
484
+ if (index === -1) break;
441
485
 
442
- yield find
443
- startIndex = index + find.length
486
+ yield find;
487
+ startIndex = index + find.length;
444
488
  }
445
- }
489
+ };
446
490
 
447
491
  export const TrimmedBoundaryReplacer: Replacer = function* (content, find) {
448
- const trimmedFind = find.trim()
492
+ const trimmedFind = find.trim();
449
493
 
450
494
  if (trimmedFind === find) {
451
495
  // Already trimmed, no point in trying
452
- return
496
+ return;
453
497
  }
454
498
 
455
499
  // Try to find the trimmed version
456
500
  if (content.includes(trimmedFind)) {
457
- yield trimmedFind
501
+ yield trimmedFind;
458
502
  }
459
503
 
460
504
  // Also try finding blocks where trimmed content matches
461
- const lines = content.split("\n")
462
- const findLines = find.split("\n")
505
+ const lines = content.split('\n');
506
+ const findLines = find.split('\n');
463
507
 
464
508
  for (let i = 0; i <= lines.length - findLines.length; i++) {
465
- const block = lines.slice(i, i + findLines.length).join("\n")
509
+ const block = lines.slice(i, i + findLines.length).join('\n');
466
510
 
467
511
  if (block.trim() === trimmedFind) {
468
- yield block
512
+ yield block;
469
513
  }
470
514
  }
471
- }
515
+ };
472
516
 
473
517
  export const ContextAwareReplacer: Replacer = function* (content, find) {
474
- const findLines = find.split("\n")
518
+ const findLines = find.split('\n');
475
519
  if (findLines.length < 3) {
476
520
  // Need at least 3 lines to have meaningful context
477
- return
521
+ return;
478
522
  }
479
523
 
480
524
  // Remove trailing empty line if present
481
- if (findLines[findLines.length - 1] === "") {
482
- findLines.pop()
525
+ if (findLines[findLines.length - 1] === '') {
526
+ findLines.pop();
483
527
  }
484
528
 
485
- const contentLines = content.split("\n")
529
+ const contentLines = content.split('\n');
486
530
 
487
531
  // Extract first and last lines as context anchors
488
- const firstLine = findLines[0].trim()
489
- const lastLine = findLines[findLines.length - 1].trim()
532
+ const firstLine = findLines[0].trim();
533
+ const lastLine = findLines[findLines.length - 1].trim();
490
534
 
491
535
  // Find blocks that start and end with the context anchors
492
536
  for (let i = 0; i < contentLines.length; i++) {
493
- if (contentLines[i].trim() !== firstLine) continue
537
+ if (contentLines[i].trim() !== firstLine) continue;
494
538
 
495
539
  // Look for the matching last line
496
540
  for (let j = i + 2; j < contentLines.length; j++) {
497
541
  if (contentLines[j].trim() === lastLine) {
498
542
  // Found a potential context block
499
- const blockLines = contentLines.slice(i, j + 1)
500
- const block = blockLines.join("\n")
543
+ const blockLines = contentLines.slice(i, j + 1);
544
+ const block = blockLines.join('\n');
501
545
 
502
546
  // Check if the middle content has reasonable similarity
503
547
  // (simple heuristic: at least 50% of non-empty lines should match when trimmed)
504
548
  if (blockLines.length === findLines.length) {
505
- let matchingLines = 0
506
- let totalNonEmptyLines = 0
549
+ let matchingLines = 0;
550
+ let totalNonEmptyLines = 0;
507
551
 
508
552
  for (let k = 1; k < blockLines.length - 1; k++) {
509
- const blockLine = blockLines[k].trim()
510
- const findLine = findLines[k].trim()
553
+ const blockLine = blockLines[k].trim();
554
+ const findLine = findLines[k].trim();
511
555
 
512
556
  if (blockLine.length > 0 || findLine.length > 0) {
513
- totalNonEmptyLines++
557
+ totalNonEmptyLines++;
514
558
  if (blockLine === findLine) {
515
- matchingLines++
559
+ matchingLines++;
516
560
  }
517
561
  }
518
562
  }
519
563
 
520
- if (totalNonEmptyLines === 0 || matchingLines / totalNonEmptyLines >= 0.5) {
521
- yield block
522
- break // Only match the first occurrence
564
+ if (
565
+ totalNonEmptyLines === 0 ||
566
+ matchingLines / totalNonEmptyLines >= 0.5
567
+ ) {
568
+ yield block;
569
+ break; // Only match the first occurrence
523
570
  }
524
571
  }
525
- break
572
+ break;
526
573
  }
527
574
  }
528
575
  }
529
- }
576
+ };
530
577
 
531
578
  export function trimDiff(diff: string): string {
532
- const lines = diff.split("\n")
579
+ const lines = diff.split('\n');
533
580
  const contentLines = lines.filter(
534
581
  (line) =>
535
- (line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
536
- !line.startsWith("---") &&
537
- !line.startsWith("+++"),
538
- )
582
+ (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) &&
583
+ !line.startsWith('---') &&
584
+ !line.startsWith('+++')
585
+ );
539
586
 
540
- if (contentLines.length === 0) return diff
587
+ if (contentLines.length === 0) return diff;
541
588
 
542
- let min = Infinity
589
+ let min = Infinity;
543
590
  for (const line of contentLines) {
544
- const content = line.slice(1)
591
+ const content = line.slice(1);
545
592
  if (content.trim().length > 0) {
546
- const match = content.match(/^(\s*)/)
547
- if (match) min = Math.min(min, match[1].length)
593
+ const match = content.match(/^(\s*)/);
594
+ if (match) min = Math.min(min, match[1].length);
548
595
  }
549
596
  }
550
- if (min === Infinity || min === 0) return diff
597
+ if (min === Infinity || min === 0) return diff;
551
598
  const trimmedLines = lines.map((line) => {
552
599
  if (
553
- (line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
554
- !line.startsWith("---") &&
555
- !line.startsWith("+++")
600
+ (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) &&
601
+ !line.startsWith('---') &&
602
+ !line.startsWith('+++')
556
603
  ) {
557
- const prefix = line[0]
558
- const content = line.slice(1)
559
- return prefix + content.slice(min)
604
+ const prefix = line[0];
605
+ const content = line.slice(1);
606
+ return prefix + content.slice(min);
560
607
  }
561
- return line
562
- })
608
+ return line;
609
+ });
563
610
 
564
- return trimmedLines.join("\n")
611
+ return trimmedLines.join('\n');
565
612
  }
566
613
 
567
- export function replace(content: string, oldString: string, newString: string, replaceAll = false): string {
614
+ export function replace(
615
+ content: string,
616
+ oldString: string,
617
+ newString: string,
618
+ replaceAll = false
619
+ ): string {
568
620
  if (oldString === newString) {
569
- throw new Error("oldString and newString must be different")
621
+ throw new Error('oldString and newString must be different');
570
622
  }
571
623
 
572
- let notFound = true
624
+ let notFound = true;
573
625
 
574
626
  for (const replacer of [
575
627
  SimpleReplacer,
@@ -583,22 +635,26 @@ export function replace(content: string, oldString: string, newString: string, r
583
635
  MultiOccurrenceReplacer,
584
636
  ]) {
585
637
  for (const search of replacer(content, oldString)) {
586
- const index = content.indexOf(search)
587
- if (index === -1) continue
588
- notFound = false
638
+ const index = content.indexOf(search);
639
+ if (index === -1) continue;
640
+ notFound = false;
589
641
  if (replaceAll) {
590
- return content.replaceAll(search, newString)
642
+ return content.replaceAll(search, newString);
591
643
  }
592
- const lastIndex = content.lastIndexOf(search)
593
- if (index !== lastIndex) continue
594
- return content.substring(0, index) + newString + content.substring(index + search.length)
644
+ const lastIndex = content.lastIndexOf(search);
645
+ if (index !== lastIndex) continue;
646
+ return (
647
+ content.substring(0, index) +
648
+ newString +
649
+ content.substring(index + search.length)
650
+ );
595
651
  }
596
652
  }
597
653
 
598
654
  if (notFound) {
599
- throw new Error("oldString not found in content")
655
+ throw new Error('oldString not found in content');
600
656
  }
601
657
  throw new Error(
602
- "Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match.",
603
- )
658
+ 'Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match.'
659
+ );
604
660
  }