opencode-gbk-tools 0.1.14 → 0.1.15

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.
@@ -16306,6 +16306,14 @@ async function assertPathAllowed(filePath, context, allowExternal = false) {
16306
16306
 
16307
16307
  // src/lib/gbk-file.ts
16308
16308
  var STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
16309
+ var STREAM_READ_CHUNK_SIZE_BYTES = 1024 * 1024;
16310
+ var gbkLineIndexCache = /* @__PURE__ */ new Map();
16311
+ function toSafeNumber(value) {
16312
+ return typeof value === "bigint" ? Number(value) : value;
16313
+ }
16314
+ function invalidateGbkLineIndex(filePath) {
16315
+ gbkLineIndexCache.delete(filePath);
16316
+ }
16309
16317
  function assertStringArgument(value, name) {
16310
16318
  if (typeof value !== "string") {
16311
16319
  throw createGbkError("GBK_INVALID_ARGUMENT", `${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
@@ -16469,6 +16477,16 @@ function finalizeNewlineStyle(crlfCount, lfCount) {
16469
16477
  }
16470
16478
  return "none";
16471
16479
  }
16480
+ function formatLineWindowContent(text, startLine, expectedLineCount) {
16481
+ const lines = text.length === 0 ? [""] : text.split(/\r?\n/);
16482
+ while (lines.length > expectedLineCount && lines[lines.length - 1] === "") {
16483
+ lines.pop();
16484
+ }
16485
+ while (lines.length < expectedLineCount) {
16486
+ lines.push("");
16487
+ }
16488
+ return lines.slice(0, expectedLineCount).map((line, index) => `${startLine + index}: ${line}`).join("\n");
16489
+ }
16472
16490
  function applyLineRange(text, startLine, endLine) {
16473
16491
  if (startLine === void 0 && endLine === void 0) {
16474
16492
  return {
@@ -16594,6 +16612,29 @@ function hasLineNumberPrefixes(lines) {
16594
16612
  function stripLineNumberPrefixes(lines) {
16595
16613
  return lines.map((l) => l.replace(/^\d+: /, ""));
16596
16614
  }
16615
+ function parseLineNumberPrefixedBlock(text) {
16616
+ const lines = splitNormalizedLines(text);
16617
+ if (!hasLineNumberPrefixes(lines)) {
16618
+ return null;
16619
+ }
16620
+ const lineNumbers = [];
16621
+ const strippedLines = [];
16622
+ for (const line of lines) {
16623
+ const match = /^(\d+): ?(.*)$/.exec(line);
16624
+ if (!match) {
16625
+ return null;
16626
+ }
16627
+ lineNumbers.push(Number(match[1]));
16628
+ strippedLines.push(match[2]);
16629
+ }
16630
+ return {
16631
+ lineNumbers,
16632
+ strippedText: strippedLines.join("\n"),
16633
+ isContiguous: lineNumbers.every((lineNumber, index) => index === 0 || lineNumber === lineNumbers[index - 1] + 1),
16634
+ startLine: lineNumbers[0],
16635
+ endLine: lineNumbers[lineNumbers.length - 1]
16636
+ };
16637
+ }
16597
16638
  function matchLooseBlock(contentLines, oldLines, newLines, newlineStyle, content) {
16598
16639
  for (let start = 0; start < contentLines.length; start += 1) {
16599
16640
  let contentIndex = start;
@@ -16710,7 +16751,7 @@ async function readWholeGbkTextFile(input) {
16710
16751
  }
16711
16752
  async function visitDecodedTextChunks(input, visitor) {
16712
16753
  const decoder = import_iconv_lite.default.getDecoder(input.encoding);
16713
- const stream = createReadStream(input.filePath);
16754
+ const stream = createReadStream(input.filePath, { highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES });
16714
16755
  try {
16715
16756
  for await (const chunk of stream) {
16716
16757
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
@@ -16733,109 +16774,89 @@ async function visitDecodedTextChunks(input, visitor) {
16733
16774
  stream.destroy();
16734
16775
  }
16735
16776
  }
16736
- function createLineCollector(offset, limit) {
16737
- const lines = [];
16738
- let pending = "";
16739
- let totalLines = 0;
16777
+ async function getGbkLineIndex(input) {
16778
+ const cached2 = gbkLineIndexCache.get(input.filePath);
16779
+ if (cached2 && cached2.fileSize === toSafeNumber(input.stat.size) && cached2.mtimeMs === toSafeNumber(input.stat.mtimeMs)) {
16780
+ return cached2;
16781
+ }
16782
+ const lineStartOffsets = [0];
16783
+ let byteOffset = 0;
16784
+ let previousByteWasCR = false;
16740
16785
  let crlfCount = 0;
16741
16786
  let lfCount = 0;
16742
- const emitLine = (line) => {
16743
- totalLines += 1;
16744
- if (totalLines >= offset && lines.length < limit) {
16745
- lines.push(`${totalLines}: ${line}`);
16746
- }
16747
- };
16748
- return {
16749
- push(text) {
16750
- if (text.length === 0) {
16751
- return;
16752
- }
16753
- const combined = pending + text;
16754
- let start = 0;
16755
- while (true) {
16756
- const newlineIndex = combined.indexOf("\n", start);
16757
- if (newlineIndex === -1) {
16758
- break;
16759
- }
16760
- let line = combined.slice(start, newlineIndex);
16761
- if (line.endsWith("\r")) {
16762
- line = line.slice(0, -1);
16763
- crlfCount += 1;
16764
- } else {
16765
- lfCount += 1;
16787
+ const stream = createReadStream(input.filePath, { highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES });
16788
+ try {
16789
+ for await (const chunk of stream) {
16790
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
16791
+ assertNotBinary(buffer);
16792
+ for (let index = 0; index < buffer.length; index += 1) {
16793
+ const byte = buffer[index];
16794
+ if (byte === 10) {
16795
+ if (previousByteWasCR) {
16796
+ crlfCount += 1;
16797
+ } else {
16798
+ lfCount += 1;
16799
+ }
16800
+ lineStartOffsets.push(byteOffset + index + 1);
16801
+ previousByteWasCR = false;
16802
+ continue;
16766
16803
  }
16767
- emitLine(line);
16768
- start = newlineIndex + 1;
16804
+ previousByteWasCR = byte === 13;
16769
16805
  }
16770
- pending = combined.slice(start);
16771
- },
16772
- finish() {
16773
- emitLine(pending);
16774
- const endLine = lines.length === 0 ? totalLines : offset + lines.length - 1;
16775
- return {
16776
- startLine: offset,
16777
- endLine,
16778
- totalLines,
16779
- content: lines.join("\n"),
16780
- tail: false,
16781
- truncated: endLine < totalLines,
16782
- newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
16783
- };
16806
+ byteOffset += buffer.length;
16784
16807
  }
16785
- };
16786
- }
16787
- function createTailCollector(limit) {
16788
- assertPositiveInteger(limit, "limit");
16789
- const lines = [];
16790
- let pending = "";
16791
- let totalLines = 0;
16792
- let crlfCount = 0;
16793
- let lfCount = 0;
16794
- const emitLine = (line) => {
16795
- totalLines += 1;
16796
- lines.push(line);
16797
- if (lines.length > limit) {
16798
- lines.shift();
16808
+ } catch (error45) {
16809
+ if (error45 instanceof Error && "code" in error45) {
16810
+ throw error45;
16799
16811
  }
16812
+ throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${input.filePath}`, error45);
16813
+ } finally {
16814
+ stream.destroy();
16815
+ }
16816
+ const result = {
16817
+ filePath: input.filePath,
16818
+ fileSize: toSafeNumber(input.stat.size),
16819
+ mtimeMs: toSafeNumber(input.stat.mtimeMs),
16820
+ lineStartOffsets,
16821
+ totalLines: lineStartOffsets.length,
16822
+ newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
16800
16823
  };
16801
- return {
16802
- push(text) {
16803
- if (text.length === 0) {
16804
- return;
16805
- }
16806
- const combined = pending + text;
16807
- let start = 0;
16808
- while (true) {
16809
- const newlineIndex = combined.indexOf("\n", start);
16810
- if (newlineIndex === -1) {
16811
- break;
16812
- }
16813
- let line = combined.slice(start, newlineIndex);
16814
- if (line.endsWith("\r")) {
16815
- line = line.slice(0, -1);
16816
- crlfCount += 1;
16817
- } else {
16818
- lfCount += 1;
16819
- }
16820
- emitLine(line);
16821
- start = newlineIndex + 1;
16824
+ gbkLineIndexCache.set(input.filePath, result);
16825
+ return result;
16826
+ }
16827
+ async function readDecodedGbkByteRange(input, start, endExclusive) {
16828
+ if (endExclusive <= start) {
16829
+ return "";
16830
+ }
16831
+ const decoder = import_iconv_lite.default.getDecoder(input.encoding);
16832
+ const stream = createReadStream(input.filePath, {
16833
+ start,
16834
+ end: endExclusive - 1,
16835
+ highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES
16836
+ });
16837
+ const parts = [];
16838
+ try {
16839
+ for await (const chunk of stream) {
16840
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
16841
+ assertNotBinary(buffer);
16842
+ const text = decoder.write(buffer);
16843
+ if (text.length > 0) {
16844
+ parts.push(text);
16822
16845
  }
16823
- pending = combined.slice(start);
16824
- },
16825
- finish() {
16826
- emitLine(pending);
16827
- const startLine = Math.max(1, totalLines - lines.length + 1);
16828
- return {
16829
- startLine,
16830
- endLine: totalLines,
16831
- totalLines,
16832
- content: lines.map((line, index) => `${startLine + index}: ${line}`).join("\n"),
16833
- truncated: startLine > 1,
16834
- tail: true,
16835
- newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
16836
- };
16837
16846
  }
16838
- };
16847
+ const trailingText = decoder.end() ?? "";
16848
+ if (trailingText.length > 0) {
16849
+ parts.push(trailingText);
16850
+ }
16851
+ return parts.join("");
16852
+ } catch (error45) {
16853
+ if (error45 instanceof Error && "code" in error45) {
16854
+ throw error45;
16855
+ }
16856
+ throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${input.filePath}`, error45);
16857
+ } finally {
16858
+ stream.destroy();
16859
+ }
16839
16860
  }
16840
16861
  async function writeAll(handle, buffer) {
16841
16862
  let offset = 0;
@@ -16852,6 +16873,257 @@ async function writeEncodedText(handle, encoding, text) {
16852
16873
  await writeAll(handle, buffer);
16853
16874
  return buffer.byteLength;
16854
16875
  }
16876
+ async function appendEncodedText(filePath, encoding, text) {
16877
+ if (text.length === 0) {
16878
+ return 0;
16879
+ }
16880
+ const buffer = import_iconv_lite.default.encode(text, encoding);
16881
+ await fs2.appendFile(filePath, buffer);
16882
+ return buffer.byteLength;
16883
+ }
16884
+ async function copyFileByteRangeToHandle(sourcePath, handle, start, endExclusive) {
16885
+ if (endExclusive <= start) {
16886
+ return 0;
16887
+ }
16888
+ const stream = createReadStream(sourcePath, {
16889
+ start,
16890
+ end: endExclusive - 1,
16891
+ highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES
16892
+ });
16893
+ let bytesWritten = 0;
16894
+ try {
16895
+ for await (const chunk of stream) {
16896
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
16897
+ await writeAll(handle, buffer);
16898
+ bytesWritten += buffer.byteLength;
16899
+ }
16900
+ return bytesWritten;
16901
+ } catch (error45) {
16902
+ if (error45 instanceof Error && "code" in error45) {
16903
+ throw error45;
16904
+ }
16905
+ throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${sourcePath}`, error45);
16906
+ } finally {
16907
+ stream.destroy();
16908
+ }
16909
+ }
16910
+ function alignTextToNewlineStyle(text, newlineStyle) {
16911
+ return newlineStyle === "crlf" ? text.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : newlineStyle === "lf" ? text.replace(/\r\n/g, "\n") : text;
16912
+ }
16913
+ function replaceScopedTextContent(scopeText, oldString, newString, replaceAll, newlineStyle) {
16914
+ const occurrencesBefore = countOccurrences(scopeText, oldString);
16915
+ if (!replaceAll && occurrencesBefore === 0) {
16916
+ const loose = tryLooseBlockReplace(scopeText, oldString, newString);
16917
+ if (loose !== null) {
16918
+ return {
16919
+ replacedText: loose.content,
16920
+ replacements: 1,
16921
+ occurrencesBefore: loose.occurrencesBefore
16922
+ };
16923
+ }
16924
+ }
16925
+ if (replaceAll) {
16926
+ if (occurrencesBefore === 0) {
16927
+ throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scopeText, oldString));
16928
+ }
16929
+ } else if (occurrencesBefore === 0) {
16930
+ throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scopeText, oldString));
16931
+ } else if (occurrencesBefore > 1) {
16932
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${oldString}`);
16933
+ }
16934
+ const alignedNewString = alignTextToNewlineStyle(newString, newlineStyle);
16935
+ return {
16936
+ replacedText: replaceAll ? scopeText.split(oldString).join(alignedNewString) : scopeText.replace(oldString, alignedNewString),
16937
+ replacements: replaceAll ? occurrencesBefore : 1,
16938
+ occurrencesBefore
16939
+ };
16940
+ }
16941
+ async function replaceLargeGbkFileTextInLineRange(input) {
16942
+ const lineIndex = await getGbkLineIndex(input);
16943
+ const requestedStartLine = input.startLine ?? 1;
16944
+ const requestedEndLine = input.endLine ?? lineIndex.totalLines;
16945
+ if (requestedEndLine < requestedStartLine) {
16946
+ throw createGbkError("GBK_INVALID_ARGUMENT", "endLine \u4E0D\u80FD\u5C0F\u4E8E startLine");
16947
+ }
16948
+ const actualStartLine = Math.min(requestedStartLine, lineIndex.totalLines);
16949
+ const actualEndLine = Math.min(requestedEndLine, lineIndex.totalLines);
16950
+ const rangeStart = lineIndex.lineStartOffsets[Math.max(0, actualStartLine - 1)] ?? toSafeNumber(input.stat.size);
16951
+ const rangeEnd = actualEndLine < lineIndex.totalLines ? lineIndex.lineStartOffsets[actualEndLine] : toSafeNumber(input.stat.size);
16952
+ const scopeText = await readDecodedGbkByteRange(input, rangeStart, rangeEnd);
16953
+ const replaced = replaceScopedTextContent(
16954
+ scopeText,
16955
+ input.oldString,
16956
+ input.newString,
16957
+ input.replaceAll,
16958
+ lineIndex.newlineStyle
16959
+ );
16960
+ const tempPath = path2.join(
16961
+ path2.dirname(input.filePath),
16962
+ `${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
16963
+ );
16964
+ const handle = await fs2.open(tempPath, "w");
16965
+ let bytesWritten = 0;
16966
+ try {
16967
+ bytesWritten += await copyFileByteRangeToHandle(input.filePath, handle, 0, rangeStart);
16968
+ bytesWritten += await writeEncodedText(handle, input.encoding, replaced.replacedText);
16969
+ bytesWritten += await copyFileByteRangeToHandle(input.filePath, handle, rangeEnd, toSafeNumber(input.stat.size));
16970
+ await handle.close();
16971
+ const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
16972
+ await fs2.chmod(tempPath, mode);
16973
+ await fs2.rename(tempPath, input.filePath);
16974
+ return {
16975
+ mode: "replace",
16976
+ filePath: input.filePath,
16977
+ encoding: input.encoding,
16978
+ replacements: replaced.replacements,
16979
+ occurrencesBefore: replaced.occurrencesBefore,
16980
+ bytesRead: input.stat.size,
16981
+ bytesWritten
16982
+ };
16983
+ } catch (error45) {
16984
+ await handle.close().catch(() => void 0);
16985
+ await fs2.rm(tempPath, { force: true }).catch(() => void 0);
16986
+ throw error45;
16987
+ }
16988
+ }
16989
+ async function replaceLargeGbkFileByAnchor(input) {
16990
+ const tempPath = path2.join(
16991
+ path2.dirname(input.filePath),
16992
+ `${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
16993
+ );
16994
+ const handle = await fs2.open(tempPath, "w");
16995
+ const alignedContent = input.content.replace(/\r\n/g, "\n");
16996
+ const anchorLength = input.anchor.length;
16997
+ const carryLength = Math.max(anchorLength + alignedContent.length, 1);
16998
+ let decoded = "";
16999
+ let scanFrom = 0;
17000
+ let totalMatches = 0;
17001
+ let inserted = false;
17002
+ let skipped = false;
17003
+ let bytesWritten = 0;
17004
+ const flushPrefix = async (preserveTailLength) => {
17005
+ const flushLength = Math.max(0, decoded.length - preserveTailLength);
17006
+ if (flushLength === 0) {
17007
+ return;
17008
+ }
17009
+ bytesWritten += await writeEncodedText(handle, input.encoding, decoded.slice(0, flushLength));
17010
+ decoded = decoded.slice(flushLength);
17011
+ scanFrom = Math.max(0, scanFrom - flushLength);
17012
+ };
17013
+ const finalizeInserted = async () => {
17014
+ bytesWritten += await writeEncodedText(handle, input.encoding, decoded);
17015
+ decoded = "";
17016
+ };
17017
+ try {
17018
+ await visitDecodedTextChunks(input, async (text) => {
17019
+ decoded += text;
17020
+ while (!inserted) {
17021
+ const foundAt = decoded.indexOf(input.anchor, scanFrom);
17022
+ if (foundAt === -1) {
17023
+ break;
17024
+ }
17025
+ totalMatches += 1;
17026
+ const afterAnchor = foundAt + anchorLength;
17027
+ if (totalMatches !== input.occurrence) {
17028
+ scanFrom = afterAnchor;
17029
+ continue;
17030
+ }
17031
+ const before = decoded.slice(0, foundAt);
17032
+ const after = decoded.slice(afterAnchor);
17033
+ const anchorAndAfter = decoded.slice(foundAt);
17034
+ const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
17035
+ if (alreadyExists) {
17036
+ if (input.ifExists === "error") {
17037
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
17038
+ }
17039
+ if (input.ifExists === "skip") {
17040
+ skipped = true;
17041
+ inserted = true;
17042
+ bytesWritten += await writeEncodedText(handle, input.encoding, decoded);
17043
+ decoded = "";
17044
+ return;
17045
+ }
17046
+ }
17047
+ if (input.mode === "insertAfter") {
17048
+ bytesWritten += await writeEncodedText(handle, input.encoding, before);
17049
+ bytesWritten += await writeEncodedText(handle, input.encoding, input.anchor);
17050
+ bytesWritten += await writeEncodedText(handle, input.encoding, alignedContent);
17051
+ bytesWritten += await writeEncodedText(handle, input.encoding, after);
17052
+ } else {
17053
+ bytesWritten += await writeEncodedText(handle, input.encoding, before);
17054
+ bytesWritten += await writeEncodedText(handle, input.encoding, alignedContent);
17055
+ bytesWritten += await writeEncodedText(handle, input.encoding, anchorAndAfter);
17056
+ }
17057
+ decoded = "";
17058
+ scanFrom = 0;
17059
+ inserted = true;
17060
+ return;
17061
+ }
17062
+ if (!inserted) {
17063
+ await flushPrefix(carryLength);
17064
+ }
17065
+ });
17066
+ if (!inserted) {
17067
+ while (true) {
17068
+ const foundAt = decoded.indexOf(input.anchor, scanFrom);
17069
+ if (foundAt === -1) {
17070
+ break;
17071
+ }
17072
+ totalMatches += 1;
17073
+ const afterAnchor = foundAt + anchorLength;
17074
+ if (totalMatches !== input.occurrence) {
17075
+ scanFrom = afterAnchor;
17076
+ continue;
17077
+ }
17078
+ const before = decoded.slice(0, foundAt);
17079
+ const after = decoded.slice(afterAnchor);
17080
+ const anchorAndAfter = decoded.slice(foundAt);
17081
+ const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
17082
+ if (alreadyExists) {
17083
+ if (input.ifExists === "error") {
17084
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
17085
+ }
17086
+ if (input.ifExists === "skip") {
17087
+ skipped = true;
17088
+ inserted = true;
17089
+ break;
17090
+ }
17091
+ }
17092
+ decoded = input.mode === "insertAfter" ? `${before}${input.anchor}${alignedContent}${after}` : `${before}${alignedContent}${anchorAndAfter}`;
17093
+ inserted = true;
17094
+ break;
17095
+ }
17096
+ }
17097
+ if (!inserted && totalMatches === 0) {
17098
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${input.anchor}`);
17099
+ }
17100
+ if (!inserted && totalMatches > 0) {
17101
+ throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${input.anchor} \u53EA\u627E\u5230 ${totalMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${input.occurrence} \u5904`);
17102
+ }
17103
+ await finalizeInserted();
17104
+ await handle.close();
17105
+ const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
17106
+ await fs2.chmod(tempPath, mode);
17107
+ await fs2.rename(tempPath, input.filePath);
17108
+ invalidateGbkLineIndex(input.filePath);
17109
+ return {
17110
+ mode: input.mode,
17111
+ filePath: input.filePath,
17112
+ encoding: input.encoding,
17113
+ anchor: input.anchor,
17114
+ occurrence: input.occurrence,
17115
+ anchorMatches: totalMatches,
17116
+ inserted: !skipped,
17117
+ skipped,
17118
+ bytesRead: input.stat.size,
17119
+ bytesWritten
17120
+ };
17121
+ } catch (error45) {
17122
+ await handle.close().catch(() => void 0);
17123
+ await fs2.rm(tempPath, { force: true }).catch(() => void 0);
17124
+ throw error45;
17125
+ }
17126
+ }
16855
17127
  async function replaceLargeGbkFileText(input) {
16856
17128
  const tempPath = path2.join(
16857
17129
  path2.dirname(input.filePath),
@@ -16898,6 +17170,7 @@ async function replaceLargeGbkFileText(input) {
16898
17170
  const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
16899
17171
  await fs2.chmod(tempPath, mode);
16900
17172
  await fs2.rename(tempPath, input.filePath);
17173
+ invalidateGbkLineIndex(input.filePath);
16901
17174
  return {
16902
17175
  filePath: input.filePath,
16903
17176
  encoding: input.encoding,
@@ -16912,22 +17185,6 @@ async function replaceLargeGbkFileText(input) {
16912
17185
  throw error45;
16913
17186
  }
16914
17187
  }
16915
- async function loadGbkTextFile(input) {
16916
- const resolved = await resolveReadableGbkFile(input);
16917
- if (resolved.stat.size < STREAMING_FILE_SIZE_THRESHOLD_BYTES) {
16918
- return await readWholeGbkTextFile(resolved);
16919
- }
16920
- const chunks = [];
16921
- await visitDecodedTextChunks(resolved, (text) => {
16922
- chunks.push(text);
16923
- });
16924
- return {
16925
- filePath: resolved.filePath,
16926
- encoding: resolved.encoding,
16927
- content: chunks.join(""),
16928
- bytesRead: resolved.stat.size
16929
- };
16930
- }
16931
17188
  async function readGbkFile(input) {
16932
17189
  const offset = normalizeOptionalPositiveInteger(input.offset, "offset") ?? 1;
16933
17190
  const limit = normalizeOptionalPositiveInteger(input.limit, "limit") ?? 2e3;
@@ -16953,11 +17210,49 @@ async function readGbkFile(input) {
16953
17210
  ...lineWindow2
16954
17211
  };
16955
17212
  }
16956
- const collector = tail ? createTailCollector(limit) : createLineCollector(offset, limit);
16957
- await visitDecodedTextChunks(resolved, (text) => {
16958
- collector.push(text);
16959
- });
16960
- const lineWindow = collector.finish();
17213
+ const lineIndex = await getGbkLineIndex(resolved);
17214
+ const totalLines = lineIndex.totalLines;
17215
+ let lineWindow;
17216
+ if (tail) {
17217
+ const startLine = Math.max(1, totalLines - limit + 1);
17218
+ const expectedLineCount = totalLines - startLine + 1;
17219
+ const startOffset = lineIndex.lineStartOffsets[startLine - 1] ?? 0;
17220
+ const text = await readDecodedGbkByteRange(resolved, startOffset, toSafeNumber(resolved.stat.size));
17221
+ lineWindow = {
17222
+ startLine,
17223
+ endLine: totalLines,
17224
+ totalLines,
17225
+ content: formatLineWindowContent(text, startLine, expectedLineCount),
17226
+ truncated: startLine > 1,
17227
+ tail: true,
17228
+ newlineStyle: lineIndex.newlineStyle
17229
+ };
17230
+ } else if (offset > totalLines) {
17231
+ lineWindow = {
17232
+ startLine: offset,
17233
+ endLine: totalLines,
17234
+ totalLines,
17235
+ content: "",
17236
+ tail: false,
17237
+ truncated: false,
17238
+ newlineStyle: lineIndex.newlineStyle
17239
+ };
17240
+ } else {
17241
+ const endLine = Math.min(offset + limit - 1, totalLines);
17242
+ const expectedLineCount = endLine - offset + 1;
17243
+ const startOffset = lineIndex.lineStartOffsets[offset - 1] ?? 0;
17244
+ const endOffset = endLine < totalLines ? lineIndex.lineStartOffsets[endLine] : toSafeNumber(resolved.stat.size);
17245
+ const text = await readDecodedGbkByteRange(resolved, startOffset, endOffset);
17246
+ lineWindow = {
17247
+ startLine: offset,
17248
+ endLine,
17249
+ totalLines,
17250
+ content: formatLineWindowContent(text, offset, expectedLineCount),
17251
+ tail: false,
17252
+ truncated: endLine < totalLines,
17253
+ newlineStyle: lineIndex.newlineStyle
17254
+ };
17255
+ }
16961
17256
  return {
16962
17257
  filePath: resolved.filePath,
16963
17258
  encoding: resolved.encoding,
@@ -16977,8 +17272,18 @@ async function replaceGbkFileText(input) {
16977
17272
  if (mode === "insertAfter" || mode === "insertBefore") {
16978
17273
  assertInsertArguments(input);
16979
17274
  const resolved2 = await resolveReadableGbkFile(normalizedInput);
16980
- const current2 = await readWholeGbkTextFile(resolved2);
16981
17275
  const occurrence = normalizeOptionalPositiveInteger(input.occurrence, "occurrence") ?? 1;
17276
+ if (resolved2.stat.size >= STREAMING_FILE_SIZE_THRESHOLD_BYTES) {
17277
+ return await replaceLargeGbkFileByAnchor({
17278
+ ...resolved2,
17279
+ mode,
17280
+ anchor: input.anchor,
17281
+ content: input.content,
17282
+ occurrence,
17283
+ ifExists: input.ifExists ?? "skip"
17284
+ });
17285
+ }
17286
+ const current2 = await readWholeGbkTextFile(resolved2);
16982
17287
  const insertResult = insertByAnchor(
16983
17288
  current2.content,
16984
17289
  mode,
@@ -17023,24 +17328,47 @@ async function replaceGbkFileText(input) {
17023
17328
  }
17024
17329
  const replaceAll = normalizedInput.replaceAll ?? false;
17025
17330
  const resolved = await resolveReadableGbkFile(normalizedInput);
17026
- const hasScopedRange = normalizedInput.startLine !== void 0 || normalizedInput.endLine !== void 0 || normalizedInput.startAnchor !== void 0 || normalizedInput.endAnchor !== void 0;
17331
+ const prefixedBlock = parseLineNumberPrefixedBlock(input.oldString);
17332
+ const derivedScopedRange = prefixedBlock?.isContiguous ? {
17333
+ startLine: normalizedInput.startLine ?? prefixedBlock.startLine,
17334
+ endLine: normalizedInput.endLine ?? prefixedBlock.endLine,
17335
+ oldString: prefixedBlock.strippedText
17336
+ } : null;
17337
+ const effectiveOldString = derivedScopedRange?.oldString ?? input.oldString;
17338
+ const hasScopedRange = normalizedInput.startLine !== void 0 || normalizedInput.endLine !== void 0 || normalizedInput.startAnchor !== void 0 || normalizedInput.endAnchor !== void 0 || derivedScopedRange !== null;
17027
17339
  if (resolved.stat.size >= STREAMING_FILE_SIZE_THRESHOLD_BYTES && !hasScopedRange) {
17028
17340
  return await replaceLargeGbkFileText({
17029
17341
  ...resolved,
17030
- oldString: input.oldString,
17342
+ oldString: effectiveOldString,
17031
17343
  newString: input.newString,
17032
17344
  replaceAll
17033
17345
  });
17034
17346
  }
17347
+ if (resolved.stat.size >= STREAMING_FILE_SIZE_THRESHOLD_BYTES && normalizedInput.startAnchor === void 0 && normalizedInput.endAnchor === void 0 && (normalizedInput.startLine !== void 0 || normalizedInput.endLine !== void 0 || derivedScopedRange !== null)) {
17348
+ return await replaceLargeGbkFileTextInLineRange({
17349
+ ...resolved,
17350
+ oldString: effectiveOldString,
17351
+ newString: input.newString,
17352
+ replaceAll,
17353
+ startLine: derivedScopedRange?.startLine ?? normalizedInput.startLine,
17354
+ endLine: derivedScopedRange?.endLine ?? normalizedInput.endLine
17355
+ });
17356
+ }
17035
17357
  const current = await readWholeGbkTextFile(resolved);
17036
- const scope = resolveEditScope(current.content, normalizedInput);
17037
- const occurrencesBefore = countOccurrences(scope.selectedText, input.oldString);
17358
+ const scopedInput = derivedScopedRange === null ? normalizedInput : {
17359
+ ...normalizedInput,
17360
+ startLine: derivedScopedRange.startLine,
17361
+ endLine: derivedScopedRange.endLine
17362
+ };
17363
+ const scope = resolveEditScope(current.content, scopedInput);
17364
+ const occurrencesBefore = countOccurrences(scope.selectedText, effectiveOldString);
17038
17365
  if (!replaceAll && occurrencesBefore === 0) {
17039
- const loose = tryLooseBlockReplace(scope.selectedText, input.oldString, input.newString);
17366
+ const loose = tryLooseBlockReplace(scope.selectedText, effectiveOldString, input.newString);
17040
17367
  if (loose !== null) {
17041
17368
  const outputText2 = `${current.content.slice(0, scope.rangeStart)}${loose.content}${current.content.slice(scope.rangeEnd)}`;
17042
17369
  const buffer2 = import_iconv_lite.default.encode(outputText2, current.encoding);
17043
17370
  await fs2.writeFile(current.filePath, buffer2);
17371
+ invalidateGbkLineIndex(current.filePath);
17044
17372
  return {
17045
17373
  mode: "replace",
17046
17374
  filePath: current.filePath,
@@ -17054,19 +17382,20 @@ async function replaceGbkFileText(input) {
17054
17382
  }
17055
17383
  if (replaceAll) {
17056
17384
  if (occurrencesBefore === 0) {
17057
- throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, input.oldString));
17385
+ throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, effectiveOldString));
17058
17386
  }
17059
17387
  } else if (occurrencesBefore === 0) {
17060
- throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, input.oldString));
17388
+ throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, effectiveOldString));
17061
17389
  } else if (occurrencesBefore > 1) {
17062
- throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
17390
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${effectiveOldString}`);
17063
17391
  }
