diff --git a/src/screen-manager.ts b/src/screen-manager.ts index 79d97205..463adae1 100644 --- a/src/screen-manager.ts +++ b/src/screen-manager.ts @@ -22,50 +22,47 @@ import { homedir } from 'node:os'; import { ScreenSession, ProcessStats, ScreenSessionWithStats, PersistedRespawnConfig, getErrorMessage } from './types.js'; // ============================================================================ -// Claude CLI Path Resolution +// Claude CLI PATH Resolution // ============================================================================ -/** Common installation paths for the Claude CLI binary */ -const CLAUDE_COMMON_PATHS = [ - `${homedir()}/.local/bin/claude`, - `${homedir()}/.claude/local/claude`, - '/usr/local/bin/claude', - '/usr/bin/claude', - `${homedir()}/.npm-global/bin/claude`, - `${homedir()}/bin/claude`, +/** Common directories where the Claude CLI binary may be installed */ +const CLAUDE_SEARCH_DIRS = [ + `${homedir()}/.local/bin`, + `${homedir()}/.claude/local`, + '/usr/local/bin', + `${homedir()}/.npm-global/bin`, + `${homedir()}/bin`, ]; -/** Cached resolved path to the claude binary */ -let _resolvedClaudePath: string | null = null; +/** Cached directory containing the claude binary */ +let _claudeDir: string | null = null; /** - * Resolves the absolute path to the `claude` CLI binary. - * Uses `which` first, then falls back to common installation locations. + * Finds the directory containing the `claude` binary. + * Returns null if not found (will rely on PATH as-is). */ -function resolveClaudePath(): string { - if (_resolvedClaudePath) return _resolvedClaudePath; +function findClaudeDir(): string | null { + if (_claudeDir !== null) return _claudeDir; try { const result = execSync('which claude', { encoding: 'utf-8', timeout: 5000 }).trim(); if (result && existsSync(result)) { - _resolvedClaudePath = result; - return _resolvedClaudePath; + _claudeDir = dirname(result); + return _claudeDir; } } catch { - // `which` failed, try common paths + // not in PATH } - for (const p of CLAUDE_COMMON_PATHS) { - if (existsSync(p)) { - _resolvedClaudePath = p; - return _resolvedClaudePath; + for (const dir of CLAUDE_SEARCH_DIRS) { + if (existsSync(`${dir}/claude`)) { + _claudeDir = dir; + return _claudeDir; } } - throw new Error( - 'Claude CLI not found. Ensure `claude` is installed and available in PATH. ' + - 'Checked: PATH lookup and common paths: ' + CLAUDE_COMMON_PATHS.join(', ') - ); + _claudeDir = ''; // mark as searched, not found + return null; } /** Path to persisted screen session metadata */ @@ -215,10 +212,11 @@ export class ScreenManager extends EventEmitter { // Create screen in detached mode with the appropriate command // Set CLAUDEMAN_SCREEN=1 so Claude sessions know they're running in Claudeman // This helps prevent Claude from attempting to kill its own screen session + const claudeDir = findClaudeDir(); + const pathExport = claudeDir ? `export PATH="${claudeDir}:$PATH" && ` : ''; const envVars = `CLAUDEMAN_SCREEN=1 CLAUDEMAN_SESSION_ID=${sessionId} CLAUDEMAN_SCREEN_NAME=${screenName} CLAUDEMAN_API_URL=${process.env.CLAUDEMAN_API_URL || 'http://localhost:3000'}`; - const claudePath = resolveClaudePath(); const cmd = mode === 'claude' - ? `${envVars} ${claudePath} --dangerously-skip-permissions` + ? `${envVars} claude --dangerously-skip-permissions` : `${envVars} $SHELL`; try { @@ -226,7 +224,7 @@ export class ScreenManager extends EventEmitter { const screenProcess = spawn('screen', [ '-dmS', screenName, '-c', '/dev/null', // Use empty config - 'bash', '-c', `cd "${workingDir}" && ${cmd}` + 'bash', '-c', `${pathExport}cd "${workingDir}" && ${cmd}` ], { cwd: workingDir, detached: true, diff --git a/src/session.ts b/src/session.ts index a9cd19ee..56ab895b 100644 --- a/src/session.ts +++ b/src/session.ts @@ -18,6 +18,7 @@ import { EventEmitter } from 'node:events'; import { execSync } from 'node:child_process'; import { existsSync } from 'node:fs'; +import { dirname } from 'node:path'; import { v4 as uuidv4 } from 'uuid'; import * as pty from 'node-pty'; import { SessionState, SessionStatus, SessionConfig, ScreenSession, RalphTrackerState, RalphTodoItem } from './types.js'; @@ -64,58 +65,62 @@ const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g; const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/; // ============================================================================ -// Claude CLI Path Resolution +// Claude CLI PATH Resolution // ============================================================================ -/** Common installation paths for the Claude CLI binary */ -const CLAUDE_COMMON_PATHS = [ - `${process.env.HOME}/.local/bin/claude`, - `${process.env.HOME}/.claude/local/claude`, - '/usr/local/bin/claude', - '/usr/bin/claude', - `${process.env.HOME}/.npm-global/bin/claude`, - `${process.env.HOME}/bin/claude`, +/** Common directories where the Claude CLI binary may be installed */ +const CLAUDE_SEARCH_DIRS = [ + `${process.env.HOME}/.local/bin`, + `${process.env.HOME}/.claude/local`, + '/usr/local/bin', + `${process.env.HOME}/.npm-global/bin`, + `${process.env.HOME}/bin`, ]; -/** Cached resolved path to the claude binary */ -let _resolvedClaudePath: string | null = null; +/** Cached PATH string with claude's directory prepended */ +let _augmentedPath: string | null = null; /** - * Resolves the absolute path to the `claude` CLI binary. + * Returns a PATH string that includes the directory containing `claude`. * - * Uses `which` to find the binary in PATH first, then falls back to common - * installation locations. The result is cached for subsequent calls. - * - * @throws {Error} If claude CLI cannot be found in PATH or common locations + * Finds the claude binary (via `which` or common install locations), then + * prepends its directory to the current PATH if not already present. + * Result is cached for subsequent calls. */ -function resolveClaudePath(): string { - if (_resolvedClaudePath) return _resolvedClaudePath; +function getAugmentedPath(): string { + if (_augmentedPath) return _augmentedPath; - // Try `which` first (respects PATH) + const currentPath = process.env.PATH || ''; + let claudeDir: string | null = null; + + // Try `which` first (respects current PATH) try { const result = execSync('which claude', { encoding: 'utf-8', timeout: 5000 }).trim(); if (result && existsSync(result)) { - _resolvedClaudePath = result; - console.log('[Session] Resolved claude CLI path:', _resolvedClaudePath); - return _resolvedClaudePath; + claudeDir = dirname(result); } } catch { - // `which` failed, try common paths + // not in PATH, check common locations } - // Check common installation paths - for (const p of CLAUDE_COMMON_PATHS) { - if (existsSync(p)) { - _resolvedClaudePath = p; - console.log('[Session] Found claude CLI at common path:', _resolvedClaudePath); - return _resolvedClaudePath; + // Fallback: check common installation directories + if (!claudeDir) { + for (const dir of CLAUDE_SEARCH_DIRS) { + if (existsSync(`${dir}/claude`)) { + claudeDir = dir; + break; + } } } - throw new Error( - 'Claude CLI not found. Ensure `claude` is installed and available in PATH. ' + - 'Checked: PATH lookup and common paths: ' + CLAUDE_COMMON_PATHS.join(', ') - ); + if (claudeDir && !currentPath.split(':').includes(claudeDir)) { + _augmentedPath = `${claudeDir}:${currentPath}`; + console.log('[Session] Augmented PATH with claude directory:', claudeDir); + } else { + _augmentedPath = currentPath; + } + + return _augmentedPath; } // ============================================================================ @@ -793,8 +798,7 @@ export class Session extends EventEmitter { // Fallback to direct PTY if screen is not used if (!this.ptyProcess) { - const claudePath = resolveClaudePath(); - this.ptyProcess = pty.spawn(claudePath, [ + this.ptyProcess = pty.spawn('claude', [ '--dangerously-skip-permissions' ], { name: 'xterm-256color', @@ -803,6 +807,7 @@ export class Session extends EventEmitter { cwd: this.workingDir, env: { ...process.env, + PATH: getAugmentedPath(), TERM: 'xterm-256color', // Inform Claude it's running within Claudeman (helps prevent self-termination) CLAUDEMAN_SCREEN: '1', @@ -1072,8 +1077,7 @@ export class Session extends EventEmitter { // Spawn claude in a real PTY console.log('[Session] Spawning PTY for claude with prompt:', prompt.substring(0, 50)); - const claudePath = resolveClaudePath(); - this.ptyProcess = pty.spawn(claudePath, [ + this.ptyProcess = pty.spawn('claude', [ '-p', '--dangerously-skip-permissions', '--output-format', 'stream-json', @@ -1085,6 +1089,7 @@ export class Session extends EventEmitter { cwd: this.workingDir, env: { ...process.env, + PATH: getAugmentedPath(), TERM: 'xterm-256color', // Inform Claude it's running within Claudeman CLAUDEMAN_SCREEN: '1',