fix-vietnamese-claude-code 1.0.2 → 1.0.3

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
@@ -1,75 +1,46 @@
1
1
  # Claude Code Vietnamese IME Fix
2
2
 
3
- Fix lỗi gõ tiếng Việt trong Claude Code CLI với các bộ gõ như OpenKey, EVKey, PHTV, Unikey... Hỗ trợ đa nền tảng (macOS Windows).
3
+ Fix lỗi gõ tiếng Việt trong Claude Code CLI cho các bộ gõ (OpenKey, EVKey, PHTV, Unikey...). Hỗ trợ cả phiên bản **npm** và **binary** (macOS, Windows, Linux).
4
4
 
5
- **Phiên bản đã test:** Claude Code v2.0.64 → v2.1.12
6
-
7
- ## Vấn đề
8
-
9
- Khi gõ tiếng Việt trong Claude Code CLI, các bộ gõ sử dụng kỹ thuật "backspace rồi thay thế" để chuyển đổi ký tự (ví dụ: `a` + `s` → `á`). Claude Code xử lý phần backspace (ký tự DEL `\x7f`) nhưng không đưa ký tự thay thế vào đúng vị trí, dẫn đến:
10
-
11
- - Ký tự bị "nuốt" hoặc mất khi gõ.
12
- - Văn bản hiển thị không đúng với những gì đã gõ.
13
- - Gây khó khăn khi nhập liệu trực tiếp trong terminal.
14
-
15
- Script này patch tệp `cli.js` của Claude Code để xử lý đúng các ký tự tiếng Việt sau khi nhận tín hiệu xóa từ bộ gõ.
5
+ **Phiên bản đã test:**
6
+ - npm: v2.1.39
7
+ - binary: v2.1.39
8
+ (Chi tiết tại [CHANGELOG.md](./CHANGELOG.md))
16
9
 
17
10
  ## Cài đặt & Sử dụng
18
11
 
19
- > [!IMPORTANT]
20
- > **Yêu cầu:** Chỉ hỗ trợ phiên bản cài đặt qua **npm**. Nếu bạn cài Claude Code qua các đường dẫn khác (MSI installer, Homebrew binary), vui lòng gỡ cài đặt và cài lại qua npm:
21
- > ```bash
22
- > npm install -g @anthropic-ai/claude-code
23
- > ```
24
-
25
- ### 2. Chạy patch
26
-
27
- Bạn có thể chạy trực tiếp bằng **npx** (không cần tải file):
12
+ lệnh sau trong terminal để áp dụng bản vá:
28
13
 
29
14
  ```bash
30
15
  npx fix-vietnamese-claude-code
31
16
  ```
32
17
 
33
- Hoặc nếu bạn đã tải tệp `patch-cli-claude-code.js` về máy:
34
-
35
- ```bash
36
- node patch-cli-claude-code.js
37
- ```
38
-
39
- Script sẽ tự động tìm kiếm đường dẫn đến tệp `cli.js` của Claude Code trên hệ thống của bạn và áp dụng bản vá.
18
+ Lệnh trên sẽ tự động tìm và vá tệp `cli.js` (npm) hoặc file binary của Claude Code trên hệ thống của bạn.
40
19
 
41
20
  ### Tùy chọn nâng cao
42
21
 
43
- Nếu script không tự động tìm thấy đường dẫn, bạn có thể chỉ định thủ công:
22
+ Nếu cài đặt đường dẫn không mặc định, bạn có thể chỉ định thủ công:
44
23
 
45
24
  ```bash
46
- node patch-cli-claude-code.js -f "/đường/dẫn/đến/@anthropic-ai/claude-code/cli.js"
47
- ```
25
+ # Đối với bản npm cli.js
26
+ npx fix-vietnamese-claude-code -f "/đường/dẫn/đến/cli.js"
48
27
 
49
- Xem hướng dẫn chi tiết:
50
- ```bash
51
- node patch-cli-claude-code.js --help
28
+ # Đối với bản binary
29
+ npx fix-vietnamese-claude-code -f "/đường/dẫn/đến/claude"
52
30
  ```
53
31
 
54
32
  ## Lưu ý
33
+ - **Cập nhật:** Bạn cần chạy lại lệnh patch mỗi khi Claude Code cập nhật phiên bản mới.
34
+ - **Môi trường:** Đã kiểm tra tốt trên Windows (CMD/PowerShell), macOS và Linux.
55
35
 
56
- - **Cập nhật:** Mỗi khi Claude Code được cập nhật phiên bản mới, bạn cần chạy lại script patch này vì tệp `cli.js` sẽ bị ghi đè.
57
- - **Môi trường:** Đã kiểm tra hoạt động tốt trên Windows (CMD/PowerShell) và macOS.
58
-
59
- ## Phát triển (Dành cho Developer)
60
-
61
- Dự án sử dụng **Vitest** để kiểm tra tính đúng đắn của bản vá trên nhiều phiên bản Claude Code khác nhau.
36
+ ## Phát triển
37
+ Dự án sử dụng **Vitest** để kiểm tra tính đúng đắn trên nhiều phiên bản.
62
38
 
63
39
  ```bash
64
- # Cài đặt dependencies
65
40
  npm install
66
-
67
- # Chạy test
68
41
  npm test
69
42
  ```
70
43
 
71
- Script test sẽ tự động tải các phiên bản thực tế của Claude Code từ npm (từ `2.0.64` trở đi) để đảm bảo regex luôn khớp.
72
-
73
44
  ## Credits
74
45
 
75
46
  Dự án tham khảo và cải tiến từ:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fix-vietnamese-claude-code",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Fix Vietnamese IME compatibility issues in Claude Code.",
5
5
  "bin": {
6
6
  "fix-vietnamese-claude-code": "patch-cli-claude-code.js"
@@ -5,37 +5,7 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
7
 
8
- /**
9
- * Original method stolen from this
10
- * https://github.com/manhit96/claude-code-vietnamese-fix
11
- * This patch fixes Vietnamese input method editors (IMEs) compatibility issues in Claude.
12
- * By modifying `cli.js` code from this:
13
- * ```js
14
- * if (!S.equals(CA)) {
15
- * if (S.text !== CA.text) Q(CA.text);
16
- * T(CA.offset)
17
- * }
18
- * ct1(), lt1();
19
- * return
20
- * ```
21
- * To this:
22
- * ```js
23
- * /* Vietnamese IME fix * /
24
- * let _vn = n.replace(/\x7f/g, "");
25
- * if (_vn.length > 0) {
26
- * for (const _c of _vn) CA = CA.insert(_c);
27
- * if (!S.equals(CA)) {
28
- * if (S.text !== CA.text) Q(CA.text);
29
- * T(CA.offset)
30
- * }
31
- * }
32
- * Oe1(), Me1();
33
- * return
34
- * ```
35
- * Note: I have no idea how this works, just ported python logic to JS with make it better performance.
36
- */
37
-
38
- const DORK = '/* Vietnamese IME fix */';
8
+ const DORK = '/* _0x0a0d_ime_fix_ */';
39
9
 
40
10
  function usage() {
41
11
  console.log(`
@@ -43,112 +13,261 @@ Usage:
43
13
  fix-claude-code-vn [options]
44
14
 
45
15
  Options:
46
- -f, --file <path> Path to Claude's cli.js file
16
+ -f, --file <_path_> Path to cli.js or claude file
17
+ -d, --dry-run Test without overwriting the file
18
+ -o, --output <path> Write patched content to a new file
47
19
  -h, --help Show this help message
48
20
 
49
21
  Description:
50
- This script patches Claude Code's cli.js to fix Vietnamese IME issues.
22
+ This script patches Claude Code CLI tool to fix Vietnamese IME issues.
51
23
  If no file is specified, it will try to find it automatically.
52
24
  `);
53
25
  }
54
26
 
55
27
  function findClaudePath() {
56
- // 1. Try which/where
57
- try {
58
- const cmd = os.platform() === 'win32' ? 'where claude' : 'which claude';
59
- const binPath = execSync(cmd).toString().split('\n')[0].trim();
60
- if (binPath) {
61
- // On Unix, it might be a symlink, resolve it
62
- let realPath = binPath;
63
- if (os.platform() !== 'win32') {
28
+ const isWin = os.platform() === "win32";
29
+
30
+ const run = (cmd) => {
31
+ try {
32
+ return execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] })
33
+ .toString()
34
+ .split(/\r?\n/)[0]
35
+ .trim();
36
+ } catch {
37
+ return "";
38
+ }
39
+ };
40
+
41
+ const exists = (p) => p && fs.existsSync(p);
42
+
43
+ // 1) which / where / bun which
44
+ for (const cmd of [
45
+ isWin ? "where claude" : "which claude",
46
+ "bun which claude",
47
+ ]) {
48
+ const p = run(cmd);
49
+ if (exists(p)) {
50
+ if (!isWin) {
64
51
  try {
65
- realPath = execSync(`realpath "${binPath}"`).toString().trim();
66
- } catch (e) { /* ignore */ }
52
+ return execSync(`realpath "${ p }"`).toString().trim();
53
+ } catch {}
67
54
  }
55
+ return p;
56
+ }
57
+ }
68
58
 
69
- if (realPath.endsWith('.js')) return realPath;
70
-
71
- // If it's a binary/shell script, try to find the node_modules
72
- const dir = path.dirname(realPath);
73
- const npmPath = path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
74
- if (fs.existsSync(npmPath)) return npmPath;
59
+ // 2) Bun global paths
60
+ const bunInstall =
61
+ process.env.BUN_INSTALL ||
62
+ (isWin
63
+ ? path.join(process.env.USERPROFILE || "", ".bun")
64
+ : path.join(process.env.HOME || "", ".bun"));
65
+
66
+ const bunPaths = [
67
+ path.join(bunInstall, "bin", isWin ? "claude.exe" : "claude"),
68
+ path.join(bunInstall, "bin", isWin ? "claude.cmd" : "claude"),
69
+ path.join(
70
+ bunInstall,
71
+ "install",
72
+ "global",
73
+ "node_modules",
74
+ "@anthropic-ai",
75
+ "claude-code",
76
+ "cli.js"
77
+ ),
78
+ ];
79
+
80
+ for (const p of bunPaths) {
81
+ if (exists(p)) {
82
+ return p;
75
83
  }
76
- } catch (e) { /* ignore */ }
84
+ }
77
85
 
