claude-code-cli-acp 0.1.1

An ACP-compatible adapter for the real Claude Code CLI
Documentation
use std::{path::PathBuf, str::FromStr, time::Duration};

use clap::ValueEnum;
use serde::Serialize;
use uuid::Uuid;

use crate::session::manager::{ManagedSession, SessionManager, TurnOptions};

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
    Text,
    Json,
    StreamJson,
}

impl FromStr for OutputFormat {
    type Err = String;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value {
            "text" => Ok(Self::Text),
            "json" => Ok(Self::Json),
            "stream-json" => Ok(Self::StreamJson),
            other => Err(format!("unsupported output format: {other}")),
        }
    }
}

#[derive(Debug, Clone)]
pub struct PrintRequest {
    pub prompt: String,
    pub output_format: OutputFormat,
    pub resume: Option<String>,
    pub continue_last: bool,
    pub session_id: Option<String>,
    pub cwd: Option<PathBuf>,
    pub model: Option<String>,
    pub permission_mode: Option<String>,
    pub timeout: Duration,
    pub attach_on_timeout: bool,
    pub attach_on_permission: bool,
}

#[derive(Debug, Serialize)]
struct JsonOutput {
    session_id: String,
    text: String,
    model: Option<String>,
}

pub async fn run(request: PrintRequest) -> anyhow::Result<()> {
    let cwd = request.cwd.clone().unwrap_or(std::env::current_dir()?);
    let session_id = request
        .session_id
        .clone()
        .unwrap_or_else(|| Uuid::new_v4().to_string());
    let manager = SessionManager::new();
    let session = manager.create_print_session(session_id.clone(), cwd, request.model.clone())?;
    let turn = session
        .prompt(
            request.prompt,
            TurnOptions {
                timeout: request.timeout,
                model: request.model,
                permission_mode: request.permission_mode,
                resume: request.resume,
                continue_last: request.continue_last,
                initial_prompt_argument: false,
                attach_on_timeout: request.attach_on_timeout,
                attach_on_permission: request.attach_on_permission,
            },
        )
        .await?;
    let emit_result = emit_output(
        request.output_format,
        &session,
        &turn.final_text(),
        turn.model(),
    )
    .await;
    let shutdown_result = session.shutdown().await;
    emit_result?;
    shutdown_result
}

async fn emit_output(
    format: OutputFormat,
    session: &ManagedSession,
    text: &str,
    model: Option<String>,
) -> anyhow::Result<()> {
    match format {
        OutputFormat::Text => {
            write_stdout(format!("{text}\n"))?;
        }
        OutputFormat::Json => {
            write_stdout(format!(
                "{}\n",
                serde_json::to_string(&JsonOutput {
                    session_id: session.session_id().0.to_string(),
                    text: text.to_string(),
                    model,
                })?
            ))?;
        }
        OutputFormat::StreamJson => {
            write_stdout(format!(
                "{}\n",
                serde_json::to_string(&serde_json::json!({
                    "type": "message",
                    "session_id": session.session_id().0.to_string(),
                    "text": text,
                    "model": model,
                }))?
            ))?;
        }
    }
    Ok(())
}

fn write_stdout(output: String) -> anyhow::Result<()> {
    use std::io::Write;
    std::io::stdout().write_all(output.as_bytes())?;
    Ok(())
}