claude-code-vietnamese-fix 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 ADDED
@@ -0,0 +1,124 @@
1
+ # claude-code-vietnamese-fix
2
+
3
+ > Fix Vietnamese input (Telex/VNI) for Claude Code CLI on Windows — one command, zero config.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![Platform](https://img.shields.io/badge/platform-Windows-blue)]()
7
+ [![Claude Code](https://img.shields.io/badge/Claude_Code-v2.x-blueviolet)]()
8
+
9
+ ## The Problem
10
+
11
+ Vietnamese users **cannot type** in Claude Code CLI. Characters get duplicated or garbled.
12
+
13
+ **Expected:** `tôi` → **Actual:** `toôooi`
14
+
15
+ Unikey sends backspace (`\x08`) + replacement chars embedded in the input string. Claude Code's input handler doesn't process these embedded BS chars — it inserts the entire string as-is.
16
+
17
+ ## Quick Start
18
+
19
+ ### Option 1: npx (no install needed)
20
+
21
+ ```bash
22
+ npx claude-code-vietnamese-fix
23
+ ```
24
+
25
+ ### Option 2: Install globally
26
+
27
+ ```bash
28
+ npm install -g claude-code-vietnamese-fix
29
+ claude-code-vietnamese-fix
30
+ ```
31
+
32
+ ### Option 3: Clone and run
33
+
34
+ ```bash
35
+ git clone https://github.com/tvtdev94/claude-code-vietnamese-fix.git
36
+ cd claude-code-vietnamese-fix
37
+ node patch-vietnamese-ime.js
38
+ ```
39
+
40
+ Then **restart Claude Code**.
41
+
42
+ ## How it works
43
+
44
+ The patch wraps Claude Code's `onInput` handler. When embedded BS chars (`\x08`) are detected, it processes each character sequentially against the cursor state:
45
+ - `\x08` (BS) → `cursor.backspace()` — deletes the previous char
46
+ - Any other char → `cursor.insert(char)` — inserts normally
47
+
48
+ The final cursor state is applied atomically.
49
+
50
+ ## Commands
51
+
52
+ | Command | Description |
53
+ |---------|-------------|
54
+ | `npx claude-code-vietnamese-fix` | Patch cli.js (auto-finds, auto-backups) |
55
+ | `npx claude-code-vietnamese-fix --status` | Check current patch status |
56
+ | `npx claude-code-vietnamese-fix --restore` | Restore original cli.js from backup |
57
+ | `npx claude-code-vietnamese-fix --silent` | Patch without output if already patched |
58
+
59
+ ## Auto-patch After Updates
60
+
61
+ Claude Code updates will overwrite the patch. Add a SessionStart hook to auto-patch:
62
+
63
+ Add to `~/.claude/settings.json`:
64
+
65
+ ```json
66
+ {
67
+ "hooks": {
68
+ "SessionStart": [
69
+ {
70
+ "matcher": "startup|resume|clear|compact",
71
+ "hooks": [
72
+ {
73
+ "type": "command",
74
+ "command": "npx claude-code-vietnamese-fix --silent"
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ }
80
+ }
81
+ ```
82
+
83
+ The script is **idempotent** — safe to run on every session start.
84
+
85
+ ## Restore
86
+
87
+ If anything goes wrong:
88
+
89
+ ```bash
90
+ npx claude-code-vietnamese-fix --restore
91
+ ```
92
+
93
+ Backup location: `%APPDATA%\npm\node_modules\@anthropic-ai\claude-code\cli.js.bak`
94
+
95
+ ## Compatibility
96
+
97
+ | Item | Status |
98
+ |------|--------|
99
+ | Windows 10/11 | ✅ |
100
+ | Unikey (Telex) | ✅ Tested |
101
+ | Unikey (VNI, VIQR) | ⚠️ Untested (should work) |
102
+ | OpenKey / EVKey | ⚠️ Untested (should work) |
103
+ | npm global install | ✅ |
104
+ | NVM for Windows | ✅ |
105
+ | Claude Code v2.x | ✅ |
106
+
107
+ ## Debug
108
+
109
+ Capture raw input bytes to diagnose IME behavior:
110
+
111
+ ```bash
112
+ node capture-input.js
113
+ # Type Vietnamese text, press Ctrl+C to stop
114
+ ```
115
+
116
+ ## Related
117
+
118
+ - [Issue #3961](https://github.com/anthropics/claude-code/issues/3961) — Unicode Input Handling Fails for Vietnamese Characters
119
+ - [Issue #7989](https://github.com/anthropics/claude-code/issues/7989) — Error typing Vietnamese Telex
120
+ - [Issue #10429](https://github.com/anthropics/claude-code/issues/10429) — Vietnamese Input Not Working
121
+
122
+ ## License
123
+
124
+ [MIT](LICENSE)
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Capture raw stdin bytes to debug Vietnamese IME input.
4
+ * Run: node capture-input.js
5
+ * Type Vietnamese text, press Ctrl+C to stop.
6
+ * Shows hex dump of every byte received.
7
+ */
8
+ process.stdin.setRawMode(true);
9
+ process.stdin.resume();
10
+ process.stdin.setEncoding(null);
11
+
12
+ console.log("Type Vietnamese text (Ctrl+C to quit):\n");
13
+
14
+ process.stdin.on("data", (buf) => {
15
+ const hex = [...buf].map(b => b.toString(16).padStart(2, "0")).join(" ");
16
+ const chars = [...buf].map(b => b >= 32 && b < 127 ? String.fromCharCode(b) : b === 0x7f ? "<DEL>" : b === 0x08 ? "<BS>" : `\\x${b.toString(16).padStart(2, "0")}`).join("");
17
+ const utf8 = buf.toString("utf8");
18
+ console.log(`HEX: ${hex}`);
19
+ console.log(`RAW: ${chars}`);
20
+ console.log(`UTF: ${utf8}`);
21
+ console.log("---");
22
+ });
23
+
24
+ process.on("SIGINT", () => { console.log("\nDone."); process.exit(); });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "claude-code-vietnamese-fix",
3
+ "version": "1.0.0",
4
+ "description": "Fix Vietnamese IME input (Unikey/Telex) for Claude Code CLI on Windows",
5
+ "bin": {
6
+ "claude-code-vietnamese-fix": "patch-vietnamese-ime.js"
7
+ },
8
+ "scripts": {
9
+ "patch": "node patch-vietnamese-ime.js",
10
+ "patch:silent": "node patch-vietnamese-ime.js --silent",
11
+ "status": "node patch-vietnamese-ime.js --status",
12
+ "restore": "node patch-vietnamese-ime.js --restore"
13
+ },
14
+ "keywords": ["claude-code", "vietnamese", "ime", "unikey", "telex", "windows", "patch"],
15
+ "author": "tvtdev94",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/tvtdev94/claude-code-vietnamese-fix"
20
+ },
21
+ "engines": {
22
+ "node": ">=16"
23
+ }
24
+ }
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Patch Claude Code CLI to fix Vietnamese IME input (Unikey/Telex) on Windows.
5
+ *
6
+ * Root cause: Unikey sends backspace chars (\x08) embedded in the input string
7
+ * along with replacement chars. Claude Code's input handler doesn't process
8
+ * these embedded BS chars — it inserts the entire string as-is, causing
9
+ * duplicate/garbled characters.
10
+ *
11
+ * Fix: Wrap the input handler to detect embedded \x08 (BS) in the input
12
+ * string. When found, process each char sequentially: regular chars get
13
+ * inserted, \x08 triggers a cursor.backspace(). The result is applied
14
+ * atomically to avoid stale-state issues.
15
+ *
16
+ * Usage:
17
+ * node patch-vietnamese-ime.js # patch (auto-find cli.js)
18
+ * node patch-vietnamese-ime.js --silent # patch, no output if already patched
19
+ * node patch-vietnamese-ime.js --restore # restore from backup
20
+ * node patch-vietnamese-ime.js --status # check patch status
21
+ * node patch-vietnamese-ime.js -f <path> # specify cli.js path manually
22
+ */
23
+
24
+ const fs = require("fs");
25
+ const path = require("path");
26
+ const { execSync } = require("child_process");
27
+
28
+ const PATCH_MARKER = "/* _vietnamese_ime_fix_v4_ */";
29
+
30
+ // --- CLI argument parsing ---
31
+
32
+ function parseArgs() {
33
+ const args = process.argv.slice(2);
34
+ const opts = { silent: false, restore: false, status: false, file: null };
35
+ for (let i = 0; i < args.length; i++) {
36
+ if (args[i] === "--silent") opts.silent = true;
37
+ else if (args[i] === "--restore") opts.restore = true;
38
+ else if (args[i] === "--status") opts.status = true;
39
+ else if (args[i] === "-f" || args[i] === "--file") opts.file = args[++i];
40
+ }
41
+ return opts;
42
+ }
43
+
44
+ // --- Locate cli.js on Windows ---
45
+
46
+ function findCliJs() {
47
+ const run = (cmd) => {
48
+ try {
49
+ return execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] })
50
+ .toString().split(/\r?\n/)[0].trim();
51
+ } catch { return ""; }
52
+ };
53
+ const exists = (p) => p && fs.existsSync(p);
54
+
55
+ // npm global
56
+ try {
57
+ const npmRoot = run("npm root -g");
58
+ const p = path.join(npmRoot, "@anthropic-ai", "claude-code", "cli.js");
59
+ if (exists(p)) return p;
60
+ } catch {}
61
+
62
+ // Common Windows paths
63
+ const paths = [
64
+ path.join(process.env.APPDATA || "", "npm", "node_modules", "@anthropic-ai", "claude-code", "cli.js"),
65
+ path.join(process.env.LOCALAPPDATA || "", "npm", "node_modules", "@anthropic-ai", "claude-code", "cli.js"),
66
+ ];
67
+ if (process.env.NVM_HOME) {
68
+ try {
69
+ for (const dir of fs.readdirSync(process.env.NVM_HOME)) {
70
+ paths.push(path.join(process.env.NVM_HOME, dir, "node_modules", "@anthropic-ai", "claude-code", "cli.js"));
71
+ }
72
+ } catch {}
73
+ }
74
+ for (const p of paths) { if (exists(p)) return p; }
75
+ return null;
76
+ }
77
+
78
+ // --- Patch logic ---
79
+
80
+ function patchContent(content) {
81
+ if (content.includes(PATCH_MARKER)) {
82
+ return { success: true, alreadyPatched: true };
83
+ }
84
+
85
+ let patched = content;
86
+
87
+ // Find the input handler function:
88
+ // function n(t,r){let l=G?G(t,r):t;if(l===""&&t!=="")return;...}
89
+ // This is the onInput handler for Claude Code's text input component.
90
+ const outerRe = /function\s+([\w$]+)\(([\w$]+),([\w$]+)\)\{let\s+([\w$]+)=([\w$]+)\?\5\(\2,\3\):\2;if\(\4===""&&\2!==""\)return;/;
91
+ const outerMatch = patched.match(outerRe);
92
+
93
+ if (!outerMatch) {
94
+ return { success: false, message: "Patch failed: input handler function not found" };
95
+ }
96
+
97
+ const [fullMatch, fn, p1, p2, p3, p4] = outerMatch;
98
+
99
+ // The wrapper intercepts input strings containing \x08 (BS) chars.
100
+ // When detected, it processes each char against the cursor state `h`:
101
+ // - \x08: cursor.backspace()
102
+ // - other: cursor.insert(char)
103
+ // Then applies the final state atomically.
104
+ //
105
+ // For strings without \x08, passes through to original handler unchanged.
106
+ //
107
+ // Variables from the enclosing closure (captured by the original function):
108
+ // h = cursor state, q = setText, L = setOffset
109
+ // These are referenced via the original function's scope.
110
+ const wrapper =
111
+ `${PATCH_MARKER}` +
112
+ `function ${fn}(${p1},${p2}){` +
113
+ // Check for embedded BS chars (0x08) — Vietnamese IME signature
114
+ `if(!${p2}.backspace&&!${p2}.delete&&${p1}.includes("\\b")){` +
115
+ // Process char-by-char against cursor state h (from enclosing closure)
116
+ `var _c=h;` +
117
+ `for(var _i=0;_i<${p1}.length;_i++){` +
118
+ `var _ch=${p1}[_i];` +
119
+ `if(_ch==="\\b"){` +
120
+ `_c=_c.backspace()` +
121
+ `}else{` +
122
+ `_c=_c.insert(_ch)` +
123
+ `}` +
124
+ `}` +
125
+ // Apply final state atomically
126
+ `if(!h.equals(_c)){` +
127
+ `if(h.text!==_c.text)q(_c.text);` +
128
+ `L(_c.offset)` +
129
+ `}` +
130
+ `return` +
131
+ `}` +
132
+ // No embedded BS: pass through to original handler
133
+ `return _imeR(${p1},${p2})}` +
134
+ // Original function renamed to _imeR
135
+ `function _imeR(${p1},${p2}){` +
136
+ `let ${p3}=${p4}?${p4}(${p1},${p2}):${p1};` +
137
+ `if(${p3}===""&&${p1}!=="")return;`;
138
+
139
+ patched = patched.replace(fullMatch, wrapper);
140
+
141
+ if (patched.includes(fullMatch) && !patched.includes("_imeR")) {
142
+ return { success: false, message: "Patch: replacement failed" };
143
+ }
144
+
145
+ return { success: true, alreadyPatched: false, content: patched };
146
+ }
147
+
148
+ // --- Backup & restore ---
149
+
150
+ function backupPath(cliPath) { return cliPath + ".bak"; }
151
+
152
+ function createBackup(cliPath) {
153
+ const bak = backupPath(cliPath);
154
+ fs.copyFileSync(cliPath, bak);
155
+ return bak;
156
+ }
157
+
158
+ function restoreBackup(cliPath) {
159
+ const bak = backupPath(cliPath);
160
+ if (!fs.existsSync(bak)) return { success: false, message: "No backup found at " + bak };
161
+ fs.copyFileSync(bak, cliPath);
162
+ return { success: true };
163
+ }
164
+
165
+ // --- Main ---
166
+
167
+ function main() {
168
+ const opts = parseArgs();
169
+ const cliPath = opts.file || findCliJs();
170
+
171
+ if (!cliPath || !fs.existsSync(cliPath)) {
172
+ console.error("Error: Could not find Claude Code cli.js");
173
+ if (cliPath) console.error("Tried: " + cliPath);
174
+ process.exit(1);
175
+ }
176
+
177
+ if (opts.status) {
178
+ const content = fs.readFileSync(cliPath, "latin1");
179
+ const patched = content.includes(PATCH_MARKER);
180
+ console.log(`Target: ${cliPath}`);
181
+ console.log(`Status: ${patched ? "PATCHED" : "NOT PATCHED"}`);
182
+ console.log(`Backup: ${fs.existsSync(backupPath(cliPath)) ? "EXISTS" : "NONE"}`);
183
+ process.exit(0);
184
+ }
185
+
186
+ if (opts.restore) {
187
+ const result = restoreBackup(cliPath);
188
+ if (!result.success) { console.error(result.message); process.exit(1); }
189
+ console.log("Restored cli.js from backup");
190
+ process.exit(0);
191
+ }
192
+
193
+ // Restore from backup first if exists (ensure clean base for patching)
194
+ if (fs.existsSync(backupPath(cliPath))) {
195
+ fs.copyFileSync(backupPath(cliPath), cliPath);
196
+ }
197
+
198
+ const content = fs.readFileSync(cliPath, "latin1");
199
+ const result = patchContent(content);
200
+
201
+ if (result.alreadyPatched) {
202
+ if (!opts.silent) console.log("Already patched — skipping");
203
+ process.exit(0);
204
+ }
205
+
206
+ if (!result.success) {
207
+ console.error(result.message);
208
+ process.exit(1);
209
+ }
210
+
211
+ // Backup original before writing patch
212
+ createBackup(cliPath);
213
+ if (!opts.silent) console.log("Backup: " + backupPath(cliPath));
214
+
215
+ fs.writeFileSync(cliPath, result.content, "latin1");
216
+ console.log("Patched: " + cliPath);
217
+ }
218
+
219
+ main();