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.
Files changed (2) hide show
  1. package/bin/claude-fixed.js +209 -27
  2. package/package.json +7 -3
@@ -2,44 +2,226 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * wrapper script - runs claude with the terminal fix loaded
5
+ * PTY wrapper for Claude Code terminal fix
6
6
  *
7
- * finds your claude binary and runs it with our fix injected
8
- * you don't need any manual setup, just run claude-fixed instead of claude
9
- * it'll handle the rest
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, execSync } = require('child_process');
12
+ const { spawn } = require('child_process');
13
13
  const path = require('path');
14
14
  const fs = require('fs');
15
15
 
16
- // find the loader path - it's in the parent dir
17
- const loaderPath = path.join(__dirname, '..', 'loader.cjs');
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
- if (!fs.existsSync(loaderPath)) {
20
- console.error('loader not found at ' + loaderPath);
21
- process.exit(1);
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
- // find claude binary
25
- let claudeBin;
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
- // run claude with our fix loaded via NODE_OPTIONS - it's the cleanest way
34
- const env = Object.assign({}, process.env, {
35
- NODE_OPTIONS: '--require ' + loaderPath + ' ' + (process.env.NODE_OPTIONS || '')
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 child = spawn(claudeBin, process.argv.slice(2), {
39
- stdio: 'inherit',
40
- env: env
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
- child.on('exit', (code) => {
44
- process.exit(code || 0);
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.0.0",
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 -e \"const fix = require('./index.cjs'); fix.install(); console.log(fix.getStats());\""
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
  }