bridge-echo 0.2.0

HTTP bridge for Claude Code CLI
use std::env;

const RESET: &str = "\x1b[0m";
const BOLD: &str = "\x1b[1m";
const DIM: &str = "\x1b[2m";
const BLUE: &str = "\x1b[38;5;75m";
const GREEN: &str = "\x1b[38;5;78m";
const ORANGE: &str = "\x1b[38;5;208m";
const RED: &str = "\x1b[38;5;203m";
const PURPLE: &str = "\x1b[38;5;141m";
const GRAY: &str = "\x1b[38;5;243m";
const CLEAR_SCREEN: &str = "\x1b[2J\x1b[H";

pub async fn run(once: bool) {
    let port = env::var("BRIDGE_ECHO_PORT").unwrap_or_else(|_| "3100".into());
    let url = format!("http://127.0.0.1:{port}/api/status");

    let client = reqwest::Client::new();

    loop {
        if !once {
            print!("{CLEAR_SCREEN}");
        }

        match client.get(&url).send().await {
            Ok(resp) if resp.status().is_success() => {
                let data: serde_json::Value = match resp.json().await {
                    Ok(v) => v,
                    Err(e) => {
                        eprintln!("failed to parse response: {e}");
                        if once {
                            std::process::exit(1);
                        }
                        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
                        continue;
                    }
                };
                render(&data);
            }
            Ok(resp) => {
                eprintln!("server returned {}", resp.status());
                if once {
                    std::process::exit(1);
                }
            }
            Err(e) => {
                println!("{BOLD}{BLUE}bridge-echo{RESET}");
                println!();
                println!("{RED}● offline{RESET} {DIM}{e}{RESET}");
                if once {
                    std::process::exit(1);
                }
            }
        }

        if once {
            break;
        }

        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    }
}

fn render(data: &serde_json::Value) {
    let active = data["active"].as_array();
    let active_count = active.map(|a| a.len()).unwrap_or(0);

    println!("{BOLD}{BLUE}bridge-echo{RESET}");
    println!();

    if active_count == 0 {
        println!("{GREEN}● idle{RESET}");
    } else {
        for req in active.unwrap() {
            let channel = req["channel"].as_str().unwrap_or("?");
            let preview = req["message_preview"].as_str().unwrap_or("");
            let elapsed = req["elapsed_secs"].as_u64().unwrap_or(0);

            let elapsed_str = fmt_duration(elapsed);
            let (color, label) = if elapsed >= 600 {
                (RED, "stuck")
            } else if elapsed >= 300 {
                (ORANGE, "active")
            } else {
                (GREEN, "active")
            };

            println!("{color}{label}{RESET}  {color}{elapsed_str}{RESET}  {PURPLE}{channel}{RESET}");
            if !preview.is_empty() {
                println!("  {GRAY}{preview}{RESET}");
            }
        }
    }
}

fn fmt_duration(secs: u64) -> String {
    let m = secs / 60;
    let s = secs % 60;
    if m > 0 {
        format!("{m}m {s:02}s")
    } else {
        format!("{s}s")
    }
}