claudefix 2.7.0 → 2.7.1
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/index.cjs +184 -246
- package/package.json +2 -2
package/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* claudescreenfix-hardwicksoftware - stops the scroll glitch from cooking your terminal
|
|
5
5
|
*
|
|
6
6
|
* the problem:
|
|
7
7
|
* claude code uses ink (react for terminals) and it dont clear scrollback
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
* - hook stdout.write to inject scrollback clears periodically
|
|
14
14
|
* - debounce SIGWINCH so resize aint thrashing
|
|
15
15
|
* - enhance /clear to actually clear scrollback not just the screen
|
|
16
|
-
* - RESOURCE LIMITING: cap CPU + RAM via cpulimit/cgroup/nice + V8 flags
|
|
17
|
-
* - GC FORCING: periodic garbage collection to fight V8 heap bloat
|
|
18
16
|
*
|
|
19
17
|
* FIXED v1.0.1: typing issue where stdin echo was being intercepted
|
|
20
18
|
* - now detects stdin echo writes and passes them through unmodified
|
|
@@ -25,15 +23,7 @@
|
|
|
25
23
|
* - auto-detects Xvfb/VNC/headless environments
|
|
26
24
|
* - strips BACKGROUND colors that cause VTE rendering glitches
|
|
27
25
|
* - keeps foreground colors and spinners working perfectly
|
|
28
|
-
*
|
|
29
|
-
* NEW v2.7.0: resource limiting
|
|
30
|
-
* - reads ~/.claudefix.json for memPercent/cpuPercent settings
|
|
31
|
-
* - applies --max-old-space-size to child processes via NODE_OPTIONS
|
|
32
|
-
* - cpulimit/cgroup/renice for CPU capping
|
|
33
|
-
* - periodic forced GC via --expose-gc
|
|
34
|
-
* - RAM monitoring with threshold warnings
|
|
35
|
-
*
|
|
36
|
-
* Developed by Hardwick Software Services - https://justcalljon.pro
|
|
26
|
+
* - your Zesting still zests, just no broken color blocks
|
|
37
27
|
*/
|
|
38
28
|
|
|
39
29
|
|
|
@@ -88,14 +78,21 @@ function stripCompoundBgCodes(str) {
|
|
|
88
78
|
|
|
89
79
|
// supported terminals - only run fix on these
|
|
90
80
|
const SUPPORTED_TERMINALS = [
|
|
91
|
-
'xterm', 'xterm-256color', '
|
|
92
|
-
'
|
|
93
|
-
'
|
|
94
|
-
'
|
|
95
|
-
'
|
|
96
|
-
'
|
|
81
|
+
'xterm', 'xterm-256color', 'xterm-color',
|
|
82
|
+
'screen', 'screen-256color',
|
|
83
|
+
'tmux', 'tmux-256color',
|
|
84
|
+
'linux', 'vt100', 'vt220',
|
|
85
|
+
'rxvt', 'rxvt-unicode', 'rxvt-unicode-256color',
|
|
86
|
+
'gnome', 'gnome-256color',
|
|
87
|
+
'konsole', 'konsole-256color',
|
|
97
88
|
];
|
|
98
89
|
|
|
90
|
+
function isTerminalSupported() {
|
|
91
|
+
const term = process.env.TERM || '';
|
|
92
|
+
// check exact match or prefix match
|
|
93
|
+
return SUPPORTED_TERMINALS.some(t => term === t || term.startsWith(t + '-'));
|
|
94
|
+
}
|
|
95
|
+
|
|
99
96
|
// config - tweak these if needed
|
|
100
97
|
const config = {
|
|
101
98
|
resizeDebounceMs: 150, // how long to wait before firing resize
|
|
@@ -109,156 +106,82 @@ const config = {
|
|
|
109
106
|
};
|
|
110
107
|
|
|
111
108
|
// ============================================================================
|
|
112
|
-
// RESOURCE LIMITER —
|
|
109
|
+
// RESOURCE LIMITER — V8 heap cap, forced GC, CPU limiting
|
|
113
110
|
// ============================================================================
|
|
114
|
-
// Reads from ~/.claudefix.json (memPercent, cpuPercent) or env vars:
|
|
115
|
-
// CLAUDE_MAX_CPU=50 → max 50% CPU
|
|
116
|
-
// CLAUDE_MAX_RAM=4096 → max 4GB RSS in MB
|
|
117
|
-
// CLAUDEFIX_MEM_PERCENT=35 → 35% of system RAM for V8 heap
|
|
118
|
-
// CLAUDEFIX_CPU_PERCENT=80 → 80% CPU cap
|
|
119
|
-
// CLAUDE_RESOURCE_LIMIT=0 → disable resource limiting
|
|
120
|
-
|
|
121
111
|
const os = require('os');
|
|
122
112
|
const fs = require('fs');
|
|
123
113
|
const path = require('path');
|
|
124
114
|
|
|
125
|
-
// Load user config from ~/.claudefix.json
|
|
126
115
|
function _loadUserConfig() {
|
|
127
116
|
try {
|
|
128
117
|
const cfgPath = path.join(os.homedir(), '.claudefix.json');
|
|
129
|
-
if (fs.existsSync(cfgPath))
|
|
130
|
-
return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
131
|
-
}
|
|
118
|
+
if (fs.existsSync(cfgPath)) return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
132
119
|
} catch (_) {}
|
|
133
120
|
return {};
|
|
134
121
|
}
|
|
135
122
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
memEnabled: _userConfig.memoryLimit !== false && process.env.CLAUDE_RESOURCE_LIMIT !== '0',
|
|
142
|
-
// CPU: percentage cap (0 = no limit)
|
|
143
|
-
cpuPercent: parseInt(process.env.CLAUDEFIX_CPU_PERCENT || process.env.CLAUDE_MAX_CPU || '', 10) || _userConfig.cpuPercent || 0,
|
|
144
|
-
// RAM monitoring
|
|
145
|
-
maxRamMB: parseInt(process.env.CLAUDE_MAX_RAM || '', 10) || 0, // 0 = auto from memPercent
|
|
146
|
-
checkIntervalMs: 30000,
|
|
147
|
-
gcIntervalMs: 60000,
|
|
148
|
-
enabled: process.env.CLAUDE_RESOURCE_LIMIT !== '0',
|
|
149
|
-
_intervalId: null,
|
|
150
|
-
_gcIntervalId: null,
|
|
151
|
-
_cpulimitPid: null,
|
|
152
|
-
};
|
|
123
|
+
const _userCfg = _loadUserConfig();
|
|
124
|
+
const _totalMemMB = Math.floor(os.totalmem() / 1048576);
|
|
125
|
+
const _memPct = parseInt(process.env.CLAUDEFIX_MEM_PERCENT || '', 10) || _userCfg.memPercent || 35;
|
|
126
|
+
const _cpuPct = parseInt(process.env.CLAUDEFIX_CPU_PERCENT || process.env.CLAUDE_MAX_CPU || '', 10) || _userCfg.cpuPercent || 0;
|
|
127
|
+
const MAX_HEAP_MB = parseInt(process.env.CLAUDE_MAX_RAM || '', 10) || Math.floor(_totalMemMB * Math.min(100, Math.max(1, _memPct)) / 100);
|
|
153
128
|
|
|
154
|
-
|
|
155
|
-
const TOTAL_MEM_MB = Math.floor(os.totalmem() / 1048576);
|
|
156
|
-
const MAX_HEAP_MB = resourceConfig.maxRamMB || Math.floor(TOTAL_MEM_MB * Math.min(100, Math.max(1, resourceConfig.memPercent)) / 100);
|
|
157
|
-
const WARN_THRESHOLD_MB = Math.floor(MAX_HEAP_MB * 0.7);
|
|
158
|
-
const CRITICAL_THRESHOLD_MB = Math.floor(MAX_HEAP_MB * 0.9);
|
|
129
|
+
const _resLimiter = { intervalId: null, gcId: null, cpulimitPid: null };
|
|
159
130
|
|
|
160
131
|
function installResourceLimiter() {
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
const { execSync, spawn } = require('child_process');
|
|
164
|
-
const pid = process.pid;
|
|
165
|
-
|
|
166
|
-
// --- Set NODE_OPTIONS for V8 heap limit + GC exposure ---
|
|
167
|
-
// This affects the CURRENT process and any child processes
|
|
168
|
-
if (resourceConfig.memEnabled) {
|
|
169
|
-
const existingOpts = process.env.NODE_OPTIONS || '';
|
|
170
|
-
if (!existingOpts.includes('--max-old-space-size')) {
|
|
171
|
-
process.env.NODE_OPTIONS = (existingOpts + ' --max-old-space-size=' + MAX_HEAP_MB).trim();
|
|
172
|
-
log('resource: NODE_OPTIONS set --max-old-space-size=' + MAX_HEAP_MB + 'MB (' + resourceConfig.memPercent + '% of ' + TOTAL_MEM_MB + 'MB)');
|
|
173
|
-
}
|
|
174
|
-
if (!existingOpts.includes('--expose-gc')) {
|
|
175
|
-
process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS + ' --expose-gc').trim();
|
|
176
|
-
log('resource: NODE_OPTIONS set --expose-gc');
|
|
177
|
-
}
|
|
178
|
-
}
|
|
132
|
+
if (process.env.CLAUDE_RESOURCE_LIMIT === '0') return;
|
|
133
|
+
if (process.platform !== 'linux' && process.platform !== 'darwin') return;
|
|
179
134
|
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
135
|
+
// V8 heap cap + expose GC for child processes
|
|
136
|
+
const opts = process.env.NODE_OPTIONS || '';
|
|
137
|
+
if (!opts.includes('--max-old-space-size')) {
|
|
138
|
+
process.env.NODE_OPTIONS = (opts + ' --max-old-space-size=' + MAX_HEAP_MB).trim();
|
|
139
|
+
}
|
|
140
|
+
if (!opts.includes('--expose-gc')) {
|
|
141
|
+
process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS + ' --expose-gc').trim();
|
|
142
|
+
}
|
|
185
143
|
|
|
144
|
+
// CPU limiting (only if configured > 0)
|
|
145
|
+
if (_cpuPct > 0 && process.platform === 'linux') {
|
|
146
|
+
const { execSync, spawn } = require('child_process');
|
|
147
|
+
const pid = process.pid;
|
|
148
|
+
const cores = os.cpus().length;
|
|
149
|
+
const cpulimitVal = Math.floor(_cpuPct * cores / 100) * 100 || _cpuPct;
|
|
186
150
|
try {
|
|
187
151
|
execSync('which cpulimit 2>/dev/null', { stdio: 'pipe' });
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
cpulimitProc.unref();
|
|
192
|
-
resourceConfig._cpulimitPid = cpulimitProc.pid;
|
|
193
|
-
log('resource: cpulimit attached (pid=' + pid + ', limit=' + resourceConfig.cpuPercent + '%, cpulimit=' + cpulimitVal + ')');
|
|
152
|
+
const proc = spawn('cpulimit', ['-p', String(pid), '-l', String(cpulimitVal), '-z'], { stdio: 'ignore', detached: true });
|
|
153
|
+
proc.unref();
|
|
154
|
+
_resLimiter.cpulimitPid = proc.pid;
|
|
194
155
|
} catch (_) {
|
|
195
|
-
// No cpulimit — try cgroup v2
|
|
196
156
|
try {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const quota = resourceConfig.cpuPercent * 1000;
|
|
201
|
-
fs.writeFileSync(cgroupDir + '/cpu.max', quota + ' 100000');
|
|
202
|
-
fs.writeFileSync(cgroupDir + '/cgroup.procs', String(pid));
|
|
203
|
-
log('resource: cgroup v2 attached (cpu=' + resourceConfig.cpuPercent + '%)');
|
|
204
|
-
}
|
|
205
|
-
} catch (_cgErr) {
|
|
206
|
-
// Fallback: renice
|
|
207
|
-
try {
|
|
208
|
-
const niceVal = Math.max(0, Math.min(19, Math.floor(19 * (1 - resourceConfig.cpuPercent / 100))));
|
|
209
|
-
execSync('renice ' + niceVal + ' -p ' + pid + ' 2>/dev/null', { stdio: 'pipe' });
|
|
210
|
-
log('resource: renice applied (nice=' + niceVal + ')');
|
|
211
|
-
} catch (_) {}
|
|
212
|
-
}
|
|
157
|
+
const niceVal = Math.max(0, Math.min(19, Math.floor(19 * (1 - _cpuPct / 100))));
|
|
158
|
+
execSync('renice ' + niceVal + ' -p ' + process.pid + ' 2>/dev/null', { stdio: 'pipe' });
|
|
159
|
+
} catch (_) {}
|
|
213
160
|
}
|
|
214
161
|
}
|
|
215
162
|
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
log('resource: forced GC');
|
|
222
|
-
}
|
|
223
|
-
} catch (_) {}
|
|
224
|
-
}, resourceConfig.gcIntervalMs);
|
|
225
|
-
if (resourceConfig._gcIntervalId && resourceConfig._gcIntervalId.unref) {
|
|
226
|
-
resourceConfig._gcIntervalId.unref();
|
|
227
|
-
}
|
|
163
|
+
// Periodic forced GC (every 60s)
|
|
164
|
+
_resLimiter.gcId = setInterval(() => {
|
|
165
|
+
try { if (global.gc) global.gc(); } catch (_) {}
|
|
166
|
+
}, 60000);
|
|
167
|
+
if (_resLimiter.gcId && _resLimiter.gcId.unref) _resLimiter.gcId.unref();
|
|
228
168
|
|
|
229
|
-
//
|
|
230
|
-
|
|
169
|
+
// RAM monitoring (every 30s)
|
|
170
|
+
const warnMB = Math.floor(MAX_HEAP_MB * 0.7);
|
|
171
|
+
const critMB = Math.floor(MAX_HEAP_MB * 0.9);
|
|
172
|
+
_resLimiter.intervalId = setInterval(() => {
|
|
231
173
|
try {
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
const heapMB = Math.round(mem.heapUsed / 1048576);
|
|
235
|
-
|
|
236
|
-
if (rssMB > CRITICAL_THRESHOLD_MB) {
|
|
237
|
-
log('resource: CRITICAL RAM ' + rssMB + 'MB (heap=' + heapMB + 'MB) exceeds ' + CRITICAL_THRESHOLD_MB + 'MB');
|
|
238
|
-
if (global.gc) global.gc();
|
|
239
|
-
} else if (rssMB > WARN_THRESHOLD_MB) {
|
|
240
|
-
log('resource: WARNING RAM ' + rssMB + 'MB approaching limit ' + MAX_HEAP_MB + 'MB');
|
|
241
|
-
}
|
|
174
|
+
const rssMB = Math.round(process.memoryUsage().rss / 1048576);
|
|
175
|
+
if (rssMB > critMB && global.gc) global.gc();
|
|
242
176
|
} catch (_) {}
|
|
243
|
-
},
|
|
244
|
-
if (
|
|
245
|
-
resourceConfig._intervalId.unref();
|
|
246
|
-
}
|
|
177
|
+
}, 30000);
|
|
178
|
+
if (_resLimiter.intervalId && _resLimiter.intervalId.unref) _resLimiter.intervalId.unref();
|
|
247
179
|
}
|
|
248
180
|
|
|
249
181
|
function cleanupResourceLimiter() {
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
if (resourceConfig._gcIntervalId) {
|
|
255
|
-
clearInterval(resourceConfig._gcIntervalId);
|
|
256
|
-
resourceConfig._gcIntervalId = null;
|
|
257
|
-
}
|
|
258
|
-
if (resourceConfig._cpulimitPid) {
|
|
259
|
-
try { process.kill(resourceConfig._cpulimitPid); } catch (_) {}
|
|
260
|
-
resourceConfig._cpulimitPid = null;
|
|
261
|
-
}
|
|
182
|
+
if (_resLimiter.intervalId) { clearInterval(_resLimiter.intervalId); _resLimiter.intervalId = null; }
|
|
183
|
+
if (_resLimiter.gcId) { clearInterval(_resLimiter.gcId); _resLimiter.gcId = null; }
|
|
184
|
+
if (_resLimiter.cpulimitPid) { try { process.kill(_resLimiter.cpulimitPid); } catch (_) {} _resLimiter.cpulimitPid = null; }
|
|
262
185
|
}
|
|
263
186
|
|
|
264
187
|
// state tracking
|
|
@@ -273,7 +196,7 @@ let clearIntervalId = null;
|
|
|
273
196
|
|
|
274
197
|
function log(...args) {
|
|
275
198
|
if (config.debug) {
|
|
276
|
-
process.stderr.write('[
|
|
199
|
+
process.stderr.write('[terminal-fix] ' + args.join(' ') + '\n');
|
|
277
200
|
}
|
|
278
201
|
}
|
|
279
202
|
|
|
@@ -284,79 +207,85 @@ function log(...args) {
|
|
|
284
207
|
* fixes VTE rendering glitches where BG colors overlay text
|
|
285
208
|
*/
|
|
286
209
|
function stripBackgroundColors(chunk) {
|
|
287
|
-
|
|
210
|
+
if (typeof chunk !== 'string') return chunk;
|
|
288
211
|
|
|
289
|
-
|
|
212
|
+
let result = chunk;
|
|
213
|
+
|
|
214
|
+
// first pass: strip simple bg patterns
|
|
290
215
|
for (const pattern of ANSI_BG_PATTERNS) {
|
|
291
|
-
|
|
216
|
+
result = result.replace(pattern, '');
|
|
292
217
|
}
|
|
293
218
|
|
|
294
|
-
// second pass:
|
|
295
|
-
|
|
219
|
+
// second pass: handle compound sequences like \x1b[0;48;5;236m
|
|
220
|
+
result = stripCompoundBgCodes(result);
|
|
296
221
|
|
|
297
|
-
|
|
222
|
+
// cleanup: remove empty/malformed sequences
|
|
223
|
+
result = result.replace(/\x1b\[;*m/g, '\x1b[0m'); // \x1b[;m -> \x1b[0m
|
|
224
|
+
result = result.replace(/\x1b\[m/g, '\x1b[0m'); // \x1b[m -> \x1b[0m
|
|
225
|
+
|
|
226
|
+
return result;
|
|
298
227
|
}
|
|
299
228
|
|
|
300
229
|
/**
|
|
301
|
-
* check if user is
|
|
230
|
+
* check if user is actively typing (within cooldown window)
|
|
302
231
|
*/
|
|
303
232
|
function isTypingActive() {
|
|
304
233
|
return (Date.now() - lastTypingTime) < config.typingCooldownMs;
|
|
305
234
|
}
|
|
306
235
|
|
|
307
236
|
/**
|
|
308
|
-
* detect if
|
|
309
|
-
*
|
|
237
|
+
* detect if this looks like a stdin echo (single printable char or short sequence)
|
|
238
|
+
* stdin echoes are typically: single chars, backspace sequences, arrow key echoes
|
|
310
239
|
*/
|
|
311
240
|
function isStdinEcho(chunk) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (len <= 20 && !chunk.includes('\n') && !chunk.includes('\x1b[')) return true;
|
|
329
|
-
|
|
241
|
+
// single printable character (including space)
|
|
242
|
+
if (chunk.length === 1 && chunk.charCodeAt(0) >= 32 && chunk.charCodeAt(0) <= 126) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
// backspace/delete echo (usually 1-3 chars with control codes)
|
|
246
|
+
if (chunk.length <= 4 && (chunk.includes('\b') || chunk.includes('\x7f'))) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
// arrow key echo or cursor movement (short escape sequences)
|
|
250
|
+
if (chunk.length <= 6 && chunk.startsWith('\x1b[') && !chunk.includes('J') && !chunk.includes('H')) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
// enter/newline
|
|
254
|
+
if (chunk === '\n' || chunk === '\r' || chunk === '\r\n') {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
330
257
|
return false;
|
|
331
258
|
}
|
|
332
259
|
|
|
333
260
|
/**
|
|
334
|
-
*
|
|
335
|
-
* uses save/restore cursor and waits for non-typing moment
|
|
261
|
+
* safe clear - defers if typing active
|
|
336
262
|
*/
|
|
337
263
|
function safeClearScrollback() {
|
|
338
|
-
if (!originalWrite) return;
|
|
339
|
-
|
|
340
|
-
// don't clear during typing
|
|
341
264
|
if (isTypingActive()) {
|
|
342
|
-
pendingClear
|
|
265
|
+
if (!pendingClear) {
|
|
266
|
+
pendingClear = true;
|
|
267
|
+
log('deferring clear - typing active');
|
|
268
|
+
setTimeout(() => {
|
|
269
|
+
pendingClear = false;
|
|
270
|
+
if (!isTypingActive()) {
|
|
271
|
+
safeClearScrollback();
|
|
272
|
+
}
|
|
273
|
+
}, config.typingCooldownMs);
|
|
274
|
+
}
|
|
343
275
|
return;
|
|
344
276
|
}
|
|
345
277
|
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
log('scrollback cleared (periodic)');
|
|
353
|
-
} catch (e) {
|
|
354
|
-
// stdout might be destroyed
|
|
278
|
+
if (originalWrite && process.stdout.isTTY) {
|
|
279
|
+
// use setImmediate to not block the event loop
|
|
280
|
+
setImmediate(() => {
|
|
281
|
+
log('executing deferred scrollback clear');
|
|
282
|
+
originalWrite(CURSOR_SAVE + CLEAR_SCROLLBACK + CURSOR_RESTORE);
|
|
283
|
+
});
|
|
355
284
|
}
|
|
356
285
|
}
|
|
357
286
|
|
|
358
287
|
/**
|
|
359
|
-
*
|
|
288
|
+
* installs the fix - hooks into stdout and sigwinch
|
|
360
289
|
* call this once at startup, calling again is a no-op
|
|
361
290
|
*/
|
|
362
291
|
function install() {
|
|
@@ -365,7 +294,13 @@ function install() {
|
|
|
365
294
|
return;
|
|
366
295
|
}
|
|
367
296
|
|
|
368
|
-
//
|
|
297
|
+
// only run on supported terminals
|
|
298
|
+
if (!isTerminalSupported()) {
|
|
299
|
+
log('terminal not supported: ' + (process.env.TERM || 'unknown') + ' - skipping install');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Resource limiting — V8 heap cap, forced GC, CPU limit
|
|
369
304
|
installResourceLimiter();
|
|
370
305
|
|
|
371
306
|
originalWrite = process.stdout.write.bind(process.stdout);
|
|
@@ -388,112 +323,117 @@ function install() {
|
|
|
388
323
|
return originalWrite(chunk, encoding, callback);
|
|
389
324
|
}
|
|
390
325
|
|
|
391
|
-
|
|
392
|
-
|
|
326
|
+
renderCount++;
|
|
327
|
+
|
|
328
|
+
// strip colors that cause VTE rendering glitches
|
|
329
|
+
if (config.stripBgColors) {
|
|
393
330
|
chunk = stripBackgroundColors(chunk);
|
|
394
331
|
}
|
|
395
|
-
}
|
|
396
332
|
|
|
397
|
-
|
|
333
|
+
// ink clears screen before re-render, we piggyback on that
|
|
334
|
+
// but only if not actively typing
|
|
335
|
+
if (chunk.includes(CLEAR_SCREEN) || chunk.includes(HOME_CURSOR)) {
|
|
336
|
+
if (config.clearAfterRenders > 0 && renderCount >= config.clearAfterRenders) {
|
|
337
|
+
if (!isTypingActive()) {
|
|
338
|
+
log('clearing scrollback after ' + renderCount + ' renders');
|
|
339
|
+
renderCount = 0;
|
|
340
|
+
chunk = CLEAR_SCROLLBACK + chunk;
|
|
341
|
+
} else {
|
|
342
|
+
log('skipping render-based clear - typing active');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
398
346
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
347
|
+
// /clear command should actually clear everything (immediate, user-requested)
|
|
348
|
+
if (chunk.includes('Conversation cleared') || chunk.includes('Chat cleared')) {
|
|
349
|
+
log('/clear detected, nuking scrollback');
|
|
350
|
+
chunk = CLEAR_SCROLLBACK + chunk;
|
|
351
|
+
}
|
|
402
352
|
}
|
|
403
353
|
|
|
404
354
|
return originalWrite(chunk, encoding, callback);
|
|
405
355
|
};
|
|
406
356
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
if (pendingClear || renderCount > 100) {
|
|
410
|
-
setImmediate(safeClearScrollback);
|
|
411
|
-
}
|
|
412
|
-
}, config.periodicClearMs);
|
|
357
|
+
// debounce resize events - tmux users know the pain
|
|
358
|
+
installResizeDebounce();
|
|
413
359
|
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
|
|
360
|
+
// periodic cleanup so long sessions dont get cooked
|
|
361
|
+
// uses safeClearScrollback which respects typing activity
|
|
362
|
+
if (config.periodicClearMs > 0) {
|
|
363
|
+
clearIntervalId = setInterval(() => {
|
|
364
|
+
log('periodic clear check');
|
|
365
|
+
safeClearScrollback();
|
|
366
|
+
}, config.periodicClearMs);
|
|
417
367
|
}
|
|
418
368
|
|
|
419
|
-
// install resize debouncing
|
|
420
|
-
installResizeDebounce();
|
|
421
|
-
|
|
422
369
|
installed = true;
|
|
423
|
-
|
|
424
|
-
|
|
370
|
+
const mode = config.stripBgColors ? 'bg+dim colors stripped' : 'all colors preserved';
|
|
371
|
+
log('installed successfully - v2.3.1 - ' + mode + ' - TERM=' + process.env.TERM);
|
|
425
372
|
}
|
|
426
373
|
|
|
427
|
-
/**
|
|
428
|
-
* debounce SIGWINCH events
|
|
429
|
-
* tmux/screen fire these like crazy during resize
|
|
430
|
-
*/
|
|
431
374
|
function installResizeDebounce() {
|
|
432
|
-
const
|
|
375
|
+
const originalOn = process.on.bind(process);
|
|
376
|
+
let sigwinchHandlers = [];
|
|
433
377
|
|
|
434
378
|
function debouncedSigwinch() {
|
|
435
379
|
const now = Date.now();
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
380
|
+
const timeSince = now - lastResizeTime;
|
|
381
|
+
lastResizeTime = now;
|
|
382
|
+
|
|
383
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
384
|
+
|
|
385
|
+
// if events coming too fast, batch em
|
|
386
|
+
if (timeSince < config.resizeDebounceMs) {
|
|
439
387
|
resizeTimeout = setTimeout(() => {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
listener();
|
|
443
|
-
}
|
|
388
|
+
log('firing debounced resize');
|
|
389
|
+
sigwinchHandlers.forEach(h => { try { h(); } catch(e) {} });
|
|
444
390
|
}, config.resizeDebounceMs);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
lastResizeTime = now;
|
|
448
|
-
for (const listener of originalListeners) {
|
|
449
|
-
listener();
|
|
391
|
+
} else {
|
|
392
|
+
sigwinchHandlers.forEach(h => { try { h(); } catch(e) {} });
|
|
450
393
|
}
|
|
451
394
|
}
|
|
452
395
|
|
|
453
|
-
// replace SIGWINCH handler
|
|
454
|
-
process.removeAllListeners('SIGWINCH');
|
|
455
|
-
process.on('SIGWINCH', debouncedSigwinch);
|
|
456
|
-
|
|
457
|
-
// intercept future .on('SIGWINCH') calls
|
|
458
|
-
const origOn = process.on.bind(process);
|
|
459
396
|
process.on = function(event, handler) {
|
|
460
397
|
if (event === 'SIGWINCH') {
|
|
461
|
-
|
|
462
|
-
|
|
398
|
+
sigwinchHandlers.push(handler);
|
|
399
|
+
if (sigwinchHandlers.length === 1) {
|
|
400
|
+
originalOn('SIGWINCH', debouncedSigwinch);
|
|
401
|
+
}
|
|
402
|
+
return this;
|
|
463
403
|
}
|
|
464
|
-
return
|
|
404
|
+
return originalOn(event, handler);
|
|
465
405
|
};
|
|
406
|
+
|
|
407
|
+
log('resize debounce installed');
|
|
466
408
|
}
|
|
467
409
|
|
|
468
410
|
/**
|
|
469
|
-
* manually clear scrollback
|
|
411
|
+
* manually clear scrollback - call this whenever you want
|
|
470
412
|
*/
|
|
471
413
|
function clearScrollback() {
|
|
472
414
|
if (originalWrite) {
|
|
473
|
-
originalWrite(
|
|
474
|
-
|
|
475
|
-
|
|
415
|
+
originalWrite(CLEAR_SCROLLBACK);
|
|
416
|
+
} else {
|
|
417
|
+
process.stdout.write(CLEAR_SCROLLBACK);
|
|
476
418
|
}
|
|
419
|
+
log('manual scrollback clear');
|
|
477
420
|
}
|
|
478
421
|
|
|
479
422
|
/**
|
|
480
|
-
* get stats
|
|
423
|
+
* get current stats for debugging
|
|
481
424
|
*/
|
|
482
425
|
function getStats() {
|
|
483
426
|
return {
|
|
484
|
-
installed,
|
|
485
427
|
renderCount,
|
|
486
428
|
lastResizeTime,
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
maxHeapMB: MAX_HEAP_MB,
|
|
490
|
-
memPercent: resourceConfig.memPercent,
|
|
491
|
-
cpuPercent: resourceConfig.cpuPercent,
|
|
492
|
-
totalMemMB: TOTAL_MEM_MB,
|
|
493
|
-
}
|
|
429
|
+
installed,
|
|
430
|
+
config
|
|
494
431
|
};
|
|
495
432
|
}
|
|
496
433
|
|
|
434
|
+
/**
|
|
435
|
+
* update config at runtime
|
|
436
|
+
*/
|
|
497
437
|
function setConfig(key, value) {
|
|
498
438
|
if (key in config) {
|
|
499
439
|
config[key] = value;
|
|
@@ -519,7 +459,5 @@ module.exports = {
|
|
|
519
459
|
setConfig,
|
|
520
460
|
disable,
|
|
521
461
|
stripColors: stripBackgroundColors,
|
|
522
|
-
config
|
|
523
|
-
resourceConfig,
|
|
524
|
-
MAX_HEAP_MB
|
|
462
|
+
config
|
|
525
463
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudefix",
|
|
3
|
-
"version": "2.7.
|
|
4
|
-
"description": "Fixes screen glitching, blocky colors,
|
|
3
|
+
"version": "2.7.1",
|
|
4
|
+
"description": "Fixes screen glitching, blocky colors, AND MEMORY LEAKS in Claude Code CLI on Linux and macOS. All features optional via env vars. Shows config options on install. Developed by Hardwick Software Services @ https://justcalljon.pro",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-fixed": "bin/claude-fixed.js",
|