@delorenj/claude-notifications 1.2.0 → 2.1.0

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.
Files changed (67) hide show
  1. package/DO.md +5 -0
  2. package/FIXES-APPLIED.md +195 -0
  3. package/INTEGRATION.md +445 -0
  4. package/LAYOUT-INTEGRATION.md +191 -0
  5. package/QUICK-REFERENCE.md +195 -0
  6. package/README.md +145 -14
  7. package/TASK.md +15 -0
  8. package/ZELLIJ-NOTIFY.md +523 -0
  9. package/_bmad-output/implementation-artifacts/spec-install-multi-cli-hooks.md +241 -0
  10. package/bin/claude-notifications.js +424 -305
  11. package/bin/claude-notify.js +47 -1
  12. package/bin/zellij-notify.js +346 -0
  13. package/bun.lock +35 -0
  14. package/diagnose-zellij.sh +105 -0
  15. package/examples/settings-with-zellij.json +18 -0
  16. package/examples/settings-zellij-only.json +18 -0
  17. package/examples/zellij-notify-examples.sh +143 -0
  18. package/lib/adapters/_stub.js +35 -0
  19. package/lib/adapters/auggie.js +10 -0
  20. package/lib/adapters/claude-code.js +181 -0
  21. package/lib/adapters/codex.js +10 -0
  22. package/lib/adapters/copilot.js +10 -0
  23. package/lib/adapters/gemini.js +10 -0
  24. package/lib/adapters/index.js +240 -0
  25. package/lib/adapters/kimi.js +10 -0
  26. package/lib/adapters/opencode.js +14 -0
  27. package/lib/adapters/vibe.js +10 -0
  28. package/lib/config.js +121 -5
  29. package/lib/tui.js +115 -0
  30. package/lib/zellij.js +248 -0
  31. package/package.json +6 -4
  32. package/postinstall.js +28 -25
  33. package/preuninstall.js +18 -9
  34. package/test/adapters/claude-code.test.js +144 -0
  35. package/test/adapters/patches.test.js +81 -0
  36. package/test/adapters/registry.test.js +89 -0
  37. package/test/adapters/stubs.test.js +46 -0
  38. package/test/cli-json.test.js +79 -0
  39. package/test/helpers/fake-fs.js +59 -0
  40. package/test-integration.sh +113 -0
  41. package/test-notification-plugin.kdl +34 -0
  42. package/test-updated-layout.sh +75 -0
  43. package/test-zellij-cli.sh +72 -0
  44. package/test.sh +1 -1
  45. package/zellij-plugin/.cargo/config.toml +5 -0
  46. package/zellij-plugin/.github/workflows/ci.yml +97 -0
  47. package/zellij-plugin/Cargo.lock +3558 -0
  48. package/zellij-plugin/Cargo.toml +40 -0
  49. package/zellij-plugin/README.md +290 -0
  50. package/zellij-plugin/build.sh +179 -0
  51. package/zellij-plugin/configs/examples/accessibility.kdl +31 -0
  52. package/zellij-plugin/configs/examples/catppuccin.kdl +32 -0
  53. package/zellij-plugin/configs/examples/default.kdl +34 -0
  54. package/zellij-plugin/configs/examples/minimal.kdl +22 -0
  55. package/zellij-plugin/docs/CONFIGURATION.md +191 -0
  56. package/zellij-plugin/docs/INTEGRATION.md +333 -0
  57. package/zellij-plugin/src/animation.rs +451 -0
  58. package/zellij-plugin/src/colors.rs +407 -0
  59. package/zellij-plugin/src/config.rs +664 -0
  60. package/zellij-plugin/src/event_bridge.rs +339 -0
  61. package/zellij-plugin/src/main.rs +420 -0
  62. package/zellij-plugin/src/notification.rs +466 -0
  63. package/zellij-plugin/src/queue.rs +399 -0
  64. package/zellij-plugin/src/renderer.rs +477 -0
  65. package/zellij-plugin/src/state.rs +338 -0
  66. package/zellij-plugin/src/tests.rs +413 -0
  67. package/ruv-swarm-mcp.db +0 -0
