main
John Lauer bridge-rs: Rust bridge vertical slice (health via ab, agent file-IPC, Phase 1 verbs) — verified live on AdomLapper; doc… 0be1503 1mo ago
//! Loopback client for Adom Bridge's direct API — the bridge calls ab for everything
//! desktop-shaped (windows, dialogs, screenshots, UIA, shell) instead of re-implementing Win32.
use serde_json::{json, Value};

pub fn call(command: &str, args: Value, forwarded: &[(String, String)], timeout_s: u64) -> Value {
    let base = std::env::var("ADOM_DIRECT_API_URL").unwrap_or_else(|_| "http://127.0.0.1:24680".into());
    let url = format!("{}/command", base.trim_end_matches('/'));
    let mut args = args;
    if forwarded.is_empty() {
        if let Some(o) = args.as_object_mut() {
            o.entry("caller").or_insert(json!({"aiThread": "altium-bridge", "source": "bridge"}));
        }
    }
    let mut req = ureq::post(&url)
        .timeout(std::time::Duration::from_secs(timeout_s))
        .set("Content-Type", "application/json");
    if let Ok(tok) = std::env::var("ADOM_BRIDGE_TOKEN") {
        req = req.set("X-Adom-Bridge-Token", &tok);
    }
    for (k, v) in forwarded {
        req = req.set(k, v);
    }
    req = req.set("X-Adom-Caller-Delegate", "altium");
    match req.send_json(json!({"command": command, "args": args})) {
        Ok(r) => unwrap_output(r.into_json::<Value>().unwrap_or_else(|e| json!({"success": false, "error": format!("ab reply not JSON: {e}")}))),
        Err(ureq::Error::Status(code, r)) => {
            let body = r.into_string().unwrap_or_default();
            json!({"success": false, "error": format!("ab HTTP {code}: {}", body.chars().take(200).collect::<String>()), "errorCode": "ab_callback_failed"})
        }
        Err(e) => json!({"success": false, "error": format!("ab unreachable: {e}"), "errorCode": "ab_unreachable"}),
    }
}

/// ab's direct API returns {error, output, status, success} with the verb's real result JSON-encoded
/// as a STRING in `output`. Surface the inner object, keeping the outer status fields.
fn unwrap_output(v: Value) -> Value {
    let inner = v.get("output").and_then(|o| o.as_str()).and_then(|s| serde_json::from_str::<Value>(s).ok());
    match inner {
        Some(Value::Object(mut m)) => {
            if let Some(o) = v.as_object() {
                for k in ["success", "status", "error", "errorCode"] {
                    if let Some(x) = o.get(k) { m.entry(k.to_string()).or_insert(x.clone()); }
                }
            }
            Value::Object(m)
        }
        _ => v,
    }
}