opencode-gbk-tools 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,24 @@
13
13
 
14
14
  推荐方式:直接作为 OpenCode npm plugin 加载工具。
15
15
 
16
+ 最方便的一键接入:
17
+
18
+ ```bash
19
+ npx opencode-gbk-tools setup --project
20
+ ```
21
+
22
+ 这条命令会自动:
23
+
24
+ - 在当前项目写入或更新 `opencode.json`
25
+ - 自动加入 `plugin: ["opencode-gbk-tools"]`
26
+ - 安装 `.opencode/agents/gbk-engine.md`
27
+
28
+ 如果你想装到用户全局 OpenCode 配置:
29
+
30
+ ```bash
31
+ npx opencode-gbk-tools setup
32
+ ```
33
+
16
34
  `opencode.json`:
17
35
 
18
36
  ```json
@@ -30,6 +48,8 @@
30
48
 
31
49
  如果你还需要预置 `gbk-engine` agent,请继续使用下面的 CLI 安装方式把 agent 文件写入本地配置目录。
32
50
 
51
+ 推荐优先使用 `setup`,`install` 更适合手动拆分安装流程。
52
+
33
53
  一次性使用:
34
54
 
35
55
  ```bash
@@ -41,6 +61,7 @@ npx opencode-gbk-tools install
41
61
  ```bash
42
62
  npm install -g opencode-gbk-tools
43
63
  opencode-gbk install
64
+ opencode-gbk setup
44
65
  ```
45
66
 
46
67
  安装到当前项目:
@@ -57,6 +78,9 @@ opencode-gbk install --project
57
78
  ## CLI
58
79
 
59
80
  ```bash
81
+ opencode-gbk setup
82
+ opencode-gbk setup --project
83
+
60
84
  opencode-gbk install
61
85
  opencode-gbk install --project
62
86
  opencode-gbk install --force
@@ -75,6 +99,7 @@ opencode-gbk doctor --project
75
99
  说明:
76
100
 
77
101
  - npm `plugin` 方式负责注册 `gbk_read`、`gbk_write`、`gbk_edit`
102
+ - `setup` 会自动写入 `plugin` 配置并安装 `gbk-engine`
78
103
  - `gbk-engine` 仍通过 CLI 安装到 `.opencode/agents/` 或 `~/.config/opencode/agents/`
79
104
  - 如果你只需要工具,不需要 agent,只配置 `plugin` 即可
80
105
 
@@ -93,6 +118,14 @@ opencode-gbk doctor --project
93
118
  - `edit: deny` 在 OpenCode 中会一起限制内置 `write`、`patch`、`multiedit`
94
119
  - 对无法映射的字符沿用 `iconv-lite` 默认替代行为
95
120
 
121
+ ## 大文件建议
122
+
123
+ - `gbk_read` 支持 `tail` 预览最后 N 行
124
+ - `gbk_read` 返回 `fileSize`、`newlineStyle`、`streamed`、`truncated`
125
+ - `gbk_edit` 支持 `startLine/endLine`
126
+ - `gbk_edit` 支持 `startAnchor/endAnchor`
127
+ - 大文件会自动走更省内存的流式路径
128
+
96
129
  ## 发布
97
130
 
98
131
  ```bash
@@ -18,6 +18,8 @@ permission:
18
18
  - 读取文件时优先使用 `gbk_read`
19
19
  - 创建或覆盖文件时优先使用 `gbk_write`
20
20
  - 修改已有文件时优先使用 `gbk_edit`
21
+ - 大文件读取优先使用 `gbk_read` 的分页或 `tail` 能力,避免一次读取过多内容
22
+ - 修改时优先缩小编辑范围:能用 `startLine/endLine` 或 `startAnchor/endAnchor` 就不要全文替换
21
23
  - 文件发现可使用 `glob`,但文件内容读取必须使用 `gbk_read`
22
24
  - 禁止依赖内置 `read`、`grep`、`write`、`edit`、`patch`
23
25
  - `edit: deny` 同时覆盖内置 `write`、`patch`、`multiedit`
package/dist/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/cli/index.ts
2
- import path6 from "path";
2
+ import path7 from "path";
3
3
  import { fileURLToPath } from "url";
4
4
 
5
5
  // src/cli/install.ts
