claw_core_protocol/
client.rs1use crate::types::*;
2use anyhow::{bail, Context, Result};
3use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
4use tokio::net::UnixStream;
5
6pub const DEFAULT_SOCKET_PATH: &str = "/tmp/trl.sock";
8
9pub struct ClawCoreClient {
36 reader: BufReader<tokio::io::ReadHalf<UnixStream>>,
37 writer: tokio::io::WriteHalf<UnixStream>,
38 req_counter: u64,
39}
40
41impl ClawCoreClient {
42 pub async fn connect(socket_path: &str) -> Result<Self> {
44 let stream = UnixStream::connect(socket_path)
45 .await
46 .with_context(|| format!("failed to connect to claw_core daemon at {socket_path}"))?;
47 let (read_half, write_half) = tokio::io::split(stream);
48 Ok(Self {
49 reader: BufReader::new(read_half),
50 writer: write_half,
51 req_counter: 0,
52 })
53 }
54
55 pub async fn send_request(
57 &mut self,
58 method: &str,
59 params: serde_json::Value,
60 ) -> Result<RpcResponse> {
61 self.req_counter += 1;
62 let request = RpcRequest {
63 id: format!("req-{}", self.req_counter),
64 method: method.to_string(),
65 params,
66 };
67
68 let mut line = serde_json::to_string(&request)
69 .context("failed to serialize RPC request")?;
70 line.push('\n');
71
72 self.writer
73 .write_all(line.as_bytes())
74 .await
75 .context("failed to write to claw_core socket")?;
76 self.writer.flush().await.context("failed to flush socket")?;
77
78 let mut response_line = String::new();
79 let bytes_read = self
80 .reader
81 .read_line(&mut response_line)
82 .await
83 .context("failed to read response from claw_core socket")?;
84
85 if bytes_read == 0 {
86 bail!("claw_core daemon closed the connection");
87 }
88
89 let response: RpcResponse = serde_json::from_str(response_line.trim())
90 .context("failed to parse RPC response from claw_core")?;
91
92 Ok(response)
93 }
94
95 pub async fn ping(&mut self) -> Result<PingResult> {
101 let resp = self.send_request("system.ping", serde_json::json!({})).await?;
102 extract_data(resp)
103 }
104
105 pub async fn stats(&mut self) -> Result<SystemStats> {
107 let resp = self.send_request("system.stats", serde_json::json!({})).await?;
108 extract_data(resp)
109 }
110
111 pub async fn create_session(
113 &mut self,
114 params: CreateSessionParams,
115 ) -> Result<CreateSessionResult> {
116 let value = serde_json::to_value(¶ms).context("serialize CreateSessionParams")?;
117 let resp = self.send_request("session.create", value).await?;
118 extract_data(resp)
119 }
120
121 pub async fn list_sessions(&mut self) -> Result<SessionListResult> {
123 let resp = self.send_request("session.list", serde_json::json!({})).await?;
124 extract_data(resp)
125 }
126
127 pub async fn session_info(&mut self, session_id: &str) -> Result<SessionInfo> {
129 let resp = self
130 .send_request("session.info", serde_json::json!({ "session_id": session_id }))
131 .await?;
132 extract_data(resp)
133 }
134
135 pub async fn destroy_session(
137 &mut self,
138 params: DestroySessionParams,
139 ) -> Result<DestroySessionResult> {
140 let value = serde_json::to_value(¶ms).context("serialize DestroySessionParams")?;
141 let resp = self.send_request("session.destroy", value).await?;
142 extract_data(resp)
143 }
144
145 pub async fn exec_run(&mut self, params: ExecRunParams) -> Result<ExecRunResult> {
147 let value = serde_json::to_value(¶ms).context("serialize ExecRunParams")?;
148 let resp = self.send_request("exec.run", value).await?;
149 extract_data(resp)
150 }
151}
152
153fn extract_data<T: serde::de::DeserializeOwned>(resp: RpcResponse) -> Result<T> {
155 if !resp.ok {
156 let err = resp.error.unwrap_or(RpcError {
157 code: "UNKNOWN".to_string(),
158 message: "unknown error".to_string(),
159 });
160 bail!("claw_core error [{}]: {}", err.code, err.message);
161 }
162 let data = resp.data.context("response ok=true but data is null")?;
163 serde_json::from_value(data).context("failed to deserialize response data")
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn default_socket_path_is_set() {
172 assert_eq!(DEFAULT_SOCKET_PATH, "/tmp/trl.sock");
173 }
174
175 #[test]
176 fn extract_data_success() {
177 let resp = RpcResponse {
178 id: "req-1".into(),
179 ok: true,
180 data: Some(serde_json::json!({ "uptime_s": 42, "version": "0.1.0" })),
181 error: None,
182 };
183 let ping: PingResult = extract_data(resp).unwrap();
184 assert_eq!(ping.uptime_s, 42);
185 assert_eq!(ping.version, "0.1.0");
186 }
187
188 #[test]
189 fn extract_data_error() {
190 let resp = RpcResponse {
191 id: "req-2".into(),
192 ok: false,
193 data: None,
194 error: Some(RpcError {
195 code: "SESSION_NOT_FOUND".into(),
196 message: "session not found".into(),
197 }),
198 };
199 let result = extract_data::<PingResult>(resp);
200 assert!(result.is_err());
201 let err_msg = result.unwrap_err().to_string();
202 assert!(err_msg.contains("SESSION_NOT_FOUND"));
203 }
204
205 #[test]
206 fn create_session_params_default_serializes_cleanly() {
207 let params = CreateSessionParams::default();
208 let json = serde_json::to_string(¶ms).unwrap();
209 assert_eq!(json, "{}");
210 }
211
212 #[test]
213 fn exec_run_params_serializes() {
214 let params = ExecRunParams {
215 session_id: "s-123".into(),
216 command: "echo hi".into(),
217 timeout_s: Some(30),
218 stdin: None,
219 env: None,
220 };
221 let json: serde_json::Value = serde_json::to_value(¶ms).unwrap();
222 assert_eq!(json["session_id"], "s-123");
223 assert_eq!(json["command"], "echo hi");
224 assert_eq!(json["timeout_s"], 30);
225 assert!(json.get("stdin").is_none());
226 }
227}