bridge-echo 0.1.0

HTTP bridge for Claude Code CLI
use axum::extract::State;
use axum::http::StatusCode;
use axum::Json;
use serde::Deserialize;
use serde_json::{json, Value};
use tracing::{info, warn};

use crate::claude;
use crate::prompt;
use crate::state::AppState;

#[derive(Deserialize)]
pub struct ChatRequest {
    pub message: Option<String>,
    pub channel: Option<String>,
}

pub async fn chat(
    State(state): State<AppState>,
    Json(body): Json<ChatRequest>,
) -> (StatusCode, Json<Value>) {
    let message = match body.message.as_deref().map(str::trim) {
        Some(m) if !m.is_empty() => m.to_string(),
        _ => {
            return (
                StatusCode::BAD_REQUEST,
                Json(json!({"response": "Missing message"})),
            )
        }
    };

    let channel = body.channel.unwrap_or_else(|| "slack".into());

    let truncated = if message.len() > 120 {
        format!("{}...", &message[..120])
    } else {
        message.clone()
    };
    info!("[{channel}] Received: {truncated}");

    state.sessions.cleanup().await;

    let final_prompt = prompt::build(&message, &channel, &state.detector);

    if state.detector.detect(&message) {
        warn!("[{channel}] INJECTION DETECTED in message");
    }

    let session_id = state.sessions.get(&channel).await;

    let self_doc = state
        .config
        .self_path
        .as_deref()
        .and_then(|path| std::fs::read_to_string(path).ok());

    let response = claude::invoke(
        &state.config.claude_bin,
        &final_prompt,
        &state.config.home,
        state.config.timeout_secs,
        session_id.as_deref(),
        self_doc.as_deref(),
    )
    .await;

    if let Some(sid) = &response.session_id {
        state.sessions.set(&channel, sid.clone()).await;
    }

    let resp_truncated = if response.text.len() > 120 {
        format!("{}...", &response.text[..120])
    } else {
        response.text.clone()
    };
    info!("[{channel}] Response: {resp_truncated}");

    (StatusCode::OK, Json(json!({"response": response.text})))
}