17064
17392
  const fileNewlineStyle = detectNewlineStyle(current.content);
17065
17393
  const alignedNewString = fileNewlineStyle === "crlf" ? input.newString.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : fileNewlineStyle === "lf" ? input.newString.replace(/\r\n/g, "\n") : input.newString;
17066
- const replaced = replaceAll ? scope.selectedText.split(input.oldString).join(alignedNewString) : scope.selectedText.replace(input.oldString, alignedNewString);
17394
+ const replaced = replaceAll ? scope.selectedText.split(effectiveOldString).join(alignedNewString) : scope.selectedText.replace(effectiveOldString, alignedNewString);
17067
17395
  const outputText = `${current.content.slice(0, scope.rangeStart)}${replaced}${current.content.slice(scope.rangeEnd)}`;
17068
17396
  const buffer = import_iconv_lite.default.encode(outputText, current.encoding);
17069
17397
  await fs2.writeFile(current.filePath, buffer);
17398
+ invalidateGbkLineIndex(current.filePath);
17070
17399
  return {
17071
17400
  mode: "replace",
17072
17401
  filePath: current.filePath,
@@ -17083,25 +17412,58 @@ async function searchGbkFile(input) {
17083
17412
  }
17084
17413
  const contextLines = Math.max(0, input.contextLines ?? 3);
17085
17414
  const resolved = await resolveReadableGbkFile(input);
17086
- const loaded = await loadGbkTextFile({
17087
- filePath: resolved.filePath,
17088
- encoding: resolved.encoding,
17089
- allowExternal: input.allowExternal,
17090
- context: input.context
17091
- });
17092
- const lines = loaded.content.split(/\r?\n/);
17093
- const totalLines = lines.length;
17094
17415
  const matches = [];
17095
- for (let i = 0; i < lines.length; i += 1) {
17096
- if (lines[i].includes(input.pattern)) {
17097
- matches.push({
17098
- lineNumber: i + 1,
17099
- line: lines[i],
17100
- contextBefore: lines.slice(Math.max(0, i - contextLines), i),
17101
- contextAfter: lines.slice(i + 1, Math.min(totalLines, i + 1 + contextLines))
17102
- });
17416
+ const beforeBuffer = [];
17417
+ const activeAfterCollectors = [];
17418
+ let pending = "";
17419
+ let totalLines = 0;
17420
+ const emitLine = (line) => {
17421
+ totalLines += 1;
17422
+ for (let index = activeAfterCollectors.length - 1; index >= 0; index -= 1) {
17423
+ const collector = activeAfterCollectors[index];
17424
+ if (collector.remaining > 0) {
17425
+ collector.match.contextAfter.push(line);
17426
+ collector.remaining -= 1;
17427
+ }
17428
+ if (collector.remaining === 0) {
17429
+ activeAfterCollectors.splice(index, 1);
17430
+ }
17431
+ }
17432
+ if (line.includes(input.pattern)) {
17433
+ const match = {
17434
+ lineNumber: totalLines,
17435
+ line,
17436
+ contextBefore: beforeBuffer.slice(Math.max(0, beforeBuffer.length - contextLines)),
17437
+ contextAfter: []
17438
+ };
17439
+ matches.push(match);
17440
+ if (contextLines > 0) {
17441
+ activeAfterCollectors.push({ match, remaining: contextLines });
17442
+ }
17103
17443
  }
17104
- }
17444
+ beforeBuffer.push(line);
17445
+ if (beforeBuffer.length > contextLines) {
17446
+ beforeBuffer.shift();
17447
+ }
17448
+ };
17449
+ await visitDecodedTextChunks(resolved, (text) => {
17450
+ const combined = pending + text;
17451
+ let start = 0;
17452
+ while (true) {
17453
+ const newlineIndex = combined.indexOf("\n", start);
17454
+ if (newlineIndex === -1) {
17455
+ break;
17456
+ }
17457
+ let line = combined.slice(start, newlineIndex);
17458
+ if (line.endsWith("\r")) {
17459
+ line = line.slice(0, -1);
17460
+ }
17461
+ emitLine(line);
17462
+ start = newlineIndex + 1;
17463
+ }
17464
+ pending = combined.slice(start);
17465
+ });
17466
+ emitLine(pending);
17105
17467
  return {
17106
17468
  filePath: resolved.filePath,
17107
17469
  encoding: resolved.encoding,
@@ -17134,24 +17496,21 @@ async function writeGbkFile(input) {
17134
17496
  throw error45;
17135
17497
  }
17136
17498
  }
17137
- let existingContent = "";
17138
17499
  let existed = false;
17139
17500
  try {
17140
- const existingBuffer = await fs2.readFile(candidatePath);
17141
- existingContent = import_iconv_lite.default.decode(existingBuffer, encoding);
17501
+ await fs2.stat(candidatePath);
17142
17502
  existed = true;
17143
17503
  } catch (error45) {
17144
17504
  if (!(error45 instanceof Error && "code" in error45 && error45.code === "ENOENT")) {
17145
17505
  throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${candidatePath}`, error45);
17146
17506
  }
17147
17507
  }
17148
- const combined = existingContent + input.content;
17149
- const buffer = import_iconv_lite.default.encode(combined, encoding);
17150
- await fs2.writeFile(candidatePath, buffer);
17508
+ const bytesWritten = await appendEncodedText(candidatePath, encoding, input.content);
17509
+ invalidateGbkLineIndex(candidatePath);
17151
17510
  return {
17152
17511
  filePath: candidatePath,
17153
17512
  encoding,
17154
- bytesWritten: buffer.byteLength,
17513
+ bytesWritten,
17155
17514
  created: !existed,
17156
17515
  overwritten: false,
17157
17516
  appended: true
@@ -17193,6 +17552,7 @@ async function writeGbkFile(input) {
17193
17552
  const existed = await fs2.stat(candidatePath).then(() => true).catch(() => false);
17194
17553
  const buffer = import_iconv_lite.default.encode(input.content, encoding);
17195
17554
  await fs2.writeFile(candidatePath, buffer);
17555
+ invalidateGbkLineIndex(candidatePath);
17196
17556
  return {
17197
17557
  filePath: candidatePath,
17198
17558
  encoding,
@@ -17212,7 +17572,8 @@ async function writeGbkFile(input) {
17212
17572
  var gbk_edit_default = tool({
17213
17573
  description: `Edit GBK/GB18030 encoded text files with exact string replacement.
17214
17574
 
17215
- Reads the FULL file content regardless of file size \u2014 not limited by gbk_read's line window.
17575
+ For whole-file replace, large files use a streaming path.
17576
+ For line-scoped replace, large files use a cached line-byte index and only decode the selected line window.
17216
17577
  Safe to use on files with more than 2000 lines.
17217
17578
 
17218
17579
  CRITICAL \u2014 do NOT include line number prefixes in oldString or newString:
@@ -17256,7 +17617,7 @@ Insert mode:
17256
17617
  },
17257
17618
  async execute(args, context) {
17258
17619
  const result = await replaceGbkFileText({ ...args, context });
17259
- const isReplace = !("mode" in result) || result.mode === "replace";
17620
+ const isReplace = "replacements" in result;
17260
17621
  const title = isReplace ? `GBK \u7F16\u8F91 ${result.encoding.toUpperCase()} x${result.replacements}` : result.inserted ? `GBK \u63D2\u5165 ${result.encoding.toUpperCase()} #${result.occurrence}` : `GBK \u8DF3\u8FC7 ${result.encoding.toUpperCase()} #${result.occurrence}`;
17261
17622
  context.metadata({
17262
17623
  title,
@@ -17284,6 +17645,7 @@ var gbk_read_default = tool({
17284
17645
  description: `Read GBK/GB18030 encoded text files with line numbers.
17285
17646
 
17286
17647
  Returns up to 'limit' lines (default 2000) starting from 'offset'.
17648
+ Large files use a cached line-byte index so reading a small window does not require decoding the whole file each time.
17287
17649
  When the file has more lines than the window, 'truncated' is true and 'totalLines' shows the full count.
17288
17650
 
17289
17651
  IMPORTANT \u2014 line number format: each output line is prefixed with "N: " (e.g. "3787: content").
@@ -17367,7 +17729,7 @@ var gbk_write_default = tool({
17367
17729
  description: `Write GBK encoded text files.
17368
17730
 
17369
17731
  **append=true** (recommended for adding content to existing files):
17370
- - Reads the existing file content and appends new content at the end.
17732
+ - Appends encoded content directly to the end of the file without re-reading the whole file.
17371
17733
  - Works whether the file exists or not (creates it if missing).
17372
17734
  - Use this whenever you want to add lines/content to an existing GBK file.
17373
17735
  - Example: gbk_write(filePath=..., content="\\r\\n\u65B0\u5185\u5BB9", append=true)
@@ -17680,7 +18042,7 @@ function buildLineDiffPreview(filePath, encoding, beforeText, afterText) {
17680
18042
  return lines.join("\n");
17681
18043
  }
17682
18044
  function buildInsertOutput(text, mode, anchor, content, occurrence, ifExists, newlineStyle) {
17683
- const alignedContent = alignTextToNewlineStyle(content, newlineStyle);
18045
+ const alignedContent = alignTextToNewlineStyle2(content, newlineStyle);
17684
18046
  const located = findOccurrenceIndex2(text, anchor, occurrence);
17685
18047
  const insertionPoint = mode === "insertAfter" ? located.index + anchor.length : located.index;
17686
18048
  const alreadyExists = mode === "insertAfter" ? text.slice(insertionPoint, insertionPoint + alignedContent.length) === alignedContent : text.slice(Math.max(0, insertionPoint - alignedContent.length), insertionPoint) === alignedContent;
@@ -17804,7 +18166,7 @@ async function detectReadableTextFile(input) {
17804
18166
  hasBom: detected.hasBom
17805
18167
  };
17806
18168
  }
17807
- function createLineCollector2(offset, limit) {
18169
+ function createLineCollector(offset, limit) {
17808
18170
  const lines = [];
17809
18171
  let pending = "";
17810
18172
  let totalLines = 0;
@@ -17854,7 +18216,7 @@ function createLineCollector2(offset, limit) {
17854
18216
  }
17855
18217
  };
17856
18218
  }
17857
- function createTailCollector2(limit) {
18219
+ function createTailCollector(limit) {
17858
18220
  assertPositiveInteger(limit, "limit");
17859
18221
  const lines = [];
17860
18222
  let pending = "";
@@ -18068,7 +18430,7 @@ function resolveEditScope2(text, input) {
18068
18430
  rangeEnd: anchored.rangeStart + lineRanged.rangeEnd
18069
18431
  };
18070
18432
  }
18071
- function alignTextToNewlineStyle(text, newlineStyle) {
18433
+ function alignTextToNewlineStyle2(text, newlineStyle) {
18072
18434
  assertStringArgument2(text, "text");
18073
18435
  const normalized = text.replace(/\r\n/g, "\n");
18074
18436
  if (newlineStyle === "crlf") {
@@ -18133,7 +18495,7 @@ async function readTextFile(input) {
18133
18495
  ...lineWindow2
18134
18496
  };
18135
18497
  }
18136
- const collector = tail ? createTailCollector2(limit) : createLineCollector2(offset, limit);
18498
+ const collector = tail ? createTailCollector(limit) : createLineCollector(offset, limit);
18137
18499
  await visitDecodedTextChunks2(resolved, (text) => {
18138
18500
  collector.push(text);
18139
18501
  });
@@ -18186,7 +18548,7 @@ async function writeTextFile(input) {
18186
18548
  const targetHasBom = targetEncoding === "utf8-bom" ? true : existing && preserveEncoding ? existing.hasBom : targetEncoding === "utf16le" || targetEncoding === "utf16be" ? true : false;
18187
18549
  const baseContent = append ? existing?.content ?? "" : "";
18188
18550
  const rawContent = `${baseContent}${input.content}`;
18189
- const outputContent = existing && preserveNewlineStyle ? alignTextToNewlineStyle(rawContent, existing.newlineStyle) : rawContent;
18551
+ const outputContent = existing && preserveNewlineStyle ? alignTextToNewlineStyle2(rawContent, existing.newlineStyle) : rawContent;
18190
18552
  ensureLossless(outputContent, targetEncoding, targetHasBom);
18191
18553
  const buffer = encodeText(outputContent, targetEncoding, targetHasBom);
18192
18554
  await fs3.writeFile(candidatePath, buffer);
@@ -18303,7 +18665,7 @@ async function replaceTextFileText(input) {
18303
18665
  } else if (occurrencesBefore > 1) {
18304
18666
  throw createTextError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
18305
18667
  }
18306
- const alignedNewString = normalizedInput.preserveNewlineStyle === false ? input.newString : alignTextToNewlineStyle(input.newString, loaded.newlineStyle);
18668
+ const alignedNewString = normalizedInput.preserveNewlineStyle === false ? input.newString : alignTextToNewlineStyle2(input.newString, loaded.newlineStyle);
18307
18669
  const replaced = replaceAll ? scope.selectedText.split(input.oldString).join(alignedNewString) : scope.selectedText.replace(input.oldString, alignedNewString);
18308
18670
  const outputText = `${loaded.content.slice(0, scope.rangeStart)}${replaced}${loaded.content.slice(scope.rangeEnd)}`;
18309
18671
  const targetEncoding = preserveEncoding || requestedEncoding === "auto" ? loaded.encoding : resolveExplicitTextEncoding(requestedEncoding, loaded.encoding);