opencode-gbk-tools 0.1.5 → 0.1.7

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.
@@ -16508,14 +16508,14 @@ function buildNoMatchMessage(content, oldString) {
16508
16508
  getNearestContext(content, oldString)
16509
16509
  ].join("\n");
16510
16510
  }
16511
- function tryLooseBlockReplace(content, oldString, newString) {
16512
- const normalizedContent = normalizeNewlines(content);
16513
- const contentLines = splitNormalizedLines(content);
16514
- const oldLines = trimTrailingEmptyLines(splitNormalizedLines(oldString));
16515
- const newLines = splitNormalizedLines(newString);
16516
- if (oldLines.length === 0) {
16517
- return null;
16518
- }
16511
+ function hasLineNumberPrefixes(lines) {
16512
+ const nonEmpty = lines.filter((l) => l.trim().length > 0);
16513
+ return nonEmpty.length > 0 && nonEmpty.every((l) => /^\d+: /.test(l));
16514
+ }
16515
+ function stripLineNumberPrefixes(lines) {
16516
+ return lines.map((l) => l.replace(/^\d+: /, ""));
16517
+ }
16518
+ function matchLooseBlock(contentLines, oldLines, newLines, newlineStyle, content) {
16519
16519
  for (let start = 0; start < contentLines.length; start += 1) {
16520
16520
  let contentIndex = start;
16521
16521
  let oldIndex = 0;
@@ -16545,12 +16545,32 @@ function tryLooseBlockReplace(content, oldString, newString) {
16545
16545
  ].join("\n");
16546
16546
  return {
16547
16547
  occurrencesBefore: 1,
16548
- content: detectNewlineStyle(content) === "crlf" ? replacedNormalized.replace(/\n/g, "\r\n") : replacedNormalized
16548
+ content: newlineStyle === "crlf" ? replacedNormalized.replace(/\n/g, "\r\n") : replacedNormalized
16549
16549
  };
16550
16550
  }
16551
16551
  }
16552
16552
  return null;
16553
16553
  }
