1use serde::{Deserialize, Serialize};
2
3pub const DEFAULT_TCP_PORT: u16 = 7422;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Repository {
10 pub id: String,
11 pub name: String,
12 pub path: String,
14 pub registered_at: u64,
15 pub workstreams: Vec<Workstream>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Workstream {
20 pub id: String,
21 pub name: String,
22 pub repo_id: String,
23 pub branch: String,
24 pub worktree_path: String,
26 pub tmux_session: String,
28 pub status: WorkstreamStatus,
29 pub agents: Vec<Agent>,
30 pub created_at: u64,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub enum WorkstreamStatus {
35 Idle,
36 Running,
37 Stopped,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct Agent {
42 pub id: String,
43 pub workstream_id: String,
44 pub tmux_window: u32,
46 pub prompt: String,
47 pub status: AgentStatus,
48 pub exit_code: Option<i32>,
49 pub spawned_at: u64,
50 pub exited_at: Option<u64>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54pub enum AgentStatus {
55 Running,
56 Exited,
57 Failed,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(tag = "type", content = "data")]
64pub enum Command {
65 Status,
67 Whoami,
68 PairCreate {
69 label: Option<String>,
70 expire_secs: Option<u64>,
72 },
73 PairList,
74 PairRevoke {
75 id: String,
76 },
77 PairRevokeAll,
78
79 RepoRegister {
82 path: String,
83 },
84 RepoList,
85 RepoUnregister {
86 repo_id: String,
87 },
88
89 WorkstreamCreate {
91 repo_id: String,
92 name: String,
93 branch: String,
94 },
95 WorkstreamList {
97 repo_id: Option<String>,
98 },
99 WorkstreamDelete {
100 workstream_id: String,
101 },
102
103 AgentSpawn {
105 workstream_id: String,
106 prompt: String,
107 },
108 AgentSpawnInPlace {
111 workstream_id: String,
112 tmux_window: u32,
114 prompt: Option<String>,
116 },
117 AgentKill {
118 agent_id: String,
119 },
120 AgentList {
121 workstream_id: String,
122 },
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(tag = "type", content = "data")]
127pub enum Response {
128 Pong,
130 Ok,
131 DaemonStatus(DaemonStatus),
132 ClientInfo(ClientInfo),
133 Pair(PairPayload),
135 PairedClient(PairedClient),
136 PairedClients(Vec<PairedClient>),
137 Revoked(u32),
139 Error(VexProtoError),
140
141 RepoRegistered(Repository),
143 RepoList(Vec<Repository>),
144 RepoUnregistered,
145
146 WorkstreamCreated(Workstream),
148 WorkstreamList(Vec<Repository>),
150 WorkstreamDeleted,
151
152 AgentSpawned(Agent),
154 AgentSpawnedInPlace {
156 agent: Agent,
157 exec_cmd: String,
159 },
160 AgentKilled,
161 AgentList(Vec<Agent>),
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct DaemonStatus {
168 pub uptime_secs: u64,
169 pub connected_clients: u32,
170 pub version: String,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct PairPayload {
176 pub token_id: String,
177 pub token_secret: String,
178 pub host: Option<String>,
180}
181
182impl PairPayload {
183 pub fn pairing_string(&self) -> String {
185 format!("{}:{}", self.token_id, self.token_secret)
186 }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct PairedClient {
191 pub token_id: String,
192 pub label: Option<String>,
193 pub created_at: String,
194 pub expires_at: Option<String>,
195 pub last_seen: Option<String>,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct ClientInfo {
200 pub token_id: Option<String>,
201 pub is_local: bool,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205#[serde(rename_all = "snake_case")]
206pub enum Transport {
207 Unix,
208 Tcp,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(tag = "code", content = "message")]
213pub enum VexProtoError {
214 Unauthorized,
215 LocalOnly,
216 NotFound,
217 Internal(String),
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct AuthToken {
223 pub token_id: String,
224 pub token_secret: String,
226}
227
228pub mod framing {
231 use serde::{Deserialize, Serialize};
232 use std::io;
233 use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
234
235 #[derive(Debug)]
236 pub enum VexFrameError {
237 Io(io::Error),
238 Json(serde_json::Error),
239 }
240
241 impl std::fmt::Display for VexFrameError {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 match self {
244 VexFrameError::Io(e) => write!(f, "IO error: {e}"),
245 VexFrameError::Json(e) => write!(f, "JSON error: {e}"),
246 }
247 }
248 }
249
250 impl std::error::Error for VexFrameError {}
251
252 impl From<io::Error> for VexFrameError {
253 fn from(e: io::Error) -> Self {
254 VexFrameError::Io(e)
255 }
256 }
257
258 impl From<serde_json::Error> for VexFrameError {
259 fn from(e: serde_json::Error) -> Self {
260 VexFrameError::Json(e)
261 }
262 }
263
264 pub async fn send<W, T>(w: &mut W, msg: &T) -> Result<(), VexFrameError>
266 where
267 W: AsyncWrite + Unpin,
268 T: Serialize,
269 {
270 let body = serde_json::to_vec(msg)?;
271 w.write_u32(body.len() as u32).await?;
272 w.write_all(&body).await?;
273 Ok(())
274 }
275
276 pub async fn recv<R, T>(r: &mut R) -> Result<T, VexFrameError>
278 where
279 R: AsyncRead + Unpin,
280 T: for<'de> Deserialize<'de>,
281 {
282 let len = r.read_u32().await?;
283 let mut buf = vec![0u8; len as usize];
284 r.read_exact(&mut buf).await?;
285 Ok(serde_json::from_slice(&buf)?)
286 }
287}