78
- // 2. Try npm root -g
86
+ // 3) npm global
79
87
  try {
80
- const npmRoot = execSync('npm root -g').toString().trim();
81
- const cliPath = path.join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js');
82
- if (fs.existsSync(cliPath)) return cliPath;
83
- } catch (e) { /* ignore */ }
84
-
85
- // 3. Common paths for Windows
86
- if (os.platform() === 'win32') {
87
- const commonPaths = [
88
- path.join(process.env.APPDATA || '', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
89
- path.join(process.env.LOCALAPPDATA || '', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js')
88
+ const npmRoot = execSync("npm root -g").toString().trim();
89
+
90
+ const cliPath = path.join(
91
+ npmRoot,
92
+ "@anthropic-ai",
93
+ "claude-code",
94
+ "cli.js"
95
+ );
96
+ if (exists(cliPath)) {
97
+
98
+ return cliPath;
99
+ }
100
+ } catch (e) {
101
+
102
+ }
103
+
104
+ // 4) Windows fallbacks
105
+ if (isWin) {
106
+
107
+ const paths = [
108
+ path.join(
109
+ process.env.APPDATA || "",
110
+ "npm",
111
+ "node_modules",
112
+ "@anthropic-ai",
113
+ "claude-code",
114
+ "cli.js"
115
+ ),
116
+ path.join(
117
+ process.env.LOCALAPPDATA || "",
118
+ "npm",
119
+ "node_modules",
120
+ "@anthropic-ai",
121
+ "claude-code",
122
+ "cli.js"
123
+ ),
90
124
  ];
91
-
92
- // nvm-windows
125
+
93
126
  if (process.env.NVM_HOME) {
94
127
  try {
95
- const dirs = fs.readdirSync(process.env.NVM_HOME);
96
- for (const d of dirs) {
97
- const p = path.join(process.env.NVM_HOME, d, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
98
- if (fs.existsSync(p)) commonPaths.push(p);
128
+ for (const d of fs.readdirSync(process.env.NVM_HOME)) {
129
+ paths.push(
130
+ path.join(
131
+ process.env.NVM_HOME,
132
+ d,
133
+ "node_modules",
134
+ "@anthropic-ai",
135
+ "claude-code",
136
+ "cli.js"
137
+ )
138
+ );
99
139
  }
100
- } catch (e) { /* ignore */ }
140
+ } catch (e) {
141
+
142
+ }
101
143
  }
102
144
 
103
- for (const p of commonPaths) {
104
- if (fs.existsSync(p)) return p;
145
+ for (const p of paths) {
146
+ if (exists(p)) {
147
+
148
+ return p;
149
+ }
105
150
  }
106
151
  }
107
152
 
153
+
108
154
  return null;
109
155
  }
110
156
 
111
- function patchContent(fileContent) {
157
+ // stolen fixed solution from manhit96/claude-code-vietnamese-fix
158
+ function patchContentJs(fileContent) {
112
159
  if (fileContent.includes(DORK)) {
113
- return { success: true, alreadyPatched: true, content: fileContent };
160
+ return { success: true, alreadyPatched: true };
114
161
  }
115
162
 
116
163
  // Pattern matching:
117
164
  // match this: l.match(/\x7f/g)...if(!S.equals(CA)){if(S.text!==CA.text)Q(CA.text);T(CA.offset)}ct1(),lt1();return
118
165
  // We use a regex that captures variable and function names dynamically.
119
- const re = /((?<var0>[\w$]+)\.match\(\/\\x7f\/g\).*?)if\(!(?<var1>[\w$]+)\.equals\((?<var2>[\w$]+)\)\){if\(\k<var1>\.text!==\k<var2>\.text\)(?<func1>[\w$]+)\(\k<var2>\.text\);(?<func2>[\w$]+)\(\k<var2>\.offset\)}(?<remain>(?:[\w$]+\(\),?\s*)*;?\s*return)/;
120
-
121
- const newContent = fileContent.replace(re, (match, m0, var0, var1, var2, func1, func2, remain) => {
166
+ const re = /(?<m0>(?<var0>[\w$]+)\.match\(\/\\x7f\/g\).*?)(?<m1>if\(!(?<var1>[\w$]+)\.equals\((?<var2>[\w$]+)\)\){if\(\k<var1>\.text!==\k<var2>\.text\)(?<func1>[\w$]+)\(\k<var2>\.text\);(?<func2>[\w$]+)\(\k<var2>\.offset\)})(?<m2>(?:[\w$]+\(\),?\s*)*;?\s*return)/g;
167
+
168
+ const newContent = fileContent.replace(re, (...args) => {
169
+ const { m0, m1, var0, var2, m2 } = args[args.length - 1];
122
170
  return `
123
171
  ${DORK}
124
172
  ${m0}
125
173
  let _vn = ${var0}.replace(/\\x7f/g, "");
126
174
  if (_vn.length > 0) {
127
175
  for (const _c of _vn) ${var2} = ${var2}.insert(_c);
128
- if (!${var1}.equals(${var2})) {
129
- if (${var1}.text !== ${var2}.text) ${func1}(${var2}.text);
130
- ${func2}(${var2}.offset);
131
- }
176
+ ${m1}
132
177
  }
133
- ${remain}
178
+ ${m2}
134
179
  `;
135
180
  });
136
181
 
137
- if (newContent === fileContent) {
138
- return { success: false, content: fileContent };
182
+ if (newContent.length === fileContent.length) {
183
+ return { success: false, message: 'Patch failed: no match found' };
139
184
  }
140
185
 
141
186
  return { success: true, alreadyPatched: false, content: newContent };
142
187
  }
143
188
 
189
+ function patchContentBinary(binaryContent) {
190
+ if (binaryContent.includes(DORK)) {
191
+ return { success: true, alreadyPatched: true };
192
+ }
193
+
194
+ const re = /(?<m0>(?<var0>[\w$]+)\.match\(\/\\x7f\/g\).*?)(?<m1>if\(!(?<var1>[\w$]+)\.equals\((?<var2>[\w$]+)\)\){if\(\k<var1>\.text!==\k<var2>\.text\)(?<func1>[\w$]+)\(\k<var2>\.text\);(?<func2>[\w$]+)\(\k<var2>\.offset\)})(?<m2>(?:[\w$]+\(\),?\s*)*;?\s*return)/g;
195
+
196
+ const matches = [];
197
+ binaryContent = binaryContent.replace(re, (...args) => {
198
+ const groups = args[args.length - 1];
199
+ const offset = args[args.length - 3];
200
+ const { m0, m1, var0, var2, m2 } = groups;
201
+
202
+ const patchedContent = `${DORK}
203
+ ${m0}
204
+ let _vn = ${var0}.replace(/\\x7f/g, "");
205
+ if (_vn.length > 0) {
206
+ for (const _c of _vn) ${var2} = ${var2}.insert(_c);
207
+ ${m1}
208
+ }
209
+ ${m2}`.replace(/^\s+/gm, '');
210
+ matches.push({ diff: patchedContent.length - args[0].length, index: offset });
211
+ return patchedContent;
212
+ });
213
+
214
+ if (matches.length === 0) {
215
+ return { success: false, message: 'Patch failed: no match found' };
216
+ }
217
+
218
+ // now from index, we must look back for `\x00// @bun `
219
+ const pragma = `// @bun `
220
+ const pragmaLength = pragma.length
221
+ for (let i = 0; i < matches.length; i++) {
222
+ for (let j = matches[i].index - 1; j >= (i === 0 ? 0 : matches[i - 1].index); j--) {
223
+ if (binaryContent[j] === '\x00') {
224
+ // pragma test
225
+ if (binaryContent.slice(j + 1, j + 1 + pragmaLength).toString() === pragma) {
226
+ // look next to find first `\n//`
227
+ let found = false;
228
+ for (let k = j + 1 + pragmaLength; k < matches[i].index; k++) {
229
+ if (binaryContent[k] === '\n' && binaryContent[k + 1] === '/' && binaryContent[k + 2] === '/') {
230
+ // remove from binaryContent after `//` exactly `diff` bytes
231
+ const diff = matches[i].diff;
232
+ const sliceStart = k + 3;
233
+ binaryContent = binaryContent.slice(0, sliceStart) + binaryContent.slice(sliceStart + diff);
234
+ found = true;
235
+ break;
236
+ }
237
+ }
238
+ if (found) {
239
+ matches[i].found = true;
240
+ break;
241
+ };
242
+ }
243
+ }
244
+ }
245
+ if (!matches[i].found) {
246
+ break;
247
+ }
248
+ }
249
+
250
+ if (matches.every(m => !m.found)) {
251
+ return { success: false, message: 'Patch failed: pragma' };
252
+ }
253
+
254
+ return { success: true, alreadyPatched: false, content: binaryContent };
255
+ }
256
+
144
257
  // Main execution
145
258
  if (require.main === module) {
146
259
  let targetPath = null;
260
+ let isDryRun = false;
261
+ let outputPath = null;
147
262
  const args = process.argv.slice(2);
148
263
 
149
264
  for (let i = 0; i < args.length; i++) {
150
265
  if (args[i] === '-f' || args[i] === '--file') {
151
266
  targetPath = args[++i];
267
+ } else if (args[i] === '-d' || args[i] === '--dry-run') {
268
+ isDryRun = true;
269
+ } else if (args[i] === '-o' || args[i] === '--output') {
270
+ outputPath = args[++i];
152
271
  } else if (args[i] === '-h' || args[i] === '--help') {
153
272
  usage();
154
273
  process.exit(0);
@@ -160,7 +279,7 @@ if (require.main === module) {
160
279
  }
161
280
 
162
281
  if (!targetPath || !fs.existsSync(targetPath)) {
163
- console.error('Error: Could not find Claude Code cli.js.');
282
+ console.error('Error: Could not find Claude Code CLI.');
164
283
  if (targetPath) console.error(`Path tried: ${targetPath}`);
165
284
  usage();
166
285
  process.exit(1);
@@ -168,13 +287,9 @@ if (require.main === module) {
168
287
 
169
288
  console.log(`Target: ${targetPath}`);
170
289
 
171
- if (!targetPath.endsWith('.js')) {
172
- console.error('Error: Target file must be a .js file.');
173
- process.exit(1);
174
- }
175
-
176
- const content = fs.readFileSync(targetPath, 'utf-8');
177
- const result = patchContent(content);
290
+ const result = targetPath.endsWith('.js')
291
+ ? patchContentJs(fs.readFileSync(targetPath, 'latin1'))
292
+ : patchContentBinary(fs.readFileSync(targetPath, 'latin1'));
178
293
 
179
294
  if (result.alreadyPatched) {
180
295
  console.log('Claude is already patched for Vietnamese IME.');
@@ -182,13 +297,23 @@ if (require.main === module) {
182
297
  }
183
298
 
184
299
  if (!result.success) {
185
- console.error('Error: Failed to patch Claude. The code structure might have changed.');
300
+ console.error(result.message);
186
301
  process.exit(1);
187
302
  }
188
303
 
189
- fs.writeFileSync(targetPath, result.content, 'utf-8');
190
- console.log('Success: Claude has been patched for Vietnamese IME.');
304
+ if (isDryRun) {
305
+ console.log('Dry run: patch applied successfully (not saved).');
306
+ process.exit(0);
307
+ }
308
+
309
+ const finalPath = outputPath || targetPath;
310
+ fs.writeFileSync(finalPath, result.content, 'latin1');
311
+ console.log(`Success: Claude has been patched at ${finalPath}`);
191
312
  }
192
313
 
193
314
  // Export for testing
194
- module.exports = { patchContent };
315
+ module.exports = {
316
+ DORK,
317
+ patchContentJs,
318
+ patchContentBinary,
319
+ };