claudefix 2.7.1 → 2.7.3

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.
@@ -225,7 +225,7 @@ const TOTAL_MEM_MB = Math.floor(os.totalmem() / 1024 / 1024);
225
225
  const MEM_PERCENT = Math.min(100, Math.max(1,
226
226
  parseInt(process.env.CLAUDEFIX_MEM_PERCENT, 10) || config.memPercent || 35
227
227
  )) / 100;
228
- const MAX_HEAP_MB = Math.floor(TOTAL_MEM_MB * MEM_PERCENT);
228
+ const MAX_HEAP_MB = Math.min(4096, Math.floor(TOTAL_MEM_MB * MEM_PERCENT));
229
229
  const WARN_THRESHOLD_MB = Math.floor(MAX_HEAP_MB * 0.7);
230
230
  const CRITICAL_THRESHOLD_MB = Math.floor(MAX_HEAP_MB * 0.9);
231
231
 
@@ -295,131 +295,56 @@ if (debug) {
295
295
  */
296
296
  function stripColors(data) {
297
297
  let str = data;
298
- const isGtk4 = forceNuclear || terminalType === 'ptyxis' || terminalType === 'gtk4-vte';
299
298
 
300
- if (isGtk4) {
301
- // THERMONUCLEAR MODE for Ptyxis/GTK4 terminals
302
- // Strip ALL styling except basic foreground colors
303
- // This is aggressive but ensures clean rendering
304
-
305
- // First, strip ALL escape sequences
306
- str = str.replace(/\x1b\[[0-9;]*m/g, (match) => {
307
- // Only keep sequences that are JUST foreground colors (30-37, 90-97, 38;5;X, 38;2;R;G;B)
308
- // and reset (0)
309
-
310
- // Extract the codes
311
- const codes = match.slice(2, -1).split(';').filter(c => c !== '');
312
-
313
- if (codes.length === 0) return '\x1b[0m';
314
-
315
- // Filter to only allowed codes
316
- const allowed = [];
317
- let i = 0;
318
- while (i < codes.length) {
319
- const code = parseInt(codes[i], 10);
320
-
321
- // Reset
322
- if (code === 0) {
323
- allowed.push('0');
324
- i++;
325
- }
326
- // Bold (1), but NOT dim (2)
327
- else if (code === 1) {
328
- allowed.push('1');
329
- i++;
330
- }
331
- // Standard foreground (30-37)
332
- else if (code >= 30 && code <= 37) {
333
- allowed.push(codes[i]);
334
- i++;
335
- }
336
- // Default foreground (39)
337
- else if (code === 39) {
338
- allowed.push('39');
339
- i++;
340
- }
341
- // Bright foreground (90-97)
342
- else if (code >= 90 && code <= 97) {
343
- allowed.push(codes[i]);
344
- i++;
345
- }
346
- // 256-color foreground (38;5;X)
347
- else if (code === 38 && codes[i + 1] === '5' && codes[i + 2]) {
348
- allowed.push('38', '5', codes[i + 2]);
349
- i += 3;
350
- }
351
- // True color foreground (38;2;R;G;B)
352
- else if (code === 38 && codes[i + 1] === '2' && codes[i + 4]) {
353
- allowed.push('38', '2', codes[i + 2], codes[i + 3], codes[i + 4]);
354
- i += 5;
355
- }
356
- // Skip backgrounds (40-47, 49, 100-107, 48;5;X, 48;2;R;G;B)
357
- else if (code >= 40 && code <= 49) {
358
- i++;
359
- }
360
- else if (code >= 100 && code <= 107) {
361
- i++;
362
- }
363
- else if (code === 48 && codes[i + 1] === '5') {
364
- i += 3; // Skip 48;5;X
365
- }
366
- else if (code === 48 && codes[i + 1] === '2') {
367
- i += 5; // Skip 48;2;R;G;B
368
- }
369
- // Skip other problematic codes (2=dim, 7=inverse, 8=hidden)
370
- else if (code === 2 || code === 7 || code === 8 || code === 22 || code === 27 || code === 28) {
371
- i++;
372
- }
373
- else {
374
- // Unknown - skip it
375
- i++;
376
- }
299
+ // Universal approach: parse each SGR sequence, keep only safe codes
300
+ // This handles ALL compound sequences correctly (e.g. \x1b[1;38;5;196;48;5;236m)
301
+ // by parsing code-by-code instead of regex pattern matching
302
+ str = str.replace(/\x1b\[[0-9;]*m/g, (match) => {
303
+ const codes = match.slice(2, -1).split(';').filter(c => c !== '');
304
+
305
+ if (codes.length === 0) return '\x1b[0m';
306
+
307
+ const allowed = [];
308
+ let i = 0;
309
+ while (i < codes.length) {
310
+ const code = parseInt(codes[i], 10);
311
+
312
+ // Reset
313
+ if (code === 0) { allowed.push('0'); i++; }
314
+ // Bold (1), italic (3), underline (4), strikethrough (9)
315
+ else if (code === 1 || code === 3 || code === 4 || code === 9) { allowed.push(codes[i]); i++; }
316
+ // Bold off (22), italic off (23), underline off (24), strikethrough off (29)
317
+ else if (code === 22 || code === 23 || code === 24 || code === 29) { allowed.push(codes[i]); i++; }
318
+ // Standard foreground (30-37)
319
+ else if (code >= 30 && code <= 37) { allowed.push(codes[i]); i++; }
320
+ // Default foreground (39)
321
+ else if (code === 39) { allowed.push('39'); i++; }
322
+ // Bright foreground (90-97)
323
+ else if (code >= 90 && code <= 97) { allowed.push(codes[i]); i++; }
324
+ // 256-color foreground (38;5;X)
325
+ else if (code === 38 && codes[i + 1] === '5' && codes[i + 2]) {
326
+ allowed.push('38', '5', codes[i + 2]); i += 3;
377
327
  }
328
+ // True color foreground (38;2;R;G;B)
329
+ else if (code === 38 && codes[i + 1] === '2' && codes[i + 4]) {
330
+ allowed.push('38', '2', codes[i + 2], codes[i + 3], codes[i + 4]); i += 5;
331
+ }
332
+ // Skip ALL backgrounds: 40-47, 49, 100-107
333
+ else if ((code >= 40 && code <= 49) || (code >= 100 && code <= 107)) { i++; }
334
+ // Skip 256-color bg (48;5;X)
335
+ else if (code === 48 && codes[i + 1] === '5') { i += 3; }
336
+ // Skip true color bg (48;2;R;G;B)
337
+ else if (code === 48 && codes[i + 1] === '2') { i += 5; }
338
+ // Skip dim (2), inverse (7), hidden (8) and their offs
339
+ else if (code === 2 || code === 7 || code === 8 || code === 27 || code === 28) { i++; }
340
+ // Unknown - skip
341
+ else { i++; }
342
+ }
378
343
 
379
- if (allowed.length === 0) return '';
380
- return `\x1b[${allowed.join(';')}m`;
381
- });
382
-
383
- return str;
384
- }
385
-
386
- // Regular mode for other terminals
387
- // Remove standalone background color sequences entirely
388
- str = str.replace(/\x1b\[48;5;\d+m/g, ''); // 256-color bg
389
- str = str.replace(/\x1b\[48;2;\d+;\d+;\d+m/g, ''); // true color bg
390
- str = str.replace(/\x1b\[4[0-7]m/g, ''); // standard bg 40-47
391
- str = str.replace(/\x1b\[49m/g, ''); // default bg
392
- str = str.replace(/\x1b\[10[0-7]m/g, ''); // bright bg 100-107
393
- str = str.replace(/\x1b\[7m/g, ''); // inverse
394
- str = str.replace(/\x1b\[27m/g, ''); // inverse off
395
-
396
- // NUCLEAR MODE: Extra aggressive stripping for VTE issues
397
- str = str.replace(/\x1b\[2m/g, ''); // dim text (causes grey)
398
- str = str.replace(/\x1b\[22m/g, ''); // dim off
399
- str = str.replace(/\x1b\[8m/g, ''); // hidden text
400
- str = str.replace(/\x1b\[28m/g, ''); // hidden off
401
-
402
- // Strip any remaining compound sequences with background codes
403
- // Matches patterns like \x1b[0;48;5;236m or \x1b[38;5;196;48;5;236m
404
- str = str.replace(/\x1b\[([0-9;]*?)(;?48;[52];[0-9;]+)(;?[0-9;]*)m/g, (match, before, bg, after) => {
405
- const parts = [before, after].filter(p => p && p.length > 0 && p !== ';');
406
- if (parts.length === 0) return '\x1b[0m';
407
- return `\x1b[${parts.join(';').replace(/^;|;$/g, '').replace(/;;+/g, ';')}m`;
344
+ if (allowed.length === 0) return '';
345
+ return `\x1b[${allowed.join(';')}m`;
408
346
  });
409
347
 
410
- // For combined sequences like \x1b[1;41m (bold + red bg), remove just the bg part
411
- // This regex finds sequences with bg codes and removes just those codes
412
- str = str.replace(/\x1b\[([0-9;]*)(?:;?)(4[0-7]|49|10[0-7]|48;5;\d+|48;2;\d+;\d+;\d+)(?:;?)([0-9;]*)m/g,
413
- (match, before, bg, after) => {
414
- const parts = [before, after].filter(p => p && p.length > 0);
415
- if (parts.length === 0) return '';
416
- return `\x1b[${parts.join(';')}m`;
417
- });
418
-
419
- // Clean up any malformed sequences that might be left
420
- str = str.replace(/\x1b\[;+m/g, '\x1b[0m'); // \x1b[;;m -> \x1b[0m
421
- str = str.replace(/\x1b\[m/g, '\x1b[0m'); // \x1b[m -> \x1b[0m
422
-
423
348
  return str;
424
349
  }
425
350
 
@@ -480,6 +405,15 @@ function getTerminalType() {
480
405
  return 'ptyxis';
481
406
  }
482
407
 
408
+ // XFCE4 Terminal - uses VTE but handles ANSI fine, does NOT need thermonuclear mode
409
+ // Must check BEFORE the VTE version check or it gets misclassified as gtk4-vte
410
+ if (termProgram === 'xfce4-terminal' || termProgram === 'Xfce Terminal' ||
411
+ process.env.XFCE_TERMINAL_VERSION ||
412
+ process.env.WINDOWPATH || // XFCE sets this
413
+ (process.env.XDG_CURRENT_DESKTOP || '').toLowerCase().includes('xfce')) {
414
+ return 'xfce-terminal';
415
+ }
416
+
483
417
  // GTK4 terminals (like Ptyxis) often have GDK_BACKEND set
484
418
  if (gdkBackend === 'wayland' && vteVersion) {
485
419
  return 'gtk4-vte';
@@ -871,15 +805,11 @@ if (!usePTY) {
871
805
  const cols = process.stdout.columns || 80;
872
806
 
873
807
  if (sshMode || !showFooter) {
874
- // SSH mode or no footer: NO scroll region manipulation
875
808
  ptyProcess.resize(cols, rows);
876
809
  } else {
877
- // Local mode with footer: reserve bottom row(s) for footer
878
- // FIX: Handle edge case where terminal is too small
810
+ // Reserve bottom row(s) for footer via scroll region + PTY resize
879
811
  const contentRows = Math.max(1, rows - footerRows);
880
812
 
881
- // FIX: Reset scroll region first, then set new one to avoid clipping
882
- // This prevents content from being cut off during dynamic resize
883
813
  process.stdout.write(
884
814
  '\x1b[r' + // Reset scroll region to full terminal
885
815
  '\x1b7' + // Save cursor position
@@ -887,7 +817,6 @@ if (!usePTY) {
887
817
  '\x1b8' // Restore cursor position
888
818
  );
889
819
 
890
- // Resize PTY to content area (excludes footer row(s))
891
820
  ptyProcess.resize(cols, contentRows);
892
821
  }
893
822
  }
@@ -932,6 +861,13 @@ if (!usePTY) {
932
861
  );
933
862
  }
934
863
 
864
+ // FIX (Linux only): Clear screen once at startup to prevent ghost frames.
865
+ // Ink renders inline (no alternate screen) and re-renders during startup cause
866
+ // content to stack/triplicate. A single clear before first output fixes this.
867
+ // NOTE: Do NOT use alternate screen (\x1b[?1049h) — it makes Ink switch to
868
+ // full-screen mode where it sends \x1b[H\x1b[J which erases the footer.
869
+ let startupCleared = false;
870
+
935
871
  // Initial setup
936
872
  setupScrollRegion();
937
873
  if (!sshMode && showFooter) drawFooter();
@@ -969,53 +905,74 @@ if (!usePTY) {
969
905
  // macOS Terminal.app doesn't have VTE bugs, so skip aggressive escape mangling
970
906
  const isAppleTerminal = process.env.TERM_PROGRAM === 'Apple_Terminal';
971
907
 
972
- ptyProcess.onData((data) => {
973
- // Stop forwarding output once we're cleaning up - prevents Claude's
974
- // dying output from overwriting our exit banner
975
- if (exiting) return;
908
+ // FIX: Buffer-and-flush approach for ghost frame elimination on Linux.
909
+ // Problem: Ink sends renders in multiple small chunks. Injecting \x1b[J on any
910
+ // individual chunk either misses (threshold not met) or nukes partial renders.
911
+ // Solution: Buffer ALL output, flush after a short idle gap (16ms). When flushing,
912
+ // if the buffer contains \x1b[H (home cursor = new render), inject \x1b[J after it.
913
+ // Since the entire render is flushed atomically, clear + content arrive together.
914
+ let outputBuffer = '';
915
+ let flushTimer = null;
916
+ const FLUSH_DELAY_MS = 16; // Normal flush delay (~1 frame)
917
+ const COALESCE_DELAY_MS = 80; // Extended delay during rapid full repaints
918
+ let lastFullRenderTime = 0; // When we last flushed a full repaint
919
+
920
+ function processAndFlush() {
921
+ flushTimer = null;
922
+ if (!outputBuffer || exiting) return;
923
+
924
+ let output = outputBuffer;
925
+ outputBuffer = '';
926
+
976
927
  // Only strip colors if config enabled and env var not disabled
977
- // On macOS: skip color stripping entirely for Apple Terminal (no VTE bugs)
978
928
  const shouldStrip = config.colorStripping &&
979
929
  process.env.CLAUDE_STRIP_BG_COLORS !== '0' &&
980
930
  !isAppleTerminal;
981
- let output = shouldStrip ? stripColors(data) : data;
931
+ if (shouldStrip) output = stripColors(output);
982
932
 
983
- // FIX: Intercept scroll region resets from Ink/Claude output.
984
- // Ink sends \x1b[r which resets scroll region to full terminal,
985
- // causing our footer area to become content area and old content bleeds through.
986
- // Replace with our constrained scroll region.
933
+ // Intercept scroll region resets from Ink — replace with our constrained region
987
934
  if (showFooter && !sshMode) {
988
935
  const cr = contentRows();
989
- // Replace bare scroll region reset with our constrained one
990
936
  output = output.replace(/\x1b\[r/g, `\x1b[1;${cr}r`);
991
- // Also catch explicit full-terminal scroll regions like \x1b[1;24r
992
937
  const fullRows = process.stdout.rows || 24;
993
938
  output = output.replace(new RegExp(`\\x1b\\[1;${fullRows}r`, 'g'), `\x1b[1;${cr}r`);
994
939
  }
995
940
 
996
- // FIX (Linux only): When Ink sends a screen clear (\x1b[2J), inject scrollback
997
- // clear to prevent old content from sticking around in VTE terminals.
998
- // SKIP on macOS: Terminal.app handles \x1b[3J differently and it causes
999
- // visual glitches (screen flashing, content disappearing).
941
+ // FIX (Linux only): Inject scrollback clear on full screen clears
1000
942
  if (!isMac) {
1001
- if (output.includes('\x1b[2J') || output.includes('\x1b[3J')) {
943
+ if (output.includes('\x1b[2J')) {
1002
944
  output = output.replace(/\x1b\[2J/g, '\x1b[2J\x1b[3J');
1003
945
  }
1004
946
  }
1005
947
 
1006
- // FIX (Linux only): Detect Ink's home cursor (\x1b[H or \x1b[1;1H) followed
1007
- // by content - this is a differential re-render. Clear to end of screen after
1008
- // home to prevent stale content below the new render from showing through.
1009
- // SKIP on macOS: Apple Terminal repaints on \x1b[J which causes rapid
1010
- // flickering when combined with Ink's frequent re-renders.
1011
- if (!isMac) {
1012
- if (output.includes('\x1b[H') || output.includes('\x1b[1;1H')) {
1013
- output = output.replace(/(\x1b\[(?:1;1)?H)/, '$1\x1b[J');
1014
- }
948
+ // FIX (Linux only): Clear stale content when Ink does a FULL re-render.
949
+ // Ink sends \x1b[H for both full repaints AND partial updates (just prompt).
950
+ // We must ONLY clear on full repaints or we wipe content Ink didn't re-send.
951
+ // Since we buffer the entire render cycle, we can check the buffer size:
952
+ // - Full repaint: large buffer (most of the screen rewritten)
953
+ // - Partial update (prompt only): small buffer
954
+ // Threshold: at least half the screen worth of content (contentRows * 30 bytes)
955
+ // FIX (Linux only): Clear screen once before first output to prevent
956
+ // startup ghost frames (triplicated content from Ink's initial renders)
957
+ if (!isMac && !startupCleared) {
958
+ startupCleared = true;
959
+ output = '\x1b[2J\x1b[3J\x1b[H' + output;
1015
960
  }
1016
961
 
1017
962
  process.stdout.write(output);
1018
963
  if (showFooter) scheduleFooterDraw();
964
+ }
965
+
966
+ ptyProcess.onData((data) => {
967
+ if (exiting) return;
968
+ // Accumulate into buffer
969
+ outputBuffer += data;
970
+ // Reset flush timer — wait for output burst to finish
971
+ if (flushTimer) clearTimeout(flushTimer);
972
+ // Use longer delay if we recently flushed a full render (coalesce rapid repaints)
973
+ const recentFullRender = (Date.now() - lastFullRenderTime) < 200;
974
+ const delay = recentFullRender ? COALESCE_DELAY_MS : FLUSH_DELAY_MS;
975
+ flushTimer = setTimeout(processAndFlush, delay);
1019
976
  });
1020
977
 
1021
978
  // Forward stdin with Ctrl+Shift+H hotkey
@@ -1080,7 +1037,9 @@ if (!usePTY) {
1080
1037
  clearInterval(memCheckInterval);
1081
1038
  if (gcInterval) clearInterval(gcInterval);
1082
1039
  if (pendingDraw) clearTimeout(pendingDraw);
1083
- // Clean exit: reset scroll region, clear screen, show exit banner
1040
+ if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
1041
+ if (outputBuffer) { process.stdout.write(outputBuffer); outputBuffer = ''; }
1042
+ // Clean exit: leave alternate screen, reset scroll region
1084
1043
  if (!sshMode && showFooter) {
1085
1044
  process.stdout.write(
1086
1045
  '\x1b[r' + // Reset scroll region to full terminal
@@ -1138,6 +1097,8 @@ if (!usePTY) {
1138
1097
  if (cpuLimiter) try { cpuLimiter.kill(); } catch {}
1139
1098
  if (footerInterval) clearInterval(footerInterval);
1140
1099
  if (pendingDraw) clearTimeout(pendingDraw);
1100
+ if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
1101
+ if (outputBuffer) { process.stdout.write(outputBuffer); outputBuffer = ''; }
1141
1102
  if (!sshMode && showFooter) {
1142
1103
  process.stdout.write(
1143
1104
  '\x1b[r' + // Reset scroll region
@@ -18,9 +18,9 @@ const os = require('os');
18
18
  const VERSIONS_DIR = path.join(os.homedir(), '.local/share/claude/versions');
19
19
  const CLAUDEFIX_SCRIPT = '/usr/lib/node_modules/claudefix/bin/claude-fixed.js'; // Absolute path set at install time
20
20
 
21
- // Memory limit: 35% of total RAM
21
+ // Memory limit: 35% of total RAM, capped at 4096MB
22
22
  const TOTAL_MB = Math.floor(os.totalmem() / 1024 / 1024);
23
- const MAX_HEAP = Math.floor(TOTAL_MB * 0.35);
23
+ const MAX_HEAP = Math.min(4096, Math.floor(TOTAL_MB * 0.35));
24
24
 
25
25
  function findClaude() {
26
26
  // 1. Check versions directory (self-updating claude)
@@ -73,15 +73,13 @@ if (!isLinux || disabled) {
73
73
  c.on('exit', code => process.exit(code || 0));
74
74
  } else {
75
75
  // Linux - apply PTY color filter AND memory limits
76
+ // MUST spawn child process — NODE_OPTIONS/require() in same process
77
+ // does NOT apply --max-old-space-size (V8 sets heap at startup)
76
78
  process.env.CLAUDE_REAL_BINARY = bin;
77
- process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS || '') + ' --max-old-space-size=' + MAX_HEAP;
78
- try { require(CLAUDEFIX_SCRIPT); }
79
- catch (e) {
80
- // Fallback: run with memory limits at minimum
81
- const c = spawn(bin, process.argv.slice(2), {
82
- stdio: 'inherit',
83
- env: { ...process.env, NODE_OPTIONS: '--max-old-space-size=' + MAX_HEAP }
84
- });
85
- c.on('exit', code => process.exit(code || 0));
86
- }
79
+ const nodeArgs = ['--max-old-space-size=' + MAX_HEAP, CLAUDEFIX_SCRIPT, ...process.argv.slice(2)];
80
+ const c = spawn(process.execPath, nodeArgs, {
81
+ stdio: 'inherit',
82
+ env: { ...process.env, CLAUDE_REAL_BINARY: bin }
83
+ });
84
+ c.on('exit', code => process.exit(code ?? 0));
87
85
  }
package/index.cjs CHANGED
@@ -105,85 +105,6 @@ const config = {
105
105
  stripColors: process.env.CLAUDE_STRIP_COLORS !== '0', // strip by default, disable with =0
106
106
  };
107
107
 
108
- // ============================================================================
109
- // RESOURCE LIMITER — V8 heap cap, forced GC, CPU limiting
110
- // ============================================================================
111
- const os = require('os');
112
- const fs = require('fs');
113
- const path = require('path');
114
-
115
- function _loadUserConfig() {
116
- try {
117
- const cfgPath = path.join(os.homedir(), '.claudefix.json');
118
- if (fs.existsSync(cfgPath)) return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
119
- } catch (_) {}
120
- return {};
121
- }
122
-
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);
128
-
129
- const _resLimiter = { intervalId: null, gcId: null, cpulimitPid: null };
130
-
131
- function installResourceLimiter() {
132
- if (process.env.CLAUDE_RESOURCE_LIMIT === '0') return;
133
- if (process.platform !== 'linux' && process.platform !== 'darwin') return;
134
-
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
- }
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;
150
- try {
151
- execSync('which cpulimit 2>/dev/null', { stdio: 'pipe' });
152
- const proc = spawn('cpulimit', ['-p', String(pid), '-l', String(cpulimitVal), '-z'], { stdio: 'ignore', detached: true });
153
- proc.unref();
154
- _resLimiter.cpulimitPid = proc.pid;
155
- } catch (_) {
156
- try {
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 (_) {}
160
- }
161
- }
162
-
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();
168
-
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(() => {
173
- try {
174
- const rssMB = Math.round(process.memoryUsage().rss / 1048576);
175
- if (rssMB > critMB && global.gc) global.gc();
176
- } catch (_) {}
177
- }, 30000);
178
- if (_resLimiter.intervalId && _resLimiter.intervalId.unref) _resLimiter.intervalId.unref();
179
- }
180
-
181
- function cleanupResourceLimiter() {
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; }
185
- }
186
-
187
108
  // state tracking
188
109
  let renderCount = 0;
189
110
  let lastResizeTime = 0;
@@ -300,9 +221,6 @@ function install() {
300
221
  return;
301
222
  }
302
223
 
303
- // Resource limiting — V8 heap cap, forced GC, CPU limit
304
- installResourceLimiter();
305
-
306
224
  originalWrite = process.stdout.write.bind(process.stdout);
307
225
 
308
226
  // track stdin to know when user is typing
@@ -447,7 +365,6 @@ function setConfig(key, value) {
447
365
  function disable() {
448
366
  if (originalWrite) {
449
367
  process.stdout.write = originalWrite;
450
- cleanupResourceLimiter();
451
368
  log('disabled');
452
369
  }
453
370
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudefix",
3
- "version": "2.7.1",
3
+ "version": "2.7.3",
4
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": {