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:
parent
68cdbb49aa
commit
93f5134291
76
main.ts
76
main.ts
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue