use anyhow::Result;
use claude_wrapper::types::QueryResult;
use claude_wrapper::{Claude, Effort, PermissionMode, QueryCommand};
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<Effort>,
pub agent: Option<String>,
pub permissions: Permissions,
pub permission_mode: Option<PermissionMode>,
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()
}
}
}
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, &claude).await?;
if config.json_schema.is_some() {
surface_structured_output(&mut result);
}
Ok(result)
}
pub async fn execute(config: &Config, claude: &Claude) -> Result<QueryResult> {
let name = derive_session_name(&config.prompt);
let result = apply_session(
QueryCommand::new(config.prompt.clone())
.name(name)
.prompt_via_stdin(true),
config,
)
.execute_json(claude)
.await?;
Ok(result)
}
pub fn surface_structured_output(result: &mut QueryResult) {
if result
.extra
.get("structured_output")
.is_some_and(|v| !v.is_null())
{
return;
}
let Some(inner) = unfence_single_block(&result.result) else {
return;
};
let Ok(value) = serde_json::from_str::<serde_json::Value>(&inner) else {
return;
};
result.extra.insert("structured_output".to_string(), value);
result.result = inner;
}
fn unfence_single_block(text: &str) -> Option<String> {
let trimmed = text.trim();
let mut lines = trimmed.lines();
let first = lines.next()?;
if !first.starts_with("```") {
return None;
}
let rest: Vec<&str> = lines.collect();
let (last, body) = rest.split_last()?;
if last.trim() != "```" {
return None;
}
Some(body.join("\n").trim().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_new_defaults_are_safe() {
let c = Config::new("hi");
assert_eq!(c.prompt, "hi");
assert!(matches!(c.permissions, Permissions::ReadOnly));
assert!(matches!(c.session, Session::Fresh));
assert!(!c.fork && c.worktree.is_none());
assert!(c.max_turns.is_none() && c.max_budget_usd.is_none());
assert!(!c.no_agent_notice && c.agent_notice.is_none());
}
fn query_result_body(body: &str) -> QueryResult {
serde_json::from_value(serde_json::json!({
"result": body,
"session_id": "s1",
"is_error": false,
}))
.expect("build QueryResult fixture")
}
#[test]
fn surface_structured_output_unfences_json_block() {
let mut result = query_result_body("```json\n{\"answer\": \"Paris\"}\n```");
surface_structured_output(&mut result);
assert_eq!(
result.extra.get("structured_output").unwrap()["answer"],
"Paris"
);
assert_eq!(result.result, "{\"answer\": \"Paris\"}");
}
#[test]
fn surface_structured_output_handles_untagged_fence() {
let mut result = query_result_body("```\n[1, 2, 3]\n```");
surface_structured_output(&mut result);
assert_eq!(
result.extra.get("structured_output").unwrap(),
&serde_json::json!([1, 2, 3])
);
}
#[test]
fn surface_structured_output_noop_when_already_present() {
let mut result: QueryResult = serde_json::from_value(serde_json::json!({
"result": "```json\n{\"from\": \"body\"}\n```",
"session_id": "s1",
"is_error": false,
"structured_output": {"from": "native"},
}))
.expect("build QueryResult fixture");
surface_structured_output(&mut result);
assert_eq!(
result.extra.get("structured_output").unwrap()["from"],
"native"
);
assert!(result.result.contains("```"));
}
#[test]
fn surface_structured_output_noop_on_non_fenced_text() {
let mut result = query_result_body("The answer is 42.");
surface_structured_output(&mut result);
assert!(!result.extra.contains_key("structured_output"));
assert_eq!(result.result, "The answer is 42.");
}
#[test]
fn surface_structured_output_noop_when_fence_is_not_json() {
let mut result = query_result_body("```rust\nfn main() {}\n```");
surface_structured_output(&mut result);
assert!(!result.extra.contains_key("structured_output"));
assert!(result.result.contains("fn main"));
}
#[test]
fn surface_structured_output_noop_on_prose_around_block() {
let mut result =
query_result_body("Here you go:\n```json\n{\"a\": 1}\n```\nHope that helps.");
surface_structured_output(&mut result);
assert!(!result.extra.contains_key("structured_output"));
}
}