a3s 0.8.1

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
use super::model::canonical_model_name;
use crate::account_providers::cli_transport::{
    account_cli_system_prompt, complete_streaming, CliInvocation,
};
use a3s_code_core::llm::{Message, StreamEvent, ToolDefinition};
use anyhow::Result;
use std::ffi::OsString;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;

pub(crate) struct ClaudeCodeCliAdapter {
    model: String,
}

impl ClaudeCodeCliAdapter {
    pub(crate) fn new(model: &str) -> Self {
        Self {
            model: canonical_model_name(model),
        }
    }

    pub(crate) async fn complete_streaming(
        &self,
        messages: &[Message],
        system: Option<&str>,
        tools: &[ToolDefinition],
        cancel_token: CancellationToken,
    ) -> Result<mpsc::Receiver<StreamEvent>> {
        let appended_system = account_cli_system_prompt(system, tools, "Claude Code");
        let args = claude_cli_args(&self.model, appended_system.as_deref());
        let request_label = claude_cli_request_label(&args);
        let invocation = CliInvocation::new(
            "claude",
            args.into_iter().map(OsString::from).collect(),
            "claude-code-cli",
            self.model.clone(),
            request_label,
        );
        complete_streaming(invocation, messages, tools, cancel_token).await
    }
}

fn claude_cli_args(model: &str, appended_system: Option<&str>) -> Vec<String> {
    let mut args = vec![
        "-p".into(),
        "--safe-mode".into(),
        "--model".into(),
        canonical_model_name(model),
        "--output-format".into(),
        "stream-json".into(),
        "--verbose".into(),
        "--include-partial-messages".into(),
        "--tools".into(),
        String::new(),
        "--no-session-persistence".into(),
    ];
    if let Some(appended_system) =
        appended_system.filter(|appended_system| !appended_system.trim().is_empty())
    {
        args.push("--append-system-prompt".into());
        args.push(appended_system.to_string());
    }
    args
}

fn claude_cli_request_label(args: &[String]) -> String {
    let mut redacted = Vec::with_capacity(args.len());
    let mut redact_next = false;
    for arg in args {
        if redact_next {
            redacted.push("[system prompt redacted]".to_string());
            redact_next = false;
            continue;
        }
        redacted.push(arg.clone());
        if arg == "--append-system-prompt" || arg == "--system-prompt" {
            redact_next = true;
        }
    }
    format!("claude {}", redacted.join(" "))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::account_providers::cli_transport::account_cli_system_prompt;
    use serde_json::json;

    #[test]
    fn args_use_safe_text_only_streaming_mode() {
        let args = claude_cli_args(" claude-opus-4-8[1m] ", Some("secret system prompt"));

        assert!(args.contains(&"--safe-mode".to_string()));
        assert!(args.contains(&"--no-session-persistence".to_string()));
        assert!(args.contains(&"--include-partial-messages".to_string()));
        assert_eq!(
            args.windows(2)
                .find(|window| window[0] == "--model")
                .map(|window| window[1].as_str()),
            Some("claude-opus-4-8")
        );
        assert_eq!(
            args.windows(2)
                .find(|window| window[0] == "--tools")
                .map(|window| window[1].as_str()),
            Some("")
        );
        assert_eq!(
            claude_cli_request_label(&args),
            "claude -p --safe-mode --model claude-opus-4-8 --output-format stream-json --verbose --include-partial-messages --tools  --no-session-persistence --append-system-prompt [system prompt redacted]"
        );
    }

    #[test]
    fn shared_system_prompt_injects_host_tool_protocol() {
        let prompt = account_cli_system_prompt(
            Some("Be concise."),
            &[ToolDefinition {
                name: "read_file".into(),
                description: "Read a file".into(),
                parameters: json!({"type":"object"}),
            }],
            "Claude Code",
        )
        .unwrap();

        assert!(prompt.contains("# A3S System"));
        assert!(prompt.contains("Claude Code's own built-in tools"));
        assert!(prompt.contains("<function_calls>"));
    }
}