oxios-cli 1.0.1

Interactive CLI channel for Oxios
//! Meta-command parsing for the CLI channel.
//!
//! Recognises dot-commands that control the interactive session:
//! `.quit`, `.help`, `.reset`, `.model`, `.persona`, `.clear`.

/// A parsed meta-command.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MetaCommand {
    /// Quit the session.
    Quit,
    /// Show help text.
    Help,
    /// Reset the current session / conversation.
    Reset,
    /// Switch the active model. Carries the model name, if provided.
    Model(Option<String>),
    /// Switch the active persona. Carries the persona name, if provided.
    Persona(Option<String>),
    /// Switch the active Space. Carries the Space name/ID, if provided.
    Space(Option<String>),
    /// List all Spaces.
    Spaces,
    /// Clear the terminal screen.
    Clear,
}

impl MetaCommand {
    /// Attempt to parse a line as a meta-command.
    ///
    /// Returns `Some(MetaCommand)` if the line starts with `.`,
    /// or `None` if it is a regular user message.
    pub fn parse(line: &str) -> Option<Self> {
        let trimmed = line.trim();
        if !trimmed.starts_with('.') {
            return None;
        }

        let parts: Vec<&str> = trimmed.splitn(2, whitespace_or_end).collect();
        let cmd = parts[0];
        let arg = parts
            .get(1)
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty());

        match cmd {
            ".quit" | ".exit" | ".q" => Some(Self::Quit),
            ".help" | ".h" | ".?" => Some(Self::Help),
            ".reset" | ".r" => Some(Self::Reset),
            ".model" | ".m" => Some(Self::Model(arg)),
            ".persona" | ".p" => Some(Self::Persona(arg)),
            ".space" | ".sp" => Some(Self::Space(arg)),
            ".spaces" => Some(Self::Spaces),
            ".clear" | ".cls" => Some(Self::Clear),
            _ => None,
        }
    }

    /// Returns the help text shown by `.help`.
    pub fn help_text() -> &'static str {
        r#"Oxios CLI — Meta-commands:
  .quit, .exit, .q      Exit the session
  .help, .h, .?         Show this help
  .reset, .r             Reset the current session
  .model, .m [NAME]      Show or switch the active model
  .persona, .p [NAME]    Show or switch the active persona
  .space, .sp [ID|NAME]  Show or switch the active Space
  .spaces                List all Spaces
  .clear, .cls           Clear the terminal screen
"#
    }
}

/// Helper: find the first whitespace or end-of-string.
fn whitespace_or_end(c: char) -> bool {
    c.is_whitespace()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_quit() {
        assert_eq!(MetaCommand::parse(".quit"), Some(MetaCommand::Quit));
        assert_eq!(MetaCommand::parse(".exit"), Some(MetaCommand::Quit));
        assert_eq!(MetaCommand::parse(".q"), Some(MetaCommand::Quit));
    }

    #[test]
    fn parse_help() {
        assert_eq!(MetaCommand::parse(".help"), Some(MetaCommand::Help));
        assert_eq!(MetaCommand::parse(".h"), Some(MetaCommand::Help));
    }

    #[test]
    fn parse_model_with_arg() {
        assert_eq!(
            MetaCommand::parse(".model gpt-4o"),
            Some(MetaCommand::Model(Some("gpt-4o".into())))
        );
    }

    #[test]
    fn parse_model_no_arg() {
        assert_eq!(MetaCommand::parse(".model"), Some(MetaCommand::Model(None)));
    }

    #[test]
    fn parse_persona_with_arg() {
        assert_eq!(
            MetaCommand::parse(".persona coder"),
            Some(MetaCommand::Persona(Some("coder".into())))
        );
    }

    #[test]
    fn parse_space_no_arg() {
        assert_eq!(MetaCommand::parse(".space"), Some(MetaCommand::Space(None)));
        assert_eq!(MetaCommand::parse(".sp"), Some(MetaCommand::Space(None)));
    }

    #[test]
    fn parse_space_with_arg() {
        assert_eq!(
            MetaCommand::parse(".space my-space"),
            Some(MetaCommand::Space(Some("my-space".into())))
        );
        assert_eq!(
            MetaCommand::parse(".sp 550e8400-e29b-41d4-a716-446655440000"),
            Some(MetaCommand::Space(Some(
                "550e8400-e29b-41d4-a716-446655440000".into()
            )))
        );
    }

    #[test]
    fn parse_spaces() {
        assert_eq!(MetaCommand::parse(".spaces"), Some(MetaCommand::Spaces));
    }

    #[test]
    fn not_a_command() {
        assert_eq!(MetaCommand::parse("hello world"), None);
        assert_eq!(MetaCommand::parse("exit"), None); // no dot prefix
        assert_eq!(MetaCommand::parse("quit"), None);
    }
}