@amical/aiagent-sdk 0.1.0 → 0.1.2

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 (42) hide show
  1. package/dist/index.d.mts +476 -0
  2. package/dist/index.d.ts +476 -0
  3. package/dist/index.js +968 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/index.mjs +951 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +7 -1
  8. package/.agents/skills/Viral_Writer/SKILL.md +0 -239
  9. package/.cline_memory.json +0 -58
  10. package/.node-version +0 -1
  11. package/Agents.md +0 -95
  12. package/src/agent/Agent.ts +0 -566
  13. package/src/agent/index.ts +0 -1
  14. package/src/index.ts +0 -15
  15. package/src/llm/NodeApiHandler.ts +0 -117
  16. package/src/llm/WebApiHandler.ts +0 -118
  17. package/src/llm/index.ts +0 -47
  18. package/src/mcp/NodeMcpManager.ts +0 -158
  19. package/src/mcp/WebMcpManager.ts +0 -30
  20. package/src/mcp/index.ts +0 -56
  21. package/src/memory/NodeMemoryManager.ts +0 -82
  22. package/src/memory/WebMemoryManager.ts +0 -89
  23. package/src/memory/index.ts +0 -36
  24. package/src/parser/diff.ts +0 -855
  25. package/src/parser/index.ts +0 -111
  26. package/src/parser/parse-assistant-message.ts +0 -240
  27. package/src/prompt/NodePromptManager.ts +0 -276
  28. package/src/prompt/WebPromptManager.ts +0 -277
  29. package/src/prompt/index.ts +0 -28
  30. package/src/runtime/nodeRuntime.ts +0 -112
  31. package/src/runtime/web.test.ts +0 -95
  32. package/src/runtime/webRuntime.ts +0 -119
  33. package/src/runtime.ts +0 -46
  34. package/src/shared/tools.ts +0 -270
  35. package/test-node-agent.ts +0 -113
  36. package/test-runtime.mjs +0 -36
  37. package/test.md +0 -33
  38. package/tsconfig.json +0 -28
  39. package/tsup.config.ts +0 -21
  40. package/uapi.ts +0 -3
  41. package/webFetch.ts +0 -49
  42. package/webSearch.ts +0 -19