16554
+ function tryLooseBlockReplace(content, oldString, newString) {
16555
+ const contentLines = splitNormalizedLines(content);
16556
+ const oldLines = trimTrailingEmptyLines(splitNormalizedLines(oldString));
16557
+ const newLines = splitNormalizedLines(newString);
16558
+ const newlineStyle = detectNewlineStyle(content);
16559
+ if (oldLines.length === 0) {
16560
+ return null;
16561
+ }
16562
+ const result = matchLooseBlock(contentLines, oldLines, newLines, newlineStyle, content);
16563
+ if (result !== null) {
16564
+ return result;
16565
+ }
16566
+ if (hasLineNumberPrefixes(oldLines)) {
16567
+ const strippedOldLines = trimTrailingEmptyLines(stripLineNumberPrefixes(oldLines));
16568
+ if (strippedOldLines.length > 0) {
16569
+ return matchLooseBlock(contentLines, strippedOldLines, newLines, newlineStyle, content);
16570
+ }
16571
+ }
16572
+ return null;
16573
+ }
16554
16574
  function countOccurrences(text, target) {
16555
16575
  if (target.length === 0) {
16556
16576
  throw createGbkError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
@@ -16811,6 +16831,22 @@ async function replaceLargeGbkFileText(input) {
16811
16831
  throw error45;
16812
16832
  }
16813
16833
  }
16834
+ async function loadGbkTextFile(input) {
16835
+ const resolved = await resolveReadableGbkFile(input);
16836
+ if (resolved.stat.size < STREAMING_FILE_SIZE_THRESHOLD_BYTES) {
16837
+ return await readWholeGbkTextFile(resolved);
16838
+ }
16839
+ const chunks = [];
16840
+ await visitDecodedTextChunks(resolved, (text) => {
16841
+ chunks.push(text);
16842
+ });
16843
+ return {
16844
+ filePath: resolved.filePath,
16845
+ encoding: resolved.encoding,
16846
+ content: chunks.join(""),
16847
+ bytesRead: resolved.stat.size
16848
+ };
16849
+ }
16814
16850
  async function readGbkFile(input) {
16815
16851
  const offset = normalizeOptionalPositiveInteger(input.offset, "offset") ?? 1;
16816
16852
  const limit = normalizeOptionalPositiveInteger(input.limit, "limit") ?? 2e3;
@@ -16911,6 +16947,39 @@ async function replaceGbkFileText(input) {
16911
16947
  bytesWritten: buffer.byteLength
16912
16948
  };
16913
16949
  }
16950
+ async function searchGbkFile(input) {
16951
+ if (input.pattern.length === 0) {
16952
+ throw createGbkError("GBK_INVALID_ARGUMENT", "pattern \u4E0D\u80FD\u4E3A\u7A7A");
16953
+ }
16954
+ const contextLines = Math.max(0, input.contextLines ?? 3);
16955
+ const resolved = await resolveReadableGbkFile(input);
16956
+ const loaded = await loadGbkTextFile({
16957
+ filePath: resolved.filePath,
16958
+ encoding: resolved.encoding,
16959
+ allowExternal: input.allowExternal,
16960
+ context: input.context
16961
+ });
16962
+ const lines = loaded.content.split(/\r?\n/);
16963
+ const totalLines = lines.length;
16964
+ const matches = [];
16965
+ for (let i = 0; i < lines.length; i += 1) {
16966
+ if (lines[i].includes(input.pattern)) {
16967
+ matches.push({
16968
+ lineNumber: i + 1,
16969
+ line: lines[i],
16970
+ contextBefore: lines.slice(Math.max(0, i - contextLines), i),
16971
+ contextAfter: lines.slice(i + 1, Math.min(totalLines, i + 1 + contextLines))
16972
+ });
16973
+ }
16974
+ }
16975
+ return {
16976
+ filePath: resolved.filePath,
16977
+ encoding: resolved.encoding,
16978
+ totalLines,
16979
+ matchCount: matches.length,
16980
+ matches
16981
+ };
16982
+ }
16914
16983
  async function writeGbkFile(input) {
16915
16984
  const encoding = input.encoding ?? "gbk";
16916
16985
  const createDirectories = input.createDirectories ?? true;
@@ -16976,12 +17045,22 @@ var gbk_edit_default = tool({
16976
17045
  Reads the FULL file content regardless of file size \u2014 not limited by gbk_read's line window.
16977
17046
  Safe to use on files with more than 2000 lines.
16978
17047
 
17048
+ CRITICAL \u2014 do NOT include line number prefixes in oldString or newString:
17049
+ gbk_read output looks like "3787: SENDMSG 0 content". The "3787: " is a navigation prefix, NOT file content.
17050
+ oldString must be the raw file content: "SENDMSG 0 content" (no line number prefix).
17051
+ Including line numbers in oldString will cause GBK_NO_MATCH even if the content exists.
17052
+
17053
+ Recommended workflow for large files (when gbk_read returned truncated=true):
17054
+ 1. gbk_search(pattern) \u2192 find exact lineNumber
17055
+ 2. gbk_read(offset=<lineNumber>, limit=20) \u2192 get the exact block (strip "N: " prefixes)
17056
+ 3. gbk_edit(oldString=<content without prefixes>, newString=<new content>)
17057
+
16979
17058
  For large files, use 'startLine'/'endLine' or 'startAnchor'/'endAnchor' to narrow the search scope
16980
17059
  and avoid false matches. Scoped edits also improve performance on very large files.`,
16981
17060
  args: {
16982
17061
  filePath: tool.schema.string().describe("Target file path"),
16983
- oldString: tool.schema.string().describe("Exact text to replace (must match file content, not gbk_read output with line numbers)"),
16984
- newString: tool.schema.string().describe("Replacement text"),
17062
+ oldString: tool.schema.string().describe("Exact text to replace \u2014 raw file content only, no 'N: ' line number prefixes from gbk_read output"),
17063
+ newString: tool.schema.string().describe("Replacement text \u2014 raw content only, no line number prefixes"),
16985
17064
  replaceAll: tool.schema.boolean().optional().describe("Replace all occurrences (default: false, requires unique match)"),
16986
17065
  startLine: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("Restrict edit scope to 1-based start line (inclusive)"),
16987
17066
  endLine: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("Restrict edit scope to 1-based end line (inclusive)"),
@@ -17003,14 +17082,23 @@ var gbk_read_default = tool({
17003
17082
  Returns up to 'limit' lines (default 2000) starting from 'offset'.
17004
17083
  When the file has more lines than the window, 'truncated' is true and 'totalLines' shows the full count.
17005
17084
 
17006
- IMPORTANT: If 'truncated' is true, the returned content is incomplete.
17007
- DO NOT use the returned content as 'oldString' for gbk_edit on a truncated file.
17008
- To edit content beyond the visible window, use gbk_edit with 'startLine'/'endLine' to target the exact range,
17009
- or read the specific range first with 'offset' set to the desired line number.`,
17085
+ IMPORTANT \u2014 line number format: each output line is prefixed with "N: " (e.g. "3787: content").
17086
+ These prefixes are for navigation only. Strip them before using any line as 'oldString' in gbk_edit.
17087
+
17088
+ IMPORTANT \u2014 do NOT use large limits to read the whole file:
17089
+ - Setting limit=40000 or similar to "read everything" is WRONG. It produces an enormous response
17090
+ that is unreliable to process and may be truncated by the protocol.
17091
+ - To find content in a large file, use gbk_search instead.
17092
+ - To read a specific section, set offset=<lineNumber> and limit=<small number like 10-30>.
17093
+
17094
+ Workflow when truncated=true:
17095
+ 1. gbk_search(pattern) \u2192 get exact lineNumber
17096
+ 2. gbk_read(offset=<lineNumber>, limit=20) \u2192 get the exact block
17097
+ 3. gbk_edit(oldString=<exact content without line prefixes>) \u2192 edit reliably`,
17010
17098
  args: {
17011
17099
  filePath: tool.schema.string().describe("Target file path"),
17012
17100
  offset: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("1-based start line (default: 1)"),
17013
- limit: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("Number of lines to read (default: 2000). Use -1 to apply the default."),
17101
+ limit: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("Number of lines to read (default: 2000, max recommended: 200). Use -1 to apply the default."),
17014
17102
  tail: tool.schema.boolean().optional().describe("Read last N lines instead of offset-based window"),
17015
17103
  encoding: tool.schema.enum(["gbk", "gb18030"]).optional().describe("Text encoding (default: gbk)"),
17016
17104
  allowExternal: tool.schema.boolean().optional().describe("Allow paths outside workspace root")
@@ -17021,6 +17109,33 @@ or read the specific range first with 'offset' set to the desired line number.`,
17021
17109
  }
17022
17110
  });
17023
17111
 
17112
+ // src/tools/gbk_search.ts
17113
+ var gbk_search_default = tool({
17114
+ description: `Search for exact text in GBK/GB18030 files. Returns matching line numbers and surrounding context.
17115
+
17116
+ Use this BEFORE gbk_edit when:
17117
+ - gbk_read returned truncated: true (file has more than the visible window of lines)
17118
+ - You need to verify the exact content and line number before constructing oldString for gbk_edit
17119
+ - You are unsure whether a string exists in the file
17120
+
17121
+ Workflow for large files:
17122
+ 1. gbk_read \u2192 if truncated: true, use gbk_search to locate the target
17123
+ 2. gbk_search \u2192 get exact lineNumber and contextAfter/contextBefore
17124
+ 3. gbk_read(offset=<lineNumber>, limit=<N>) \u2192 get the exact block of text
17125
+ 4. gbk_edit with the exact content as oldString`,
17126
+ args: {
17127
+ filePath: tool.schema.string().describe("Target file path"),
17128
+ pattern: tool.schema.string().describe("Exact string to search for (case-sensitive, plain text, not regex)"),
17129
+ contextLines: tool.schema.number().int().nonnegative().optional().describe("Number of context lines to return before and after each match (default: 3)"),
17130
+ encoding: tool.schema.enum(["gbk", "gb18030"]).optional().describe("Text encoding (default: gbk)"),
17131
+ allowExternal: tool.schema.boolean().optional().describe("Allow paths outside workspace root")
17132
+ },
17133
+ async execute(args, context) {
17134
+ const result = await searchGbkFile({ ...args, context });
17135
+ return JSON.stringify(result, null, 2);
17136
+ }
17137
+ });
17138
+
17024
17139
  // src/tools/gbk_write.ts
17025
17140
  var gbk_write_default = tool({
17026
17141
  description: "Write GBK encoded text files",
@@ -17046,7 +17161,8 @@ var pluginModule = {
17046
17161
  tool: {
17047
17162
  gbk_read: gbk_read_default,
17048
17163
  gbk_write: gbk_write_default,
17049
- gbk_edit: gbk_edit_default
17164
+ gbk_edit: gbk_edit_default,
17165
+ gbk_search: gbk_search_default
17050
17166
  }
17051
17167
  };
17052
17168
  }
@@ -1,18 +1,24 @@
1
1
  {
2
2
  "manifestVersion": 1,
3
3
  "packageName": "opencode-gbk-tools",
4
- "packageVersion": "0.1.5",
4
+ "packageVersion": "0.1.7",
5
5
  "artifacts": [
6
6
  {
7
7
  "relativePath": "tools/gbk_edit.js",
8
8
  "kind": "tool",
9
- "expectedHash": "3d0eb2fab5e3eca869923c2a3cc00e58ed429a4be4dbed22d13e5a5343bd5acc",
9
+ "expectedHash": "783a97cc6f7d1378f0b841ce0c6809e93db104aa192a64f72878ce6d432c997e",
10
10
  "hashAlgorithm": "sha256"
11
11
  },
12
12
  {
13
13
  "relativePath": "tools/gbk_read.js",
14
14
  "kind": "tool",
15
- "expectedHash": "799c53835bfe7237c5bf1e4858a67d20fffe00f90958ef48b72877cb95c1c07f",
15
+ "expectedHash": "90c0c3ca519000bcc1fc65f84615a5a280866a45104d08678f3ed92dbfda9f45",
16
+ "hashAlgorithm": "sha256"
17
+ },
18
+ {
19
+ "relativePath": "tools/gbk_search.js",
20
+ "kind": "tool",
21
+ "expectedHash": "dc779c10156dacf1e88195d68445dcd25da0f279b30887559da2a8d5c32b1a02",
16
22
  "hashAlgorithm": "sha256"
17
23
  },
18
24
  {
@@ -24,7 +30,7 @@
24
30
  {
25
31
  "relativePath": "agents/gbk-engine.md",
26
32
  "kind": "agent",
27
- "expectedHash": "755b6278033400bdc08bb0d7dc491eabe5aa3a0157c6bef4304c7a5f2038c0dd",
33
+ "expectedHash": "3ff7de130d4fd3c318627efd56e2be08ec6ea5b9d37b3d6bf4a7122a9e0997b1",
28
34
  "hashAlgorithm": "sha256"
29
35
  }
30
36
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gbk-tools",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "GBK/GB18030 custom tools and agent for OpenCode",
5
5
  "type": "module",
6
6
  "license": "MIT",