opencode-gbk-tools 0.1.10 → 0.1.11

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.
@@ -16306,6 +16306,82 @@ async function assertPathAllowed(filePath, context, allowExternal = false) {
16306
16306
 
16307
16307
  // src/lib/gbk-file.ts
16308
16308
  var STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
16309
+ function assertStringArgument(value, name) {
16310
+ if (typeof value !== "string") {
16311
+ throw createGbkError("GBK_INVALID_ARGUMENT", `${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
16312
+ }
16313
+ }
16314
+ function assertReplaceArguments(input) {
16315
+ assertStringArgument(input.oldString, "oldString");
16316
+ assertStringArgument(input.newString, "newString");
16317
+ }
16318
+ function assertInsertArguments(input) {
16319
+ assertStringArgument(input.anchor, "anchor");
16320
+ assertStringArgument(input.content, "content");
16321
+ if (input.content.length === 0) {
16322
+ throw createGbkError("GBK_INVALID_ARGUMENT", "content \u4E0D\u80FD\u4E3A\u7A7A");
16323
+ }
16324
+ if (input.replaceAll !== void 0) {
16325
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\u6301 replaceAll");
16326
+ }
16327
+ if (input.startLine !== void 0 || input.endLine !== void 0 || input.startAnchor !== void 0 || input.endAnchor !== void 0) {
16328
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\u6301 startLine/endLine/startAnchor/endAnchor");
16329
+ }
16330
+ }
16331
+ function findOccurrenceIndex(text, token, occurrence) {
16332
+ assertStringArgument(text, "text");
16333
+ assertStringArgument(token, "anchor");
16334
+ assertPositiveInteger(occurrence, "occurrence");
16335
+ if (token.length === 0) {
16336
+ throw createGbkError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
16337
+ }
16338
+ let count = 0;
16339
+ let searchFrom = 0;
16340
+ while (true) {
16341
+ const index = text.indexOf(token, searchFrom);
16342
+ if (index === -1) {
16343
+ break;
16344
+ }
16345
+ count += 1;
16346
+ if (count === occurrence) {
16347
+ return { index, total: countOccurrences(text, token) };
16348
+ }
16349
+ searchFrom = index + token.length;
16350
+ }
16351
+ if (count === 0) {
16352
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${token}`);
16353
+ }
16354
+ throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\u5230 ${count} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${occurrence} \u5904`);
16355
+ }
16356
+ function insertByAnchor(text, mode, anchor, content, occurrence, ifExists, newlineStyle) {
16357
+ const alignedContent = newlineStyle === "crlf" ? content.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : newlineStyle === "lf" ? content.replace(/\r\n/g, "\n") : content;
16358
+ const located = findOccurrenceIndex(text, anchor, occurrence);
16359
+ const insertionPoint = mode === "insertAfter" ? located.index + anchor.length : located.index;
16360
+ const alreadyExists = mode === "insertAfter" ? text.slice(insertionPoint, insertionPoint + alignedContent.length) === alignedContent : text.slice(Math.max(0, insertionPoint - alignedContent.length), insertionPoint) === alignedContent;
16361
+ if (alreadyExists) {
16362
+ if (ifExists === "error") {
16363
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
16364
+ }
16365
+ if (ifExists === "skip") {
16366
+ return {
16367
+ outputText: text,
16368
+ inserted: false,
16369
+ skipped: true,
16370
+ anchorMatches: located.total,
16371
+ occurrence,
16372
+ anchor
16373
+ };
16374
+ }
16375
+ }
16376
+ return {
16377
+ outputText: `${text.slice(0, insertionPoint)}${alignedContent}${text.slice(insertionPoint)}`,
16378
+ inserted: true,
16379
+ skipped: false,
16380
+ anchorMatches: located.total,
16381
+ occurrence,
16382
+ anchor
16383
+ };
16384
+ }
16309
16385
  function assertEncodingSupported(encoding) {
16310
16386
  if (encoding !== "gbk" && encoding !== "gb18030") {
16311
16387
  throw createGbkError("GBK_INVALID_ENCODING", `\u4E0D\u652F\u6301\u7684\u7F16\u7801: ${encoding}`);
@@ -16475,9 +16551,11 @@ function resolveEditScope(text, input) {
16475
16551
  };
16476
16552
  }
16477
16553
  function normalizeNewlines(text) {
16554
+ assertStringArgument(text, "text");
16478
16555
  return text.replace(/\r\n/g, "\n");
16479
16556
  }
16480
16557
  function trimRightSpaces(text) {
16558
+ assertStringArgument(text, "text");
16481
16559
  return text.replace(/[ \t]+$/g, "");
16482
16560
  }
16483
16561
  function splitNormalizedLines(text) {
@@ -16573,6 +16651,8 @@ function tryLooseBlockReplace(content, oldString, newString) {
16573
16651
  return null;
16574
16652
  }
16575
16653
  function countOccurrences(text, target) {
16654
+ assertStringArgument(text, "text");
16655
+ assertStringArgument(target, "oldString");
16576
16656
  if (target.length === 0) {
16577
16657
  throw createGbkError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
16578
16658
  }
@@ -16888,11 +16968,56 @@ async function readGbkFile(input) {
16888
16968
  };
16889
16969
  }
16890
16970
  async function replaceGbkFileText(input) {
16971
+ const mode = input.mode ?? "replace";
16891
16972
  const normalizedInput = {
16892
16973
  ...input,
16893
16974
  startLine: normalizeOptionalPositiveInteger(input.startLine, "startLine"),
16894
16975
  endLine: normalizeOptionalPositiveInteger(input.endLine, "endLine")
16895
16976
  };
16977
+ if (mode === "insertAfter" || mode === "insertBefore") {
16978
+ assertInsertArguments(input);
16979
+ const resolved2 = await resolveReadableGbkFile(normalizedInput);
16980
+ const current2 = await readWholeGbkTextFile(resolved2);
16981
+ const occurrence = normalizeOptionalPositiveInteger(input.occurrence, "occurrence") ?? 1;
16982
+ const insertResult = insertByAnchor(
16983
+ current2.content,
16984
+ mode,
16985
+ input.anchor,
16986
+ input.content,
16987
+ occurrence,
16988
+ input.ifExists ?? "skip",
16989
+ detectNewlineStyle(current2.content)
16990
+ );
16991
+ if (insertResult.skipped) {
16992
+ return {
16993
+ mode,
16994
+ filePath: current2.filePath,
16995
+ encoding: current2.encoding,
16996
+ anchor: insertResult.anchor,
16997
+ occurrence: insertResult.occurrence,
16998
+ anchorMatches: insertResult.anchorMatches,
16999
+ inserted: false,
17000
+ skipped: true,
17001
+ bytesRead: current2.bytesRead,
17002
+ bytesWritten: 0
17003
+ };
17004
+ }
17005
+ const buffer2 = import_iconv_lite.default.encode(insertResult.outputText, current2.encoding);
17006
+ await fs2.writeFile(current2.filePath, buffer2);
17007
+ return {
17008
+ mode,
17009
+ filePath: current2.filePath,
17010
+ encoding: current2.encoding,
17011
+ anchor: insertResult.anchor,
17012
+ occurrence: insertResult.occurrence,
17013
+ anchorMatches: insertResult.anchorMatches,
17014
+ inserted: true,
17015
+ skipped: false,
17016
+ bytesRead: current2.bytesRead,
17017
+ bytesWritten: buffer2.byteLength
17018
+ };
17019
+ }
17020
+ assertReplaceArguments(input);
16896
17021
  if (input.oldString.length === 0) {
16897
17022
  throw createGbkError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
16898
17023
  }
@@ -16909,14 +17034,15 @@ async function replaceGbkFileText(input) {
16909
17034
  }
16910
17035
  const current = await readWholeGbkTextFile(resolved);
16911
17036
  const scope = resolveEditScope(current.content, normalizedInput);
16912
- const occurrencesBefore = countOccurrences(scope.selectedText, normalizedInput.oldString);
17037
+ const occurrencesBefore = countOccurrences(scope.selectedText, input.oldString);
16913
17038
  if (!replaceAll && occurrencesBefore === 0) {
16914
- const loose = tryLooseBlockReplace(scope.selectedText, normalizedInput.oldString, normalizedInput.newString);
17039
+ const loose = tryLooseBlockReplace(scope.selectedText, input.oldString, input.newString);
16915
17040
  if (loose !== null) {
16916
17041
  const outputText2 = `${current.content.slice(0, scope.rangeStart)}${loose.content}${current.content.slice(scope.rangeEnd)}`;
16917
17042
  const buffer2 = import_iconv_lite.default.encode(outputText2, current.encoding);
16918
17043
  await fs2.writeFile(current.filePath, buffer2);
16919
17044
  return {
17045
+ mode: "replace",
16920
17046
  filePath: current.filePath,
16921
17047
  encoding: current.encoding,
16922
17048
  replacements: 1,
@@ -16928,20 +17054,21 @@ async function replaceGbkFileText(input) {
16928
17054
  }
16929
17055
  if (replaceAll) {
16930
17056
  if (occurrencesBefore === 0) {
16931
- throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, normalizedInput.oldString));
17057
+ throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, input.oldString));
16932
17058
  }
16933
17059
  } else if (occurrencesBefore === 0) {
16934
- throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, normalizedInput.oldString));
17060
+ throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, input.oldString));
16935
17061
  } else if (occurrencesBefore > 1) {
16936
- throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${normalizedInput.oldString}`);
17062
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
16937
17063
  }
