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