@@ -0,0 +1,339 @@
1
+ //! Event Bridge module for Zellij Visual Notifications
2
+ //!
3
+ //! Handles communication with the claude-notifications system via IPC/pipe messages.
4
+
5
+ use serde::{Deserialize, Serialize};
6
+ use crate::notification::{Notification, NotificationBuilder, NotificationType, Priority};
7
+
8
+ /// Event bridge for receiving notifications from claude-notifications
9
+ #[derive(Debug, Default)]
10
+ pub struct EventBridge {
11
+ /// Connection state
12
+ connection_state: ConnectionState,
13
+ /// Protocol version
14
+ protocol_version: String,
15
+ /// Last received message timestamp
16
+ last_message_timestamp: u64,
17
+ /// Error count for retry logic
18
+ error_count: u32,
19
+ /// Maximum errors before fallback
20
+ max_errors: u32,
21
+ }
22
+
23
+ /// Connection state for the event bridge
24
+ #[derive(Debug, Clone, PartialEq, Default)]
25
+ pub enum ConnectionState {
26
+ /// Not connected
27
+ #[default]
28
+ Disconnected,
29
+ /// Connecting
30
+ Connecting,
31
+ /// Connected and receiving events
32
+ Connected,
33
+ /// Connection error
34
+ Error(String),
35
+ }
36
+
37
+ impl EventBridge {
38
+ /// Create a new event bridge
39
+ pub fn new() -> Self {
40
+ Self {
41
+ connection_state: ConnectionState::Disconnected,
42
+ protocol_version: "1.0".to_string(),
43
+ last_message_timestamp: 0,
44
+ error_count: 0,
45
+ max_errors: 5,
46
+ }
47
+ }
48
+
49
+ /// Get the current connection state
50
+ pub fn connection_state(&self) -> &ConnectionState {
51
+ &self.connection_state
52
+ }
53
+
54
+ /// Check if connected
55
+ pub fn is_connected(&self) -> bool {
56
+ matches!(self.connection_state, ConnectionState::Connected)
57
+ }
58
+
59
+ /// Parse a notification from a JSON payload
60
+ pub fn parse_notification(&mut self, payload: &str) -> Result<Notification, EventBridgeError> {
61
+ // Try to parse as NotificationMessage first
62
+ match serde_json::from_str::<NotificationMessage>(payload) {
63
+ Ok(msg) => {
64
+ self.connection_state = ConnectionState::Connected;
65
+ self.error_count = 0;
66
+ self.last_message_timestamp = msg.timestamp.unwrap_or(0);
67
+ Ok(self.convert_message_to_notification(msg))
68
+ }
69
+ Err(e) => {
70
+ // Try legacy format
71
+ if let Ok(legacy) = serde_json::from_str::<LegacyNotificationMessage>(payload) {
72
+ self.connection_state = ConnectionState::Connected;
73
+ self.error_count = 0;
74
+ return Ok(self.convert_legacy_to_notification(legacy));
75
+ }
76
+
77
+ self.error_count += 1;
78
+ if self.error_count >= self.max_errors {
79
+ self.connection_state = ConnectionState::Error("Too many parse errors".to_string());
80
+ }
81
+
82
+ Err(EventBridgeError::ParseError(e.to_string()))
83
+ }
84
+ }
85
+ }
86
+
87
+ /// Convert a NotificationMessage to a Notification
88
+ fn convert_message_to_notification(&self, msg: NotificationMessage) -> Notification {
89
+ let notification_type = msg.notification_type
90
+ .map(|t| NotificationType::from_str(&t))
91
+ .unwrap_or(NotificationType::Attention);
92
+
93
+ let priority = msg.priority
94
+ .map(|p| match p.to_lowercase().as_str() {
95
+ "low" => Priority::Low,
96
+ "normal" => Priority::Normal,
97
+ "high" => Priority::High,
98
+ "critical" => Priority::Critical,
99
+ _ => Priority::from(&notification_type),
100
+ })
101
+ .unwrap_or_else(|| Priority::from(&notification_type));
102
+
103
+ let mut builder = NotificationBuilder::new()
104
+ .notification_type(notification_type)
105
+ .message(&msg.message.unwrap_or_else(|| "Claude is waiting...".to_string()))
106
+ .title(&msg.title.unwrap_or_else(|| "Claude Code".to_string()))
107
+ .source(&msg.source.unwrap_or_else(|| "claude-notifications".to_string()))
108
+ .priority(priority)
109
+ .timestamp(msg.timestamp.unwrap_or(0))
110
+ .ttl(msg.ttl_ms.unwrap_or(300_000));
111
+
112
+ // Add pane_id if present
113
+ if let Some(pane_id) = msg.pane_id {
114
+ builder = builder.pane_id(pane_id);
115
+ }
116
+
117
+ // Add tab_index if present
118
+ if let Some(tab_index) = msg.tab_index {
119
+ builder = builder.tab_index(tab_index);
120
+ }
121
+
122
+ builder.build()
123
+ }
124
+
125
+ /// Convert a legacy message format to a Notification
126
+ fn convert_legacy_to_notification(&self, msg: LegacyNotificationMessage) -> Notification {
127
+ Notification::attention(&msg.message)
128
+ .from_source("claude-notifications-legacy")
129
+ }
130
+
131
+ /// Handle connection established
132
+ pub fn on_connected(&mut self) {
133
+ self.connection_state = ConnectionState::Connected;
134
+ self.error_count = 0;
135
+ }
136
+
137
+ /// Handle connection error
138
+ pub fn on_error(&mut self, error: &str) {
139
+ self.error_count += 1;
140
+ if self.error_count >= self.max_errors {
141
+ self.connection_state = ConnectionState::Error(error.to_string());
142
+ }
143
+ }
144
+
145
+ /// Handle connection lost
146
+ pub fn on_disconnected(&mut self) {
147
+ self.connection_state = ConnectionState::Disconnected;
148
+ }
149
+
150
+ /// Get health status
151
+ pub fn health_status(&self) -> EventBridgeHealth {
152
+ EventBridgeHealth {
153
+ connected: self.is_connected(),
154
+ error_count: self.error_count,
155
+ last_message_timestamp: self.last_message_timestamp,
156
+ protocol_version: self.protocol_version.clone(),
157
+ }
158
+ }
159
+
160
+ /// Reset error count (for recovery)
161
+ pub fn reset_errors(&mut self) {
162
+ self.error_count = 0;
163
+ if matches!(self.connection_state, ConnectionState::Error(_)) {
164
+ self.connection_state = ConnectionState::Disconnected;
165
+ }
166
+ }
167
+ }
168
+
169
+ /// Notification message format from claude-notifications
170
+ #[derive(Debug, Serialize, Deserialize)]
171
+ pub struct NotificationMessage {
172
+ /// Protocol version
173
+ #[serde(default)]
174
+ pub version: Option<String>,
175
+ /// Notification type (success, error, warning, info, attention)
176
+ #[serde(rename = "type")]
177
+ pub notification_type: Option<String>,
178
+ /// Message content
179
+ pub message: Option<String>,
180
+ /// Title
181
+ pub title: Option<String>,
182
+ /// Source identifier
183
+ pub source: Option<String>,
184
+ /// Target pane ID
185
+ pub pane_id: Option<u32>,
186
+ /// Target tab index
187
+ pub tab_index: Option<usize>,
188
+ /// Priority (low, normal, high, critical)
189
+ pub priority: Option<String>,
190
+ /// Timestamp (Unix timestamp in milliseconds)
191
+ pub timestamp: Option<u64>,
192
+ /// TTL in milliseconds
193
+ pub ttl_ms: Option<u64>,
194
+ /// Command that triggered the notification
195
+ pub command: Option<String>,
196
+ /// Exit code
197
+ pub exit_code: Option<i32>,
198
+ /// Duration in milliseconds
199
+ pub duration_ms: Option<u64>,
200
+ }
201
+
202
+ /// Legacy notification message format (simple JSON)
203
+ #[derive(Debug, Serialize, Deserialize)]
204
+ struct LegacyNotificationMessage {
205
+ /// Message content
206
+ message: String,
207
+ }
208
+
209
+ /// Event bridge error types
210
+ #[derive(Debug, Clone)]
211
+ pub enum EventBridgeError {
212
+ /// JSON parse error
213
+ ParseError(String),
214
+ /// Connection error
215
+ ConnectionError(String),
216
+ /// Protocol version mismatch
217
+ VersionMismatch(String),
218
+ /// Invalid message format
219
+ InvalidFormat(String),
220
+ }
221
+
222
+ impl std::fmt::Display for EventBridgeError {
223
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224
+ match self {
225
+ EventBridgeError::ParseError(e) => write!(f, "Parse error: {}", e),
226
+ EventBridgeError::ConnectionError(e) => write!(f, "Connection error: {}", e),
227
+ EventBridgeError::VersionMismatch(e) => write!(f, "Version mismatch: {}", e),
228
+ EventBridgeError::InvalidFormat(e) => write!(f, "Invalid format: {}", e),
229
+ }
230
+ }
231
+ }
232
+
233
+ /// Event bridge health status
234
+ #[derive(Debug, Clone, Serialize, Deserialize)]
235
+ pub struct EventBridgeHealth {
236
+ /// Whether connected
237
+ pub connected: bool,
238
+ /// Number of errors
239
+ pub error_count: u32,
240
+ /// Last message timestamp
241
+ pub last_message_timestamp: u64,
242
+ /// Protocol version
243
+ pub protocol_version: String,
244
+ }
245
+
246
+ /// Create a test notification message (for testing)
247
+ pub fn create_test_message(notification_type: &str, message: &str) -> String {
248
+ let msg = NotificationMessage {
249
+ version: Some("1.0".to_string()),
250
+ notification_type: Some(notification_type.to_string()),
251
+ message: Some(message.to_string()),
252
+ title: Some("Test".to_string()),
253
+ source: Some("test".to_string()),
254
+ pane_id: None,
255
+ tab_index: None,
256
+ priority: None,
257
+ timestamp: Some(0),
258
+ ttl_ms: Some(300_000),
259
+ command: None,
260
+ exit_code: None,
261
+ duration_ms: None,
262
+ };
263
+ serde_json::to_string(&msg).unwrap_or_default()
264
+ }
265
+
266
+ #[cfg(test)]
267
+ mod tests {
268
+ use super::*;
269
+
270
+ #[test]
271
+ fn test_event_bridge_creation() {
272
+ let bridge = EventBridge::new();
273
+ assert!(!bridge.is_connected());
274
+ assert_eq!(bridge.error_count, 0);
275
+ }
276
+
277
+ #[test]
278
+ fn test_parse_notification_message() {
279
+ let mut bridge = EventBridge::new();
280
+
281
+ let json = r#"{
282
+ "version": "1.0",
283
+ "type": "success",
284
+ "message": "Build completed",
285
+ "title": "Claude Code",
286
+ "source": "claude-notifications"
287
+ }"#;
288
+
289
+ let result = bridge.parse_notification(json);
290
+ assert!(result.is_ok());
291
+
292
+ let notif = result.unwrap();
293
+ assert_eq!(notif.notification_type, NotificationType::Success);
294
+ assert_eq!(notif.message, "Build completed");
295
+ }
296
+
297
+ #[test]
298
+ fn test_parse_legacy_message() {
299
+ let mut bridge = EventBridge::new();
300
+
301
+ let json = r#"{"message": "Claude is waiting for you..."}"#;
302
+
303
+ let result = bridge.parse_notification(json);
304
+ assert!(result.is_ok());
305
+
306
+ let notif = result.unwrap();
307
+ assert_eq!(notif.notification_type, NotificationType::Attention);
308
+ }
309
+
310
+ #[test]
311
+ fn test_parse_error_handling() {
312
+ let mut bridge = EventBridge::new();
313
+
314
+ let invalid_json = "not valid json";
315
+
316
+ for _ in 0..5 {
317
+ let _ = bridge.parse_notification(invalid_json);
318
+ }
319
+
320
+ assert!(matches!(bridge.connection_state, ConnectionState::Error(_)));
321
+ }
322
+
323
+ #[test]
324
+ fn test_health_status() {
325
+ let bridge = EventBridge::new();
326
+ let health = bridge.health_status();
327
+
328
+ assert!(!health.connected);
329
+ assert_eq!(health.error_count, 0);
330
+ assert_eq!(health.protocol_version, "1.0");
331
+ }
332
+
333
+ #[test]
334
+ fn test_create_test_message() {
335
+ let msg = create_test_message("success", "Test message");
336
+ assert!(msg.contains("success"));
337
+ assert!(msg.contains("Test message"));
338
+ }
339
+ }