opencode-gbk-tools 0.1.29 → 0.1.31

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.
package/README.md CHANGED
@@ -60,6 +60,8 @@ npx opencode-gbk-tools uninstall
60
60
  ### text_read / text_write / text_edit
61
61
 
62
62
  - 默认 `encoding=auto`
63
+ - 新建 `.txt` 文件时,优先直接使用 `gbk_write` 创建;plugin 会把这类创建请求优先路由到 `gbk_*`
64
+ - 当前会话只要已经成功使用过 `gbk_*`,后续新建文件也应继续优先使用 `gbk_write` / `gbk_*`
63
65
  - 已有文件会尽量保持:
64
66
  - 原编码
65
67
  - BOM
@@ -208,7 +210,9 @@ A:不要。现在优先用 `mode="insertAfter"` 或 `mode="insertBefore"`,
208
210
 
209
211
  | 版本 | 说明 |
210
212
  |------|------|
211
- | 0.1.29 | 在方案 B 基础上新增 GBK 文件持久记忆:普通 agent 继续按编码分流,UTF-8 优先使用 OpenCode 内置工具,已确认是 GBK/GB18030 的文件会按完整路径 + mtime/size 持久记忆;后续再次操作同一路径时,`text_*` 会自动补齐记忆编码,内置 `read` / `write` / `edit` 会被拦截并提示改用 `gbk_*`;`gbk-engine` 继续保持为强制 GBK 专属模式 |
213
+ | 0.1.31 | `0.1.30` 基础上继续收紧新建文件路由:新建 `.txt` 文件必须优先直接使用 `gbk_write`,不再先走 `text_write` 或内置 `write/edit`;并新增“当前会话一旦成功使用过 `gbk_*`,后续新建文件继续优先走 `gbk_*`”的插件级约束与测试覆盖 |
214
+ | 0.1.30 | 补强 `text_*` 针对真实 GBK 样本与大体积重复样本的高强度测试:新增 `QFunction-0.txt` 真实样本读取、编辑、追加与端到端工作流覆盖,并补充大文件重复样本下的 `text_read` / `text_edit` 行为验证;同时继续收紧路由规则:新建 `.txt` 文件直接要求改用 `gbk_write`,且当前会话一旦使用过 `gbk_*`,后续新建文件也继续要求走 `gbk_*` |
215
+ | 0.1.29 | 在方案 B 基础上新增 GBK 文件持久记忆:普通 agent 继续按编码分流,UTF-8 优先使用 OpenCode 内置工具;对现有文件,已确认或首次检测到是 GBK/GB18030 的路径会按完整路径 + mtime/size 持久记忆;后续再次操作同一路径时,`text_*` 与内置 `read` / `write` / `edit` 都会被直接拦截并提示改用 `gbk_*`,避免先误走 `text_edit` / `edit` 再失败;`gbk-engine` 继续保持为强制 GBK 专属模式 |
212
216
  | 0.1.28 | 调整为方案 B:普通 agent 按编码分流,UTF-8 优先使用 OpenCode 内置工具,GBK/乱码风险优先使用 `gbk_*`;`gbk-engine` 保持为强制 GBK 专属模式;同时修复 `src/lib/gbk-file.ts` 中多处用户可见乱码错误文案,并增强 `text_*` 的匹配容错与大文件流式替换稳定性 |
213
217
  | 0.1.27 | 重新发布当前稳定内容,包含:新建 `.txt` 文件在 `encoding=auto` 下默认使用 `gbk`;保留已有文件的原编码检测与保持逻辑;修复 plugin 追加 system 规则时可能触发部分 Jinja/chat template 的“System message must be at the beginning”异常;同步更新工具提示、README 与测试覆盖 |
214
218
  | 0.1.26 | 仅提升发布版本号,用于重新发布当时的稳定内容 |
@@ -3816,6 +3816,9 @@ var require_lib = __commonJS({
3816
3816
  }
3817
3817
  });
3818
3818
 
3819
+ // src/plugin/index.ts
3820
+ import path5 from "path";
3821
+
3819
3822
  // src/lib/session-pressure.ts
3820
3823
  var AUTO_SUMMARIZE_PRESSURE_RATIO = 0.85;
3821
3824
  var AUTO_SUMMARIZE_COOLDOWN_MS = 6e4;
@@ -4043,6 +4046,67 @@ async function getRememberedGbkEncoding(filePath) {
4043
4046
  return entry;
4044
4047
  }
4045
4048
 
4049
+ // src/lib/path-sandbox.ts
4050
+ import fs2 from "fs/promises";
4051
+ import path2 from "path";
4052
+
4053
+ // src/lib/errors.ts
4054
+ var GbkToolError = class extends Error {
4055
+ code;
4056
+ cause;
4057
+ constructor(code, message, cause) {
4058
+ super(message);
4059
+ this.name = "GbkToolError";
4060
+ this.code = code;
4061
+ this.cause = cause;
4062
+ }
4063
+ };
4064
+ function createGbkError(code, message, cause) {
4065
+ return new GbkToolError(code, message, cause);
4066
+ }
4067
+ var createTextError = createGbkError;
4068
+
4069
+ // src/lib/path-sandbox.ts
4070
+ function resolveBaseDirectory(context) {
4071
+ const value = context.directory ?? process.cwd();
4072
+ return path2.isAbsolute(value) ? value : path2.resolve(process.cwd(), value);
4073
+ }
4074
+ function resolveWorkspaceRoot(context) {
4075
+ const value = context.worktree ?? context.directory ?? process.cwd();
4076
+ return path2.isAbsolute(value) ? value : path2.resolve(process.cwd(), value);
4077
+ }
4078
+ function resolveCandidatePath(filePath, context) {
4079
+ return path2.resolve(resolveBaseDirectory(context), filePath);
4080
+ }
4081
+ async function resolveExistingAnchor(filePath) {
4082
+ let current = filePath;
4083
+ while (true) {
4084
+ try {
4085
+ return await fs2.realpath(current);
4086
+ } catch {
4087
+ const parent = path2.dirname(current);
4088
+ if (parent === current) {
4089
+ throw createGbkError("GBK_IO_ERROR", `\u65E0\u6CD5\u89E3\u6790\u8DEF\u5F84\u951A\u70B9: ${filePath}`);
4090
+ }
4091
+ current = parent;
4092
+ }
4093
+ }
4094
+ }
4095
+ async function assertPathAllowed(filePath, context, allowExternal = false) {
4096
+ const candidatePath = resolveCandidatePath(filePath, context);
4097
+ const workspaceRoot = resolveWorkspaceRoot(context);
4098
+ if (!allowExternal) {
4099
+ const realWorkspaceRoot = await fs2.realpath(workspaceRoot);
4100
+ const realCandidateAnchor = await resolveExistingAnchor(candidatePath);
4101
+ const relative = path2.relative(realWorkspaceRoot, realCandidateAnchor);
4102
+ if (relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative)) {
4103
+ return { candidatePath, workspaceRoot };
4104
+ }
4105
+ throw createGbkError("GBK_PATH_OUTSIDE_ROOT", `\u76EE\u6807\u8DEF\u5F84\u8D85\u51FA\u5DE5\u4F5C\u76EE\u5F55\u8303\u56F4: ${candidatePath}`);
4106
+ }
4107
+ return { candidatePath, workspaceRoot };
4108
+ }
4109
+
4046
4110
  // node_modules/zod/v4/classic/external.js
