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 ) max_sessions : Option< u32 >,
pub( crate ) retry_on_transient : Option< u8 >,
pub( crate ) transient_delay : Option< u32 >,
pub( crate ) timeout : Option< u32 >,
pub( crate ) retry_on_account : Option< u8 >,
pub( crate ) account_delay : Option< u32 >,
pub( crate ) retry_on_auth : Option< u8 >,
pub( crate ) auth_delay : Option< u32 >,
pub( crate ) retry_on_service : Option< u8 >,
pub( crate ) service_delay : Option< u32 >,
pub( crate ) retry_on_process : Option< u8 >,
pub( crate ) process_delay : Option< u32 >,
pub( crate ) retry_on_validation : Option< u8 >,
pub( crate ) validation_delay : Option< u32 >,
pub( crate ) retry_on_runner : Option< u8 >,
pub( crate ) runner_delay : Option< u32 >,
pub( crate ) retry_on_unknown : Option< u8 >,
pub( crate ) unknown_delay : Option< u32 >,
pub( crate ) retry_override : Option< u8 >,
pub( crate ) retry_override_delay : Option< u32 >,
pub( crate ) retry_default : Option< u8 >,
pub( crate ) retry_default_delay : Option< u32 >,
pub( crate ) output_format : Option< String >,
pub( crate ) max_turns : Option< String >,
pub( crate ) allowed_tools : Option< String >,
pub( crate ) disallowed_tools : Option< String >,
pub( crate ) max_budget_usd : Option< String >,
pub( crate ) add_dir : Option< String >,
pub( crate ) fallback_model : Option< String >,
pub( crate ) output_style : Option< String >,
}
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 )? );
}
"--output-format" =>
{
parsed.output_format = Some( next_value( tokens, next, "--output-format" )?.to_string() );
}
"--max-turns" =>
{
parsed.max_turns = Some( next_value( tokens, next, "--max-turns" )?.to_string() );
}
"--allowed-tools" =>
{
parsed.allowed_tools = Some( next_value( tokens, next, "--allowed-tools" )?.to_string() );
}
"--disallowed-tools" =>
{
parsed.disallowed_tools = Some( next_value( tokens, next, "--disallowed-tools" )?.to_string() );
}
"--max-budget-usd" =>
{
parsed.max_budget_usd = Some( next_value( tokens, next, "--max-budget-usd" )?.to_string() );
}
"--add-dir" =>
{
parsed.add_dir = Some( next_value( tokens, next, "--add-dir" )?.to_string() );
}
"--fallback-model" =>
{
parsed.fallback_model = Some( next_value( tokens, next, "--fallback-model" )?.to_string() );
}
_ => return parse_runner_value_flag( token, tokens, next, parsed ),
}
Ok( true )
}
#[ allow( clippy::too_many_lines ) ] 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" )? )?
);
}
"--max-sessions" =>
{
parsed.max_sessions = Some(
parse_u32_flag( next_value( tokens, next, "--max-sessions" )?, "--max-sessions", " (0 = unlimited)" )?
);
}
"--retry-on-transient" =>
{
parsed.retry_on_transient = Some(
parse_u8_bounded( next_value( tokens, next, "--retry-on-transient" )?, "--retry-on-transient" )?
);
}
"--transient-delay" =>
{
parsed.transient_delay = Some(
parse_u32_flag( next_value( tokens, next, "--transient-delay" )?, "--transient-delay", " (seconds)" )?
);
}
"--timeout" =>
{
parsed.timeout = Some(
parse_u32_flag( next_value( tokens, next, "--timeout" )?, "--timeout", " (seconds; 0 = unlimited)" )?
);
}
"--retry-on-account" =>
{
parsed.retry_on_account = Some(
parse_u8_bounded( next_value( tokens, next, "--retry-on-account" )?, "--retry-on-account" )?
);
}
"--account-delay" =>
{
parsed.account_delay = Some(
parse_u32_flag( next_value( tokens, next, "--account-delay" )?, "--account-delay", " (seconds)" )?
);
}
"--retry-on-auth" =>
{
parsed.retry_on_auth = Some(
parse_u8_bounded( next_value( tokens, next, "--retry-on-auth" )?, "--retry-on-auth" )?
);
}
"--auth-delay" =>
{
parsed.auth_delay = Some(
parse_u32_flag( next_value( tokens, next, "--auth-delay" )?, "--auth-delay", " (seconds)" )?
);
}
"--retry-on-service" =>
{
parsed.retry_on_service = Some(
parse_u8_bounded( next_value( tokens, next, "--retry-on-service" )?, "--retry-on-service" )?
);
}
"--service-delay" =>
{
parsed.service_delay = Some(
parse_u32_flag( next_value( tokens, next, "--service-delay" )?, "--service-delay", " (seconds)" )?
);
}
"--retry-on-process" =>
{
parsed.retry_on_process = Some(
parse_u8_bounded( next_value( tokens, next, "--retry-on-process" )?, "--retry-on-process" )?
);
}
"--process-delay" =>
{
parsed.process_delay = Some(
parse_u32_flag( next_value( tokens, next, "--process-delay" )?, "--process-delay", " (seconds)" )?
);
}
"--retry-on-validation" =>
{
parsed.retry_on_validation = Some(
parse_u8_bounded( next_value( tokens, next, "--retry-on-validation" )?, "--retry-on-validation" )?
);
}
"--validation-delay" =>
{
parsed.validation_delay = Some(
parse_u32_flag( next_value( tokens, next, "--validation-delay" )?, "--validation-delay", " (seconds)" )?
);
}
"--retry-on-runner" =>
{
parsed.retry_on_runner = Some(
parse_u8_bounded( next_value( tokens, next, "--retry-on-runner" )?, "--retry-on-runner" )?
);
}
"--runner-delay" =>
{
parsed.runner_delay = Some(
parse_u32_flag( next_value( tokens, next, "--runner-delay" )?, "--runner-delay", " (seconds)" )?
);
}
"--retry-on-unknown" =>
{
parsed.retry_on_unknown = Some(
parse_u8_bounded( next_value( tokens, next, "--retry-on-unknown" )?, "--retry-on-unknown" )?
);
}
"--unknown-delay" =>
{
parsed.unknown_delay = Some(
parse_u32_flag( next_value( tokens, next, "--unknown-delay" )?, "--unknown-delay", " (seconds)" )?
);
}
"--retry-override" =>
{
parsed.retry_override = Some(
parse_u8_bounded( next_value( tokens, next, "--retry-override" )?, "--retry-override" )?
);
}
"--retry-override-delay" =>
{
parsed.retry_override_delay = Some(
parse_u32_flag( next_value( tokens, next, "--retry-override-delay" )?, "--retry-override-delay", " (seconds)" )?
);
}
"--retry-default" =>
{
parsed.retry_default = Some(
parse_u8_bounded( next_value( tokens, next, "--retry-default" )?, "--retry-default" )?
);
}
"--retry-default-delay" =>
{
parsed.retry_default_delay = Some(
parse_u32_flag( next_value( tokens, next, "--retry-default-delay" )?, "--retry-default-delay", " (seconds)" )?
);
}
"--output-style" =>
{
let v = next_value( tokens, next, "--output-style" )?;
if !matches!( v, "summary" | "raw" )
{
return Err( Error::msg( format!(
"invalid output-style '{v}' — expected: summary, raw"
) ) );
}
parsed.output_style = Some( v.to_string() );
}
_ => 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,
max_sessions : None,
retry_on_transient : None,
transient_delay : None,
timeout : None,
retry_on_account : None,
account_delay : None,
retry_on_auth : None,
auth_delay : None,
retry_on_service : None,
service_delay : None,
retry_on_process : None,
process_delay : None,
retry_on_validation : None,
validation_delay : None,
retry_on_runner : None,
runner_delay : None,
retry_on_unknown : None,
unknown_delay : None,
retry_override : None,
retry_override_delay : None,
retry_default : None,
retry_default_delay : None,
output_format : None,
max_turns : None,
allowed_tools : None,
disallowed_tools : None,
max_budget_usd : None,
add_dir : None,
fallback_model : None,
output_style : 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 )
}