agent-relay 0.4.1

Agent-to-agent messaging for AI coding tools. Local or networked — run a relay server and let Claude talk to Gemini across the internet.
Documentation
//! HTTP client for connecting to a remote relay server.

use crate::hub;
use crate::{AgentRegistration, Message};

pub struct RemoteRelay {
    base_url: String,
}

impl RemoteRelay {
    pub fn new(server_url: &str) -> Self {
        let base_url = server_url.trim_end_matches('/').to_string();
        Self { base_url }
    }

    pub fn health(&self) -> Result<String, String> {
        let resp = ureq::get(&format!("{}/health", self.base_url))
            .call()
            .map_err(|e| format!("Connection failed: {}", e))?;
        resp.into_string().map_err(|e| format!("Read error: {}", e))
    }

    pub fn register(
        &self,
        agent_id: &str,
        session_id: &str,
        pid: u32,
    ) -> Result<AgentRegistration, String> {
        let resp = ureq::post(&format!("{}/agents/register", self.base_url))
            .send_json(serde_json::json!({
                "session_id": session_id,
                "agent_id": agent_id,
                "pid": pid,
            }))
            .map_err(|e| format!("Request failed: {}", e))?;

        let body: AgentRegistration = resp
            .into_json()
            .map_err(|e| format!("Parse error: {}", e))?;
        Ok(body)
    }

    pub fn unregister(&self, session_id: &str) -> Result<(), String> {
        ureq::post(&format!("{}/agents/unregister", self.base_url))
            .send_json(serde_json::json!({"session_id": session_id}))
            .map_err(|e| format!("Request failed: {}", e))?;
        Ok(())
    }

    pub fn agents(&self) -> Result<Vec<AgentRegistration>, String> {
        let resp = ureq::get(&format!("{}/agents", self.base_url))
            .call()
            .map_err(|e| format!("Request failed: {}", e))?;

        let body: Vec<AgentRegistration> = resp
            .into_json()
            .map_err(|e| format!("Parse error: {}", e))?;
        Ok(body)
    }

    pub fn send(
        &self,
        from_session: &str,
        from_agent: &str,
        to_session: Option<&str>,
        content: &str,
    ) -> Result<Message, String> {
        self.send_scoped(from_session, from_agent, to_session, content, None, None)
    }

    pub fn send_scoped(
        &self,
        from_session: &str,
        from_agent: &str,
        to_session: Option<&str>,
        content: &str,
        team_id: Option<&str>,
        channel: Option<&str>,
    ) -> Result<Message, String> {
        let resp = ureq::post(&format!("{}/messages/send", self.base_url))
            .send_json(serde_json::json!({
                "from_session": from_session,
                "from_agent": from_agent,
                "to_session": to_session,
                "content": content,
                "team_id": team_id,
                "channel": channel,
            }))
            .map_err(|e| format!("Request failed: {}", e))?;

        let body: Message = resp
            .into_json()
            .map_err(|e| format!("Parse error: {}", e))?;
        Ok(body)
    }

    pub fn inbox(&self, session_id: &str, limit: usize) -> Result<Vec<Message>, String> {
        self.inbox_scoped(session_id, limit, None, None)
    }

    pub fn inbox_scoped(
        &self,
        session_id: &str,
        limit: usize,
        team: Option<&str>,
        channel: Option<&str>,
    ) -> Result<Vec<Message>, String> {
        let mut url = format!(
            "{}/messages/inbox?session={}&limit={}",
            self.base_url, session_id, limit
        );
        if let Some(t) = team {
            url.push_str(&format!("&team={}", t));
        }
        if let Some(c) = channel {
            url.push_str(&format!("&channel={}", c));
        }
        let resp = ureq::get(&url)
            .call()
            .map_err(|e| format!("Request failed: {}", e))?;

        let body: Vec<Message> = resp
            .into_json()
            .map_err(|e| format!("Parse error: {}", e))?;
        Ok(body)
    }

    pub fn unread_count(&self, session_id: &str) -> Result<u64, String> {
        let resp = ureq::get(&format!(
            "{}/messages/unread?session={}",
            self.base_url, session_id
        ))
        .call()
        .map_err(|e| format!("Request failed: {}", e))?;

        let body: serde_json::Value = resp
            .into_json()
            .map_err(|e| format!("Parse error: {}", e))?;
        Ok(body["count"].as_u64().unwrap_or(0))
    }

    // ── Auth ──

