opencode-gbk-tools 0.1.27 → 0.1.29

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.
@@ -3910,6 +3910,139 @@ function estimateSessionTokens(messages) {
3910
3910
  return Math.ceil(totalChars / 4);
3911
3911
  }
3912
3912
 
3913
+ // src/lib/encoding-memory.ts
3914
+ import fs from "fs/promises";
3915
+ import os from "os";
3916
+ import path from "path";
3917
+ var ENCODING_MEMORY_VERSION = 1;
3918
+ var ENCODING_MEMORY_FILE_NAME = "encoding-memory.json";
3919
+ var CONFIG_DIR_ENV = "OPENCODE_GBK_TOOLS_CONFIG_DIR";
3920
+ var memoryCache = null;
3921
+ var loadingPromise = null;
3922
+ function resolveConfigDirectory() {
3923
+ const override = process.env[CONFIG_DIR_ENV];
3924
+ if (typeof override === "string" && override.trim().length > 0) {
3925
+ return path.resolve(override);
3926
+ }
3927
+ if (process.platform === "win32") {
3928
+ return path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "opencode-gbk-tools");
3929
+ }
3930
+ if (process.platform === "darwin") {
3931
+ return path.join(os.homedir(), "Library", "Application Support", "opencode-gbk-tools");
3932
+ }
3933
+ return path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "opencode-gbk-tools");
3934
+ }
3935
+ function normalizeFilePath(filePath) {
3936
+ const resolved = path.normalize(path.resolve(filePath));
3937
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
3938
+ }
3939
+ function toStoredFilePath(filePath) {
3940
+ return path.normalize(path.resolve(filePath));
3941
+ }
3942
+ function isRememberedEntry(value) {
3943
+ if (!value || typeof value !== "object") {
3944
+ return false;
3945
+ }
3946
+ const entry = value;
3947
+ return typeof entry.filePath === "string" && isRememberedGbkEncoding(entry.encoding) && typeof entry.mtimeMs === "number" && Number.isFinite(entry.mtimeMs) && typeof entry.size === "number" && Number.isFinite(entry.size) && typeof entry.lastConfirmedAt === "number" && Number.isFinite(entry.lastConfirmedAt);
3948
+ }
3949
+ async function readEncodingMemoryMap() {
3950
+ if (memoryCache) {
3951
+ return memoryCache;
3952
+ }
3953
+ if (loadingPromise) {
3954
+ return loadingPromise;
3955
+ }
3956
+ loadingPromise = (async () => {
3957
+ const map2 = /* @__PURE__ */ new Map();
3958
+ try {
3959
+ const raw = await fs.readFile(getEncodingMemoryFilePath(), "utf8");
3960
+ const parsed = JSON.parse(raw);
3961
+ const entries = Array.isArray(parsed.entries) ? parsed.entries : [];
3962
+ for (const entry of entries) {
3963
+ if (isRememberedEntry(entry)) {
3964
+ map2.set(normalizeFilePath(entry.filePath), entry);
3965
+ }
3966
+ }
3967
+ } catch (error45) {
3968
+ if (!(error45 instanceof Error && "code" in error45 && error45.code === "ENOENT")) {
3969
+ throw error45;
3970
+ }
3971
+ }
3972
+ memoryCache = map2;
3973
+ loadingPromise = null;
3974
+ return map2;
3975
+ })();
3976
+ return loadingPromise;
3977
+ }
3978
+ async function persistEncodingMemoryMap(map2) {
3979
+ const entries = [...map2.values()].sort((a, b) => a.filePath.localeCompare(b.filePath));
3980
+ const payload = {
3981
+ version: ENCODING_MEMORY_VERSION,
3982
+ entries
3983
+ };
3984
+ const filePath = getEncodingMemoryFilePath();
3985
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
3986
+ await fs.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
3987
+ }
3988
+ async function statRegularFile(filePath) {
3989
+ try {
3990
+ const stat = await fs.stat(filePath);
3991
+ return stat.isFile() ? stat : null;
3992
+ } catch (error45) {
3993
+ if (error45 instanceof Error && "code" in error45 && error45.code === "ENOENT") {
3994
+ return null;
3995
+ }
3996
+ throw error45;
3997
+ }
3998
+ }
3999
+ function isRememberedGbkEncoding(encoding) {
4000
+ return encoding === "gbk" || encoding === "gb18030";
4001
+ }
4002
+ function getEncodingMemoryFilePath() {
4003
+ return path.join(resolveConfigDirectory(), ENCODING_MEMORY_FILE_NAME);
4004
+ }
4005
+ async function rememberGbkEncoding(filePath, encoding) {
4006
+ const stat = await statRegularFile(filePath);
4007
+ if (!stat) {
4008
+ return null;
4009
+ }
4010
+ const map2 = await readEncodingMemoryMap();
4011
+ const entry = {
4012
+ filePath: toStoredFilePath(filePath),
4013
+ encoding,
4014
+ mtimeMs: stat.mtimeMs,
4015
+ size: Number(stat.size),
4016
+ lastConfirmedAt: Date.now()
4017
+ };
4018
+ map2.set(normalizeFilePath(filePath), entry);
4019
+ await persistEncodingMemoryMap(map2);
4020
+ return entry;
4021
+ }
4022
+ async function forgetRememberedEncoding(filePath) {
4023
+ const map2 = await readEncodingMemoryMap();
4024
+ const deleted = map2.delete(normalizeFilePath(filePath));
4025
+ if (deleted) {
4026
+ await persistEncodingMemoryMap(map2);
4027
+ }
4028
+ return deleted;
4029
+ }
4030
+ async function getRememberedGbkEncoding(filePath) {
4031
+ const map2 = await readEncodingMemoryMap();
4032
+ const key = normalizeFilePath(filePath);
4033
+ const entry = map2.get(key);
4034
+ if (!entry) {
4035
+ return null;
4036
+ }
4037
+ const stat = await statRegularFile(filePath);
4038
+ if (!stat || stat.mtimeMs !== entry.mtimeMs || Number(stat.size) !== entry.size) {
4039
+ map2.delete(key);
4040
+ await persistEncodingMemoryMap(map2);
4041
+ return null;
4042
+ }
4043
+ return entry;
4044
+ }
4045
+
3913
4046
  // node_modules/zod/v4/classic/external.js
3914
4047
  var external_exports = {};
