pub mod verbosity;
pub use verbosity::VerbosityLevel;
pub const COMMANDS_YAML : &str = concat!( env!( "CARGO_MANIFEST_DIR" ), "/claude.commands.yaml" );
#[ cfg( feature = "enabled" ) ]
#[ inline ]
pub fn register_commands( _registry : &mut unilang::registry::CommandRegistry ) {}
#[ cfg( feature = "enabled" ) ]
mod cli
{
use super::VerbosityLevel;
use claude_runner_core::ClaudeCommand;
use error_tools::{ Error, Result };
#[ allow( clippy::struct_excessive_bools ) ]
#[ derive( Default ) ]
pub( super ) struct CliArgs
{
pub( super ) message : Option< String >,
pub( super ) print_mode : bool,
pub( super ) interactive : bool,
pub( super ) new_session : bool,
pub( super ) model : Option< String >,
pub( super ) verbose : bool,
pub( super ) no_skip_permissions : bool,
pub( super ) max_tokens : Option< u32 >,
pub( super ) session_dir : Option< String >,
pub( super ) dir : Option< String >,
pub( super ) dry_run : bool,
pub( super ) trace : bool,
pub( super ) verbosity : VerbosityLevel,
pub( super ) help : bool,
pub( super ) system_prompt : Option< String >,
pub( super ) append_system_prompt : Option< String >,
pub( super ) no_ultrathink : bool,
}
pub( super ) fn print_help()
{
println!( "clr — Execute Claude Code with configurable parameters" );
println!();
println!( "USAGE:" );
println!( " clr [OPTIONS] [MESSAGE]" );
println!();
println!( "ARGUMENTS:" );
println!( " [MESSAGE] Prompt message for Claude" );
println!();
println!( "OPTIONS:" );
println!( " -p, --print Non-interactive mode (capture and print output)" );
println!( " --interactive Force interactive mode even when a message is given" );
println!( " --new-session Start a new session (default: continues previous)" );
println!( " --model <MODEL> Model to use" );
println!( " --verbose Enable verbose output" );
println!( " --no-skip-permissions Disable automatic permission bypass (on by default)" );
println!( " --max-tokens <N> Max output tokens (default: 200000)" );
println!( " --session-dir <PATH> Session storage directory" );
println!( " --dir <PATH> Working directory" );
println!( " --dry-run Print command without executing" );
println!( " --trace Print command to stderr then execute (like set -x)" );
println!( " --system-prompt <TEXT> Set system prompt (replaces the default)" );
println!( " --append-system-prompt <TEXT> Append text to the default system prompt" );
println!( " --no-ultrathink Disable automatic \"\\n\\nultrathink\" message suffix" );
println!( " --verbosity <0-5> Runner output verbosity level (default: 3)" );
println!( " -h, --help Show this help" );
}
fn next_value<'a>( tokens : &'a [ String ], idx : usize, flag : &str ) -> Result< &'a str >
{
tokens.get( idx ).map( String::as_str ).ok_or_else( ||
Error::msg( format!( "{flag} requires a value" ) )
)
}
fn parse_token_limit( raw : &str ) -> Result< u32 >
{
raw.parse::< u32 >().map_err( | _ |
Error::msg( format!(
"invalid --max-tokens value: {raw}\n\
Expected unsigned integer 0–4294967295"
) )
)
}
#[ allow( clippy::too_many_lines ) ]
pub( super ) fn parse_args( tokens : &[ String ] ) -> Result< CliArgs >
{
if tokens.iter().any( | t | t == "--help" || t == "-h" )
{
return Ok( CliArgs { help : true, ..CliArgs::default() } );
}
let mut parsed = CliArgs::default();
let mut positional : Vec< String > = Vec::new();
let mut i = 0;
while i < tokens.len()
{
let token = tokens[ i ].as_str();
match token
{
"-h" | "--help" =>
{
parsed.help = true;
}
"-p" | "--print" =>
{
parsed.print_mode = true;
}
"--interactive" =>
{
parsed.interactive = true;
}
"--new-session" =>
{
parsed.new_session = true;
}
"--verbose" =>
{
parsed.verbose = true;
}
"--no-skip-permissions" =>
{
parsed.no_skip_permissions = true;
}
"--dry-run" =>
{
parsed.dry_run = true;
}
"--trace" =>
{
parsed.trace = true;
}
"--no-ultrathink" =>
{
parsed.no_ultrathink = true;
}
"--system-prompt" =>
{
i += 1;
parsed.system_prompt = Some( next_value( tokens, i, "--system-prompt" )?.to_string() );
}
"--append-system-prompt" =>
{
i += 1;
parsed.append_system_prompt = Some( next_value( tokens, i, "--append-system-prompt" )?.to_string() );
}
"--model" =>
{
i += 1;
parsed.model = Some( next_value( tokens, i, "--model" )?.to_string() );
}
"--max-tokens" =>
{
i += 1;
parsed.max_tokens = Some( parse_token_limit( next_value( tokens, i, "--max-tokens" )? )? );
}
"--session-dir" =>
{
i += 1;
parsed.session_dir = Some( next_value( tokens, i, "--session-dir" )?.to_string() );
}
"--dir" =>
{
i += 1;
parsed.dir = Some( next_value( tokens, i, "--dir" )?.to_string() );
}
"--verbosity" =>
{
i += 1;
let raw = next_value( tokens, i, "--verbosity" )?;
parsed.verbosity = raw.parse::< VerbosityLevel >().map_err( Error::msg )?;
}
"--" =>
{
positional.extend( tokens[ i + 1 .. ].iter().filter( | t | !t.is_empty() ).cloned() );
break;
}
s if s.starts_with( '-' ) =>
{
return Err( Error::msg( format!( "unknown option: {s}\nRun with --help for usage." ) ) );
}
_ =>
{
if !tokens[ i ].is_empty()
{
positional.push( tokens[ i ].clone() );
}
}
}
i += 1;
}
if !positional.is_empty()
{
parsed.message = Some( positional.join( " " ) );
}
Ok( parsed )
}
pub( super ) fn build_claude_command( cli : &CliArgs ) -> ClaudeCommand
{
let mut builder = ClaudeCommand::new();
if let Some( ref dir ) = cli.dir
{
builder = builder.with_working_directory( dir.clone() );
}
if let Some( n ) = cli.max_tokens
{
builder = builder.with_max_output_tokens( n );
}
if !cli.new_session
{
builder = builder.with_continue_conversation( true );
}
if !cli.no_skip_permissions
{
builder = builder.with_skip_permissions( true );
}
if cli.verbose
{
builder = builder.with_verbose( true );
}
if let Some( ref model ) = cli.model
{
builder = builder.with_model( model.clone() );
}
if let Some( ref sd ) = cli.session_dir
{
builder = builder.with_session_dir( sd.clone() );
}
if let Some( ref sp ) = cli.system_prompt
{
builder = builder.with_system_prompt( sp.clone() );
}
if let Some( ref asp ) = cli.append_system_prompt
{
builder = builder.with_append_system_prompt( asp.clone() );
}
let use_print = cli.print_mode || ( cli.message.is_some() && !cli.interactive );
if use_print
{
builder = builder.with_arg( "--print" );
}
if let Some( ref msg ) = cli.message
{
let effective_msg = if cli.no_ultrathink || msg.trim_end().ends_with( "ultrathink" )
{
msg.clone()
}
else
{
format!( "{msg}\n\nultrathink" )
};
builder = builder.with_message( effective_msg );
}
builder
}
pub( super ) fn handle_dry_run( builder : &ClaudeCommand )
{
let env = builder.describe_env();
let command = builder.describe();
if !env.is_empty() { println!( "{env}" ); }
println!( "{command}" );
}
pub( super ) fn run_print_mode( builder : &ClaudeCommand, verbosity : VerbosityLevel )
{
let output = match builder.execute()
{
Ok( o ) => o,
Err( e ) =>
{
if verbosity.shows_errors()
{
eprintln!( "Error: {e}" );
}
std::process::exit( 1 );
}
};
if !output.stderr.is_empty() { eprint!( "{}", output.stderr ); }
if output.exit_code != 0
{
if verbosity.shows_errors()
{
eprintln!( "Error: Claude exited with code {}", output.exit_code );
}
std::process::exit( 1 );
}
print!( "{}", output.stdout );
}
pub( super ) fn run_interactive( builder : &ClaudeCommand, verbosity : VerbosityLevel )
{
let status = match builder.execute_interactive()
{
Ok( s ) => s,
Err( e ) =>
{
if verbosity.shows_errors()
{
eprintln!( "Error: {e}" );
}
std::process::exit( 1 );
}
};
if !status.success()
{
std::process::exit( status.code().unwrap_or( 1 ) );
}
}
}
#[ cfg( feature = "enabled" ) ]
#[ inline ]
pub fn run_cli()
{
use cli::{ parse_args, build_claude_command, handle_dry_run, print_help, run_print_mode, run_interactive };
let tokens : Vec< String > = std::env::args().skip( 1 ).collect();
let cli = match parse_args( &tokens )
{
Ok( c ) => c,
Err( e ) =>
{
eprintln!( "Error: {e}" );
std::process::exit( 1 );
}
};
if cli.help
{
print_help();
return;
}
if cli.print_mode && cli.message.is_none()
{
eprintln!( "Error: --print requires a message argument" );
eprintln!( "Run with --help for usage." );
std::process::exit( 1 );
}
let builder = build_claude_command( &cli );
if cli.dry_run
{
handle_dry_run( &builder );
return;
}
if cli.trace || cli.verbosity.shows_verbose_detail()
{
let env = builder.describe_env();
let command = builder.describe();
let mut preview = String::new();
if !env.is_empty() { preview.push_str( &env ); preview.push( '\n' ); }
preview.push_str( &command );
eprintln!( "{preview}" );
}
if cli.print_mode || ( cli.message.is_some() && !cli.interactive )
{
run_print_mode( &builder, cli.verbosity );
}
else
{
run_interactive( &builder, cli.verbosity );
}
}