use crate::VerbosityLevel;
use claude_runner_core::EffortLevel;
use error_tools::{ Error, Result };
pub( crate ) enum ExpectStrategy
{
Fail,
Retry,
Default( String ),
}
impl core::str::FromStr for ExpectStrategy
{
type Err = String;
fn from_str( s : &str ) -> core::result::Result< Self, Self::Err >
{
match s
{
"fail" => Ok( ExpectStrategy::Fail ),
"retry" => Ok( ExpectStrategy::Retry ),
_ if s.starts_with( "default:" ) =>
{
let val = s[ "default:".len() .. ].to_string();
Ok( ExpectStrategy::Default( val ) )
}
_ => Err( format!(
"invalid --expect-strategy value: {s}\nExpected: fail, retry, or default:<VALUE>"
) ),
}
}
}
#[ allow( clippy::struct_excessive_bools ) ]
#[ derive( Default ) ]
pub( crate ) struct CliArgs
{
pub( crate ) message : Option< String >,
pub( crate ) print_mode : bool,
pub( crate ) interactive : bool,
pub( crate ) new_session : bool,
pub( crate ) model : Option< String >,
pub( crate ) verbose : bool,
pub( crate ) no_skip_permissions : bool,
pub( crate ) max_tokens : Option< u32 >,
pub( crate ) session_dir : Option< String >,
pub( crate ) dir : Option< String >,
pub( crate ) dry_run : bool,
pub( crate ) trace : bool,
pub( crate ) verbosity : Option< VerbosityLevel >,
pub( crate ) help : bool,
pub( crate ) system_prompt : Option< String >,
pub( crate ) append_system_prompt : Option< String >,
pub( crate ) no_ultrathink : bool,
pub( crate ) effort : Option< EffortLevel >,
pub( crate ) no_effort_max : bool,
pub( crate ) no_chrome : bool,
pub( crate ) no_persist : bool,
pub( crate ) json_schema : Option< String >,
pub( crate ) mcp_config : Vec< String >,
pub( crate ) file : Option< String >,
pub( crate ) strip_fences : bool,
pub( crate ) keep_claudecode : bool,
pub( crate ) subdir : Option< String >,
pub( crate ) output_file : Option< String >,
pub( crate ) expect : Option< String >,
pub( crate ) expect_strategy : Option< ExpectStrategy >,
pub( crate ) expect_retries : Option< u8 >,
pub( crate ) max_sessions : Option< u32 >,
pub( crate ) retry_on_rate_limit : Option< u8 >,
pub( crate ) retry_delay : Option< u32 >,
pub( crate ) timeout : Option< u32 >,
}
pub( super ) 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"
) )
)
}
fn parse_effort_level( raw : &str ) -> Result< EffortLevel >
{
raw.parse::< EffortLevel >().map_err( Error::msg )
}
fn parse_expect_strategy( raw : &str ) -> Result< ExpectStrategy >
{
raw.parse::< ExpectStrategy >().map_err( Error::msg )
}
pub( crate ) fn parse_u8_bounded( raw : &str, flag_name : &str ) -> Result< u8 >
{
raw.parse::< u32 >()
.ok()
.and_then( | v | u8::try_from( v ).ok() )
.ok_or_else( || Error::msg( format!(
"invalid {flag_name} value: {raw}\nExpected integer 0–255"
) ) )
}
fn parse_u32_flag( raw : &str, flag_name : &str, hint : &str ) -> Result< u32 >
{
raw.parse::< u32 >().map_err( | _ |
Error::msg( format!(
"invalid {flag_name} value: {raw}\nExpected unsigned integer{hint}"
) )
)
}
fn parse_value_flag(
token : &str,
tokens : &[ String ],
next : usize,
parsed : &mut CliArgs,
) -> Result< bool >
{
match token
{
"--effort" =>
{
parsed.effort = Some(
parse_effort_level( next_value( tokens, next, "--effort" )? )?
);
}
"--system-prompt" =>
{
parsed.system_prompt = Some( next_value( tokens, next, "--system-prompt" )?.to_string() );
}
"--append-system-prompt" =>
{
parsed.append_system_prompt = Some( next_value( tokens, next, "--append-system-prompt" )?.to_string() );
}
"--model" =>
{
parsed.model = Some( next_value( tokens, next, "--model" )?.to_string() );
}
"--max-tokens" =>
{
parsed.max_tokens = Some( parse_token_limit( next_value( tokens, next, "--max-tokens" )? )? );
}
"--session-dir" =>
{
parsed.session_dir = Some( next_value( tokens, next, "--session-dir" )?.to_string() );
}
"--dir" =>
{
parsed.dir = Some( next_value( tokens, next, "--dir" )?.to_string() );
}
"--json-schema" =>
{
parsed.json_schema = Some( next_value( tokens, next, "--json-schema" )?.to_string() );
}
"--mcp-config" =>
{
parsed.mcp_config.push( next_value( tokens, next, "--mcp-config" )?.to_string() );
}
"--file" =>
{
parsed.file = Some( next_value( tokens, next, "--file" )?.to_string() );
}
"--subdir" =>
{
let val = next_value( tokens, next, "--subdir" )?;
if val.contains( '/' )
{
return Err( Error::msg(
"--subdir must be a single directory name component (no '/' separators)"
) );
}
parsed.subdir = Some( val.to_string() );
}
"--verbosity" =>
{
let raw = next_value( tokens, next, "--verbosity" )?;
parsed.verbosity = Some( raw.parse::< VerbosityLevel >().map_err( Error::msg )? );
}
_ => return parse_runner_value_flag( token, tokens, next, parsed ),
}
Ok( true )
}
fn parse_runner_value_flag(
token : &str,
tokens : &[ String ],
next : usize,
parsed : &mut CliArgs,
) -> Result< bool >
{
match token
{
"--output-file" =>
{
parsed.output_file = Some( next_value( tokens, next, "--output-file" )?.to_string() );
}
"--expect" =>
{
parsed.expect = Some( next_value( tokens, next, "--expect" )?.to_string() );
}
"--expect-strategy" =>
{
parsed.expect_strategy = Some(
parse_expect_strategy( next_value( tokens, next, "--expect-strategy" )? )?
);
}
"--expect-retries" =>
{
parsed.expect_retries = Some(
parse_u8_bounded( next_value( tokens, next, "--expect-retries" )?, "--expect-retries" )?
);
}
"--max-sessions" =>
{
parsed.max_sessions = Some(
parse_u32_flag( next_value( tokens, next, "--max-sessions" )?, "--max-sessions", " (0 = unlimited)" )?
);
}
"--retry-on-rate-limit" =>
{
parsed.retry_on_rate_limit = Some(
parse_u8_bounded( next_value( tokens, next, "--retry-on-rate-limit" )?, "--retry-on-rate-limit" )?
);
}
"--retry-delay" =>
{
parsed.retry_delay = Some(
parse_u32_flag( next_value( tokens, next, "--retry-delay" )?, "--retry-delay", " (seconds)" )?
);
}
"--timeout" =>
{
parsed.timeout = Some(
parse_u32_flag( next_value( tokens, next, "--timeout" )?, "--timeout", " (seconds; 0 = unlimited)" )?
);
}
_ => return Ok( false ),
}
Ok( true )
}
#[ allow( clippy::too_many_lines ) ]
pub( crate ) fn parse_args( tokens : &[ String ] ) -> Result< CliArgs >
{
if tokens.iter().any( | t | t == "--help" || t == "-h" )
{
return Ok( CliArgs
{
help : true,
message : None,
print_mode : false,
interactive : false,
new_session : false,
model : None,
verbose : false,
no_skip_permissions : false,
max_tokens : None,
session_dir : None,
dir : None,
dry_run : false,
trace : false,
verbosity : None,
system_prompt : None,
append_system_prompt : None,
no_ultrathink : false,
effort : None,
no_effort_max : false,
no_chrome : false,
no_persist : false,
json_schema : None,
mcp_config : Vec::new(),
file : None,
strip_fences : false,
keep_claudecode : false,
subdir : None,
output_file : None,
expect : None,
expect_strategy : None,
expect_retries : None,
max_sessions : None,
retry_on_rate_limit : None,
retry_delay : None,
timeout : None,
} );
}
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;
}
"--no-effort-max" =>
{
parsed.no_effort_max = true;
}
"--no-chrome" =>
{
parsed.no_chrome = true;
}
"--no-persist" =>
{
parsed.no_persist = true;
}
"--strip-fences" =>
{
parsed.strip_fences = true;
}
"--keep-claudecode" =>
{
parsed.keep_claudecode = true;
}
"--" =>
{
positional.extend( tokens[ i + 1 .. ].iter().filter( | t | !t.is_empty() ).cloned() );
break;
}
s if s.starts_with( '-' ) =>
{
if parse_value_flag( s, tokens, i + 1, &mut parsed )?
{
i += 1; }
else
{
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 )
}