4047
4111
  var external_exports = {};
4048
4112
  __export(external_exports, {
@@ -4774,10 +4838,10 @@ function mergeDefs(...defs) {
4774
4838
  function cloneDef(schema) {
4775
4839
  return mergeDefs(schema._zod.def);
4776
4840
  }
4777
- function getElementAtPath(obj, path5) {
4778
- if (!path5)
4841
+ function getElementAtPath(obj, path6) {
4842
+ if (!path6)
4779
4843
  return obj;
4780
- return path5.reduce((acc, key) => acc?.[key], obj);
4844
+ return path6.reduce((acc, key) => acc?.[key], obj);
4781
4845
  }
4782
4846
  function promiseAllObject(promisesObj) {
4783
4847
  const keys = Object.keys(promisesObj);
@@ -5138,11 +5202,11 @@ function aborted(x, startIndex = 0) {
5138
5202
  }
5139
5203
  return false;
5140
5204
  }
5141
- function prefixIssues(path5, issues) {
5205
+ function prefixIssues(path6, issues) {
5142
5206
  return issues.map((iss) => {
5143
5207
  var _a;
5144
5208
  (_a = iss).path ?? (_a.path = []);
5145
- iss.path.unshift(path5);
5209
+ iss.path.unshift(path6);
5146
5210
  return iss;
5147
5211
  });
5148
5212
  }
@@ -5310,7 +5374,7 @@ function treeifyError(error45, _mapper) {
5310
5374
  return issue2.message;
5311
5375
  };
5312
5376
  const result = { errors: [] };
5313
- const processError = (error46, path5 = []) => {
5377
+ const processError = (error46, path6 = []) => {
5314
5378
  var _a, _b;
5315
5379
  for (const issue2 of error46.issues) {
5316
5380
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -5320,7 +5384,7 @@ function treeifyError(error45, _mapper) {
5320
5384
  } else if (issue2.code === "invalid_element") {
5321
5385
  processError({ issues: issue2.issues }, issue2.path);
5322
5386
  } else {
5323
- const fullpath = [...path5, ...issue2.path];
5387
+ const fullpath = [...path6, ...issue2.path];
5324
5388
  if (fullpath.length === 0) {
5325
5389
  result.errors.push(mapper(issue2));
5326
5390
  continue;
@@ -5352,8 +5416,8 @@ function treeifyError(error45, _mapper) {
5352
5416
  }
5353
5417
  function toDotPath(_path) {
5354
5418
  const segs = [];
5355
- const path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5356
- for (const seg of path5) {
5419
+ const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5420
+ for (const seg of path6) {
5357
5421
  if (typeof seg === "number")
5358
5422
  segs.push(`[${seg}]`);
5359
5423
  else if (typeof seg === "symbol")
@@ -16597,67 +16661,6 @@ import crypto from "crypto";
16597
16661
  import { createReadStream } from "fs";
16598
16662
  import fs3 from "fs/promises";
16599
16663
  import path3 from "path";
16600
-
16601
- // src/lib/errors.ts
16602
- var GbkToolError = class extends Error {
16603
- code;
16604
- cause;
16605
- constructor(code, message, cause) {
16606
- super(message);
16607
- this.name = "GbkToolError";
16608
- this.code = code;
16609
- this.cause = cause;
16610
- }
16611
- };
16612
- function createGbkError(code, message, cause) {
16613
- return new GbkToolError(code, message, cause);
16614
- }
16615
- var createTextError = createGbkError;
16616
-
16617
- // src/lib/path-sandbox.ts
16618
- import fs2 from "fs/promises";
16619
- import path2 from "path";
16620
- function resolveBaseDirectory(context) {
16621
- const value = context.directory ?? process.cwd();
16622
- return path2.isAbsolute(value) ? value : path2.resolve(process.cwd(), value);
16623
- }
16624
- function resolveWorkspaceRoot(context) {
16625
- const value = context.worktree ?? context.directory ?? process.cwd();
16626
- return path2.isAbsolute(value) ? value : path2.resolve(process.cwd(), value);
16627
- }
16628
- function resolveCandidatePath(filePath, context) {
16629
- return path2.resolve(resolveBaseDirectory(context), filePath);
16630
- }
16631
- async function resolveExistingAnchor(filePath) {
16632
- let current = filePath;
16633
- while (true) {
16634
- try {
16635
- return await fs2.realpath(current);
16636
- } catch {
16637
- const parent = path2.dirname(current);
16638
- if (parent === current) {
16639
- throw createGbkError("GBK_IO_ERROR", `\u65E0\u6CD5\u89E3\u6790\u8DEF\u5F84\u951A\u70B9: ${filePath}`);
16640
- }
16641
- current = parent;
16642
- }
16643
- }
16644
- }
16645
- async function assertPathAllowed(filePath, context, allowExternal = false) {
16646
- const candidatePath = resolveCandidatePath(filePath, context);
16647
- const workspaceRoot = resolveWorkspaceRoot(context);
16648
- if (!allowExternal) {
16649
- const realWorkspaceRoot = await fs2.realpath(workspaceRoot);
16650
- const realCandidateAnchor = await resolveExistingAnchor(candidatePath);
16651
- const relative = path2.relative(realWorkspaceRoot, realCandidateAnchor);
16652
- if (relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative)) {
16653
- return { candidatePath, workspaceRoot };
16654
- }
16655
- throw createGbkError("GBK_PATH_OUTSIDE_ROOT", `\u76EE\u6807\u8DEF\u5F84\u8D85\u51FA\u5DE5\u4F5C\u76EE\u5F55\u8303\u56F4: ${candidatePath}`);
16656
- }
16657
- return { candidatePath, workspaceRoot };
16658
- }
16659
-
16660
- // src/lib/gbk-file.ts
16661
16664
  var STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
16662
16665
  var STREAM_READ_CHUNK_SIZE_BYTES = 1024 * 1024;
16663
16666
  var gbkLineIndexCache = /* @__PURE__ */ new Map();
@@ -18243,35 +18246,6 @@ var gbk_write_default = tool({
18243
18246
  }
18244
18247
  });
18245
18248
 
18246
- // src/plugin/text-guidance.ts
18247
- var TEXT_TOOL_SYSTEM_MARKER = "[opencode-gbk-tools:text-rules]";
18248
- var TEXT_TOOL_SYSTEM_PROMPT = [
18249
- TEXT_TOOL_SYSTEM_MARKER,
18250
- "\u6587\u672C\u6587\u4EF6\u5904\u7406\u89C4\u5219\uFF1A",
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",
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",
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",
18257
- "- \u53EA\u6709\u5728\u660E\u786E\u505A\u7CBE\u786E\u66FF\u6362\u65F6\uFF0C\u624D\u4F7F\u7528 oldString/newString\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"
18261
- ].join("\n");
18262
- function appendTextToolSystemPrompt(system) {
18263
- if (system.some((item) => item.includes(TEXT_TOOL_SYSTEM_MARKER))) {
18264
- return;
18265
- }
18266
- if (system.length === 0) {
18267
- system.push(TEXT_TOOL_SYSTEM_PROMPT);
18268
- return;
18269
- }
18270
- system[0] = `${system[0]}
18271
-
18272
- ${TEXT_TOOL_SYSTEM_PROMPT}`;
18273
- }
18274
-
18275
18249
  // src/lib/text-file.ts
18276
18250
  var import_iconv_lite2 = __toESM(require_lib(), 1);
18277
18251
  import crypto2 from "crypto";
@@ -19220,6 +19194,20 @@ async function ensureParentDirectory(parent, createDirectories) {
19220
19194
  throw error45;
19221
19195
  }
19222
19196
  }
19197
+ async function detectTextFileEncoding(input) {
19198
+ const detected = await detectReadableTextFile(input);
19199
+ const fileSize = typeof detected.stat.size === "bigint" ? Number(detected.stat.size) : detected.stat.size;
19200
+ return {
19201
+ filePath: detected.filePath,
19202
+ encoding: detected.encoding,
19203
+ requestedEncoding: detected.requestedEncoding,
19204
+ detectedEncoding: detected.detectedEncoding,
19205
+ confidence: detected.confidence,
19206
+ hasBom: detected.hasBom,
19207
+ bytesRead: Math.min(fileSize, TEXT_DETECTION_SAMPLE_BYTES),
19208
+ newlineStyle: "unknown"
19209
+ };
19210
+ }
19223
19211
  async function readTextFile(input) {
19224
19212
  const offset = normalizeOptionalPositiveInteger(input.offset, "offset") ?? 1;
19225
19213
  const limit = normalizeOptionalPositiveInteger(input.limit, "limit") ?? 2e3;
@@ -19497,6 +19485,35 @@ async function replaceTextFileText(input) {
19497
19485
  };
19498
19486
  }
19499
19487
 
19488
+ // src/plugin/text-guidance.ts
19489
+ var TEXT_TOOL_SYSTEM_MARKER = "[opencode-gbk-tools:text-rules]";
19490
+ var TEXT_TOOL_SYSTEM_PROMPT = [
19491
+ TEXT_TOOL_SYSTEM_MARKER,
19492
+ "\u6587\u672C\u6587\u4EF6\u5904\u7406\u89C4\u5219\uFF1A",
19493
+ "- \u666E\u901A UTF-8 / UTF-8 BOM / UTF-16 \u6587\u672C\uFF0C\u4F18\u5148\u4F7F\u7528 OpenCode \u5185\u7F6E read\u3001write\u3001edit\u3002",
19494
+ "- \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\uFF0C\u4E0D\u8981\u5148\u5C1D\u8BD5 text_* \u6216\u5185\u7F6E read/write/edit\u3002",
19495
+ "- \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",
19496
+ "- \u5BF9\u73B0\u6709 .txt / .cfg / .ini / .log \u7B49\u65E7\u6587\u672C\u6587\u4EF6\uFF0C\u53EA\u8981\u6000\u7591\u662F\u4E2D\u6587\u672C\u5730\u7F16\u7801\uFF0C\u4F18\u5148\u5148\u7528 gbk_read \u5224\u65AD\uFF0C\u4E0D\u8981\u5148 edit \u518D\u56E0\u4E3A\u5339\u914D\u5931\u8D25\u6216\u6587\u4EF6\u65F6\u95F4\u6233\u53D8\u5316\u800C\u56DE\u9000\u3002",
19497
+ "- \u5DF2\u786E\u8BA4\u6216\u9996\u6B21\u68C0\u6D4B\u5230\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\u4F1A\u76F4\u63A5\u62E6\u622A\u5185\u7F6E read/write/edit \u548C text_*\uFF0C\u5E76\u8981\u6C42\u6539\u7528 gbk_*\u3002",
19498
+ "- \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",
19499
+ "- \u53EA\u6709\u5728\u660E\u786E\u505A\u7CBE\u786E\u66FF\u6362\u65F6\uFF0C\u624D\u4F7F\u7528 oldString/newString\u3002",
19500
+ "- 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",
19501
+ '- \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',
19502
+ "- 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"
19503
+ ].join("\n");
19504
+ function appendTextToolSystemPrompt(system) {
19505
+ if (system.some((item) => item.includes(TEXT_TOOL_SYSTEM_MARKER))) {
19506
+ return;
19507
+ }
19508
+ if (system.length === 0) {
19509
+ system.push(TEXT_TOOL_SYSTEM_PROMPT);
19510
+ return;
19511
+ }
19512
+ system[0] = `${system[0]}
19513
+
19514
+ ${TEXT_TOOL_SYSTEM_PROMPT}`;
19515
+ }
19516
+
19500
19517
  // src/tools/text_edit.ts
19501
19518
  var text_edit_default = tool({
19502
19519
  description: `Edit text files with automatic encoding detection and preservation.
@@ -19641,7 +19658,8 @@ var MANAGED_TOOL_IDS = /* @__PURE__ */ new Set([
19641
19658
  "text_edit"
19642
19659
  ]);
19643
19660
  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"]);
19661
+ var TEXT_TOOL_IDS = /* @__PURE__ */ new Set(["text_read", "text_write", "text_edit"]);
19662
+ var ROUTED_TEXT_TOOL_IDS = /* @__PURE__ */ new Set([...BUILTIN_TEXT_TOOL_IDS, ...TEXT_TOOL_IDS]);
19645
19663
  function getToolFilePath(args) {
19646
19664
  if (!args || typeof args !== "object") {
19647
19665
  return null;
@@ -19649,6 +19667,35 @@ function getToolFilePath(args) {
19649
19667
  const filePath = args.filePath;
19650
19668
  return typeof filePath === "string" ? filePath : null;
19651
19669
  }
19670
+ function getToolAllowExternal(args) {
19671
+ if (!args || typeof args !== "object") {
19672
+ return false;
19673
+ }
19674
+ return args.allowExternal === true;
19675
+ }
19676
+ function normalizeSessionFilePath(filePath, directory, worktree) {
19677
+ const resolved = path5.normalize(resolveCandidatePath(filePath, { directory, worktree }));
19678
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
19679
+ }
19680
+ function buildGbkRoutingMessage(filePath, encoding) {
19681
+ return `\u6587\u4EF6\u68C0\u6D4B\u4E3A ${encoding.toUpperCase()} \u7F16\u7801\uFF0C\u8BF7\u76F4\u63A5\u6539\u7528 gbk_read\u3001gbk_write\u3001gbk_edit \u6216 gbk_search\uFF0C\u4E0D\u8981\u5148\u4F7F\u7528\u5185\u7F6E read/write/edit \u6216 text_*\uFF1A${filePath}`;
19682
+ }
19683
+ function buildTextEditSessionRoutingMessage(filePath) {
19684
+ return `\u5F53\u524D\u4F1A\u8BDD\u5DF2\u5BF9\u8BE5\u6587\u4EF6\u4F7F\u7528 text_edit\uFF0C\u8BF7\u7EE7\u7EED\u4F7F\u7528 text_read\u3001text_write\u3001text_edit \u6216\u76F4\u63A5\u6539\u7528 gbk_*\uFF1B\u4E0D\u8981\u518D\u5207\u56DE\u5185\u7F6E read/write/edit\uFF0C\u4EE5\u514D\u89E6\u53D1\u6587\u4EF6\u65B0\u9C9C\u5EA6\u68C0\u67E5\u51B2\u7A81\uFF1A${filePath}`;
19685
+ }
19686
+ async function detectExistingGbkEncoding(filePath, allowExternal, directory, worktree) {
19687
+ try {
19688
+ const detected = await detectTextFileEncoding({
19689
+ filePath,
19690
+ encoding: "auto",
19691
+ allowExternal,
19692
+ context: { directory, worktree }
19693
+ });
19694
+ return isRememberedGbkEncoding(detected.encoding) ? detected.encoding : null;
19695
+ } catch {
19696
+ return null;
19697
+ }
19698
+ }
19652
19699
  async function maybePersistRememberedEncoding(metadata) {
19653
19700
  const filePath = typeof metadata.filePath === "string" ? metadata.filePath : null;
19654
19701
  const encoding = metadata.encoding;
@@ -19712,7 +19759,25 @@ async function maybeAutoSummarizeSession(client, directory, input) {
19712
19759
  markAutoSummarizeFinished(input.sessionID, false);
19713
19760
  }
19714
19761
  }
19715
- function createOpencodeGbkHooks(client, directory) {
19762
+ function createOpencodeGbkHooks(client, directory, worktree) {
19763
+ const sessionTextEditedFiles = /* @__PURE__ */ new Map();
19764
+ function rememberSessionTextEditFile(sessionID, normalizedFilePath) {
19765
+ if (!sessionID) {
19766
+ return;
19767
+ }
19768
+ let files = sessionTextEditedFiles.get(sessionID);
19769
+ if (!files) {
19770
+ files = /* @__PURE__ */ new Set();
19771
+ sessionTextEditedFiles.set(sessionID, files);
19772
+ }
19773
+ files.add(normalizedFilePath);
19774
+ }
19775
+ function hasSessionTextEditFile(sessionID, normalizedFilePath) {
19776
+ if (!sessionID) {
19777
+ return false;
19778
+ }
19779
+ return sessionTextEditedFiles.get(sessionID)?.has(normalizedFilePath) ?? false;
19780
+ }
19716
19781
  return {
19717
19782
  tool: {
19718
19783
  gbk_read: gbk_read_default,
@@ -19743,19 +19808,32 @@ function createOpencodeGbkHooks(client, directory) {
19743
19808
  if (!filePath) {
19744
19809
  return;
19745
19810
  }
19746
- const remembered = await getRememberedGbkEncoding(filePath);
19747
- if (!remembered) {
19811
+ if (!ROUTED_TEXT_TOOL_IDS.has(input.tool)) {
19748
19812
  return;
19749
19813
  }
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
- }
19814
+ const normalizedFilePath = normalizeSessionFilePath(filePath, directory, worktree);
19815
+ if (BUILTIN_TEXT_TOOL_IDS.has(input.tool) && hasSessionTextEditFile(input.sessionID, normalizedFilePath)) {
19816
+ throw new Error(buildTextEditSessionRoutingMessage(filePath));
19817
+ }
19818
+ const resolvedFilePath = resolveCandidatePath(filePath, { directory, worktree });
19819
+ const remembered = await getRememberedGbkEncoding(resolvedFilePath);
19820
+ if (remembered) {
19821
+ throw new Error(buildGbkRoutingMessage(filePath, remembered.encoding));
19822
+ }
19823
+ const detectedEncoding = await detectExistingGbkEncoding(
19824
+ resolvedFilePath,
19825
+ getToolAllowExternal(output.args),
19826
+ directory,
19827
+ worktree
19828
+ );
19829
+ if (!detectedEncoding) {
19754
19830
  return;
19755
19831
  }
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}`);
19832
+ try {
19833
+ await rememberGbkEncoding(resolvedFilePath, detectedEncoding);
19834
+ } catch {
19758
19835
  }
19836
+ throw new Error(buildGbkRoutingMessage(filePath, detectedEncoding));
19759
19837
  },
19760
19838
  async "tool.execute.after"(input, output) {
19761
19839
  if (!MANAGED_TOOL_IDS.has(input.tool)) return;
@@ -19772,6 +19850,12 @@ function createOpencodeGbkHooks(client, directory) {
19772
19850
  if (typeof metadata.diffPreview === "string") {
19773
19851
  metadata.diffPreview = truncateMetadataPreview(metadata.diffPreview, input.sessionID);
19774
19852
  }
19853
+ if (input.tool === "text_edit" && typeof metadata.filePath === "string") {
19854
+ rememberSessionTextEditFile(
19855
+ input.sessionID,
19856
+ normalizeSessionFilePath(metadata.filePath, directory, worktree)
19857
+ );
19858
+ }
19775
19859
  try {
19776
19860
  await maybePersistRememberedEncoding(metadata);
19777
19861
  } catch {
@@ -19803,7 +19887,7 @@ function createOpencodeGbkHooks(client, directory) {
19803
19887
  var pluginModule = {
19804
19888
  id: "opencode-gbk-tools",
19805
19889
  async server(ctx) {
19806
- return createOpencodeGbkHooks(ctx.client, ctx.directory);
19890
+ return createOpencodeGbkHooks(ctx.client, ctx.directory, ctx.worktree);
19807
19891
  }
19808
19892
  };
19809
19893
  var plugin_default = pluginModule;
@@ -3816,6 +3816,9 @@ var require_lib = __commonJS({
3816
3816
  }
3817
3817
  });
3818
3818
 
3819
+ // src/plugin/index.ts
3820
+ import path5 from "path";
3821
+
3819
3822
  // src/lib/session-pressure.ts
3820
3823
  var AUTO_SUMMARIZE_PRESSURE_RATIO = 0.85;
3821
3824
  var AUTO_SUMMARIZE_COOLDOWN_MS = 6e4;
@@ -4043,6 +4046,67 @@ async function getRememberedGbkEncoding(filePath) {
4043
4046
  return entry;
4044
4047
  }
4045
4048
 
4049
+ // src/lib/path-sandbox.ts
4050
+ import fs2 from "fs/promises";
4051
+ import path2 from "path";
4052
+
4053
+ // src/lib/errors.ts
4054
+ var GbkToolError = class extends Error {
4055
+ code;
4056
+ cause;
4057
+ constructor(code, message, cause) {
4058
+ super(message);
4059
+ this.name = "GbkToolError";
4060
+ this.code = code;
4061
+ this.cause = cause;
4062
+ }
4063
+ };
4064
+ function createGbkError(code, message, cause) {
4065
+ return new GbkToolError(code, message, cause);
4066
+ }
4067
+ var createTextError = createGbkError;
4068
+
4069
+ // src/lib/path-sandbox.ts
4070
+ function resolveBaseDirectory(context) {
4071
+ const value = context.directory ?? process.cwd();
4072
+ return path2.isAbsolute(value) ? value : path2.resolve(process.cwd(), value);
4073
+ }
4074
+ function resolveWorkspaceRoot(context) {
4075
+ const value = context.worktree ?? context.directory ?? process.cwd();
4076
+ return path2.isAbsolute(value) ? value : path2.resolve(process.cwd(), value);
4077
+ }
4078
+ function resolveCandidatePath(filePath, context) {
4079
+ return path2.resolve(resolveBaseDirectory(context), filePath);
4080
+ }
4081
+ async function resolveExistingAnchor(filePath) {
4082
+ let current = filePath;
4083
+ while (true) {
4084
+ try {
4085
+ return await fs2.realpath(current);
4086
+ } catch {
4087
+ const parent = path2.dirname(current);
4088
+ if (parent === current) {
4089
+ throw createGbkError("GBK_IO_ERROR", `\u65E0\u6CD5\u89E3\u6790\u8DEF\u5F84\u951A\u70B9: ${filePath}`);
4090
+ }
4091
+ current = parent;
4092
+ }
4093
+ }
4094
+ }
4095
+ async function assertPathAllowed(filePath, context, allowExternal = false) {
4096
+ const candidatePath = resolveCandidatePath(filePath, context);
4097
+ const workspaceRoot = resolveWorkspaceRoot(context);
4098
+ if (!allowExternal) {
4099
+ const realWorkspaceRoot = await fs2.realpath(workspaceRoot);
4100
+ const realCandidateAnchor = await resolveExistingAnchor(candidatePath);
4101
+ const relative = path2.relative(realWorkspaceRoot, realCandidateAnchor);
4102
+ if (relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative)) {
4103
+ return { candidatePath, workspaceRoot };
4104
+ }
4105
+ throw createGbkError("GBK_PATH_OUTSIDE_ROOT", `\u76EE\u6807\u8DEF\u5F84\u8D85\u51FA\u5DE5\u4F5C\u76EE\u5F55\u8303\u56F4: ${candidatePath}`);
4106
+ }
4107
+ return { candidatePath, workspaceRoot };
4108
+ }
4109
+
4046
4110
  // node_modules/zod/v4/classic/external.js
4047
4111
  var external_exports = {};
4048
4112
  __export(external_exports, {
@@ -4774,10 +4838,10 @@ function mergeDefs(...defs) {
4774
4838
  function cloneDef(schema) {
4775
4839
  return mergeDefs(schema._zod.def);
4776
4840
  }
4777
- function getElementAtPath(obj, path5) {
4778
- if (!path5)
4841
+ function getElementAtPath(obj, path6) {
4842
+ if (!path6)
4779
4843
  return obj;
4780
- return path5.reduce((acc, key) => acc?.[key], obj);
4844
+ return path6.reduce((acc, key) => acc?.[key], obj);
4781
4845
  }
4782
4846
  function promiseAllObject(promisesObj) {
4783
4847
  const keys = Object.keys(promisesObj);
@@ -5138,11 +5202,11 @@ function aborted(x, startIndex = 0) {
5138
5202
  }
5139
5203
  return false;
5140
5204
  }
5141
- function prefixIssues(path5, issues) {
5205
+ function prefixIssues(path6, issues) {
5142
5206
  return issues.map((iss) => {
5143
5207
  var _a;
5144
5208
  (_a = iss).path ?? (_a.path = []);
5145
- iss.path.unshift(path5);
5209
+ iss.path.unshift(path6);
5146
5210
  return iss;
5147
5211
  });
5148
5212
  }
@@ -5310,7 +5374,7 @@ function treeifyError(error45, _mapper) {
5310
5374
  return issue2.message;
5311
5375
  };
5312
5376
  const result = { errors: [] };
5313
- const processError = (error46, path5 = []) => {
5377
+ const processError = (error46, path6 = []) => {
5314
5378
  var _a, _b;
5315
5379
  for (const issue2 of error46.issues) {
5316
5380
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -5320,7 +5384,7 @@ function treeifyError(error45, _mapper) {
5320
5384
  } else if (issue2.code === "invalid_element") {
5321
5385
  processError({ issues: issue2.issues }, issue2.path);
5322
5386
  } else {
5323
- const fullpath = [...path5, ...issue2.path];
5387
+ const fullpath = [...path6, ...issue2.path];
5324
5388
  if (fullpath.length === 0) {
5325
5389
  result.errors.push(mapper(issue2));
5326
5390
  continue;
@@ -5352,8 +5416,8 @@ function treeifyError(error45, _mapper) {
5352
5416
  }
5353
5417
  function toDotPath(_path) {
5354
5418
  const segs = [];
5355
- const path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5356
- for (const seg of path5) {
5419
+ const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5420
+ for (const seg of path6) {
5357
5421
  if (typeof seg === "number")
5358
5422
  segs.push(`[${seg}]`);
5359
5423
  else if (typeof seg === "symbol")
@@ -16597,67 +16661,6 @@ import crypto from "crypto";
16597
16661
  import { createReadStream } from "fs";
16598
16662
  import fs3 from "fs/promises";
16599
16663
  import path3 from "path";
16600
-
16601
- // src/lib/errors.ts
16602
- var GbkToolError = class extends Error {
16603
- code;
16604
- cause;
16605
- constructor(code, message, cause) {
16606
- super(message);
16607
- this.name = "GbkToolError";
16608
- this.code = code;
16609
- this.cause = cause;
16610
- }
16611
- };
16612
- function createGbkError(code, message, cause) {
16613
- return new GbkToolError(code, message, cause);
16614
- }
16615
- var createTextError = createGbkError;
16616
-
16617
- // src/lib/path-sandbox.ts
16618
- import fs2 from "fs/promises";
16619
- import path2 from "path";
16620
- function resolveBaseDirectory(context) {
16621
- const value = context.directory ?? process.cwd();
16622
- return path2.isAbsolute(value) ? value : path2.resolve(process.cwd(), value);
16623
- }
16624
- function resolveWorkspaceRoot(context) {
16625
- const value = context.worktree ?? context.directory ?? process.cwd();
16626
- return path2.isAbsolute(value) ? value : path2.resolve(process.cwd(), value);
16627
- }
16628
- function resolveCandidatePath(filePath, context) {
16629
- return path2.resolve(resolveBaseDirectory(context), filePath);
16630
- }
16631
- async function resolveExistingAnchor(filePath) {
16632
- let current = filePath;
16633
- while (true) {
16634
- try {
16635
- return await fs2.realpath(current);
16636
- } catch {
16637
- const parent = path2.dirname(current);
16638
- if (parent === current) {
16639
- throw createGbkError("GBK_IO_ERROR", `\u65E0\u6CD5\u89E3\u6790\u8DEF\u5F84\u951A\u70B9: ${filePath}`);
16640
- }
16641
- current = parent;
16642
- }
16643
- }
16644
- }
16645
- async function assertPathAllowed(filePath, context, allowExternal = false) {
16646
- const candidatePath = resolveCandidatePath(filePath, context);
16647
- const workspaceRoot = resolveWorkspaceRoot(context);
16648
- if (!allowExternal) {
16649
- const realWorkspaceRoot = await fs2.realpath(workspaceRoot);
16650
- const realCandidateAnchor = await resolveExistingAnchor(candidatePath);
16651
- const relative = path2.relative(realWorkspaceRoot, realCandidateAnchor);
16652
- if (relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative)) {
16653
- return { candidatePath, workspaceRoot };
16654
- }
16655
- throw createGbkError("GBK_PATH_OUTSIDE_ROOT", `\u76EE\u6807\u8DEF\u5F84\u8D85\u51FA\u5DE5\u4F5C\u76EE\u5F55\u8303\u56F4: ${candidatePath}`);
16656
- }
16657
- return { candidatePath, workspaceRoot };
16658
- }
16659
-
16660
- // src/lib/gbk-file.ts
16661
16664
  var STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
16662
16665
  var STREAM_READ_CHUNK_SIZE_BYTES = 1024 * 1024;
16663
16666
  var gbkLineIndexCache = /* @__PURE__ */ new Map();
@@ -18243,35 +18246,6 @@ var gbk_write_default = tool({
18243
18246
  }
18244
18247
  });
18245
18248
 
18246
- // src/plugin/text-guidance.ts
18247
- var TEXT_TOOL_SYSTEM_MARKER = "[opencode-gbk-tools:text-rules]";
18248
- var TEXT_TOOL_SYSTEM_PROMPT = [
18249
- TEXT_TOOL_SYSTEM_MARKER,
18250
- "\u6587\u672C\u6587\u4EF6\u5904\u7406\u89C4\u5219\uFF1A",
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",
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",
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",
18257
- "- \u53EA\u6709\u5728\u660E\u786E\u505A\u7CBE\u786E\u66FF\u6362\u65F6\uFF0C\u624D\u4F7F\u7528 oldString/newString\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"
18261
- ].join("\n");
18262
- function appendTextToolSystemPrompt(system) {
18263
- if (system.some((item) => item.includes(TEXT_TOOL_SYSTEM_MARKER))) {
18264
- return;
18265
- }
18266
- if (system.length === 0) {
18267
- system.push(TEXT_TOOL_SYSTEM_PROMPT);
18268
- return;
18269
- }
18270
- system[0] = `${system[0]}
18271
-
18272
- ${TEXT_TOOL_SYSTEM_PROMPT}`;
18273
- }
18274
-
18275
18249
  // src/lib/text-file.ts
18276
18250
  var import_iconv_lite2 = __toESM(require_lib(), 1);
18277
18251
  import crypto2 from "crypto";
@@ -19220,6 +19194,20 @@ async function ensureParentDirectory(parent, createDirectories) {
19220
19194
  throw error45;
19221
19195
  }
19222
19196
  }
19197
+ async function detectTextFileEncoding(input) {
19198
+ const detected = await detectReadableTextFile(input);
19199
+ const fileSize = typeof detected.stat.size === "bigint" ? Number(detected.stat.size) : detected.stat.size;
19200
+ return {
19201
+ filePath: detected.filePath,
19202
+ encoding: detected.encoding,
19203
+ requestedEncoding: detected.requestedEncoding,
19204
+ detectedEncoding: detected.detectedEncoding,
19205
+ confidence: detected.confidence,
19206
+ hasBom: detected.hasBom,
19207
+ bytesRead: Math.min(fileSize, TEXT_DETECTION_SAMPLE_BYTES),
19208
+ newlineStyle: "unknown"
19209
+ };
19210
+ }
19223
19211
  async function readTextFile(input) {
19224
19212
  const offset = normalizeOptionalPositiveInteger(input.offset, "offset") ?? 1;
19225
19213
  const limit = normalizeOptionalPositiveInteger(input.limit, "limit") ?? 2e3;
@@ -19497,6 +19485,35 @@ async function replaceTextFileText(input) {
19497
19485
  };
19498
19486
  }
19499
19487
 
19488
+ // src/plugin/text-guidance.ts
19489
+ var TEXT_TOOL_SYSTEM_MARKER = "[opencode-gbk-tools:text-rules]";
19490
+ var TEXT_TOOL_SYSTEM_PROMPT = [
19491
+ TEXT_TOOL_SYSTEM_MARKER,
19492
+ "\u6587\u672C\u6587\u4EF6\u5904\u7406\u89C4\u5219\uFF1A",
19493
+ "- \u666E\u901A UTF-8 / UTF-8 BOM / UTF-16 \u6587\u672C\uFF0C\u4F18\u5148\u4F7F\u7528 OpenCode \u5185\u7F6E read\u3001write\u3001edit\u3002",
19494
+ "- \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\uFF0C\u4E0D\u8981\u5148\u5C1D\u8BD5 text_* \u6216\u5185\u7F6E read/write/edit\u3002",
19495
+ "- \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",
19496
+ "- \u5BF9\u73B0\u6709 .txt / .cfg / .ini / .log \u7B49\u65E7\u6587\u672C\u6587\u4EF6\uFF0C\u53EA\u8981\u6000\u7591\u662F\u4E2D\u6587\u672C\u5730\u7F16\u7801\uFF0C\u4F18\u5148\u5148\u7528 gbk_read \u5224\u65AD\uFF0C\u4E0D\u8981\u5148 edit \u518D\u56E0\u4E3A\u5339\u914D\u5931\u8D25\u6216\u6587\u4EF6\u65F6\u95F4\u6233\u53D8\u5316\u800C\u56DE\u9000\u3002",
19497
+ "- \u5DF2\u786E\u8BA4\u6216\u9996\u6B21\u68C0\u6D4B\u5230\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\u4F1A\u76F4\u63A5\u62E6\u622A\u5185\u7F6E read/write/edit \u548C text_*\uFF0C\u5E76\u8981\u6C42\u6539\u7528 gbk_*\u3002",
19498
+ "- \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",
19499
+ "- \u53EA\u6709\u5728\u660E\u786E\u505A\u7CBE\u786E\u66FF\u6362\u65F6\uFF0C\u624D\u4F7F\u7528 oldString/newString\u3002",
19500
+ "- 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",
19501
+ '- \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',
19502
+ "- 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"
19503
+ ].join("\n");
19504
+ function appendTextToolSystemPrompt(system) {
19505
+ if (system.some((item) => item.includes(TEXT_TOOL_SYSTEM_MARKER))) {
19506
+ return;
19507
+ }
19508
+ if (system.length === 0) {
19509
+ system.push(TEXT_TOOL_SYSTEM_PROMPT);
19510
+ return;
19511
+ }
19512
+ system[0] = `${system[0]}
19513
+
19514
+ ${TEXT_TOOL_SYSTEM_PROMPT}`;
19515
+ }
19516
+
19500
19517
  // src/tools/text_edit.ts
19501
19518
  var text_edit_default = tool({
19502
19519
  description: `Edit text files with automatic encoding detection and preservation.
@@ -19641,7 +19658,8 @@ var MANAGED_TOOL_IDS = /* @__PURE__ */ new Set([
19641
19658
  "text_edit"
19642
19659
  ]);
19643
19660
  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"]);
19661
+ var TEXT_TOOL_IDS = /* @__PURE__ */ new Set(["text_read", "text_write", "text_edit"]);
19662
+ var ROUTED_TEXT_TOOL_IDS = /* @__PURE__ */ new Set([...BUILTIN_TEXT_TOOL_IDS, ...TEXT_TOOL_IDS]);
19645
19663
  function getToolFilePath(args) {
19646
19664
  if (!args || typeof args !== "object") {
19647
19665
  return null;
@@ -19649,6 +19667,35 @@ function getToolFilePath(args) {
19649
19667
  const filePath = args.filePath;
19650
19668
  return typeof filePath === "string" ? filePath : null;
19651
19669
  }
19670
+ function getToolAllowExternal(args) {
19671
+ if (!args || typeof args !== "object") {
19672
+ return false;
19673
+ }
19674
+ return args.allowExternal === true;
19675
+ }
19676
+ function normalizeSessionFilePath(filePath, directory, worktree) {
19677
+ const resolved = path5.normalize(resolveCandidatePath(filePath, { directory, worktree }));
19678
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
19679
+ }
19680
+ function buildGbkRoutingMessage(filePath, encoding) {
19681
+ return `\u6587\u4EF6\u68C0\u6D4B\u4E3A ${encoding.toUpperCase()} \u7F16\u7801\uFF0C\u8BF7\u76F4\u63A5\u6539\u7528 gbk_read\u3001gbk_write\u3001gbk_edit \u6216 gbk_search\uFF0C\u4E0D\u8981\u5148\u4F7F\u7528\u5185\u7F6E read/write/edit \u6216 text_*\uFF1A${filePath}`;
19682
+ }
19683
+ function buildTextEditSessionRoutingMessage(filePath) {
19684
+ return `\u5F53\u524D\u4F1A\u8BDD\u5DF2\u5BF9\u8BE5\u6587\u4EF6\u4F7F\u7528 text_edit\uFF0C\u8BF7\u7EE7\u7EED\u4F7F\u7528 text_read\u3001text_write\u3001text_edit \u6216\u76F4\u63A5\u6539\u7528 gbk_*\uFF1B\u4E0D\u8981\u518D\u5207\u56DE\u5185\u7F6E read/write/edit\uFF0C\u4EE5\u514D\u89E6\u53D1\u6587\u4EF6\u65B0\u9C9C\u5EA6\u68C0\u67E5\u51B2\u7A81\uFF1A${filePath}`;
19685
+ }
19686
+ async function detectExistingGbkEncoding(filePath, allowExternal, directory, worktree) {
19687
+ try {
19688
+ const detected = await detectTextFileEncoding({
19689
+ filePath,
19690
+ encoding: "auto",
19691
+ allowExternal,
19692
+ context: { directory, worktree }
19693
+ });
19694
+ return isRememberedGbkEncoding(detected.encoding) ? detected.encoding : null;
19695
+ } catch {
19696
+ return null;
19697
+ }
19698
+ }
19652
19699
  async function maybePersistRememberedEncoding(metadata) {
19653
19700
  const filePath = typeof metadata.filePath === "string" ? metadata.filePath : null;
19654
19701
  const encoding = metadata.encoding;
@@ -19712,7 +19759,25 @@ async function maybeAutoSummarizeSession(client, directory, input) {
19712
19759
  markAutoSummarizeFinished(input.sessionID, false);
19713
19760
  }
19714
19761
  }
19715
- function createOpencodeGbkHooks(client, directory) {
19762
+ function createOpencodeGbkHooks(client, directory, worktree) {
19763
+ const sessionTextEditedFiles = /* @__PURE__ */ new Map();
19764
+ function rememberSessionTextEditFile(sessionID, normalizedFilePath) {
19765
+ if (!sessionID) {
19766
+ return;
19767
+ }
19768
+ let files = sessionTextEditedFiles.get(sessionID);
19769
+ if (!files) {
19770
+ files = /* @__PURE__ */ new Set();
19771
+ sessionTextEditedFiles.set(sessionID, files);
19772
+ }
19773
+ files.add(normalizedFilePath);
19774
+ }
19775
+ function hasSessionTextEditFile(sessionID, normalizedFilePath) {
19776
+ if (!sessionID) {
19777
+ return false;
19778
+ }
19779
+ return sessionTextEditedFiles.get(sessionID)?.has(normalizedFilePath) ?? false;
19780
+ }
19716
19781
  return {
19717
19782
  tool: {
19718
19783
  gbk_read: gbk_read_default,
@@ -19743,19 +19808,32 @@ function createOpencodeGbkHooks(client, directory) {
19743
19808
  if (!filePath) {
19744
19809
  return;
19745
19810
  }
19746
- const remembered = await getRememberedGbkEncoding(filePath);
19747
- if (!remembered) {
19811
+ if (!ROUTED_TEXT_TOOL_IDS.has(input.tool)) {
19748
19812
  return;
19749
19813
  }
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
- }
19814
+ const normalizedFilePath = normalizeSessionFilePath(filePath, directory, worktree);
19815
+ if (BUILTIN_TEXT_TOOL_IDS.has(input.tool) && hasSessionTextEditFile(input.sessionID, normalizedFilePath)) {
19816
+ throw new Error(buildTextEditSessionRoutingMessage(filePath));
19817
+ }
19818
+ const resolvedFilePath = resolveCandidatePath(filePath, { directory, worktree });
19819
+ const remembered = await getRememberedGbkEncoding(resolvedFilePath);
19820
+ if (remembered) {
19821
+ throw new Error(buildGbkRoutingMessage(filePath, remembered.encoding));
19822
+ }
19823
+ const detectedEncoding = await detectExistingGbkEncoding(
19824
+ resolvedFilePath,
19825
+ getToolAllowExternal(output.args),
19826
+ directory,
19827
+ worktree
19828
+ );
19829
+ if (!detectedEncoding) {
19754
19830
  return;
19755
19831
  }
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}`);
19832
+ try {
19833
+ await rememberGbkEncoding(resolvedFilePath, detectedEncoding);
19834
+ } catch {
19758
19835
  }
19836
+ throw new Error(buildGbkRoutingMessage(filePath, detectedEncoding));
19759
19837
  },
19760
19838
  async "tool.execute.after"(input, output) {
19761
19839
  if (!MANAGED_TOOL_IDS.has(input.tool)) return;
@@ -19772,6 +19850,12 @@ function createOpencodeGbkHooks(client, directory) {
19772
19850
  if (typeof metadata.diffPreview === "string") {
19773
19851
  metadata.diffPreview = truncateMetadataPreview(metadata.diffPreview, input.sessionID);
19774
19852
  }
19853
+ if (input.tool === "text_edit" && typeof metadata.filePath === "string") {
19854
+ rememberSessionTextEditFile(
19855
+ input.sessionID,
19856
+ normalizeSessionFilePath(metadata.filePath, directory, worktree)
19857
+ );
19858
+ }
19775
19859
  try {
19776
19860
  await maybePersistRememberedEncoding(metadata);
19777
19861
  } catch {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "manifestVersion": 1,
3
3
  "packageName": "opencode-gbk-tools",
4
- "packageVersion": "0.1.29",
4
+ "packageVersion": "0.1.30",
5
5
  "artifacts": [
6
6
  {
7
7
  "relativePath": "plugins/opencode-gbk-tools.js",
8
8
  "kind": "plugin",
9
- "expectedHash": "0b1824ee451a19ab284a2840f35ed8a1867ef14d1c697d353ca611f6e8f2d8d5",
9
+ "expectedHash": "ce0215b44f5166b283e083969533307a4fc54f62e594dc11c1ca669d6c9b495e",
10
10
  "hashAlgorithm": "sha256"
11
11
  },
12
12
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gbk-tools",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "Auto-encoding text tools plus GBK/GB18030 tools for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin/index.js",