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.
Files changed (2) hide show
  1. package/index.cjs +184 -246
  2. package/package.json +2 -2
package/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * claudefix - stops the scroll glitch from cooking your terminal
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', 'screen', 'screen-256color',
92
- 'tmux', 'tmux-256color', 'rxvt', 'rxvt-unicode', 'rxvt-unicode-256color',
93
- 'vt100', 'vt220', 'linux', 'ansi', 'cygwin',
94
- 'alacritty', 'foot', 'foot-extra', 'kitty', 'kitty-direct', 'xterm-kitty',
95
- 'wezterm', 'xterm-ghostty', 'ghostty',
96
- 'dumb' // support dumb terminals too (VNC/headless)
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 — Cap Claude's CPU & RAM usage
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 _userConfig = _loadUserConfig();
137
-
138
- const resourceConfig = {
139
- // Memory: percentage of system RAM for V8 heap
140
- memPercent: parseInt(process.env.CLAUDEFIX_MEM_PERCENT || '', 10) || _userConfig.memPercent || 35,
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
- // Calculate actual limits
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 (!resourceConfig.enabled || process.platform !== 'linux') return;
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
- // --- CPU limiting via cpulimit (if configured and available) ---
181
- if (resourceConfig.cpuPercent > 0) {
182
- const cpuCores = os.cpus().length;
183
- // cpulimit uses percentage per-core, so 50% on 4 cores = 200% cpulimit value
184
- const cpulimitVal = Math.floor(resourceConfig.cpuPercent * cpuCores / 100) * 100 || resourceConfig.cpuPercent;
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 cpulimitProc = spawn('cpulimit', ['-p', String(pid), '-l', String(cpulimitVal), '-z'], {
189
- stdio: 'ignore', detached: true
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 cgroupDir = '/sys/fs/cgroup/claudefix-' + pid;
198
- if (fs.existsSync('/sys/fs/cgroup/cgroup.controllers')) {
199
- fs.mkdirSync(cgroupDir, { recursive: true });
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
- // --- Periodic forced GC ---
217
- resourceConfig._gcIntervalId = setInterval(() => {
218
- try {
219
- if (global.gc) {
220
- global.gc();
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
- // --- RAM monitoring ---
230
- resourceConfig._intervalId = setInterval(() => {
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 mem = process.memoryUsage();
233
- const rssMB = Math.round(mem.rss / 1048576);
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
- }, resourceConfig.checkIntervalMs);
244
- if (resourceConfig._intervalId && resourceConfig._intervalId.unref) {
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 (resourceConfig._intervalId) {
251
- clearInterval(resourceConfig._intervalId);
252
- resourceConfig._intervalId = null;
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('[claudefix] ' + args.join(' ') + '\n');
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
- let str = typeof chunk === 'string' ? chunk : chunk.toString();
210
+ if (typeof chunk !== 'string') return chunk;
288
211
 
289
- // first pass: strip simple standalone sequences
212
+ let result = chunk;
213
+
214
+ // first pass: strip simple bg patterns
290
215
  for (const pattern of ANSI_BG_PATTERNS) {
291
- str = str.replace(pattern, '');
216
+ result = result.replace(pattern, '');
292
217
  }
293
218
 
294
- // second pass: strip compound sequences
295
- str = stripCompoundBgCodes(str);
219
+ // second pass: handle compound sequences like \x1b[0;48;5;236m
220
+ result = stripCompoundBgCodes(result);
296
221
 
297
- return str;
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 currently typing (within cooldown period)
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 a write is an stdin echo (user typing)
309
- * these are typically single chars or short sequences
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
- if (typeof chunk !== 'string') return false;
313
- const len = chunk.length;
314
-
315
- // single printable char = definitely typing
316
- if (len === 1 && chunk.charCodeAt(0) >= 32) return true;
317
-
318
- // newline/carriage return = enter key
319
- if (len === 1 && (chunk === '\n' || chunk === '\r')) return true;
320
-
321
- // backspace sequences
322
- if (chunk === '\b \b' || chunk === '\x7f') return true;
323
-
324
- // short escape sequences (arrow keys, etc)
325
- if (len <= 4 && chunk.startsWith('\x1b[')) return true;
326
-
327
- // tab completion results are usually short
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
- * safely clear scrollback without disrupting display
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 = true;
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
- try {
347
- // save cursor, clear scrollback, restore cursor
348
- // this prevents the "jump to top" glitch
349
- originalWrite(CURSOR_SAVE + CLEAR_SCROLLBACK + CURSOR_RESTORE);
350
- renderCount = 0;
351
- pendingClear = false;
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
- * install the fix
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
- // Resource limiting — cap CPU & RAM based on user config
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
- // strip background colors if enabled
392
- if (config.stripBgColors || config.stripColors) {
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
- renderCount++;
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
- // check if we should clear scrollback
400
- if (renderCount >= config.clearAfterRenders && !isTypingActive()) {
401
- setImmediate(safeClearScrollback);
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
- // periodic scrollback clearing
408
- clearIntervalId = setInterval(() => {
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
- // don't let the interval keep the process alive
415
- if (clearIntervalId && clearIntervalId.unref) {
416
- clearIntervalId.unref();
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
- log('installed (periodic=' + config.periodicClearMs + 'ms, renders=' + config.clearAfterRenders +
424
- ', heap=' + MAX_HEAP_MB + 'MB, cpu=' + (resourceConfig.cpuPercent || 'unlimited') + ')');
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 originalListeners = process.listeners('SIGWINCH');
375
+ const originalOn = process.on.bind(process);
376
+ let sigwinchHandlers = [];
433
377
 
434
378
  function debouncedSigwinch() {
435
379
  const now = Date.now();
436
- if (now - lastResizeTime < config.resizeDebounceMs) {
437
- // too fast, schedule for later
438
- clearTimeout(resizeTimeout);
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
- lastResizeTime = Date.now();
441
- for (const listener of originalListeners) {
442
- listener();
443
- }
388
+ log('firing debounced resize');
389
+ sigwinchHandlers.forEach(h => { try { h(); } catch(e) {} });
444
390
  }, config.resizeDebounceMs);
445
- return;
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
- originalListeners.push(handler);
462
- return process;
398
+ sigwinchHandlers.push(handler);
399
+ if (sigwinchHandlers.length === 1) {
400
+ originalOn('SIGWINCH', debouncedSigwinch);
401
+ }
402
+ return this;
463
403
  }
464
- return origOn(event, handler);
404
+ return originalOn(event, handler);
465
405
  };
406
+
407
+ log('resize debounce installed');
466
408
  }
467
409
 
468
410
  /**
469
- * manually clear scrollback (e.g., on /clear command)
411
+ * manually clear scrollback - call this whenever you want
470
412
  */
471
413
  function clearScrollback() {
472
414
  if (originalWrite) {
473
- originalWrite(CLEAR_SCREEN + HOME_CURSOR + CLEAR_SCROLLBACK);
474
- renderCount = 0;
475
- log('scrollback cleared (manual)');
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 about the fix
423
+ * get current stats for debugging
481
424
  */
482
425
  function getStats() {
483
426
  return {
484
- installed,
485
427
  renderCount,
486
428
  lastResizeTime,
487
- config: { ...config },
488
- resourceLimits: {
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.0",
4
- "description": "Fixes screen glitching, blocky colors, memory leaks AND resource hogging in Claude Code CLI on Linux and macOS. V8 heap capping (--max-old-space-size), forced GC (--expose-gc), CPU limiting (cpulimit/cgroup/nice), RAM monitoring. All configurable via ~/.claudefix.json or env vars. Developed by Hardwick Software Services @ https://justcalljon.pro",
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",