use std::path::PathBuf;
use error_tools::{ Result, Error };
mod params_core;
mod params_security;
mod params_extended;
#[derive( Debug )]
pub struct ClaudeCommand {
pub(super) working_directory: Option<PathBuf>,
pub(super) max_output_tokens: Option<u32>,
pub(super) continue_conversation: bool,
pub(super) message: Option<String>,
pub(super) args: Vec<String>,
pub(super) bash_default_timeout_ms: Option<u32>,
pub(super) bash_max_timeout_ms: Option<u32>,
pub(super) auto_continue: Option<bool>,
pub(super) telemetry: Option<bool>,
pub(super) auto_approve_tools: Option<bool>,
pub(super) action_mode: Option<crate::types::ActionMode>,
pub(super) log_level: Option<crate::types::LogLevel>,
pub(super) temperature: Option<f64>,
pub(super) skip_permissions: bool,
pub(super) chrome: Option<bool>,
pub(super) sandbox_mode: Option<bool>,
pub(super) session_dir: Option<PathBuf>,
pub(super) top_p: Option<f64>,
pub(super) top_k: Option<u32>,
pub(super) dry_run: bool,
}
impl ClaudeCommand {
#[inline]
#[must_use]
pub fn new() -> Self {
Self {
working_directory: None,
max_output_tokens: Some( 200_000 ),
continue_conversation: false,
message: None,
args: Vec::new(),
bash_default_timeout_ms: Some( 3_600_000 ), bash_max_timeout_ms: Some( 7_200_000 ), auto_continue: Some( true ), telemetry: Some( false ),
skip_permissions: false,
chrome: Some( true ),
auto_approve_tools: None, action_mode: None, log_level: None, temperature: None, sandbox_mode: None, session_dir: None, top_p: None, top_k: None,
dry_run: false,
}
}
#[inline]
#[must_use]
pub fn describe_compact( &self ) -> String {
self.describe()
.lines()
.last()
.unwrap_or( "claude" )
.to_string()
}
#[inline]
#[must_use]
pub fn describe( &self ) -> String {
let mut lines = Vec::new();
if let Some( ref dir ) = self.working_directory {
lines.push( format!( "cd {}", dir.display() ) );
}
let mut parts = vec![ "claude".to_string() ];
if self.skip_permissions {
parts.push( "--dangerously-skip-permissions".to_string() );
}
match self.chrome {
Some( true ) => parts.push( "--chrome".to_string() ),
Some( false ) => parts.push( "--no-chrome".to_string() ),
None => {}
}
for arg in &self.args {
parts.push( arg.clone() );
}
if self.continue_conversation {
parts.push( "-c".to_string() );
}
if let Some( ref msg ) = self.message {
let escaped = msg.replace( '\\', "\\\\" ).replace( '"', "\\\"" );
parts.push( format!( "\"{escaped}\"" ) );
}
lines.push( parts.join( " " ) );
lines.join( "\n" )
}
#[inline]
#[must_use]
pub fn describe_env( &self ) -> String {
let mut lines = Vec::new();
if let Some( tokens ) = self.max_output_tokens {
lines.push( format!( "CLAUDE_CODE_MAX_OUTPUT_TOKENS={tokens}" ) );
}
if let Some( timeout ) = self.bash_default_timeout_ms {
lines.push( format!( "CLAUDE_CODE_BASH_TIMEOUT={timeout}" ) );
}
if let Some( max_timeout ) = self.bash_max_timeout_ms {
lines.push( format!( "CLAUDE_CODE_BASH_MAX_TIMEOUT={max_timeout}" ) );
}
if let Some( auto_continue ) = self.auto_continue {
lines.push( format!( "CLAUDE_CODE_AUTO_CONTINUE={auto_continue}" ) );
}
if let Some( telemetry ) = self.telemetry {
lines.push( format!( "CLAUDE_CODE_TELEMETRY={telemetry}" ) );
}
if let Some( approve ) = self.auto_approve_tools {
lines.push( format!( "CLAUDE_CODE_AUTO_APPROVE_TOOLS={approve}" ) );
}
if let Some( mode ) = self.action_mode {
lines.push( format!( "CLAUDE_CODE_ACTION_MODE={}", mode.as_str() ) );
}
if let Some( level ) = self.log_level {
lines.push( format!( "CLAUDE_CODE_LOG_LEVEL={}", level.as_str() ) );
}
if let Some( temp ) = self.temperature {
lines.push( format!( "CLAUDE_CODE_TEMPERATURE={temp}" ) );
}
if let Some( sandbox ) = self.sandbox_mode {
lines.push( format!( "CLAUDE_CODE_SANDBOX_MODE={sandbox}" ) );
}
if let Some( ref dir ) = self.session_dir {
lines.push( format!( "CLAUDE_CODE_SESSION_DIR={}", dir.display() ) );
}
if let Some( top_p ) = self.top_p {
lines.push( format!( "CLAUDE_CODE_TOP_P={top_p}" ) );
}
if let Some( top_k ) = self.top_k {
lines.push( format!( "CLAUDE_CODE_TOP_K={top_k}" ) );
}
lines.join( "\n" )
}
#[inline]
pub fn execute( &self ) -> Result< crate::types::ExecutionOutput > {
if self.dry_run {
return Ok( crate::types::ExecutionOutput {
stdout: self.describe_compact(),
stderr: String::new(),
exit_code: 0,
} );
}
let mut cmd = self.build_command();
let output = cmd.output()
.map_err( |e| Error::msg( format!( "Failed to execute Claude Code: {e}" ) ) )?;
let stdout = String::from_utf8_lossy( &output.stdout ).to_string();
let stderr = String::from_utf8_lossy( &output.stderr ).to_string();
let exit_code = output.status.code().unwrap_or( -1 );
Ok( crate::types::ExecutionOutput { stdout, stderr, exit_code } )
}
#[inline]
pub fn execute_interactive( &self ) -> Result< std::process::ExitStatus > {
if self.dry_run {
#[cfg( unix )]
{
use std::os::unix::process::ExitStatusExt;
return Ok( std::process::ExitStatus::from_raw( 0 ) );
}
#[cfg( not( unix ) )]
{
let status = std::process::Command::new( "cmd" )
.args( [ "/C", "exit", "0" ] )
.status()
.map_err( |e| Error::msg( format!( "Failed to create dry-run status: {e}" ) ) )?;
return Ok( status );
}
}
let mut cmd = self.build_command();
let status = cmd.status()
.map_err( |e| Error::msg( format!( "Failed to execute Claude Code: {e}" ) ) )?;
Ok( status )
}
#[inline]
fn build_command( &self ) -> std::process::Command {
use std::process::Command;
let mut cmd = Command::new( "claude" );
if let Some( ref dir ) = self.working_directory {
cmd.current_dir( dir );
}
if let Some( tokens ) = self.max_output_tokens {
cmd.env( "CLAUDE_CODE_MAX_OUTPUT_TOKENS", tokens.to_string() );
}
if let Some( timeout ) = self.bash_default_timeout_ms {
cmd.env( "CLAUDE_CODE_BASH_TIMEOUT", timeout.to_string() );
}
if let Some( max_timeout ) = self.bash_max_timeout_ms {
cmd.env( "CLAUDE_CODE_BASH_MAX_TIMEOUT", max_timeout.to_string() );
}
if let Some( auto_continue ) = self.auto_continue {
cmd.env( "CLAUDE_CODE_AUTO_CONTINUE", auto_continue.to_string() );
}
if let Some( telemetry ) = self.telemetry {
cmd.env( "CLAUDE_CODE_TELEMETRY", telemetry.to_string() );
}
if let Some( approve ) = self.auto_approve_tools {
cmd.env( "CLAUDE_CODE_AUTO_APPROVE_TOOLS", approve.to_string() );
}
if let Some( mode ) = self.action_mode {
cmd.env( "CLAUDE_CODE_ACTION_MODE", mode.as_str() );
}
if let Some( level ) = self.log_level {
cmd.env( "CLAUDE_CODE_LOG_LEVEL", level.as_str() );
}
if let Some( temp ) = self.temperature {
cmd.env( "CLAUDE_CODE_TEMPERATURE", temp.to_string() );
}
if let Some( sandbox ) = self.sandbox_mode {
cmd.env( "CLAUDE_CODE_SANDBOX_MODE", sandbox.to_string() );
}
if let Some( ref dir ) = self.session_dir {
cmd.env( "CLAUDE_CODE_SESSION_DIR", dir.to_string_lossy().as_ref() );
}
if let Some( top_p ) = self.top_p {
cmd.env( "CLAUDE_CODE_TOP_P", top_p.to_string() );
}
if let Some( top_k ) = self.top_k {
cmd.env( "CLAUDE_CODE_TOP_K", top_k.to_string() );
}
if self.skip_permissions {
cmd.arg( "--dangerously-skip-permissions" );
}
match self.chrome {
Some( true ) => { cmd.arg( "--chrome" ); }
Some( false ) => { cmd.arg( "--no-chrome" ); }
None => {}
}
for arg in &self.args {
cmd.arg( arg );
}
if self.continue_conversation {
cmd.arg( "-c" );
}
if let Some( ref msg ) = self.message {
cmd.arg( msg );
}
cmd
}
}
#[ inline ]
#[ must_use ]
pub fn claude_version() -> Option< String >
{
let output = std::process::Command::new( "claude" )
.arg( "--version" )
.output()
.ok()?;
let s = String::from_utf8_lossy( &output.stdout ).trim().to_string();
if s.is_empty() { None } else { Some( s ) }
}
impl Default for ClaudeCommand {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl ClaudeCommand {
#[ doc( hidden ) ]
#[ inline ]
#[ must_use ]
pub fn build_command_for_test( &self ) -> std::process::Command {
self.build_command()
}
}