summaryrefslogtreecommitdiff
path: root/makima/daemon/src/process/claude_protocol.rs
blob: 930152b4c397e3e2cdcefd4d4604d103b9835fb7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//! Claude Code JSON protocol types for stdin communication.
//!
//! When using `--input-format=stream-json`, Claude Code expects
//! newline-delimited JSON messages on stdin.

use serde::Serialize;

/// Message sent to Claude Code via stdin.
///
/// Format based on Claude Code's stream-json input protocol.
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClaudeInputMessage {
    /// A user message to send to Claude.
    User { message: UserMessage },
}

/// The inner user message structure.
#[derive(Debug, Clone, Serialize)]
pub struct UserMessage {
    /// Always "user" for user messages.
    pub role: String,
    /// The message content.
    pub content: String,
}

impl ClaudeInputMessage {
    /// Create a new user message.
    pub fn user(content: impl Into<String>) -> Self {
        Self::User {
            message: UserMessage {
                role: "user".to_string(),
                content: content.into(),
            },
        }
    }

    /// Serialize to a JSON string with trailing newline (NDJSON format).
    pub fn to_json_line(&self) -> Result<String, serde_json::Error> {
        let mut json = serde_json::to_string(self)?;
        json.push('\n');
        Ok(json)
    }
}

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

    #[test]
    fn test_user_message_serialization() {
        let msg = ClaudeInputMessage::user("Hello, Claude!");
        let json = msg.to_json_line().unwrap();

        // Should produce: {"type":"user","message":{"role":"user","content":"Hello, Claude!"}}\n
        assert!(json.starts_with(r#"{"type":"user","message":{"role":"user","content":"Hello, Claude!"}}"#));
        assert!(json.ends_with('\n'));
    }
}