raymon 0.6.0

Stateful MCP server and TUI for Ray-style logs
Documentation
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Default, Deserialize, Serialize, JsonSchema)]
#[serde(default)]
pub struct ListEntriesParams {
    pub(super) query: Option<String>,
    pub(super) types: StringListSelector,
    pub(super) colors: StringListSelector,
    pub(super) screen: Option<String>,
    pub(super) project: Option<String>,
    pub(super) host: Option<String>,
    pub(super) limit: Option<usize>,
    pub(super) offset: Option<usize>,
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum StringListSelector {
    One(String),
    Many(Vec<String>),
}

impl Default for StringListSelector {
    fn default() -> Self {
        Self::Many(Vec::new())
    }
}

impl StringListSelector {
    pub(super) fn to_vec(&self) -> Vec<String> {
        match self {
            Self::One(value) => comma_separated_values(value),
            Self::Many(values) => values.clone(),
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum UuidSelector {
    One(String),
    Many(Vec<String>),
}

impl UuidSelector {
    pub(super) fn into_vec(self) -> Vec<String> {
        match self {
            Self::One(uuid) if uuid.contains(',') => {
                uuid.split(',').map(compact_uuid_segment).collect()
            }
            Self::One(uuid) => vec![uuid.trim().to_string()],
            Self::Many(uuids) => uuids,
        }
    }
}

#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct GetEntriesParams {
    #[serde(alias = "uuid")]
    pub(super) uuids: UuidSelector,
    #[serde(default, alias = "redacted", alias = "redact_payloads")]
    pub(super) redact: bool,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct ListEntriesResult {
    pub(super) entries: Vec<EntrySummary>,
    pub(super) count: usize,
    pub(super) limit: usize,
    pub(super) offset: usize,
    pub(super) scan_limit: usize,
}

#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct GetEntriesResult {
    pub(super) entries: Vec<McpEntry>,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct EntrySummary {
    pub(super) uuid: String,
    pub(super) received_at: u64,
    pub(super) project: String,
    pub(super) host: String,
    pub(super) screen: String,
    pub(super) payload_count: usize,
    pub(super) payload_types: Vec<String>,
}

impl From<crate::raymon_core::Entry> for EntrySummary {
    fn from(entry: crate::raymon_core::Entry) -> Self {
        let mut payload_types = Vec::new();
        for payload in &entry.payloads {
            if !payload_types.iter().any(|value| value == &payload.r#type) {
                payload_types.push(payload.r#type.clone());
            }
        }
        Self {
            uuid: entry.uuid,
            received_at: entry.received_at,
            project: entry.project,
            host: entry.host,
            screen: entry.screen.as_str().to_string(),
            payload_count: entry.payloads.len(),
            payload_types,
        }
    }
}

#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct McpEntry {
    pub(super) uuid: String,
    pub(super) received_at: u64,
    pub(super) project: String,
    pub(super) host: String,
    pub(super) screen: String,
    pub(super) session_id: Option<String>,
    pub(super) payloads: Vec<McpPayload>,
}

impl From<crate::raymon_core::Entry> for McpEntry {
    fn from(entry: crate::raymon_core::Entry) -> Self {
        Self::from_entry(entry, false)
    }
}

impl McpEntry {
    pub(super) fn from_entry(entry: crate::raymon_core::Entry, redact_payloads: bool) -> Self {
        Self {
            uuid: entry.uuid,
            received_at: entry.received_at,
            project: entry.project,
            host: entry.host,
            screen: entry.screen.as_str().to_string(),
            session_id: entry.session_id.map(|value| value.0),
            payloads: entry
                .payloads
                .into_iter()
                .map(|payload| McpPayload::from_payload(payload, redact_payloads))
                .collect(),
        }
    }
}

#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct McpPayload {
    pub(super) r#type: String,
    pub(super) content: Value,
    pub(super) origin: McpOrigin,
}

impl From<crate::raymon_core::Payload> for McpPayload {
    fn from(payload: crate::raymon_core::Payload) -> Self {
        Self::from_payload(payload, false)
    }
}

impl McpPayload {
    fn from_payload(payload: crate::raymon_core::Payload, redact_payloads: bool) -> Self {
        let mut content = payload.content;
        if redact_payloads {
            crate::sanitize::redact_sensitive_payload_value(&mut content);
        }

        Self { r#type: payload.r#type, content, origin: McpOrigin::from(payload.origin) }
    }
}

#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct McpOrigin {
    pub(super) project: String,
    pub(super) host: String,
    pub(super) screen: Option<String>,
    pub(super) session_id: Option<String>,
    pub(super) function_name: Option<String>,
    pub(super) file: Option<String>,
    pub(super) line_number: Option<u32>,
}

impl From<crate::raymon_core::Origin> for McpOrigin {
    fn from(origin: crate::raymon_core::Origin) -> Self {
        Self {
            project: origin.project,
            host: origin.host,
            screen: origin.screen.map(|screen| screen.as_str().to_string()),
            session_id: origin.session_id.map(|value| value.0),
            function_name: origin.function_name,
            file: origin.file,
            line_number: origin.line_number,
        }
    }
}

fn comma_separated_values(value: &str) -> Vec<String> {
    value.split(',').map(|segment| segment.trim().to_string()).collect()
}

fn compact_uuid_segment(uuid: &str) -> String {
    uuid.chars().filter(|ch| !ch.is_whitespace()).collect()
}