@@ -1,855 +0,0 @@
1
- const SEARCH_BLOCK_START = "------- SEARCH"
2
- const SEARCH_BLOCK_END = "======="
3
- const REPLACE_BLOCK_END = "+++++++ REPLACE"
4
-
5
- /**
6
- * Converts a character index in a string to a 1-based line number.
7
- * @param content - The full content string
8
- * @param charIndex - The character index in the content
9
- * @returns The 1-based line number where charIndex falls
10
- */
11
- export function getLineNumberFromCharIndex(content: string, charIndex: number): number {
12
- if (charIndex <= 0) return 1
13
- return content.substring(0, charIndex).split("\n").length
14
- }
15
-
16
- const SEARCH_BLOCK_CHAR = "-"
17
- const REPLACE_BLOCK_CHAR = "+"
18
- const LEGACY_SEARCH_BLOCK_CHAR = "<"
19
- const LEGACY_REPLACE_BLOCK_CHAR = ">"
20
-
21
- // Replace the exact string constants with flexible regex patterns
22
- const SEARCH_BLOCK_START_REGEX = /^[-]{3,} SEARCH>?$/
23
- const LEGACY_SEARCH_BLOCK_START_REGEX = /^[<]{3,} SEARCH>?$/
24
-
25
- const SEARCH_BLOCK_END_REGEX = /^[=]{3,}$/
26
-
27
- const REPLACE_BLOCK_END_REGEX = /^[+]{3,} REPLACE>?$/
28
- const LEGACY_REPLACE_BLOCK_END_REGEX = /^[>]{3,} REPLACE>?$/
29
-
30
- // Helper functions to check if a line matches the flexible patterns
31
- function isSearchBlockStart(line: string): boolean {
32
- return SEARCH_BLOCK_START_REGEX.test(line) || LEGACY_SEARCH_BLOCK_START_REGEX.test(line)
33
- }
34
-
35
- function isSearchBlockEnd(line: string): boolean {
36
- return SEARCH_BLOCK_END_REGEX.test(line)
37
- }
38
-
39
- function isReplaceBlockEnd(line: string): boolean {
40
- return REPLACE_BLOCK_END_REGEX.test(line) || LEGACY_REPLACE_BLOCK_END_REGEX.test(line)
41
- }
42
-
43
- /**
44
- * Attempts a line-trimmed fallback match for the given search content in the original content.
45
- * It tries to match `searchContent` lines against a block of lines in `originalContent` starting
46
- * from `lastProcessedIndex`. Lines are matched by trimming leading/trailing whitespace and ensuring
47
- * they are identical afterwards.
48
- *
49
- * Returns [matchIndexStart, matchIndexEnd] if found, or false if not found.
50
- */
51
- function lineTrimmedFallbackMatch(originalContent: string, searchContent: string, startIndex: number): [number, number] | false {
52
- // Split both contents into lines
53
- const originalLines = originalContent.split("\n")
54
- const searchLines = searchContent.split("\n")
55
-
56
- // Trim trailing empty line if exists (from the trailing \n in searchContent)
57
- if (searchLines[searchLines.length - 1] === "") {
58
- searchLines.pop()
59
- }
60
-
61
- // Find the line number where startIndex falls
62
- let startLineNum = 0
63
- let currentIndex = 0
64
- while (currentIndex < startIndex && startLineNum < originalLines.length) {
65
- currentIndex += originalLines[startLineNum].length + 1 // +1 for \n
66
- startLineNum++
67
- }
68
-
69
- // For each possible starting position in original content
70
- for (let i = startLineNum; i <= originalLines.length - searchLines.length; i++) {
71
- let matches = true
72
-
73
- // Try to match all search lines from this position
74
- for (let j = 0; j < searchLines.length; j++) {
75
- const originalTrimmed = originalLines[i + j].trim()
76
- const searchTrimmed = searchLines[j].trim()
77
-
78
- if (originalTrimmed !== searchTrimmed) {
79
- matches = false
80
- break
81
- }
82
- }
83
-
84
- // If we found a match, calculate the exact character positions
85
- if (matches) {
86
- // Find start character index
87
- let matchStartIndex = 0
88
- for (let k = 0; k < i; k++) {
89
- matchStartIndex += originalLines[k].length + 1 // +1 for \n
90
- }
91
-
92
- // Find end character index
93
- let matchEndIndex = matchStartIndex
94
- for (let k = 0; k < searchLines.length; k++) {
95
- matchEndIndex += originalLines[i + k].length + 1 // +1 for \n
96
- }
97
-
98
- return [matchStartIndex, matchEndIndex]
99
- }
100
- }
101
-
102
- return false
103
- }
104
-
105
- /**
106
- * Attempts to match blocks of code by using the first and last lines as anchors.
107
- * This is a third-tier fallback strategy that helps match blocks where we can identify
108
- * the correct location by matching the beginning and end, even if the exact content
109
- * differs slightly.
110
- *
111
- * The matching strategy:
112
- * 1. Only attempts to match blocks of 3 or more lines to avoid false positives
113
- * 2. Extracts from the search content:
114
- * - First line as the "start anchor"
115
- * - Last line as the "end anchor"
116
- * 3. For each position in the original content:
117
- * - Checks if the next line matches the start anchor
118
- * - If it does, jumps ahead by the search block size
119
- * - Checks if that line matches the end anchor
120
- * - All comparisons are done after trimming whitespace
121
- *
122
- * This approach is particularly useful for matching blocks of code where:
123
- * - The exact content might have minor differences
124
- * - The beginning and end of the block are distinctive enough to serve as anchors
125
- * - The overall structure (number of lines) remains the same
126
- *
127
- * @param originalContent - The full content of the original file
128
- * @param searchContent - The content we're trying to find in the original file
129
- * @param startIndex - The character index in originalContent where to start searching
130
- * @returns A tuple of [startIndex, endIndex] if a match is found, false otherwise
131
- */
132
- function blockAnchorFallbackMatch(originalContent: string, searchContent: string, startIndex: number): [number, number] | false {
133
- const originalLines = originalContent.split("\n")
134
- const searchLines = searchContent.split("\n")
135
-
136
- // Only use this approach for blocks of 3+ lines
137
- if (searchLines.length < 3) {
138
- return false
139
- }
140
-
141
- // Trim trailing empty line if exists
142
- if (searchLines[searchLines.length - 1] === "") {
143
- searchLines.pop()
144
- }
145
-
146
- const firstLineSearch = searchLines[0].trim()
147
- const lastLineSearch = searchLines[searchLines.length - 1].trim()
148
- const searchBlockSize = searchLines.length
149
-
150
- // Find the line number where startIndex falls
151
- let startLineNum = 0
152
- let currentIndex = 0
153
- while (currentIndex < startIndex && startLineNum < originalLines.length) {
154
- currentIndex += originalLines[startLineNum].length + 1
155
- startLineNum++
156
- }
157
-
158
- // Look for matching start and end anchors
159
- for (let i = startLineNum; i <= originalLines.length - searchBlockSize; i++) {
160
- // Check if first line matches
161
- if (originalLines[i].trim() !== firstLineSearch) {
162
- continue
163
- }
164
-
165
- // Check if last line matches at the expected position
166
- if (originalLines[i + searchBlockSize - 1].trim() !== lastLineSearch) {
167
- continue
168
- }
169
-
170
- // Calculate exact character positions
171
- let matchStartIndex = 0
172
- for (let k = 0; k < i; k++) {
173
- matchStartIndex += originalLines[k].length + 1
174
- }
175
-
176
- let matchEndIndex = matchStartIndex
177
- for (let k = 0; k < searchBlockSize; k++) {
178
- matchEndIndex += originalLines[i + k].length + 1
179
- }
180
-
181
- return [matchStartIndex, matchEndIndex]
182
- }
183
-
184
- return false
185
- }
186
-
187
- /**
188
- * This function reconstructs the file content by applying a streamed diff (in a
189
- * specialized SEARCH/REPLACE block format) to the original file content. It is designed
190
- * to handle both incremental updates and the final resulting file after all chunks have
191
- * been processed.
192
- *
193
- * The diff format is a custom structure that uses three markers to define changes:
194
- *
195
- * ------- SEARCH
196
- * [Exact content to find in the original file]
197
- * =======
198
- * [Content to replace with]
199
- * +++++++ REPLACE
200
- *
201
- * Behavior and Assumptions:
202
- * 1. The file is processed chunk-by-chunk. Each chunk of `diffContent` may contain
203
- * partial or complete SEARCH/REPLACE blocks. By calling this function with each
204
- * incremental chunk (with `isFinal` indicating the last chunk), the final reconstructed
205
- * file content is produced.
206
- *
207
- * 2. Matching Strategy (in order of attempt):
208
- * a. Exact Match: First attempts to find the exact SEARCH block text in the original file
209
- * b. Line-Trimmed Match: Falls back to line-by-line comparison ignoring leading/trailing whitespace
210
- * c. Block Anchor Match: For blocks of 3+ lines, tries to match using first/last lines as anchors
211
- * If all matching strategies fail, an error is thrown.
212
- *
213
- * 3. Empty SEARCH Section:
214
- * - If SEARCH is empty and the original file is empty, this indicates creating a new file
215
- * (pure insertion).
216
- * - If SEARCH is empty and the original file is not empty, this indicates a complete
217
- * file replacement (the entire original content is considered matched and replaced).
218
- *
219
- * 4. Applying Changes:
220
- * - Before encountering the "=======" marker, lines are accumulated as search content.
221
- * - After "=======" and before ">>>>>>> REPLACE", lines are accumulated as replacement content.
222
- * - Once the block is complete (">>>>>>> REPLACE"), the matched section in the original
223
- * file is replaced with the accumulated replacement lines, and the position in the original
224
- * file is advanced.
225
- *
226
- * 5. Incremental Output:
227
- * - As soon as the match location is found and we are in the REPLACE section, each new
228
- * replacement line is appended to the result so that partial updates can be viewed
229
- * incrementally.
230
- *
231
- * 6. Partial Markers:
232
- * - If the final line of the chunk looks like it might be part of a marker but is not one
233
- * of the known markers, it is removed. This prevents incomplete or partial markers
234
- * from corrupting the output.
235
- *
236
- * 7. Finalization:
237
- * - Once all chunks have been processed (when `isFinal` is true), any remaining original
238
- * content after the last replaced section is appended to the result.
239
- * - Trailing newlines are not forcibly added. The code tries to output exactly what is specified.
240
- *
241
- * Errors:
242
- * - If the search block cannot be matched using any of the available matching strategies,
243
- * an error is thrown.
244
- */
245
- export async function constructNewFileContent(
246
- diffContent: string,
247
- originalContent: string,
248
- isFinal: boolean,
249
- version: "v1" | "v2" = "v1",
250
- ): Promise<{ newContent: string; matchIndices: number[] }> {
251
- const constructor = constructNewFileContentVersionMapping[version]
252
- if (!constructor) {
253
- throw new Error(`Invalid version '${version}' for file content constructor`)
254
- }
255
- return constructor(diffContent, originalContent, isFinal)
256
- }
257
-
258
- const constructNewFileContentVersionMapping: Record<
259
- string,
260
- (diffContent: string, originalContent: string, isFinal: boolean) => Promise<{ newContent: string; matchIndices: number[] }>
261
- > = {
262
- v1: constructNewFileContentV1,
263
- v2: constructNewFileContentV2,
264
- } as const
265
-
266
- async function constructNewFileContentV1(
267
- diffContent: string,
268
- originalContent: string,
269
- isFinal: boolean,
270
- ): Promise<{ newContent: string; matchIndices: number[] }> {
271
- let result = ""
272
- let lastProcessedIndex = 0
273
-
274
- let currentSearchContent = ""
275
- let currentReplaceContent = ""
276
- let inSearch = false
277
- let inReplace = false
278
-
279
- let searchMatchIndex = -1
280
- let searchEndIndex = -1
281
-
282
- // Track all replacements to handle out-of-order edits
283
- const replacements: Array<{ start: number; end: number; content: string }> = []
284
- let pendingOutOfOrderReplacement = false
285
-
286
- const lines = diffContent.split("\n")
287
-
288
- // If the last line looks like a partial marker but isn't recognized,
289
- // remove it because it might be incomplete.
290
- const lastLine = lines[lines.length - 1]
291
- if (
292
- lines.length > 0 &&
293
- (lastLine.startsWith(SEARCH_BLOCK_CHAR) ||
294
- lastLine.startsWith(LEGACY_SEARCH_BLOCK_CHAR) ||
295
- lastLine.startsWith("=") ||
296
- lastLine.startsWith(REPLACE_BLOCK_CHAR) ||
297
- lastLine.startsWith(LEGACY_REPLACE_BLOCK_CHAR)) &&
298
- !isSearchBlockStart(lastLine) &&
299
- !isSearchBlockEnd(lastLine) &&
300
- !isReplaceBlockEnd(lastLine)
301
- ) {
302
- lines.pop()
303
- }
304
-
305
- for (const line of lines) {
306
- if (isSearchBlockStart(line)) {
307
- inSearch = true
308
- currentSearchContent = ""
309
- currentReplaceContent = ""
310
- continue
311
- }
312
-
313
- if (isSearchBlockEnd(line)) {
314
- inSearch = false
315
- inReplace = true
316
-
317
- // Remove trailing linebreak for adding the === marker
318
- // if (currentSearchContent.endsWith("\r\n")) {
319
- // currentSearchContent = currentSearchContent.slice(0, -2)
320
- // } else if (currentSearchContent.endsWith("\n")) {
321
- // currentSearchContent = currentSearchContent.slice(0, -1)
322
- // }
323
-
324
- if (!currentSearchContent) {
325
- // Empty search block
326
- if (originalContent.length === 0) {
327
- // New file scenario: nothing to match, just start inserting
328
- searchMatchIndex = 0
329
- searchEndIndex = 0
330
- } else {
331
- // ERROR: Empty search block with non-empty file indicates malformed SEARCH marker
332
- throw new Error(
333
- "Empty SEARCH block detected with non-empty file. This usually indicates a malformed SEARCH marker.\n" +
334
- "Please ensure your SEARCH marker follows the correct format:\n" +
335
- "- Use '------- SEARCH' (7+ dashes + space + SEARCH)\n",
336
- )
337
- }
338
- } else {
339
- // Add check for inefficient full-file search
340
- // if (currentSearchContent.trim() === originalContent.trim()) {
341
- // throw new Error(
342
- // "The SEARCH block contains the entire file content. Please either:\n" +
343
- // "1. Use an empty SEARCH block to replace the entire file, or\n" +
344
- // "2. Make focused changes to specific parts of the file that need modification.",
345
- // )
346
- // }
347
-
348
- // Exact search match scenario
349
- const exactIndex = originalContent.indexOf(currentSearchContent, lastProcessedIndex)
350
- if (exactIndex !== -1) {
351
- searchMatchIndex = exactIndex
352
- searchEndIndex = exactIndex + currentSearchContent.length
353
- } else {
354
- // Attempt fallback line-trimmed matching
355
- const lineMatch = lineTrimmedFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
356
- if (lineMatch) {
357
- ;[searchMatchIndex, searchEndIndex] = lineMatch
358
- } else {
359
- // Try block anchor fallback for larger blocks
360
- const blockMatch = blockAnchorFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
361
- if (blockMatch) {
362
- ;[searchMatchIndex, searchEndIndex] = blockMatch
363
- } else {
364
- // Last resort: search the entire file from the beginning
365
- const fullFileIndex = originalContent.indexOf(currentSearchContent, 0)
366
- if (fullFileIndex !== -1) {
367
- // Found in the file - could be out of order
368
- searchMatchIndex = fullFileIndex
369
- searchEndIndex = fullFileIndex + currentSearchContent.length
370
- if (searchMatchIndex < lastProcessedIndex) {
371
- pendingOutOfOrderReplacement = true
372
- }
373
- } else {
374
- throw new Error(
375
- `The SEARCH block:\n${currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
376
- )
377
- }
378
- }
379
- }
380
- }
381
- }
382
-
383
- // Check if this is an out-of-order replacement
384
- if (searchMatchIndex < lastProcessedIndex) {
385
- pendingOutOfOrderReplacement = true
386
- }
387
-
388
- // For in-order replacements, output everything up to the match location
389
- if (!pendingOutOfOrderReplacement) {
390
- result += originalContent.slice(lastProcessedIndex, searchMatchIndex)
391
- }
392
- continue
393
- }
394
-
395
- if (isReplaceBlockEnd(line)) {
396
- // Finished one replace block
397
-
398
- if (searchMatchIndex === -1) {
399
- throw new Error(`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...is malformatted.`)
400
- }
401
-
402
- // Store this replacement
403
- replacements.push({
404
- start: searchMatchIndex,
405
- end: searchEndIndex,
406
- content: currentReplaceContent,
407
- })
408
-
409
- // If this was an in-order replacement, advance lastProcessedIndex
410
- if (!pendingOutOfOrderReplacement) {
411
- lastProcessedIndex = searchEndIndex
412
- }
413
-
414
- // Reset for next block
415
- inSearch = false
416
- inReplace = false
417
- currentSearchContent = ""
418
- currentReplaceContent = ""
419
- searchMatchIndex = -1
420
- searchEndIndex = -1
421
- pendingOutOfOrderReplacement = false
422
- continue
423
- }
424
-
425
- // Accumulate content for search or replace
426
- // (currentReplaceContent is not being used for anything right now since we directly append to result.)
427
- // (We artificially add a linebreak since we split on \n at the beginning. In order to not include a trailing linebreak in the final search/result blocks we need to remove it before using them. This allows for partial line matches to be correctly identified.)
428
- // NOTE: search/replace blocks must be arranged in the order they appear in the file due to how we build the content using lastProcessedIndex. We also cannot strip the trailing newline since for non-partial lines it would remove the linebreak from the original content. (If we remove end linebreak from search, then we'd also have to remove it from replace but we can't know if it's a partial line or not since the model may be using the line break to indicate the end of the block rather than as part of the search content.) We require the model to output full lines in order for our fallbacks to work as well.
429
- if (inSearch) {
430
- currentSearchContent += line + "\n"
431
- } else if (inReplace) {
432
- currentReplaceContent += line + "\n"
433
- // Only output replacement lines immediately for in-order replacements
434
- if (searchMatchIndex !== -1 && !pendingOutOfOrderReplacement) {
435
- result += line + "\n"
436
- }
437
- }
438
- }
439
-
440
- // If this is the final chunk, we need to apply all replacements and build the final result
441
- if (isFinal) {
442
- // Handle the case where we're still in replace mode when processing ends
443
- // and this is the final chunk - treat it as if we encountered the REPLACE marker
444
- if (inReplace && searchMatchIndex !== -1) {
445
- // Store this replacement
446
- replacements.push({
447
- start: searchMatchIndex,
448
- end: searchEndIndex,
449
- content: currentReplaceContent,
450
- })
451
-
452
- // If this was an in-order replacement, advance lastProcessedIndex
453
- if (!pendingOutOfOrderReplacement) {
454
- lastProcessedIndex = searchEndIndex
455
- }
456
-
457
- // Reset state
458
- inSearch = false
459
- inReplace = false
460
- currentSearchContent = ""
461
- currentReplaceContent = ""
462
- searchMatchIndex = -1
463
- searchEndIndex = -1
464
- pendingOutOfOrderReplacement = false
465
- }
466
- // end of handling missing replace marker
467
-
468
- // Sort replacements by start position
469
- replacements.sort((a, b) => a.start - b.start)
470
-
471
- // Rebuild the entire result by applying all replacements
472
- result = ""
473
- let currentPos = 0
474
-
475
- for (const replacement of replacements) {
476
- // Add original content up to this replacement
477
- result += originalContent.slice(currentPos, replacement.start)
478
- // Add the replacement content
479
- result += replacement.content
480
- // Move position to after the replaced section
481
- currentPos = replacement.end
482
- }
483
-
484
- // Add any remaining original content
485
- result += originalContent.slice(currentPos)
486
- }
487
-
488
- // Return all match indices from the replacements
489
- // This is used to determine the line numbers for each SEARCH/REPLACE block in the UI
490
- return { newContent: result, matchIndices: replacements.map((r) => r.start) }
491
- }
492
-
493
- enum ProcessingState {
494
- Idle = 0,
495
- StateSearch = 1 << 0,
496
- StateReplace = 1 << 1,
497
- }
498
-
499
- class NewFileContentConstructor {
500
- private originalContent: string
501
- private isFinal: boolean
502
- private state: number
503
- private pendingNonStandardLines: string[]
504
- private result: string
505
- private lastProcessedIndex: number
506
- private currentSearchContent: string
507
- private searchMatchIndex: number
508
- private searchEndIndex: number
509
-
510
- constructor(originalContent: string, isFinal: boolean) {
511
- this.originalContent = originalContent
512
- this.isFinal = isFinal
513
- this.pendingNonStandardLines = []
514
- this.result = ""
515
- this.lastProcessedIndex = 0
516
- this.state = ProcessingState.Idle
517
- this.currentSearchContent = ""
518
- this.searchMatchIndex = -1
519
- this.searchEndIndex = -1
520
- }
521
-
522
- private resetForNextBlock() {
523
- // Reset for next block
524
- this.state = ProcessingState.Idle
525
- this.currentSearchContent = ""
526
- this.searchMatchIndex = -1
527
- this.searchEndIndex = -1
528
- }
529
-
530
- private findLastMatchingLineIndex(regx: RegExp, lineLimit: number) {
531
- for (let i = lineLimit; i > 0; ) {
532
- i--
533
- if (this.pendingNonStandardLines[i].match(regx)) {
534
- return i
535
- }
536
- }
537
- return -1
538
- }
539
-
540
- private updateProcessingState(newState: ProcessingState) {
541
- const isValidTransition =
542
- (this.state === ProcessingState.Idle && newState === ProcessingState.StateSearch) ||
543
- (this.state === ProcessingState.StateSearch && newState === ProcessingState.StateReplace)
544
-
545
- if (!isValidTransition) {
546
- throw new Error(
547
- `Invalid state transition.\n` +
548
- "Valid transitions are:\n" +
549
- "- Idle → StateSearch\n" +
550
- "- StateSearch → StateReplace",
551
- )
552
- }
553
-
554
- this.state |= newState
555
- }
556
-
557
- private isStateActive(state: ProcessingState): boolean {
558
- return (this.state & state) === state
559
- }
560
-
561
- private activateReplaceState() {
562
- this.updateProcessingState(ProcessingState.StateReplace)
563
- }
564
-
565
- private activateSearchState() {
566
- this.updateProcessingState(ProcessingState.StateSearch)
567
- this.currentSearchContent = ""
568
- }
569
-
570
- private isSearchingActive(): boolean {
571
- return this.isStateActive(ProcessingState.StateSearch)
572
- }
573
-
574
- private isReplacingActive(): boolean {
575
- return this.isStateActive(ProcessingState.StateReplace)
576
- }
577
-
578
- private hasPendingNonStandardLines(pendingNonStandardLineLimit: number): boolean {
579
- return this.pendingNonStandardLines.length - pendingNonStandardLineLimit < this.pendingNonStandardLines.length
580
- }
581
-
582
- public processLine(line: string) {
583
- this.internalProcessLine(line, true, this.pendingNonStandardLines.length)
584
- }
585
-
586
- public getResult(): { newContent: string; matchIndices: number[] } {
587
- // If this is the final chunk, append any remaining original content
588
- if (this.isFinal && this.lastProcessedIndex < this.originalContent.length) {
589
- this.result += this.originalContent.slice(this.lastProcessedIndex)
590
- }
591
- if (this.isFinal && this.state !== ProcessingState.Idle) {
592
- throw new Error("File processing incomplete - SEARCH/REPLACE operations still active during finalization")
593
- }
594
- // Note: V2 implementation doesn't currently track match indices
595
- // For now, return empty array. If V2 becomes the default and we need line numbers,
596
- // we should add state to track all match indices.
597
- return { newContent: this.result, matchIndices: [] }
598
- }
599
-
600
- private internalProcessLine(
601
- line: string,
602
- canWritependingNonStandardLines: boolean,
603
- pendingNonStandardLineLimit: number,
604
- ): number {
605
- let removeLineCount = 0
606
- if (isSearchBlockStart(line)) {
607
- removeLineCount = this.trimPendingNonStandardTrailingEmptyLines(pendingNonStandardLineLimit)
608
- if (removeLineCount > 0) {
609
- pendingNonStandardLineLimit = pendingNonStandardLineLimit - removeLineCount
610
- }
611
- if (this.hasPendingNonStandardLines(pendingNonStandardLineLimit)) {
612
- this.tryFixSearchReplaceBlock(pendingNonStandardLineLimit)
613
- canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
614
- }
615
- this.activateSearchState()
616
- } else if (isSearchBlockEnd(line)) {
617
- // 校验非标内容
618
- if (!this.isSearchingActive()) {
619
- this.tryFixSearchBlock(pendingNonStandardLineLimit)
620
- canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
621
- }
622
- this.activateReplaceState()
623
- this.beforeReplace()
624
- } else if (isReplaceBlockEnd(line)) {
625
- if (!this.isReplacingActive()) {
626
- this.tryFixReplaceBlock(pendingNonStandardLineLimit)
627
- canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
628
- }
629
- this.lastProcessedIndex = this.searchEndIndex
630
- this.resetForNextBlock()
631
- } else {
632
- // Accumulate content for search or replace
633
- // (currentReplaceContent is not being used for anything right now since we directly append to result.)
634
- // (We artificially add a linebreak since we split on \n at the beginning. In order to not include a trailing linebreak in the final search/result blocks we need to remove it before using them. This allows for partial line matches to be correctly identified.)
635
- // NOTE: search/replace blocks must be arranged in the order they appear in the file due to how we build the content using lastProcessedIndex. We also cannot strip the trailing newline since for non-partial lines it would remove the linebreak from the original content. (If we remove end linebreak from search, then we'd also have to remove it from replace but we can't know if it's a partial line or not since the model may be using the line break to indicate the end of the block rather than as part of the search content.) We require the model to output full lines in order for our fallbacks to work as well.
636
- if (this.isReplacingActive()) {
637
- // Output replacement lines immediately if we know the insertion point
638
- if (this.searchMatchIndex !== -1) {
639
- this.result += line + "\n"
640
- }
641
- } else if (this.isSearchingActive()) {
642
- this.currentSearchContent += line + "\n"
643
- } else {
644
- const appendToPendingNonStandardLines = canWritependingNonStandardLines
645
- if (appendToPendingNonStandardLines) {
646
- // 处理非标内容
647
- this.pendingNonStandardLines.push(line)
648
- }
649
- }
650
- }
651
- return removeLineCount
652
- }
653
-
654
- private beforeReplace() {
655
- // Remove trailing linebreak for adding the === marker
656
- // if (currentSearchContent.endsWith("\r\n")) {
657
- // currentSearchContent = currentSearchContent.slice(0, -2)
658
- // } else if (currentSearchContent.endsWith("\n")) {
659
- // currentSearchContent = currentSearchContent.slice(0, -1)
660
- // }
661
-
662
- if (!this.currentSearchContent) {
663
- // Empty search block
664
- if (this.originalContent.length === 0) {
665
- // New file scenario: nothing to match, just start inserting
666
- this.searchMatchIndex = 0
667
- this.searchEndIndex = 0
668
- } else {
669
- // Complete file replacement scenario: treat the entire file as matched
670
- this.searchMatchIndex = 0
671
- this.searchEndIndex = this.originalContent.length
672
- }
673
- } else {
674
- // Add check for inefficient full-file search
675
- // if (currentSearchContent.trim() === originalContent.trim()) {
676
- // throw new Error(
677
- // "The SEARCH block contains the entire file content. Please either:\n" +
678
- // "1. Use an empty SEARCH block to replace the entire file, or\n" +
679
- // "2. Make focused changes to specific parts of the file that need modification.",
680
- // )
681
- // }
682
- // Exact search match scenario
683
- const exactIndex = this.originalContent.indexOf(this.currentSearchContent, this.lastProcessedIndex)
684
- if (exactIndex !== -1) {
685
- this.searchMatchIndex = exactIndex
686
- this.searchEndIndex = exactIndex + this.currentSearchContent.length
687
- } else {
688
- // Attempt fallback line-trimmed matching
689
- const lineMatch = lineTrimmedFallbackMatch(
690
- this.originalContent,
691
- this.currentSearchContent,
692
- this.lastProcessedIndex,
693
- )
694
- if (lineMatch) {
695
- ;[this.searchMatchIndex, this.searchEndIndex] = lineMatch
696
- } else {
697
- // Try block anchor fallback for larger blocks
698
- const blockMatch = blockAnchorFallbackMatch(
699
- this.originalContent,
700
- this.currentSearchContent,
701
- this.lastProcessedIndex,
702
- )
703
- if (blockMatch) {
704
- ;[this.searchMatchIndex, this.searchEndIndex] = blockMatch
705
- } else {
706
- throw new Error(
707
- `The SEARCH block:\n${this.currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
708
- )
709
- }
710
- }
711
- }
712
- }
713
- if (this.searchMatchIndex < this.lastProcessedIndex) {
714
- throw new Error(
715
- `The SEARCH block:\n${this.currentSearchContent.trimEnd()}\n...matched an incorrect content in the file.`,
716
- )
717
- }
718
- // Output everything up to the match location
719
- this.result += this.originalContent.slice(this.lastProcessedIndex, this.searchMatchIndex)
720
- }
721
-
722
- private tryFixSearchBlock(lineLimit: number): number {
723
- let removeLineCount = 0
724
- if (lineLimit < 0) {
725
- lineLimit = this.pendingNonStandardLines.length
726
- }
727
- if (!lineLimit) {
728
- throw new Error("Invalid SEARCH/REPLACE block structure - no lines available to process")
729
- }
730
- const searchTagRegexp = /^([-]{3,}|[<]{3,}) SEARCH$/
731
- const searchTagIndex = this.findLastMatchingLineIndex(searchTagRegexp, lineLimit)
732
- if (searchTagIndex !== -1) {
733
- const fixLines = this.pendingNonStandardLines.slice(searchTagIndex, lineLimit)
734
- fixLines[0] = SEARCH_BLOCK_START
735
- for (const line of fixLines) {
736
- removeLineCount += this.internalProcessLine(line, false, searchTagIndex)
737
- }
738
- } else {
739
- throw new Error(
740
- `Invalid REPLACE marker detected - could not find matching SEARCH block starting from line ${searchTagIndex + 1}`,
741
- )
742
- }
743
- return removeLineCount
744
- }
745
-
746
- private tryFixReplaceBlock(lineLimit: number): number {
747
- let removeLineCount = 0
748
- if (lineLimit < 0) {
749
- lineLimit = this.pendingNonStandardLines.length
750
- }
751
- if (!lineLimit) {
752
- throw new Error()
753
- }
754
- const replaceBeginTagRegexp = /^[=]{3,}$/
755
- const replaceBeginTagIndex = this.findLastMatchingLineIndex(replaceBeginTagRegexp, lineLimit)
756
- if (replaceBeginTagIndex !== -1) {
757
- // // 校验非标内容
758
- // if (!this.isSearchingActive()) {
759
- // removeLineCount += this.tryFixSearchBlock(replaceBeginTagIndex)
760
- // }
761
- const fixLines = this.pendingNonStandardLines.slice(
762
- replaceBeginTagIndex - removeLineCount,
763
- lineLimit - removeLineCount,
764
- )
765
- fixLines[0] = SEARCH_BLOCK_END
766
- for (const line of fixLines) {
767
- removeLineCount += this.internalProcessLine(line, false, replaceBeginTagIndex - removeLineCount)
768
- }
769
- } else {
770
- throw new Error(`Malformed REPLACE block - missing valid separator after line ${replaceBeginTagIndex + 1}`)
771
- }
772
- return removeLineCount
773
- }
774
-
775
- private tryFixSearchReplaceBlock(lineLimit: number): number {
776
- let removeLineCount = 0
777
- if (lineLimit < 0) {
778
- lineLimit = this.pendingNonStandardLines.length
779
- }
780
- if (!lineLimit) {
781
- throw new Error()
782
- }
783
-
784
- const replaceEndTagRegexp = /^([+]{3,}|[>]{3,}) REPLACE$/
785
- const replaceEndTagIndex = this.findLastMatchingLineIndex(replaceEndTagRegexp, lineLimit)
786
- const likeReplaceEndTag = replaceEndTagIndex === lineLimit - 1
787
- if (likeReplaceEndTag) {
788
- // // 校验非标内容
789
- // if (!this.isReplacingActive()) {
790
- // removeLineCount += this.tryFixReplaceBlock(replaceEndTagIndex)
791
- // }
792
- const fixLines = this.pendingNonStandardLines.slice(replaceEndTagIndex - removeLineCount, lineLimit - removeLineCount)
793
- fixLines[fixLines.length - 1] = REPLACE_BLOCK_END
794
- for (const line of fixLines) {
795
- removeLineCount += this.internalProcessLine(line, false, replaceEndTagIndex - removeLineCount)
796
- }
797
- } else {
798
- throw new Error("Malformed SEARCH/REPLACE block structure: Missing valid closing REPLACE marker")
799
- }
800
- return removeLineCount
801
- }
802
-
803
- /**
804
- * Removes trailing empty lines from the pendingNonStandardLines array
805
- * @param lineLimit - The index to start checking from (exclusive).
806
- * Removes empty lines from lineLimit-1 backwards.
807
- * @returns The number of empty lines removed
808
- */
809
- private trimPendingNonStandardTrailingEmptyLines(lineLimit: number): number {
810
- let removedCount = 0
811
- let i = Math.min(lineLimit, this.pendingNonStandardLines.length) - 1
812
-
813
- while (i >= 0 && this.pendingNonStandardLines[i].trim() === "") {
814
- this.pendingNonStandardLines.pop()
815
- removedCount++
816
- i--
817
- }
818
-
819
- return removedCount
820
- }
821
- }
822
-
823
- export async function constructNewFileContentV2(
824
- diffContent: string,
825
- originalContent: string,
826
- isFinal: boolean,
827
- ): Promise<{ newContent: string; matchIndices: number[] }> {
828
- const newFileContentConstructor = new NewFileContentConstructor(originalContent, isFinal)
829
-
830
- const lines = diffContent.split("\n")
831
-
832
- // If the last line looks like a partial marker but isn't recognized,
833
- // remove it because it might be incomplete.
834
- const lastLine = lines[lines.length - 1]
835
- if (
836
- lines.length > 0 &&
837
- (lastLine.startsWith(SEARCH_BLOCK_CHAR) ||
838
- lastLine.startsWith(LEGACY_SEARCH_BLOCK_CHAR) ||
839
- lastLine.startsWith("=") ||
840
- lastLine.startsWith(REPLACE_BLOCK_CHAR) ||
841
- lastLine.startsWith(LEGACY_REPLACE_BLOCK_CHAR)) &&
842
- lastLine !== SEARCH_BLOCK_START &&
843
- lastLine !== SEARCH_BLOCK_END &&
844
- lastLine !== REPLACE_BLOCK_END
845
- ) {
846
- lines.pop()
847
- }
848
-
849
- for (const line of lines) {
850
- newFileContentConstructor.processLine(line)
851
- }
852
-
853
- const result = newFileContentConstructor.getResult()
854
- return result
855
- }