From 19490dd763afccd2c48b9d3e070d5562e00c6593 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 24 Jan 2026 17:47:59 +0100 Subject: [PATCH] fix: augment PATH with claude directory instead of using absolute path The previous fix used absolute paths to spawn claude, but the CLI itself checks if its directory is in PATH and warns if not. Instead, find where claude is installed and prepend its directory to PATH in the spawn environment. This ensures both execvp resolution and Claude's own PATH check are satisfied. Co-Authored-By: Claude Opus 4.5 --- src/screen-manager.ts | 56 +++++++++++++++--------------- src/session.ts | 79 +++++++++++++++++++++++-------------------- 2 files changed, 69 insertions(+), 66 deletions(-) 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',