opencode-gbk-tools 0.1.12 → 0.1.14

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
@@ -52,6 +52,7 @@ npx opencode-gbk-tools uninstall
52
52
  - 原编码
53
53
  - BOM
54
54
  - 换行风格
55
+ - 新文件在 `encoding=auto` 下不会自动默认成 `utf8`,需要显式指定 `encoding`
55
56
  - 当前支持:
56
57
  - `utf8`
57
58
  - `utf8-bom`
@@ -194,6 +195,8 @@ A:不要。现在优先用 `mode="insertAfter"` 或 `mode="insertBefore"`,
194
195
 
195
196
  | 版本 | 说明 |
196
197
  |------|------|
198
+ | 0.1.14 | 重新发布当前稳定产物,核对 `npm pack --dry-run` 输出包含完整 `dist/`,为下一次公开发布提供一致的打包基线 |
199
+ | 0.1.13 | `text_write` 在新文件 + `encoding=auto` 下改为显式报错,不再静默默认 `utf8`;同时优化 `text_read` 流式读取与 `text_edit` 预览生成,减少重复读取 |
197
200
  | 0.1.12 | 仅提升发布版本号,用于重新发布当前 `0.1.11` 的稳定内容 |
198
201
  | 0.1.11 | `text_edit` / `gbk_edit` 新增 `insertAfter` / `insertBefore`,插入内容不再依赖 `oldString`;README 与全局提示同步改为“插入优先用 anchor/content” |
199
202
  | 0.1.10 | 新增 `text_read` / `text_write` / `text_edit`,自动识别并保持原编码、BOM 与换行风格;同时通过 plugin 规则让所有 agents 默认优先使用 `text_*` |
@@ -16240,6 +16240,8 @@ tool.schema = external_exports;
16240
16240
 
16241
16241
  // src/lib/text-file.ts
16242
16242
  var import_iconv_lite2 = __toESM(require_lib(), 1);
16243
+ import crypto2 from "crypto";
16244
+ import { createReadStream as createReadStream2 } from "fs";
16243
16245
  import fs3 from "fs/promises";
16244
16246
  import path3 from "path";
16245
16247
 
