use anyhow::Result;
use claude_wrapper::types::QueryResult;
use claude_wrapper::{Claude, QueryCommand};
use crate::cli::{AskArgs, EffortLevel, PermMode};
use crate::session::{apply_session, derive_session_name};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Session {
#[default]
Fresh,
Continue,
Resume(String),
WithId(String),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Permissions {
#[default]
ReadOnly,
Writable,
FullAuto,
}
#[derive(Debug, Clone, Default)]
pub struct Config {
pub prompt: String,
pub model: Option<String>,
pub fallback_model: Option<String>,
pub effort: Option<EffortLevel>,
pub agent: Option<String>,
pub permissions: Permissions,
pub permission_mode: Option<PermMode>,
pub allow_tools: Vec<String>,
pub deny_tools: Vec<String>,
pub session: Session,
pub fork: bool,
pub worktree: Option<Option<String>>,
pub max_turns: Option<u32>,
pub max_budget_usd: Option<f64>,
pub timeout_secs: Option<u64>,
pub json_schema: Option<String>,
pub system_prompt: Option<String>,
pub append_system_prompt: Option<String>,
pub agent_notice: Option<String>,
pub no_agent_notice: bool,
pub no_retry: bool,
pub no_session_persistence: bool,
pub bare: bool,
pub safe_mode: bool,
pub add_dir: Vec<String>,
pub mcp_config: Vec<String>,
pub strict_mcp_config: bool,
}
impl Config {
pub fn new(prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
..Self::default()
}
}
fn to_ask_args(&self) -> AskArgs {
let (continue_session, session_id) = match &self.session {
Session::Fresh => (None, None),
Session::Continue => (Some(None), None),
Session::Resume(id) => (Some(Some(id.clone())), None),
Session::WithId(id) => (None, Some(id.clone())),
};
let (writable, full_auto) = match self.permissions {
Permissions::ReadOnly => (false, false),
Permissions::Writable => (true, false),
Permissions::FullAuto => (false, true),
};
AskArgs {
model: self.model.clone(),
fallback_model: self.fallback_model.clone(),
effort: self.effort,
agent: self.agent.clone(),
writable,
full_auto,
permission_mode: self.permission_mode,
allow_tool: self.allow_tools.clone(),
deny_tool: self.deny_tools.clone(),
continue_session,
session_id,
fork: self.fork,
worktree: self.worktree.clone(),
max_turns: self.max_turns,
max_budget_usd: self.max_budget_usd,
json_schema: self.json_schema.clone(),
system_prompt: self.system_prompt.clone(),
append_system_prompt: self.append_system_prompt.clone(),
agent_notice: self.agent_notice.clone(),
no_agent_notice: self.no_agent_notice,
no_retry: self.no_retry,
no_session_persistence: self.no_session_persistence,
bare: self.bare,
safe_mode: self.safe_mode,
add_dir: self.add_dir.clone(),
mcp_config: self.mcp_config.clone(),
strict_mcp_config: self.strict_mcp_config,
..AskArgs::default()
}
}
}
pub async fn run(config: &Config) -> Result<QueryResult> {
let mut builder = Claude::builder();
if let Some(secs) = config.timeout_secs
&& secs > 0
{
builder = builder.timeout_secs(secs);
}
let claude = builder.build()?;
let mut result = execute(&config.to_ask_args(), &config.prompt, &claude).await?;
if config.json_schema.is_some() {
crate::output::surface_structured_output(&mut result);
}
Ok(result)
}
pub(crate) async fn execute(args: &AskArgs, prompt: &str, claude: &Claude) -> Result<QueryResult> {
let name = derive_session_name(prompt);
let result = apply_session(
QueryCommand::new(prompt.to_string())
.name(name)
.prompt_via_stdin(true),
args,
)
.execute_json(claude)
.await?;
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_ask_args_defaults_are_safe() {
let args = Config::new("hi").to_ask_args();
assert!(!args.writable && !args.full_auto, "default is read-only");
assert!(
args.continue_session.is_none() && args.session_id.is_none(),
"default is a fresh session"
);
assert!(args.worktree.is_none() && !args.fork);
assert!(args.max_turns.is_none() && args.max_budget_usd.is_none());
assert!(args.fallback_model.is_none() && args.effort.is_none());
assert!(args.agent.is_none() && args.permission_mode.is_none());
assert!(!args.no_retry && !args.bare && !args.safe_mode);
assert!(!args.no_session_persistence && !args.strict_mcp_config);
assert!(args.add_dir.is_empty() && args.mcp_config.is_empty());
assert!(!args.no_agent_notice && args.agent_notice.is_none());
assert!(args.system_prompt.is_none() && args.append_system_prompt.is_none());
}
#[test]
fn to_ask_args_maps_session_variants() {
let resume = Config {
session: Session::Resume("abc123".into()),
fork: true,
..Config::new("p")
}
.to_ask_args();
assert_eq!(resume.continue_session, Some(Some("abc123".to_string())));
assert!(resume.session_id.is_none() && resume.fork);
let cont = Config {
session: Session::Continue,
..Config::new("p")
}
.to_ask_args();
assert_eq!(cont.continue_session, Some(None));
let with_id = Config {
session: Session::WithId("mine".into()),
..Config::new("p")
}
.to_ask_args();
assert_eq!(with_id.session_id, Some("mine".to_string()));
assert!(with_id.continue_session.is_none());
}
#[test]
fn to_ask_args_maps_permissions_and_tools() {
let writable = Config {
permissions: Permissions::Writable,
..Config::new("p")
}
.to_ask_args();
assert!(writable.writable && !writable.full_auto);
let auto = Config {
permissions: Permissions::FullAuto,
allow_tools: vec!["Bash(git:*)".into()],
deny_tools: vec!["Write".into()],
..Config::new("p")
}
.to_ask_args();
assert!(auto.full_auto);
assert_eq!(auto.allow_tool, vec!["Bash(git:*)".to_string()]);
assert_eq!(auto.deny_tool, vec!["Write".to_string()]);
}
#[test]
fn to_ask_args_maps_the_full_run_surface() {
let cfg = Config {
fallback_model: Some("claude-opus-4-8".into()),
effort: Some(EffortLevel::High),
agent: Some("reviewer".into()),
permission_mode: Some(PermMode::Plan),
system_prompt: Some("be terse".into()),
append_system_prompt: Some("cite files".into()),
agent_notice: Some("custom".into()),
no_agent_notice: true,
no_retry: true,
no_session_persistence: true,
bare: true,
safe_mode: true,
add_dir: vec!["/repo".into()],
mcp_config: vec!["mcp.json".into()],
strict_mcp_config: true,
..Config::new("p")
};
let a = cfg.to_ask_args();
assert_eq!(a.fallback_model.as_deref(), Some("claude-opus-4-8"));
assert!(matches!(a.effort, Some(EffortLevel::High)));
assert_eq!(a.agent.as_deref(), Some("reviewer"));
assert!(matches!(a.permission_mode, Some(PermMode::Plan)));
assert_eq!(a.system_prompt.as_deref(), Some("be terse"));
assert_eq!(a.append_system_prompt.as_deref(), Some("cite files"));
assert_eq!(a.agent_notice.as_deref(), Some("custom"));
assert!(a.no_agent_notice && a.no_retry && a.no_session_persistence);
assert!(a.bare && a.safe_mode && a.strict_mcp_config);
assert_eq!(a.add_dir, vec!["/repo".to_string()]);
assert_eq!(a.mcp_config, vec!["mcp.json".to_string()]);
}
}