mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
+27
-29
@@ -22,50 +22,47 @@ import { homedir } from 'node:os';
|
|||||||
import { ScreenSession, ProcessStats, ScreenSessionWithStats, PersistedRespawnConfig, getErrorMessage } from './types.js';
|
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 */
|
/** Common directories where the Claude CLI binary may be installed */
|
||||||
const CLAUDE_COMMON_PATHS = [
|
const CLAUDE_SEARCH_DIRS = [
|
||||||
`${homedir()}/.local/bin/claude`,
|
`${homedir()}/.local/bin`,
|
||||||
`${homedir()}/.claude/local/claude`,
|
`${homedir()}/.claude/local`,
|
||||||
'/usr/local/bin/claude',
|
'/usr/local/bin',
|
||||||
'/usr/bin/claude',
|
`${homedir()}/.npm-global/bin`,
|
||||||
`${homedir()}/.npm-global/bin/claude`,
|
`${homedir()}/bin`,
|
||||||
`${homedir()}/bin/claude`,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Cached resolved path to the claude binary */
|
/** Cached directory containing the claude binary */
|
||||||
let _resolvedClaudePath: string | null = null;
|
let _claudeDir: string | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the absolute path to the `claude` CLI binary.
|
* Finds the directory containing the `claude` binary.
|
||||||
* Uses `which` first, then falls back to common installation locations.
|
* Returns null if not found (will rely on PATH as-is).
|
||||||
*/
|
*/
|
||||||
function resolveClaudePath(): string {
|
function findClaudeDir(): string | null {
|
||||||
if (_resolvedClaudePath) return _resolvedClaudePath;
|
if (_claudeDir !== null) return _claudeDir;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = execSync('which claude', { encoding: 'utf-8', timeout: 5000 }).trim();
|
const result = execSync('which claude', { encoding: 'utf-8', timeout: 5000 }).trim();
|
||||||
if (result && existsSync(result)) {
|
if (result && existsSync(result)) {
|
||||||
_resolvedClaudePath = result;
|
_claudeDir = dirname(result);
|
||||||
return _resolvedClaudePath;
|
return _claudeDir;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// `which` failed, try common paths
|
// not in PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const p of CLAUDE_COMMON_PATHS) {
|
for (const dir of CLAUDE_SEARCH_DIRS) {
|
||||||
if (existsSync(p)) {
|
if (existsSync(`${dir}/claude`)) {
|
||||||
_resolvedClaudePath = p;
|
_claudeDir = dir;
|
||||||
return _resolvedClaudePath;
|
return _claudeDir;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
_claudeDir = ''; // mark as searched, not found
|
||||||
'Claude CLI not found. Ensure `claude` is installed and available in PATH. ' +
|
return null;
|
||||||
'Checked: PATH lookup and common paths: ' + CLAUDE_COMMON_PATHS.join(', ')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Path to persisted screen session metadata */
|
/** Path to persisted screen session metadata */
|
||||||
@@ -215,10 +212,11 @@ export class ScreenManager extends EventEmitter {
|
|||||||
// Create screen in detached mode with the appropriate command
|
// Create screen in detached mode with the appropriate command
|
||||||
// Set CLAUDEMAN_SCREEN=1 so Claude sessions know they're running in Claudeman
|
// 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
|
// 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 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'
|
const cmd = mode === 'claude'
|
||||||
? `${envVars} ${claudePath} --dangerously-skip-permissions`
|
? `${envVars} claude --dangerously-skip-permissions`
|
||||||
: `${envVars} $SHELL`;
|
: `${envVars} $SHELL`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -226,7 +224,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
const screenProcess = spawn('screen', [
|
const screenProcess = spawn('screen', [
|
||||||
'-dmS', screenName,
|
'-dmS', screenName,
|
||||||
'-c', '/dev/null', // Use empty config
|
'-c', '/dev/null', // Use empty config
|
||||||
'bash', '-c', `cd "${workingDir}" && ${cmd}`
|
'bash', '-c', `${pathExport}cd "${workingDir}" && ${cmd}`
|
||||||
], {
|
], {
|
||||||
cwd: workingDir,
|
cwd: workingDir,
|
||||||
detached: true,
|
detached: true,
|
||||||
|
|||||||
+42
-37
@@ -18,6 +18,7 @@
|
|||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
|
import { dirname } from 'node:path';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import * as pty from 'node-pty';
|
import * as pty from 'node-pty';
|
||||||
import { SessionState, SessionStatus, SessionConfig, ScreenSession, RalphTrackerState, RalphTodoItem } from './types.js';
|
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/;
|
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 */
|
/** Common directories where the Claude CLI binary may be installed */
|
||||||
const CLAUDE_COMMON_PATHS = [
|
const CLAUDE_SEARCH_DIRS = [
|
||||||
`${process.env.HOME}/.local/bin/claude`,
|
`${process.env.HOME}/.local/bin`,
|
||||||
`${process.env.HOME}/.claude/local/claude`,
|
`${process.env.HOME}/.claude/local`,
|
||||||
'/usr/local/bin/claude',
|
'/usr/local/bin',
|
||||||
'/usr/bin/claude',
|
`${process.env.HOME}/.npm-global/bin`,
|
||||||
`${process.env.HOME}/.npm-global/bin/claude`,
|
`${process.env.HOME}/bin`,
|
||||||
`${process.env.HOME}/bin/claude`,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Cached resolved path to the claude binary */
|
/** Cached PATH string with claude's directory prepended */
|
||||||
let _resolvedClaudePath: string | null = null;
|
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
|
* Finds the claude binary (via `which` or common install locations), then
|
||||||
* installation locations. The result is cached for subsequent calls.
|
* prepends its directory to the current PATH if not already present.
|
||||||
*
|
* Result is cached for subsequent calls.
|
||||||
* @throws {Error} If claude CLI cannot be found in PATH or common locations
|
|
||||||
*/
|
*/
|
||||||
function resolveClaudePath(): string {
|
function getAugmentedPath(): string {
|
||||||
if (_resolvedClaudePath) return _resolvedClaudePath;
|
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 {
|
try {
|
||||||
const result = execSync('which claude', { encoding: 'utf-8', timeout: 5000 }).trim();
|
const result = execSync('which claude', { encoding: 'utf-8', timeout: 5000 }).trim();
|
||||||
if (result && existsSync(result)) {
|
if (result && existsSync(result)) {
|
||||||
_resolvedClaudePath = result;
|
claudeDir = dirname(result);
|
||||||
console.log('[Session] Resolved claude CLI path:', _resolvedClaudePath);
|
|
||||||
return _resolvedClaudePath;
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// `which` failed, try common paths
|
// not in PATH, check common locations
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check common installation paths
|
// Fallback: check common installation directories
|
||||||
for (const p of CLAUDE_COMMON_PATHS) {
|
if (!claudeDir) {
|
||||||
if (existsSync(p)) {
|
for (const dir of CLAUDE_SEARCH_DIRS) {
|
||||||
_resolvedClaudePath = p;
|
if (existsSync(`${dir}/claude`)) {
|
||||||
console.log('[Session] Found claude CLI at common path:', _resolvedClaudePath);
|
claudeDir = dir;
|
||||||
return _resolvedClaudePath;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
if (claudeDir && !currentPath.split(':').includes(claudeDir)) {
|
||||||
'Claude CLI not found. Ensure `claude` is installed and available in PATH. ' +
|
_augmentedPath = `${claudeDir}:${currentPath}`;
|
||||||
'Checked: PATH lookup and common paths: ' + CLAUDE_COMMON_PATHS.join(', ')
|
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
|
// Fallback to direct PTY if screen is not used
|
||||||
if (!this.ptyProcess) {
|
if (!this.ptyProcess) {
|
||||||
const claudePath = resolveClaudePath();
|
this.ptyProcess = pty.spawn('claude', [
|
||||||
this.ptyProcess = pty.spawn(claudePath, [
|
|
||||||
'--dangerously-skip-permissions'
|
'--dangerously-skip-permissions'
|
||||||
], {
|
], {
|
||||||
name: 'xterm-256color',
|
name: 'xterm-256color',
|
||||||
@@ -803,6 +807,7 @@ export class Session extends EventEmitter {
|
|||||||
cwd: this.workingDir,
|
cwd: this.workingDir,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
PATH: getAugmentedPath(),
|
||||||
TERM: 'xterm-256color',
|
TERM: 'xterm-256color',
|
||||||
// Inform Claude it's running within Claudeman (helps prevent self-termination)
|
// Inform Claude it's running within Claudeman (helps prevent self-termination)
|
||||||
CLAUDEMAN_SCREEN: '1',
|
CLAUDEMAN_SCREEN: '1',
|
||||||
@@ -1072,8 +1077,7 @@ export class Session extends EventEmitter {
|
|||||||
// Spawn claude in a real PTY
|
// Spawn claude in a real PTY
|
||||||
console.log('[Session] Spawning PTY for claude with prompt:', prompt.substring(0, 50));
|
console.log('[Session] Spawning PTY for claude with prompt:', prompt.substring(0, 50));
|
||||||
|
|
||||||
const claudePath = resolveClaudePath();
|
this.ptyProcess = pty.spawn('claude', [
|
||||||
this.ptyProcess = pty.spawn(claudePath, [
|
|
||||||
'-p',
|
'-p',
|
||||||
'--dangerously-skip-permissions',
|
'--dangerously-skip-permissions',
|
||||||
'--output-format', 'stream-json',
|
'--output-format', 'stream-json',
|
||||||
@@ -1085,6 +1089,7 @@ export class Session extends EventEmitter {
|
|||||||
cwd: this.workingDir,
|
cwd: this.workingDir,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
PATH: getAugmentedPath(),
|
||||||
TERM: 'xterm-256color',
|
TERM: 'xterm-256color',
|
||||||
// Inform Claude it's running within Claudeman
|
// Inform Claude it's running within Claudeman
|
||||||
CLAUDEMAN_SCREEN: '1',
|
CLAUDEMAN_SCREEN: '1',
|
||||||
|
|||||||
Reference in New Issue
Block a user