claudefix 2.7.0 → 2.7.2
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 +114 -153
- package/index.cjs +136 -281
- package/package.json +2 -2
package/bin/claude-fixed.js
CHANGED
|
@@ -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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
380
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
931
|
+
if (shouldStrip) output = stripColors(output);
|
|
982
932
|
|
|
983
|
-
//
|
|
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):
|
|
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')
|
|
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):
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
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
|
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
|
|
@@ -108,159 +105,6 @@ const config = {
|
|
|
108
105
|
stripColors: process.env.CLAUDE_STRIP_COLORS !== '0', // strip by default, disable with =0
|
|
109
106
|
};
|
|
110
107
|
|
|
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
|
-
|
|
264
108
|
// state tracking
|
|
265
109
|
let renderCount = 0;
|
|
266
110
|
let lastResizeTime = 0;
|
|
@@ -273,7 +117,7 @@ let clearIntervalId = null;
|
|
|
273
117
|
|
|
274
118
|
function log(...args) {
|
|
275
119
|
if (config.debug) {
|
|
276
|
-
process.stderr.write('[
|
|
120
|
+
process.stderr.write('[terminal-fix] ' + args.join(' ') + '\n');
|
|
277
121
|
}
|
|
278
122
|
}
|
|
279
123
|
|
|
@@ -284,79 +128,85 @@ function log(...args) {
|
|
|
284
128
|
* fixes VTE rendering glitches where BG colors overlay text
|
|
285
129
|
*/
|
|
286
130
|
function stripBackgroundColors(chunk) {
|
|
287
|
-
|
|
131
|
+
if (typeof chunk !== 'string') return chunk;
|
|
132
|
+
|
|
133
|
+
let result = chunk;
|
|
288
134
|
|
|
289
|
-
// first pass: strip simple
|
|
135
|
+
// first pass: strip simple bg patterns
|
|
290
136
|
for (const pattern of ANSI_BG_PATTERNS) {
|
|
291
|
-
|
|
137
|
+
result = result.replace(pattern, '');
|
|
292
138
|
}
|
|
293
139
|
|
|
294
|
-
// second pass:
|
|
295
|
-
|
|
140
|
+
// second pass: handle compound sequences like \x1b[0;48;5;236m
|
|
141
|
+
result = stripCompoundBgCodes(result);
|
|
296
142
|
|
|
297
|
-
|
|
143
|
+
// cleanup: remove empty/malformed sequences
|
|
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;
|
|
298
148
|
}
|
|
299
149
|
|
|
300
150
|
/**
|
|
301
|
-
* check if user is
|
|
151
|
+
* check if user is actively typing (within cooldown window)
|
|
302
152
|
*/
|
|
303
153
|
function isTypingActive() {
|
|
304
154
|
return (Date.now() - lastTypingTime) < config.typingCooldownMs;
|
|
305
155
|
}
|
|
306
156
|
|
|
307
157
|
/**
|
|
308
|
-
* detect if
|
|
309
|
-
*
|
|
158
|
+
* detect if this looks like a stdin echo (single printable char or short sequence)
|
|
159
|
+
* stdin echoes are typically: single chars, backspace sequences, arrow key echoes
|
|
310
160
|
*/
|
|
311
161
|
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
|
-
|
|
162
|
+
// single printable character (including space)
|
|
163
|
+
if (chunk.length === 1 && chunk.charCodeAt(0) >= 32 && chunk.charCodeAt(0) <= 126) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
// backspace/delete echo (usually 1-3 chars with control codes)
|
|
167
|
+
if (chunk.length <= 4 && (chunk.includes('\b') || chunk.includes('\x7f'))) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
// arrow key echo or cursor movement (short escape sequences)
|
|
171
|
+
if (chunk.length <= 6 && chunk.startsWith('\x1b[') && !chunk.includes('J') && !chunk.includes('H')) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
// enter/newline
|
|
175
|
+
if (chunk === '\n' || chunk === '\r' || chunk === '\r\n') {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
330
178
|
return false;
|
|
331
179
|
}
|
|
332
180
|
|
|
333
181
|
/**
|
|
334
|
-
*
|
|
335
|
-
* uses save/restore cursor and waits for non-typing moment
|
|
182
|
+
* safe clear - defers if typing active
|
|
336
183
|
*/
|
|
337
184
|
function safeClearScrollback() {
|
|
338
|
-
if (!originalWrite) return;
|
|
339
|
-
|
|
340
|
-
// don't clear during typing
|
|
341
185
|
if (isTypingActive()) {
|
|
342
|
-
pendingClear
|
|
186
|
+
if (!pendingClear) {
|
|
187
|
+
pendingClear = true;
|
|
188
|
+
log('deferring clear - typing active');
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
pendingClear = false;
|
|
191
|
+
if (!isTypingActive()) {
|
|
192
|
+
safeClearScrollback();
|
|
193
|
+
}
|
|
194
|
+
}, config.typingCooldownMs);
|
|
195
|
+
}
|
|
343
196
|
return;
|
|
344
197
|
}
|
|
345
198
|
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
log('scrollback cleared (periodic)');
|
|
353
|
-
} catch (e) {
|
|
354
|
-
// stdout might be destroyed
|
|
199
|
+
if (originalWrite && process.stdout.isTTY) {
|
|
200
|
+
// use setImmediate to not block the event loop
|
|
201
|
+
setImmediate(() => {
|
|
202
|
+
log('executing deferred scrollback clear');
|
|
203
|
+
originalWrite(CURSOR_SAVE + CLEAR_SCROLLBACK + CURSOR_RESTORE);
|
|
204
|
+
});
|
|
355
205
|
}
|
|
356
206
|
}
|
|
357
207
|
|
|
358
208
|
/**
|
|
359
|
-
*
|
|
209
|
+
* installs the fix - hooks into stdout and sigwinch
|
|
360
210
|
* call this once at startup, calling again is a no-op
|
|
361
211
|
*/
|
|
362
212
|
function install() {
|
|
@@ -365,8 +215,11 @@ function install() {
|
|
|
365
215
|
return;
|
|
366
216
|
}
|
|
367
217
|
|
|
368
|
-
//
|
|
369
|
-
|
|
218
|
+
// only run on supported terminals
|
|
219
|
+
if (!isTerminalSupported()) {
|
|
220
|
+
log('terminal not supported: ' + (process.env.TERM || 'unknown') + ' - skipping install');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
370
223
|
|
|
371
224
|
originalWrite = process.stdout.write.bind(process.stdout);
|
|
372
225
|
|
|
@@ -388,112 +241,117 @@ function install() {
|
|
|
388
241
|
return originalWrite(chunk, encoding, callback);
|
|
389
242
|
}
|
|
390
243
|
|
|
391
|
-
|
|
392
|
-
|
|
244
|
+
renderCount++;
|
|
245
|
+
|
|
246
|
+
// strip colors that cause VTE rendering glitches
|
|
247
|
+
if (config.stripBgColors) {
|
|
393
248
|
chunk = stripBackgroundColors(chunk);
|
|
394
249
|
}
|
|
395
|
-
}
|
|
396
250
|
|
|
397
|
-
|
|
251
|
+
// ink clears screen before re-render, we piggyback on that
|
|
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
|
+
}
|
|
398
264
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
265
|
+
// /clear command should actually clear everything (immediate, user-requested)
|
|
266
|
+
if (chunk.includes('Conversation cleared') || chunk.includes('Chat cleared')) {
|
|
267
|
+
log('/clear detected, nuking scrollback');
|
|
268
|
+
chunk = CLEAR_SCROLLBACK + chunk;
|
|
269
|
+
}
|
|
402
270
|
}
|
|
403
271
|
|
|
404
272
|
return originalWrite(chunk, encoding, callback);
|
|
405
273
|
};
|
|
406
274
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
if (pendingClear || renderCount > 100) {
|
|
410
|
-
setImmediate(safeClearScrollback);
|
|
411
|
-
}
|
|
412
|
-
}, config.periodicClearMs);
|
|
275
|
+
// debounce resize events - tmux users know the pain
|
|
276
|
+
installResizeDebounce();
|
|
413
277
|
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
|
|
278
|
+
// periodic cleanup so long sessions dont get cooked
|
|
279
|
+
// uses safeClearScrollback which respects typing activity
|
|
280
|
+
if (config.periodicClearMs > 0) {
|
|
281
|
+
clearIntervalId = setInterval(() => {
|
|
282
|
+
log('periodic clear check');
|
|
283
|
+
safeClearScrollback();
|
|
284
|
+
}, config.periodicClearMs);
|
|
417
285
|
}
|
|
418
286
|
|
|
419
|
-
// install resize debouncing
|
|
420
|
-
installResizeDebounce();
|
|
421
|
-
|
|
422
287
|
installed = true;
|
|
423
|
-
|
|
424
|
-
|
|
288
|
+
const mode = config.stripBgColors ? 'bg+dim colors stripped' : 'all colors preserved';
|
|
289
|
+
log('installed successfully - v2.3.1 - ' + mode + ' - TERM=' + process.env.TERM);
|
|
425
290
|
}
|
|
426
291
|
|
|
427
|
-
/**
|
|
428
|
-
* debounce SIGWINCH events
|
|
429
|
-
* tmux/screen fire these like crazy during resize
|
|
430
|
-
*/
|
|
431
292
|
function installResizeDebounce() {
|
|
432
|
-
const
|
|
293
|
+
const originalOn = process.on.bind(process);
|
|
294
|
+
let sigwinchHandlers = [];
|
|
433
295
|
|
|
434
296
|
function debouncedSigwinch() {
|
|
435
297
|
const now = Date.now();
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
298
|
+
const timeSince = now - lastResizeTime;
|
|
299
|
+
lastResizeTime = now;
|
|
300
|
+
|
|
301
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
302
|
+
|
|
303
|
+
// if events coming too fast, batch em
|
|
304
|
+
if (timeSince < config.resizeDebounceMs) {
|
|
439
305
|
resizeTimeout = setTimeout(() => {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
listener();
|
|
443
|
-
}
|
|
306
|
+
log('firing debounced resize');
|
|
307
|
+
sigwinchHandlers.forEach(h => { try { h(); } catch(e) {} });
|
|
444
308
|
}, config.resizeDebounceMs);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
lastResizeTime = now;
|
|
448
|
-
for (const listener of originalListeners) {
|
|
449
|
-
listener();
|
|
309
|
+
} else {
|
|
310
|
+
sigwinchHandlers.forEach(h => { try { h(); } catch(e) {} });
|
|
450
311
|
}
|
|
451
312
|
}
|
|
452
313
|
|
|
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
314
|
process.on = function(event, handler) {
|
|
460
315
|
if (event === 'SIGWINCH') {
|
|
461
|
-
|
|
462
|
-
|
|
316
|
+
sigwinchHandlers.push(handler);
|
|
317
|
+
if (sigwinchHandlers.length === 1) {
|
|
318
|
+
originalOn('SIGWINCH', debouncedSigwinch);
|
|
319
|
+
}
|
|
320
|
+
return this;
|
|
463
321
|
}
|
|
464
|
-
return
|
|
322
|
+
return originalOn(event, handler);
|
|
465
323
|
};
|
|
324
|
+
|
|
325
|
+
log('resize debounce installed');
|
|
466
326
|
}
|
|
467
327
|
|
|
468
328
|
/**
|
|
469
|
-
* manually clear scrollback
|
|
329
|
+
* manually clear scrollback - call this whenever you want
|
|
470
330
|
*/
|
|
471
331
|
function clearScrollback() {
|
|
472
332
|
if (originalWrite) {
|
|
473
|
-
originalWrite(
|
|
474
|
-
|
|
475
|
-
|
|
333
|
+
originalWrite(CLEAR_SCROLLBACK);
|
|
334
|
+
} else {
|
|
335
|
+
process.stdout.write(CLEAR_SCROLLBACK);
|
|
476
336
|
}
|
|
337
|
+
log('manual scrollback clear');
|
|
477
338
|
}
|
|
478
339
|
|
|
479
340
|
/**
|
|
480
|
-
* get stats
|
|
341
|
+
* get current stats for debugging
|
|
481
342
|
*/
|
|
482
343
|
function getStats() {
|
|
483
344
|
return {
|
|
484
|
-
installed,
|
|
485
345
|
renderCount,
|
|
486
346
|
lastResizeTime,
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
maxHeapMB: MAX_HEAP_MB,
|
|
490
|
-
memPercent: resourceConfig.memPercent,
|
|
491
|
-
cpuPercent: resourceConfig.cpuPercent,
|
|
492
|
-
totalMemMB: TOTAL_MEM_MB,
|
|
493
|
-
}
|
|
347
|
+
installed,
|
|
348
|
+
config
|
|
494
349
|
};
|
|
495
350
|
}
|
|
496
351
|
|
|
352
|
+
/**
|
|
353
|
+
* update config at runtime
|
|
354
|
+
*/
|
|
497
355
|
function setConfig(key, value) {
|
|
498
356
|
if (key in config) {
|
|
499
357
|
config[key] = value;
|
|
@@ -507,7 +365,6 @@ function setConfig(key, value) {
|
|
|
507
365
|
function disable() {
|
|
508
366
|
if (originalWrite) {
|
|
509
367
|
process.stdout.write = originalWrite;
|
|
510
|
-
cleanupResourceLimiter();
|
|
511
368
|
log('disabled');
|
|
512
369
|
}
|
|
513
370
|
}
|
|
@@ -519,7 +376,5 @@ module.exports = {
|
|
|
519
376
|
setConfig,
|
|
520
377
|
disable,
|
|
521
378
|
stripColors: stripBackgroundColors,
|
|
522
|
-
config
|
|
523
|
-
resourceConfig,
|
|
524
|
-
MAX_HEAP_MB
|
|
379
|
+
config
|
|
525
380
|
};
|
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.2",
|
|
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",
|