3915
4048
  __export(external_exports, {
@@ -4641,10 +4774,10 @@ function mergeDefs(...defs) {
4641
4774
  function cloneDef(schema) {
4642
4775
  return mergeDefs(schema._zod.def);
4643
4776
  }
4644
- function getElementAtPath(obj, path4) {
4645
- if (!path4)
4777
+ function getElementAtPath(obj, path5) {
4778
+ if (!path5)
4646
4779
  return obj;
4647
- return path4.reduce((acc, key) => acc?.[key], obj);
4780
+ return path5.reduce((acc, key) => acc?.[key], obj);
4648
4781
  }
4649
4782
  function promiseAllObject(promisesObj) {
4650
4783
  const keys = Object.keys(promisesObj);
@@ -5005,11 +5138,11 @@ function aborted(x, startIndex = 0) {
5005
5138
  }
5006
5139
  return false;
5007
5140
  }
5008
- function prefixIssues(path4, issues) {
5141
+ function prefixIssues(path5, issues) {
5009
5142
  return issues.map((iss) => {
5010
5143
  var _a;
5011
5144
  (_a = iss).path ?? (_a.path = []);
5012
- iss.path.unshift(path4);
5145
+ iss.path.unshift(path5);
5013
5146
  return iss;
5014
5147
  });
5015
5148
  }
@@ -5177,7 +5310,7 @@ function treeifyError(error45, _mapper) {
5177
5310
  return issue2.message;
5178
5311
  };
5179
5312
  const result = { errors: [] };
5180
- const processError = (error46, path4 = []) => {
5313
+ const processError = (error46, path5 = []) => {
5181
5314
  var _a, _b;
5182
5315
  for (const issue2 of error46.issues) {
5183
5316
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -5187,7 +5320,7 @@ function treeifyError(error45, _mapper) {
5187
5320
  } else if (issue2.code === "invalid_element") {
5188
5321
  processError({ issues: issue2.issues }, issue2.path);
5189
5322
  } else {
5190
- const fullpath = [...path4, ...issue2.path];
5323
+ const fullpath = [...path5, ...issue2.path];
5191
5324
  if (fullpath.length === 0) {
5192
5325
  result.errors.push(mapper(issue2));
5193
5326
  continue;
@@ -5219,8 +5352,8 @@ function treeifyError(error45, _mapper) {
5219
5352
  }
5220
5353
  function toDotPath(_path) {
5221
5354
  const segs = [];
5222
- const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5223
- for (const seg of path4) {
5355
+ const path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5356
+ for (const seg of path5) {
5224
5357
  if (typeof seg === "number")
5225
5358
  segs.push(`[${seg}]`);
5226
5359
  else if (typeof seg === "symbol")
@@ -16462,8 +16595,8 @@ function truncateToolOutput(content, sessionID, fallback = FALLBACK_MAX_OUTPUT_C
16462
16595
  var import_iconv_lite = __toESM(require_lib(), 1);
16463
16596
  import crypto from "crypto";
16464
16597
  import { createReadStream } from "fs";
16465
- import fs2 from "fs/promises";
16466
- import path2 from "path";
16598
+ import fs3 from "fs/promises";
16599
+ import path3 from "path";
16467
16600
 
16468
16601
  // src/lib/errors.ts
16469
16602
  var GbkToolError = class extends Error {
@@ -16482,26 +16615,26 @@ function createGbkError(code, message, cause) {
16482
16615
  var createTextError = createGbkError;
16483
16616
 
16484
16617
  // src/lib/path-sandbox.ts
16485
- import fs from "fs/promises";
16486
- import path from "path";
16618
+ import fs2 from "fs/promises";
16619
+ import path2 from "path";
16487
16620
  function resolveBaseDirectory(context) {
16488
16621
  const value = context.directory ?? process.cwd();
16489
- return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
16622
+ return path2.isAbsolute(value) ? value : path2.resolve(process.cwd(), value);
16490
16623
  }
16491
16624
  function resolveWorkspaceRoot(context) {
16492
16625
  const value = context.worktree ?? context.directory ?? process.cwd();
16493
- return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
16626
+ return path2.isAbsolute(value) ? value : path2.resolve(process.cwd(), value);
16494
16627
  }
16495
16628
  function resolveCandidatePath(filePath, context) {
16496
- return path.resolve(resolveBaseDirectory(context), filePath);
16629
+ return path2.resolve(resolveBaseDirectory(context), filePath);
16497
16630
  }
16498
16631
  async function resolveExistingAnchor(filePath) {
16499
16632
  let current = filePath;
16500
16633
  while (true) {
16501
16634
  try {
16502
- return await fs.realpath(current);
16635
+ return await fs2.realpath(current);
16503
16636
  } catch {
16504
- const parent = path.dirname(current);
16637
+ const parent = path2.dirname(current);
16505
16638
  if (parent === current) {
16506
16639
  throw createGbkError("GBK_IO_ERROR", `\u65E0\u6CD5\u89E3\u6790\u8DEF\u5F84\u951A\u70B9: ${filePath}`);
16507
16640
  }
@@ -16513,10 +16646,10 @@ async function assertPathAllowed(filePath, context, allowExternal = false) {
16513
16646
  const candidatePath = resolveCandidatePath(filePath, context);
16514
16647
  const workspaceRoot = resolveWorkspaceRoot(context);
16515
16648
  if (!allowExternal) {
16516
- const realWorkspaceRoot = await fs.realpath(workspaceRoot);
16649
+ const realWorkspaceRoot = await fs2.realpath(workspaceRoot);
16517
16650
  const realCandidateAnchor = await resolveExistingAnchor(candidatePath);
16518
- const relative = path.relative(realWorkspaceRoot, realCandidateAnchor);
16519
- if (relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative)) {
16651
+ const relative = path2.relative(realWorkspaceRoot, realCandidateAnchor);
16652
+ if (relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative)) {
16520
16653
  return { candidatePath, workspaceRoot };
16521
16654
  }
16522
16655
  throw createGbkError("GBK_PATH_OUTSIDE_ROOT", `\u76EE\u6807\u8DEF\u5F84\u8D85\u51FA\u5DE5\u4F5C\u76EE\u5F55\u8303\u56F4: ${candidatePath}`);
@@ -16538,8 +16671,8 @@ function buildGbkLineDiffPreview(filePath, encoding, beforeText, afterText) {
16538
16671
  const afterLines = normalizeNewlines(afterText).split("\n");
16539
16672
  const maxLines = Math.max(beforeLines.length, afterLines.length);
16540
16673
  const header = [
16541
- `${ANSI_DIM}--- ${path2.basename(filePath)} (${encoding})${ANSI_RESET}`,
16542
- `${ANSI_DIM}+++ ${path2.basename(filePath)} (${encoding})${ANSI_RESET}`
16674
+ `${ANSI_DIM}--- ${path3.basename(filePath)} (${encoding})${ANSI_RESET}`,
16675
+ `${ANSI_DIM}+++ ${path3.basename(filePath)} (${encoding})${ANSI_RESET}`
16543
16676
  ];
16544
16677
  const body = [];
16545
16678
  for (let index = 0; index < maxLines; index += 1) {
@@ -16585,45 +16718,103 @@ function assertInsertArguments(input) {
16585
16718
  throw createGbkError("GBK_INVALID_ARGUMENT", "content \u4E0D\u80FD\u4E3A\u7A7A");
16586
16719
  }
16587
16720
  if (input.replaceAll !== void 0) {
16588
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\uFFFD? replaceAll");
16721
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\u6301 replaceAll");
16589
16722
  }
16590
16723
  if (input.startLine !== void 0 || input.endLine !== void 0 || input.startAnchor !== void 0 || input.endAnchor !== void 0) {
16591
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\uFFFD? startLine/endLine/startAnchor/endAnchor");
16724
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\u6301 startLine/endLine/startAnchor/endAnchor");
16592
16725
  }
16593
16726
  }
16594
- function findOccurrenceIndex(text, token, occurrence) {
16595
- assertStringArgument(text, "text");
16596
- assertStringArgument(token, "anchor");
16597
- assertPositiveInteger(occurrence, "occurrence");
16727
+ function collectOccurrencePositions(text, token) {
16598
16728
  if (token.length === 0) {
16599
- throw createGbkError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
16729
+ return [];
16600
16730
  }
16601
- let count = 0;
16731
+ const positions = [];
16602
16732
  let searchFrom = 0;
16603
16733
  while (true) {
16604
16734
  const index = text.indexOf(token, searchFrom);
16605
16735
  if (index === -1) {
16606
- break;
16607
- }
16608
- count += 1;
16609
- if (count === occurrence) {
16610
- return { index, total: countOccurrences(text, token) };
16736
+ return positions;
16611
16737
  }
16738
+ positions.push(index);
16612
16739
  searchFrom = index + token.length;
16613
16740
  }
16614
- if (count === 0) {
16615
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\uFFFD?: ${token}`);
16741
+ }
16742
+ function buildFlexibleSearchVariants(token, newlineStyle) {
16743
+ const variants = /* @__PURE__ */ new Set();
16744
+ const pushVariant = (value) => {
16745
+ if (typeof value !== "string" || value.length === 0) {
16746
+ return;
16747
+ }
16748
+ variants.add(value);
16749
+ };
16750
+ pushVariant(token);
16751
+ pushVariant(alignTextToNewlineStyle(token, newlineStyle));
16752
+ const prefixedBlock = parseLineNumberPrefixedBlock(token);
16753
+ if (prefixedBlock) {
16754
+ pushVariant(prefixedBlock.strippedText);
16755
+ pushVariant(alignTextToNewlineStyle(prefixedBlock.strippedText, newlineStyle));
16756
+ }
16757
+ const trimmedLines = trimTrailingEmptyLines(splitNormalizedLines(token));
16758
+ if (trimmedLines.length > 0) {
16759
+ const trimmedToken = trimmedLines.join("\n");
16760
+ pushVariant(trimmedToken);
16761
+ pushVariant(alignTextToNewlineStyle(trimmedToken, newlineStyle));
16762
+ const trimmedPrefixedBlock = parseLineNumberPrefixedBlock(trimmedToken);
16763
+ if (trimmedPrefixedBlock) {
16764
+ pushVariant(trimmedPrefixedBlock.strippedText);
16765
+ pushVariant(alignTextToNewlineStyle(trimmedPrefixedBlock.strippedText, newlineStyle));
16766
+ }
16767
+ }
16768
+ return [...variants];
16769
+ }
16770
+ function findNextFlexibleMatch(text, token, searchFrom, newlineStyle) {
16771
+ let bestMatch = null;
16772
+ for (const candidate of buildFlexibleSearchVariants(token, newlineStyle)) {
16773
+ const index = text.indexOf(candidate, searchFrom);
16774
+ if (index === -1) {
16775
+ continue;
16776
+ }
16777
+ if (bestMatch === null || index < bestMatch.index || index === bestMatch.index && candidate.length > bestMatch.matchedToken.length) {
16778
+ bestMatch = { index, matchedToken: candidate };
16779
+ }
16780
+ }
16781
+ return bestMatch;
16782
+ }
16783
+ function findFlexibleOccurrenceIndex(text, token, occurrence, newlineStyle) {
16784
+ assertStringArgument(text, "text");
16785
+ assertStringArgument(token, "anchor");
16786
+ assertPositiveInteger(occurrence, "occurrence");
16787
+ if (token.length === 0) {
16788
+ throw createGbkError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
16789
+ }
16790
+ let maxMatches = 0;
16791
+ for (const candidate of buildFlexibleSearchVariants(token, newlineStyle)) {
16792
+ const positions = collectOccurrencePositions(text, candidate);
16793
+ if (positions.length === 0) {
16794
+ continue;
16795
+ }
16796
+ maxMatches = Math.max(maxMatches, positions.length);
16797
+ if (positions.length >= occurrence) {
16798
+ return {
16799
+ index: positions[occurrence - 1],
16800
+ total: positions.length,
16801
+ matchedToken: candidate
16802
+ };
16803
+ }
16804
+ }
16805
+ if (maxMatches === 0) {
16806
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${token}`);
16616
16807
  }
16617
- throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\uFFFD? ${count} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\uFFFD? ${occurrence} \u5904`);
16808
+ throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\u5230 ${maxMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${occurrence} \u5904`);
16618
16809
  }
16619
16810
  function insertByAnchor(text, mode, anchor, content, occurrence, ifExists, newlineStyle) {
16620
16811
  const alignedContent = newlineStyle === "crlf" ? content.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : newlineStyle === "lf" ? content.replace(/\r\n/g, "\n") : content;
16621
- const located = findOccurrenceIndex(text, anchor, occurrence);
16622
- const insertionPoint = mode === "insertAfter" ? located.index + anchor.length : located.index;
16812
+ const located = findFlexibleOccurrenceIndex(text, anchor, occurrence, newlineStyle);
16813
+ const insertionPoint = mode === "insertAfter" ? located.index + located.matchedToken.length : located.index;
16623
16814
  const alreadyExists = mode === "insertAfter" ? text.slice(insertionPoint, insertionPoint + alignedContent.length) === alignedContent : text.slice(Math.max(0, insertionPoint - alignedContent.length), insertionPoint) === alignedContent;
16624
16815
  if (alreadyExists) {
16625
16816
  if (ifExists === "error") {
16626
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\uFFFD?");
16817
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
16627
16818
  }
16628
16819
  if (ifExists === "skip") {
16629
16820
  return {
@@ -16664,7 +16855,7 @@ function normalizeOptionalPositiveInteger(value, name) {
16664
16855
  }
16665
16856
  function assertNotBinary(buffer) {
16666
16857
  if (buffer.includes(0)) {
16667
- throw createGbkError("GBK_BINARY_FILE", "\u7591\u4F3C\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u65E0\u6CD5\uFFFD? GBK \u6587\u672C\u5904\u7406");
16858
+ throw createGbkError("GBK_BINARY_FILE", "\u7591\u4F3C\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u65E0\u6CD5\u6309 GBK \u6587\u672C\u5904\u7406");
16668
16859
  }
16669
16860
  }
16670
16861
  function splitLinesWithNumbers(text, offset = 1, limit = 2e3) {
@@ -16791,19 +16982,15 @@ function applyAnchors(text, startAnchor, endAnchor) {
16791
16982
  }
16792
16983
  let rangeStart = 0;
16793
16984
  let rangeEnd = text.length;
16985
+ const newlineStyle = detectNewlineStyle(text);
16794
16986
  if (startAnchor) {
16795
- const found = text.indexOf(startAnchor);
16796
- if (found === -1) {
16797
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8D77\u59CB\u951A\uFFFD?: ${startAnchor}`);
16798
- }
16799
- rangeStart = found + startAnchor.length;
16987
+ const found = findFlexibleOccurrenceIndex(text, startAnchor, 1, newlineStyle);
16988
+ rangeStart = found.index + found.matchedToken.length;
16800
16989
  }
16801
16990
  if (endAnchor) {
16802
- const found = text.indexOf(endAnchor, rangeStart);
16803
- if (found === -1) {
16804
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u7ED3\u675F\u951A\uFFFD?: ${endAnchor}`);
16805
- }
16806
- rangeEnd = found;
16991
+ const searchText = text.slice(rangeStart);
16992
+ const found = findFlexibleOccurrenceIndex(searchText, endAnchor, 1, newlineStyle);
16993
+ rangeEnd = rangeStart + found.index;
16807
16994
  }
16808
16995
  if (rangeEnd < rangeStart) {
16809
16996
  throw createGbkError("GBK_INVALID_ARGUMENT", "\u951A\u70B9\u8303\u56F4\u65E0\u6548");
@@ -16854,9 +17041,9 @@ function getNearestContext(content, oldString) {
16854
17041
  }
16855
17042
  function buildNoMatchMessage(content, oldString) {
16856
17043
  return [
16857
- "\u672A\u627E\u5230\u9700\u8981\u66FF\u6362\u7684\u6587\u672C\uFFFD?",
16858
- "oldString \u5FC5\u987B\u4E0E\u6587\u4EF6\u5B9E\u9645\u5185\u5BB9\u5B8C\u5168\u5BF9\u5E94\uFF0C\u6216\u4EC5\u5728\u6362\uFFFD?/\u5C3E\u968F\u7A7A\u884C\u4E0A\u5B58\u5728\u8F7B\u5FAE\u5DEE\u5F02\uFFFD?",
16859
- "\u6700\u63A5\u8FD1\u7684\u4E0A\u4E0B\u6587\uFFFD?",
17044
+ "\u672A\u627E\u5230\u9700\u8981\u66FF\u6362\u7684\u6587\u672C\u3002",
17045
+ "oldString \u5FC5\u987B\u4E0E\u6587\u4EF6\u5B9E\u9645\u5185\u5BB9\u5B8C\u5168\u5BF9\u5E94\uFF0C\u6216\u4EC5\u5728\u6362\u884C/\u5C3E\u968F\u7A7A\u884C\u4E0A\u5B58\u5728\u8F7B\u5FAE\u5DEE\u5F02\u3002",
17046
+ "\u6700\u63A5\u8FD1\u7684\u4E0A\u4E0B\u6587\uFF1A",
16860
17047
  getNearestContext(content, oldString)
16861
17048
  ].join("\n");
16862
17049
  }
@@ -16868,8 +17055,10 @@ function stripLineNumberPrefixes(lines) {
16868
17055
  return lines.map((l) => l.replace(/^\d+: /, ""));
16869
17056
  }
16870
17057
  function parseLineNumberPrefixedBlock(text) {
16871
- const lines = splitNormalizedLines(text);
16872
- if (!hasLineNumberPrefixes(lines)) {
17058
+ const normalizedText = normalizeNewlines(text);
17059
+ const hasTrailingNewline = normalizedText.endsWith("\n");
17060
+ const lines = trimTrailingEmptyLines(splitNormalizedLines(text));
17061
+ if (lines.length === 0 || !hasLineNumberPrefixes(lines)) {
16873
17062
  return null;
16874
17063
  }
16875
17064
  const lineNumbers = [];
@@ -16884,7 +17073,7 @@ function parseLineNumberPrefixedBlock(text) {
16884
17073
  }
16885
17074
  return {
16886
17075
  lineNumbers,
16887
- strippedText: strippedLines.join("\n"),
17076
+ strippedText: `${strippedLines.join("\n")}${hasTrailingNewline ? "\n" : ""}`,
16888
17077
  isContiguous: lineNumbers.every((lineNumber, index) => index === 0 || lineNumber === lineNumbers[index - 1] + 1),
16889
17078
  startLine: lineNumbers[0],
16890
17079
  endLine: lineNumbers[lineNumbers.length - 1]
@@ -16977,12 +17166,12 @@ async function resolveReadableGbkFile(input) {
16977
17166
  const { candidatePath } = await assertPathAllowed(input.filePath, input.context, input.allowExternal ?? false);
16978
17167
  let stat;
16979
17168
  try {
16980
- stat = await fs2.stat(candidatePath);
17169
+ stat = await fs3.stat(candidatePath);
16981
17170
  } catch (error45) {
16982
- throw createGbkError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\uFFFD?: ${candidatePath}`, error45);
17171
+ throw createGbkError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\u5728: ${candidatePath}`, error45);
16983
17172
  }
16984
17173
  if (stat.isDirectory()) {
16985
- throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\uFFFD?: ${candidatePath}`);
17174
+ throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\u5F55: ${candidatePath}`);
16986
17175
  }
16987
17176
  return {
16988
17177
  filePath: candidatePath,
@@ -16992,7 +17181,7 @@ async function resolveReadableGbkFile(input) {
16992
17181
  }
16993
17182
  async function readWholeGbkTextFile(input) {
16994
17183
  try {
16995
- const buffer = await fs2.readFile(input.filePath);
17184
+ const buffer = await fs3.readFile(input.filePath);
16996
17185
  const content = await readBufferAsText(buffer, input.encoding);
16997
17186
  return {
16998
17187
  filePath: input.filePath,
@@ -17136,7 +17325,7 @@ async function appendEncodedText(filePath, encoding, text) {
17136
17325
  return 0;
17137
17326
  }
17138
17327
  const buffer = import_iconv_lite.default.encode(text, encoding);
17139
- await fs2.appendFile(filePath, buffer);
17328
+ await fs3.appendFile(filePath, buffer);
17140
17329
  return buffer.byteLength;
17141
17330
  }
17142
17331
  async function copyFileByteRangeToHandle(sourcePath, handle, start, endExclusive) {
@@ -17187,7 +17376,7 @@ function replaceScopedTextContent(scopeText, oldString, newString, replaceAll, n
17187
17376
  } else if (occurrencesBefore === 0) {
17188
17377
  throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scopeText, oldString));
17189
17378
  } else if (occurrencesBefore > 1) {
17190
- throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\uFFFD?: ${oldString}`);
17379
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${oldString}`);
17191
17380
  }
17192
17381
  const alignedNewString = alignTextToNewlineStyle(newString, newlineStyle);
17193
17382
  return {
@@ -17215,11 +17404,11 @@ async function replaceLargeGbkFileTextInLineRange(input) {
17215
17404
  input.replaceAll,
17216
17405
  lineIndex.newlineStyle
17217
17406
  );
17218
- const tempPath = path2.join(
17219
- path2.dirname(input.filePath),
17220
- `${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17407
+ const tempPath = path3.join(
17408
+ path3.dirname(input.filePath),
17409
+ `${path3.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17221
17410
  );
17222
- const handle = await fs2.open(tempPath, "w");
17411
+ const handle = await fs3.open(tempPath, "w");
17223
17412
  let bytesWritten = 0;
17224
17413
  try {
17225
17414
  bytesWritten += await copyFileByteRangeToHandle(input.filePath, handle, 0, rangeStart);
@@ -17227,8 +17416,8 @@ async function replaceLargeGbkFileTextInLineRange(input) {
17227
17416
  bytesWritten += await copyFileByteRangeToHandle(input.filePath, handle, rangeEnd, toSafeNumber(input.stat.size));
17228
17417
  await handle.close();
17229
17418
  const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
17230
- await fs2.chmod(tempPath, mode);
17231
- await fs2.rename(tempPath, input.filePath);
17419
+ await fs3.chmod(tempPath, mode);
17420
+ await fs3.rename(tempPath, input.filePath);
17232
17421
  return {
17233
17422
  mode: "replace",
17234
17423
  filePath: input.filePath,
@@ -17240,19 +17429,22 @@ async function replaceLargeGbkFileTextInLineRange(input) {
17240
17429
  };
17241
17430
  } catch (error45) {
17242
17431
  await handle.close().catch(() => void 0);
17243
- await fs2.rm(tempPath, { force: true }).catch(() => void 0);
17432
+ await fs3.rm(tempPath, { force: true }).catch(() => void 0);
17244
17433
  throw error45;
17245
17434
  }
17246
17435
  }
17247
17436
  async function replaceLargeGbkFileByAnchor(input) {
17248
- const tempPath = path2.join(
17249
- path2.dirname(input.filePath),
17250
- `${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17437
+ const lineIndex = await getGbkLineIndex(input);
17438
+ const newlineStyle = lineIndex.newlineStyle;
17439
+ const tempPath = path3.join(
17440
+ path3.dirname(input.filePath),
17441
+ `${path3.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17251
17442
  );
17252
- const handle = await fs2.open(tempPath, "w");
17253
- const alignedContent = input.content.replace(/\r\n/g, "\n");
17254
- const anchorLength = input.anchor.length;
17255
- const carryLength = Math.max(anchorLength + alignedContent.length, 1);
17443
+ const handle = await fs3.open(tempPath, "w");
17444
+ const alignedContent = alignTextToNewlineStyle(input.content, newlineStyle);
17445
+ const anchorVariants = buildFlexibleSearchVariants(input.anchor, newlineStyle);
17446
+ const maxAnchorLength = anchorVariants.reduce((maxLength, candidate) => Math.max(maxLength, candidate.length), input.anchor.length);
17447
+ const carryLength = Math.max(maxAnchorLength + alignedContent.length, 1);
17256
17448
  let decoded = "";
17257
17449
  let scanFrom = 0;
17258
17450
  let totalMatches = 0;
@@ -17276,23 +17468,23 @@ async function replaceLargeGbkFileByAnchor(input) {
17276
17468
  await visitDecodedTextChunks(input, async (text) => {
17277
17469
  decoded += text;
17278
17470
  while (!inserted) {
17279
- const foundAt = decoded.indexOf(input.anchor, scanFrom);
17280
- if (foundAt === -1) {
17471
+ const located = findNextFlexibleMatch(decoded, input.anchor, scanFrom, newlineStyle);
17472
+ if (located === null) {
17281
17473
  break;
17282
17474
  }
17283
17475
  totalMatches += 1;
17284
- const afterAnchor = foundAt + anchorLength;
17476
+ const afterAnchor = located.index + located.matchedToken.length;
17285
17477
  if (totalMatches !== input.occurrence) {
17286
17478
  scanFrom = afterAnchor;
17287
17479
  continue;
17288
17480
  }
17289
- const before = decoded.slice(0, foundAt);
17481
+ const before = decoded.slice(0, located.index);
17290
17482
  const after = decoded.slice(afterAnchor);
17291
- const anchorAndAfter = decoded.slice(foundAt);
17483
+ const anchorAndAfter = decoded.slice(located.index);
17292
17484
  const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
17293
17485
  if (alreadyExists) {
17294
17486
  if (input.ifExists === "error") {
17295
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\uFFFD?");
17487
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
17296
17488
  }
17297
17489
  if (input.ifExists === "skip") {
17298
17490
  skipped = true;
@@ -17304,7 +17496,7 @@ async function replaceLargeGbkFileByAnchor(input) {
17304
17496
  }
17305
17497
  if (input.mode === "insertAfter") {
17306
17498
  bytesWritten += await writeEncodedText(handle, input.encoding, before);
17307
- bytesWritten += await writeEncodedText(handle, input.encoding, input.anchor);
17499
+ bytesWritten += await writeEncodedText(handle, input.encoding, located.matchedToken);
17308
17500
  bytesWritten += await writeEncodedText(handle, input.encoding, alignedContent);
17309
17501
  bytesWritten += await writeEncodedText(handle, input.encoding, after);
17310
17502
  } else {
@@ -17323,23 +17515,23 @@ async function replaceLargeGbkFileByAnchor(input) {
17323
17515
  });
17324
17516
  if (!inserted) {
17325
17517
  while (true) {
17326
- const foundAt = decoded.indexOf(input.anchor, scanFrom);
17327
- if (foundAt === -1) {
17518
+ const located = findNextFlexibleMatch(decoded, input.anchor, scanFrom, newlineStyle);
17519
+ if (located === null) {
17328
17520
  break;
17329
17521
  }
17330
17522
  totalMatches += 1;
17331
- const afterAnchor = foundAt + anchorLength;
17523
+ const afterAnchor = located.index + located.matchedToken.length;
17332
17524
  if (totalMatches !== input.occurrence) {
17333
17525
  scanFrom = afterAnchor;
17334
17526
  continue;
17335
17527
  }
17336
- const before = decoded.slice(0, foundAt);
17528
+ const before = decoded.slice(0, located.index);
17337
17529
  const after = decoded.slice(afterAnchor);
17338
- const anchorAndAfter = decoded.slice(foundAt);
17530
+ const anchorAndAfter = decoded.slice(located.index);
17339
17531
  const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
17340
17532
  if (alreadyExists) {
17341
17533
  if (input.ifExists === "error") {
17342
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\uFFFD?");
17534
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
17343
17535
  }
17344
17536
  if (input.ifExists === "skip") {
17345
17537
  skipped = true;
@@ -17347,22 +17539,22 @@ async function replaceLargeGbkFileByAnchor(input) {
17347
17539
  break;
17348
17540
  }
17349
17541
  }
17350
- decoded = input.mode === "insertAfter" ? `${before}${input.anchor}${alignedContent}${after}` : `${before}${alignedContent}${anchorAndAfter}`;
17542
+ decoded = input.mode === "insertAfter" ? `${before}${located.matchedToken}${alignedContent}${after}` : `${before}${alignedContent}${anchorAndAfter}`;
17351
17543
  inserted = true;
17352
17544
  break;
17353
17545
  }
17354
17546
  }
17355
17547
  if (!inserted && totalMatches === 0) {
17356
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\uFFFD?: ${input.anchor}`);
17548
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${input.anchor}`);
17357
17549
  }
17358
17550
  if (!inserted && totalMatches > 0) {
17359
- throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${input.anchor} \u53EA\u627E\uFFFD? ${totalMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\uFFFD? ${input.occurrence} \u5904`);
17551
+ throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${input.anchor} \u53EA\u627E\u5230 ${totalMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${input.occurrence} \u5904`);
17360
17552
  }
17361
17553
  await finalizeInserted();
17362
17554
  await handle.close();
17363
17555
  const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
17364
- await fs2.chmod(tempPath, mode);
17365
- await fs2.rename(tempPath, input.filePath);
17556
+ await fs3.chmod(tempPath, mode);
17557
+ await fs3.rename(tempPath, input.filePath);
17366
17558
  invalidateGbkLineIndex(input.filePath);
17367
17559
  return {
17368
17560
  mode: input.mode,
@@ -17378,40 +17570,62 @@ async function replaceLargeGbkFileByAnchor(input) {
17378
17570
  };
17379
17571
  } catch (error45) {
17380
17572
  await handle.close().catch(() => void 0);
17381
- await fs2.rm(tempPath, { force: true }).catch(() => void 0);
17573
+ await fs3.rm(tempPath, { force: true }).catch(() => void 0);
17382
17574
  throw error45;
17383
17575
  }
17384
17576
  }
17385
17577
  async function replaceLargeGbkFileText(input) {
17386
- const tempPath = path2.join(
17387
- path2.dirname(input.filePath),
17388
- `${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17578
+ const lineIndex = await getGbkLineIndex(input);
17579
+ const newlineStyle = lineIndex.newlineStyle;
17580
+ const tempPath = path3.join(
17581
+ path3.dirname(input.filePath),
17582
+ `${path3.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17389
17583
  );
17390
- const handle = await fs2.open(tempPath, "w");
17584
+ const handle = await fs3.open(tempPath, "w");
17391
17585
  const carryLength = Math.max(input.oldString.length - 1, 0);
17586
+ const alignedNewString = alignTextToNewlineStyle(input.newString, newlineStyle);
17392
17587
  let carry = "";
17393
17588
  let occurrencesBefore = 0;
17394
17589
  let bytesWritten = 0;
17590
+ let replacedFirstMatch = false;
17395
17591
  const flushText = async (text, flush = false) => {
17396
17592
  const combined = carry + text;
17397
- const splitAt = flush ? combined.length : Math.max(0, combined.length - carryLength);
17398
- const processable = combined.slice(0, splitAt);
17399
- carry = combined.slice(splitAt);
17400
- if (processable.length === 0) {
17593
+ if (combined.length === 0) {
17401
17594
  return;
17402
17595
  }
17403
- const matchCount = countOccurrences(processable, input.oldString);
17404
- const seenBefore = occurrencesBefore;
17405
- occurrencesBefore += matchCount;
17406
- let output = processable;
17407
- if (input.replaceAll) {
17408
- if (matchCount > 0) {
17409
- output = processable.split(input.oldString).join(input.newString);
17596
+ const safeEnd = flush ? combined.length : Math.max(0, combined.length - carryLength);
17597
+ const outputParts = [];
17598
+ let cursor = 0;
17599
+ let flushUpto = safeEnd;
17600
+ while (true) {
17601
+ const index = combined.indexOf(input.oldString, cursor);
17602
+ if (index === -1) {
17603
+ break;
17604
+ }
17605
+ const matchEnd = index + input.oldString.length;
17606
+ if (!flush && matchEnd > safeEnd) {
17607
+ flushUpto = index;
17608
+ break;
17609
+ }
17610
+ occurrencesBefore += 1;
17611
+ outputParts.push(combined.slice(cursor, index));
17612
+ if (input.replaceAll) {
17613
+ outputParts.push(alignedNewString);
17614
+ } else if (!replacedFirstMatch) {
17615
+ outputParts.push(alignedNewString);
17616
+ replacedFirstMatch = true;
17617
+ } else {
17618
+ outputParts.push(input.oldString);
17410
17619
  }
17411
- } else if (seenBefore === 0 && matchCount > 0) {
17412
- output = processable.replace(input.oldString, input.newString);
17620
+ cursor = matchEnd;
17413
17621
  }
17414
- bytesWritten += await writeEncodedText(handle, input.encoding, output);
17622
+ outputParts.push(combined.slice(cursor, flushUpto));
17623
+ carry = combined.slice(flushUpto);
17624
+ const processable = outputParts.join("");
17625
+ if (processable.length === 0) {
17626
+ return;
17627
+ }
17628
+ bytesWritten += await writeEncodedText(handle, input.encoding, processable);
17415
17629
  };
17416
17630
  try {
17417
17631
  await visitDecodedTextChunks(input, async (text) => {
@@ -17419,15 +17633,15 @@ async function replaceLargeGbkFileText(input) {
17419
17633
  });
17420
17634
  await flushText("", true);
17421
17635
  if (occurrencesBefore === 0) {
17422
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\uFFFD?: ${input.oldString}`);
17636
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\u5BB9: ${input.oldString}`);
17423
17637
  }
17424
17638
  if (!input.replaceAll && occurrencesBefore > 1) {
17425
- throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\uFFFD?: ${input.oldString}`);
17639
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
17426
17640
  }
17427
17641
  await handle.close();
17428
17642
  const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
17429
- await fs2.chmod(tempPath, mode);
17430
- await fs2.rename(tempPath, input.filePath);
17643
+ await fs3.chmod(tempPath, mode);
17644
+ await fs3.rename(tempPath, input.filePath);
17431
17645
  invalidateGbkLineIndex(input.filePath);
17432
17646
  return {
17433
17647
  filePath: input.filePath,
@@ -17439,7 +17653,7 @@ async function replaceLargeGbkFileText(input) {
17439
17653
  };
17440
17654
  } catch (error45) {
17441
17655
  await handle.close().catch(() => void 0);
17442
- await fs2.rm(tempPath, { force: true }).catch(() => void 0);
17656
+ await fs3.rm(tempPath, { force: true }).catch(() => void 0);
17443
17657
  throw error45;
17444
17658
  }
17445
17659
  }
@@ -17567,7 +17781,7 @@ async function replaceGbkFileText(input) {
17567
17781
  };
17568
17782
  }
17569
17783
  const buffer2 = import_iconv_lite.default.encode(insertResult.outputText, current2.encoding);
17570
- await fs2.writeFile(current2.filePath, buffer2);
17784
+ await fs3.writeFile(current2.filePath, buffer2);
17571
17785
  return {
17572
17786
  mode,
17573
17787
  filePath: current2.filePath,
@@ -17627,7 +17841,7 @@ async function replaceGbkFileText(input) {
17627
17841
  if (loose !== null) {
17628
17842
  const outputText2 = `${current.content.slice(0, scope.rangeStart)}${loose.content}${current.content.slice(scope.rangeEnd)}`;
17629
17843
  const buffer2 = import_iconv_lite.default.encode(outputText2, current.encoding);
17630
- await fs2.writeFile(current.filePath, buffer2);
17844
+ await fs3.writeFile(current.filePath, buffer2);
17631
17845
  invalidateGbkLineIndex(current.filePath);
17632
17846
  return {
17633
17847
  mode: "replace",
@@ -17648,14 +17862,14 @@ async function replaceGbkFileText(input) {
17648
17862
  } else if (occurrencesBefore === 0) {
17649
17863
  throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, effectiveOldString));
17650
17864
  } else if (occurrencesBefore > 1) {
17651
- throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\uFFFD?: ${effectiveOldString}`);
17865
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${effectiveOldString}`);
17652
17866
  }
17653
17867
  const fileNewlineStyle = detectNewlineStyle(current.content);
17654
17868
  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;
17655
17869
  const replaced = replaceAll ? scope.selectedText.split(effectiveOldString).join(alignedNewString) : scope.selectedText.replace(effectiveOldString, alignedNewString);
17656
17870
  const outputText = `${current.content.slice(0, scope.rangeStart)}${replaced}${current.content.slice(scope.rangeEnd)}`;
17657
17871
  const buffer = import_iconv_lite.default.encode(outputText, current.encoding);
17658
- await fs2.writeFile(current.filePath, buffer);
17872
+ await fs3.writeFile(current.filePath, buffer);
17659
17873
  invalidateGbkLineIndex(current.filePath);
17660
17874
  return {
17661
17875
  mode: "replace",
@@ -17740,11 +17954,11 @@ async function writeGbkFile(input) {
17740
17954
  const overwrite = input.overwrite ?? false;
17741
17955
  const append = input.append ?? false;
17742
17956
  const { candidatePath } = await assertPathAllowed(input.filePath, input.context, input.allowExternal ?? false);
17743
- const parent = path2.dirname(candidatePath);
17957
+ const parent = path3.dirname(candidatePath);
17744
17958
  assertEncodingSupported(encoding);
17745
17959
  if (append) {
17746
17960
  try {
17747
- const parentStat = await fs2.stat(parent);
17961
+ const parentStat = await fs3.stat(parent);
17748
17962
  if (!parentStat.isDirectory()) {
17749
17963
  throw createGbkError("GBK_PARENT_DIRECTORY_MISSING", `\u7236\u76EE\u5F55\u4E0D\u5B58\u5728: ${parent}`);
17750
17964
  }
@@ -17753,14 +17967,14 @@ async function writeGbkFile(input) {
17753
17967
  if (!createDirectories) {
17754
17968
  throw createGbkError("GBK_PARENT_DIRECTORY_MISSING", `\u7236\u76EE\u5F55\u4E0D\u5B58\u5728: ${parent}`);
17755
17969
  }
17756
- await fs2.mkdir(parent, { recursive: true });
17970
+ await fs3.mkdir(parent, { recursive: true });
17757
17971
  } else if (error45 instanceof Error && "code" in error45) {
17758
17972
  throw error45;
17759
17973
  }
17760
17974
  }
17761
17975
  let existed = false;
17762
17976
  try {
17763
- await fs2.stat(candidatePath);
17977
+ await fs3.stat(candidatePath);
17764
17978
  existed = true;
17765
17979
  } catch (error45) {
17766
17980
  if (!(error45 instanceof Error && "code" in error45 && error45.code === "ENOENT")) {
@@ -17779,12 +17993,12 @@ async function writeGbkFile(input) {
17779
17993
  };
17780
17994
  }
17781
17995
  try {
17782
- const stat = await fs2.stat(candidatePath);
17996
+ const stat = await fs3.stat(candidatePath);
17783
17997
  if (stat.isDirectory()) {
17784
- throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\uFFFD?: ${candidatePath}`);
17998
+ throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\u5F55: ${candidatePath}`);
17785
17999
  }
17786
18000
  if (!overwrite) {
17787
- throw createGbkError("GBK_FILE_EXISTS", `\u76EE\u6807\u6587\u4EF6\u5DF2\u5B58\uFFFD?: ${candidatePath}`);
18001
+ throw createGbkError("GBK_FILE_EXISTS", `\u76EE\u6807\u6587\u4EF6\u5DF2\u5B58\u5728: ${candidatePath}`);
17788
18002
  }
17789
18003
  } catch (error45) {
17790
18004
  if (error45 instanceof Error && "code" in error45) {
@@ -17796,7 +18010,7 @@ async function writeGbkFile(input) {
17796
18010
  }
17797
18011
  }
17798
18012
  try {
17799
- const parentStat = await fs2.stat(parent);
18013
+ const parentStat = await fs3.stat(parent);
17800
18014
  if (!parentStat.isDirectory()) {
17801
18015
  throw createGbkError("GBK_PARENT_DIRECTORY_MISSING", `\u7236\u76EE\u5F55\u4E0D\u5B58\u5728: ${parent}`);
17802
18016
  }
@@ -17805,15 +18019,15 @@ async function writeGbkFile(input) {
17805
18019
  if (!createDirectories) {
17806
18020
  throw createGbkError("GBK_PARENT_DIRECTORY_MISSING", `\u7236\u76EE\u5F55\u4E0D\u5B58\u5728: ${parent}`);
17807
18021
  }
17808
- await fs2.mkdir(parent, { recursive: true });
18022
+ await fs3.mkdir(parent, { recursive: true });
17809
18023
  } else if (error45 instanceof Error && "code" in error45) {
17810
18024
  throw error45;
17811
18025
  }
17812
18026
  }
17813
18027
  try {
17814
- const existed = await fs2.stat(candidatePath).then(() => true).catch(() => false);
18028
+ const existed = await fs3.stat(candidatePath).then(() => true).catch(() => false);
17815
18029
  const buffer = import_iconv_lite.default.encode(input.content, encoding);
17816
- await fs2.writeFile(candidatePath, buffer);
18030
+ await fs3.writeFile(candidatePath, buffer);
17817
18031
  invalidateGbkLineIndex(candidatePath);
17818
18032
  return {
17819
18033
  filePath: candidatePath,
@@ -18034,13 +18248,16 @@ var TEXT_TOOL_SYSTEM_MARKER = "[opencode-gbk-tools:text-rules]";
18034
18248
  var TEXT_TOOL_SYSTEM_PROMPT = [
18035
18249
  TEXT_TOOL_SYSTEM_MARKER,
18036
18250
  "\u6587\u672C\u6587\u4EF6\u5904\u7406\u89C4\u5219\uFF1A",
18037
- "- \u5904\u7406\u6587\u672C\u6587\u4EF6\u65F6\uFF0C\u4F18\u5148\u4F7F\u7528 text_read\u3001text_write\u3001text_edit\u3002",
18038
- "- text_* \u9ED8\u8BA4\u4F1A\u81EA\u52A8\u8BC6\u522B\u73B0\u6709\u6587\u4EF6\u7F16\u7801\uFF0C\u5E76\u5728\u4FEE\u6539\u65F6\u5C3D\u91CF\u4FDD\u6301\u539F\u7F16\u7801\u3001BOM \u548C\u6362\u884C\u98CE\u683C\u3002",
18251
+ "- \u666E\u901A UTF-8 / UTF-8 BOM / UTF-16 \u6587\u672C\uFF0C\u4F18\u5148\u4F7F\u7528 OpenCode \u5185\u7F6E read\u3001write\u3001edit\u3002",
18252
+ "- \u9047\u5230 GBK / GB18030 \u6587\u4EF6\u3001\u4E2D\u6587\u4E71\u7801\u3001\u975E UTF-8 \u65E7\u6587\u672C\uFF0C\u4F18\u5148\u4F7F\u7528 gbk_read\u3001gbk_write\u3001gbk_edit\u3001gbk_search\u3002",
18039
18253
  "- \u65B0\u5EFA .txt \u6587\u4EF6\u5728 encoding=auto \u4E0B\u9ED8\u8BA4\u4F7F\u7528 GBK\uFF1B\u5176\u4ED6\u65B0\u6587\u4EF6\u8BF7\u663E\u5F0F\u6307\u5B9A encoding\u3002",
18254
+ "- \u65E0\u6CD5\u786E\u5B9A\u7F16\u7801\u65F6\uFF0C\u5148\u5C1D\u8BD5\u5185\u7F6E read\uFF1B\u82E5\u51FA\u73B0\u4E2D\u6587\u4E71\u7801\u3001\u66FF\u6362\u5931\u8D25\u6216\u7F16\u7801\u98CE\u9669\uFF0C\u518D\u5207\u6362\u5230 gbk_*\u3002",
18255
+ "- \u5DF2\u786E\u8BA4\u662F GBK/GB18030 \u7684\u6587\u4EF6\u4F1A\u88AB\u63D2\u4EF6\u6301\u4E45\u8BB0\u5FC6\uFF1B\u518D\u6B21\u64CD\u4F5C\u540C\u4E00\u8DEF\u5F84\u65F6\uFF0C\u4F18\u5148\u7EE7\u7EED\u6309 GBK \u5904\u7406\u3002",
18040
18256
  "- \u5982\u679C\u610F\u56FE\u662F\u2018\u5728\u67D0\u6807\u7B7E\u524D\u540E\u63D2\u5165\u5185\u5BB9\u2019\uFF0C\u4F18\u5148\u4F7F\u7528 mode=insertAfter \u6216 mode=insertBefore\uFF0C\u5E76\u4F20 anchor/content\u3002",
18041
18257
  "- \u53EA\u6709\u5728\u660E\u786E\u505A\u7CBE\u786E\u66FF\u6362\u65F6\uFF0C\u624D\u4F7F\u7528 oldString/newString\u3002",
18042
- "- \u82E5\u68C0\u6D4B\u7F6E\u4FE1\u5EA6\u4E0D\u8DB3\u6216\u51FA\u73B0 TEXT_UNKNOWN_ENCODING\uFF0C\u8BF7\u663E\u5F0F\u6307\u5B9A encoding \u540E\u91CD\u8BD5\u3002",
18043
- "- \u5904\u7406\u660E\u786E\u7684 GBK/GB18030 \u6587\u4EF6\u65F6\uFF0C\u4E5F\u53EF\u7EE7\u7EED\u4F7F\u7528 gbk_read\u3001gbk_write\u3001gbk_edit\u3001gbk_search\u3002"
18258
+ "- anchor\u3001startAnchor\u3001endAnchor\u3001oldString \u82E5\u76F4\u63A5\u590D\u5236\u81EA\u8BFB\u53D6\u7ED3\u679C\uFF0C\u53EF\u4FDD\u7559 LF \u6362\u884C\uFF1Bgbk_edit / text_edit \u4F1A\u5C3D\u91CF\u6309\u6587\u4EF6\u6362\u884C\u98CE\u683C\u81EA\u52A8\u5BF9\u9F50\u3002",
18259
+ '- \u82E5\u8BFB\u53D6\u7ED3\u679C\u5E26\u6709 "N: " \u884C\u53F7\u524D\u7F00\uFF0Cgbk_edit / text_edit \u4F1A\u5C3D\u91CF\u81EA\u52A8\u5265\u79BB\u8FD9\u4E9B\u524D\u7F00\u540E\u518D\u5339\u914D\u3002',
18260
+ "- gbk-engine \u662F\u5F3A\u5236 GBK \u4E13\u5C5E\u6A21\u5F0F\uFF1A\u53EA\u5141\u8BB8 gbk_*\uFF0C\u4E0D\u8D70\u5185\u7F6E\u8BFB\u5199\u7F16\u8F91\u5DE5\u5177\u3002"
18044
18261
  ].join("\n");
18045
18262
  function appendTextToolSystemPrompt(system) {
18046
18263
  if (system.some((item) => item.includes(TEXT_TOOL_SYSTEM_MARKER))) {
@@ -18059,8 +18276,8 @@ ${TEXT_TOOL_SYSTEM_PROMPT}`;
18059
18276
  var import_iconv_lite2 = __toESM(require_lib(), 1);
18060
18277
  import crypto2 from "crypto";
18061
18278
  import { createReadStream as createReadStream2 } from "fs";
18062
- import fs3 from "fs/promises";
18063
- import path3 from "path";
18279
+ import fs4 from "fs/promises";
18280
+ import path4 from "path";
18064
18281
  var TEXT_STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
18065
18282
  var TEXT_DETECTION_SAMPLE_BYTES = 64 * 1024;
18066
18283
  var UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
@@ -18096,7 +18313,7 @@ function resolveExplicitTextEncoding(value, fallback) {
18096
18313
  return requested === "auto" ? fallback : requested;
18097
18314
  }
18098
18315
  function shouldDefaultNewTextFileToGbk(filePath) {
18099
- return path3.extname(filePath).toLowerCase() === ".txt";
18316
+ return path4.extname(filePath).toLowerCase() === ".txt";
18100
18317
  }
18101
18318
  function getBomPrefix(encoding, hasBom) {
18102
18319
  if (!hasBom) {
@@ -18248,35 +18465,126 @@ function buildNoMatchMessage2(content, oldString) {
18248
18465
  assertStringArgument2(oldString, "oldString");
18249
18466
  return [
18250
18467
  "\u672A\u627E\u5230\u9700\u8981\u66FF\u6362\u7684\u6587\u672C\u3002",
18251
- "oldString \u5FC5\u987B\u4E0E\u6587\u4EF6\u5B9E\u9645\u5185\u5BB9\u5B8C\u5168\u5BF9\u5E94\u3002",
18468
+ "oldString \u5FC5\u987B\u4E0E\u6587\u4EF6\u5B9E\u9645\u5185\u5BB9\u5B8C\u5168\u5BF9\u5E94\uFF0C\u6216\u4EC5\u5728\u6362\u884C/\u5C3E\u968F\u7A7A\u884C\u4E0A\u5B58\u5728\u8F7B\u5FAE\u5DEE\u5F02\u3002",
18252
18469
  "\u6700\u63A5\u8FD1\u7684\u4E0A\u4E0B\u6587\uFF1A",
18253
18470
  getNearestContext2(content, oldString)
18254
18471
  ].join("\n");
18255
18472
  }
18256
- function findOccurrenceIndex2(text, token, occurrence) {
18473
+ function trimRightSpaces2(text) {
18257
18474
  assertStringArgument2(text, "text");
18258
- assertStringArgument2(token, "anchor");
18259
- assertPositiveInteger(occurrence, "occurrence");
18475
+ return text.replace(/[ \t]+$/g, "");
18476
+ }
18477
+ function splitNormalizedLines2(text) {
18478
+ return normalizeNewlines2(text).split("\n");
18479
+ }
18480
+ function trimTrailingEmptyLines2(lines) {
18481
+ const result = [...lines];
18482
+ while (result.length > 0 && trimRightSpaces2(result[result.length - 1]) === "") {
18483
+ result.pop();
18484
+ }
18485
+ return result;
18486
+ }
18487
+ function hasLineNumberPrefixes2(lines) {
18488
+ const nonEmpty = lines.filter((line) => line.trim().length > 0);
18489
+ return nonEmpty.length > 0 && nonEmpty.every((line) => /^\d+: /.test(line));
18490
+ }
18491
+ function stripLineNumberPrefixes2(lines) {
18492
+ return lines.map((line) => line.replace(/^\d+: /, ""));
18493
+ }
18494
+ function parseLineNumberPrefixedBlock2(text) {
18495
+ const normalizedText = normalizeNewlines2(text);
18496
+ const hasTrailingNewline = normalizedText.endsWith("\n");
18497
+ const lines = trimTrailingEmptyLines2(splitNormalizedLines2(text));
18498
+ if (lines.length === 0 || !hasLineNumberPrefixes2(lines)) {
18499
+ return null;
18500
+ }
18501
+ const lineNumbers = [];
18502
+ const strippedLines = [];
18503
+ for (const line of lines) {
18504
+ const match = /^(\d+): ?(.*)$/.exec(line);
18505
+ if (!match) {
18506
+ return null;
18507
+ }
18508
+ lineNumbers.push(Number(match[1]));
18509
+ strippedLines.push(match[2]);
18510
+ }
18511
+ return {
18512
+ lineNumbers,
18513
+ strippedText: `${strippedLines.join("\n")}${hasTrailingNewline ? "\n" : ""}`,
18514
+ isContiguous: lineNumbers.every((lineNumber, index) => index === 0 || lineNumber === lineNumbers[index - 1] + 1),
18515
+ startLine: lineNumbers[0],
18516
+ endLine: lineNumbers[lineNumbers.length - 1]
18517
+ };
18518
+ }
18519
+ function collectOccurrencePositions2(text, token) {
18260
18520
  if (token.length === 0) {
18261
- throw createTextError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
18521
+ return [];
18262
18522
  }
18263
- let count = 0;
18523
+ const positions = [];
18264
18524
  let searchFrom = 0;
18265
18525
  while (true) {
18266
18526
  const index = text.indexOf(token, searchFrom);
18267
18527
  if (index === -1) {
18268
- break;
18269
- }
18270
- count += 1;
18271
- if (count === occurrence) {
18272
- return { index, total: countOccurrences(text, token) };
18528
+ return positions;
18273
18529
  }
18530
+ positions.push(index);
18274
18531
  searchFrom = index + token.length;
18275
18532
  }
18276
- if (count === 0) {
18533
+ }
18534
+ function buildFlexibleSearchVariants2(token, newlineStyle) {
18535
+ const variants = /* @__PURE__ */ new Set();
18536
+ const pushVariant = (value) => {
18537
+ if (typeof value !== "string" || value.length === 0) {
18538
+ return;
18539
+ }
18540
+ variants.add(value);
18541
+ };
18542
+ pushVariant(token);
18543
+ pushVariant(alignTextToNewlineStyle2(token, newlineStyle));
18544
+ const prefixedBlock = parseLineNumberPrefixedBlock2(token);
18545
+ if (prefixedBlock) {
18546
+ pushVariant(prefixedBlock.strippedText);
18547
+ pushVariant(alignTextToNewlineStyle2(prefixedBlock.strippedText, newlineStyle));
18548
+ }
18549
+ const trimmedLines = trimTrailingEmptyLines2(splitNormalizedLines2(token));
18550
+ if (trimmedLines.length > 0) {
18551
+ const trimmedToken = trimmedLines.join("\n");
18552
+ pushVariant(trimmedToken);
18553
+ pushVariant(alignTextToNewlineStyle2(trimmedToken, newlineStyle));
18554
+ const trimmedPrefixedBlock = parseLineNumberPrefixedBlock2(trimmedToken);
18555
+ if (trimmedPrefixedBlock) {
18556
+ pushVariant(trimmedPrefixedBlock.strippedText);
18557
+ pushVariant(alignTextToNewlineStyle2(trimmedPrefixedBlock.strippedText, newlineStyle));
18558
+ }
18559
+ }
18560
+ return [...variants];
18561
+ }
18562
+ function findOccurrenceIndex(text, token, occurrence, newlineStyle) {
18563
+ assertStringArgument2(text, "text");
18564
+ assertStringArgument2(token, "anchor");
18565
+ assertPositiveInteger(occurrence, "occurrence");
18566
+ if (token.length === 0) {
18567
+ throw createTextError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
18568
+ }
18569
+ let maxMatches = 0;
18570
+ for (const candidate of buildFlexibleSearchVariants2(token, newlineStyle)) {
18571
+ const positions = collectOccurrencePositions2(text, candidate);
18572
+ if (positions.length === 0) {
18573
+ continue;
18574
+ }
18575
+ maxMatches = Math.max(maxMatches, positions.length);
18576
+ if (positions.length >= occurrence) {
18577
+ return {
18578
+ index: positions[occurrence - 1],
18579
+ total: positions.length,
18580
+ matchedToken: candidate
18581
+ };
18582
+ }
18583
+ }
18584
+ if (maxMatches === 0) {
18277
18585
  throw createTextError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${token}`);
18278
18586
  }
18279
- throw createTextError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\u5230 ${count} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${occurrence} \u5904`);
18587
+ throw createTextError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\u5230 ${maxMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${occurrence} \u5904`);
18280
18588
  }
18281
18589
  function assertInsertArguments2(input) {
18282
18590
  assertStringArgument2(input.anchor, "anchor");
@@ -18301,8 +18609,8 @@ function buildLineDiffPreview(filePath, encoding, beforeText, afterText) {
18301
18609
  const afterLines = normalizeNewlines2(afterText).split("\n");
18302
18610
  const maxLines = Math.max(beforeLines.length, afterLines.length);
18303
18611
  const header = [
18304
- `${ANSI_DIM2}--- ${path3.basename(filePath)} (${encoding})${ANSI_RESET2}`,
18305
- `${ANSI_DIM2}+++ ${path3.basename(filePath)} (${encoding})${ANSI_RESET2}`
18612
+ `${ANSI_DIM2}--- ${path4.basename(filePath)} (${encoding})${ANSI_RESET2}`,
18613
+ `${ANSI_DIM2}+++ ${path4.basename(filePath)} (${encoding})${ANSI_RESET2}`
18306
18614
  ];
18307
18615
  const body = [];
18308
18616
  for (let index = 0; index < maxLines; index += 1) {
@@ -18328,8 +18636,8 @@ function buildLineDiffPreview(filePath, encoding, beforeText, afterText) {
18328
18636
  }
18329
18637
  function buildInsertOutput(text, mode, anchor, content, occurrence, ifExists, newlineStyle) {
18330
18638
  const alignedContent = alignTextToNewlineStyle2(content, newlineStyle);
18331
- const located = findOccurrenceIndex2(text, anchor, occurrence);
18332
- const insertionPoint = mode === "insertAfter" ? located.index + anchor.length : located.index;
18639
+ const located = findOccurrenceIndex(text, anchor, occurrence, newlineStyle);
18640
+ const insertionPoint = mode === "insertAfter" ? located.index + located.matchedToken.length : located.index;
18333
18641
  const alreadyExists = mode === "insertAfter" ? text.slice(insertionPoint, insertionPoint + alignedContent.length) === alignedContent : text.slice(Math.max(0, insertionPoint - alignedContent.length), insertionPoint) === alignedContent;
18334
18642
  if (alreadyExists) {
18335
18643
  if (ifExists === "error") {
@@ -18343,8 +18651,8 @@ function buildInsertOutput(text, mode, anchor, content, occurrence, ifExists, ne
18343
18651
  anchorMatches: located.total,
18344
18652
  occurrence,
18345
18653
  anchor,
18346
- previewBefore: text.slice(Math.max(0, located.index - 80), Math.min(text.length, located.index + anchor.length + 80)),
18347
- previewAfter: text.slice(Math.max(0, located.index - 80), Math.min(text.length, located.index + anchor.length + 80))
18654
+ previewBefore: text.slice(Math.max(0, located.index - 80), Math.min(text.length, located.index + located.matchedToken.length + 80)),
18655
+ previewAfter: text.slice(Math.max(0, located.index - 80), Math.min(text.length, located.index + located.matchedToken.length + 80))
18348
18656
  };
18349
18657
  }
18350
18658
  }
@@ -18418,7 +18726,7 @@ async function resolveReadableTextFile(input) {
18418
18726
  const { candidatePath } = await assertPathAllowed(input.filePath, input.context, input.allowExternal ?? false);
18419
18727
  let stat;
18420
18728
  try {
18421
- stat = await fs3.stat(candidatePath);
18729
+ stat = await fs4.stat(candidatePath);
18422
18730
  } catch (error45) {
18423
18731
  throw createTextError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\u5728: ${candidatePath}`, error45);
18424
18732
  }
@@ -18428,7 +18736,7 @@ async function resolveReadableTextFile(input) {
18428
18736
  return { filePath: candidatePath, stat };
18429
18737
  }
18430
18738
  async function readDetectionBuffer(filePath, sampleSize = TEXT_DETECTION_SAMPLE_BYTES) {
18431
- const handle = await fs3.open(filePath, "r");
18739
+ const handle = await fs4.open(filePath, "r");
18432
18740
  try {
18433
18741
  const buffer = Buffer.alloc(sampleSize);
18434
18742
  const { bytesRead } = await handle.read(buffer, 0, sampleSize, 0);
@@ -18590,10 +18898,118 @@ async function visitDecodedTextChunks2(resolved, visitor) {
18590
18898
  stream.destroy();
18591
18899
  }
18592
18900
  }
18901
+ async function writeLargeTextFile(filePath, encoding, hasBom, producer) {
18902
+ const tempPath = path4.join(path4.dirname(filePath), `${path4.basename(filePath)}.opencode-text-${crypto2.randomUUID()}.tmp`);
18903
+ const handle = await fs4.open(tempPath, "w");
18904
+ try {
18905
+ if (hasBom) {
18906
+ const bom = getBomPrefix(encoding, hasBom);
18907
+ if (bom.length > 0) {
18908
+ await handle.writeFile(bom);
18909
+ }
18910
+ }
18911
+ const bytesWritten = await producer(handle);
18912
+ await handle.close();
18913
+ await fs4.rename(tempPath, filePath);
18914
+ return bytesWritten + getBomPrefix(encoding, hasBom).length;
18915
+ } catch (error45) {
18916
+ await handle.close().catch(() => void 0);
18917
+ await fs4.rm(tempPath, { force: true }).catch(() => void 0);
18918
+ throw error45;
18919
+ }
18920
+ }
18921
+ async function writeEncodedTextChunk(handle, encoding, text) {
18922
+ if (text.length === 0) {
18923
+ return 0;
18924
+ }
18925
+ const buffer = encodeTextBody(text, encoding);
18926
+ await handle.writeFile(buffer);
18927
+ return buffer.byteLength;
18928
+ }
18929
+ async function replaceLargeTextFileText(input) {
18930
+ const carryLength = Math.max(input.oldString.length - 1, 0);
18931
+ const fileNewlineStyle = input.loaded.newlineStyle;
18932
+ 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;
18933
+ let carry = "";
18934
+ let occurrencesBefore = 0;
18935
+ let replacedFirstMatch = false;
18936
+ const bytesWritten = await writeLargeTextFile(input.loaded.filePath, input.loaded.encoding, input.loaded.hasBom, async (handle) => {
18937
+ let written = 0;
18938
+ const flushText = async (text, flush = false) => {
18939
+ const combined = carry + text;
18940
+ if (combined.length === 0) {
18941
+ return;
18942
+ }
18943
+ const safeEnd = flush ? combined.length : Math.max(0, combined.length - carryLength);
18944
+ const outputParts = [];
18945
+ let cursor = 0;
18946
+ let flushUpto = safeEnd;
18947
+ while (true) {
18948
+ const index = combined.indexOf(input.oldString, cursor);
18949
+ if (index === -1) {
18950
+ break;
18951
+ }
18952
+ const matchEnd = index + input.oldString.length;
18953
+ if (!flush && matchEnd > safeEnd) {
18954
+ flushUpto = index;
18955
+ break;
18956
+ }
18957
+ occurrencesBefore += 1;
18958
+ outputParts.push(combined.slice(cursor, index));
18959
+ if (input.replaceAll) {
18960
+ outputParts.push(alignedNewString);
18961
+ } else if (!replacedFirstMatch) {
18962
+ outputParts.push(alignedNewString);
18963
+ replacedFirstMatch = true;
18964
+ } else {
18965
+ outputParts.push(input.oldString);
18966
+ }
18967
+ cursor = matchEnd;
18968
+ }
18969
+ outputParts.push(combined.slice(cursor, flushUpto));
18970
+ carry = combined.slice(flushUpto);
18971
+ const processable = outputParts.join("");
18972
+ if (processable.length === 0) {
18973
+ return;
18974
+ }
18975
+ written += await writeEncodedTextChunk(handle, input.loaded.encoding, processable);
18976
+ };
18977
+ await visitDecodedTextChunks2({
18978
+ filePath: input.loaded.filePath,
18979
+ encoding: input.loaded.encoding,
18980
+ hasBom: input.loaded.hasBom
18981
+ }, async (text) => {
18982
+ await flushText(text);
18983
+ });
18984
+ await flushText("", true);
18985
+ if (occurrencesBefore === 0) {
18986
+ throw createTextError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\u5BB9: ${input.oldString}`);
18987
+ }
18988
+ if (!input.replaceAll && occurrencesBefore > 1) {
18989
+ throw createTextError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
18990
+ }
18991
+ return written;
18992
+ });
18993
+ return {
18994
+ mode: "replace",
18995
+ filePath: input.loaded.filePath,
18996
+ encoding: input.loaded.encoding,
18997
+ requestedEncoding: input.loaded.requestedEncoding,
18998
+ detectedEncoding: input.loaded.detectedEncoding,
18999
+ confidence: input.loaded.confidence,
19000
+ hasBom: input.loaded.hasBom,
19001
+ replacements: input.replaceAll ? occurrencesBefore : 1,
19002
+ occurrencesBefore,
19003
+ bytesRead: input.loaded.bytesRead,
19004
+ bytesWritten,
19005
+ newlineStyle: input.loaded.newlineStyle,
19006
+ diffPreview: void 0
19007
+ };
19008
+ }
18593
19009
  async function readWholeTextFile(input) {
18594
19010
  const resolved = await resolveReadableTextFile(input);
18595
19011
  try {
18596
- const buffer = await fs3.readFile(resolved.filePath);
19012
+ const buffer = await fs4.readFile(resolved.filePath);
18597
19013
  assertLikelyTextBuffer(buffer);
18598
19014
  const detected = detectTextEncodingFromBuffer(buffer, input.encoding ?? "auto");
18599
19015
  const content = decodeText(buffer, detected.detectedEncoding);
@@ -18683,19 +19099,14 @@ function applyAnchors2(text, startAnchor, endAnchor) {
18683
19099
  }
18684
19100
  let rangeStart = 0;
18685
19101
  let rangeEnd = text.length;
19102
+ const newlineStyle = detectNewlineStyle(text);
18686
19103
  if (startAnchor) {
18687
- const found = text.indexOf(startAnchor);
18688
- if (found === -1) {
18689
- throw createTextError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8D77\u59CB\u951A\u70B9: ${startAnchor}`);
18690
- }
18691
- rangeStart = found + startAnchor.length;
19104
+ const found = findOccurrenceIndex(text, startAnchor, 1, newlineStyle);
19105
+ rangeStart = found.index + found.matchedToken.length;
18692
19106
  }
18693
19107
  if (endAnchor) {
18694
- const found = text.indexOf(endAnchor, rangeStart);
18695
- if (found === -1) {
18696
- throw createTextError("GBK_NO_MATCH", `\u672A\u627E\u5230\u7ED3\u675F\u951A\u70B9: ${endAnchor}`);
18697
- }
18698
- rangeEnd = found;
19108
+ const found = findOccurrenceIndex(text.slice(rangeStart), endAnchor, 1, newlineStyle);
19109
+ rangeEnd = rangeStart + found.index;
18699
19110
  }
18700
19111
  if (rangeEnd < rangeStart) {
18701
19112
  throw createTextError("GBK_INVALID_ARGUMENT", "\u951A\u70B9\u8303\u56F4\u65E0\u6548");
@@ -18726,6 +19137,64 @@ function alignTextToNewlineStyle2(text, newlineStyle) {
18726
19137
  }
18727
19138
  return text;
18728
19139
  }
19140
+ function matchLooseBlock2(contentLines, oldLines, newLines, newlineStyle) {
19141
+ for (let start = 0; start < contentLines.length; start += 1) {
19142
+ let contentIndex = start;
19143
+ let oldIndex = 0;
19144
+ while (oldIndex < oldLines.length && contentIndex < contentLines.length) {
19145
+ const expected = trimRightSpaces2(oldLines[oldIndex]);
19146
+ const actual = trimRightSpaces2(contentLines[contentIndex]);
19147
+ if (expected === actual) {
19148
+ oldIndex += 1;
19149
+ contentIndex += 1;
19150
+ continue;
19151
+ }
19152
+ if (actual === "") {
19153
+ contentIndex += 1;
19154
+ continue;
19155
+ }
19156
+ if (expected === "") {
19157
+ oldIndex += 1;
19158
+ continue;
19159
+ }
19160
+ break;
19161
+ }
19162
+ if (oldIndex === oldLines.length) {
19163
+ const matchedOriginal = contentLines.slice(start, contentIndex).join("\n");
19164
+ const replacedNormalized = [
19165
+ ...contentLines.slice(0, start),
19166
+ ...newLines,
19167
+ ...contentLines.slice(contentIndex)
19168
+ ].join("\n");
19169
+ return {
19170
+ occurrencesBefore: 1,
19171
+ matchedOriginal,
19172
+ content: newlineStyle === "crlf" ? replacedNormalized.replace(/\n/g, "\r\n") : replacedNormalized
19173
+ };
19174
+ }
19175
+ }
19176
+ return null;
19177
+ }
19178
+ function tryLooseBlockReplace2(content, oldString, newString) {
19179
+ const contentLines = splitNormalizedLines2(content);
19180
+ const oldLines = trimTrailingEmptyLines2(splitNormalizedLines2(oldString));
19181
+ const newLines = splitNormalizedLines2(newString);
19182
+ const newlineStyle = detectNewlineStyle(content);
19183
+ if (oldLines.length === 0) {
19184
+ return null;
19185
+ }
19186
+ const direct = matchLooseBlock2(contentLines, oldLines, newLines, newlineStyle);
19187
+ if (direct !== null) {
19188
+ return direct;
19189
+ }
19190
+ if (hasLineNumberPrefixes2(oldLines)) {
19191
+ const strippedOldLines = trimTrailingEmptyLines2(stripLineNumberPrefixes2(oldLines));
19192
+ if (strippedOldLines.length > 0) {
19193
+ return matchLooseBlock2(contentLines, strippedOldLines, newLines, newlineStyle);
19194
+ }
19195
+ }
19196
+ return null;
19197
+ }
18729
19198
  function ensureLossless(input, encoding, hasBom = encoding === "utf8-bom") {
18730
19199
  assertStringArgument2(input, "content");
18731
19200
  const buffer = encodeText(input, encoding, hasBom);
@@ -18736,7 +19205,7 @@ function ensureLossless(input, encoding, hasBom = encoding === "utf8-bom") {
18736
19205
  }
18737
19206
  async function ensureParentDirectory(parent, createDirectories) {
18738
19207
  try {
18739
- const parentStat = await fs3.stat(parent);
19208
+ const parentStat = await fs4.stat(parent);
18740
19209
  if (!parentStat.isDirectory()) {
18741
19210
  throw createTextError("GBK_PARENT_DIRECTORY_MISSING", `\u7236\u76EE\u5F55\u4E0D\u5B58\u5728: ${parent}`);
18742
19211
  }
@@ -18745,7 +19214,7 @@ async function ensureParentDirectory(parent, createDirectories) {
18745
19214
  if (!createDirectories) {
18746
19215
  throw createTextError("GBK_PARENT_DIRECTORY_MISSING", `\u7236\u76EE\u5F55\u4E0D\u5B58\u5728: ${parent}`);
18747
19216
  }
18748
- await fs3.mkdir(parent, { recursive: true });
19217
+ await fs4.mkdir(parent, { recursive: true });
18749
19218
  return;
18750
19219
  }
18751
19220
  throw error45;
@@ -18808,7 +19277,7 @@ async function writeTextFile(input) {
18808
19277
  const overwrite = input.overwrite ?? false;
18809
19278
  const append = input.append ?? false;
18810
19279
  const { candidatePath } = await assertPathAllowed(input.filePath, input.context, input.allowExternal ?? false);
18811
- const parent = path3.dirname(candidatePath);
19280
+ const parent = path4.dirname(candidatePath);
18812
19281
  await ensureParentDirectory(parent, createDirectories);
18813
19282
  let existing = null;
18814
19283
  try {
@@ -18837,7 +19306,7 @@ async function writeTextFile(input) {
18837
19306
  const outputContent = existing && preserveNewlineStyle ? alignTextToNewlineStyle2(rawContent, existing.newlineStyle) : rawContent;
18838
19307
  ensureLossless(outputContent, targetEncoding, targetHasBom);
18839
19308
  const buffer = encodeText(outputContent, targetEncoding, targetHasBom);
18840
- await fs3.writeFile(candidatePath, buffer);
19309
+ await fs4.writeFile(candidatePath, buffer);
18841
19310
  return {
18842
19311
  filePath: candidatePath,
18843
19312
  encoding: targetEncoding,
@@ -18901,7 +19370,7 @@ async function replaceTextFileText(input) {
18901
19370
  const targetHasBom2 = normalizedInput.preserveEncoding === false ? targetEncoding2 === "utf8-bom" || targetEncoding2 === "utf16le" || targetEncoding2 === "utf16be" : loaded2.hasBom;
18902
19371
  ensureLossless(insertResult.outputText, targetEncoding2, targetHasBom2);
18903
19372
  const buffer2 = encodeText(insertResult.outputText, targetEncoding2, targetHasBom2);
18904
- await fs3.writeFile(loaded2.filePath, buffer2);
19373
+ await fs4.writeFile(loaded2.filePath, buffer2);
18905
19374
  return {
18906
19375
  mode,
18907
19376
  filePath: loaded2.filePath,
@@ -18925,40 +19394,92 @@ async function replaceTextFileText(input) {
18925
19394
  if (input.oldString.length === 0) {
18926
19395
  throw createTextError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
18927
19396
  }
19397
+ const prefixedBlock = parseLineNumberPrefixedBlock2(input.oldString);
19398
+ const derivedScopedRange = prefixedBlock?.isContiguous ? {
19399
+ startLine: normalizedInput.startLine ?? prefixedBlock.startLine,
19400
+ endLine: normalizedInput.endLine ?? prefixedBlock.endLine,
19401
+ oldString: prefixedBlock.strippedText
19402
+ } : null;
19403
+ const effectiveOldString = derivedScopedRange?.oldString ?? input.oldString;
18928
19404
  const resolved = await detectReadableTextFile({
18929
19405
  filePath: input.filePath,
18930
19406
  encoding: input.encoding ?? "auto",
18931
19407
  allowExternal: input.allowExternal,
18932
19408
  context: input.context
18933
19409
  });
19410
+ const hasScopedRange = normalizedInput.startLine !== void 0 || normalizedInput.endLine !== void 0 || normalizedInput.startAnchor !== void 0 || normalizedInput.endAnchor !== void 0 || derivedScopedRange !== null;
19411
+ const preserveEncoding = normalizedInput.preserveEncoding ?? true;
19412
+ const preserveNewlineStyle = normalizedInput.preserveNewlineStyle ?? true;
19413
+ const requestedEncoding = normalizedInput.encoding ?? "auto";
19414
+ if (resolved.stat.size >= TEXT_STREAMING_FILE_SIZE_THRESHOLD_BYTES && !hasScopedRange && preserveEncoding && preserveNewlineStyle && requestedEncoding === "auto") {
19415
+ const loadedForStreaming = await readWholeTextFile({
19416
+ filePath: input.filePath,
19417
+ encoding: requestedEncoding,
19418
+ allowExternal: input.allowExternal,
19419
+ context: input.context
19420
+ });
19421
+ return await replaceLargeTextFileText({
19422
+ loaded: loadedForStreaming,
19423
+ oldString: effectiveOldString,
19424
+ newString: input.newString,
19425
+ replaceAll: normalizedInput.replaceAll ?? false
19426
+ });
19427
+ }
18934
19428
  const loaded = await readWholeTextFile({
18935
19429
  filePath: input.filePath,
18936
19430
  encoding: input.encoding ?? "auto",
18937
19431
  allowExternal: input.allowExternal,
18938
19432
  context: input.context
18939
19433
  });
18940
- const scope = resolveEditScope2(loaded.content, normalizedInput);
19434
+ const scopedInput = derivedScopedRange === null ? normalizedInput : {
19435
+ ...normalizedInput,
19436
+ startLine: derivedScopedRange.startLine,
19437
+ endLine: derivedScopedRange.endLine
19438
+ };
19439
+ const scope = resolveEditScope2(loaded.content, scopedInput);
18941
19440
  const replaceAll = normalizedInput.replaceAll ?? false;
18942
- const preserveEncoding = normalizedInput.preserveEncoding ?? true;
18943
- const requestedEncoding = normalizedInput.encoding ?? "auto";
18944
- const occurrencesBefore = countOccurrences(scope.selectedText, input.oldString);
19441
+ const targetEncoding = preserveEncoding || requestedEncoding === "auto" ? loaded.encoding : resolveExplicitTextEncoding(requestedEncoding, loaded.encoding);
19442
+ const targetHasBom = preserveEncoding ? loaded.hasBom : targetEncoding === "utf8-bom" || targetEncoding === "utf16le" || targetEncoding === "utf16be";
19443
+ const occurrencesBefore = countOccurrences(scope.selectedText, effectiveOldString);
19444
+ if (!replaceAll && occurrencesBefore === 0) {
19445
+ const loose = tryLooseBlockReplace2(scope.selectedText, effectiveOldString, input.newString);
19446
+ if (loose !== null) {
19447
+ const outputText2 = `${loaded.content.slice(0, scope.rangeStart)}${loose.content}${loaded.content.slice(scope.rangeEnd)}`;
19448
+ ensureLossless(outputText2, targetEncoding, targetHasBom);
19449
+ const buffer2 = encodeText(outputText2, targetEncoding, targetHasBom);
19450
+ await fs4.writeFile(loaded.filePath, buffer2);
19451
+ return {
19452
+ mode: "replace",
19453
+ filePath: loaded.filePath,
19454
+ encoding: targetEncoding,
19455
+ requestedEncoding: loaded.requestedEncoding,
19456
+ detectedEncoding: loaded.detectedEncoding,
19457
+ confidence: loaded.confidence,
19458
+ hasBom: targetHasBom,
19459
+ replacements: 1,
19460
+ occurrencesBefore: loose.occurrencesBefore,
19461
+ bytesRead: loaded.bytesRead,
19462
+ bytesWritten: buffer2.byteLength,
19463
+ newlineStyle: detectNewlineStyle(outputText2),
19464
+ diffPreview: buildLineDiffPreview(loaded.filePath, targetEncoding, loose.matchedOriginal, input.newString)
19465
+ };
19466
+ }
19467
+ }
18945
19468
  if (replaceAll) {
18946
19469
  if (occurrencesBefore === 0) {
18947
- throw createTextError("GBK_NO_MATCH", buildNoMatchMessage2(scope.selectedText, input.oldString));
19470
+ throw createTextError("GBK_NO_MATCH", buildNoMatchMessage2(scope.selectedText, effectiveOldString));
18948
19471
  }
18949
19472
  } else if (occurrencesBefore === 0) {
18950
- throw createTextError("GBK_NO_MATCH", buildNoMatchMessage2(scope.selectedText, input.oldString));
19473
+ throw createTextError("GBK_NO_MATCH", buildNoMatchMessage2(scope.selectedText, effectiveOldString));
18951
19474
  } else if (occurrencesBefore > 1) {
18952
- throw createTextError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
19475
+ throw createTextError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${effectiveOldString}`);
18953
19476
  }
18954
19477
  const alignedNewString = normalizedInput.preserveNewlineStyle === false ? input.newString : alignTextToNewlineStyle2(input.newString, loaded.newlineStyle);
18955
- const replaced = replaceAll ? scope.selectedText.split(input.oldString).join(alignedNewString) : scope.selectedText.replace(input.oldString, alignedNewString);
19478
+ const replaced = replaceAll ? scope.selectedText.split(effectiveOldString).join(alignedNewString) : scope.selectedText.replace(effectiveOldString, alignedNewString);
18956
19479
  const outputText = `${loaded.content.slice(0, scope.rangeStart)}${replaced}${loaded.content.slice(scope.rangeEnd)}`;
18957
- const targetEncoding = preserveEncoding || requestedEncoding === "auto" ? loaded.encoding : resolveExplicitTextEncoding(requestedEncoding, loaded.encoding);
18958
- const targetHasBom = preserveEncoding ? loaded.hasBom : targetEncoding === "utf8-bom" || targetEncoding === "utf16le" || targetEncoding === "utf16be";
18959
19480
  ensureLossless(outputText, targetEncoding, targetHasBom);
18960
19481
  const buffer = encodeText(outputText, targetEncoding, targetHasBom);
18961
- await fs3.writeFile(loaded.filePath, buffer);
19482
+ await fs4.writeFile(loaded.filePath, buffer);
18962
19483
  return {
18963
19484
  mode: "replace",
18964
19485
  filePath: loaded.filePath,
@@ -18972,7 +19493,7 @@ async function replaceTextFileText(input) {
18972
19493
  bytesRead: loaded.bytesRead,
18973
19494
  bytesWritten: buffer.byteLength,
18974
19495
  newlineStyle: detectNewlineStyle(outputText),
18975
- diffPreview: buildLineDiffPreview(loaded.filePath, targetEncoding, input.oldString, alignedNewString)
19496
+ diffPreview: buildLineDiffPreview(loaded.filePath, targetEncoding, effectiveOldString, alignedNewString)
18976
19497
  };
18977
19498
  }
18978
19499
 
@@ -19119,6 +19640,28 @@ var MANAGED_TOOL_IDS = /* @__PURE__ */ new Set([
19119
19640
  "text_write",
19120
19641
  "text_edit"
19121
19642
  ]);
19643
+ var BUILTIN_TEXT_TOOL_IDS = /* @__PURE__ */ new Set(["read", "write", "edit"]);
19644
+ var REMEMBERABLE_TEXT_TOOL_IDS = /* @__PURE__ */ new Set(["text_read", "text_write", "text_edit"]);
19645
+ function getToolFilePath(args) {
19646
+ if (!args || typeof args !== "object") {
19647
+ return null;
19648
+ }
19649
+ const filePath = args.filePath;
19650
+ return typeof filePath === "string" ? filePath : null;
19651
+ }
19652
+ async function maybePersistRememberedEncoding(metadata) {
19653
+ const filePath = typeof metadata.filePath === "string" ? metadata.filePath : null;
19654
+ const encoding = metadata.encoding;
19655
+ if (!filePath) {
19656
+ return;
19657
+ }
19658
+ if (isRememberedGbkEncoding(encoding)) {
19659
+ await rememberGbkEncoding(filePath, encoding);
19660
+ metadata.rememberedEncoding = encoding;
19661
+ return;
19662
+ }
19663
+ await forgetRememberedEncoding(filePath);
19664
+ }
19122
19665
  function truncateMetadataPreview(value, sessionID) {
19123
19666
  const previewMaxChars = Math.max(800, Math.min(2e3, Math.floor(getMaxOutputChars(sessionID) / 2)));
19124
19667
  if (value.length <= previewMaxChars) return value;
@@ -19195,6 +19738,25 @@ function createOpencodeGbkHooks(client, directory) {
19195
19738
  await maybeAutoSummarizeSession(client, directory, input);
19196
19739
  }
19197
19740
  },
19741
+ async "tool.execute.before"(input, output) {
19742
+ const filePath = getToolFilePath(output.args);
19743
+ if (!filePath) {
19744
+ return;
19745
+ }
19746
+ const remembered = await getRememberedGbkEncoding(filePath);
19747
+ if (!remembered) {
19748
+ return;
19749
+ }
19750
+ if (REMEMBERABLE_TEXT_TOOL_IDS.has(input.tool)) {
19751
+ if (output.args && (output.args.encoding === void 0 || output.args.encoding === "auto")) {
19752
+ output.args.encoding = remembered.encoding;
19753
+ }
19754
+ return;
19755
+ }
19756
+ if (BUILTIN_TEXT_TOOL_IDS.has(input.tool)) {
19757
+ throw new Error(`\u6587\u4EF6\u5DF2\u8BB0\u5FC6\u4E3A ${remembered.encoding.toUpperCase()} \u7F16\u7801\uFF0C\u8BF7\u6539\u7528 gbk_read\u3001gbk_write\u3001gbk_edit \u6216 gbk_search\uFF1A${filePath}`);
19758
+ }
19759
+ },
19198
19760
  async "tool.execute.after"(input, output) {
19199
19761
  if (!MANAGED_TOOL_IDS.has(input.tool)) return;
19200
19762
  const maxOutputChars = getMaxOutputChars(input.sessionID);
@@ -19210,6 +19772,11 @@ function createOpencodeGbkHooks(client, directory) {
19210
19772
  if (typeof metadata.diffPreview === "string") {
19211
19773
  metadata.diffPreview = truncateMetadataPreview(metadata.diffPreview, input.sessionID);
19212
19774
  }
19775
+ try {
19776
+ await maybePersistRememberedEncoding(metadata);
19777
+ } catch {
19778
+ metadata.encodingMemoryWarning = "\u8BB0\u5FC6\u6587\u4EF6\u7F16\u7801\u5931\u8D25\uFF0C\u5DF2\u5FFD\u7565";
19779
+ }
19213
19780
  metadata.maxOutputChars = maxOutputChars;
19214
19781
  if (compactionCount > 0) {
19215
19782
  metadata.sessionCompactions = compactionCount;