fix-vietnamese-claude-code 1.0.0
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 +75 -0
- package/package.json +29 -0
- package/patch-cli-claude-code.js +194 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Claude Code Vietnamese IME Fix
|
|
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 và Windows).
|
|
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õ.
|
|
16
|
+
|
|
17
|
+
## Cài đặt & Sử dụng
|
|
18
|
+
|
|
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
|
+
### 1. Tải script patch
|
|
26
|
+
Tải tệp `patch-cli-claude-code.js` về máy của bạn.
|
|
27
|
+
|
|
28
|
+
### 2. Chạy patch
|
|
29
|
+
Mở terminal (CMD/PowerShell trên Windows hoặc Terminal trên macOS) và chạy lệnh:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node patch-cli-claude-code.js
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
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á.
|
|
36
|
+
|
|
37
|
+
### Tùy chọn nâng cao
|
|
38
|
+
|
|
39
|
+
Nếu script không tự động tìm thấy đường dẫn, bạn có thể chỉ định thủ công:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
node patch-cli-claude-code.js -f "/đường/dẫn/đến/@anthropic-ai/claude-code/cli.js"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Xem hướng dẫn chi tiết:
|
|
46
|
+
```bash
|
|
47
|
+
node patch-cli-claude-code.js --help
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Lưu ý
|
|
51
|
+
|
|
52
|
+
- **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 đè.
|
|
53
|
+
- **Môi trường:** Đã kiểm tra và hoạt động tốt trên Windows (CMD/PowerShell) và macOS.
|
|
54
|
+
|
|
55
|
+
## Phát triển (Dành cho Developer)
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Cài đặt dependencies
|
|
61
|
+
npm install
|
|
62
|
+
|
|
63
|
+
# Chạy test
|
|
64
|
+
npm test
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
## Credits
|
|
70
|
+
|
|
71
|
+
Dự án tham khảo và cải tiến từ:
|
|
72
|
+
- [claude-code-vietnamese-fix](https://github.com/manhit96/claude-code-vietnamese-fix)
|
|
73
|
+
- [PHTV](https://github.com/phamhungtien/PHTV)
|
|
74
|
+
|
|
75
|
+
Trân trọng cảm ơn các tác giả đã đi trước!
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fix-vietnamese-claude-code",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fix Vietnamese IME compatibility issues in Claude Code.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"fix-vietnamese-claude-code": "./patch-cli-claude-code.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"claude",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"vietnamese",
|
|
12
|
+
"fix"
|
|
13
|
+
],
|
|
14
|
+
"author": "0x0a0d",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/0x0a0d/fix-vietnamese-claude-code"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"vitest": "^4.0.17"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "vitest run"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"patch-cli-claude-code.js"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
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 */';
|
|
39
|
+
|
|
40
|
+
function usage() {
|
|
41
|
+
console.log(`
|
|
42
|
+
Usage:
|
|
43
|
+
fix-claude-code-vn [options]
|
|
44
|
+
|
|
45
|
+
Options:
|
|
46
|
+
-f, --file <path> Path to Claude's cli.js file
|
|
47
|
+
-h, --help Show this help message
|
|
48
|
+
|
|
49
|
+
Description:
|
|
50
|
+
This script patches Claude Code's cli.js to fix Vietnamese IME issues.
|
|
51
|
+
If no file is specified, it will try to find it automatically.
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
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') {
|
|
64
|
+
try {
|
|
65
|
+
realPath = execSync(`realpath "${binPath}"`).toString().trim();
|
|
66
|
+
} catch (e) { /* ignore */ }
|
|
67
|
+
}
|
|
68
|
+
|
|
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;
|
|
75
|
+
}
|
|
76
|
+
} catch (e) { /* ignore */ }
|
|
77
|
+
|
|
78
|
+
// 2. Try npm root -g
|
|
79
|
+
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')
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// nvm-windows
|
|
93
|
+
if (process.env.NVM_HOME) {
|
|
94
|
+
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);
|
|
99
|
+
}
|
|
100
|
+
} catch (e) { /* ignore */ }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const p of commonPaths) {
|
|
104
|
+
if (fs.existsSync(p)) return p;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function patchContent(fileContent) {
|
|
112
|
+
if (fileContent.includes(DORK)) {
|
|
113
|
+
return { success: true, alreadyPatched: true, content: fileContent };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Pattern matching:
|
|
117
|
+
// 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
|
+
// 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) => {
|
|
122
|
+
return `
|
|
123
|
+
${DORK}
|
|
124
|
+
${m0}
|
|
125
|
+
let _vn = ${var0}.replace(/\\x7f/g, "");
|
|
126
|
+
if (_vn.length > 0) {
|
|
127
|
+
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
|
+
}
|
|
132
|
+
}
|
|
133
|
+
${remain}
|
|
134
|
+
`;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (newContent === fileContent) {
|
|
138
|
+
return { success: false, content: fileContent };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { success: true, alreadyPatched: false, content: newContent };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Main execution
|
|
145
|
+
if (require.main === module) {
|
|
146
|
+
let targetPath = null;
|
|
147
|
+
const args = process.argv.slice(2);
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < args.length; i++) {
|
|
150
|
+
if (args[i] === '-f' || args[i] === '--file') {
|
|
151
|
+
targetPath = args[++i];
|
|
152
|
+
} else if (args[i] === '-h' || args[i] === '--help') {
|
|
153
|
+
usage();
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!targetPath) {
|
|
159
|
+
targetPath = findClaudePath();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!targetPath || !fs.existsSync(targetPath)) {
|
|
163
|
+
console.error('Error: Could not find Claude Code cli.js.');
|
|
164
|
+
if (targetPath) console.error(`Path tried: ${targetPath}`);
|
|
165
|
+
usage();
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(`Target: ${targetPath}`);
|
|
170
|
+
|
|
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);
|
|
178
|
+
|
|
179
|
+
if (result.alreadyPatched) {
|
|
180
|
+
console.log('Claude is already patched for Vietnamese IME.');
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!result.success) {
|
|
185
|
+
console.error('Error: Failed to patch Claude. The code structure might have changed.');
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fs.writeFileSync(targetPath, result.content, 'utf-8');
|
|
190
|
+
console.log('Success: Claude has been patched for Vietnamese IME.');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Export for testing
|
|
194
|
+
module.exports = { patchContent };
|