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.
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
+ });