codex-mobile-bridge 0.2.6

Remote bridge and service manager for codex-mobile.
Documentation
use serde::{Deserialize, Serialize};

use super::helpers::now_millis;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiError {
    pub code: String,
    pub message: String,
}

impl ApiError {
    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
        Self {
            code: code.into(),
            message: message.into(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeRecord {
    pub runtime_id: String,
    pub display_name: String,
    pub codex_home: Option<String>,
    pub codex_binary: String,
    pub is_primary: bool,
    pub auto_start: bool,
    pub created_at_ms: i64,
    pub updated_at_ms: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppServerHandshakeSummary {
    pub state: String,
    pub protocol: String,
    pub transport: String,
    pub experimental_api_enabled: bool,
    pub notification_mode: String,
    #[serde(default)]
    pub opt_out_notification_methods: Vec<String>,
    pub detail: Option<String>,
    pub updated_at_ms: i64,
}

impl Default for AppServerHandshakeSummary {
    fn default() -> Self {
        Self::inactive()
    }
}

impl AppServerHandshakeSummary {
    pub fn new(
        state: impl Into<String>,
        experimental_api_enabled: bool,
        opt_out_notification_methods: Vec<String>,
        detail: Option<String>,
    ) -> Self {
        let notification_mode = if opt_out_notification_methods.is_empty() {
            "full".to_string()
        } else {
            "optimized".to_string()
        };
        Self {
            state: state.into(),
            protocol: "jsonrpc2.0".to_string(),
            transport: "stdio".to_string(),
            experimental_api_enabled,
            notification_mode,
            opt_out_notification_methods,
            detail,
            updated_at_ms: now_millis(),
        }
    }

    pub fn inactive() -> Self {
        Self::new("inactive", false, Vec::new(), None)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStatusSnapshot {
    pub runtime_id: String,
    pub status: String,
    pub codex_home: Option<String>,
    pub user_agent: Option<String>,
    pub platform_family: Option<String>,
    pub platform_os: Option<String>,
    pub last_error: Option<String>,
    pub pid: Option<u32>,
    #[serde(default)]
    pub app_server_handshake: AppServerHandshakeSummary,
    pub updated_at_ms: i64,
}

impl RuntimeStatusSnapshot {
    pub fn stopped(runtime_id: impl Into<String>) -> Self {
        Self {
            runtime_id: runtime_id.into(),
            status: "stopped".to_string(),
            codex_home: None,
            user_agent: None,
            platform_family: None,
            platform_os: None,
            last_error: None,
            pid: None,
            app_server_handshake: AppServerHandshakeSummary::inactive(),
            updated_at_ms: now_millis(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeSummary {
    pub runtime_id: String,
    pub display_name: String,
    pub codex_home: Option<String>,
    pub codex_binary: String,
    pub is_primary: bool,
    pub auto_start: bool,
    pub created_at_ms: i64,
    pub updated_at_ms: i64,
    pub status: RuntimeStatusSnapshot,
}

impl RuntimeSummary {
    pub fn from_parts(record: &RuntimeRecord, status: RuntimeStatusSnapshot) -> Self {
        Self {
            runtime_id: record.runtime_id.clone(),
            display_name: record.display_name.clone(),
            codex_home: record.codex_home.clone(),
            codex_binary: record.codex_binary.clone(),
            is_primary: record.is_primary,
            auto_start: record.auto_start,
            created_at_ms: record.created_at_ms,
            updated_at_ms: record.updated_at_ms,
            status,
        }
    }
}