claudescreenfix-hardwicksoftware 2.0.0 → 2.1.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/bin/claude-fixed.js +209 -27
- package/package.json +7 -3
package/bin/claude-fixed.js
CHANGED
|
@@ -2,44 +2,226 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* wrapper
|
|
5
|
+
* PTY wrapper for Claude Code terminal fix
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Since Claude's binary is a Node.js SEA (ELF), we can't inject via --import.
|
|
8
|
+
* Instead, we spawn claude in a pseudo-terminal and intercept stdout to inject
|
|
9
|
+
* scrollback clears and handle SIGWINCH debouncing.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const { spawn
|
|
12
|
+
const { spawn } = require('child_process');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
const
|
|
16
|
+
// Terminal escape codes
|
|
17
|
+
const CLEAR_SCROLLBACK = '\x1b[3J';
|
|
18
|
+
const CURSOR_SAVE = '\x1b[s';
|
|
19
|
+
const CURSOR_RESTORE = '\x1b[u';
|
|
20
|
+
const CLEAR_SCREEN = '\x1b[2J';
|
|
21
|
+
const HOME_CURSOR = '\x1b[H';
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
// Config
|
|
24
|
+
const config = {
|
|
25
|
+
resizeDebounceMs: 150,
|
|
26
|
+
periodicClearMs: 60000,
|
|
27
|
+
clearAfterRenders: 500,
|
|
28
|
+
typingCooldownMs: 500,
|
|
29
|
+
maxLineCount: 120,
|
|
30
|
+
debug: process.env.CLAUDE_TERMINAL_FIX_DEBUG === '1',
|
|
31
|
+
disabled: process.env.CLAUDE_TERMINAL_FIX_DISABLED === '1'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// State
|
|
35
|
+
let renderCount = 0;
|
|
36
|
+
let lineCount = 0;
|
|
37
|
+
let lastTypingTime = 0;
|
|
38
|
+
let lastResizeTime = 0;
|
|
39
|
+
let resizeTimeout = null;
|
|
40
|
+
|
|
41
|
+
function log(...args) {
|
|
42
|
+
if (config.debug) {
|
|
43
|
+
process.stderr.write('[terminal-fix] ' + args.join(' ') + '\n');
|
|
44
|
+
}
|
|
22
45
|
}
|
|
23
46
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
claudeBin = execSync('which claude', { encoding: 'utf8' }).trim();
|
|
28
|
-
} catch (e) {
|
|
29
|
-
console.error('claude not found in PATH - make sure it\'s installed');
|
|
30
|
-
process.exit(1);
|
|
47
|
+
function isTypingActive() {
|
|
48
|
+
return (Date.now() - lastTypingTime) < config.typingCooldownMs;
|
|
31
49
|
}
|
|
32
50
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
// Find the claude binary
|
|
52
|
+
function findClaude() {
|
|
53
|
+
const possiblePaths = [
|
|
54
|
+
path.join(process.env.HOME || '', '.local/bin/claude'),
|
|
55
|
+
'/usr/local/bin/claude',
|
|
56
|
+
'/usr/bin/claude'
|
|
57
|
+
];
|
|
37
58
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
59
|
+
for (const p of possiblePaths) {
|
|
60
|
+
if (fs.existsSync(p)) {
|
|
61
|
+
return p;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Try which
|
|
66
|
+
try {
|
|
67
|
+
const { execSync } = require('child_process');
|
|
68
|
+
return execSync('which claude', { encoding: 'utf8' }).trim();
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Process output chunk, injecting clears as needed
|
|
75
|
+
function processOutput(chunk) {
|
|
76
|
+
if (config.disabled) return chunk;
|
|
77
|
+
|
|
78
|
+
let output = chunk;
|
|
79
|
+
const str = chunk.toString();
|
|
80
|
+
|
|
81
|
+
renderCount++;
|
|
82
|
+
|
|
83
|
+
// Count newlines
|
|
84
|
+
const newlines = (str.match(/\n/g) || []).length;
|
|
85
|
+
lineCount += newlines;
|
|
86
|
+
|
|
87
|
+
// Line limit exceeded - force clear
|
|
88
|
+
if (lineCount > config.maxLineCount) {
|
|
89
|
+
log('line limit exceeded (' + lineCount + '), forcing clear');
|
|
90
|
+
lineCount = 0;
|
|
91
|
+
output = CURSOR_SAVE + CLEAR_SCROLLBACK + CURSOR_RESTORE + output;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Screen clear detected - piggyback our scrollback clear
|
|
95
|
+
if (str.includes(CLEAR_SCREEN) || str.includes(HOME_CURSOR)) {
|
|
96
|
+
lineCount = 0;
|
|
97
|
+
if (config.clearAfterRenders > 0 && renderCount >= config.clearAfterRenders) {
|
|
98
|
+
if (!isTypingActive()) {
|
|
99
|
+
log('clearing after ' + renderCount + ' renders');
|
|
100
|
+
renderCount = 0;
|
|
101
|
+
output = CLEAR_SCROLLBACK + output;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// /clear command - nuke everything
|
|
107
|
+
if (str.includes('Conversation cleared') || str.includes('Chat cleared')) {
|
|
108
|
+
log('/clear detected');
|
|
109
|
+
lineCount = 0;
|
|
110
|
+
output = CLEAR_SCROLLBACK + output;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return output;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Main
|
|
117
|
+
async function main() {
|
|
118
|
+
if (config.disabled) {
|
|
119
|
+
log('disabled via env');
|
|
120
|
+
}
|
|
42
121
|
|
|
43
|
-
|
|
44
|
-
|
|
122
|
+
const claudePath = findClaude();
|
|
123
|
+
if (!claudePath) {
|
|
124
|
+
console.error('claude not found in PATH');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
log('using claude at: ' + claudePath);
|
|
129
|
+
log('fix enabled, config:', JSON.stringify(config));
|
|
130
|
+
|
|
131
|
+
// Try to use node-pty for proper PTY support
|
|
132
|
+
let pty;
|
|
133
|
+
try {
|
|
134
|
+
pty = require('node-pty');
|
|
135
|
+
} catch (e) {
|
|
136
|
+
// Fall back to basic spawn with pipe
|
|
137
|
+
log('node-pty not available, using basic spawn');
|
|
138
|
+
pty = null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (pty && process.stdin.isTTY) {
|
|
142
|
+
// PTY mode - full terminal emulation
|
|
143
|
+
const term = pty.spawn(claudePath, process.argv.slice(2), {
|
|
144
|
+
name: process.env.TERM || 'xterm-256color',
|
|
145
|
+
cols: process.stdout.columns || 80,
|
|
146
|
+
rows: process.stdout.rows || 24,
|
|
147
|
+
cwd: process.cwd(),
|
|
148
|
+
env: process.env
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Handle resize with debounce
|
|
152
|
+
process.stdout.on('resize', () => {
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
155
|
+
|
|
156
|
+
if (now - lastResizeTime < config.resizeDebounceMs) {
|
|
157
|
+
resizeTimeout = setTimeout(() => {
|
|
158
|
+
log('debounced resize');
|
|
159
|
+
term.resize(process.stdout.columns, process.stdout.rows);
|
|
160
|
+
}, config.resizeDebounceMs);
|
|
161
|
+
} else {
|
|
162
|
+
term.resize(process.stdout.columns, process.stdout.rows);
|
|
163
|
+
}
|
|
164
|
+
lastResizeTime = now;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Track typing
|
|
168
|
+
process.stdin.on('data', (data) => {
|
|
169
|
+
lastTypingTime = Date.now();
|
|
170
|
+
term.write(data);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Process output
|
|
174
|
+
term.onData((data) => {
|
|
175
|
+
const processed = processOutput(data);
|
|
176
|
+
process.stdout.write(processed);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
term.onExit(({ exitCode }) => {
|
|
180
|
+
process.exit(exitCode);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Raw mode for proper terminal handling
|
|
184
|
+
if (process.stdin.setRawMode) {
|
|
185
|
+
process.stdin.setRawMode(true);
|
|
186
|
+
}
|
|
187
|
+
process.stdin.resume();
|
|
188
|
+
|
|
189
|
+
// Periodic clear
|
|
190
|
+
if (config.periodicClearMs > 0) {
|
|
191
|
+
setInterval(() => {
|
|
192
|
+
if (!isTypingActive()) {
|
|
193
|
+
log('periodic clear');
|
|
194
|
+
process.stdout.write(CURSOR_SAVE + CLEAR_SCROLLBACK + CURSOR_RESTORE);
|
|
195
|
+
}
|
|
196
|
+
}, config.periodicClearMs);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
} else {
|
|
200
|
+
// Basic mode - just spawn and pipe (limited fix capability)
|
|
201
|
+
log('basic mode (no PTY)');
|
|
202
|
+
|
|
203
|
+
const child = spawn(claudePath, process.argv.slice(2), {
|
|
204
|
+
stdio: ['inherit', 'pipe', 'inherit'],
|
|
205
|
+
env: process.env
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
child.stdout.on('data', (data) => {
|
|
209
|
+
const processed = processOutput(data);
|
|
210
|
+
process.stdout.write(processed);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
child.on('exit', (code) => {
|
|
214
|
+
process.exit(code || 0);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Track typing via stdin
|
|
218
|
+
process.stdin.on('data', () => {
|
|
219
|
+
lastTypingTime = Date.now();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
main().catch(err => {
|
|
225
|
+
console.error('terminal fix error:', err.message);
|
|
226
|
+
process.exit(1);
|
|
45
227
|
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudescreenfix-hardwicksoftware",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "fixes the scroll glitch in claude code cli - now with GLITCH DETECTION, 120-line limit enforcement, and auto-recovery",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-fixed": "./bin/claude-fixed.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "node -
|
|
10
|
+
"test": "node bin/claude-fixed.js --version",
|
|
11
|
+
"test-lib": "node -e \"const fix = require('./index.cjs'); fix.install(); console.log(fix.getStats());\""
|
|
11
12
|
},
|
|
12
13
|
"keywords": [
|
|
13
14
|
"claude",
|
|
@@ -41,5 +42,8 @@
|
|
|
41
42
|
"bin/",
|
|
42
43
|
"README.md",
|
|
43
44
|
"LICENSE"
|
|
44
|
-
]
|
|
45
|
+
],
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"node-pty": "^1.1.0"
|
|
48
|
+
}
|
|
45
49
|
}
|