//! Wire types for the daemon ↔ stdio-MCP-bridge unix socket protocol. //! //! Same shape as `damocles-daemon`'s `DaemonRequest`/`DaemonResponse` //! (which this is forked from in spirit). Each request is a single //! JSON line; each response is a single JSON line back. The stdio MCP //! bridge holds a fresh connection per tool call — claude's tool //! lifecycle is shorter than a persistent matrix-sdk Client wants to //! live, so the daemon stays alive and the MCP reconnects per call. use serde::{Deserialize, Serialize}; /// Request from the stdio MCP bridge to the daemon. The MCP bridge /// owns the on-wire shape claude sees; this enum is the internal /// shape the daemon dispatches over. #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "method")] pub enum DaemonRequest { /// Post a plain-text or markdown message to a room. `room` accepts /// either a matrix room id (`!abc:server`) or a canonical alias /// (`#name:server`); the daemon resolves aliases server-side. #[serde(rename = "send_message")] SendMessage { room: String, body: String }, /// Open (or reuse) a DM with `user_id` and post `body`. Creates /// the DM room if one doesn't already exist between this agent /// and the user. #[serde(rename = "send_dm")] SendDm { user_id: String, body: String }, /// React to a specific event with an emoji `key`. Matrix-spec /// `m.reaction` annotation. #[serde(rename = "send_reaction")] SendReaction { room: String, event_id: String, key: String, }, /// Reply to `event_id` in `room` with `body` as a threaded reply. /// Sets the `m.in_reply_to` relation so matrix clients render the /// thread. #[serde(rename = "send_reply")] SendReply { room: String, event_id: String, body: String, }, /// Mark `event_id` (in `room`) as read for this agent. Sends a /// read receipt; bumps the room's "unread" indicator down on /// matrix clients (and for other agents). #[serde(rename = "mark_read")] MarkRead { room: String, event_id: String }, /// List rooms the agent has joined. Returns each room's id + /// canonical alias (when present) + name + member count. #[serde(rename = "list_rooms")] ListRooms, /// List the members of a room. Each entry carries the matrix /// user id + the resolved display name (when set). #[serde(rename = "list_room_members")] ListRoomMembers { room: String }, /// Read the last `limit` events from a room's timeline. Caller /// gets each event's id, sender, `server_ts`, type, and body (best- /// effort plain-text extraction from `m.text` / `m.notice` etc.). #[serde(rename = "read_room")] ReadRoom { room: String, limit: Option }, /// List rooms this agent has been invited to but not yet joined. /// Returns each room's id, canonical alias (when present), and /// display name. #[serde(rename = "list_invites")] ListInvites, /// Join a room by id (`!abc:server`) or alias (`#name:server`). /// Accepts a pending invite if one exists; also joins public rooms /// the agent hasn't been explicitly invited to. After joining the /// room will appear in `list_rooms`. #[serde(rename = "join_room")] JoinRoom { room: String }, /// Return the count of rooms with unread notifications. Used by /// the harness `get_loose_ends` to surface unread matrix activity /// without exposing message content. #[serde(rename = "unread_count")] UnreadCount, /// Return per-room unread summaries. For rooms with exactly one /// unread notification, attempts to include the sender + truncated /// body; rooms with multiple unreads carry only the count. Used by /// `get_loose_ends` and the wake-signal formatter. #[serde(rename = "unread_summary")] UnreadSummary, /// Liveness probe — fast "are you up?" round-trip that doesn't /// touch matrix-sdk. Not used by the in-tree stdio MCP bridge /// (which surfaces a daemon-down condition as a normal tool-call /// connect error); reserved for external clients that want an /// explicit health check without doing real work. #[serde(rename = "ping")] Ping, } /// One entry in the [`DaemonRequest::UnreadSummary`] response payload. #[derive(Debug, Serialize, Deserialize)] pub struct RoomUnread { /// Canonical alias (`#name:server`) or room id (`!id:server`). pub label: String, /// Server-side push-notification count for this room. Always ≥ 1. pub count: u32, /// Truncated body of the last message in the room. Present only when /// `count == 1` and the fetch succeeded; absent otherwise. #[serde(skip_serializing_if = "Option::is_none")] pub last_body: Option, /// Sender of the last message (`@user:server`). Present when /// `last_body` is present. #[serde(skip_serializing_if = "Option::is_none")] pub last_sender: Option, } /// Response shape: `ok` carries the payload (any JSON; the MCP bridge /// passes it back to claude as the tool result), `error` carries a /// human-readable error string. #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum DaemonResponse { Ok { payload: serde_json::Value }, Error { message: String }, } impl DaemonResponse { /// Convenience: build an Ok response from any serializable value. /// Serialisation failure (impossible for the small handler structs /// we use, but the API is generic) surfaces as a structured /// `{"serialise_error": "..."}` payload so callers see WHY the /// data is missing instead of a silent `null`. pub fn ok(payload: &T) -> Self { let payload = serde_json::to_value(payload) .unwrap_or_else(|e| serde_json::json!({ "serialise_error": e.to_string() })); Self::Ok { payload } } /// Convenience: build an Error response from any `Display` value. pub fn error(msg: impl std::fmt::Display) -> Self { Self::Error { message: msg.to_string(), } } }