16938
17064
  const fileNewlineStyle = detectNewlineStyle(current.content);
16939
- const alignedNewString = fileNewlineStyle === "crlf" ? normalizedInput.newString.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : fileNewlineStyle === "lf" ? normalizedInput.newString.replace(/\r\n/g, "\n") : normalizedInput.newString;
16940
- const replaced = replaceAll ? scope.selectedText.split(normalizedInput.oldString).join(alignedNewString) : scope.selectedText.replace(normalizedInput.oldString, alignedNewString);
17065
+ const alignedNewString = fileNewlineStyle === "crlf" ? input.newString.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : fileNewlineStyle === "lf" ? input.newString.replace(/\r\n/g, "\n") : input.newString;
17066
+ const replaced = replaceAll ? scope.selectedText.split(input.oldString).join(alignedNewString) : scope.selectedText.replace(input.oldString, alignedNewString);
16941
17067
  const outputText = `${current.content.slice(0, scope.rangeStart)}${replaced}${current.content.slice(scope.rangeEnd)}`;
16942
17068
  const buffer = import_iconv_lite.default.encode(outputText, current.encoding);
16943
17069
  await fs2.writeFile(current.filePath, buffer);
16944
17070
  return {
17071
+ mode: "replace",
16945
17072
  filePath: current.filePath,
16946
17073
  encoding: current.encoding,
16947
17074
  replacements: replaceAll ? occurrencesBefore : 1,
@@ -17105,11 +17232,20 @@ Recommended workflow for large files (when gbk_read returned truncated=true):
17105
17232
  3. gbk_edit(oldString=<content without prefixes>, newString=<new content>)
17106
17233
 
17107
17234
  For large files, use 'startLine'/'endLine' or 'startAnchor'/'endAnchor' to narrow the search scope
17108
- and avoid false matches. Scoped edits also improve performance on very large files.`,
17235
+ and avoid false matches. Scoped edits also improve performance on very large files.
17236
+
17237
+ Insert mode:
17238
+ - Use mode=insertAfter or mode=insertBefore with anchor/content
17239
+ - This is recommended when the intent is "insert after label" instead of exact replacement.`,
17109
17240
  args: {
17110
17241
  filePath: tool.schema.string().describe("Target file path"),
17111
- oldString: tool.schema.string().describe("Exact text to replace \u2014 raw file content only, no 'N: ' line number prefixes from gbk_read output"),
17112
- newString: tool.schema.string().describe("Replacement text \u2014 raw content only, no line number prefixes"),
17242
+ mode: tool.schema.enum(["replace", "insertAfter", "insertBefore"]).optional().describe("Edit mode, default replace"),
17243
+ oldString: tool.schema.string().optional().describe("Exact text to replace in replace mode"),
17244
+ newString: tool.schema.string().optional().describe("Replacement text in replace mode"),
17245
+ anchor: tool.schema.string().optional().describe("Anchor text used by insertAfter/insertBefore"),
17246
+ content: tool.schema.string().optional().describe("Inserted content used by insertAfter/insertBefore"),
17247
+ occurrence: tool.schema.number().int().positive().optional().describe("1-based anchor occurrence for insert mode"),
17248
+ ifExists: tool.schema.enum(["allow", "skip", "error"]).optional().describe("What to do when inserted content already exists at target position"),
17113
17249
  replaceAll: tool.schema.boolean().optional().describe("Replace all occurrences (default: false, requires unique match)"),
17114
17250
  startLine: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("Restrict edit scope to 1-based start line (inclusive)"),
17115
17251
  endLine: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("Restrict edit scope to 1-based end line (inclusive)"),
@@ -17120,13 +17256,21 @@ and avoid false matches. Scoped edits also improve performance on very large fil
17120
17256
  },
17121
17257
  async execute(args, context) {
17122
17258
  const result = await replaceGbkFileText({ ...args, context });
17259
+ const isReplace = !("mode" in result) || result.mode === "replace";
17260
+ const title = isReplace ? `GBK \u7F16\u8F91 ${result.encoding.toUpperCase()} x${result.replacements}` : result.inserted ? `GBK \u63D2\u5165 ${result.encoding.toUpperCase()} #${result.occurrence}` : `GBK \u8DF3\u8FC7 ${result.encoding.toUpperCase()} #${result.occurrence}`;
17123
17261
  context.metadata({
17124
- title: `GBK \u7F16\u8F91 ${result.encoding.toUpperCase()} x${result.replacements}`,
17262
+ title,
17125
17263
  metadata: {
17126
17264
  filePath: result.filePath,
17127
17265
  encoding: result.encoding,
17128
- replacements: result.replacements,
17129
- occurrencesBefore: result.occurrencesBefore,
17266
+ mode: isReplace ? "replace" : result.mode,
17267
+ replacements: isReplace ? result.replacements : void 0,
17268
+ occurrencesBefore: isReplace ? result.occurrencesBefore : void 0,
17269
+ anchor: isReplace ? void 0 : result.anchor,
17270
+ occurrence: isReplace ? void 0 : result.occurrence,
17271
+ anchorMatches: isReplace ? void 0 : result.anchorMatches,
17272
+ inserted: isReplace ? void 0 : result.inserted,
17273
+ skipped: isReplace ? void 0 : result.skipped,
17130
17274
  bytesRead: result.bytesRead,
17131
17275
  bytesWritten: result.bytesWritten
17132
17276
  }
@@ -17265,6 +17409,8 @@ var TEXT_TOOL_SYSTEM_PROMPT = [
17265
17409
  "\u6587\u672C\u6587\u4EF6\u5904\u7406\u89C4\u5219\uFF1A",
17266
17410
  "- \u5904\u7406\u6587\u672C\u6587\u4EF6\u65F6\uFF0C\u4F18\u5148\u4F7F\u7528 text_read\u3001text_write\u3001text_edit\u3002",
17267
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
+ "- \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
+ "- \u53EA\u6709\u5728\u660E\u786E\u505A\u7CBE\u786E\u66FF\u6362\u65F6\uFF0C\u624D\u4F7F\u7528 oldString/newString\u3002",
17268
17414
  "- \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",
17269
17415
  "- \u5904\u7406\u660E\u786E\u7684 GBK/GB18030 \u6587\u4EF6\u65F6\uFF0C\u4E5F\u53EF\u7EE7\u7EED\u4F7F\u7528 gbk_read\u3001gbk_write\u3001gbk_edit\u3001gbk_search\u3002"
17270
17416
  ].join("\n");
@@ -17281,6 +17427,25 @@ import fs3 from "fs/promises";
17281
17427
  import path3 from "path";
17282
17428
  var TEXT_STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
17283
17429
  var UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
17430
+ function assertStringArgument2(value, name) {
17431
+ if (typeof value !== "string") {
17432
+ throw createTextError("GBK_INVALID_ARGUMENT", `${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
17433
+ }
17434
+ }
17435
+ function assertReplaceArguments2(input) {
17436
+ assertStringArgument2(input.oldString, "oldString");
17437
+ assertStringArgument2(input.newString, "newString");
17438
+ }
17439
+ function resolveEditMode(mode) {
17440
+ return mode ?? "replace";
17441
+ }
17442
+ function resolveInsertIfExists(value) {
17443
+ return value ?? "skip";
17444
+ }
17445
+ function resolveExplicitTextEncoding(value, fallback) {
17446
+ const requested = value ?? "auto";
17447
+ return requested === "auto" ? fallback : requested;
17448
+ }
17284
17449
  function isSupportedEncoding(value) {
17285
17450
  return value === "utf8" || value === "utf8-bom" || value === "utf16le" || value === "utf16be" || value === "gbk" || value === "gb18030";
17286
17451
  }
@@ -17399,6 +17564,8 @@ function assertLikelyTextBuffer(buffer) {
17399
17564
  throw createTextError("GBK_BINARY_FILE", "\u7591\u4F3C\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u65E0\u6CD5\u6309\u6587\u672C\u5904\u7406");
17400
17565
  }
17401
17566
  function getNearestContext2(content, oldString) {
17567
+ assertStringArgument2(content, "content");
17568
+ assertStringArgument2(oldString, "oldString");
17402
17569
  const lines = content.split(/\r?\n/);
17403
17570
  const oldLines = oldString.replace(/\r\n/g, "\n").split("\n").filter(Boolean);
17404
17571
  const firstToken = oldLines[0] || oldString.trim();
@@ -17410,6 +17577,8 @@ function getNearestContext2(content, oldString) {
17410
17577
  return lines.slice(0, 4).join("\n");
17411
17578
  }
17412
17579
  function buildNoMatchMessage2(content, oldString) {
17580
+ assertStringArgument2(content, "content");
17581
+ assertStringArgument2(oldString, "oldString");
17413
17582
  return [
17414
17583
  "\u672A\u627E\u5230\u9700\u8981\u66FF\u6362\u7684\u6587\u672C\u3002",
17415
17584
  "oldString \u5FC5\u987B\u4E0E\u6587\u4EF6\u5B9E\u9645\u5185\u5BB9\u5B8C\u5168\u5BF9\u5E94\u3002",
@@ -17417,6 +17586,105 @@ function buildNoMatchMessage2(content, oldString) {
17417
17586
  getNearestContext2(content, oldString)
17418
17587
  ].join("\n");
17419
17588
  }
17589
+ function findOccurrenceIndex2(text, token, occurrence) {
17590
+ assertStringArgument2(text, "text");
17591
+ assertStringArgument2(token, "anchor");
17592
+ assertPositiveInteger(occurrence, "occurrence");
17593
+ if (token.length === 0) {
17594
+ throw createTextError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
17595
+ }
17596
+ let count = 0;
17597
+ let searchFrom = 0;
17598
+ while (true) {
17599
+ const index = text.indexOf(token, searchFrom);
17600
+ if (index === -1) {
17601
+ break;
17602
+ }
17603
+ count += 1;
17604
+ if (count === occurrence) {
17605
+ return { index, total: countOccurrences(text, token) };
17606
+ }
17607
+ searchFrom = index + token.length;
17608
+ }
17609
+ if (count === 0) {
17610
+ throw createTextError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${token}`);
17611
+ }
17612
+ throw createTextError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\u5230 ${count} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${occurrence} \u5904`);
17613
+ }
17614
+ function assertInsertArguments2(input) {
17615
+ assertStringArgument2(input.anchor, "anchor");
17616
+ assertStringArgument2(input.content, "content");
17617
+ if (input.content.length === 0) {
17618
+ throw createTextError("GBK_INVALID_ARGUMENT", "content \u4E0D\u80FD\u4E3A\u7A7A");
17619
+ }
17620
+ if (input.replaceAll !== void 0) {
17621
+ throw createTextError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\u6301 replaceAll");
17622
+ }
17623
+ if (input.startLine !== void 0 || input.endLine !== void 0 || input.startAnchor !== void 0 || input.endAnchor !== void 0) {
17624
+ throw createTextError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\u6301 startLine/endLine/startAnchor/endAnchor");
17625
+ }
17626
+ }
17627
+ function buildLineDiffPreview(filePath, encoding, beforeText, afterText) {
17628
+ const beforeLines = normalizeNewlines2(beforeText).split("\n");
17629
+ const afterLines = normalizeNewlines2(afterText).split("\n");
17630
+ const maxLines = Math.max(beforeLines.length, afterLines.length);
17631
+ const lines = [
17632
+ `--- ${path3.basename(filePath)} (${encoding})`,
17633
+ `+++ ${path3.basename(filePath)} (${encoding})`
17634
+ ];
17635
+ for (let index = 0; index < maxLines; index += 1) {
17636
+ const before = beforeLines[index];
17637
+ const after = afterLines[index];
17638
+ if (before !== void 0 && after !== void 0 && before === after) {
17639
+ lines.push(` ${before}`);
17640
+ continue;
17641
+ }
17642
+ if (before !== void 0) {
17643
+ lines.push(`-${before}`);
17644
+ }
17645
+ if (after !== void 0) {
17646
+ lines.push(`+${after}`);
17647
+ }
17648
+ }
17649
+ return lines.join("\n");
17650
+ }
17651
+ function buildInsertOutput(text, mode, anchor, content, occurrence, ifExists, newlineStyle) {
17652
+ const alignedContent = alignTextToNewlineStyle(content, newlineStyle);
17653
+ const located = findOccurrenceIndex2(text, anchor, occurrence);
17654
+ const insertionPoint = mode === "insertAfter" ? located.index + anchor.length : located.index;
17655
+ const alreadyExists = mode === "insertAfter" ? text.slice(insertionPoint, insertionPoint + alignedContent.length) === alignedContent : text.slice(Math.max(0, insertionPoint - alignedContent.length), insertionPoint) === alignedContent;
17656
+ if (alreadyExists) {
17657
+ if (ifExists === "error") {
17658
+ throw createTextError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
17659
+ }
17660
+ if (ifExists === "skip") {
17661
+ return {
17662
+ outputText: text,
17663
+ inserted: false,
17664
+ skipped: true,
17665
+ anchorMatches: located.total,
17666
+ occurrence,
17667
+ anchor,
17668
+ previewBefore: text.slice(Math.max(0, located.index - 80), Math.min(text.length, located.index + anchor.length + 80)),
17669
+ previewAfter: text.slice(Math.max(0, located.index - 80), Math.min(text.length, located.index + anchor.length + 80))
17670
+ };
17671
+ }
17672
+ }
17673
+ const outputText = `${text.slice(0, insertionPoint)}${alignedContent}${text.slice(insertionPoint)}`;
17674
+ const previewStart = Math.max(0, insertionPoint - 80);
17675
+ const previewBeforeEnd = Math.min(text.length, insertionPoint + 80);
17676
+ const previewAfterEnd = Math.min(outputText.length, insertionPoint + alignedContent.length + 80);
17677
+ return {
17678
+ outputText,
17679
+ inserted: true,
17680
+ skipped: false,
17681
+ anchorMatches: located.total,
17682
+ occurrence,
17683
+ anchor,
17684
+ previewBefore: text.slice(previewStart, previewBeforeEnd),
17685
+ previewAfter: outputText.slice(previewStart, previewAfterEnd)
17686
+ };
17687
+ }
17420
17688
  function detectTextEncodingFromBuffer(buffer, requestedEncoding = "auto") {
17421
17689
  if (requestedEncoding !== "auto") {
17422
17690
  assertTextEncodingSupported(requestedEncoding);
@@ -17607,6 +17875,7 @@ function resolveEditScope2(text, input) {
17607
17875
  };
17608
17876
  }
17609
17877
  function alignTextToNewlineStyle(text, newlineStyle) {
17878
+ assertStringArgument2(text, "text");
17610
17879
  const normalized = text.replace(/\r\n/g, "\n");
17611
17880
  if (newlineStyle === "crlf") {
17612
17881
  return normalized.replace(/\n/g, "\r\n");
@@ -17617,6 +17886,7 @@ function alignTextToNewlineStyle(text, newlineStyle) {
17617
17886
  return text;
17618
17887
  }
17619
17888
  function ensureLossless(input, encoding, hasBom = encoding === "utf8-bom") {
17889
+ assertStringArgument2(input, "content");
17620
17890
  const buffer = encodeText(input, encoding, hasBom);
17621
17891
  const roundTrip = decodeText(buffer, encoding);
17622
17892
  if (roundTrip !== input) {
@@ -17668,6 +17938,7 @@ async function readTextFile(input) {
17668
17938
  };
17669
17939
  }
17670
17940
  async function writeTextFile(input) {
17941
+ assertStringArgument2(input.content, "content");
17671
17942
  const requestedEncoding = input.encoding ?? "auto";
17672
17943
  assertTextEncodingSupported(requestedEncoding);
17673
17944
  const preserveEncoding = input.preserveEncoding ?? true;
@@ -17717,43 +17988,100 @@ async function writeTextFile(input) {
17717
17988
  };
17718
17989
  }
17719
17990
  async function replaceTextFileText(input) {
17991
+ const mode = resolveEditMode(input.mode);
17720
17992
  const normalizedInput = {
17721
17993
  ...input,
17722
17994
  startLine: normalizeOptionalPositiveInteger(input.startLine, "startLine"),
17723
17995
  endLine: normalizeOptionalPositiveInteger(input.endLine, "endLine")
17724
17996
  };
17725
- if (input.oldString.length === 0) {
17726
- throw createTextError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
17727
- }
17728
17997
  const loaded = await readWholeTextFile({
17729
17998
  filePath: input.filePath,
17730
17999
  encoding: input.encoding ?? "auto",
17731
18000
  allowExternal: input.allowExternal,
17732
18001
  context: input.context
17733
18002
  });
18003
+ if (mode === "insertAfter" || mode === "insertBefore") {
18004
+ assertInsertArguments2(normalizedInput);
18005
+ const occurrence = normalizeOptionalPositiveInteger(normalizedInput.occurrence, "occurrence") ?? 1;
18006
+ const insertResult = buildInsertOutput(
18007
+ loaded.content,
18008
+ mode,
18009
+ normalizedInput.anchor,
18010
+ normalizedInput.content,
18011
+ occurrence,
18012
+ resolveInsertIfExists(normalizedInput.ifExists),
18013
+ loaded.newlineStyle
18014
+ );
18015
+ if (insertResult.skipped) {
18016
+ return {
18017
+ 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,
18024
+ anchor: insertResult.anchor,
18025
+ occurrence: insertResult.occurrence,
18026
+ anchorMatches: insertResult.anchorMatches,
18027
+ inserted: false,
18028
+ skipped: true,
18029
+ bytesRead: loaded.bytesRead,
18030
+ bytesWritten: 0,
18031
+ newlineStyle: loaded.newlineStyle
18032
+ };
18033
+ }
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;
18036
+ ensureLossless(insertResult.outputText, targetEncoding2, targetHasBom2);
18037
+ const buffer2 = encodeText(insertResult.outputText, targetEncoding2, targetHasBom2);
18038
+ await fs3.writeFile(loaded.filePath, buffer2);
18039
+ return {
18040
+ mode,
18041
+ filePath: loaded.filePath,
18042
+ encoding: targetEncoding2,
18043
+ requestedEncoding: loaded.requestedEncoding,
18044
+ detectedEncoding: loaded.detectedEncoding,
18045
+ confidence: loaded.confidence,
18046
+ hasBom: targetHasBom2,
18047
+ anchor: insertResult.anchor,
18048
+ occurrence: insertResult.occurrence,
18049
+ anchorMatches: insertResult.anchorMatches,
18050
+ inserted: true,
18051
+ skipped: false,
18052
+ bytesRead: loaded.bytesRead,
18053
+ bytesWritten: buffer2.byteLength,
18054
+ newlineStyle: detectNewlineStyle(insertResult.outputText)
18055
+ };
18056
+ }
18057
+ assertReplaceArguments2(input);
18058
+ if (input.oldString.length === 0) {
18059
+ throw createTextError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
18060
+ }
17734
18061
  const scope = resolveEditScope2(loaded.content, normalizedInput);
17735
18062
  const replaceAll = normalizedInput.replaceAll ?? false;
17736
18063
  const preserveEncoding = normalizedInput.preserveEncoding ?? true;
17737
18064
  const requestedEncoding = normalizedInput.encoding ?? "auto";
17738
- const occurrencesBefore = countOccurrences(scope.selectedText, normalizedInput.oldString);
18065
+ const occurrencesBefore = countOccurrences(scope.selectedText, input.oldString);
17739
18066
  if (replaceAll) {
17740
18067
  if (occurrencesBefore === 0) {
17741
- throw createTextError("GBK_NO_MATCH", buildNoMatchMessage2(scope.selectedText, normalizedInput.oldString));
18068
+ throw createTextError("GBK_NO_MATCH", buildNoMatchMessage2(scope.selectedText, input.oldString));
17742
18069
  }
17743
18070
  } else if (occurrencesBefore === 0) {
17744
- throw createTextError("GBK_NO_MATCH", buildNoMatchMessage2(scope.selectedText, normalizedInput.oldString));
18071
+ throw createTextError("GBK_NO_MATCH", buildNoMatchMessage2(scope.selectedText, input.oldString));
17745
18072
  } else if (occurrencesBefore > 1) {
17746
- throw createTextError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${normalizedInput.oldString}`);
18073
+ throw createTextError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
17747
18074
  }
17748
- const alignedNewString = normalizedInput.preserveNewlineStyle === false ? normalizedInput.newString : alignTextToNewlineStyle(normalizedInput.newString, loaded.newlineStyle);
17749
- const replaced = replaceAll ? scope.selectedText.split(normalizedInput.oldString).join(alignedNewString) : scope.selectedText.replace(normalizedInput.oldString, alignedNewString);
18075
+ const alignedNewString = normalizedInput.preserveNewlineStyle === false ? input.newString : alignTextToNewlineStyle(input.newString, loaded.newlineStyle);
18076
+ const replaced = replaceAll ? scope.selectedText.split(input.oldString).join(alignedNewString) : scope.selectedText.replace(input.oldString, alignedNewString);
17750
18077
  const outputText = `${loaded.content.slice(0, scope.rangeStart)}${replaced}${loaded.content.slice(scope.rangeEnd)}`;
17751
- const targetEncoding = preserveEncoding || requestedEncoding === "auto" ? loaded.encoding : requestedEncoding;
18078
+ const targetEncoding = preserveEncoding || requestedEncoding === "auto" ? loaded.encoding : resolveExplicitTextEncoding(requestedEncoding, loaded.encoding);
17752
18079
  const targetHasBom = preserveEncoding ? loaded.hasBom : targetEncoding === "utf8-bom" || targetEncoding === "utf16le" || targetEncoding === "utf16be";
17753
18080
  ensureLossless(outputText, targetEncoding, targetHasBom);
17754
18081
  const buffer = encodeText(outputText, targetEncoding, targetHasBom);
17755
18082
  await fs3.writeFile(loaded.filePath, buffer);
17756
18083
  return {
18084
+ mode: "replace",
17757
18085
  filePath: loaded.filePath,
17758
18086
  encoding: targetEncoding,
17759
18087
  requestedEncoding: loaded.requestedEncoding,
@@ -17768,40 +18096,39 @@ async function replaceTextFileText(input) {
17768
18096
  };
17769
18097
  }
17770
18098
  async function createTextDiffPreview(input) {
18099
+ const mode = resolveEditMode(input.mode);
17771
18100
  const loaded = await readWholeTextFile({
17772
18101
  filePath: input.filePath,
17773
18102
  encoding: input.encoding ?? "auto",
17774
18103
  allowExternal: input.allowExternal,
17775
18104
  context: input.context
17776
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);
17777
18125
  const scope = resolveEditScope2(loaded.content, input);
17778
18126
  const alignedNewString = alignTextToNewlineStyle(input.newString, loaded.newlineStyle);
17779
18127
  const replaced = input.replaceAll ?? false ? scope.selectedText.split(input.oldString).join(alignedNewString) : scope.selectedText.replace(input.oldString, alignedNewString);
17780
- const beforeLines = normalizeNewlines2(scope.selectedText).split("\n");
17781
- const afterLines = normalizeNewlines2(replaced).split("\n");
17782
- const maxLines = Math.max(beforeLines.length, afterLines.length);
17783
- const lines = [
17784
- `--- ${path3.basename(loaded.filePath)} (${loaded.encoding})`,
17785
- `+++ ${path3.basename(loaded.filePath)} (${loaded.encoding})`
17786
- ];
17787
- for (let index = 0; index < maxLines; index += 1) {
17788
- const before = beforeLines[index];
17789
- const after = afterLines[index];
17790
- if (before !== void 0 && after !== void 0 && before === after) {
17791
- lines.push(` ${before}`);
17792
- continue;
17793
- }
17794
- if (before !== void 0) {
17795
- lines.push(`-${before}`);
17796
- }
17797
- if (after !== void 0) {
17798
- lines.push(`+${after}`);
17799
- }
17800
- }
17801
18128
  return {
17802
18129
  filePath: loaded.filePath,
17803
18130
  encoding: loaded.encoding,
17804
- preview: lines.join("\n")
18131
+ preview: buildLineDiffPreview(loaded.filePath, loaded.encoding, scope.selectedText, replaced)
17805
18132
  };
17806
18133
  }
17807
18134
 
@@ -17811,11 +18138,17 @@ var text_edit_default = tool({
17811
18138
 
17812
18139
  - Existing files keep their original encoding and BOM by default.
17813
18140
  - Existing newline style is preserved by default.
17814
- - Use startLine/endLine or anchors to narrow the edit scope.`,
18141
+ - Replace mode uses oldString/newString.
18142
+ - Insert mode uses mode=insertAfter or insertBefore with anchor/content.`,
17815
18143
  args: {
17816
18144
  filePath: tool.schema.string().describe("Target file path"),
17817
- oldString: tool.schema.string().describe("Exact text to replace"),
17818
- newString: tool.schema.string().describe("Replacement text"),
18145
+ mode: tool.schema.enum(["replace", "insertAfter", "insertBefore"]).optional().describe("Edit mode, default replace"),
18146
+ oldString: tool.schema.string().optional().describe("Exact text to replace when mode=replace"),
18147
+ newString: tool.schema.string().optional().describe("Replacement text when mode=replace"),
18148
+ anchor: tool.schema.string().optional().describe("Anchor text used by insertAfter/insertBefore"),
18149
+ content: tool.schema.string().optional().describe("Inserted content used by insertAfter/insertBefore"),
18150
+ occurrence: tool.schema.number().int().positive().optional().describe("1-based anchor occurrence for insert mode"),
18151
+ ifExists: tool.schema.enum(["allow", "skip", "error"]).optional().describe("What to do when inserted content already exists at target position"),
17819
18152
  replaceAll: tool.schema.boolean().optional().describe("Replace all occurrences"),
17820
18153
  startLine: tool.schema.number().int().positive().optional().describe("Start line for scoped edit"),
17821
18154
  endLine: tool.schema.number().int().positive().optional().describe("End line for scoped edit"),
@@ -17829,16 +18162,23 @@ var text_edit_default = tool({
17829
18162
  async execute(args, context) {
17830
18163
  const result = await replaceTextFileText({ ...args, context });
17831
18164
  const preview = await createTextDiffPreview({ ...args, context });
18165
+ 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}`;
17832
18166
  context.metadata({
17833
- title: `\u6587\u672C\u7F16\u8F91 ${result.encoding.toUpperCase()} x${result.replacements}`,
18167
+ title,
17834
18168
  metadata: {
17835
18169
  filePath: result.filePath,
17836
18170
  encoding: result.encoding,
17837
18171
  requestedEncoding: result.requestedEncoding,
17838
18172
  confidence: result.confidence,
17839
18173
  hasBom: result.hasBom,
17840
- replacements: result.replacements,
17841
- occurrencesBefore: result.occurrencesBefore,
18174
+ mode: result.mode,
18175
+ replacements: result.mode === "replace" ? result.replacements : void 0,
18176
+ occurrencesBefore: result.mode === "replace" ? result.occurrencesBefore : void 0,
18177
+ anchor: result.mode === "replace" ? void 0 : result.anchor,
18178
+ occurrence: result.mode === "replace" ? void 0 : result.occurrence,
18179
+ anchorMatches: result.mode === "replace" ? void 0 : result.anchorMatches,
18180
+ inserted: result.mode === "replace" ? void 0 : result.inserted,
18181
+ skipped: result.mode === "replace" ? void 0 : result.skipped,
17842
18182
  newlineStyle: result.newlineStyle,
17843
18183
  diffPreview: preview.preview
17844
18184
  }
@@ -5,6 +5,8 @@ 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
+ "- \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
+ "- \u53EA\u6709\u5728\u660E\u786E\u505A\u7CBE\u786E\u66FF\u6362\u65F6\uFF0C\u624D\u4F7F\u7528 oldString/newString\u3002",
8
10
  "- \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",
9
11
  "- \u5904\u7406\u660E\u786E\u7684 GBK/GB18030 \u6587\u4EF6\u65F6\uFF0C\u4E5F\u53EF\u7EE7\u7EED\u4F7F\u7528 gbk_read\u3001gbk_write\u3001gbk_edit\u3001gbk_search\u3002"
10
12
  ].join("\n");