4runr-os 2.4.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,303 +1,303 @@
1
- /**
2
- * WebSocket Client for MK3 TUI
3
- * Connects to Node.js backend WebSocket server
4
- * Handles bidirectional JSON message communication
5
- */
6
-
7
- use anyhow::{Result, Context};
8
- use futures_util::{SinkExt, StreamExt};
9
- use serde::{Deserialize, Serialize};
10
- use tokio::sync::mpsc;
11
- use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage};
12
- use std::sync::{Arc, Mutex};
13
-
14
- // Message types from STEP-2-COMPLETE-SPECIFICATION.md
15
- #[derive(Debug, Clone, Serialize, Deserialize)]
16
- #[serde(tag = "type")]
17
- #[serde(rename_all = "lowercase")]
18
- pub enum Message {
19
- #[serde(rename = "command")]
20
- Command(CommandMessage),
21
- #[serde(rename = "response")]
22
- Response(ResponseMessage),
23
- #[serde(rename = "event")]
24
- Event(EventMessage),
25
- }
26
-
27
- #[derive(Debug, Clone, Serialize, Deserialize)]
28
- pub struct CommandMessage {
29
- pub id: String,
30
- pub timestamp: String,
31
- pub payload: CommandPayload,
32
- }
33
-
34
- #[derive(Debug, Clone, Serialize, Deserialize)]
35
- pub struct CommandPayload {
36
- pub command: String,
37
- #[serde(skip_serializing_if = "Option::is_none")]
38
- pub data: Option<serde_json::Value>,
39
- }
40
-
41
- #[derive(Debug, Clone, Serialize, Deserialize)]
42
- pub struct ResponseMessage {
43
- pub id: String,
44
- pub timestamp: String,
45
- pub payload: ResponsePayload,
46
- }
47
-
48
- #[derive(Debug, Clone, Serialize, Deserialize)]
49
- pub struct ResponsePayload {
50
- pub success: bool,
51
- #[serde(skip_serializing_if = "Option::is_none")]
52
- pub data: Option<serde_json::Value>,
53
- #[serde(skip_serializing_if = "Option::is_none")]
54
- pub error: Option<String>,
55
- }
56
-
57
- #[derive(Debug, Clone, Serialize, Deserialize)]
58
- pub struct EventMessage {
59
- pub timestamp: String,
60
- pub payload: EventPayload,
61
- }
62
-
63
- #[derive(Debug, Clone, Serialize, Deserialize)]
64
- pub struct EventPayload {
65
- pub event: String,
66
- pub data: serde_json::Value,
67
- }
68
-
69
- pub enum WsClientMessage {
70
- Connected,
71
- Disconnected,
72
- Response(ResponseMessage),
73
- Event(EventMessage),
74
- Error(String),
75
- ConnectionLost,
76
- Reconnecting,
77
- ReconnectFailed(String),
78
- }
79
-
80
- pub struct WebSocketClient {
81
- url: String,
82
- tx: mpsc::UnboundedSender<WsMessage>,
83
- rx: Arc<Mutex<mpsc::UnboundedReceiver<WsClientMessage>>>,
84
- connected: Arc<Mutex<bool>>,
85
- pending_commands: Arc<Mutex<Vec<(String, String, Option<serde_json::Value>)>>>, // (id, command, data)
86
- }
87
-
88
- impl WebSocketClient {
89
- pub async fn connect(url: &str) -> Result<Self> {
90
- let (msg_tx, msg_rx) = mpsc::unbounded_channel::<WsMessage>();
91
- let (client_tx, client_rx) = mpsc::unbounded_channel::<WsClientMessage>();
92
-
93
- let url_clone = url.to_string();
94
- let connected = Arc::new(Mutex::new(false));
95
- let connected_clone = connected.clone();
96
-
97
- // Spawn WebSocket connection task
98
- tokio::spawn(async move {
99
- if let Err(e) = Self::run_connection(&url_clone, client_tx.clone(), msg_rx, connected_clone).await {
100
- let _ = client_tx.send(WsClientMessage::Error(e.to_string()));
101
- }
102
- });
103
-
104
- Ok(Self {
105
- url: url.to_string(),
106
- tx: msg_tx,
107
- rx: Arc::new(Mutex::new(client_rx)),
108
- connected,
109
- pending_commands: Arc::new(Mutex::new(Vec::new())),
110
- })
111
- }
112
-
113
- async fn run_connection(
114
- url: &str,
115
- client_tx: mpsc::UnboundedSender<WsClientMessage>,
116
- mut client_rx: mpsc::UnboundedReceiver<WsMessage>,
117
- connected: Arc<Mutex<bool>>,
118
- ) -> Result<()> {
119
- // Connect to WebSocket server
120
- let (ws_stream, _) = connect_async(url)
121
- .await
122
- .context("Failed to connect to WebSocket server")?;
123
-
124
- *connected.lock().unwrap() = true;
125
- let _ = client_tx.send(WsClientMessage::Connected);
126
-
127
- let (mut write, mut read) = ws_stream.split();
128
-
129
- // Spawn task to handle outgoing messages
130
- tokio::spawn(async move {
131
- while let Some(msg) = client_rx.recv().await {
132
- if let Err(e) = write.send(msg).await {
133
- eprintln!("[WebSocket] Error sending message: {}", e);
134
- break;
135
- }
136
- }
137
- });
138
-
139
- // Handle incoming messages
140
- while let Some(msg) = read.next().await {
141
- match msg {
142
- Ok(WsMessage::Text(text)) => {
143
- if let Err(e) = Self::handle_message(&text, &client_tx) {
144
- eprintln!("[WebSocket] Error handling message: {}", e);
145
- }
146
- }
147
- Ok(WsMessage::Close(_)) => {
148
- *connected.lock().unwrap() = false;
149
- let _ = client_tx.send(WsClientMessage::Disconnected);
150
- break;
151
- }
152
- Err(e) => {
153
- *connected.lock().unwrap() = false;
154
- let _ = client_tx.send(WsClientMessage::ConnectionLost);
155
- let _ = client_tx.send(WsClientMessage::Error(format!("Connection error: {}", e)));
156
- break;
157
- }
158
- _ => {}
159
- }
160
- }
161
-
162
- Ok(())
163
- }
164
-
165
- fn handle_message(
166
- text: &str,
167
- client_tx: &mpsc::UnboundedSender<WsClientMessage>,
168
- ) -> Result<()> {
169
- let message: Message = serde_json::from_str(text)
170
- .context("Failed to parse message")?;
171
-
172
- match message {
173
- Message::Response(resp) => {
174
- let _ = client_tx.send(WsClientMessage::Response(resp));
175
- }
176
- Message::Event(event) => {
177
- let _ = client_tx.send(WsClientMessage::Event(event));
178
- }
179
- Message::Command(_) => {
180
- // Client should not receive commands
181
- eprintln!("[WebSocket] Unexpected command message from server");
182
- }
183
- }
184
-
185
- Ok(())
186
- }
187
-
188
- pub fn send_command(&self, command: &str, data: Option<serde_json::Value>) -> Result<String> {
189
- let id = format!("req-{}", uuid::Uuid::new_v4());
190
- let timestamp = chrono::Utc::now().to_rfc3339();
191
-
192
- let cmd = Message::Command(CommandMessage {
193
- id: id.clone(),
194
- timestamp,
195
- payload: CommandPayload {
196
- command: command.to_string(),
197
- data: data.clone(),
198
- },
199
- });
200
-
201
- let json = serde_json::to_string(&cmd)?;
202
-
203
- // Track pending command for retry
204
- if self.is_connected() {
205
- self.pending_commands.lock().unwrap().push((id.clone(), command.to_string(), data));
206
- }
207
-
208
- self.tx.send(WsMessage::Text(json))?;
209
-
210
- Ok(id)
211
- }
212
-
213
- /// Retry pending commands after reconnection
214
- pub fn retry_pending_commands(&self) -> Result<Vec<String>> {
215
- let mut pending = self.pending_commands.lock().unwrap();
216
- let mut retried_ids = Vec::new();
217
-
218
- for (old_id, command, data) in pending.drain(..) {
219
- // Generate new ID for retry
220
- let new_id = format!("retry-{}", uuid::Uuid::new_v4());
221
- let timestamp = chrono::Utc::now().to_rfc3339();
222
-
223
- let cmd = Message::Command(CommandMessage {
224
- id: new_id.clone(),
225
- timestamp,
226
- payload: CommandPayload {
227
- command,
228
- data,
229
- },
230
- });
231
-
232
- let json = serde_json::to_string(&cmd)?;
233
- self.tx.send(WsMessage::Text(json))?;
234
-
235
- retried_ids.push(format!("{} -> {}", &old_id[old_id.len().saturating_sub(8)..], &new_id[new_id.len().saturating_sub(8)..]));
236
- }
237
-
238
- Ok(retried_ids)
239
- }
240
-
241
- /// Clear pending commands (e.g., on explicit disconnect)
242
- pub fn clear_pending_commands(&self) {
243
- self.pending_commands.lock().unwrap().clear();
244
- }
245
-
246
- pub fn try_recv(&self) -> Option<WsClientMessage> {
247
- self.rx.lock().unwrap().try_recv().ok()
248
- }
249
-
250
- pub fn is_connected(&self) -> bool {
251
- *self.connected.lock().unwrap()
252
- }
253
- }
254
-
255
- // Helper for creating UUID
256
- mod uuid {
257
- use std::sync::atomic::{AtomicU64, Ordering};
258
-
259
- static COUNTER: AtomicU64 = AtomicU64::new(0);
260
-
261
- pub struct Uuid;
262
-
263
- impl Uuid {
264
- pub fn new_v4() -> String {
265
- let count = COUNTER.fetch_add(1, Ordering::SeqCst);
266
- let timestamp = std::time::SystemTime::now()
267
- .duration_since(std::time::UNIX_EPOCH)
268
- .unwrap()
269
- .as_secs();
270
- format!("{:016x}{:016x}", timestamp, count)
271
- }
272
- }
273
- }
274
-
275
- // Helper for timestamps
276
- mod chrono {
277
- pub struct Utc;
278
-
279
- impl Utc {
280
- pub fn now() -> DateTime {
281
- DateTime
282
- }
283
- }
284
-
285
- pub struct DateTime;
286
-
287
- impl DateTime {
288
- pub fn to_rfc3339(&self) -> String {
289
- let now = std::time::SystemTime::now()
290
- .duration_since(std::time::UNIX_EPOCH)
291
- .unwrap();
292
-
293
- // Simple ISO 8601 format
294
- format!(
295
- "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
296
- 1970, 1, 1, // Simplified - real impl would calculate actual date
297
- (now.as_secs() / 3600) % 24,
298
- (now.as_secs() / 60) % 60,
299
- now.as_secs() % 60
300
- )
301
- }
302
- }
303
- }
1
+ /**
2
+ * WebSocket Client for MK3 TUI
3
+ * Connects to Node.js backend WebSocket server
4
+ * Handles bidirectional JSON message communication
5
+ */
6
+
7
+ use anyhow::{Result, Context};
8
+ use futures_util::{SinkExt, StreamExt};
9
+ use serde::{Deserialize, Serialize};
10
+ use tokio::sync::mpsc;
11
+ use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage};
12
+ use std::sync::{Arc, Mutex};
13
+
14
+ // Message types from STEP-2-COMPLETE-SPECIFICATION.md
15
+ #[derive(Debug, Clone, Serialize, Deserialize)]
16
+ #[serde(tag = "type")]
17
+ #[serde(rename_all = "lowercase")]
18
+ pub enum Message {
19
+ #[serde(rename = "command")]
20
+ Command(CommandMessage),
21
+ #[serde(rename = "response")]
22
+ Response(ResponseMessage),
23
+ #[serde(rename = "event")]
24
+ Event(EventMessage),
25
+ }
26
+
27
+ #[derive(Debug, Clone, Serialize, Deserialize)]
28
+ pub struct CommandMessage {
29
+ pub id: String,
30
+ pub timestamp: String,
31
+ pub payload: CommandPayload,
32
+ }
33
+
34
+ #[derive(Debug, Clone, Serialize, Deserialize)]
35
+ pub struct CommandPayload {
36
+ pub command: String,
37
+ #[serde(skip_serializing_if = "Option::is_none")]
38
+ pub data: Option<serde_json::Value>,
39
+ }
40
+
41
+ #[derive(Debug, Clone, Serialize, Deserialize)]
42
+ pub struct ResponseMessage {
43
+ pub id: String,
44
+ pub timestamp: String,
45
+ pub payload: ResponsePayload,
46
+ }
47
+
48
+ #[derive(Debug, Clone, Serialize, Deserialize)]
49
+ pub struct ResponsePayload {
50
+ pub success: bool,
51
+ #[serde(skip_serializing_if = "Option::is_none")]
52
+ pub data: Option<serde_json::Value>,
53
+ #[serde(skip_serializing_if = "Option::is_none")]
54
+ pub error: Option<String>,
55
+ }
56
+
57
+ #[derive(Debug, Clone, Serialize, Deserialize)]
58
+ pub struct EventMessage {
59
+ pub timestamp: String,
60
+ pub payload: EventPayload,
61
+ }
62
+
63
+ #[derive(Debug, Clone, Serialize, Deserialize)]
64
+ pub struct EventPayload {
65
+ pub event: String,
66
+ pub data: serde_json::Value,
67
+ }
68
+
69
+ pub enum WsClientMessage {
70
+ Connected,
71
+ Disconnected,
72
+ Response(ResponseMessage),
73
+ Event(EventMessage),
74
+ Error(String),
75
+ ConnectionLost,
76
+ Reconnecting,
77
+ ReconnectFailed(String),
78
+ }
79
+
80
+ pub struct WebSocketClient {
81
+ url: String,
82
+ tx: mpsc::UnboundedSender<WsMessage>,
83
+ rx: Arc<Mutex<mpsc::UnboundedReceiver<WsClientMessage>>>,
84
+ connected: Arc<Mutex<bool>>,
85
+ pending_commands: Arc<Mutex<Vec<(String, String, Option<serde_json::Value>)>>>, // (id, command, data)
86
+ }
87
+
88
+ impl WebSocketClient {
89
+ pub async fn connect(url: &str) -> Result<Self> {
90
+ let (msg_tx, msg_rx) = mpsc::unbounded_channel::<WsMessage>();
91
+ let (client_tx, client_rx) = mpsc::unbounded_channel::<WsClientMessage>();
92
+
93
+ let url_clone = url.to_string();
94
+ let connected = Arc::new(Mutex::new(false));
95
+ let connected_clone = connected.clone();
96
+
97
+ // Spawn WebSocket connection task
98
+ tokio::spawn(async move {
99
+ if let Err(e) = Self::run_connection(&url_clone, client_tx.clone(), msg_rx, connected_clone).await {
100
+ let _ = client_tx.send(WsClientMessage::Error(e.to_string()));
101
+ }
102
+ });
103
+
104
+ Ok(Self {
105
+ url: url.to_string(),
106
+ tx: msg_tx,
107
+ rx: Arc::new(Mutex::new(client_rx)),
108
+ connected,
109
+ pending_commands: Arc::new(Mutex::new(Vec::new())),
110
+ })
111
+ }
112
+
113
+ async fn run_connection(
114
+ url: &str,
115
+ client_tx: mpsc::UnboundedSender<WsClientMessage>,
116
+ mut client_rx: mpsc::UnboundedReceiver<WsMessage>,
117
+ connected: Arc<Mutex<bool>>,
118
+ ) -> Result<()> {
119
+ // Connect to WebSocket server
120
+ let (ws_stream, _) = connect_async(url)
121
+ .await
122
+ .context("Failed to connect to WebSocket server")?;
123
+
124
+ *connected.lock().unwrap() = true;
125
+ let _ = client_tx.send(WsClientMessage::Connected);
126
+
127
+ let (mut write, mut read) = ws_stream.split();
128
+
129
+ // Spawn task to handle outgoing messages
130
+ tokio::spawn(async move {
131
+ while let Some(msg) = client_rx.recv().await {
132
+ if let Err(e) = write.send(msg).await {
133
+ eprintln!("[WebSocket] Error sending message: {}", e);
134
+ break;
135
+ }
136
+ }
137
+ });
138
+
139
+ // Handle incoming messages
140
+ while let Some(msg) = read.next().await {
141
+ match msg {
142
+ Ok(WsMessage::Text(text)) => {
143
+ if let Err(e) = Self::handle_message(&text, &client_tx) {
144
+ eprintln!("[WebSocket] Error handling message: {}", e);
145
+ }
146
+ }
147
+ Ok(WsMessage::Close(_)) => {
148
+ *connected.lock().unwrap() = false;
149
+ let _ = client_tx.send(WsClientMessage::Disconnected);
150
+ break;
151
+ }
152
+ Err(e) => {
153
+ *connected.lock().unwrap() = false;
154
+ let _ = client_tx.send(WsClientMessage::ConnectionLost);
155
+ let _ = client_tx.send(WsClientMessage::Error(format!("Connection error: {}", e)));
156
+ break;
157
+ }
158
+ _ => {}
159
+ }
160
+ }
161
+
162
+ Ok(())
163
+ }
164
+
165
+ fn handle_message(
166
+ text: &str,
167
+ client_tx: &mpsc::UnboundedSender<WsClientMessage>,
168
+ ) -> Result<()> {
169
+ let message: Message = serde_json::from_str(text)
170
+ .context("Failed to parse message")?;
171
+
172
+ match message {
173
+ Message::Response(resp) => {
174
+ let _ = client_tx.send(WsClientMessage::Response(resp));
175
+ }
176
+ Message::Event(event) => {
177
+ let _ = client_tx.send(WsClientMessage::Event(event));
178
+ }
179
+ Message::Command(_) => {
180
+ // Client should not receive commands
181
+ eprintln!("[WebSocket] Unexpected command message from server");
182
+ }
183
+ }
184
+
185
+ Ok(())
186
+ }
187
+
188
+ pub fn send_command(&self, command: &str, data: Option<serde_json::Value>) -> Result<String> {
189
+ let id = format!("req-{}", uuid::Uuid::new_v4());
190
+ let timestamp = chrono::Utc::now().to_rfc3339();
191
+
192
+ let cmd = Message::Command(CommandMessage {
193
+ id: id.clone(),
194
+ timestamp,
195
+ payload: CommandPayload {
196
+ command: command.to_string(),
197
+ data: data.clone(),
198
+ },
199
+ });
200
+
201
+ let json = serde_json::to_string(&cmd)?;
202
+
203
+ // Track pending command for retry
204
+ if self.is_connected() {
205
+ self.pending_commands.lock().unwrap().push((id.clone(), command.to_string(), data));
206
+ }
207
+
208
+ self.tx.send(WsMessage::Text(json))?;
209
+
210
+ Ok(id)
211
+ }
212
+
213
+ /// Retry pending commands after reconnection
214
+ pub fn retry_pending_commands(&self) -> Result<Vec<String>> {
215
+ let mut pending = self.pending_commands.lock().unwrap();
216
+ let mut retried_ids = Vec::new();
217
+
218
+ for (old_id, command, data) in pending.drain(..) {
219
+ // Generate new ID for retry
220
+ let new_id = format!("retry-{}", uuid::Uuid::new_v4());
221
+ let timestamp = chrono::Utc::now().to_rfc3339();
222
+
223
+ let cmd = Message::Command(CommandMessage {
224
+ id: new_id.clone(),
225
+ timestamp,
226
+ payload: CommandPayload {
227
+ command,
228
+ data,
229
+ },
230
+ });
231
+
232
+ let json = serde_json::to_string(&cmd)?;
233
+ self.tx.send(WsMessage::Text(json))?;
234
+
235
+ retried_ids.push(format!("{} -> {}", &old_id[old_id.len().saturating_sub(8)..], &new_id[new_id.len().saturating_sub(8)..]));
236
+ }
237
+
238
+ Ok(retried_ids)
239
+ }
240
+
241
+ /// Clear pending commands (e.g., on explicit disconnect)
242
+ pub fn clear_pending_commands(&self) {
243
+ self.pending_commands.lock().unwrap().clear();
244
+ }
245
+
246
+ pub fn try_recv(&self) -> Option<WsClientMessage> {
247
+ self.rx.lock().unwrap().try_recv().ok()
248
+ }
249
+
250
+ pub fn is_connected(&self) -> bool {
251
+ *self.connected.lock().unwrap()
252
+ }
253
+ }
254
+
255
+ // Helper for creating UUID
256
+ mod uuid {
257
+ use std::sync::atomic::{AtomicU64, Ordering};
258
+
259
+ static COUNTER: AtomicU64 = AtomicU64::new(0);
260
+
261
+ pub struct Uuid;
262
+
263
+ impl Uuid {
264
+ pub fn new_v4() -> String {
265
+ let count = COUNTER.fetch_add(1, Ordering::SeqCst);
266
+ let timestamp = std::time::SystemTime::now()
267
+ .duration_since(std::time::UNIX_EPOCH)
268
+ .unwrap()
269
+ .as_secs();
270
+ format!("{:016x}{:016x}", timestamp, count)
271
+ }
272
+ }
273
+ }
274
+
275
+ // Helper for timestamps
276
+ mod chrono {
277
+ pub struct Utc;
278
+
279
+ impl Utc {
280
+ pub fn now() -> DateTime {
281
+ DateTime
282
+ }
283
+ }
284
+
285
+ pub struct DateTime;
286
+
287
+ impl DateTime {
288
+ pub fn to_rfc3339(&self) -> String {
289
+ let now = std::time::SystemTime::now()
290
+ .duration_since(std::time::UNIX_EPOCH)
291
+ .unwrap();
292
+
293
+ // Simple ISO 8601 format
294
+ format!(
295
+ "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
296
+ 1970, 1, 1, // Simplified - real impl would calculate actual date
297
+ (now.as_secs() / 3600) % 24,
298
+ (now.as_secs() / 60) % 60,
299
+ now.as_secs() % 60
300
+ )
301
+ }
302
+ }
303
+ }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "type": "module",
5
+ "private": false,
5
6
  "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.3.5: Fixed boot screen logo rendering (restored original working version), fully functional Agent Builder with input handling and TUI styling. Built with Rust + Ratatui. ⚠️ Pre-MVP / Development Phase",
6
7
  "main": "dist/index.js",
7
8
  "bin": {