opencode-gbk-tools 0.1.28 → 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) {
@@ -17033,7 +17166,7 @@ async function resolveReadableGbkFile(input) {
17033
17166
  const { candidatePath } = await assertPathAllowed(input.filePath, input.context, input.allowExternal ?? false);
17034
17167
  let stat;
17035
17168
  try {
17036
- stat = await fs2.stat(candidatePath);
17169
+ stat = await fs3.stat(candidatePath);
17037
17170
  } catch (error45) {
17038
17171
  throw createGbkError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\u5728: ${candidatePath}`, error45);
17039
17172
  }
@@ -17048,7 +17181,7 @@ async function resolveReadableGbkFile(input) {
17048
17181
  }
17049
17182
  async function readWholeGbkTextFile(input) {
17050
17183
  try {
17051
- const buffer = await fs2.readFile(input.filePath);
17184
+ const buffer = await fs3.readFile(input.filePath);
17052
17185
  const content = await readBufferAsText(buffer, input.encoding);
17053
17186
  return {
17054
17187
  filePath: input.filePath,
@@ -17192,7 +17325,7 @@ async function appendEncodedText(filePath, encoding, text) {
17192
17325
  return 0;
17193
17326
  }
17194
17327
  const buffer = import_iconv_lite.default.encode(text, encoding);
17195
- await fs2.appendFile(filePath, buffer);
17328
+ await fs3.appendFile(filePath, buffer);
17196
17329
  return buffer.byteLength;
17197
17330
  }
17198
17331
  async function copyFileByteRangeToHandle(sourcePath, handle, start, endExclusive) {
@@ -17271,11 +17404,11 @@ async function replaceLargeGbkFileTextInLineRange(input) {
17271
17404
  input.replaceAll,
17272
17405
  lineIndex.newlineStyle
17273
17406
  );
17274
- const tempPath = path2.join(
17275
- path2.dirname(input.filePath),
17276
- `${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`
17277
17410
  );
17278
- const handle = await fs2.open(tempPath, "w");
17411
+ const handle = await fs3.open(tempPath, "w");
17279
17412
  let bytesWritten = 0;
17280
17413
  try {
17281
17414
  bytesWritten += await copyFileByteRangeToHandle(input.filePath, handle, 0, rangeStart);
@@ -17283,8 +17416,8 @@ async function replaceLargeGbkFileTextInLineRange(input) {
17283
17416
  bytesWritten += await copyFileByteRangeToHandle(input.filePath, handle, rangeEnd, toSafeNumber(input.stat.size));
17284
17417
  await handle.close();
17285
17418
  const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
17286
- await fs2.chmod(tempPath, mode);
17287
- await fs2.rename(tempPath, input.filePath);
17419
+ await fs3.chmod(tempPath, mode);
17420
+ await fs3.rename(tempPath, input.filePath);
17288
17421
  return {
17289
17422
  mode: "replace",
17290
17423
  filePath: input.filePath,
@@ -17296,18 +17429,18 @@ async function replaceLargeGbkFileTextInLineRange(input) {
17296
17429
  };
17297
17430
  } catch (error45) {
17298
17431
  await handle.close().catch(() => void 0);
17299
- await fs2.rm(tempPath, { force: true }).catch(() => void 0);
17432
+ await fs3.rm(tempPath, { force: true }).catch(() => void 0);
17300
17433
  throw error45;
17301
17434
  }
17302
17435
  }
17303
17436
  async function replaceLargeGbkFileByAnchor(input) {
17304
17437
  const lineIndex = await getGbkLineIndex(input);
17305
17438
  const newlineStyle = lineIndex.newlineStyle;
17306
- const tempPath = path2.join(
17307
- path2.dirname(input.filePath),
17308
- `${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17439
+ const tempPath = path3.join(
17440
+ path3.dirname(input.filePath),
17441
+ `${path3.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17309
17442
  );
17310
- const handle = await fs2.open(tempPath, "w");
17443
+ const handle = await fs3.open(tempPath, "w");
17311
17444
  const alignedContent = alignTextToNewlineStyle(input.content, newlineStyle);
17312
17445
  const anchorVariants = buildFlexibleSearchVariants(input.anchor, newlineStyle);
17313
17446
  const maxAnchorLength = anchorVariants.reduce((maxLength, candidate) => Math.max(maxLength, candidate.length), input.anchor.length);
@@ -17420,8 +17553,8 @@ async function replaceLargeGbkFileByAnchor(input) {
17420
17553
  await finalizeInserted();
17421
17554
  await handle.close();
17422
17555
  const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
17423
- await fs2.chmod(tempPath, mode);
17424
- await fs2.rename(tempPath, input.filePath);
17556
+ await fs3.chmod(tempPath, mode);
17557
+ await fs3.rename(tempPath, input.filePath);
17425
17558
  invalidateGbkLineIndex(input.filePath);
17426
17559
  return {
17427
17560
  mode: input.mode,
@@ -17437,18 +17570,18 @@ async function replaceLargeGbkFileByAnchor(input) {
17437
17570
  };
17438
17571
  } catch (error45) {
17439
17572
  await handle.close().catch(() => void 0);
17440
- await fs2.rm(tempPath, { force: true }).catch(() => void 0);
17573
+ await fs3.rm(tempPath, { force: true }).catch(() => void 0);
17441
17574
  throw error45;
17442
17575
  }
17443
17576
  }
17444
17577
  async function replaceLargeGbkFileText(input) {
17445
17578
  const lineIndex = await getGbkLineIndex(input);
17446
17579
  const newlineStyle = lineIndex.newlineStyle;
17447
- const tempPath = path2.join(
17448
- path2.dirname(input.filePath),
17449
- `${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17580
+ const tempPath = path3.join(
17581
+ path3.dirname(input.filePath),
17582
+ `${path3.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17450
17583
  );
17451
- const handle = await fs2.open(tempPath, "w");
17584
+ const handle = await fs3.open(tempPath, "w");
17452
17585
  const carryLength = Math.max(input.oldString.length - 1, 0);
17453
17586
  const alignedNewString = alignTextToNewlineStyle(input.newString, newlineStyle);
17454
17587
  let carry = "";
@@ -17507,8 +17640,8 @@ async function replaceLargeGbkFileText(input) {
17507
17640
  }
17508
17641
  await handle.close();
17509
17642
  const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
17510
- await fs2.chmod(tempPath, mode);
17511
- await fs2.rename(tempPath, input.filePath);
17643
+ await fs3.chmod(tempPath, mode);
17644
+ await fs3.rename(tempPath, input.filePath);
17512
17645
  invalidateGbkLineIndex(input.filePath);
17513
17646
  return {
17514
17647
  filePath: input.filePath,
@@ -17520,7 +17653,7 @@ async function replaceLargeGbkFileText(input) {
17520
17653
  };
17521
17654
  } catch (error45) {
17522
17655
  await handle.close().catch(() => void 0);
17523
- await fs2.rm(tempPath, { force: true }).catch(() => void 0);
17656
+ await fs3.rm(tempPath, { force: true }).catch(() => void 0);
17524
17657
  throw error45;
17525
17658
  }
17526
17659
  }
@@ -17648,7 +17781,7 @@ async function replaceGbkFileText(input) {
17648
17781
  };
17649
17782
  }
17650
17783
  const buffer2 = import_iconv_lite.default.encode(insertResult.outputText, current2.encoding);
17651
- await fs2.writeFile(current2.filePath, buffer2);
17784
+ await fs3.writeFile(current2.filePath, buffer2);
17652
17785
  return {
17653
17786
  mode,
17654
17787
  filePath: current2.filePath,
@@ -17708,7 +17841,7 @@ async function replaceGbkFileText(input) {
17708
17841
  if (loose !== null) {
17709
17842
  const outputText2 = `${current.content.slice(0, scope.rangeStart)}${loose.content}${current.content.slice(scope.rangeEnd)}`;
17710
17843
  const buffer2 = import_iconv_lite.default.encode(outputText2, current.encoding);
17711
- await fs2.writeFile(current.filePath, buffer2);
17844
+ await fs3.writeFile(current.filePath, buffer2);
17712
17845
  invalidateGbkLineIndex(current.filePath);
17713
17846
  return {
17714
17847
  mode: "replace",
@@ -17736,7 +17869,7 @@ async function replaceGbkFileText(input) {
17736
17869
  const replaced = replaceAll ? scope.selectedText.split(effectiveOldString).join(alignedNewString) : scope.selectedText.replace(effectiveOldString, alignedNewString);
17737
17870
  const outputText = `${current.content.slice(0, scope.rangeStart)}${replaced}${current.content.slice(scope.rangeEnd)}`;
17738
17871
  const buffer = import_iconv_lite.default.encode(outputText, current.encoding);
17739
- await fs2.writeFile(current.filePath, buffer);
17872
+ await fs3.writeFile(current.filePath, buffer);
17740
17873
  invalidateGbkLineIndex(current.filePath);
17741
17874
  return {
17742
17875
  mode: "replace",
@@ -17821,11 +17954,11 @@ async function writeGbkFile(input) {
17821
17954
  const overwrite = input.overwrite ?? false;
17822
17955
  const append = input.append ?? false;
17823
17956
  const { candidatePath } = await assertPathAllowed(input.filePath, input.context, input.allowExternal ?? false);
17824
- const parent = path2.dirname(candidatePath);
17957
+ const parent = path3.dirname(candidatePath);
17825
17958
  assertEncodingSupported(encoding);
17826
17959
  if (append) {
17827
17960
  try {
17828
- const parentStat = await fs2.stat(parent);
17961
+ const parentStat = await fs3.stat(parent);
17829
17962
  if (!parentStat.isDirectory()) {
17830
17963
  throw createGbkError("GBK_PARENT_DIRECTORY_MISSING", `\u7236\u76EE\u5F55\u4E0D\u5B58\u5728: ${parent}`);
17831
17964
  }
@@ -17834,14 +17967,14 @@ async function writeGbkFile(input) {
17834
17967
  if (!createDirectories) {
17835
17968
  throw createGbkError("GBK_PARENT_DIRECTORY_MISSING", `\u7236\u76EE\u5F55\u4E0D\u5B58\u5728: ${parent}`);
17836
17969
  }
17837
- await fs2.mkdir(parent, { recursive: true });
17970
+ await fs3.mkdir(parent, { recursive: true });
17838
17971
  } else if (error45 instanceof Error && "code" in error45) {
17839
17972
  throw error45;
17840
17973
  }
17841
17974
  }
17842
17975
  let existed = false;
17843
17976
  try {
17844
- await fs2.stat(candidatePath);
17977
+ await fs3.stat(candidatePath);
17845
17978
  existed = true;
17846
17979
  } catch (error45) {
17847
17980
  if (!(error45 instanceof Error && "code" in error45 && error45.code === "ENOENT")) {
@@ -17860,7 +17993,7 @@ async function writeGbkFile(input) {
17860
17993
  };
17861
17994
  }
17862
17995
  try {
17863
- const stat = await fs2.stat(candidatePath);
17996
+ const stat = await fs3.stat(candidatePath);
17864
17997
  if (stat.isDirectory()) {
17865
17998
  throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\u5F55: ${candidatePath}`);
17866
17999
  }
@@ -17877,7 +18010,7 @@ async function writeGbkFile(input) {
17877
18010
  }
17878
18011
  }
17879
18012
  try {
17880
- const parentStat = await fs2.stat(parent);
18013
+ const parentStat = await fs3.stat(parent);
17881
18014
  if (!parentStat.isDirectory()) {
17882
18015
  throw createGbkError("GBK_PARENT_DIRECTORY_MISSING", `\u7236\u76EE\u5F55\u4E0D\u5B58\u5728: ${parent}`);
17883
18016
  }
@@ -17886,15 +18019,15 @@ async function writeGbkFile(input) {
17886
18019
  if (!createDirectories) {
17887
18020
  throw createGbkError("GBK_PARENT_DIRECTORY_MISSING", `\u7236\u76EE\u5F55\u4E0D\u5B58\u5728: ${parent}`);
17888
18021
  }
17889
- await fs2.mkdir(parent, { recursive: true });
18022
+ await fs3.mkdir(parent, { recursive: true });
17890
18023
  } else if (error45 instanceof Error && "code" in error45) {
17891
18024
  throw error45;
17892
18025
  }
17893
18026
  }
17894
18027
  try {
17895
- const existed = await fs2.stat(candidatePath).then(() => true).catch(() => false);
18028
+ const existed = await fs3.stat(candidatePath).then(() => true).catch(() => false);
17896
18029
  const buffer = import_iconv_lite.default.encode(input.content, encoding);
17897
- await fs2.writeFile(candidatePath, buffer);
18030
+ await fs3.writeFile(candidatePath, buffer);
17898
18031
  invalidateGbkLineIndex(candidatePath);
17899
18032
  return {
17900
18033
  filePath: candidatePath,
@@ -18115,14 +18248,16 @@ var TEXT_TOOL_SYSTEM_MARKER = "[opencode-gbk-tools:text-rules]";
18115
18248
  var TEXT_TOOL_SYSTEM_PROMPT = [
18116
18249
  TEXT_TOOL_SYSTEM_MARKER,
18117
18250
  "\u6587\u672C\u6587\u4EF6\u5904\u7406\u89C4\u5219\uFF1A",
18118
- "- \u5904\u7406\u6587\u672C\u6587\u4EF6\u65F6\uFF0C\u9ED8\u8BA4\u4F18\u5148\u4F7F\u7528 gbk_read\u3001gbk_write\u3001gbk_edit\u3001gbk_search\u3002",
18119
- "- gbk_* \u9002\u5408\u5F53\u524D\u4ED3\u5E93\u7684\u81EA\u52A8\u7F16\u7801\u6587\u672C\u4E0E GBK/GB18030 \u6587\u4EF6\uFF1B\u5373\u4F7F\u4E0D\u662F\u660E\u786E\u7684 GBK \u6587\u4EF6\uFF0C\u4E5F\u4F18\u5148\u8D70\u8FD9\u7EC4\u5DE5\u5177\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",
18120
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",
18121
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",
18122
18257
  "- \u53EA\u6709\u5728\u660E\u786E\u505A\u7CBE\u786E\u66FF\u6362\u65F6\uFF0C\u624D\u4F7F\u7528 oldString/newString\u3002",
18123
- "- anchor\u3001startAnchor\u3001endAnchor\u3001oldString \u82E5\u76F4\u63A5\u590D\u5236\u81EA\u8BFB\u53D6\u7ED3\u679C\uFF0C\u53EF\u4FDD\u7559 LF \u6362\u884C\uFF1B\u5DE5\u5177\u4F1A\u5C3D\u91CF\u6309\u6587\u4EF6\u6362\u884C\u98CE\u683C\u81EA\u52A8\u5BF9\u9F50\u3002",
18124
- '- \u82E5\u8BFB\u53D6\u7ED3\u679C\u5E26\u6709 "N: " \u884C\u53F7\u524D\u7F00\uFF0Cgbk_edit \u4F1A\u5C3D\u91CF\u81EA\u52A8\u5265\u79BB\u8FD9\u4E9B\u524D\u7F00\u540E\u518D\u5339\u914D\u3002',
18125
- "- text_read\u3001text_write\u3001text_edit \u4EC5\u4F5C\u4E3A\u517C\u5BB9\u5DE5\u5177\uFF0C\u4E0D\u518D\u4F5C\u4E3A\u9ED8\u8BA4\u63A8\u8350\u8DEF\u5F84\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"
18126
18261
  ].join("\n");
18127
18262
  function appendTextToolSystemPrompt(system) {
18128
18263
  if (system.some((item) => item.includes(TEXT_TOOL_SYSTEM_MARKER))) {
@@ -18141,8 +18276,8 @@ ${TEXT_TOOL_SYSTEM_PROMPT}`;
18141
18276
  var import_iconv_lite2 = __toESM(require_lib(), 1);
18142
18277
  import crypto2 from "crypto";
18143
18278
  import { createReadStream as createReadStream2 } from "fs";
18144
- import fs3 from "fs/promises";
18145
- import path3 from "path";
18279
+ import fs4 from "fs/promises";
18280
+ import path4 from "path";
18146
18281
  var TEXT_STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
18147
18282
  var TEXT_DETECTION_SAMPLE_BYTES = 64 * 1024;
18148
18283
  var UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
@@ -18178,7 +18313,7 @@ function resolveExplicitTextEncoding(value, fallback) {
18178
18313
  return requested === "auto" ? fallback : requested;
18179
18314
  }
18180
18315
  function shouldDefaultNewTextFileToGbk(filePath) {
18181
- return path3.extname(filePath).toLowerCase() === ".txt";
18316
+ return path4.extname(filePath).toLowerCase() === ".txt";
18182
18317
  }
18183
18318
  function getBomPrefix(encoding, hasBom) {
18184
18319
  if (!hasBom) {
@@ -18474,8 +18609,8 @@ function buildLineDiffPreview(filePath, encoding, beforeText, afterText) {
18474
18609
  const afterLines = normalizeNewlines2(afterText).split("\n");
18475
18610
  const maxLines = Math.max(beforeLines.length, afterLines.length);
18476
18611
  const header = [
18477
- `${ANSI_DIM2}--- ${path3.basename(filePath)} (${encoding})${ANSI_RESET2}`,
18478
- `${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}`
18479
18614
  ];
18480
18615
  const body = [];
18481
18616
  for (let index = 0; index < maxLines; index += 1) {
@@ -18591,7 +18726,7 @@ async function resolveReadableTextFile(input) {
18591
18726
  const { candidatePath } = await assertPathAllowed(input.filePath, input.context, input.allowExternal ?? false);
18592
18727
  let stat;
18593
18728
  try {
18594
- stat = await fs3.stat(candidatePath);
18729
+ stat = await fs4.stat(candidatePath);
18595
18730
  } catch (error45) {
18596
18731
  throw createTextError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\u5728: ${candidatePath}`, error45);
18597
18732
  }
@@ -18601,7 +18736,7 @@ async function resolveReadableTextFile(input) {
18601
18736
  return { filePath: candidatePath, stat };
18602
18737
  }
18603
18738
  async function readDetectionBuffer(filePath, sampleSize = TEXT_DETECTION_SAMPLE_BYTES) {
18604
- const handle = await fs3.open(filePath, "r");
18739
+ const handle = await fs4.open(filePath, "r");
18605
18740
  try {
18606
18741
  const buffer = Buffer.alloc(sampleSize);
18607
18742
  const { bytesRead } = await handle.read(buffer, 0, sampleSize, 0);
@@ -18764,8 +18899,8 @@ async function visitDecodedTextChunks2(resolved, visitor) {
18764
18899
  }
18765
18900
  }
18766
18901
  async function writeLargeTextFile(filePath, encoding, hasBom, producer) {
18767
- const tempPath = path3.join(path3.dirname(filePath), `${path3.basename(filePath)}.opencode-text-${crypto2.randomUUID()}.tmp`);
18768
- const handle = await fs3.open(tempPath, "w");
18902
+ const tempPath = path4.join(path4.dirname(filePath), `${path4.basename(filePath)}.opencode-text-${crypto2.randomUUID()}.tmp`);
18903
+ const handle = await fs4.open(tempPath, "w");
18769
18904
  try {
18770
18905
  if (hasBom) {
18771
18906
  const bom = getBomPrefix(encoding, hasBom);
@@ -18775,11 +18910,11 @@ async function writeLargeTextFile(filePath, encoding, hasBom, producer) {
18775
18910
  }
18776
18911
  const bytesWritten = await producer(handle);
18777
18912
  await handle.close();
18778
- await fs3.rename(tempPath, filePath);
18913
+ await fs4.rename(tempPath, filePath);
18779
18914
  return bytesWritten + getBomPrefix(encoding, hasBom).length;
18780
18915
  } catch (error45) {
18781
18916
  await handle.close().catch(() => void 0);
18782
- await fs3.rm(tempPath, { force: true }).catch(() => void 0);
18917
+ await fs4.rm(tempPath, { force: true }).catch(() => void 0);
18783
18918
  throw error45;
18784
18919
  }
18785
18920
  }
@@ -18874,7 +19009,7 @@ async function replaceLargeTextFileText(input) {
18874
19009
  async function readWholeTextFile(input) {
18875
19010
  const resolved = await resolveReadableTextFile(input);
18876
19011
  try {
18877
- const buffer = await fs3.readFile(resolved.filePath);
19012
+ const buffer = await fs4.readFile(resolved.filePath);
18878
19013
  assertLikelyTextBuffer(buffer);
18879
19014
  const detected = detectTextEncodingFromBuffer(buffer, input.encoding ?? "auto");
18880
19015
  const content = decodeText(buffer, detected.detectedEncoding);
@@ -19070,7 +19205,7 @@ function ensureLossless(input, encoding, hasBom = encoding === "utf8-bom") {
19070
19205
  }
19071
19206
  async function ensureParentDirectory(parent, createDirectories) {
19072
19207
  try {
19073
- const parentStat = await fs3.stat(parent);
19208
+ const parentStat = await fs4.stat(parent);
19074
19209
  if (!parentStat.isDirectory()) {
19075
19210
  throw createTextError("GBK_PARENT_DIRECTORY_MISSING", `\u7236\u76EE\u5F55\u4E0D\u5B58\u5728: ${parent}`);
19076
19211
  }
@@ -19079,7 +19214,7 @@ async function ensureParentDirectory(parent, createDirectories) {
19079
19214
  if (!createDirectories) {
19080
19215
  throw createTextError("GBK_PARENT_DIRECTORY_MISSING", `\u7236\u76EE\u5F55\u4E0D\u5B58\u5728: ${parent}`);
19081
19216
  }
19082
- await fs3.mkdir(parent, { recursive: true });
19217
+ await fs4.mkdir(parent, { recursive: true });
19083
19218
  return;
19084
19219
  }
19085
19220
  throw error45;
@@ -19142,7 +19277,7 @@ async function writeTextFile(input) {
19142
19277
  const overwrite = input.overwrite ?? false;
19143
19278
  const append = input.append ?? false;
19144
19279
  const { candidatePath } = await assertPathAllowed(input.filePath, input.context, input.allowExternal ?? false);
19145
- const parent = path3.dirname(candidatePath);
19280
+ const parent = path4.dirname(candidatePath);
19146
19281
  await ensureParentDirectory(parent, createDirectories);
19147
19282
  let existing = null;
19148
19283
  try {
@@ -19171,7 +19306,7 @@ async function writeTextFile(input) {
19171
19306
  const outputContent = existing && preserveNewlineStyle ? alignTextToNewlineStyle2(rawContent, existing.newlineStyle) : rawContent;
19172
19307
  ensureLossless(outputContent, targetEncoding, targetHasBom);
19173
19308
  const buffer = encodeText(outputContent, targetEncoding, targetHasBom);
19174
- await fs3.writeFile(candidatePath, buffer);
19309
+ await fs4.writeFile(candidatePath, buffer);
19175
19310
  return {
19176
19311
  filePath: candidatePath,
19177
19312
  encoding: targetEncoding,
@@ -19235,7 +19370,7 @@ async function replaceTextFileText(input) {
19235
19370
  const targetHasBom2 = normalizedInput.preserveEncoding === false ? targetEncoding2 === "utf8-bom" || targetEncoding2 === "utf16le" || targetEncoding2 === "utf16be" : loaded2.hasBom;
19236
19371
  ensureLossless(insertResult.outputText, targetEncoding2, targetHasBom2);
19237
19372
  const buffer2 = encodeText(insertResult.outputText, targetEncoding2, targetHasBom2);
19238
- await fs3.writeFile(loaded2.filePath, buffer2);
19373
+ await fs4.writeFile(loaded2.filePath, buffer2);
19239
19374
  return {
19240
19375
  mode,
19241
19376
  filePath: loaded2.filePath,
@@ -19312,7 +19447,7 @@ async function replaceTextFileText(input) {
19312
19447
  const outputText2 = `${loaded.content.slice(0, scope.rangeStart)}${loose.content}${loaded.content.slice(scope.rangeEnd)}`;
19313
19448
  ensureLossless(outputText2, targetEncoding, targetHasBom);
19314
19449
  const buffer2 = encodeText(outputText2, targetEncoding, targetHasBom);
19315
- await fs3.writeFile(loaded.filePath, buffer2);
19450
+ await fs4.writeFile(loaded.filePath, buffer2);
19316
19451
  return {
19317
19452
  mode: "replace",
19318
19453
  filePath: loaded.filePath,
@@ -19344,7 +19479,7 @@ async function replaceTextFileText(input) {
19344
19479
  const outputText = `${loaded.content.slice(0, scope.rangeStart)}${replaced}${loaded.content.slice(scope.rangeEnd)}`;
19345
19480
  ensureLossless(outputText, targetEncoding, targetHasBom);
19346
19481
  const buffer = encodeText(outputText, targetEncoding, targetHasBom);
19347
- await fs3.writeFile(loaded.filePath, buffer);
19482
+ await fs4.writeFile(loaded.filePath, buffer);
19348
19483
  return {
19349
19484
  mode: "replace",
19350
19485
  filePath: loaded.filePath,
@@ -19505,6 +19640,28 @@ var MANAGED_TOOL_IDS = /* @__PURE__ */ new Set([
19505
19640
  "text_write",
19506
19641
  "text_edit"
19507
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
+ }
19508
19665
  function truncateMetadataPreview(value, sessionID) {
19509
19666
  const previewMaxChars = Math.max(800, Math.min(2e3, Math.floor(getMaxOutputChars(sessionID) / 2)));
19510
19667
  if (value.length <= previewMaxChars) return value;
@@ -19581,6 +19738,25 @@ function createOpencodeGbkHooks(client, directory) {
19581
19738
  await maybeAutoSummarizeSession(client, directory, input);
19582
19739
  }
19583
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
+ },
19584
19760
  async "tool.execute.after"(input, output) {
19585
19761
  if (!MANAGED_TOOL_IDS.has(input.tool)) return;
19586
19762
  const maxOutputChars = getMaxOutputChars(input.sessionID);
@@ -19596,6 +19772,11 @@ function createOpencodeGbkHooks(client, directory) {
19596
19772
  if (typeof metadata.diffPreview === "string") {
19597
19773
  metadata.diffPreview = truncateMetadataPreview(metadata.diffPreview, input.sessionID);
19598
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
+ }
19599
19780
  metadata.maxOutputChars = maxOutputChars;
19600
19781
  if (compactionCount > 0) {
19601
19782
  metadata.sessionCompactions = compactionCount;