claudefix 2.6.2 → 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/index.cjs +281 -136
- package/package.json +2 -2
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",
|