claude-code-vietnamese-fix 1.1.0 → 2.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 +115 -124
- package/package.json +11 -4
- package/patch-vietnamese-ime.js +384 -271
- package/capture-input.js +0 -24
package/README.md
CHANGED
|
@@ -1,124 +1,115 @@
|
|
|
1
|
-
# claude-code-vietnamese-fix
|
|
2
|
-
|
|
3
|
-
> Fix Vietnamese input (Telex/VNI) for Claude Code CLI
|
|
4
|
-
|
|
5
|
-
[![
|
|
6
|
-
[![
|
|
7
|
-
[![
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
|
53
|
-
|
|
54
|
-
| `npx claude-code-vietnamese-fix` | Patch
|
|
55
|
-
| `npx claude-code-vietnamese-fix --
|
|
56
|
-
| `npx claude-code-vietnamese-fix
|
|
57
|
-
| `npx claude-code-vietnamese-fix
|
|
58
|
-
|
|
59
|
-
## Auto-patch After Updates
|
|
60
|
-
|
|
61
|
-
Claude Code updates
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
|
98
|
-
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
| Claude Code v2.x | ✅ |
|
|
106
|
-
|
|
107
|
-
##
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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)
|
|
1
|
+
# claude-code-vietnamese-fix
|
|
2
|
+
|
|
3
|
+
> Fix Vietnamese input (Telex/VNI) for Claude Code CLI — supports npm and native binary (WinGet/direct download).
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/claude-code-vietnamese-fix)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[]()
|
|
8
|
+
[]()
|
|
9
|
+
|
|
10
|
+
## The Problem
|
|
11
|
+
|
|
12
|
+
Vietnamese users **cannot type** in Claude Code CLI. Characters get duplicated or garbled.
|
|
13
|
+
|
|
14
|
+
**Expected:** `tôi` → **Actual:** `toôooi`
|
|
15
|
+
|
|
16
|
+
Vietnamese IME (Unikey, OpenKey, EVKey) embeds `\x7f` (DEL) chars in input strings. Claude Code's input handler inserts the entire string as-is, causing duplicate/garbled output.
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx claude-code-vietnamese-fix
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then **restart Claude Code**.
|
|
25
|
+
|
|
26
|
+
### Other install options
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Install globally
|
|
30
|
+
npm install -g claude-code-vietnamese-fix
|
|
31
|
+
claude-code-vietnamese-fix
|
|
32
|
+
|
|
33
|
+
# Clone and run
|
|
34
|
+
git clone https://github.com/tvtdev94/claude-code-vietnamese-fix.git
|
|
35
|
+
cd claude-code-vietnamese-fix
|
|
36
|
+
node patch-vietnamese-ime.js
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
The patch intercepts the existing `\x7f` detection point in Claude Code's input handler. It strips `\x7f` chars, inserts remaining chars one-by-one into the cursor state, then applies the final state atomically.
|
|
42
|
+
|
|
43
|
+
Works for both:
|
|
44
|
+
- **npm install** — patches `cli.js` directly
|
|
45
|
+
- **Native binary** (WinGet, direct download) — patches the embedded JS inside the Bun binary
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
| Command | Description |
|
|
50
|
+
|---------|-------------|
|
|
51
|
+
| `npx claude-code-vietnamese-fix` | Auto-detect and patch (creates backup) |
|
|
52
|
+
| `npx claude-code-vietnamese-fix --status` | Show patch/backup/hook status |
|
|
53
|
+
| `npx claude-code-vietnamese-fix --restore` | Restore original from backup |
|
|
54
|
+
| `npx claude-code-vietnamese-fix --silent` | Patch silently (no output if already patched) |
|
|
55
|
+
| `npx claude-code-vietnamese-fix --dry-run` | Test patch without saving |
|
|
56
|
+
| `npx claude-code-vietnamese-fix -f <path>` | Specify target file manually |
|
|
57
|
+
| `npx claude-code-vietnamese-fix -o <path>` | Write patched output to a new file |
|
|
58
|
+
|
|
59
|
+
## Auto-patch After Updates
|
|
60
|
+
|
|
61
|
+
Claude Code updates overwrite the patch. Add a SessionStart hook to auto-patch on every session:
|
|
62
|
+
|
|
63
|
+
`~/.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
|
+
```bash
|
|
88
|
+
npx claude-code-vietnamese-fix --restore
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Compatibility
|
|
92
|
+
|
|
93
|
+
| Item | Status |
|
|
94
|
+
|------|--------|
|
|
95
|
+
| Windows 10/11 | ✅ |
|
|
96
|
+
| macOS | ✅ |
|
|
97
|
+
| Linux | ✅ |
|
|
98
|
+
| Unikey (Telex/VNI/VIQR) | ✅ |
|
|
99
|
+
| OpenKey / EVKey | ✅ |
|
|
100
|
+
| npm global install | ✅ |
|
|
101
|
+
| NVM for Windows | ✅ |
|
|
102
|
+
| Bun global install | ✅ |
|
|
103
|
+
| **Native binary (WinGet)** | ✅ |
|
|
104
|
+
| **Native binary (macOS/Linux)** | ✅ |
|
|
105
|
+
| Claude Code v2.x | ✅ |
|
|
106
|
+
|
|
107
|
+
## Related
|
|
108
|
+
|
|
109
|
+
- [Issue #3961](https://github.com/anthropics/claude-code/issues/3961) — Unicode Input Handling Fails for Vietnamese Characters
|
|
110
|
+
- [Issue #7989](https://github.com/anthropics/claude-code/issues/7989) — Error typing Vietnamese Telex
|
|
111
|
+
- [Issue #10429](https://github.com/anthropics/claude-code/issues/10429) — Vietnamese Input Not Working
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-vietnamese-fix",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Fix Vietnamese IME input (Unikey/Telex) for Claude Code CLI
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Fix Vietnamese IME input (Unikey/Telex) for Claude Code CLI — supports npm and native binary",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-code-vietnamese-fix": "patch-vietnamese-ime.js"
|
|
7
7
|
},
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
"patch": "node patch-vietnamese-ime.js",
|
|
10
10
|
"patch:silent": "node patch-vietnamese-ime.js --silent",
|
|
11
11
|
"status": "node patch-vietnamese-ime.js --status",
|
|
12
|
-
"restore": "node patch-vietnamese-ime.js --restore"
|
|
12
|
+
"restore": "node patch-vietnamese-ime.js --restore",
|
|
13
|
+
"test": "vitest run"
|
|
13
14
|
},
|
|
14
|
-
"keywords": ["claude-code", "vietnamese", "ime", "unikey", "telex", "windows", "patch"],
|
|
15
|
+
"keywords": ["claude-code", "vietnamese", "ime", "unikey", "telex", "windows", "patch", "binary"],
|
|
15
16
|
"author": "tvtdev94",
|
|
16
17
|
"license": "MIT",
|
|
17
18
|
"repository": {
|
|
@@ -20,5 +21,11 @@
|
|
|
20
21
|
},
|
|
21
22
|
"engines": {
|
|
22
23
|
"node": ">=16"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"patch-vietnamese-ime.js"
|
|
27
|
+
],
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"vitest": "^4.0.17"
|
|
23
30
|
}
|
|
24
31
|
}
|
package/patch-vietnamese-ime.js
CHANGED
|
@@ -1,271 +1,384 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Patch Claude Code CLI to fix Vietnamese IME input
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* duplicate/garbled characters.
|
|
10
|
-
*
|
|
11
|
-
* Fix:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* node patch-vietnamese-ime.js
|
|
18
|
-
* node patch-vietnamese-ime.js --
|
|
19
|
-
* node patch-vietnamese-ime.js --
|
|
20
|
-
* node patch-vietnamese-ime.js --
|
|
21
|
-
* node patch-vietnamese-ime.js -f <path> # specify
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (!
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Patch Claude Code CLI to fix Vietnamese IME input on Windows/macOS/Linux.
|
|
5
|
+
* Supports both npm-installed (cli.js) and native binary versions.
|
|
6
|
+
*
|
|
7
|
+
* Root cause: Vietnamese IME (Unikey, OpenKey, EVKey) embeds \x7f (DEL) chars
|
|
8
|
+
* in input strings. Claude Code's input handler inserts the entire string as-is,
|
|
9
|
+
* causing duplicate/garbled characters.
|
|
10
|
+
*
|
|
11
|
+
* Fix: Intercept the existing \x7f detection point in Claude Code's input handler.
|
|
12
|
+
* Strip \x7f chars, insert remaining chars char-by-char into cursor state, then
|
|
13
|
+
* apply the final state atomically.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node patch-vietnamese-ime.js # patch (auto-find cli.js or binary)
|
|
17
|
+
* node patch-vietnamese-ime.js --silent # patch, no output if already patched
|
|
18
|
+
* node patch-vietnamese-ime.js --restore # restore from backup
|
|
19
|
+
* node patch-vietnamese-ime.js --status # check patch status
|
|
20
|
+
* node patch-vietnamese-ime.js --dry-run # test patch without saving
|
|
21
|
+
* node patch-vietnamese-ime.js -f <path> # specify target file manually
|
|
22
|
+
* node patch-vietnamese-ime.js -o <path> # write patched output to new file
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require("fs");
|
|
26
|
+
const path = require("path");
|
|
27
|
+
const { execSync } = require("child_process");
|
|
28
|
+
const os = require("os");
|
|
29
|
+
|
|
30
|
+
const PATCH_MARKER = "/* _vn_ime_fix_ */";
|
|
31
|
+
|
|
32
|
+
// --- CLI argument parsing ---
|
|
33
|
+
|
|
34
|
+
function parseArgs() {
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
const opts = {
|
|
37
|
+
silent: false,
|
|
38
|
+
restore: false,
|
|
39
|
+
status: false,
|
|
40
|
+
dryRun: false,
|
|
41
|
+
file: null,
|
|
42
|
+
output: null,
|
|
43
|
+
};
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
if (args[i] === "--silent") opts.silent = true;
|
|
46
|
+
else if (args[i] === "--restore") opts.restore = true;
|
|
47
|
+
else if (args[i] === "--status") opts.status = true;
|
|
48
|
+
else if (args[i] === "--dry-run" || args[i] === "-d") opts.dryRun = true;
|
|
49
|
+
else if (args[i] === "-f" || args[i] === "--file") opts.file = args[++i];
|
|
50
|
+
else if (args[i] === "-o" || args[i] === "--output") opts.output = args[++i];
|
|
51
|
+
else if (args[i] === "-h" || args[i] === "--help") {
|
|
52
|
+
console.log(`
|
|
53
|
+
Usage:
|
|
54
|
+
claude-code-vietnamese-fix [options]
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
-f, --file <path> Path to cli.js or claude binary
|
|
58
|
+
-d, --dry-run Test without overwriting the file
|
|
59
|
+
-o, --output <path> Write patched content to a new file
|
|
60
|
+
--silent No output if already patched
|
|
61
|
+
--restore Restore original from backup
|
|
62
|
+
--status Show patch/backup/hook status
|
|
63
|
+
-h, --help Show this help message
|
|
64
|
+
`);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return opts;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Locate cli.js or binary ---
|
|
72
|
+
|
|
73
|
+
function findClaudePath() {
|
|
74
|
+
const isWin = os.platform() === "win32";
|
|
75
|
+
|
|
76
|
+
const run = (cmd) => {
|
|
77
|
+
try {
|
|
78
|
+
return execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] })
|
|
79
|
+
.toString().split(/\r?\n/)[0].trim();
|
|
80
|
+
} catch { return ""; }
|
|
81
|
+
};
|
|
82
|
+
const exists = (p) => p && fs.existsSync(p);
|
|
83
|
+
|
|
84
|
+
// 1) which / where
|
|
85
|
+
for (const cmd of [isWin ? "where claude" : "which claude"]) {
|
|
86
|
+
const p = run(cmd);
|
|
87
|
+
if (exists(p)) {
|
|
88
|
+
if (!isWin) {
|
|
89
|
+
try { return execSync(`realpath "${p}"`).toString().trim(); } catch {}
|
|
90
|
+
}
|
|
91
|
+
return p;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 2) Bun global paths
|
|
96
|
+
const bunInstall =
|
|
97
|
+
process.env.BUN_INSTALL ||
|
|
98
|
+
(isWin
|
|
99
|
+
? path.join(process.env.USERPROFILE || "", ".bun")
|
|
100
|
+
: path.join(process.env.HOME || "", ".bun"));
|
|
101
|
+
|
|
102
|
+
const bunPaths = [
|
|
103
|
+
path.join(bunInstall, "bin", isWin ? "claude.exe" : "claude"),
|
|
104
|
+
path.join(bunInstall, "bin", isWin ? "claude.cmd" : "claude"),
|
|
105
|
+
path.join(bunInstall, "install", "global", "node_modules", "@anthropic-ai", "claude-code", "cli.js"),
|
|
106
|
+
];
|
|
107
|
+
for (const p of bunPaths) {
|
|
108
|
+
if (exists(p)) return p;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 3) npm global
|
|
112
|
+
try {
|
|
113
|
+
const npmRoot = run("npm root -g");
|
|
114
|
+
const p = path.join(npmRoot, "@anthropic-ai", "claude-code", "cli.js");
|
|
115
|
+
if (exists(p)) return p;
|
|
116
|
+
} catch {}
|
|
117
|
+
|
|
118
|
+
// 4) Windows fallbacks
|
|
119
|
+
if (isWin) {
|
|
120
|
+
const paths = [
|
|
121
|
+
path.join(process.env.APPDATA || "", "npm", "node_modules", "@anthropic-ai", "claude-code", "cli.js"),
|
|
122
|
+
path.join(process.env.LOCALAPPDATA || "", "npm", "node_modules", "@anthropic-ai", "claude-code", "cli.js"),
|
|
123
|
+
];
|
|
124
|
+
if (process.env.NVM_HOME) {
|
|
125
|
+
try {
|
|
126
|
+
for (const dir of fs.readdirSync(process.env.NVM_HOME)) {
|
|
127
|
+
paths.push(path.join(process.env.NVM_HOME, dir, "node_modules", "@anthropic-ai", "claude-code", "cli.js"));
|
|
128
|
+
}
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
for (const p of paths) { if (exists(p)) return p; }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- Patch logic for cli.js (npm install) ---
|
|
138
|
+
|
|
139
|
+
// Pattern matching the \x7f detection + cursor state application in Claude Code:
|
|
140
|
+
// l.match(/\x7f/g)...if(!S.equals(CA)){if(S.text!==CA.text)Q(CA.text);T(CA.offset)}ct1(),lt1();return
|
|
141
|
+
const PATCH_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;
|
|
142
|
+
|
|
143
|
+
function buildPatch(m0, m1, var0, var2, m2) {
|
|
144
|
+
return `
|
|
145
|
+
${PATCH_MARKER}
|
|
146
|
+
${m0}
|
|
147
|
+
let _vn = ${var0}.replace(/\\x7f/g, "");
|
|
148
|
+
if (_vn.length > 0) {
|
|
149
|
+
for (const _c of _vn) ${var2} = ${var2}.insert(_c);
|
|
150
|
+
${m1}
|
|
151
|
+
}
|
|
152
|
+
${m2}
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function patchContentJs(fileContent) {
|
|
157
|
+
if (fileContent.includes(PATCH_MARKER)) {
|
|
158
|
+
return { success: true, alreadyPatched: true };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const newContent = fileContent.replace(PATCH_RE, (...args) => {
|
|
162
|
+
const { m0, m1, var0, var2, m2 } = args[args.length - 1];
|
|
163
|
+
return buildPatch(m0, m1, var0, var2, m2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (newContent.length === fileContent.length) {
|
|
167
|
+
return { success: false, message: "Patch failed: no match found in cli.js" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { success: true, alreadyPatched: false, content: newContent };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- Patch logic for native binary (Bun-embedded JS) ---
|
|
174
|
+
|
|
175
|
+
function patchContentBinary(binaryContent) {
|
|
176
|
+
if (binaryContent.includes(PATCH_MARKER)) {
|
|
177
|
+
return { success: true, alreadyPatched: true };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const matches = [];
|
|
181
|
+
const newContent = binaryContent.replace(PATCH_RE, (...args) => {
|
|
182
|
+
const groups = args[args.length - 1];
|
|
183
|
+
const offset = args[args.length - 3];
|
|
184
|
+
const { m0, m1, var0, var2, m2 } = groups;
|
|
185
|
+
|
|
186
|
+
// Compact version (strip leading whitespace) to minimize size diff
|
|
187
|
+
const patched = buildPatch(m0, m1, var0, var2, m2).replace(/^\s+/gm, "");
|
|
188
|
+
matches.push({ diff: patched.length - args[0].length, index: offset });
|
|
189
|
+
return patched;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (matches.length === 0) {
|
|
193
|
+
return { success: false, message: "Patch failed: no match found in binary" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Bun binary embeds JS sections prefixed with \x00// @bun.
|
|
197
|
+
// We must compensate for the added bytes by trimming the pragma comment.
|
|
198
|
+
const pragma = "// @bun ";
|
|
199
|
+
const pragmaLength = pragma.length;
|
|
200
|
+
let result = newContent;
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < matches.length; i++) {
|
|
203
|
+
const matchIdx = matches[i].index;
|
|
204
|
+
const prevIdx = i === 0 ? 0 : matches[i - 1].index;
|
|
205
|
+
|
|
206
|
+
for (let j = matchIdx - 1; j >= prevIdx; j--) {
|
|
207
|
+
if (result[j] === "\x00") {
|
|
208
|
+
if (result.slice(j + 1, j + 1 + pragmaLength) === pragma) {
|
|
209
|
+
// Find the first newline+// after the pragma
|
|
210
|
+
for (let k = j + 1 + pragmaLength; k < matchIdx; k++) {
|
|
211
|
+
if (result[k] === "\n" && result[k + 1] === "/" && result[k + 2] === "/") {
|
|
212
|
+
const diff = matches[i].diff;
|
|
213
|
+
const sliceStart = k + 3;
|
|
214
|
+
result = result.slice(0, sliceStart) + result.slice(sliceStart + diff);
|
|
215
|
+
matches[i].found = true;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (matches[i].found) break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!matches[i].found) break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (matches.every((m) => !m.found)) {
|
|
227
|
+
return { success: false, message: "Patch failed: could not adjust binary pragma" };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { success: true, alreadyPatched: false, content: result };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- Auto-install SessionStart hook ---
|
|
234
|
+
|
|
235
|
+
const HOOK_COMMAND = "npx claude-code-vietnamese-fix --silent";
|
|
236
|
+
|
|
237
|
+
function installHook(silent) {
|
|
238
|
+
const homeDir = process.env.USERPROFILE || process.env.HOME || "";
|
|
239
|
+
const settingsPath = path.join(homeDir, ".claude", "settings.json");
|
|
240
|
+
|
|
241
|
+
let settings = {};
|
|
242
|
+
if (fs.existsSync(settingsPath)) {
|
|
243
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); } catch { return; }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check if hook already exists
|
|
247
|
+
const hooks = settings.hooks?.SessionStart || [];
|
|
248
|
+
for (const entry of hooks) {
|
|
249
|
+
for (const h of (entry.hooks || [])) {
|
|
250
|
+
if (h.command && h.command.includes("claude-code-vietnamese-fix")) return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!settings.hooks) settings.hooks = {};
|
|
255
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
256
|
+
|
|
257
|
+
const matcher = "startup|resume|clear|compact";
|
|
258
|
+
let target = settings.hooks.SessionStart.find((e) => e.matcher === matcher);
|
|
259
|
+
if (!target) {
|
|
260
|
+
target = { matcher, hooks: [] };
|
|
261
|
+
settings.hooks.SessionStart.push(target);
|
|
262
|
+
}
|
|
263
|
+
target.hooks.push({ type: "command", command: HOOK_COMMAND });
|
|
264
|
+
|
|
265
|
+
const claudeDir = path.join(homeDir, ".claude");
|
|
266
|
+
if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
|
|
267
|
+
|
|
268
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
269
|
+
if (!silent) console.log("Hook installed: auto-patch on SessionStart");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// --- Backup & restore ---
|
|
273
|
+
|
|
274
|
+
function backupPath(targetPath) { return targetPath + ".bak"; }
|
|
275
|
+
|
|
276
|
+
function createBackup(targetPath) {
|
|
277
|
+
const bak = backupPath(targetPath);
|
|
278
|
+
fs.copyFileSync(targetPath, bak);
|
|
279
|
+
return bak;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function restoreBackup(targetPath) {
|
|
283
|
+
const bak = backupPath(targetPath);
|
|
284
|
+
if (!fs.existsSync(bak)) return { success: false, message: "No backup found at " + bak };
|
|
285
|
+
fs.copyFileSync(bak, targetPath);
|
|
286
|
+
return { success: true };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// --- Main ---
|
|
290
|
+
|
|
291
|
+
function main() {
|
|
292
|
+
const opts = parseArgs();
|
|
293
|
+
const targetPath = opts.file || findClaudePath();
|
|
294
|
+
|
|
295
|
+
if (!targetPath || !fs.existsSync(targetPath)) {
|
|
296
|
+
console.error("Error: Could not find Claude Code (cli.js or binary).");
|
|
297
|
+
if (targetPath) console.error("Tried: " + targetPath);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log("Target: " + targetPath);
|
|
302
|
+
|
|
303
|
+
if (opts.status) {
|
|
304
|
+
const content = fs.readFileSync(targetPath, "latin1");
|
|
305
|
+
const patched = content.includes(PATCH_MARKER);
|
|
306
|
+
console.log("Status: " + (patched ? "PATCHED" : "NOT PATCHED"));
|
|
307
|
+
console.log("Backup: " + (fs.existsSync(backupPath(targetPath)) ? "EXISTS" : "NONE"));
|
|
308
|
+
const homeDir = process.env.USERPROFILE || process.env.HOME || "";
|
|
309
|
+
const sp = path.join(homeDir, ".claude", "settings.json");
|
|
310
|
+
let hookInstalled = false;
|
|
311
|
+
try {
|
|
312
|
+
const s = JSON.parse(fs.readFileSync(sp, "utf8"));
|
|
313
|
+
hookInstalled = JSON.stringify(s).includes("claude-code-vietnamese-fix");
|
|
314
|
+
} catch {}
|
|
315
|
+
console.log("Hook: " + (hookInstalled ? "INSTALLED" : "NOT INSTALLED"));
|
|
316
|
+
process.exit(0);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (opts.restore) {
|
|
320
|
+
const result = restoreBackup(targetPath);
|
|
321
|
+
if (!result.success) { console.error(result.message); process.exit(1); }
|
|
322
|
+
console.log("Restored from backup: " + targetPath);
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Restore from backup first if exists (ensure clean base for re-patching)
|
|
327
|
+
// Skip restore for --dry-run and --output to avoid touching the live file
|
|
328
|
+
if (!opts.dryRun && !opts.output && fs.existsSync(backupPath(targetPath))) {
|
|
329
|
+
fs.copyFileSync(backupPath(targetPath), targetPath);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const content = fs.readFileSync(targetPath, "latin1");
|
|
333
|
+
const isJs = targetPath.endsWith(".js");
|
|
334
|
+
const result = isJs ? patchContentJs(content) : patchContentBinary(content);
|
|
335
|
+
|
|
336
|
+
if (result.alreadyPatched) {
|
|
337
|
+
if (!opts.silent) console.log("Already patched — skipping");
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!result.success) {
|
|
342
|
+
console.error(result.message);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (opts.dryRun) {
|
|
347
|
+
console.log("Dry run: patch applied successfully (not saved).");
|
|
348
|
+
process.exit(0);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const finalPath = opts.output || targetPath;
|
|
352
|
+
|
|
353
|
+
// Backup original before writing (skip if writing to a different output path)
|
|
354
|
+
if (!opts.output) {
|
|
355
|
+
createBackup(targetPath);
|
|
356
|
+
if (!opts.silent) console.log("Backup: " + backupPath(targetPath));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
fs.writeFileSync(finalPath, result.content, "latin1");
|
|
360
|
+
console.log("Patched: " + finalPath);
|
|
361
|
+
|
|
362
|
+
// Re-sign binary on macOS (required to pass Gatekeeper)
|
|
363
|
+
if (os.platform() === "darwin" && !isJs) {
|
|
364
|
+
try {
|
|
365
|
+
execSync(`codesign --sign - --force --preserve-metadata=entitlements,requirements,flags "${finalPath}"`, { stdio: "inherit" });
|
|
366
|
+
console.log("Re-signed binary successfully.");
|
|
367
|
+
} catch (e) {
|
|
368
|
+
console.error("Warning: Re-sign failed:", e.message);
|
|
369
|
+
console.error(`Run manually: codesign --sign - --force --preserve-metadata=entitlements,requirements,flags "${finalPath}"`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Auto-install SessionStart hook (only for npm cli.js, not binary)
|
|
374
|
+
if (isJs && !opts.output) {
|
|
375
|
+
installHook(opts.silent);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (require.main === module) {
|
|
380
|
+
main();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Export for testing
|
|
384
|
+
module.exports = { PATCH_MARKER, patchContentJs, patchContentBinary };
|
package/capture-input.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
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(); });
|