@@ -16360,6 +16362,7 @@ function countOccurrences(text, target) {
16360
16362
 
16361
16363
  // src/lib/text-file.ts
16362
16364
  var TEXT_STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
16365
+ var TEXT_DETECTION_SAMPLE_BYTES = 64 * 1024;
16363
16366
  var UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
16364
16367
  function assertStringArgument2(value, name) {
16365
16368
  if (typeof value !== "string") {
@@ -16380,6 +16383,21 @@ function resolveExplicitTextEncoding(value, fallback) {
16380
16383
  const requested = value ?? "auto";
16381
16384
  return requested === "auto" ? fallback : requested;
16382
16385
  }
16386
+ function getBomPrefix(encoding, hasBom) {
16387
+ if (!hasBom) {
16388
+ return Buffer.alloc(0);
16389
+ }
16390
+ if (encoding === "utf8-bom") {
16391
+ return Buffer.from([239, 187, 191]);
16392
+ }
16393
+ if (encoding === "utf16le") {
16394
+ return Buffer.from([255, 254]);
16395
+ }
16396
+ if (encoding === "utf16be") {
16397
+ return Buffer.from([254, 255]);
16398
+ }
16399
+ return Buffer.alloc(0);
16400
+ }
16383
16401
  function isSupportedEncoding(value) {
16384
16402
  return value === "utf8" || value === "utf8-bom" || value === "utf16le" || value === "utf16be" || value === "gbk" || value === "gb18030";
16385
16403
  }
@@ -16391,23 +16409,23 @@ function assertTextEncodingSupported(encoding) {
16391
16409
  function decodeUtf16be(buffer) {
16392
16410
  return import_iconv_lite2.default.decode(buffer, "utf16be");
16393
16411
  }
16394
- function encodeText(content, encoding, hasBom = encoding === "utf8-bom") {
16395
- if (encoding === "utf8") {
16412
+ function encodeTextBody(content, encoding) {
16413
+ if (encoding === "utf8" || encoding === "utf8-bom") {
16396
16414
  return Buffer.from(content, "utf8");
16397
16415
  }
16398
- if (encoding === "utf8-bom") {
16399
- return Buffer.concat([Buffer.from([239, 187, 191]), Buffer.from(content, "utf8")]);
16400
- }
16401
16416
  if (encoding === "utf16le") {
16402
- const body = Buffer.from(content, "utf16le");
16403
- return hasBom ? Buffer.concat([Buffer.from([255, 254]), body]) : body;
16417
+ return Buffer.from(content, "utf16le");
16404
16418
  }
16405
16419
  if (encoding === "utf16be") {
16406
- const body = import_iconv_lite2.default.encode(content, "utf16be");
16407
- return hasBom ? Buffer.concat([Buffer.from([254, 255]), body]) : body;
16420
+ return import_iconv_lite2.default.encode(content, "utf16be");
16408
16421
  }
16409
16422
  return import_iconv_lite2.default.encode(content, encoding);
16410
16423
  }
16424
+ function encodeText(content, encoding, hasBom = encoding === "utf8-bom") {
16425
+ const bom = getBomPrefix(encoding, hasBom);
16426
+ const body = encodeTextBody(content, encoding);
16427
+ return bom.length === 0 ? body : Buffer.concat([bom, body]);
16428
+ }
16411
16429
  function decodeText(buffer, encoding) {
16412
16430
  if (encoding === "utf8") {
16413
16431
  return buffer.toString("utf8");
@@ -16683,6 +16701,30 @@ async function resolveReadableTextFile(input) {
16683
16701
  }
16684
16702
  return { filePath: candidatePath, stat };
16685
16703
  }
16704
+ async function readDetectionBuffer(filePath, sampleSize = TEXT_DETECTION_SAMPLE_BYTES) {
16705
+ const handle = await fs3.open(filePath, "r");
16706
+ try {
16707
+ const buffer = Buffer.alloc(sampleSize);
16708
+ const { bytesRead } = await handle.read(buffer, 0, sampleSize, 0);
16709
+ return buffer.subarray(0, bytesRead);
16710
+ } finally {
16711
+ await handle.close();
16712
+ }
16713
+ }
16714
+ async function detectReadableTextFile(input) {
16715
+ const resolved = await resolveReadableTextFile(input);
16716
+ const sample = await readDetectionBuffer(resolved.filePath);
16717
+ assertLikelyTextBuffer(sample);
16718
+ const detected = detectTextEncodingFromBuffer(sample, input.encoding ?? "auto");
16719
+ return {
16720
+ ...resolved,
16721
+ encoding: detected.detectedEncoding,
16722
+ requestedEncoding: detected.requestedEncoding,
16723
+ detectedEncoding: detected.detectedEncoding,
16724
+ confidence: detected.confidence,
16725
+ hasBom: detected.hasBom
16726
+ };
16727
+ }
16686
16728
  async function readWholeTextFile(input) {
16687
16729
  const resolved = await resolveReadableTextFile(input);
16688
16730
  try {
@@ -16819,70 +16861,84 @@ async function replaceTextFileText(input) {
16819
16861
  startLine: normalizeOptionalPositiveInteger(input.startLine, "startLine"),
16820
16862
  endLine: normalizeOptionalPositiveInteger(input.endLine, "endLine")
16821
16863
  };
16822
- const loaded = await readWholeTextFile({
16823
- filePath: input.filePath,
16824
- encoding: input.encoding ?? "auto",
16825
- allowExternal: input.allowExternal,
16826
- context: input.context
16827
- });
16828
16864
  if (mode === "insertAfter" || mode === "insertBefore") {
16865
+ const loaded2 = await readWholeTextFile({
16866
+ filePath: input.filePath,
16867
+ encoding: input.encoding ?? "auto",
16868
+ allowExternal: input.allowExternal,
16869
+ context: input.context
16870
+ });
16829
16871
  assertInsertArguments(normalizedInput);
16830
16872
  const occurrence = normalizeOptionalPositiveInteger(normalizedInput.occurrence, "occurrence") ?? 1;
16831
16873
  const insertResult = buildInsertOutput(
16832
- loaded.content,
16874
+ loaded2.content,
16833
16875
  mode,
16834
16876
  normalizedInput.anchor,
16835
16877
  normalizedInput.content,
16836
16878
  occurrence,
16837
16879
  resolveInsertIfExists(normalizedInput.ifExists),
16838
- loaded.newlineStyle
16880
+ loaded2.newlineStyle
16839
16881
  );
16840
16882
  if (insertResult.skipped) {
16841
16883
  return {
16842
16884
  mode,
16843
- filePath: loaded.filePath,
16844
- encoding: loaded.encoding,
16845
- requestedEncoding: loaded.requestedEncoding,
16846
- detectedEncoding: loaded.detectedEncoding,
16847
- confidence: loaded.confidence,
16848
- hasBom: loaded.hasBom,
16885
+ filePath: loaded2.filePath,
16886
+ encoding: loaded2.encoding,
16887
+ requestedEncoding: loaded2.requestedEncoding,
16888
+ detectedEncoding: loaded2.detectedEncoding,
16889
+ confidence: loaded2.confidence,
16890
+ hasBom: loaded2.hasBom,
16849
16891
  anchor: insertResult.anchor,
16850
16892
  occurrence: insertResult.occurrence,
16851
16893
  anchorMatches: insertResult.anchorMatches,
16852
16894
  inserted: false,
16853
16895
  skipped: true,
16854
- bytesRead: loaded.bytesRead,
16896
+ bytesRead: loaded2.bytesRead,
16855
16897
  bytesWritten: 0,
16856
- newlineStyle: loaded.newlineStyle
16898
+ newlineStyle: loaded2.newlineStyle,
16899
+ diffPreview: buildLineDiffPreview(loaded2.filePath, loaded2.encoding, insertResult.previewBefore, insertResult.previewAfter)
16857
16900
  };
16858
16901
  }
16859
- const targetEncoding2 = (normalizedInput.preserveEncoding ?? true) || (normalizedInput.encoding ?? "auto") === "auto" ? loaded.encoding : resolveExplicitTextEncoding(normalizedInput.encoding, loaded.encoding);
16860
- const targetHasBom2 = normalizedInput.preserveEncoding === false ? targetEncoding2 === "utf8-bom" || targetEncoding2 === "utf16le" || targetEncoding2 === "utf16be" : loaded.hasBom;
16902
+ const targetEncoding2 = (normalizedInput.preserveEncoding ?? true) || (normalizedInput.encoding ?? "auto") === "auto" ? loaded2.encoding : resolveExplicitTextEncoding(normalizedInput.encoding, loaded2.encoding);
16903
+ const targetHasBom2 = normalizedInput.preserveEncoding === false ? targetEncoding2 === "utf8-bom" || targetEncoding2 === "utf16le" || targetEncoding2 === "utf16be" : loaded2.hasBom;
16861
16904
  ensureLossless(insertResult.outputText, targetEncoding2, targetHasBom2);
16862
16905
  const buffer2 = encodeText(insertResult.outputText, targetEncoding2, targetHasBom2);
16863
- await fs3.writeFile(loaded.filePath, buffer2);
16906
+ await fs3.writeFile(loaded2.filePath, buffer2);
16864
16907
  return {
16865
16908
  mode,
16866
- filePath: loaded.filePath,
16909
+ filePath: loaded2.filePath,
16867
16910
  encoding: targetEncoding2,
16868
- requestedEncoding: loaded.requestedEncoding,
16869
- detectedEncoding: loaded.detectedEncoding,
16870
- confidence: loaded.confidence,
16911
+ requestedEncoding: loaded2.requestedEncoding,
16912
+ detectedEncoding: loaded2.detectedEncoding,
16913
+ confidence: loaded2.confidence,
16871
16914
  hasBom: targetHasBom2,
16872
16915
  anchor: insertResult.anchor,
16873
16916
  occurrence: insertResult.occurrence,
16874
16917
  anchorMatches: insertResult.anchorMatches,
16875
16918
  inserted: true,
16876
16919
  skipped: false,
16877
- bytesRead: loaded.bytesRead,
16920
+ bytesRead: loaded2.bytesRead,
16878
16921
  bytesWritten: buffer2.byteLength,
16879
- newlineStyle: detectNewlineStyle(insertResult.outputText)
16922
+ newlineStyle: detectNewlineStyle(insertResult.outputText),
16923
+ diffPreview: buildLineDiffPreview(loaded2.filePath, targetEncoding2, insertResult.previewBefore, insertResult.previewAfter)
16880
16924
  };
16881
16925
  }
16882
16926
  assertReplaceArguments(input);
16883
16927
  if (input.oldString.length === 0) {
16884
16928
  throw createTextError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
16885
16929
  }
16930
+ const resolved = await detectReadableTextFile({
16931
+ filePath: input.filePath,
16932
+ encoding: input.encoding ?? "auto",
16933
+ allowExternal: input.allowExternal,
16934
+ context: input.context
16935
+ });
16936
+ const loaded = await readWholeTextFile({
16937
+ filePath: input.filePath,
16938
+ encoding: input.encoding ?? "auto",
16939
+ allowExternal: input.allowExternal,
16940
+ context: input.context
16941
+ });
16886
16942
  const scope = resolveEditScope(loaded.content, normalizedInput);
16887
16943
  const replaceAll = normalizedInput.replaceAll ?? false;
16888
16944
  const preserveEncoding = normalizedInput.preserveEncoding ?? true;
@@ -16917,43 +16973,8 @@ async function replaceTextFileText(input) {
16917
16973
  occurrencesBefore,
16918
16974
  bytesRead: loaded.bytesRead,
16919
16975
  bytesWritten: buffer.byteLength,
16920
- newlineStyle: detectNewlineStyle(outputText)
16921
- };
16922
- }
16923
- async function createTextDiffPreview(input) {
16924
- const mode = resolveEditMode(input.mode);
16925
- const loaded = await readWholeTextFile({
16926
- filePath: input.filePath,
16927
- encoding: input.encoding ?? "auto",
16928
- allowExternal: input.allowExternal,
16929
- context: input.context
16930
- });
16931
- if (mode === "insertAfter" || mode === "insertBefore") {
16932
- assertInsertArguments(input);
16933
- const occurrence = normalizeOptionalPositiveInteger(input.occurrence, "occurrence") ?? 1;
16934
- const insertResult = buildInsertOutput(
16935
- loaded.content,
16936
- mode,
16937
- input.anchor,
16938
- input.content,
16939
- occurrence,
16940
- resolveInsertIfExists(input.ifExists),
16941
- loaded.newlineStyle
16942
- );
16943
- return {
16944
- filePath: loaded.filePath,
16945
- encoding: loaded.encoding,
16946
- preview: buildLineDiffPreview(loaded.filePath, loaded.encoding, insertResult.previewBefore, insertResult.previewAfter)
16947
- };
16948
- }
16949
- assertReplaceArguments(input);
16950
- const scope = resolveEditScope(loaded.content, input);
16951
- const alignedNewString = alignTextToNewlineStyle(input.newString, loaded.newlineStyle);
16952
- const replaced = input.replaceAll ?? false ? scope.selectedText.split(input.oldString).join(alignedNewString) : scope.selectedText.replace(input.oldString, alignedNewString);
16953
- return {
16954
- filePath: loaded.filePath,
16955
- encoding: loaded.encoding,
16956
- preview: buildLineDiffPreview(loaded.filePath, loaded.encoding, scope.selectedText, replaced)
16976
+ newlineStyle: detectNewlineStyle(outputText),
16977
+ diffPreview: buildLineDiffPreview(loaded.filePath, targetEncoding, scope.selectedText, replaced)
16957
16978
  };
16958
16979
  }
16959
16980
 
@@ -16986,7 +17007,6 @@ var text_edit_default = tool({
16986
17007
  },
16987
17008
  async execute(args, context) {
16988
17009
  const result = await replaceTextFileText({ ...args, context });
16989
- const preview = await createTextDiffPreview({ ...args, context });
16990
17010
  const title = result.mode === "replace" ? `\u6587\u672C\u7F16\u8F91 ${result.encoding.toUpperCase()} x${result.replacements}` : result.inserted ? `\u6587\u672C\u63D2\u5165 ${result.encoding.toUpperCase()} #${result.occurrence}` : `\u6587\u672C\u8DF3\u8FC7 ${result.encoding.toUpperCase()} #${result.occurrence}`;
16991
17011
  context.metadata({
16992
17012
  title,
@@ -17005,7 +17025,7 @@ var text_edit_default = tool({
17005
17025
  inserted: result.mode === "replace" ? void 0 : result.inserted,
17006
17026
  skipped: result.mode === "replace" ? void 0 : result.skipped,
17007
17027
  newlineStyle: result.newlineStyle,
17008
- diffPreview: preview.preview
17028
+ diffPreview: result.diffPreview ?? ""
17009
17029
  }
17010
17030
  });
17011
17031
  return JSON.stringify(result, null, 2);
@@ -16240,6 +16240,8 @@ tool.schema = external_exports;
16240
16240
 
16241
16241
  // src/lib/text-file.ts
16242
16242
  var import_iconv_lite2 = __toESM(require_lib(), 1);
16243
+ import crypto2 from "crypto";
16244
+ import { createReadStream as createReadStream2 } from "fs";
16243
16245
  import fs3 from "fs/promises";
16244
16246
  import path3 from "path";
16245
16247
 
@@ -16361,7 +16363,35 @@ function detectNewlineStyle(text) {
16361
16363
 
16362
16364
  // src/lib/text-file.ts
16363
16365
  var TEXT_STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
16366
+ var TEXT_DETECTION_SAMPLE_BYTES = 64 * 1024;
16364
16367
  var UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
16368
+ function finalizeNewlineStyle(crlfCount, lfCount) {
16369
+ if (crlfCount > 0 && lfCount === 0) {
16370
+ return "crlf";
16371
+ }
16372
+ if (lfCount > 0 && crlfCount === 0) {
16373
+ return "lf";
16374
+ }
16375
+ if (crlfCount > 0 && lfCount > 0) {
16376
+ return "mixed";
16377
+ }
16378
+ return "none";
16379
+ }
16380
+ function getBomPrefix(encoding, hasBom) {
16381
+ if (!hasBom) {
16382
+ return Buffer.alloc(0);
16383
+ }
16384
+ if (encoding === "utf8-bom") {
16385
+ return Buffer.from([239, 187, 191]);
16386
+ }
16387
+ if (encoding === "utf16le") {
16388
+ return Buffer.from([255, 254]);
16389
+ }
16390
+ if (encoding === "utf16be") {
16391
+ return Buffer.from([254, 255]);
16392
+ }
16393
+ return Buffer.alloc(0);
16394
+ }
16365
16395
  function isSupportedEncoding(value) {
16366
16396
  return value === "utf8" || value === "utf8-bom" || value === "utf16le" || value === "utf16be" || value === "gbk" || value === "gb18030";
16367
16397
  }
@@ -16373,23 +16403,23 @@ function assertTextEncodingSupported(encoding) {
16373
16403
  function decodeUtf16be(buffer) {
16374
16404
  return import_iconv_lite2.default.decode(buffer, "utf16be");
16375
16405
  }
16376
- function encodeText(content, encoding, hasBom = encoding === "utf8-bom") {
16377
- if (encoding === "utf8") {
16406
+ function encodeTextBody(content, encoding) {
16407
+ if (encoding === "utf8" || encoding === "utf8-bom") {
16378
16408
  return Buffer.from(content, "utf8");
16379
16409
  }
16380
- if (encoding === "utf8-bom") {
16381
- return Buffer.concat([Buffer.from([239, 187, 191]), Buffer.from(content, "utf8")]);
16382
- }
16383
16410
  if (encoding === "utf16le") {
16384
- const body = Buffer.from(content, "utf16le");
16385
- return hasBom ? Buffer.concat([Buffer.from([255, 254]), body]) : body;
16411
+ return Buffer.from(content, "utf16le");
16386
16412
  }
16387
16413
  if (encoding === "utf16be") {
16388
- const body = import_iconv_lite2.default.encode(content, "utf16be");
16389
- return hasBom ? Buffer.concat([Buffer.from([254, 255]), body]) : body;
16414
+ return import_iconv_lite2.default.encode(content, "utf16be");
16390
16415
  }
16391
16416
  return import_iconv_lite2.default.encode(content, encoding);
16392
16417
  }
16418
+ function encodeText(content, encoding, hasBom = encoding === "utf8-bom") {
16419
+ const bom = getBomPrefix(encoding, hasBom);
16420
+ const body = encodeTextBody(content, encoding);
16421
+ return bom.length === 0 ? body : Buffer.concat([bom, body]);
16422
+ }
16393
16423
  function decodeText(buffer, encoding) {
16394
16424
  if (encoding === "utf8") {
16395
16425
  return buffer.toString("utf8");
@@ -16543,6 +16573,169 @@ async function resolveReadableTextFile(input) {
16543
16573
  }
16544
16574
  return { filePath: candidatePath, stat };
16545
16575
  }
16576
+ async function readDetectionBuffer(filePath, sampleSize = TEXT_DETECTION_SAMPLE_BYTES) {
16577
+ const handle = await fs3.open(filePath, "r");
16578
+ try {
16579
+ const buffer = Buffer.alloc(sampleSize);
16580
+ const { bytesRead } = await handle.read(buffer, 0, sampleSize, 0);
16581
+ return buffer.subarray(0, bytesRead);
16582
+ } finally {
16583
+ await handle.close();
16584
+ }
16585
+ }
16586
+ async function detectReadableTextFile(input) {
16587
+ const resolved = await resolveReadableTextFile(input);
16588
+ const sample = await readDetectionBuffer(resolved.filePath);
16589
+ assertLikelyTextBuffer(sample);
16590
+ const detected = detectTextEncodingFromBuffer(sample, input.encoding ?? "auto");
16591
+ return {
16592
+ ...resolved,
16593
+ encoding: detected.detectedEncoding,
16594
+ requestedEncoding: detected.requestedEncoding,
16595
+ detectedEncoding: detected.detectedEncoding,
16596
+ confidence: detected.confidence,
16597
+ hasBom: detected.hasBom
16598
+ };
16599
+ }
16600
+ function createLineCollector(offset, limit) {
16601
+ const lines = [];
16602
+ let pending = "";
16603
+ let totalLines = 0;
16604
+ let crlfCount = 0;
16605
+ let lfCount = 0;
16606
+ const emitLine = (line) => {
16607
+ totalLines += 1;
16608
+ if (totalLines >= offset && lines.length < limit) {
16609
+ lines.push(`${totalLines}: ${line}`);
16610
+ }
16611
+ };
16612
+ return {
16613
+ push(text) {
16614
+ if (text.length === 0) {
16615
+ return;
16616
+ }
16617
+ const combined = pending + text;
16618
+ let start = 0;
16619
+ while (true) {
16620
+ const newlineIndex = combined.indexOf("\n", start);
16621
+ if (newlineIndex === -1) {
16622
+ break;
16623
+ }
16624
+ let line = combined.slice(start, newlineIndex);
16625
+ if (line.endsWith("\r")) {
16626
+ line = line.slice(0, -1);
16627
+ crlfCount += 1;
16628
+ } else {
16629
+ lfCount += 1;
16630
+ }
16631
+ emitLine(line);
16632
+ start = newlineIndex + 1;
16633
+ }
16634
+ pending = combined.slice(start);
16635
+ },
16636
+ finish() {
16637
+ emitLine(pending);
16638
+ return {
16639
+ startLine: offset,
16640
+ endLine: lines.length === 0 ? totalLines : offset + lines.length - 1,
16641
+ totalLines,
16642
+ content: lines.join("\n"),
16643
+ tail: false,
16644
+ truncated: (lines.length === 0 ? totalLines : offset + lines.length - 1) < totalLines,
16645
+ newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
16646
+ };
16647
+ }
16648
+ };
16649
+ }
16650
+ function createTailCollector(limit) {
16651
+ assertPositiveInteger(limit, "limit");
16652
+ const lines = [];
16653
+ let pending = "";
16654
+ let totalLines = 0;
16655
+ let crlfCount = 0;
16656
+ let lfCount = 0;
16657
+ const emitLine = (line) => {
16658
+ totalLines += 1;
16659
+ lines.push(line);
16660
+ if (lines.length > limit) {
16661
+ lines.shift();
16662
+ }
16663
+ };
16664
+ return {
16665
+ push(text) {
16666
+ if (text.length === 0) {
16667
+ return;
16668
+ }
16669
+ const combined = pending + text;
16670
+ let start = 0;
16671
+ while (true) {
16672
+ const newlineIndex = combined.indexOf("\n", start);
16673
+ if (newlineIndex === -1) {
16674
+ break;
16675
+ }
16676
+ let line = combined.slice(start, newlineIndex);
16677
+ if (line.endsWith("\r")) {
16678
+ line = line.slice(0, -1);
16679
+ crlfCount += 1;
16680
+ } else {
16681
+ lfCount += 1;
16682
+ }
16683
+ emitLine(line);
16684
+ start = newlineIndex + 1;
16685
+ }
16686
+ pending = combined.slice(start);
16687
+ },
16688
+ finish() {
16689
+ emitLine(pending);
16690
+ const startLine = Math.max(1, totalLines - lines.length + 1);
16691
+ return {
16692
+ startLine,
16693
+ endLine: totalLines,
16694
+ totalLines,
16695
+ content: lines.map((line, index) => `${startLine + index}: ${line}`).join("\n"),
16696
+ truncated: startLine > 1,
16697
+ tail: true,
16698
+ newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
16699
+ };
16700
+ }
16701
+ };
16702
+ }
16703
+ async function visitDecodedTextChunks(resolved, visitor) {
16704
+ const stream = createReadStream2(resolved.filePath);
16705
+ const bomLength = getBomPrefix(resolved.encoding, resolved.hasBom).length;
16706
+ const decoderEncoding = resolved.encoding === "utf8-bom" ? "utf8" : resolved.encoding;
16707
+ const decoder = import_iconv_lite2.default.getDecoder(decoderEncoding);
16708
+ let skippedBom = false;
16709
+ try {
16710
+ for await (const chunk of stream) {
16711
+ let buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
16712
+ if (!skippedBom && bomLength > 0) {
16713
+ buffer = buffer.subarray(Math.min(bomLength, buffer.length));
16714
+ skippedBom = true;
16715
+ } else {
16716
+ skippedBom = true;
16717
+ }
16718
+ if (buffer.length === 0) {
16719
+ continue;
16720
+ }
16721
+ const text = decoder.write(buffer);
16722
+ if (text.length > 0) {
16723
+ await visitor(text);
16724
+ }
16725
+ }
16726
+ const trailingText = decoder.end() ?? "";
16727
+ if (trailingText.length > 0) {
16728
+ await visitor(trailingText);
16729
+ }
16730
+ } catch (error45) {
16731
+ if (error45 instanceof Error && "code" in error45) {
16732
+ throw error45;
16733
+ }
16734
+ throw createTextError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${resolved.filePath}`, error45);
16735
+ } finally {
16736
+ stream.destroy();
16737
+ }
16738
+ }
16546
16739
  async function readWholeTextFile(input) {
16547
16740
  const resolved = await resolveReadableTextFile(input);
16548
16741
  try {
@@ -16588,26 +16781,46 @@ async function readTextFile(input) {
16588
16781
  const offset = normalizeOptionalPositiveInteger(input.offset, "offset") ?? 1;
16589
16782
  const limit = normalizeOptionalPositiveInteger(input.limit, "limit") ?? 2e3;
16590
16783
  const tail = input.tail ?? false;
16591
- const loaded = await readWholeTextFile(input);
16592
- const lineWindow = tail ? collectTailLines(loaded.content, limit) : {
16593
- ...splitLinesWithNumbers(loaded.content, offset, limit),
16594
- tail: false,
16595
- truncated: false
16596
- };
16597
- if (!tail) {
16598
- lineWindow.truncated = lineWindow.endLine < lineWindow.totalLines;
16784
+ const resolved = await detectReadableTextFile(input);
16785
+ if (resolved.stat.size < TEXT_STREAMING_FILE_SIZE_THRESHOLD_BYTES) {
16786
+ const loaded = await readWholeTextFile(input);
16787
+ const lineWindow2 = tail ? collectTailLines(loaded.content, limit) : {
16788
+ ...splitLinesWithNumbers(loaded.content, offset, limit),
16789
+ tail: false,
16790
+ truncated: false
16791
+ };
16792
+ if (!tail) {
16793
+ lineWindow2.truncated = lineWindow2.endLine < lineWindow2.totalLines;
16794
+ }
16795
+ return {
16796
+ filePath: loaded.filePath,
16797
+ encoding: loaded.encoding,
16798
+ requestedEncoding: loaded.requestedEncoding,
16799
+ detectedEncoding: loaded.detectedEncoding,
16800
+ confidence: loaded.confidence,
16801
+ hasBom: loaded.hasBom,
16802
+ bytesRead: loaded.bytesRead,
16803
+ fileSize: loaded.fileSize,
16804
+ streamed: false,
16805
+ newlineStyle: loaded.newlineStyle,
16806
+ ...lineWindow2
16807
+ };
16599
16808
  }
16809
+ const collector = tail ? createTailCollector(limit) : createLineCollector(offset, limit);
16810
+ await visitDecodedTextChunks(resolved, (text) => {
16811
+ collector.push(text);
16812
+ });
16813
+ const lineWindow = collector.finish();
16600
16814
  return {
16601
- filePath: loaded.filePath,
16602
- encoding: loaded.encoding,
16603
- requestedEncoding: loaded.requestedEncoding,
16604
- detectedEncoding: loaded.detectedEncoding,
16605
- confidence: loaded.confidence,
16606
- hasBom: loaded.hasBom,
16607
- bytesRead: loaded.bytesRead,
16608
- fileSize: loaded.fileSize,
16609
- streamed: false,
16610
- newlineStyle: loaded.newlineStyle,
16815
+ filePath: resolved.filePath,
16816
+ encoding: resolved.encoding,
16817
+ requestedEncoding: resolved.requestedEncoding,
16818
+ detectedEncoding: resolved.detectedEncoding,
16819
+ confidence: resolved.confidence,
16820
+ hasBom: resolved.hasBom,
16821
+ bytesRead: resolved.stat.size,
16822
+ fileSize: resolved.stat.size,
16823
+ streamed: true,
16611
16824
  ...lineWindow
16612
16825
  };
16613
16826
  }
@@ -16240,6 +16240,8 @@ tool.schema = external_exports;
16240
16240
 
16241
16241
  // src/lib/text-file.ts
16242
16242
  var import_iconv_lite2 = __toESM(require_lib(), 1);
16243
+ import crypto2 from "crypto";
16244
+ import { createReadStream as createReadStream2 } from "fs";
16243
16245
  import fs3 from "fs/promises";
16244
16246
  import path3 from "path";
16245
16247
 
@@ -16326,12 +16328,32 @@ function detectNewlineStyle(text) {
16326
16328
 
16327
16329
  // src/lib/text-file.ts
16328
16330
  var TEXT_STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
16331
+ var TEXT_DETECTION_SAMPLE_BYTES = 64 * 1024;
16329
16332
  var UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
16330
16333
  function assertStringArgument(value, name) {
16331
16334
  if (typeof value !== "string") {
16332
16335
  throw createTextError("GBK_INVALID_ARGUMENT", `${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
16333
16336
  }
16334
16337
  }
16338
+ function resolveExplicitTextEncoding(value, fallback) {
16339
+ const requested = value ?? "auto";
16340
+ return requested === "auto" ? fallback : requested;
16341
+ }
16342
+ function getBomPrefix(encoding, hasBom) {
16343
+ if (!hasBom) {
16344
+ return Buffer.alloc(0);
16345
+ }
16346
+ if (encoding === "utf8-bom") {
16347
+ return Buffer.from([239, 187, 191]);
16348
+ }
16349
+ if (encoding === "utf16le") {
16350
+ return Buffer.from([255, 254]);
16351
+ }
16352
+ if (encoding === "utf16be") {
16353
+ return Buffer.from([254, 255]);
16354
+ }
16355
+ return Buffer.alloc(0);
16356
+ }
16335
16357
  function isSupportedEncoding(value) {
16336
16358
  return value === "utf8" || value === "utf8-bom" || value === "utf16le" || value === "utf16be" || value === "gbk" || value === "gb18030";
16337
16359
  }
@@ -16343,23 +16365,23 @@ function assertTextEncodingSupported(encoding) {
16343
16365
  function decodeUtf16be(buffer) {
16344
16366
  return import_iconv_lite2.default.decode(buffer, "utf16be");
16345
16367
  }
16346
- function encodeText(content, encoding, hasBom = encoding === "utf8-bom") {
16347
- if (encoding === "utf8") {
16368
+ function encodeTextBody(content, encoding) {
16369
+ if (encoding === "utf8" || encoding === "utf8-bom") {
16348
16370
  return Buffer.from(content, "utf8");
16349
16371
  }
16350
- if (encoding === "utf8-bom") {
16351
- return Buffer.concat([Buffer.from([239, 187, 191]), Buffer.from(content, "utf8")]);
16352
- }
16353
16372
  if (encoding === "utf16le") {
16354
- const body = Buffer.from(content, "utf16le");
16355
- return hasBom ? Buffer.concat([Buffer.from([255, 254]), body]) : body;
16373
+ return Buffer.from(content, "utf16le");
16356
16374
  }
16357
16375
  if (encoding === "utf16be") {
16358
- const body = import_iconv_lite2.default.encode(content, "utf16be");
16359
- return hasBom ? Buffer.concat([Buffer.from([254, 255]), body]) : body;
16376
+ return import_iconv_lite2.default.encode(content, "utf16be");
16360
16377
  }
16361
16378
  return import_iconv_lite2.default.encode(content, encoding);
16362
16379
  }
16380
+ function encodeText(content, encoding, hasBom = encoding === "utf8-bom") {
16381
+ const bom = getBomPrefix(encoding, hasBom);
16382
+ const body = encodeTextBody(content, encoding);
16383
+ return bom.length === 0 ? body : Buffer.concat([bom, body]);
16384
+ }
16363
16385
  function decodeText(buffer, encoding) {
16364
16386
  if (encoding === "utf8") {
16365
16387
  return buffer.toString("utf8");
@@ -16603,7 +16625,10 @@ async function writeTextFile(input) {
16603
16625
  if (existing && !append && !overwrite) {
16604
16626
  throw createTextError("GBK_FILE_EXISTS", `\u76EE\u6807\u6587\u4EF6\u5DF2\u5B58\u5728: ${candidatePath}`);
16605
16627
  }
16606
- const targetEncoding = existing && preserveEncoding ? existing.encoding : requestedEncoding === "auto" ? "utf8" : requestedEncoding;
16628
+ if (!existing && requestedEncoding === "auto") {
16629
+ throw createTextError("TEXT_UNKNOWN_ENCODING", "\u65B0\u6587\u4EF6\u5728 encoding=auto \u4E0B\u65E0\u6CD5\u786E\u5B9A\u7F16\u7801\uFF0C\u8BF7\u663E\u5F0F\u6307\u5B9A encoding");
16630
+ }
16631
+ const targetEncoding = existing && preserveEncoding ? existing.encoding : resolveExplicitTextEncoding(requestedEncoding, existing?.encoding ?? "utf8");
16607
16632
  const targetHasBom = targetEncoding === "utf8-bom" ? true : existing && preserveEncoding ? existing.hasBom : targetEncoding === "utf16le" || targetEncoding === "utf16be" ? true : false;
16608
16633
  const baseContent = append ? existing?.content ?? "" : "";
16609
16634
  const rawContent = `${baseContent}${input.content}`;
@@ -16631,7 +16656,7 @@ var text_write_default = tool({
16631
16656
  description: `Write text files while preserving detected encoding and newline style by default.
16632
16657
 
16633
16658
  - Existing files keep their original encoding when encoding=auto.
16634
- - New files default to utf8 unless encoding is specified.
16659
+ - New files require explicit encoding when encoding=auto.
16635
16660
  - Supports append and overwrite modes.`,
16636
16661
  args: {
16637
16662
  filePath: tool.schema.string().describe("Target file path"),
@@ -17409,6 +17409,7 @@ var TEXT_TOOL_SYSTEM_PROMPT = [
17409
17409
  "\u6587\u672C\u6587\u4EF6\u5904\u7406\u89C4\u5219\uFF1A",
17410
17410
  "- \u5904\u7406\u6587\u672C\u6587\u4EF6\u65F6\uFF0C\u4F18\u5148\u4F7F\u7528 text_read\u3001text_write\u3001text_edit\u3002",
17411
17411
  "- text_* \u9ED8\u8BA4\u4F1A\u81EA\u52A8\u8BC6\u522B\u73B0\u6709\u6587\u4EF6\u7F16\u7801\uFF0C\u5E76\u5728\u4FEE\u6539\u65F6\u5C3D\u91CF\u4FDD\u6301\u539F\u7F16\u7801\u3001BOM \u548C\u6362\u884C\u98CE\u683C\u3002",
17412
+ "- \u65B0\u6587\u4EF6\u5728 encoding=auto \u4E0B\u4E0D\u4F1A\u9759\u9ED8\u9ED8\u8BA4\u6210 utf8\uFF1B\u8BF7\u663E\u5F0F\u6307\u5B9A encoding\u3002",
17412
17413
  "- \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",
17413
17414
  "- \u53EA\u6709\u5728\u660E\u786E\u505A\u7CBE\u786E\u66FF\u6362\u65F6\uFF0C\u624D\u4F7F\u7528 oldString/newString\u3002",
17414
17415
  "- \u82E5\u68C0\u6D4B\u7F6E\u4FE1\u5EA6\u4E0D\u8DB3\u6216\u51FA\u73B0 TEXT_UNKNOWN_ENCODING\uFF0C\u8BF7\u663E\u5F0F\u6307\u5B9A encoding \u540E\u91CD\u8BD5\u3002",
@@ -17423,10 +17424,25 @@ function appendTextToolSystemPrompt(system) {
17423
17424
 
17424
17425
  // src/lib/text-file.ts
17425
17426
  var import_iconv_lite2 = __toESM(require_lib(), 1);
17427
+ import crypto2 from "crypto";
17428
+ import { createReadStream as createReadStream2 } from "fs";
17426
17429
  import fs3 from "fs/promises";
17427
17430
  import path3 from "path";
17428
17431
  var TEXT_STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
17432
+ var TEXT_DETECTION_SAMPLE_BYTES = 64 * 1024;
17429
17433
  var UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
17434
+ function finalizeNewlineStyle2(crlfCount, lfCount) {
17435
+ if (crlfCount > 0 && lfCount === 0) {
17436
+ return "crlf";
17437
+ }
17438
+ if (lfCount > 0 && crlfCount === 0) {
17439
+ return "lf";
17440
+ }
17441
+ if (crlfCount > 0 && lfCount > 0) {
17442
+ return "mixed";
17443
+ }
17444
+ return "none";
17445
+ }
17430
17446
  function assertStringArgument2(value, name) {
17431
17447
  if (typeof value !== "string") {
17432
17448
  throw createTextError("GBK_INVALID_ARGUMENT", `${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
@@ -17446,6 +17462,21 @@ function resolveExplicitTextEncoding(value, fallback) {
17446
17462
  const requested = value ?? "auto";
17447
17463
  return requested === "auto" ? fallback : requested;
17448
17464
  }
17465
+ function getBomPrefix(encoding, hasBom) {
17466
+ if (!hasBom) {
17467
+ return Buffer.alloc(0);
17468
+ }
17469
+ if (encoding === "utf8-bom") {
17470
+ return Buffer.from([239, 187, 191]);
17471
+ }
17472
+ if (encoding === "utf16le") {
17473
+ return Buffer.from([255, 254]);
17474
+ }
17475
+ if (encoding === "utf16be") {
17476
+ return Buffer.from([254, 255]);
17477
+ }
17478
+ return Buffer.alloc(0);
17479
+ }
17449
17480
  function isSupportedEncoding(value) {
17450
17481
  return value === "utf8" || value === "utf8-bom" || value === "utf16le" || value === "utf16be" || value === "gbk" || value === "gb18030";
17451
17482
  }
@@ -17457,23 +17488,23 @@ function assertTextEncodingSupported(encoding) {
17457
17488
  function decodeUtf16be(buffer) {
17458
17489
  return import_iconv_lite2.default.decode(buffer, "utf16be");
17459
17490
  }
17460
- function encodeText(content, encoding, hasBom = encoding === "utf8-bom") {
17461
- if (encoding === "utf8") {
17491
+ function encodeTextBody(content, encoding) {
17492
+ if (encoding === "utf8" || encoding === "utf8-bom") {
17462
17493
  return Buffer.from(content, "utf8");
17463
17494
  }
17464
- if (encoding === "utf8-bom") {
17465
- return Buffer.concat([Buffer.from([239, 187, 191]), Buffer.from(content, "utf8")]);
17466
- }
17467
17495
  if (encoding === "utf16le") {
17468
- const body = Buffer.from(content, "utf16le");
17469
- return hasBom ? Buffer.concat([Buffer.from([255, 254]), body]) : body;
17496
+ return Buffer.from(content, "utf16le");
17470
17497
  }
17471
17498
  if (encoding === "utf16be") {
17472
- const body = import_iconv_lite2.default.encode(content, "utf16be");
17473
- return hasBom ? Buffer.concat([Buffer.from([254, 255]), body]) : body;
17499
+ return import_iconv_lite2.default.encode(content, "utf16be");
17474
17500
  }
17475
17501
  return import_iconv_lite2.default.encode(content, encoding);
17476
17502
  }
17503
+ function encodeText(content, encoding, hasBom = encoding === "utf8-bom") {
17504
+ const bom = getBomPrefix(encoding, hasBom);
17505
+ const body = encodeTextBody(content, encoding);
17506
+ return bom.length === 0 ? body : Buffer.concat([bom, body]);
17507
+ }
17477
17508
  function decodeText(buffer, encoding) {
17478
17509
  if (encoding === "utf8") {
17479
17510
  return buffer.toString("utf8");
@@ -17749,6 +17780,169 @@ async function resolveReadableTextFile(input) {
17749
17780
  }
17750
17781
  return { filePath: candidatePath, stat };
17751
17782
  }
17783
+ async function readDetectionBuffer(filePath, sampleSize = TEXT_DETECTION_SAMPLE_BYTES) {
17784
+ const handle = await fs3.open(filePath, "r");
17785
+ try {
17786
+ const buffer = Buffer.alloc(sampleSize);
17787
+ const { bytesRead } = await handle.read(buffer, 0, sampleSize, 0);
17788
+ return buffer.subarray(0, bytesRead);
17789
+ } finally {
17790
+ await handle.close();
17791
+ }
17792
+ }
17793
+ async function detectReadableTextFile(input) {
17794
+ const resolved = await resolveReadableTextFile(input);
17795
+ const sample = await readDetectionBuffer(resolved.filePath);
17796
+ assertLikelyTextBuffer(sample);
17797
+ const detected = detectTextEncodingFromBuffer(sample, input.encoding ?? "auto");
17798
+ return {
17799
+ ...resolved,
17800
+ encoding: detected.detectedEncoding,
17801
+ requestedEncoding: detected.requestedEncoding,
17802
+ detectedEncoding: detected.detectedEncoding,
17803
+ confidence: detected.confidence,
17804
+ hasBom: detected.hasBom
17805
+ };
17806
+ }
17807
+ function createLineCollector2(offset, limit) {
17808
+ const lines = [];
17809
+ let pending = "";
17810
+ let totalLines = 0;
17811
+ let crlfCount = 0;
17812
+ let lfCount = 0;
17813
+ const emitLine = (line) => {
17814
+ totalLines += 1;
17815
+ if (totalLines >= offset && lines.length < limit) {
17816
+ lines.push(`${totalLines}: ${line}`);
17817
+ }
17818
+ };
17819
+ return {
17820
+ push(text) {
17821
+ if (text.length === 0) {
17822
+ return;
17823
+ }
17824
+ const combined = pending + text;
17825
+ let start = 0;
17826
+ while (true) {
17827
+ const newlineIndex = combined.indexOf("\n", start);
17828
+ if (newlineIndex === -1) {
17829
+ break;
17830
+ }
17831
+ let line = combined.slice(start, newlineIndex);
17832
+ if (line.endsWith("\r")) {
17833
+ line = line.slice(0, -1);
17834
+ crlfCount += 1;
17835
+ } else {
17836
+ lfCount += 1;
17837
+ }
17838
+ emitLine(line);
17839
+ start = newlineIndex + 1;
17840
+ }
17841
+ pending = combined.slice(start);
17842
+ },
17843
+ finish() {
17844
+ emitLine(pending);
17845
+ return {
17846
+ startLine: offset,
17847
+ endLine: lines.length === 0 ? totalLines : offset + lines.length - 1,
17848
+ totalLines,
17849
+ content: lines.join("\n"),
17850
+ tail: false,
17851
+ truncated: (lines.length === 0 ? totalLines : offset + lines.length - 1) < totalLines,
17852
+ newlineStyle: finalizeNewlineStyle2(crlfCount, lfCount)
17853
+ };
17854
+ }
17855
+ };
17856
+ }
17857
+ function createTailCollector2(limit) {
17858
+ assertPositiveInteger(limit, "limit");
17859
+ const lines = [];
17860
+ let pending = "";
17861
+ let totalLines = 0;
17862
+ let crlfCount = 0;
17863
+ let lfCount = 0;
17864
+ const emitLine = (line) => {
17865
+ totalLines += 1;
17866
+ lines.push(line);
17867
+ if (lines.length > limit) {
17868
+ lines.shift();
17869
+ }
17870
+ };
17871
+ return {
17872
+ push(text) {
17873
+ if (text.length === 0) {
17874
+ return;
17875
+ }
17876
+ const combined = pending + text;
17877
+ let start = 0;
17878
+ while (true) {
17879
+ const newlineIndex = combined.indexOf("\n", start);
17880
+ if (newlineIndex === -1) {
17881
+ break;
17882
+ }
17883
+ let line = combined.slice(start, newlineIndex);
17884
+ if (line.endsWith("\r")) {
17885
+ line = line.slice(0, -1);
17886
+ crlfCount += 1;
17887
+ } else {
17888
+ lfCount += 1;
17889
+ }
17890
+ emitLine(line);
17891
+ start = newlineIndex + 1;
17892
+ }
17893
+ pending = combined.slice(start);
17894
+ },
17895
+ finish() {
17896
+ emitLine(pending);
17897
+ const startLine = Math.max(1, totalLines - lines.length + 1);
17898
+ return {
17899
+ startLine,
17900
+ endLine: totalLines,
17901
+ totalLines,
17902
+ content: lines.map((line, index) => `${startLine + index}: ${line}`).join("\n"),
17903
+ truncated: startLine > 1,
17904
+ tail: true,
17905
+ newlineStyle: finalizeNewlineStyle2(crlfCount, lfCount)
17906
+ };
17907
+ }
17908
+ };
17909
+ }
17910
+ async function visitDecodedTextChunks2(resolved, visitor) {
17911
+ const stream = createReadStream2(resolved.filePath);
17912
+ const bomLength = getBomPrefix(resolved.encoding, resolved.hasBom).length;
17913
+ const decoderEncoding = resolved.encoding === "utf8-bom" ? "utf8" : resolved.encoding;
17914
+ const decoder = import_iconv_lite2.default.getDecoder(decoderEncoding);
17915
+ let skippedBom = false;
17916
+ try {
17917
+ for await (const chunk of stream) {
17918
+ let buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
17919
+ if (!skippedBom && bomLength > 0) {
17920
+ buffer = buffer.subarray(Math.min(bomLength, buffer.length));
17921
+ skippedBom = true;
17922
+ } else {
17923
+ skippedBom = true;
17924
+ }
17925
+ if (buffer.length === 0) {
17926
+ continue;
17927
+ }
17928
+ const text = decoder.write(buffer);
17929
+ if (text.length > 0) {
17930
+ await visitor(text);
17931
+ }
17932
+ }
17933
+ const trailingText = decoder.end() ?? "";
17934
+ if (trailingText.length > 0) {
17935
+ await visitor(trailingText);
17936
+ }
17937
+ } catch (error45) {
17938
+ if (error45 instanceof Error && "code" in error45) {
17939
+ throw error45;
17940
+ }
17941
+ throw createTextError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${resolved.filePath}`, error45);
17942
+ } finally {
17943
+ stream.destroy();
17944
+ }
17945
+ }
17752
17946
  async function readWholeTextFile(input) {
17753
17947
  const resolved = await resolveReadableTextFile(input);
17754
17948
  try {
@@ -17914,26 +18108,46 @@ async function readTextFile(input) {
17914
18108
  const offset = normalizeOptionalPositiveInteger(input.offset, "offset") ?? 1;
17915
18109
  const limit = normalizeOptionalPositiveInteger(input.limit, "limit") ?? 2e3;
17916
18110
  const tail = input.tail ?? false;
17917
- const loaded = await readWholeTextFile(input);
17918
- const lineWindow = tail ? collectTailLines2(loaded.content, limit) : {
17919
- ...splitLinesWithNumbers(loaded.content, offset, limit),
17920
- tail: false,
17921
- truncated: false
17922
- };
17923
- if (!tail) {
17924
- lineWindow.truncated = lineWindow.endLine < lineWindow.totalLines;
18111
+ const resolved = await detectReadableTextFile(input);
18112
+ if (resolved.stat.size < TEXT_STREAMING_FILE_SIZE_THRESHOLD_BYTES) {
18113
+ const loaded = await readWholeTextFile(input);
18114
+ const lineWindow2 = tail ? collectTailLines2(loaded.content, limit) : {
18115
+ ...splitLinesWithNumbers(loaded.content, offset, limit),
18116
+ tail: false,
18117
+ truncated: false
18118
+ };
18119
+ if (!tail) {
18120
+ lineWindow2.truncated = lineWindow2.endLine < lineWindow2.totalLines;
18121
+ }
18122
+ return {
18123
+ filePath: loaded.filePath,
18124
+ encoding: loaded.encoding,
18125
+ requestedEncoding: loaded.requestedEncoding,
18126
+ detectedEncoding: loaded.detectedEncoding,
18127
+ confidence: loaded.confidence,
18128
+ hasBom: loaded.hasBom,
18129
+ bytesRead: loaded.bytesRead,
18130
+ fileSize: loaded.fileSize,
18131
+ streamed: false,
18132
+ newlineStyle: loaded.newlineStyle,
18133
+ ...lineWindow2
18134
+ };
17925
18135
  }
18136
+ const collector = tail ? createTailCollector2(limit) : createLineCollector2(offset, limit);
18137
+ await visitDecodedTextChunks2(resolved, (text) => {
18138
+ collector.push(text);
18139
+ });
18140
+ const lineWindow = collector.finish();
17926
18141
  return {
17927
- filePath: loaded.filePath,
17928
- encoding: loaded.encoding,
17929
- requestedEncoding: loaded.requestedEncoding,
17930
- detectedEncoding: loaded.detectedEncoding,
17931
- confidence: loaded.confidence,
17932
- hasBom: loaded.hasBom,
17933
- bytesRead: loaded.bytesRead,
17934
- fileSize: loaded.fileSize,
17935
- streamed: false,
17936
- newlineStyle: loaded.newlineStyle,
18142
+ filePath: resolved.filePath,
18143
+ encoding: resolved.encoding,
18144
+ requestedEncoding: resolved.requestedEncoding,
18145
+ detectedEncoding: resolved.detectedEncoding,
18146
+ confidence: resolved.confidence,
18147
+ hasBom: resolved.hasBom,
18148
+ bytesRead: resolved.stat.size,
18149
+ fileSize: resolved.stat.size,
18150
+ streamed: true,
17937
18151
  ...lineWindow
17938
18152
  };
17939
18153
  }
@@ -17965,7 +18179,10 @@ async function writeTextFile(input) {
17965
18179
  if (existing && !append && !overwrite) {
17966
18180
  throw createTextError("GBK_FILE_EXISTS", `\u76EE\u6807\u6587\u4EF6\u5DF2\u5B58\u5728: ${candidatePath}`);
17967
18181
  }
17968
- const targetEncoding = existing && preserveEncoding ? existing.encoding : requestedEncoding === "auto" ? "utf8" : requestedEncoding;
18182
+ if (!existing && requestedEncoding === "auto") {
18183
+ throw createTextError("TEXT_UNKNOWN_ENCODING", "\u65B0\u6587\u4EF6\u5728 encoding=auto \u4E0B\u65E0\u6CD5\u786E\u5B9A\u7F16\u7801\uFF0C\u8BF7\u663E\u5F0F\u6307\u5B9A encoding");
18184
+ }
18185
+ const targetEncoding = existing && preserveEncoding ? existing.encoding : resolveExplicitTextEncoding(requestedEncoding, existing?.encoding ?? "utf8");
17969
18186
  const targetHasBom = targetEncoding === "utf8-bom" ? true : existing && preserveEncoding ? existing.hasBom : targetEncoding === "utf16le" || targetEncoding === "utf16be" ? true : false;
17970
18187
  const baseContent = append ? existing?.content ?? "" : "";
17971
18188
  const rawContent = `${baseContent}${input.content}`;
@@ -17994,70 +18211,84 @@ async function replaceTextFileText(input) {
17994
18211
  startLine: normalizeOptionalPositiveInteger(input.startLine, "startLine"),
17995
18212
  endLine: normalizeOptionalPositiveInteger(input.endLine, "endLine")
17996
18213
  };
17997
- const loaded = await readWholeTextFile({
17998
- filePath: input.filePath,
17999
- encoding: input.encoding ?? "auto",
18000
- allowExternal: input.allowExternal,
18001
- context: input.context
18002
- });
18003
18214
  if (mode === "insertAfter" || mode === "insertBefore") {
18215
+ const loaded2 = await readWholeTextFile({
18216
+ filePath: input.filePath,
18217
+ encoding: input.encoding ?? "auto",
18218
+ allowExternal: input.allowExternal,
18219
+ context: input.context
18220
+ });
18004
18221
  assertInsertArguments2(normalizedInput);
18005
18222
  const occurrence = normalizeOptionalPositiveInteger(normalizedInput.occurrence, "occurrence") ?? 1;
18006
18223
  const insertResult = buildInsertOutput(
18007
- loaded.content,
18224
+ loaded2.content,
18008
18225
  mode,
18009
18226
  normalizedInput.anchor,
18010
18227
  normalizedInput.content,
18011
18228
  occurrence,
18012
18229
  resolveInsertIfExists(normalizedInput.ifExists),
18013
- loaded.newlineStyle
18230
+ loaded2.newlineStyle
18014
18231
  );
18015
18232
  if (insertResult.skipped) {
18016
18233
  return {
18017
18234
  mode,
18018
- filePath: loaded.filePath,
18019
- encoding: loaded.encoding,
18020
- requestedEncoding: loaded.requestedEncoding,
18021
- detectedEncoding: loaded.detectedEncoding,
18022
- confidence: loaded.confidence,
18023
- hasBom: loaded.hasBom,
18235
+ filePath: loaded2.filePath,
18236
+ encoding: loaded2.encoding,
18237
+ requestedEncoding: loaded2.requestedEncoding,
18238
+ detectedEncoding: loaded2.detectedEncoding,
18239
+ confidence: loaded2.confidence,
18240
+ hasBom: loaded2.hasBom,
18024
18241
  anchor: insertResult.anchor,
18025
18242
  occurrence: insertResult.occurrence,
18026
18243
  anchorMatches: insertResult.anchorMatches,
18027
18244
  inserted: false,
18028
18245
  skipped: true,
18029
- bytesRead: loaded.bytesRead,
18246
+ bytesRead: loaded2.bytesRead,
18030
18247
  bytesWritten: 0,
18031
- newlineStyle: loaded.newlineStyle
18248
+ newlineStyle: loaded2.newlineStyle,
18249
+ diffPreview: buildLineDiffPreview(loaded2.filePath, loaded2.encoding, insertResult.previewBefore, insertResult.previewAfter)
18032
18250
  };
18033
18251
  }
18034
- const targetEncoding2 = (normalizedInput.preserveEncoding ?? true) || (normalizedInput.encoding ?? "auto") === "auto" ? loaded.encoding : resolveExplicitTextEncoding(normalizedInput.encoding, loaded.encoding);
18035
- const targetHasBom2 = normalizedInput.preserveEncoding === false ? targetEncoding2 === "utf8-bom" || targetEncoding2 === "utf16le" || targetEncoding2 === "utf16be" : loaded.hasBom;
18252
+ const targetEncoding2 = (normalizedInput.preserveEncoding ?? true) || (normalizedInput.encoding ?? "auto") === "auto" ? loaded2.encoding : resolveExplicitTextEncoding(normalizedInput.encoding, loaded2.encoding);
18253
+ const targetHasBom2 = normalizedInput.preserveEncoding === false ? targetEncoding2 === "utf8-bom" || targetEncoding2 === "utf16le" || targetEncoding2 === "utf16be" : loaded2.hasBom;
18036
18254
  ensureLossless(insertResult.outputText, targetEncoding2, targetHasBom2);
18037
18255
  const buffer2 = encodeText(insertResult.outputText, targetEncoding2, targetHasBom2);
18038
- await fs3.writeFile(loaded.filePath, buffer2);
18256
+ await fs3.writeFile(loaded2.filePath, buffer2);
18039
18257
  return {
18040
18258
  mode,
18041
- filePath: loaded.filePath,
18259
+ filePath: loaded2.filePath,
18042
18260
  encoding: targetEncoding2,
18043
- requestedEncoding: loaded.requestedEncoding,
18044
- detectedEncoding: loaded.detectedEncoding,
18045
- confidence: loaded.confidence,
18261
+ requestedEncoding: loaded2.requestedEncoding,
18262
+ detectedEncoding: loaded2.detectedEncoding,
18263
+ confidence: loaded2.confidence,
18046
18264
  hasBom: targetHasBom2,
18047
18265
  anchor: insertResult.anchor,
18048
18266
  occurrence: insertResult.occurrence,
18049
18267
  anchorMatches: insertResult.anchorMatches,
18050
18268
  inserted: true,
18051
18269
  skipped: false,
18052
- bytesRead: loaded.bytesRead,
18270
+ bytesRead: loaded2.bytesRead,
18053
18271
  bytesWritten: buffer2.byteLength,
18054
- newlineStyle: detectNewlineStyle(insertResult.outputText)
18272
+ newlineStyle: detectNewlineStyle(insertResult.outputText),
18273
+ diffPreview: buildLineDiffPreview(loaded2.filePath, targetEncoding2, insertResult.previewBefore, insertResult.previewAfter)
18055
18274
  };
18056
18275
  }
18057
18276
  assertReplaceArguments2(input);
18058
18277
  if (input.oldString.length === 0) {
18059
18278
  throw createTextError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
18060
18279
  }
18280
+ const resolved = await detectReadableTextFile({
18281
+ filePath: input.filePath,
18282
+ encoding: input.encoding ?? "auto",
18283
+ allowExternal: input.allowExternal,
18284
+ context: input.context
18285
+ });
18286
+ const loaded = await readWholeTextFile({
18287
+ filePath: input.filePath,
18288
+ encoding: input.encoding ?? "auto",
18289
+ allowExternal: input.allowExternal,
18290
+ context: input.context
18291
+ });
18061
18292
  const scope = resolveEditScope2(loaded.content, normalizedInput);
18062
18293
  const replaceAll = normalizedInput.replaceAll ?? false;
18063
18294
  const preserveEncoding = normalizedInput.preserveEncoding ?? true;
@@ -18092,43 +18323,8 @@ async function replaceTextFileText(input) {
18092
18323
  occurrencesBefore,
18093
18324
  bytesRead: loaded.bytesRead,
18094
18325
  bytesWritten: buffer.byteLength,
18095
- newlineStyle: detectNewlineStyle(outputText)
18096
- };
18097
- }
18098
- async function createTextDiffPreview(input) {
18099
- const mode = resolveEditMode(input.mode);
18100
- const loaded = await readWholeTextFile({
18101
- filePath: input.filePath,
18102
- encoding: input.encoding ?? "auto",
18103
- allowExternal: input.allowExternal,
18104
- context: input.context
18105
- });
18106
- if (mode === "insertAfter" || mode === "insertBefore") {
18107
- assertInsertArguments2(input);
18108
- const occurrence = normalizeOptionalPositiveInteger(input.occurrence, "occurrence") ?? 1;
18109
- const insertResult = buildInsertOutput(
18110
- loaded.content,
18111
- mode,
18112
- input.anchor,
18113
- input.content,
18114
- occurrence,
18115
- resolveInsertIfExists(input.ifExists),
18116
- loaded.newlineStyle
18117
- );
18118
- return {
18119
- filePath: loaded.filePath,
18120
- encoding: loaded.encoding,
18121
- preview: buildLineDiffPreview(loaded.filePath, loaded.encoding, insertResult.previewBefore, insertResult.previewAfter)
18122
- };
18123
- }
18124
- assertReplaceArguments2(input);
18125
- const scope = resolveEditScope2(loaded.content, input);
18126
- const alignedNewString = alignTextToNewlineStyle(input.newString, loaded.newlineStyle);
18127
- const replaced = input.replaceAll ?? false ? scope.selectedText.split(input.oldString).join(alignedNewString) : scope.selectedText.replace(input.oldString, alignedNewString);
18128
- return {
18129
- filePath: loaded.filePath,
18130
- encoding: loaded.encoding,
18131
- preview: buildLineDiffPreview(loaded.filePath, loaded.encoding, scope.selectedText, replaced)
18326
+ newlineStyle: detectNewlineStyle(outputText),
18327
+ diffPreview: buildLineDiffPreview(loaded.filePath, targetEncoding, scope.selectedText, replaced)
18132
18328
  };
18133
18329
  }
18134
18330
 
@@ -18161,7 +18357,6 @@ var text_edit_default = tool({
18161
18357
  },
18162
18358
  async execute(args, context) {
18163
18359
  const result = await replaceTextFileText({ ...args, context });
18164
- const preview = await createTextDiffPreview({ ...args, context });
18165
18360
  const title = result.mode === "replace" ? `\u6587\u672C\u7F16\u8F91 ${result.encoding.toUpperCase()} x${result.replacements}` : result.inserted ? `\u6587\u672C\u63D2\u5165 ${result.encoding.toUpperCase()} #${result.occurrence}` : `\u6587\u672C\u8DF3\u8FC7 ${result.encoding.toUpperCase()} #${result.occurrence}`;
18166
18361
  context.metadata({
18167
18362
  title,
@@ -18180,7 +18375,7 @@ var text_edit_default = tool({
18180
18375
  inserted: result.mode === "replace" ? void 0 : result.inserted,
18181
18376
  skipped: result.mode === "replace" ? void 0 : result.skipped,
18182
18377
  newlineStyle: result.newlineStyle,
18183
- diffPreview: preview.preview
18378
+ diffPreview: result.diffPreview ?? ""
18184
18379
  }
18185
18380
  });
18186
18381
  return JSON.stringify(result, null, 2);
@@ -18228,7 +18423,7 @@ var text_write_default = tool({
18228
18423
  description: `Write text files while preserving detected encoding and newline style by default.
18229
18424
 
18230
18425
  - Existing files keep their original encoding when encoding=auto.
18231
- - New files default to utf8 unless encoding is specified.
18426
+ - New files require explicit encoding when encoding=auto.
18232
18427
  - Supports append and overwrite modes.`,
18233
18428
  args: {
18234
18429
  filePath: tool.schema.string().describe("Target file path"),
@@ -5,6 +5,7 @@ var TEXT_TOOL_SYSTEM_PROMPT = [
5
5
  "\u6587\u672C\u6587\u4EF6\u5904\u7406\u89C4\u5219\uFF1A",
6
6
  "- \u5904\u7406\u6587\u672C\u6587\u4EF6\u65F6\uFF0C\u4F18\u5148\u4F7F\u7528 text_read\u3001text_write\u3001text_edit\u3002",
7
7
  "- text_* \u9ED8\u8BA4\u4F1A\u81EA\u52A8\u8BC6\u522B\u73B0\u6709\u6587\u4EF6\u7F16\u7801\uFF0C\u5E76\u5728\u4FEE\u6539\u65F6\u5C3D\u91CF\u4FDD\u6301\u539F\u7F16\u7801\u3001BOM \u548C\u6362\u884C\u98CE\u683C\u3002",
8
+ "- \u65B0\u6587\u4EF6\u5728 encoding=auto \u4E0B\u4E0D\u4F1A\u9759\u9ED8\u9ED8\u8BA4\u6210 utf8\uFF1B\u8BF7\u663E\u5F0F\u6307\u5B9A encoding\u3002",
8
9
  "- \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",
9
10
  "- \u53EA\u6709\u5728\u660E\u786E\u505A\u7CBE\u786E\u66FF\u6362\u65F6\uFF0C\u624D\u4F7F\u7528 oldString/newString\u3002",
10
11
  "- \u82E5\u68C0\u6D4B\u7F6E\u4FE1\u5EA6\u4E0D\u8DB3\u6216\u51FA\u73B0 TEXT_UNKNOWN_ENCODING\uFF0C\u8BF7\u663E\u5F0F\u6307\u5B9A encoding \u540E\u91CD\u8BD5\u3002",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifestVersion": 1,
3
3
  "packageName": "opencode-gbk-tools",
4
- "packageVersion": "0.1.12",
4
+ "packageVersion": "0.1.14",
5
5
  "artifacts": [
6
6
  {
7
7
  "relativePath": "tools/gbk_edit.js",
@@ -30,19 +30,19 @@
30
30
  {
31
31
  "relativePath": "tools/text_edit.js",
32
32
  "kind": "tool",
33
- "expectedHash": "7389be2ae7134574a083800df62001761b993a80bffe8835c6e6a346112653f1",
33
+ "expectedHash": "28e22f96cb6e738c55a3c90b8ffae9ef5d5d702649353373d0d04e882c60a5ee",
34
34
  "hashAlgorithm": "sha256"
35
35
  },
36
36
  {
37
37
  "relativePath": "tools/text_read.js",
38
38
  "kind": "tool",
39
- "expectedHash": "54aa450b02c1efa98ee4528f9b4511ba454d02bb8b03c332558d6931fa025fb9",
39
+ "expectedHash": "09de6b032a6c622471e00701be56274e86a6019406f1bf4e45016b90ffabc4d9",
40
40
  "hashAlgorithm": "sha256"
41
41
  },
42
42
  {
43
43
  "relativePath": "tools/text_write.js",
44
44
  "kind": "tool",
45
- "expectedHash": "fada2d48f25e5348461140bc2899158af12a2b331b37c90bb20c7730c33276c6",
45
+ "expectedHash": "d9b941bb88ecad271a9865eac6595ead964357a04e278ecd43941d4212ab69f3",
46
46
  "hashAlgorithm": "sha256"
47
47
  },
48
48
  {
@@ -54,7 +54,7 @@
54
54
  {
55
55
  "relativePath": "plugins/opencode-gbk-tools.js",
56
56
  "kind": "plugin",
57
- "expectedHash": "5804344669c2fa34f9d18590b931e7649413c25f78107d60e1215e5a45b0f7f8",
57
+ "expectedHash": "efde5f79ebaa4393a276aa9da0be2f172e90127de7ccfbe16a186eb04f02975a",
58
58
  "hashAlgorithm": "sha256"
59
59
  }
60
60
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gbk-tools",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Auto-encoding text tools plus GBK/GB18030 tools for OpenCode",
5
5
  "type": "module",
6
6
  "license": "MIT",