claudescreenfix-hardwicksoftware 2.3.0 → 2.4.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 +143 -22
- package/install-hook.cjs +66 -13
- package/package.json +2 -2
package/bin/claude-fixed.js
CHANGED
|
@@ -2,25 +2,60 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* wrapper
|
|
5
|
+
* claude-fixed wrapper - runs Claude in a PTY and filters background colors
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Claude is a compiled binary that needs a TTY for its Ink-based TUI.
|
|
8
|
+
* We spawn it in a pseudo-terminal (PTY) and filter its output in real-time,
|
|
9
|
+
* stripping background colors that cause VTE rendering glitches on Xvfb/VNC.
|
|
10
|
+
*
|
|
11
|
+
* Just run `claude-fixed` instead of `claude`
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
|
-
const {
|
|
14
|
+
const { execSync } = require('child_process');
|
|
12
15
|
const path = require('path');
|
|
13
|
-
const fs = require('fs');
|
|
14
16
|
|
|
15
|
-
//
|
|
16
|
-
const
|
|
17
|
+
// ANSI background color patterns that cause VTE glitches on Xvfb/VNC
|
|
18
|
+
const ANSI_BG_PATTERNS = [
|
|
19
|
+
/\x1b\[48;5;\d+m/g, // 256-color background
|
|
20
|
+
/\x1b\[48;2;\d+;\d+;\d+m/g, // true color background (RGB)
|
|
21
|
+
/\x1b\[4[0-7]m/g, // standard background colors 40-47
|
|
22
|
+
/\x1b\[10[0-7]m/g, // bright background colors 100-107
|
|
23
|
+
/\x1b\[7m/g, // inverse video (swaps FG/BG)
|
|
24
|
+
/\x1b\[27m/g, // inverse off
|
|
25
|
+
/\x1b\[49m/g, // default background
|
|
26
|
+
];
|
|
17
27
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
function stripBackgroundColors(data) {
|
|
29
|
+
let str = data;
|
|
30
|
+
for (const pattern of ANSI_BG_PATTERNS) {
|
|
31
|
+
str = str.replace(pattern, '');
|
|
32
|
+
}
|
|
33
|
+
return str;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if we should strip colors (headless/VNC environment)
|
|
37
|
+
function isHeadless() {
|
|
38
|
+
if (process.env.CLAUDE_HEADLESS_MODE === '1') return true;
|
|
39
|
+
if (process.env.CLAUDE_HEADLESS_MODE === '0') return false;
|
|
40
|
+
|
|
41
|
+
const display = process.env.DISPLAY;
|
|
42
|
+
if (!display) return true;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const xdpyinfo = execSync(`xdpyinfo -display ${display} 2>/dev/null`, { encoding: 'utf8', timeout: 2000 });
|
|
46
|
+
if (xdpyinfo.toLowerCase().includes('xvfb') || xdpyinfo.toLowerCase().includes('virtual')) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const vnc = execSync(`pgrep -a "x11vnc|Xvnc|vncserver" 2>/dev/null || true`, { encoding: 'utf8', timeout: 2000 });
|
|
50
|
+
if (vnc.includes(display) || vnc.includes('x11vnc')) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {}
|
|
54
|
+
|
|
55
|
+
return false;
|
|
21
56
|
}
|
|
22
57
|
|
|
23
|
-
//
|
|
58
|
+
// Find claude binary
|
|
24
59
|
let claudeBin;
|
|
25
60
|
try {
|
|
26
61
|
claudeBin = execSync('which claude', { encoding: 'utf8' }).trim();
|
|
@@ -29,16 +64,102 @@ try {
|
|
|
29
64
|
process.exit(1);
|
|
30
65
|
}
|
|
31
66
|
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
67
|
+
const headless = isHeadless();
|
|
68
|
+
const debug = process.env.CLAUDE_TERMINAL_FIX_DEBUG === '1';
|
|
69
|
+
|
|
70
|
+
if (debug) {
|
|
71
|
+
console.error('[claude-fixed] headless mode:', headless);
|
|
72
|
+
console.error('[claude-fixed] claude binary:', claudeBin);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!headless) {
|
|
76
|
+
// Not headless, just exec claude directly (replaces this process)
|
|
77
|
+
const { spawn } = require('child_process');
|
|
78
|
+
const child = spawn(claudeBin, process.argv.slice(2), {
|
|
79
|
+
stdio: 'inherit'
|
|
80
|
+
});
|
|
81
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
82
|
+
} else {
|
|
83
|
+
// Headless - use PTY to preserve TTY behavior while filtering output
|
|
84
|
+
let pty;
|
|
85
|
+
try {
|
|
86
|
+
// Try to find node-pty (might be in different locations)
|
|
87
|
+
const possiblePaths = [
|
|
88
|
+
'node-pty',
|
|
89
|
+
path.join('/usr/lib/node_modules/specmem-hardwicksoftware/node_modules/node-pty'),
|
|
90
|
+
];
|
|
36
91
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
92
|
+
for (const p of possiblePaths) {
|
|
93
|
+
try {
|
|
94
|
+
pty = require(p);
|
|
95
|
+
if (debug) console.error('[claude-fixed] loaded node-pty from:', p);
|
|
96
|
+
break;
|
|
97
|
+
} catch (e) {}
|
|
98
|
+
}
|
|
41
99
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
100
|
+
if (!pty) throw new Error('node-pty not found');
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error('[claude-fixed] node-pty not available, falling back to direct spawn');
|
|
103
|
+
console.error('[claude-fixed] Install with: npm install -g node-pty');
|
|
104
|
+
// Fallback to direct spawn (may have TTY issues but better than nothing)
|
|
105
|
+
const { spawn } = require('child_process');
|
|
106
|
+
const child = spawn(claudeBin, process.argv.slice(2), {
|
|
107
|
+
stdio: 'inherit'
|
|
108
|
+
});
|
|
109
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Get terminal size
|
|
114
|
+
const cols = process.stdout.columns || 80;
|
|
115
|
+
const rows = process.stdout.rows || 24;
|
|
116
|
+
|
|
117
|
+
if (debug) {
|
|
118
|
+
console.error('[claude-fixed] terminal size:', cols, 'x', rows);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Spawn Claude in a PTY
|
|
122
|
+
const ptyProcess = pty.spawn(claudeBin, process.argv.slice(2), {
|
|
123
|
+
name: 'xterm-256color',
|
|
124
|
+
cols: cols,
|
|
125
|
+
rows: rows,
|
|
126
|
+
cwd: process.cwd(),
|
|
127
|
+
env: process.env
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Filter PTY output and write to real stdout
|
|
131
|
+
ptyProcess.onData((data) => {
|
|
132
|
+
const filtered = stripBackgroundColors(data);
|
|
133
|
+
process.stdout.write(filtered);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Forward stdin to PTY
|
|
137
|
+
if (process.stdin.isTTY) {
|
|
138
|
+
process.stdin.setRawMode(true);
|
|
139
|
+
}
|
|
140
|
+
process.stdin.resume();
|
|
141
|
+
process.stdin.on('data', (data) => {
|
|
142
|
+
ptyProcess.write(data);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Handle terminal resize
|
|
146
|
+
process.stdout.on('resize', () => {
|
|
147
|
+
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Handle exit
|
|
151
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
152
|
+
if (process.stdin.isTTY) {
|
|
153
|
+
process.stdin.setRawMode(false);
|
|
154
|
+
}
|
|
155
|
+
process.exit(exitCode);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Handle signals
|
|
159
|
+
process.on('SIGINT', () => ptyProcess.kill('SIGINT'));
|
|
160
|
+
process.on('SIGTERM', () => ptyProcess.kill('SIGTERM'));
|
|
161
|
+
process.on('SIGHUP', () => ptyProcess.kill('SIGHUP'));
|
|
162
|
+
process.on('SIGWINCH', () => {
|
|
163
|
+
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
164
|
+
});
|
|
165
|
+
}
|
package/install-hook.cjs
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Auto-install Claude hook on npm install
|
|
4
|
+
* - Creates hook file in ~/.claude/hooks/
|
|
5
|
+
* - Registers hook in ~/.claude/settings.json (without clobbering existing settings)
|
|
4
6
|
*/
|
|
5
7
|
const fs = require('fs');
|
|
6
8
|
const path = require('path');
|
|
7
9
|
const os = require('os');
|
|
8
10
|
|
|
9
11
|
const HOOK_NAME = 'screenfix-loader.js';
|
|
10
|
-
const
|
|
12
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
13
|
+
const CLAUDE_HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
|
|
14
|
+
const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
11
15
|
|
|
12
16
|
const HOOK_CONTENT = `#!/usr/bin/env node
|
|
13
17
|
/**
|
|
14
18
|
* CLAUDE SCREENFIX LOADER HOOK
|
|
15
19
|
* ============================
|
|
16
|
-
*
|
|
20
|
+
*
|
|
17
21
|
* SessionStart hook that loads claudescreenfix-hardwicksoftware
|
|
18
22
|
* to fix VTE rendering glitches on headless/VNC displays.
|
|
19
|
-
*
|
|
23
|
+
*
|
|
20
24
|
* Auto-installed by: npm install claudescreenfix-hardwicksoftware
|
|
21
|
-
*
|
|
25
|
+
*
|
|
22
26
|
* Hook Event: SessionStart
|
|
23
27
|
*/
|
|
24
28
|
|
|
@@ -41,30 +45,79 @@ try {
|
|
|
41
45
|
}
|
|
42
46
|
`;
|
|
43
47
|
|
|
48
|
+
// The hook config to add to settings.json
|
|
49
|
+
const HOOK_CONFIG = {
|
|
50
|
+
hooks: [{
|
|
51
|
+
type: 'command',
|
|
52
|
+
command: 'node ' + path.join(CLAUDE_HOOKS_DIR, HOOK_NAME),
|
|
53
|
+
timeout: 5
|
|
54
|
+
}]
|
|
55
|
+
};
|
|
56
|
+
|
|
44
57
|
function install() {
|
|
45
58
|
try {
|
|
46
|
-
// Create
|
|
59
|
+
// Create dirs if needed
|
|
60
|
+
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
61
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
62
|
+
}
|
|
47
63
|
if (!fs.existsSync(CLAUDE_HOOKS_DIR)) {
|
|
48
64
|
fs.mkdirSync(CLAUDE_HOOKS_DIR, { recursive: true });
|
|
49
65
|
console.log('[screenfix] Created ' + CLAUDE_HOOKS_DIR);
|
|
50
66
|
}
|
|
51
67
|
|
|
68
|
+
// Write the hook file
|
|
52
69
|
const hookPath = path.join(CLAUDE_HOOKS_DIR, HOOK_NAME);
|
|
53
|
-
|
|
54
|
-
// Check if hook already exists
|
|
55
|
-
if (fs.existsSync(hookPath)) {
|
|
56
|
-
console.log('[screenfix] Hook already exists at ' + hookPath);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Write the hook
|
|
61
70
|
fs.writeFileSync(hookPath, HOOK_CONTENT, { mode: 0o755 });
|
|
62
71
|
console.log('[screenfix] Installed hook to ' + hookPath);
|
|
72
|
+
|
|
73
|
+
// Register in settings.json
|
|
74
|
+
registerHook();
|
|
75
|
+
|
|
63
76
|
console.log('[screenfix] Restart Claude Code to activate headless mode fix');
|
|
64
77
|
} catch (e) {
|
|
65
78
|
console.error('[screenfix] Failed to install hook:', e.message);
|
|
66
79
|
}
|
|
67
80
|
}
|
|
68
81
|
|
|
82
|
+
function registerHook() {
|
|
83
|
+
let settings = {};
|
|
84
|
+
|
|
85
|
+
// Read existing settings
|
|
86
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
87
|
+
try {
|
|
88
|
+
settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.log('[screenfix] Could not parse settings.json, creating new');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Ensure hooks object exists
|
|
95
|
+
if (!settings.hooks) {
|
|
96
|
+
settings.hooks = {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Ensure SessionStart array exists
|
|
100
|
+
if (!settings.hooks.SessionStart) {
|
|
101
|
+
settings.hooks.SessionStart = [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if our hook is already registered
|
|
105
|
+
const alreadyRegistered = settings.hooks.SessionStart.some(h =>
|
|
106
|
+
h.hooks && h.hooks.some(hh => hh.command && hh.command.includes('screenfix-loader'))
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (alreadyRegistered) {
|
|
110
|
+
console.log('[screenfix] Hook already registered in settings.json');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Add our hook to the BEGINNING so it loads first
|
|
115
|
+
settings.hooks.SessionStart.unshift(HOOK_CONFIG);
|
|
116
|
+
|
|
117
|
+
// Write back settings
|
|
118
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
119
|
+
console.log('[screenfix] Registered hook in settings.json');
|
|
120
|
+
}
|
|
121
|
+
|
|
69
122
|
// Run install
|
|
70
123
|
install();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudescreenfix-hardwicksoftware",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "fixes scroll glitch + VNC/headless rendering in claude code cli -
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "fixes scroll glitch + VNC/headless rendering in claude code cli - use claude-fixed command to strip BG colors that break VTE on Xvfb",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-fixed": "./bin/claude-fixed.js"
|