@@ -60,9 +60,11 @@ function resolveTargetBase(target, cwd) {
60
60
  // src/cli/install.ts
61
61
  async function installCommand(args) {
62
62
  const targetBase = resolveTargetBase(args.target, args.cwd);
63
+ const allowedArtifacts = new Set(args.artifacts ?? ["tool", "agent"]);
63
64
  const releaseManifest = JSON.parse(await fs3.readFile(path3.join(args.packageRoot, "dist", "release-manifest.json"), "utf8"));
65
+ const selectedArtifacts = releaseManifest.artifacts.filter((artifact) => allowedArtifacts.has(artifact.kind));
64
66
  const existingManifest = await loadInstalledManifest(targetBase);
65
- for (const artifact of releaseManifest.artifacts) {
67
+ for (const artifact of selectedArtifacts) {
66
68
  const targetPath = path3.join(targetBase, artifact.relativePath);
67
69
  if (await pathExists(targetPath) && !existingManifest && !args.force) {
68
70
  throw new Error(`\u68C0\u6D4B\u5230\u672A\u53D7\u7BA1\u6587\u4EF6\u51B2\u7A81: ${targetPath}`);
@@ -78,13 +80,13 @@ async function installCommand(args) {
78
80
  targetType: args.target,
79
81
  targetBase,
80
82
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
81
- files: releaseManifest.artifacts.map((artifact) => ({
83
+ files: selectedArtifacts.map((artifact) => ({
82
84
  ...artifact,
83
85
  installedByVersion: releaseManifest.packageVersion
84
86
  }))
85
87
  };
86
88
  await writeInstalledManifest(targetBase, installedManifest);
87
- return { targetBase };
89
+ return { targetBase, installedFiles: selectedArtifacts.length };
88
90
  }
89
91
 
90
92
  // src/cli/uninstall.ts
@@ -131,13 +133,68 @@ async function doctorCommand(args) {
131
133
  return { ok: issues.length === 0, issues };
132
134
  }
133
135
 
136
+ // src/cli/setup.ts
137
+ import fs6 from "fs/promises";
138
+ import path6 from "path";
139
+ import os2 from "os";
140
+ import { applyEdits, modify, parse } from "jsonc-parser";
141
+ async function resolveConfigFile(baseDir) {
142
+ const jsoncPath = path6.join(baseDir, "opencode.jsonc");
143
+ const jsonPath = path6.join(baseDir, "opencode.json");
144
+ try {
145
+ await fs6.access(jsoncPath);
146
+ return jsoncPath;
147
+ } catch {
148
+ try {
149
+ await fs6.access(jsonPath);
150
+ return jsonPath;
151
+ } catch {
152
+ return jsonPath;
153
+ }
154
+ }
155
+ }
156
+ async function ensurePluginConfigured(configPath, pluginName) {
157
+ let content = '{\n "$schema": "https://opencode.ai/config.json"\n}\n';
158
+ try {
159
+ content = await fs6.readFile(configPath, "utf8");
160
+ } catch {
161
+ }
162
+ const parsed = parse(content);
163
+ const plugins = parsed?.plugin ?? [];
164
+ if (plugins.includes(pluginName)) {
165
+ return configPath;
166
+ }
167
+ const edits = modify(content, ["plugin"], [...plugins, pluginName], {
168
+ formattingOptions: {
169
+ insertSpaces: true,
170
+ tabSize: 2,
171
+ eol: "\n"
172
+ }
173
+ });
174
+ const updated = applyEdits(content, edits);
175
+ await fs6.writeFile(configPath, updated, "utf8");
176
+ return configPath;
177
+ }
178
+ async function setupCommand(args) {
179
+ const pluginName = "opencode-gbk-tools";
180
+ const configBase = args.target === "project" ? args.cwd : path6.join(os2.homedir(), ".config", "opencode");
181
+ await fs6.mkdir(configBase, { recursive: true });
182
+ const configPath = await resolveConfigFile(configBase);
183
+ await ensurePluginConfigured(configPath, pluginName);
184
+ const installResult = await installCommand({ ...args, force: true, artifacts: ["agent"] });
185
+ return {
186
+ configPath,
187
+ targetBase: installResult.targetBase
188
+ };
189
+ }
190
+
134
191
  // src/cli/index.ts
135
192
  function resolvePackageRoot(env = process.env, moduleUrl = import.meta.url) {
136
193
  if (env.OPENCODE_GBK_PACKAGE_ROOT) {
137
194
  return env.OPENCODE_GBK_PACKAGE_ROOT;
138
195
  }
139
196
  const modulePath = fileURLToPath(moduleUrl);
140
- return path6.resolve(path6.dirname(modulePath), "..", "..");
197
+ return path7.resolve(path7.dirname(modulePath), "..", "..");
141
198
  }
142
199
  async function main(argv, env = process.env) {
143
200
  const [command, ...rest] = argv;
@@ -148,6 +205,9 @@ async function main(argv, env = process.env) {
148
205
  if (command === "install") {
149
206
  return await installCommand({ packageRoot, target, cwd, force });
150
207
  }
208
+ if (command === "setup") {
209
+ return await setupCommand({ packageRoot, target, cwd });
210
+ }
151
211
  if (command === "uninstall") {
152
212
  return await uninstallCommand({ target, cwd });
153
213
  }
@@ -182,7 +182,7 @@ var require_internal = __commonJS({
182
182
  // Codec.
183
183
  _internal: InternalCodec
184
184
  };
185
- function InternalCodec(codecOptions, iconv3) {
185
+ function InternalCodec(codecOptions, iconv2) {
186
186
  this.enc = codecOptions.encodingName;
187
187
  this.bomAware = codecOptions.bomAware;
188
188
  if (this.enc === "base64") {
@@ -194,7 +194,7 @@ var require_internal = __commonJS({
194
194
  this.encoder = InternalEncoderCesu8;
195
195
  if (Buffer2.from("eda0bdedb2a9", "hex").toString() !== "\u{1F4A9}") {
196
196
  this.decoder = InternalDecoderCesu8;
197
- this.defaultCharUnicode = iconv3.defaultCharUnicode;
197
+ this.defaultCharUnicode = iconv2.defaultCharUnicode;
198
198
  }
199
199
  }
200
200
  }
@@ -351,8 +351,8 @@ var require_utf32 = __commonJS({
351
351
  "use strict";
352
352
  var Buffer2 = require_safer().Buffer;
353
353
  exports._utf32 = Utf32Codec;
354
- function Utf32Codec(codecOptions, iconv3) {
355
- this.iconv = iconv3;
354
+ function Utf32Codec(codecOptions, iconv2) {
355
+ this.iconv = iconv2;
356
356
  this.bomAware = true;
357
357
  this.isLE = codecOptions.isLE;
358
358
  }
@@ -476,8 +476,8 @@ var require_utf32 = __commonJS({
476
476
  };
477
477
  exports.utf32 = Utf32AutoCodec;
478
478
  exports.ucs4 = "utf32";
479
- function Utf32AutoCodec(options, iconv3) {
480
- this.iconv = iconv3;
479
+ function Utf32AutoCodec(options, iconv2) {
480
+ this.iconv = iconv2;
481
481
  }
482
482
  Utf32AutoCodec.prototype.encoder = Utf32AutoEncoder;
483
483
  Utf32AutoCodec.prototype.decoder = Utf32AutoDecoder;
@@ -627,8 +627,8 @@ var require_utf16 = __commonJS({
627
627
  this.overflowByte = -1;
628
628
  };
629
629
  exports.utf16 = Utf16Codec;
630
- function Utf16Codec(codecOptions, iconv3) {
631
- this.iconv = iconv3;
630
+ function Utf16Codec(codecOptions, iconv2) {
631
+ this.iconv = iconv2;
632
632
  }
633
633
  Utf16Codec.prototype.encoder = Utf16Encoder;
634
634
  Utf16Codec.prototype.decoder = Utf16Decoder;
@@ -726,8 +726,8 @@ var require_utf7 = __commonJS({
726
726
  var Buffer2 = require_safer().Buffer;
727
727
  exports.utf7 = Utf7Codec;
728
728
  exports.unicode11utf7 = "utf7";
729
- function Utf7Codec(codecOptions, iconv3) {
730
- this.iconv = iconv3;
729
+ function Utf7Codec(codecOptions, iconv2) {
730
+ this.iconv = iconv2;
731
731
  }
732
732
  Utf7Codec.prototype.encoder = Utf7Encoder;
733
733
  Utf7Codec.prototype.decoder = Utf7Decoder;
@@ -809,8 +809,8 @@ var require_utf7 = __commonJS({
809
809
  return res;
810
810
  };
811
811
  exports.utf7imap = Utf7IMAPCodec;
812
- function Utf7IMAPCodec(codecOptions, iconv3) {
813
- this.iconv = iconv3;
812
+ function Utf7IMAPCodec(codecOptions, iconv2) {
813
+ this.iconv = iconv2;
814
814
  }
815
815
  Utf7IMAPCodec.prototype.encoder = Utf7IMAPEncoder;
816
816
  Utf7IMAPCodec.prototype.decoder = Utf7IMAPDecoder;
@@ -943,7 +943,7 @@ var require_sbcs_codec = __commonJS({
943
943
  "use strict";
944
944
  var Buffer2 = require_safer().Buffer;
945
945
  exports._sbcs = SBCSCodec;
946
- function SBCSCodec(codecOptions, iconv3) {
946
+ function SBCSCodec(codecOptions, iconv2) {
947
947
  if (!codecOptions) {
948
948
  throw new Error("SBCS codec is called without the data.");
949
949
  }
@@ -958,7 +958,7 @@ var require_sbcs_codec = __commonJS({
958
958
  codecOptions.chars = asciiString + codecOptions.chars;
959
959
  }
960
960
  this.decodeBuf = Buffer2.from(codecOptions.chars, "ucs2");
961
- var encodeBuf = Buffer2.alloc(65536, iconv3.defaultCharSingleByte.charCodeAt(0));
961
+ var encodeBuf = Buffer2.alloc(65536, iconv2.defaultCharSingleByte.charCodeAt(0));
962
962
  for (var i = 0; i < codecOptions.chars.length; i++) {
963
963
  encodeBuf[codecOptions.chars.charCodeAt(i)] = i;
964
964
  }
@@ -1623,7 +1623,7 @@ var require_dbcs_codec = __commonJS({
1623
1623
  UNASSIGNED_NODE[i] = UNASSIGNED;
1624
1624
  }
1625
1625
  var i;
1626
- function DBCSCodec(codecOptions, iconv3) {
1626
+ function DBCSCodec(codecOptions, iconv2) {
1627
1627
  this.encodingName = codecOptions.encodingName;
1628
1628
  if (!codecOptions) {
1629
1629
  throw new Error("DBCS codec is called without the data.");
@@ -1672,7 +1672,7 @@ var require_dbcs_codec = __commonJS({
1672
1672
  }
1673
1673
  }
1674
1674
  }
1675
- this.defaultCharUnicode = iconv3.defaultCharUnicode;
1675
+ this.defaultCharUnicode = iconv2.defaultCharUnicode;
1676
1676
  this.encodeTable = [];
1677
1677
  this.encodeTableSeq = [];
1678
1678
  var skipEncodeChars = {};
@@ -1696,7 +1696,7 @@ var require_dbcs_codec = __commonJS({
1696
1696
  }
1697
1697
  }
1698
1698
  }
1699
- this.defCharSB = this.encodeTable[0][iconv3.defaultCharSingleByte.charCodeAt(0)];
1699
+ this.defCharSB = this.encodeTable[0][iconv2.defaultCharSingleByte.charCodeAt(0)];
1700
1700
  if (this.defCharSB === UNASSIGNED) this.defCharSB = this.encodeTable[0]["?"];
1701
1701
  if (this.defCharSB === UNASSIGNED) this.defCharSB = "?".charCodeAt(0);
1702
1702
  }
@@ -3816,9 +3816,6 @@ var require_lib = __commonJS({
3816
3816
  }
3817
3817
  });
3818
3818
 
3819
- // src/tools/gbk_edit.ts
3820
- import fs3 from "fs/promises";
3821
-
3822
3819
  // node_modules/zod/v4/classic/external.js
3823
3820
  var external_exports = {};
3824
3821
  __export(external_exports, {
@@ -16241,11 +16238,10 @@ function tool(input) {
16241
16238
  }
16242
16239
  tool.schema = external_exports;
16243
16240
 
16244
- // src/tools/gbk_edit.ts
16245
- var import_iconv_lite2 = __toESM(require_lib(), 1);
16246
-
16247
16241
  // src/lib/gbk-file.ts
16248
16242
  var import_iconv_lite = __toESM(require_lib(), 1);
16243
+ import crypto from "crypto";
16244
+ import { createReadStream } from "fs";
16249
16245
  import fs2 from "fs/promises";
16250
16246
  import path2 from "path";
16251
16247
 
@@ -16308,6 +16304,7 @@ async function assertPathAllowed(filePath, context, allowExternal = false) {
16308
16304
  }
16309
16305
 
16310
16306
  // src/lib/gbk-file.ts
16307
+ var STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
16311
16308
  function assertEncodingSupported(encoding) {
16312
16309
  if (encoding !== "gbk" && encoding !== "gb18030") {
16313
16310
  throw createGbkError("GBK_INVALID_ENCODING", `\u4E0D\u652F\u6301\u7684\u7F16\u7801: ${encoding}`);
@@ -16323,27 +16320,85 @@ function assertNotBinary(buffer) {
16323
16320
  throw createGbkError("GBK_BINARY_FILE", "\u7591\u4F3C\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u65E0\u6CD5\u6309 GBK \u6587\u672C\u5904\u7406");
16324
16321
  }
16325
16322
  }
16326
- function splitLinesWithNumbers(text, offset = 1, limit = 2e3) {
16327
- assertPositiveInteger(offset, "offset");
16328
- assertPositiveInteger(limit, "limit");
16323
+ function applyLineRange(text, startLine, endLine) {
16324
+ if (startLine === void 0 && endLine === void 0) {
16325
+ return {
16326
+ selectedText: text,
16327
+ rangeStart: 0,
16328
+ rangeEnd: text.length
16329
+ };
16330
+ }
16329
16331
  const lines = text.split(/\r?\n/);
16330
- const totalLines = lines.length;
16331
- if (offset > totalLines) {
16332
+ const actualStartLine = startLine ?? 1;
16333
+ const actualEndLine = endLine ?? lines.length;
16334
+ assertPositiveInteger(actualStartLine, "startLine");
16335
+ assertPositiveInteger(actualEndLine, "endLine");
16336
+ if (actualEndLine < actualStartLine) {
16337
+ throw createGbkError("GBK_INVALID_ARGUMENT", "endLine \u4E0D\u80FD\u5C0F\u4E8E startLine");
16338
+ }
16339
+ let cursor = 0;
16340
+ let rangeStart = 0;
16341
+ let rangeEnd = text.length;
16342
+ for (let lineNumber = 1; lineNumber <= lines.length; lineNumber += 1) {
16343
+ if (lineNumber === actualStartLine) {
16344
+ rangeStart = cursor;
16345
+ }
16346
+ cursor += lines[lineNumber - 1].length;
16347
+ if (lineNumber < lines.length) {
16348
+ const newlineLength = text.startsWith("\r\n", cursor) ? 2 : 1;
16349
+ cursor += newlineLength;
16350
+ }
16351
+ if (lineNumber === actualEndLine) {
16352
+ rangeEnd = cursor;
16353
+ break;
16354
+ }
16355
+ }
16356
+ return {
16357
+ selectedText: text.slice(rangeStart, rangeEnd),
16358
+ rangeStart,
16359
+ rangeEnd
16360
+ };
16361
+ }
16362
+ function applyAnchors(text, startAnchor, endAnchor) {
16363
+ if (!startAnchor && !endAnchor) {
16332
16364
  return {
16333
- startLine: offset,
16334
- endLine: totalLines,
16335
- totalLines,
16336
- content: ""
16365
+ selectedText: text,
16366
+ rangeStart: 0,
16367
+ rangeEnd: text.length
16337
16368
  };
16338
16369
  }
16339
- const startIndex = offset - 1;
16340
- const endIndex = Math.min(startIndex + limit, totalLines);
16341
- const content = lines.slice(startIndex, endIndex).map((line, index) => `${offset + index}: ${line}`).join("\n");
16370
+ let rangeStart = 0;
16371
+ let rangeEnd = text.length;
16372
+ if (startAnchor) {
16373
+ const found = text.indexOf(startAnchor);
16374
+ if (found === -1) {
16375
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8D77\u59CB\u951A\u70B9: ${startAnchor}`);
16376
+ }
16377
+ rangeStart = found + startAnchor.length;
16378
+ }
16379
+ if (endAnchor) {
16380
+ const found = text.indexOf(endAnchor, rangeStart);
16381
+ if (found === -1) {
16382
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u7ED3\u675F\u951A\u70B9: ${endAnchor}`);
16383
+ }
16384
+ rangeEnd = found;
16385
+ }
16386
+ if (rangeEnd < rangeStart) {
16387
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u951A\u70B9\u8303\u56F4\u65E0\u6548");
16388
+ }
16389
+ return {
16390
+ selectedText: text.slice(rangeStart, rangeEnd),
16391
+ rangeStart,
16392
+ rangeEnd
16393
+ };
16394
+ }
16395
+ function resolveEditScope(text, input) {
16396
+ const anchored = applyAnchors(text, input.startAnchor, input.endAnchor);
16397
+ const lineRanged = applyLineRange(anchored.selectedText, input.startLine, input.endLine);
16342
16398
  return {
16343
- startLine: offset,
16344
- endLine: offset + (endIndex - startIndex) - 1,
16345
- totalLines,
16346
- content
16399
+ selectedText: lineRanged.selectedText,
16400
+ rangeStart: anchored.rangeStart + lineRanged.rangeStart,
16401
+ rangeEnd: anchored.rangeStart + lineRanged.rangeEnd
16347
16402
  };
16348
16403
  }
16349
16404
  function countOccurrences(text, target) {
@@ -16366,10 +16421,9 @@ async function readBufferAsText(buffer, encoding) {
16366
16421
  assertNotBinary(buffer);
16367
16422
  return import_iconv_lite.default.decode(buffer, encoding);
16368
16423
  }
16369
- async function readGbkFile(input) {
16424
+ async function resolveReadableGbkFile(input) {
16370
16425
  const encoding = input.encoding ?? "gbk";
16371
- const offset = input.offset ?? 1;
16372
- const limit = input.limit ?? 2e3;
16426
+ assertEncodingSupported(encoding);
16373
16427
  const { candidatePath } = await assertPathAllowed(input.filePath, input.context, input.allowExternal ?? false);
16374
16428
  let stat;
16375
16429
  try {
@@ -16380,21 +16434,169 @@ async function readGbkFile(input) {
16380
16434
  if (stat.isDirectory()) {
16381
16435
  throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\u5F55: ${candidatePath}`);
16382
16436
  }
16437
+ return {
16438
+ filePath: candidatePath,
16439
+ encoding,
16440
+ stat
16441
+ };
16442
+ }
16443
+ async function readWholeGbkTextFile(input) {
16383
16444
  try {
16384
- const buffer = await fs2.readFile(candidatePath);
16385
- const text = await readBufferAsText(buffer, encoding);
16445
+ const buffer = await fs2.readFile(input.filePath);
16446
+ const content = await readBufferAsText(buffer, input.encoding);
16386
16447
  return {
16387
- filePath: candidatePath,
16388
- encoding,
16389
- ...splitLinesWithNumbers(text, offset, limit)
16448
+ filePath: input.filePath,
16449
+ encoding: input.encoding,
16450
+ content,
16451
+ bytesRead: buffer.byteLength
16390
16452
  };
16391
16453
  } catch (error45) {
16392
16454
  if (error45 instanceof Error && "code" in error45) {
16393
16455
  throw error45;
16394
16456
  }
16395
- throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${candidatePath}`, error45);
16457
+ throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${input.filePath}`, error45);
16458
+ }
16459
+ }
16460
+ async function visitDecodedTextChunks(input, visitor) {
16461
+ const decoder = import_iconv_lite.default.getDecoder(input.encoding);
16462
+ const stream = createReadStream(input.filePath);
16463
+ try {
16464
+ for await (const chunk of stream) {
16465
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
16466
+ assertNotBinary(buffer);
16467
+ const text = decoder.write(buffer);
16468
+ if (text.length > 0) {
16469
+ await visitor(text);
16470
+ }
16471
+ }
16472
+ const trailingText = decoder.end();
16473
+ if (trailingText && trailingText.length > 0) {
16474
+ await visitor(trailingText);
16475
+ }
16476
+ } catch (error45) {
16477
+ if (error45 instanceof Error && "code" in error45) {
16478
+ throw error45;
16479
+ }
16480
+ throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${input.filePath}`, error45);
16481
+ } finally {
16482
+ stream.destroy();
16483
+ }
16484
+ }
16485
+ async function writeAll(handle, buffer) {
16486
+ let offset = 0;
16487
+ while (offset < buffer.length) {
16488
+ const { bytesWritten } = await handle.write(buffer, offset, buffer.length - offset);
16489
+ offset += bytesWritten;
16396
16490
  }
16397
16491
  }
16492
+ async function writeEncodedText(handle, encoding, text) {
16493
+ if (text.length === 0) {
16494
+ return 0;
16495
+ }
16496
+ const buffer = import_iconv_lite.default.encode(text, encoding);
16497
+ await writeAll(handle, buffer);
16498
+ return buffer.byteLength;
16499
+ }
16500
+ async function replaceLargeGbkFileText(input) {
16501
+ const tempPath = path2.join(
16502
+ path2.dirname(input.filePath),
16503
+ `${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
16504
+ );
16505
+ const handle = await fs2.open(tempPath, "w");
16506
+ const carryLength = Math.max(input.oldString.length - 1, 0);
16507
+ let carry = "";
16508
+ let occurrencesBefore = 0;
16509
+ let bytesWritten = 0;
16510
+ const flushText = async (text, flush = false) => {
16511
+ const combined = carry + text;
16512
+ const splitAt = flush ? combined.length : Math.max(0, combined.length - carryLength);
16513
+ const processable = combined.slice(0, splitAt);
16514
+ carry = combined.slice(splitAt);
16515
+ if (processable.length === 0) {
16516
+ return;
16517
+ }
16518
+ const matchCount = countOccurrences(processable, input.oldString);
16519
+ const seenBefore = occurrencesBefore;
16520
+ occurrencesBefore += matchCount;
16521
+ let output = processable;
16522
+ if (input.replaceAll) {
16523
+ if (matchCount > 0) {
16524
+ output = processable.split(input.oldString).join(input.newString);
16525
+ }
16526
+ } else if (seenBefore === 0 && matchCount > 0) {
16527
+ output = processable.replace(input.oldString, input.newString);
16528
+ }
16529
+ bytesWritten += await writeEncodedText(handle, input.encoding, output);
16530
+ };
16531
+ try {
16532
+ await visitDecodedTextChunks(input, async (text) => {
16533
+ await flushText(text);
16534
+ });
16535
+ await flushText("", true);
16536
+ if (occurrencesBefore === 0) {
16537
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\u5BB9: ${input.oldString}`);
16538
+ }
16539
+ if (!input.replaceAll && occurrencesBefore > 1) {
16540
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
16541
+ }
16542
+ await handle.close();
16543
+ const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
16544
+ await fs2.chmod(tempPath, mode);
16545
+ await fs2.rename(tempPath, input.filePath);
16546
+ return {
16547
+ filePath: input.filePath,
16548
+ encoding: input.encoding,
16549
+ replacements: input.replaceAll ? occurrencesBefore : 1,
16550
+ occurrencesBefore,
16551
+ bytesRead: input.stat.size,
16552
+ bytesWritten
16553
+ };
16554
+ } catch (error45) {
16555
+ await handle.close().catch(() => void 0);
16556
+ await fs2.rm(tempPath, { force: true }).catch(() => void 0);
16557
+ throw error45;
16558
+ }
16559
+ }
16560
+ async function replaceGbkFileText(input) {
16561
+ if (input.oldString.length === 0) {
16562
+ throw createGbkError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
16563
+ }
16564
+ const replaceAll = input.replaceAll ?? false;
16565
+ const resolved = await resolveReadableGbkFile(input);
16566
+ const hasScopedRange = input.startLine !== void 0 || input.endLine !== void 0 || input.startAnchor !== void 0 || input.endAnchor !== void 0;
16567
+ if (resolved.stat.size >= STREAMING_FILE_SIZE_THRESHOLD_BYTES && !hasScopedRange) {
16568
+ return await replaceLargeGbkFileText({
16569
+ ...resolved,
16570
+ oldString: input.oldString,
16571
+ newString: input.newString,
16572
+ replaceAll
16573
+ });
16574
+ }
16575
+ const current = await readWholeGbkTextFile(resolved);
16576
+ const scope = resolveEditScope(current.content, input);
16577
+ const occurrencesBefore = countOccurrences(scope.selectedText, input.oldString);
16578
+ if (replaceAll) {
16579
+ if (occurrencesBefore === 0) {
16580
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\u5BB9: ${input.oldString}`);
16581
+ }
16582
+ } else if (occurrencesBefore === 0) {
16583
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\u5BB9: ${input.oldString}`);
16584
+ } else if (occurrencesBefore > 1) {
16585
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
16586
+ }
16587
+ const replaced = replaceAll ? scope.selectedText.split(input.oldString).join(input.newString) : scope.selectedText.replace(input.oldString, input.newString);
16588
+ const outputText = `${current.content.slice(0, scope.rangeStart)}${replaced}${current.content.slice(scope.rangeEnd)}`;
16589
+ const buffer = import_iconv_lite.default.encode(outputText, current.encoding);
16590
+ await fs2.writeFile(current.filePath, buffer);
16591
+ return {
16592
+ filePath: current.filePath,
16593
+ encoding: current.encoding,
16594
+ replacements: replaceAll ? occurrencesBefore : 1,
16595
+ occurrencesBefore,
16596
+ bytesRead: current.bytesRead,
16597
+ bytesWritten: buffer.byteLength
16598
+ };
16599
+ }
16398
16600
 
16399
16601
  // src/tools/gbk_edit.ts
16400
16602
  var gbk_edit_default = tool({
@@ -16404,43 +16606,16 @@ var gbk_edit_default = tool({
16404
16606
  oldString: tool.schema.string().describe("Text to replace"),
16405
16607
  newString: tool.schema.string().describe("Replacement text"),
16406
16608
  replaceAll: tool.schema.boolean().optional().describe("Replace all occurrences"),
16609
+ startLine: tool.schema.number().int().positive().optional().describe("Restrict edit to 1-based start line"),
16610
+ endLine: tool.schema.number().int().positive().optional().describe("Restrict edit to 1-based end line"),
16611
+ startAnchor: tool.schema.string().optional().describe("Restrict edit to content after this anchor"),
16612
+ endAnchor: tool.schema.string().optional().describe("Restrict edit to content before this anchor"),
16407
16613
  encoding: tool.schema.enum(["gbk", "gb18030"]).optional().describe("Text encoding"),
16408
16614
  allowExternal: tool.schema.boolean().optional().describe("Allow paths outside workspace root")
16409
16615
  },
16410
16616
  async execute(args, context) {
16411
- if (args.oldString.length === 0) {
16412
- throw createGbkError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
16413
- }
16414
- const encoding = args.encoding ?? "gbk";
16415
- const replaceAll = args.replaceAll ?? false;
16416
- const { candidatePath } = await assertPathAllowed(args.filePath, context, args.allowExternal ?? false);
16417
- const current = await readGbkFile({
16418
- filePath: candidatePath,
16419
- encoding,
16420
- context,
16421
- allowExternal: args.allowExternal
16422
- });
16423
- const sourceText = current.content.split("\n").map((line) => line.replace(/^\d+: /, "")).join("\n");
16424
- const occurrencesBefore = countOccurrences(sourceText, args.oldString);
16425
- if (replaceAll) {
16426
- if (occurrencesBefore === 0) {
16427
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\u5BB9: ${args.oldString}`);
16428
- }
16429
- } else if (occurrencesBefore === 0) {
16430
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\u5BB9: ${args.oldString}`);
16431
- } else if (occurrencesBefore > 1) {
16432
- throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${args.oldString}`);
16433
- }
16434
- const replaced = replaceAll ? sourceText.split(args.oldString).join(args.newString) : sourceText.replace(args.oldString, args.newString);
16435
- const buffer = import_iconv_lite2.default.encode(replaced, encoding);
16436
- await fs3.writeFile(candidatePath, buffer);
16437
- return JSON.stringify({
16438
- filePath: candidatePath,
16439
- encoding,
16440
- replacements: replaceAll ? occurrencesBefore : 1,
16441
- occurrencesBefore,
16442
- bytesWritten: buffer.byteLength
16443
- }, null, 2);
16617
+ const result = await replaceGbkFileText({ ...args, context });
16618
+ return JSON.stringify(result, null, 2);
16444
16619
  }
16445
16620
  });
16446
16621
  export {