[content] crates/cli/src/main.rs (rust)
    process_all_sessions, write_jsonl_output, ChatMessage, Tokenizer,
};

/// Python-compatible string wrapper for minijinja templates.
/// Provides methods like `startswith`, `endswith`, `strip`, etc.
#[derive(Debug, Clone)]
struct PyString(String);

impl std::fmt::Display for PyString {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl Object for PyString {
    fn repr(self: &Arc<Self>) -> minijinja::value::ObjectRepr {
        minijinja::value::ObjectRepr::Plain
    }

    fn call_method(
        self: &Arc<Self>,
        _state: &minijinja::State<'_, '_>,
        method: &str,
        args: &[Value],
    ) -> Result<Value, minijinja::Error> {
        match method {
            "startswith" => {
                let prefix = args.first()
                    .and_then(|v| v.as_str())
                    .ok_or_else(|| minijinja::Error::new(
                        minijinja::ErrorKind::InvalidOperation,
                        "startswith requires a string argument",
                    ))?;
                Ok(Value::from(self.0.starts_with(prefix)))
            }
            "endswith" => {
                let suffix = args.first()
                    .and_then(|v| v.as_str())
                    .ok_or_else(|| minijinja::Error::new(
                        minijinja::ErrorKind::InvalidOperation,
                        "endswith requires a string argument",
                    ))?;
                Ok(Value::from(self.0.ends_with(suffix)))
            }
            "strip" => {
                let chars = args.first().and_then(|v| v.as_str());
                let result = match chars {
                    Some(c) => self.0.trim_matches(|ch| c.contains(ch)).to_string(),
                    None => self.0.trim().to_string(),
                };
                Ok(Value::from_object(PyString(result)))
            }
            "lstrip" => {
                let chars = args.first().and_then(|v| v.as_str());
                let result = match chars {
                    Some(c) => self.0.trim_start_matches(|ch| c.contains(ch)).to_string(),
                    None => self.0.trim_start().to_string(),
                };
                Ok(Value::from_object(PyString(result)))
            }
            "rstrip" => {
                let chars = args.first().and_then(|v| v.as_str());
                let result = match chars {
                    Some(c) => self.0.trim_end_matches(|ch| c.contains(ch)).to_string(),
                    None => self.0.trim_end().to_string(),
                };
                Ok(Value::from_object(PyString(result)))
            }
            "split" => {
                let sep = args.first().and_then(|v| v.as_str());
                let parts: Vec<Value> = match sep {
                    Some(s) => self.0.split(s).map(|p| Value::from_object(PyString(p.to_string()))).collect(),
                    None => self.0.split_whitespace().map(|p| Value::from_object(PyString(p.to_string()))).collect(),
                };
                Ok(Value::from(parts))
            }
            "lower" => Ok(Value::from_object(PyString(self.0.to_lowercase()))),
            "upper" => Ok(Value::from_object(PyString(self.0.to_uppercase()))),
            "replace" => {
                let old = args.first()
                    .and_then(|v| v.as_str())
                    .ok_or_else(|| minijinja::Error::new(
                        minijinja::ErrorKind::InvalidOperation,
                        "replace requires two string arguments",
                    ))?;
                let new = args.get(1)
                    .and_then(|v| v.as_str())
                    .ok_or_else(|| minijinja::Error::new(
                        minijinja::ErrorKind::InvalidOperation,
                        "replace requires two string arguments",
                    ))?;
                Ok(Value::from_object(PyString(self.0.replace(old, new))))
            }
            _ => Err(minijinja::Error::new(
                minijinja::ErrorKind::UnknownMethod,
                format!("string has no method named {}", method),
            )),
        }
    }

    fn render(self: &Arc<Self>, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

// Make PyString work as a string in Jinja comparisons and concatenations
impl From<PyString> for Value {
    fn from(s: PyString) -> Self {
        Value::from_object(s)
    }
}