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.
Files changed (2) hide show
  1. package/index.cjs +281 -136
  2. package/package.json +2 -2
package/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * claudescreenfix-hardwicksoftware - stops the scroll glitch from cooking your terminal
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
- * - your Zesting still zests, just no broken color blocks
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', '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',
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('[terminal-fix] ' + args.join(' ') + '\n');
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
- if (typeof chunk !== 'string') return chunk;
132
-
133
- let result = chunk;
287
+ let str = typeof chunk === 'string' ? chunk : chunk.toString();
134
288
 
135
- // first pass: strip simple bg patterns
289
+ // first pass: strip simple standalone sequences
136
290
  for (const pattern of ANSI_BG_PATTERNS) {
137
- result = result.replace(pattern, '');
291
+ str = str.replace(pattern, '');
138
292
  }
139
293
 
140
- // second pass: handle compound sequences like \x1b[0;48;5;236m
141
- result = stripCompoundBgCodes(result);
294
+ // second pass: strip compound sequences
295
+ str = stripCompoundBgCodes(str);
142
296
 
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;
297
+ return str;
148
298
  }
149
299
 
150
300
  /**
151
- * check if user is actively typing (within cooldown window)
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 this looks like a stdin echo (single printable char or short sequence)
159
- * stdin echoes are typically: single chars, backspace sequences, arrow key echoes
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
- // 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
- }
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
- * safe clear - defers if typing active
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
- 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
- }
342
+ pendingClear = true;
196
343
  return;
197
344
  }
198
345
 
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
- });
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
- * installs the fix - hooks into stdout and sigwinch
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
- // only run on supported terminals
219
- if (!isTerminalSupported()) {
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
- renderCount++;
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
- // 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
- }
397
+ renderCount++;
264
398
 
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
- }
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
- // debounce resize events - tmux users know the pain
276
- installResizeDebounce();
407
+ // periodic scrollback clearing
408
+ clearIntervalId = setInterval(() => {
409
+ if (pendingClear || renderCount > 100) {
410
+ setImmediate(safeClearScrollback);
411
+ }
412
+ }, config.periodicClearMs);
277
413
 
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);
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
- const mode = config.stripBgColors ? 'bg+dim colors stripped' : 'all colors preserved';
289
- log('installed successfully - v2.3.1 - ' + mode + ' - TERM=' + process.env.TERM);
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 originalOn = process.on.bind(process);
294
- let sigwinchHandlers = [];
432
+ const originalListeners = process.listeners('SIGWINCH');
295
433
 
296
434
  function debouncedSigwinch() {
297
435
  const now = Date.now();
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) {
436
+ if (now - lastResizeTime < config.resizeDebounceMs) {
437
+ // too fast, schedule for later
438
+ clearTimeout(resizeTimeout);
305
439
  resizeTimeout = setTimeout(() => {
306
- log('firing debounced resize');
307
- sigwinchHandlers.forEach(h => { try { h(); } catch(e) {} });
440
+ lastResizeTime = Date.now();
441
+ for (const listener of originalListeners) {
442
+ listener();
443
+ }
308
444
  }, config.resizeDebounceMs);
309
- } else {
310
- sigwinchHandlers.forEach(h => { try { h(); } catch(e) {} });
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
- sigwinchHandlers.push(handler);
317
- if (sigwinchHandlers.length === 1) {
318
- originalOn('SIGWINCH', debouncedSigwinch);
319
- }
320
- return this;
461
+ originalListeners.push(handler);
462
+ return process;
321
463
  }
322
- return originalOn(event, handler);
464
+ return origOn(event, handler);
323
465
  };
324
-
325
- log('resize debounce installed');
326
466
  }
327
467
 
328
468
  /**
329
- * manually clear scrollback - call this whenever you want
469
+ * manually clear scrollback (e.g., on /clear command)
330
470
  */
331
471
  function clearScrollback() {
332
472
  if (originalWrite) {
333
- originalWrite(CLEAR_SCROLLBACK);
334
- } else {
335
- process.stdout.write(CLEAR_SCROLLBACK);
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 current stats for debugging
480
+ * get stats about the fix
342
481
  */
343
482
  function getStats() {
344
483
  return {
484
+ installed,
345
485
  renderCount,
346
486
  lastResizeTime,
347
- installed,
348
- config
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.6.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",
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",