Skip to main content

exeora_cli/
protocol.rs

1use crate::error::{ErrorCode, ExeoraError};
2use serde::{Deserialize, Serialize, Serializer};
3use serde_json::Value;
4use std::{fmt, str::FromStr};
5
6pub const PROTOCOL_VERSION: u32 = 1;
7pub const HEARTBEAT_REQUEST: &str = r#"{"type":"heartbeat"}"#;
8pub const HEARTBEAT_INTERVAL_MS: u64 = 30_000;
9pub const HEARTBEAT_TIMEOUT_MS: u64 = 90_000;
10pub const PRESENCE_SIGNAL_INTERVAL_MS: u64 = 5 * 60_000;
11pub const MAX_RESULT_BYTES: usize = 1_000_000;
12pub const MAX_READ_BYTES: usize = 500_000;
13pub const MAX_LIST_ENTRIES: usize = 1_000;
14pub const MAX_GREP_MATCHES: usize = 200;
15pub const MAX_COMMAND_OUTPUT_BYTES: usize = 200_000;
16pub const DEFAULT_COMMAND_TIMEOUT_MS: u64 = 60_000;
17pub const MAX_COMMAND_TIMEOUT_MS: u64 = 300_000;
18pub const MAX_PROCESS_BUFFER_BYTES: usize = 256_000;
19pub const MAX_PROCESS_CHUNK_BYTES: usize = 100_000;
20pub const MAX_PROCESSES_PER_PROJECT: usize = 8;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ToolName {
25    ReadFile,
26    ListFiles,
27    Grep,
28    EditFile,
29    WriteFile,
30    RunCommand,
31    StartCommand,
32    GetCommandOutput,
33    SendCommandInput,
34    KillCommand,
35}
36
37impl ToolName {
38    pub const ALL: [Self; 10] = [
39        Self::ReadFile,
40        Self::ListFiles,
41        Self::Grep,
42        Self::EditFile,
43        Self::WriteFile,
44        Self::RunCommand,
45        Self::StartCommand,
46        Self::GetCommandOutput,
47        Self::SendCommandInput,
48        Self::KillCommand,
49    ];
50
51    pub const fn as_str(self) -> &'static str {
52        match self {
53            Self::ReadFile => "read_file",
54            Self::ListFiles => "list_files",
55            Self::Grep => "grep",
56            Self::EditFile => "edit_file",
57            Self::WriteFile => "write_file",
58            Self::RunCommand => "run_command",
59            Self::StartCommand => "start_command",
60            Self::GetCommandOutput => "get_command_output",
61            Self::SendCommandInput => "send_command_input",
62            Self::KillCommand => "kill_command",
63        }
64    }
65
66    pub const fn read_only(self) -> bool {
67        matches!(
68            self,
69            Self::ReadFile | Self::ListFiles | Self::Grep | Self::GetCommandOutput
70        )
71    }
72}
73
74impl fmt::Display for ToolName {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        f.write_str(self.as_str())
77    }
78}
79
80impl FromStr for ToolName {
81    type Err = ExeoraError;
82
83    fn from_str(value: &str) -> Result<Self, Self::Err> {
84        Self::ALL
85            .into_iter()
86            .find(|tool| tool.as_str() == value)
87            .ok_or_else(|| {
88                ExeoraError::new(ErrorCode::UnknownTool, format!("Unknown tool: {value}"))
89            })
90    }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct ProjectHello {
96    pub id: String,
97    pub slug: String,
98}
99
100#[derive(Debug, Clone, Serialize)]
101#[serde(rename_all = "camelCase")]
102pub struct ExecutorCapabilities {
103    pub prompt: bool,
104    pub tools: Vec<String>,
105}
106
107#[derive(Debug, Clone, Serialize)]
108#[serde(tag = "type")]
109pub enum ExecutorMessage {
110    #[serde(rename = "hello")]
111    Hello {
112        #[serde(rename = "protocolVersion")]
113        protocol_version: u32,
114        #[serde(rename = "deviceId")]
115        device_id: String,
116        #[serde(rename = "cliVersion")]
117        cli_version: String,
118        platform: String,
119        projects: Vec<ProjectHello>,
120        capabilities: ExecutorCapabilities,
121    },
122    #[serde(rename = "heartbeat")]
123    Heartbeat { at: u64 },
124    #[serde(rename = "presence")]
125    Presence { at: u64 },
126    #[serde(rename = "tool.result")]
127    ToolResult {
128        #[serde(rename = "requestId")]
129        request_id: String,
130        #[serde(rename = "durationMs")]
131        duration_ms: u64,
132        result: ToolResult,
133    },
134    #[serde(rename = "approval.answer")]
135    ApprovalAnswer { id: String, approved: bool },
136}
137
138#[derive(Debug, Clone)]
139pub enum ToolResult {
140    Ok { value: Value },
141    Err { error: WireError },
142}
143
144impl Serialize for ToolResult {
145    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
146        match self {
147            Self::Ok { value } => {
148                serde_json::json!({ "ok": true, "value": value }).serialize(serializer)
149            }
150            Self::Err { error } => {
151                serde_json::json!({ "ok": false, "error": error }).serialize(serializer)
152            }
153        }
154    }
155}
156
157impl ToolResult {
158    pub fn ok(value: Value) -> Self {
159        Self::Ok { value }
160    }
161    pub fn err(error: WireError) -> Self {
162        Self::Err { error }
163    }
164    pub fn is_ok(&self) -> bool {
165        matches!(self, Self::Ok { .. })
166    }
167}
168
169#[derive(Debug, Clone, Serialize)]
170pub struct WireError {
171    pub code: &'static str,
172    pub message: String,
173}
174
175impl From<ExeoraError> for WireError {
176    fn from(error: ExeoraError) -> Self {
177        Self {
178            code: error.code.as_str(),
179            message: error.message,
180        }
181    }
182}
183
184impl WireError {
185    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
186        Self {
187            code: code.as_str(),
188            message: message.into(),
189        }
190    }
191}
192
193#[derive(Debug, Clone, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct ClientInfo {
196    pub name: Option<String>,
197    pub version: Option<String>,
198}
199
200impl ClientInfo {
201    pub fn describe(&self) -> Option<String> {
202        match (&self.name, &self.version) {
203            (Some(name), Some(version)) => Some(format!("{name} {version}")),
204            (Some(name), None) => Some(name.clone()),
205            (None, Some(version)) => Some(version.clone()),
206            (None, None) => None,
207        }
208    }
209}
210
211pub fn now_ms() -> u64 {
212    std::time::SystemTime::now()
213        .duration_since(std::time::UNIX_EPOCH)
214        .unwrap_or_default()
215        .as_millis() as u64
216}