Use dedicated command runner PTY for claude MCP operations

- Spawn a single persistent PTY at app startup for running claude commands
- Replaces execAsync with login shell to avoid PATH issues with NVM
- PTY has full interactive shell environment with proper PATH
- Commands execute instantly (no shell spawn overhead)
- Fixes "command not found: claude" in compiled DMG
- Detects command completion using isTerminalReady helper

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Amrit Subramanian 2025-10-08 13:42:40 -04:00
parent 68cdbb49aa
commit 93f5134291

76
main.ts
View file

@ -15,6 +15,7 @@ const execAsync = promisify(exec);
let mainWindow: BrowserWindow; let mainWindow: BrowserWindow;
const activePtyProcesses = new Map<string, pty.IPty>(); const activePtyProcesses = new Map<string, pty.IPty>();
const mcpPollerPtyProcesses = new Map<string, pty.IPty>(); const mcpPollerPtyProcesses = new Map<string, pty.IPty>();
let claudeCommandRunnerPty: pty.IPty | null = null;
const store = new Store(); const store = new Store();
// Helper functions for session management // Helper functions for session management
@ -632,35 +633,81 @@ async function listMcpServers() {
} }
} }
// Get user's shell, with fallback // Spawn a dedicated PTY for running claude commands
function getUserShell(): string { function spawnClaudeCommandRunner() {
return process.env.SHELL || (os.platform() === "darwin" ? "/bin/zsh" : "/bin/bash"); if (claudeCommandRunnerPty) {
return;
} }
// Execute command in user's login shell const shell = os.platform() === "darwin" ? "zsh" : "bash";
async function execInLoginShell(command: string): Promise<string> { claudeCommandRunnerPty = pty.spawn(shell, ["-l"], {
const userShell = getUserShell(); name: "xterm-color",
// Wrap command to execute in login shell cols: 80,
const wrappedCommand = `${userShell} -l -c '${command.replace(/'/g, "'\\''")}'`; rows: 30,
const { stdout } = await execAsync(wrappedCommand); cwd: os.homedir(),
return stdout; env: process.env,
});
}
// Execute claude command in dedicated PTY
async function execClaudeCommand(command: string): Promise<string> {
return new Promise((resolve, reject) => {
if (!claudeCommandRunnerPty) {
reject(new Error("Claude command runner PTY not initialized"));
return;
}
const pty = claudeCommandRunnerPty;
let outputBuffer = "";
let timeoutId: NodeJS.Timeout;
let disposed = false;
const dataHandler = pty.onData((data: string) => {
if (disposed) return;
outputBuffer += data;
// Check if command completed (prompt returned)
if (isTerminalReady(data)) {
disposed = true;
clearTimeout(timeoutId);
dataHandler.dispose();
// Extract just the command output (remove the command echo and prompt lines)
const lines = outputBuffer.split('\n');
const output = lines.slice(1, -1).join('\n').trim();
resolve(output);
}
});
// Set timeout
timeoutId = setTimeout(() => {
if (!disposed) {
disposed = true;
dataHandler.dispose();
reject(new Error("Command timeout"));
}
}, 10000);
pty.write(command + "\r");
});
} }
async function addMcpServer(name: string, config: any) { async function addMcpServer(name: string, config: any) {
// Use add-json to support full configuration including env vars, headers, etc. // Use add-json to support full configuration including env vars, headers, etc.
const jsonConfig = JSON.stringify(config).replace(/'/g, "'\\''"); // Escape single quotes for shell const jsonConfig = JSON.stringify(config).replace(/'/g, "'\\''"); // Escape single quotes for shell
const command = `claude mcp add-json --scope user "${name}" '${jsonConfig}'`; const command = `claude mcp add-json --scope user "${name}" '${jsonConfig}'`;
await execInLoginShell(command); await execClaudeCommand(command);
} }
async function removeMcpServer(name: string) { async function removeMcpServer(name: string) {
const command = `claude mcp remove "${name}"`; const command = `claude mcp remove "${name}"`;
await execInLoginShell(command); await execClaudeCommand(command);
} }
async function getMcpServerDetails(name: string) { async function getMcpServerDetails(name: string) {
try { try {
const output = await execInLoginShell(`claude mcp get "${name}"`); const output = await execClaudeCommand(`claude mcp get "${name}"`);
// Parse the output to extract details // Parse the output to extract details
const details: any = { name }; const details: any = { name };
@ -777,6 +824,9 @@ const createWindow = () => {
app.whenReady().then(() => { app.whenReady().then(() => {
createWindow(); createWindow();
// Spawn claude command runner PTY early so it's ready when needed (fire-and-forget)
spawnClaudeCommandRunner();
// Handles launch from dock on macos // Handles launch from dock on macos
app.on("activate", () => { app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) { if (BrowserWindow.getAllWindows().length === 0) {