    /// Sign up for a new account. Returns (user_id, api_key).
    pub fn signup(&self, email: &str, name: &str) -> Result<(String, String), String> {
        let resp = ureq::post(&format!("{}/auth/signup", self.base_url))
            .send_json(serde_json::json!({ "email": email, "name": name }))
            .map_err(|e| format!("Request failed: {}", e))?;

        let body: serde_json::Value = resp
            .into_json()
            .map_err(|e| format!("Parse error: {}", e))?;

        let user_id = body["user"]["id"]
            .as_str()
            .ok_or("Missing user.id in response")?
            .to_string();
        let api_key = body["api_key"]
            .as_str()
            .ok_or("Missing api_key in response")?
            .to_string();

        Ok((user_id, api_key))
    }

    /// Log in with an existing email. Returns a new api_key.
    pub fn login(&self, email: &str) -> Result<String, String> {
        let resp = ureq::post(&format!("{}/auth/login", self.base_url))
            .send_json(serde_json::json!({ "email": email }))
            .map_err(|e| format!("Request failed: {}", e))?;

        let body: serde_json::Value = resp
            .into_json()
            .map_err(|e| format!("Parse error: {}", e))?;

        body["api_key"]
            .as_str()
            .map(|s| s.to_string())
            .ok_or("Missing api_key in response".to_string())
    }

    // ── Teams ──

    pub fn create_team(&self, api_key: &str, name: &str) -> Result<hub::Team, String> {
        let resp = ureq::post(&format!("{}/teams", self.base_url))
            .set("Authorization", &format!("Bearer {}", api_key))
            .send_json(serde_json::json!({ "name": name }))
            .map_err(|e| format!("Request failed: {}", e))?;

        resp.into_json().map_err(|e| format!("Parse error: {}", e))
    }

    pub fn list_teams(&self, api_key: &str) -> Result<Vec<hub::Team>, String> {
        let resp = ureq::get(&format!("{}/teams", self.base_url))
            .set("Authorization", &format!("Bearer {}", api_key))
            .call()
            .map_err(|e| format!("Request failed: {}", e))?;

        resp.into_json().map_err(|e| format!("Parse error: {}", e))
    }

    pub fn invite(
        &self,
        api_key: &str,
        team_id: &str,
        email: Option<&str>,
    ) -> Result<String, String> {
        let resp = ureq::post(&format!("{}/teams/{}/invite", self.base_url, team_id))
            .set("Authorization", &format!("Bearer {}", api_key))
            .send_json(serde_json::json!({ "email": email }))
            .map_err(|e| format!("Request failed: {}", e))?;

        let body: serde_json::Value = resp
            .into_json()
            .map_err(|e| format!("Parse error: {}", e))?;

        body["token"]
            .as_str()
            .map(|s| s.to_string())
            .ok_or("Missing token in response".to_string())
    }

    pub fn join_team(&self, api_key: &str, token: &str) -> Result<(), String> {
        ureq::post(&format!("{}/teams/join", self.base_url))
            .set("Authorization", &format!("Bearer {}", api_key))
            .send_json(serde_json::json!({ "token": token }))
            .map_err(|e| format!("Request failed: {}", e))?;
        Ok(())
    }

    pub fn list_members(
        &self,
        api_key: &str,
        team_id: &str,
    ) -> Result<Vec<hub::TeamMember>, String> {
        let resp = ureq::get(&format!("{}/teams/{}/members", self.base_url, team_id))
            .set("Authorization", &format!("Bearer {}", api_key))
            .call()
            .map_err(|e| format!("Request failed: {}", e))?;

        resp.into_json().map_err(|e| format!("Parse error: {}", e))
    }

    pub fn kick_member(
        &self,
        api_key: &str,
        team_id: &str,
        user_id: &str,
    ) -> Result<(), String> {
        ureq::post(&format!("{}/teams/{}/kick", self.base_url, team_id))
            .set("Authorization", &format!("Bearer {}", api_key))
            .send_json(serde_json::json!({ "user_id": user_id }))
            .map_err(|e| format!("Request failed: {}", e))?;
        Ok(())
    }

    // ── Channels ──

    pub fn create_channel(
        &self,
        api_key: &str,
        team_id: &str,
        name: &str,
    ) -> Result<hub::Channel, String> {
        let resp = ureq::post(&format!(
            "{}/teams/{}/channels",
            self.base_url, team_id
        ))
        .set("Authorization", &format!("Bearer {}", api_key))
        .send_json(serde_json::json!({ "name": name }))
        .map_err(|e| format!("Request failed: {}", e))?;

        resp.into_json().map_err(|e| format!("Parse error: {}", e))
    }

    pub fn list_channels(
        &self,
        api_key: &str,
        team_id: &str,
    ) -> Result<Vec<hub::Channel>, String> {
        let resp = ureq::get(&format!(
            "{}/teams/{}/channels",
            self.base_url, team_id
        ))
        .set("Authorization", &format!("Bearer {}", api_key))
        .call()
        .map_err(|e| format!("Request failed: {}", e))?;

        resp.into_json().map_err(|e| format!("Parse error: {}", e))
    }
}