claudefix 2.6.1 → 2.7.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 +155 -116
- package/index.cjs +281 -136
- package/package.json +2 -2
package/bin/claude-fixed.js
CHANGED
|
@@ -295,56 +295,131 @@ if (debug) {
|
|
|
295
295
|
*/
|
|
296
296
|
function stripColors(data) {
|
|
297
297
|
let str = data;
|
|
298
|
+
const isGtk4 = forceNuclear || terminalType === 'ptyxis' || terminalType === 'gtk4-vte';
|
|
298
299
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
}
|
|
331
377
|
}
|
|
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
|
-
}
|
|
343
378
|
|
|
344
|
-
|
|
345
|
-
|
|
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`;
|
|
346
408
|
});
|
|
347
409
|
|
|
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
|
+
|
|
348
423
|
return str;
|
|
349
424
|
}
|
|
350
425
|
|
|
@@ -405,15 +480,6 @@ function getTerminalType() {
|
|
|
405
480
|
return 'ptyxis';
|
|
406
481
|
}
|
|
407
482
|
|
|
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
|
-
|
|
417
483
|
// GTK4 terminals (like Ptyxis) often have GDK_BACKEND set
|
|
418
484
|
if (gdkBackend === 'wayland' && vteVersion) {
|
|
419
485
|
return 'gtk4-vte';
|
|
@@ -797,19 +863,23 @@ if (!usePTY) {
|
|
|
797
863
|
}
|
|
798
864
|
}
|
|
799
865
|
|
|
800
|
-
// How many footer rows to reserve (1 for claudefix, +
|
|
801
|
-
const footerRows = specmemActive ?
|
|
866
|
+
// How many footer rows to reserve (1 for claudefix, +2 if specmem active for team comms + status)
|
|
867
|
+
const footerRows = specmemActive ? 3 : 1;
|
|
802
868
|
|
|
803
869
|
function setupScrollRegion() {
|
|
804
870
|
const rows = process.stdout.rows || 24;
|
|
805
871
|
const cols = process.stdout.columns || 80;
|
|
806
872
|
|
|
807
873
|
if (sshMode || !showFooter) {
|
|
874
|
+
// SSH mode or no footer: NO scroll region manipulation
|
|
808
875
|
ptyProcess.resize(cols, rows);
|
|
809
876
|
} else {
|
|
810
|
-
//
|
|
877
|
+
// Local mode with footer: reserve bottom row(s) for footer
|
|
878
|
+
// FIX: Handle edge case where terminal is too small
|
|
811
879
|
const contentRows = Math.max(1, rows - footerRows);
|
|
812
880
|
|
|
881
|
+
// FIX: Reset scroll region first, then set new one to avoid clipping
|
|
882
|
+
// This prevents content from being cut off during dynamic resize
|
|
813
883
|
process.stdout.write(
|
|
814
884
|
'\x1b[r' + // Reset scroll region to full terminal
|
|
815
885
|
'\x1b7' + // Save cursor position
|
|
@@ -817,6 +887,7 @@ if (!usePTY) {
|
|
|
817
887
|
'\x1b8' // Restore cursor position
|
|
818
888
|
);
|
|
819
889
|
|
|
890
|
+
// Resize PTY to content area (excludes footer row(s))
|
|
820
891
|
ptyProcess.resize(cols, contentRows);
|
|
821
892
|
}
|
|
822
893
|
}
|
|
@@ -861,13 +932,6 @@ if (!usePTY) {
|
|
|
861
932
|
);
|
|
862
933
|
}
|
|
863
934
|
|
|
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
|
-
|
|
871
935
|
// Initial setup
|
|
872
936
|
setupScrollRegion();
|
|
873
937
|
if (!sshMode && showFooter) drawFooter();
|
|
@@ -905,74 +969,53 @@ if (!usePTY) {
|
|
|
905
969
|
// macOS Terminal.app doesn't have VTE bugs, so skip aggressive escape mangling
|
|
906
970
|
const isAppleTerminal = process.env.TERM_PROGRAM === 'Apple_Terminal';
|
|
907
971
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
-
|
|
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;
|
|
927
976
|
// Only strip colors if config enabled and env var not disabled
|
|
977
|
+
// On macOS: skip color stripping entirely for Apple Terminal (no VTE bugs)
|
|
928
978
|
const shouldStrip = config.colorStripping &&
|
|
929
979
|
process.env.CLAUDE_STRIP_BG_COLORS !== '0' &&
|
|
930
980
|
!isAppleTerminal;
|
|
931
|
-
|
|
981
|
+
let output = shouldStrip ? stripColors(data) : data;
|
|
932
982
|
|
|
933
|
-
// Intercept scroll region resets from Ink
|
|
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.
|
|
934
987
|
if (showFooter && !sshMode) {
|
|
935
988
|
const cr = contentRows();
|
|
989
|
+
// Replace bare scroll region reset with our constrained one
|
|
936
990
|
output = output.replace(/\x1b\[r/g, `\x1b[1;${cr}r`);
|
|
991
|
+
// Also catch explicit full-terminal scroll regions like \x1b[1;24r
|
|
937
992
|
const fullRows = process.stdout.rows || 24;
|
|
938
993
|
output = output.replace(new RegExp(`\\x1b\\[1;${fullRows}r`, 'g'), `\x1b[1;${cr}r`);
|
|
939
994
|
}
|
|
940
995
|
|
|
941
|
-
// FIX (Linux only):
|
|
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).
|
|
942
1000
|
if (!isMac) {
|
|
943
|
-
if (output.includes('\x1b[2J')) {
|
|
1001
|
+
if (output.includes('\x1b[2J') || output.includes('\x1b[3J')) {
|
|
944
1002
|
output = output.replace(/\x1b\[2J/g, '\x1b[2J\x1b[3J');
|
|
945
1003
|
}
|
|
946
1004
|
}
|
|
947
1005
|
|
|
948
|
-
// FIX (Linux only):
|
|
949
|
-
//
|
|
950
|
-
//
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
if (!isMac && !startupCleared) {
|
|
958
|
-
startupCleared = true;
|
|
959
|
-
output = '\x1b[2J\x1b[3J\x1b[H' + output;
|
|
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
|
+
}
|
|
960
1015
|
}
|
|
961
1016
|
|
|
962
1017
|
process.stdout.write(output);
|
|
963
1018
|
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);
|
|
976
1019
|
});
|
|
977
1020
|
|
|
978
1021
|
// Forward stdin with Ctrl+Shift+H hotkey
|
|
@@ -1037,9 +1080,7 @@ if (!usePTY) {
|
|
|
1037
1080
|
clearInterval(memCheckInterval);
|
|
1038
1081
|
if (gcInterval) clearInterval(gcInterval);
|
|
1039
1082
|
if (pendingDraw) clearTimeout(pendingDraw);
|
|
1040
|
-
|
|
1041
|
-
if (outputBuffer) { process.stdout.write(outputBuffer); outputBuffer = ''; }
|
|
1042
|
-
// Clean exit: leave alternate screen, reset scroll region
|
|
1083
|
+
// Clean exit: reset scroll region, clear screen, show exit banner
|
|
1043
1084
|
if (!sshMode && showFooter) {
|
|
1044
1085
|
process.stdout.write(
|
|
1045
1086
|
'\x1b[r' + // Reset scroll region to full terminal
|
|
@@ -1097,8 +1138,6 @@ if (!usePTY) {
|
|
|
1097
1138
|
if (cpuLimiter) try { cpuLimiter.kill(); } catch {}
|
|
1098
1139
|
if (footerInterval) clearInterval(footerInterval);
|
|
1099
1140
|
if (pendingDraw) clearTimeout(pendingDraw);
|
|
1100
|
-
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
1101
|
-
if (outputBuffer) { process.stdout.write(outputBuffer); outputBuffer = ''; }
|
|
1102
1141
|
if (!sshMode && showFooter) {
|
|
1103
1142
|
process.stdout.write(
|
|
1104
1143
|
'\x1b[r' + // Reset scroll region
|
package/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* claudefix - 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,6 +13,8 @@
|
|
|
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
|
|
16
18
|
*
|
|
17
19
|
* FIXED v1.0.1: typing issue where stdin echo was being intercepted
|
|
18
20
|
* - now detects stdin echo writes and passes them through unmodified
|
|
@@ -23,7 +25,15 @@
|
|
|
23
25
|
* - auto-detects Xvfb/VNC/headless environments
|
|
24
26
|
* - strips BACKGROUND colors that cause VTE rendering glitches
|
|
25
27
|
* - keeps foreground colors and spinners working perfectly
|
|
26
|
-
*
|
|
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
|
|
27
37
|
*/
|
|
28
38
|
|
|
29
39
|
|
|
@@ -78,21 +88,14 @@ function stripCompoundBgCodes(str) {
|
|
|
78
88
|
|
|
79
89
|
// supported terminals - only run fix on these
|
|
80
90
|
const SUPPORTED_TERMINALS = [
|
|
81
|
-
'xterm', 'xterm-256color', '
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
'konsole', 'konsole-256color',
|
|
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)
|
|
88
97
|
];
|
|
89
98
|
|
|
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
|
-
|
|
96
99
|
// config - tweak these if needed
|
|
97
100
|
const config = {
|
|
98
101
|
resizeDebounceMs: 150, // how long to wait before firing resize
|
|
@@ -105,6 +108,159 @@ const config = {
|
|
|
105
108
|
stripColors: process.env.CLAUDE_STRIP_COLORS !== '0', // strip by default, disable with =0
|
|
106
109
|
};
|
|
107
110
|
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// RESOURCE LIMITER — Cap Claude's CPU & RAM usage
|
|
113
|
+
// ============================================================================
|
|
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
|
+
const os = require('os');
|
|
122
|
+
const fs = require('fs');
|
|
123
|
+
const path = require('path');
|
|
124
|
+
|
|
125
|
+
// Load user config from ~/.claudefix.json
|
|
126
|
+
function _loadUserConfig() {
|
|
127
|
+
try {
|
|
128
|
+
const cfgPath = path.join(os.homedir(), '.claudefix.json');
|
|
129
|
+
if (fs.existsSync(cfgPath)) {
|
|
130
|
+
return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
131
|
+
}
|
|
132
|
+
} catch (_) {}
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
|
|
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
|
+
};
|
|
153
|
+
|
|
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);
|
|
159
|
+
|
|
160
|
+
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
|
+
}
|
|
179
|
+
|
|
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;
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
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 + ')');
|
|
194
|
+
} catch (_) {
|
|
195
|
+
// No cpulimit — try cgroup v2
|
|
196
|
+
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
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
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
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- RAM monitoring ---
|
|
230
|
+
resourceConfig._intervalId = setInterval(() => {
|
|
231
|
+
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
|
+
}
|
|
242
|
+
} catch (_) {}
|
|
243
|
+
}, resourceConfig.checkIntervalMs);
|
|
244
|
+
if (resourceConfig._intervalId && resourceConfig._intervalId.unref) {
|
|
245
|
+
resourceConfig._intervalId.unref();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
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
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
108
264
|
// state tracking
|
|
109
265
|
let renderCount = 0;
|
|
110
266
|
let lastResizeTime = 0;
|
|
@@ -117,7 +273,7 @@ let clearIntervalId = null;
|
|
|
117
273
|
|
|
118
274
|
function log(...args) {
|
|
119
275
|
if (config.debug) {
|
|
120
|
-
process.stderr.write('[
|
|
276
|
+
process.stderr.write('[claudefix] ' + args.join(' ') + '\n');
|
|
121
277
|
}
|
|
122
278
|
}
|
|
123
279
|
|
|
@@ -128,85 +284,79 @@ function log(...args) {
|
|
|
128
284
|
* fixes VTE rendering glitches where BG colors overlay text
|
|
129
285
|
*/
|
|
130
286
|
function stripBackgroundColors(chunk) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
let result = chunk;
|
|
287
|
+
let str = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
134
288
|
|
|
135
|
-
// first pass: strip simple
|
|
289
|
+
// first pass: strip simple standalone sequences
|
|
136
290
|
for (const pattern of ANSI_BG_PATTERNS) {
|
|
137
|
-
|
|
291
|
+
str = str.replace(pattern, '');
|
|
138
292
|
}
|
|
139
293
|
|
|
140
|
-
// second pass:
|
|
141
|
-
|
|
294
|
+
// second pass: strip compound sequences
|
|
295
|
+
str = stripCompoundBgCodes(str);
|
|
142
296
|
|
|
143
|
-
|
|
144
|
-
result = result.replace(/\x1b\[;*m/g, '\x1b[0m'); // \x1b[;m -> \x1b[0m
|
|
145
|
-
result = result.replace(/\x1b\[m/g, '\x1b[0m'); // \x1b[m -> \x1b[0m
|
|
146
|
-
|
|
147
|
-
return result;
|
|
297
|
+
return str;
|
|
148
298
|
}
|
|
149
299
|
|
|
150
300
|
/**
|
|
151
|
-
* check if user is
|
|
301
|
+
* check if user is currently typing (within cooldown period)
|
|
152
302
|
*/
|
|
153
303
|
function isTypingActive() {
|
|
154
304
|
return (Date.now() - lastTypingTime) < config.typingCooldownMs;
|
|
155
305
|
}
|
|
156
306
|
|
|
157
307
|
/**
|
|
158
|
-
* detect if
|
|
159
|
-
*
|
|
308
|
+
* detect if a write is an stdin echo (user typing)
|
|
309
|
+
* these are typically single chars or short sequences
|
|
160
310
|
*/
|
|
161
311
|
function isStdinEcho(chunk) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
|
|
178
330
|
return false;
|
|
179
331
|
}
|
|
180
332
|
|
|
181
333
|
/**
|
|
182
|
-
*
|
|
334
|
+
* safely clear scrollback without disrupting display
|
|
335
|
+
* uses save/restore cursor and waits for non-typing moment
|
|
183
336
|
*/
|
|
184
337
|
function safeClearScrollback() {
|
|
338
|
+
if (!originalWrite) return;
|
|
339
|
+
|
|
340
|
+
// don't clear during typing
|
|
185
341
|
if (isTypingActive()) {
|
|
186
|
-
|
|
187
|
-
pendingClear = true;
|
|
188
|
-
log('deferring clear - typing active');
|
|
189
|
-
setTimeout(() => {
|
|
190
|
-
pendingClear = false;
|
|
191
|
-
if (!isTypingActive()) {
|
|
192
|
-
safeClearScrollback();
|
|
193
|
-
}
|
|
194
|
-
}, config.typingCooldownMs);
|
|
195
|
-
}
|
|
342
|
+
pendingClear = true;
|
|
196
343
|
return;
|
|
197
344
|
}
|
|
198
345
|
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
205
355
|
}
|
|
206
356
|
}
|
|
207
357
|
|
|
208
358
|
/**
|
|
209
|
-
*
|
|
359
|
+
* install the fix
|
|
210
360
|
* call this once at startup, calling again is a no-op
|
|
211
361
|
*/
|
|
212
362
|
function install() {
|
|
@@ -215,11 +365,8 @@ function install() {
|
|
|
215
365
|
return;
|
|
216
366
|
}
|
|
217
367
|
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
log('terminal not supported: ' + (process.env.TERM || 'unknown') + ' - skipping install');
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
368
|
+
// Resource limiting — cap CPU & RAM based on user config
|
|
369
|
+
installResourceLimiter();
|
|
223
370
|
|
|
224
371
|
originalWrite = process.stdout.write.bind(process.stdout);
|
|
225
372
|
|
|
@@ -241,117 +388,112 @@ function install() {
|
|
|
241
388
|
return originalWrite(chunk, encoding, callback);
|
|
242
389
|
}
|
|
243
390
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// strip colors that cause VTE rendering glitches
|
|
247
|
-
if (config.stripBgColors) {
|
|
391
|
+
// strip background colors if enabled
|
|
392
|
+
if (config.stripBgColors || config.stripColors) {
|
|
248
393
|
chunk = stripBackgroundColors(chunk);
|
|
249
394
|
}
|
|
395
|
+
}
|
|
250
396
|
|
|
251
|
-
|
|
252
|
-
// but only if not actively typing
|
|
253
|
-
if (chunk.includes(CLEAR_SCREEN) || chunk.includes(HOME_CURSOR)) {
|
|
254
|
-
if (config.clearAfterRenders > 0 && renderCount >= config.clearAfterRenders) {
|
|
255
|
-
if (!isTypingActive()) {
|
|
256
|
-
log('clearing scrollback after ' + renderCount + ' renders');
|
|
257
|
-
renderCount = 0;
|
|
258
|
-
chunk = CLEAR_SCROLLBACK + chunk;
|
|
259
|
-
} else {
|
|
260
|
-
log('skipping render-based clear - typing active');
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
397
|
+
renderCount++;
|
|
264
398
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
chunk = CLEAR_SCROLLBACK + chunk;
|
|
269
|
-
}
|
|
399
|
+
// check if we should clear scrollback
|
|
400
|
+
if (renderCount >= config.clearAfterRenders && !isTypingActive()) {
|
|
401
|
+
setImmediate(safeClearScrollback);
|
|
270
402
|
}
|
|
271
403
|
|
|
272
404
|
return originalWrite(chunk, encoding, callback);
|
|
273
405
|
};
|
|
274
406
|
|
|
275
|
-
//
|
|
276
|
-
|
|
407
|
+
// periodic scrollback clearing
|
|
408
|
+
clearIntervalId = setInterval(() => {
|
|
409
|
+
if (pendingClear || renderCount > 100) {
|
|
410
|
+
setImmediate(safeClearScrollback);
|
|
411
|
+
}
|
|
412
|
+
}, config.periodicClearMs);
|
|
277
413
|
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
clearIntervalId = setInterval(() => {
|
|
282
|
-
log('periodic clear check');
|
|
283
|
-
safeClearScrollback();
|
|
284
|
-
}, config.periodicClearMs);
|
|
414
|
+
// don't let the interval keep the process alive
|
|
415
|
+
if (clearIntervalId && clearIntervalId.unref) {
|
|
416
|
+
clearIntervalId.unref();
|
|
285
417
|
}
|
|
286
418
|
|
|
419
|
+
// install resize debouncing
|
|
420
|
+
installResizeDebounce();
|
|
421
|
+
|
|
287
422
|
installed = true;
|
|
288
|
-
|
|
289
|
-
|
|
423
|
+
log('installed (periodic=' + config.periodicClearMs + 'ms, renders=' + config.clearAfterRenders +
|
|
424
|
+
', heap=' + MAX_HEAP_MB + 'MB, cpu=' + (resourceConfig.cpuPercent || 'unlimited') + ')');
|
|
290
425
|
}
|
|
291
426
|
|
|
427
|
+
/**
|
|
428
|
+
* debounce SIGWINCH events
|
|
429
|
+
* tmux/screen fire these like crazy during resize
|
|
430
|
+
*/
|
|
292
431
|
function installResizeDebounce() {
|
|
293
|
-
const
|
|
294
|
-
let sigwinchHandlers = [];
|
|
432
|
+
const originalListeners = process.listeners('SIGWINCH');
|
|
295
433
|
|
|
296
434
|
function debouncedSigwinch() {
|
|
297
435
|
const now = Date.now();
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
302
|
-
|
|
303
|
-
// if events coming too fast, batch em
|
|
304
|
-
if (timeSince < config.resizeDebounceMs) {
|
|
436
|
+
if (now - lastResizeTime < config.resizeDebounceMs) {
|
|
437
|
+
// too fast, schedule for later
|
|
438
|
+
clearTimeout(resizeTimeout);
|
|
305
439
|
resizeTimeout = setTimeout(() => {
|
|
306
|
-
|
|
307
|
-
|
|
440
|
+
lastResizeTime = Date.now();
|
|
441
|
+
for (const listener of originalListeners) {
|
|
442
|
+
listener();
|
|
443
|
+
}
|
|
308
444
|
}, config.resizeDebounceMs);
|
|
309
|
-
|
|
310
|
-
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
lastResizeTime = now;
|
|
448
|
+
for (const listener of originalListeners) {
|
|
449
|
+
listener();
|
|
311
450
|
}
|
|
312
451
|
}
|
|
313
452
|
|
|
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);
|
|
314
459
|
process.on = function(event, handler) {
|
|
315
460
|
if (event === 'SIGWINCH') {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
originalOn('SIGWINCH', debouncedSigwinch);
|
|
319
|
-
}
|
|
320
|
-
return this;
|
|
461
|
+
originalListeners.push(handler);
|
|
462
|
+
return process;
|
|
321
463
|
}
|
|
322
|
-
return
|
|
464
|
+
return origOn(event, handler);
|
|
323
465
|
};
|
|
324
|
-
|
|
325
|
-
log('resize debounce installed');
|
|
326
466
|
}
|
|
327
467
|
|
|
328
468
|
/**
|
|
329
|
-
* manually clear scrollback
|
|
469
|
+
* manually clear scrollback (e.g., on /clear command)
|
|
330
470
|
*/
|
|
331
471
|
function clearScrollback() {
|
|
332
472
|
if (originalWrite) {
|
|
333
|
-
originalWrite(CLEAR_SCROLLBACK);
|
|
334
|
-
|
|
335
|
-
|
|
473
|
+
originalWrite(CLEAR_SCREEN + HOME_CURSOR + CLEAR_SCROLLBACK);
|
|
474
|
+
renderCount = 0;
|
|
475
|
+
log('scrollback cleared (manual)');
|
|
336
476
|
}
|
|
337
|
-
log('manual scrollback clear');
|
|
338
477
|
}
|
|
339
478
|
|
|
340
479
|
/**
|
|
341
|
-
* get
|
|
480
|
+
* get stats about the fix
|
|
342
481
|
*/
|
|
343
482
|
function getStats() {
|
|
344
483
|
return {
|
|
484
|
+
installed,
|
|
345
485
|
renderCount,
|
|
346
486
|
lastResizeTime,
|
|
347
|
-
|
|
348
|
-
|
|
487
|
+
config: { ...config },
|
|
488
|
+
resourceLimits: {
|
|
489
|
+
maxHeapMB: MAX_HEAP_MB,
|
|
490
|
+
memPercent: resourceConfig.memPercent,
|
|
491
|
+
cpuPercent: resourceConfig.cpuPercent,
|
|
492
|
+
totalMemMB: TOTAL_MEM_MB,
|
|
493
|
+
}
|
|
349
494
|
};
|
|
350
495
|
}
|
|
351
496
|
|
|
352
|
-
/**
|
|
353
|
-
* update config at runtime
|
|
354
|
-
*/
|
|
355
497
|
function setConfig(key, value) {
|
|
356
498
|
if (key in config) {
|
|
357
499
|
config[key] = value;
|
|
@@ -365,6 +507,7 @@ function setConfig(key, value) {
|
|
|
365
507
|
function disable() {
|
|
366
508
|
if (originalWrite) {
|
|
367
509
|
process.stdout.write = originalWrite;
|
|
510
|
+
cleanupResourceLimiter();
|
|
368
511
|
log('disabled');
|
|
369
512
|
}
|
|
370
513
|
}
|
|
@@ -376,5 +519,7 @@ module.exports = {
|
|
|
376
519
|
setConfig,
|
|
377
520
|
disable,
|
|
378
521
|
stripColors: stripBackgroundColors,
|
|
379
|
-
config
|
|
522
|
+
config,
|
|
523
|
+
resourceConfig,
|
|
524
|
+
MAX_HEAP_MB
|
|
380
525
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudefix",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Fixes screen glitching, blocky colors, AND
|
|
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",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-fixed": "bin/claude-fixed.js",
|