fix-claude-code-vietnamese 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/.test-cache/.gitkeep +0 -0
- package/package.json +26 -0
- package/patch-cli-claude-code.js +194 -0
- package/patch-cli-claude-code.test.js +91 -0
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fix-claude-code-vietnamese",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fix Vietnamese IME compatibility issues in Claude Code.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"fix-claude-code-vn": "./patch-cli-claude-code.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"claude",
|
|
10
|
+
"vietnamese",
|
|
11
|
+
"ime",
|
|
12
|
+
"fix"
|
|
13
|
+
],
|
|
14
|
+
"author": "0x0a0d",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/0x0a0d/fix-claude-code-vietnamese"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"vitest": "^4.0.17"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "vitest run"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -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 };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import { patchContent } from './patch-cli-claude-code.js';
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
|
|
9
|
+
const MIN_VERSION_TEST = '2.0.64';
|
|
10
|
+
|
|
11
|
+
function getVersions(minVersion) {
|
|
12
|
+
const versionPaths = minVersion.split('.').map(Number);
|
|
13
|
+
if (versionPaths.length === 2) {
|
|
14
|
+
versionPaths.push(0);
|
|
15
|
+
} else if (versionPaths.length === 1) {
|
|
16
|
+
versionPaths.push(0, 0);
|
|
17
|
+
} else if (versionPaths.length > 3) {
|
|
18
|
+
throw new Error('Invalid version format');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const output = execSync('npm view @anthropic-ai/claude-code versions --json').toString();
|
|
23
|
+
const allVersions = JSON.parse(output);
|
|
24
|
+
return allVersions.filter(v => {
|
|
25
|
+
// filter version >= 2.0.64
|
|
26
|
+
const parts = v.split('.').map(Number);
|
|
27
|
+
if (parts[0] > versionPaths[0]) return true;
|
|
28
|
+
if (parts[0] === versionPaths[0]) {
|
|
29
|
+
if (parts[1] > versionPaths[1]) return true;
|
|
30
|
+
if (parts[1] === versionPaths[1]) return parts[2] >= versionPaths[2];
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
});
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error('Failed to fetch versions from npm, using fallback.');
|
|
36
|
+
return ['2.0.64', '2.1.12'];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const VERSIONS = getVersions(MIN_VERSION_TEST);
|
|
41
|
+
|
|
42
|
+
const CACHE_DIR = path.join(process.cwd(), '.test-cache');
|
|
43
|
+
|
|
44
|
+
async function downloadFile(url, dest) {
|
|
45
|
+
if (fs.existsSync(dest)) return;
|
|
46
|
+
|
|
47
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
48
|
+
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const file = fs.createWriteStream(dest);
|
|
51
|
+
https.get(url, (response) => {
|
|
52
|
+
if (response.statusCode !== 200) {
|
|
53
|
+
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
response.pipe(file);
|
|
57
|
+
file.on('finish', () => {
|
|
58
|
+
file.close();
|
|
59
|
+
resolve();
|
|
60
|
+
});
|
|
61
|
+
}).on('error', (err) => {
|
|
62
|
+
fs.unlink(dest, () => reject(err));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('Claude Code Vietnamese Patch Test', () => {
|
|
68
|
+
beforeAll(async () => {
|
|
69
|
+
// download versions if not exists
|
|
70
|
+
for (const v of VERSIONS) {
|
|
71
|
+
const url = `https://unpkg.com/@anthropic-ai/claude-code@${v}/cli.js`;
|
|
72
|
+
const dest = path.join(CACHE_DIR, `cli-${v}.js`);
|
|
73
|
+
console.log(`Checking version ${v}...`);
|
|
74
|
+
await downloadFile(url, dest);
|
|
75
|
+
}
|
|
76
|
+
}, 60000); // 1 minute timeout for downloads
|
|
77
|
+
|
|
78
|
+
it.each(VERSIONS)('should successfully patch version %s', (v) => {
|
|
79
|
+
const filePath = path.join(CACHE_DIR, `cli-${v}.js`);
|
|
80
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
81
|
+
|
|
82
|
+
const result = patchContent(content);
|
|
83
|
+
|
|
84
|
+
expect(result.success, `Patch failed for version ${v}`).toBe(true);
|
|
85
|
+
expect(result.content).toContain('/* Vietnamese IME fix */');
|
|
86
|
+
|
|
87
|
+
// Verify code structure after patch
|
|
88
|
+
expect(result.content).toMatch(/let _vn = \w+\.replace\(\/\\x7f\/g, ""\);/);
|
|
89
|
+
expect(result.content).toContain('.insert(_c)');
|
|
90
|
+
});
|
|
91
|
+
});
|