rusty-beads 0.1.0

Git-backed graph issue tracker for AI coding agents - a Rust implementation with context store, dependency tracking, and semantic compaction
Documentation
//! RPC protocol for daemon communication.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Version of the client/daemon protocol.
pub const PROTOCOL_VERSION: &str = "1.0.0";

/// An RPC request to the daemon.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Request {
    /// The operation to perform.
    pub operation: Operation,
    /// Operation-specific arguments.
    #[serde(default)]
    pub args: Value,
    /// The actor making the request.
    pub actor: String,
    /// Unique request ID.
    pub request_id: String,
    /// Client version for compatibility checking.
    #[serde(default)]
    pub client_version: String,
    /// Current working directory of the client.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    /// Expected database path (for validation).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expected_db: Option<String>,
}

impl Request {
    /// Create a new request.
    pub fn new(operation: Operation, actor: impl Into<String>) -> Self {
        Self {
            operation,
            args: Value::Null,
            actor: actor.into(),
            request_id: uuid::Uuid::new_v4().to_string(),
            client_version: PROTOCOL_VERSION.to_string(),
            cwd: None,
            expected_db: None,
        }
    }

    /// Set the arguments.
    pub fn with_args(mut self, args: impl Serialize) -> Self {
        self.args = serde_json::to_value(args).unwrap_or(Value::Null);
        self
    }
}

/// An RPC response from the daemon.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response {
    /// Whether the operation succeeded.
    pub success: bool,
    /// The result data (if successful).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<Value>,
    /// Error message (if failed).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    /// Request ID this is responding to.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub request_id: Option<String>,
}

impl Response {
    /// Create a success response.
    pub fn success(data: impl Serialize) -> Self {
        Self {
            success: true,
            data: Some(serde_json::to_value(data).unwrap_or(Value::Null)),
            error: None,
            request_id: None,
        }
    }

    /// Create an empty success response.
    pub fn ok() -> Self {
        Self {
            success: true,
            data: None,
            error: None,
            request_id: None,
        }
    }

    /// Create an error response.
    pub fn error(message: impl Into<String>) -> Self {
        Self {
            success: false,
            data: None,
            error: Some(message.into()),
            request_id: None,
        }
    }

    /// Set the request ID.
    pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
        self.request_id = Some(id.into());
        self
    }

    /// Parse the data as a specific type.
    pub fn parse_data<T: for<'de> Deserialize<'de>>(&self) -> Option<T> {
        self.data.as_ref().and_then(|v| serde_json::from_value(v.clone()).ok())
    }
}

/// Available daemon operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Operation {
    // === Health & Status ===
    /// Check if daemon is alive.
    Ping,
    /// Get health information.
    Health,
    /// Get daemon status and metrics.
    Status,

    // === Issue CRUD ===
    /// Create a new issue.
    Create,
    /// Get an issue by ID.
    Show,
    /// Update an issue.
    Update,
    /// Delete (tombstone) an issue.
    Delete,
    /// List issues with filters.
    List,
    /// Count issues.
    Count,

    // === Ready Work ===
    /// Get ready (unblocked) issues.
    Ready,
    /// Get blocked issues.
    Blocked,
    /// Get stale issues.
    Stale,

    // === Dependencies ===
    /// Add a dependency.
    DepAdd,
    /// Remove a dependency.
    DepRemove,
    /// Get dependencies for an issue.
    DepList,

    // === Labels ===
    /// Add a label.
    LabelAdd,
    /// Remove a label.
    LabelRemove,
    /// List labels for an issue.
    LabelList,

    // === Comments ===
    /// Add a comment.
    CommentAdd,
    /// List comments for an issue.
    CommentList,

    // === Statistics ===
    /// Get database statistics.
    Stats,

    // === Compaction ===
    /// Compact issues.
    Compact,
    /// Get compaction statistics.
    CompactStats,

    // === Import/Export ===
    /// Export issues to JSONL.
    Export,
    /// Import issues from JSONL.
    Import,

    // === Configuration ===
    /// Get/set configuration.
    Config,

    // === Lifecycle ===
    /// Shutdown the daemon.
    Shutdown,
}

/// Health check response.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthInfo {
    /// Process ID.
    pub pid: u32,
    /// Uptime in seconds.
    pub uptime_secs: u64,
    /// Daemon version.
    pub version: String,
    /// Protocol version.
    pub protocol_version: String,
    /// Whether the client is compatible.
    pub compatible: bool,
    /// Database path.
    pub database_path: String,
}

/// Create arguments.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateArgs {
    pub title: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub issue_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub priority: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub assignee: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_id: Option<String>,
    #[serde(default)]
    pub labels: Vec<String>,
}

/// Show arguments.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShowArgs {
    pub id: String,
}

/// Update arguments.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateArgs {
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub priority: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub assignee: Option<String>,
}

/// Delete arguments.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteArgs {
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

/// List arguments.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ListArgs {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub issue_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub assignee: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
    #[serde(default)]
    pub include_closed: bool,
}

/// Dependency arguments.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepArgs {
    pub issue_id: String,
    pub depends_on_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dep_type: Option<String>,
}

/// Label arguments.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LabelArgs {
    pub issue_id: String,
    pub label: String,
}

/// Comment arguments.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommentArgs {
    pub issue_id: String,
    pub text: String,
}