@moejay/wrightty 0.0.0 → 0.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 (92) hide show
  1. package/dist/client.d.ts +14 -0
  2. package/dist/client.js +83 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +8 -0
  5. package/dist/terminal.d.ts +48 -0
  6. package/dist/terminal.js +210 -0
  7. package/dist/types.d.ts +90 -0
  8. package/dist/types.js +3 -0
  9. package/package.json +35 -15
  10. package/.github/workflows/ci.yml +0 -90
  11. package/.github/workflows/release.yml +0 -177
  12. package/Cargo.lock +0 -2662
  13. package/Cargo.toml +0 -38
  14. package/PROTOCOL.md +0 -1351
  15. package/README.md +0 -386
  16. package/agents/ceo/AGENTS.md +0 -24
  17. package/agents/ceo/HEARTBEAT.md +0 -72
  18. package/agents/ceo/SOUL.md +0 -33
  19. package/agents/ceo/TOOLS.md +0 -3
  20. package/agents/founding-engineer/AGENTS.md +0 -44
  21. package/crates/wrightty/Cargo.toml +0 -43
  22. package/crates/wrightty/src/client_cmds.rs +0 -366
  23. package/crates/wrightty/src/discover.rs +0 -78
  24. package/crates/wrightty/src/main.rs +0 -100
  25. package/crates/wrightty/src/server.rs +0 -100
  26. package/crates/wrightty/src/term.rs +0 -338
  27. package/crates/wrightty-bridge-ghostty/Cargo.toml +0 -27
  28. package/crates/wrightty-bridge-ghostty/src/ghostty.rs +0 -422
  29. package/crates/wrightty-bridge-ghostty/src/lib.rs +0 -2
  30. package/crates/wrightty-bridge-ghostty/src/main.rs +0 -146
  31. package/crates/wrightty-bridge-ghostty/src/rpc.rs +0 -307
  32. package/crates/wrightty-bridge-kitty/Cargo.toml +0 -26
  33. package/crates/wrightty-bridge-kitty/src/kitty.rs +0 -269
  34. package/crates/wrightty-bridge-kitty/src/lib.rs +0 -2
  35. package/crates/wrightty-bridge-kitty/src/main.rs +0 -124
  36. package/crates/wrightty-bridge-kitty/src/rpc.rs +0 -304
  37. package/crates/wrightty-bridge-tmux/Cargo.toml +0 -26
  38. package/crates/wrightty-bridge-tmux/src/lib.rs +0 -2
  39. package/crates/wrightty-bridge-tmux/src/main.rs +0 -119
  40. package/crates/wrightty-bridge-tmux/src/rpc.rs +0 -291
  41. package/crates/wrightty-bridge-tmux/src/tmux.rs +0 -215
  42. package/crates/wrightty-bridge-wezterm/Cargo.toml +0 -26
  43. package/crates/wrightty-bridge-wezterm/src/lib.rs +0 -2
  44. package/crates/wrightty-bridge-wezterm/src/main.rs +0 -119
  45. package/crates/wrightty-bridge-wezterm/src/rpc.rs +0 -339
  46. package/crates/wrightty-bridge-wezterm/src/wezterm.rs +0 -190
  47. package/crates/wrightty-bridge-zellij/Cargo.toml +0 -27
  48. package/crates/wrightty-bridge-zellij/src/lib.rs +0 -2
  49. package/crates/wrightty-bridge-zellij/src/main.rs +0 -125
  50. package/crates/wrightty-bridge-zellij/src/rpc.rs +0 -328
  51. package/crates/wrightty-bridge-zellij/src/zellij.rs +0 -199
  52. package/crates/wrightty-client/Cargo.toml +0 -16
  53. package/crates/wrightty-client/src/client.rs +0 -254
  54. package/crates/wrightty-client/src/lib.rs +0 -2
  55. package/crates/wrightty-core/Cargo.toml +0 -21
  56. package/crates/wrightty-core/src/input.rs +0 -212
  57. package/crates/wrightty-core/src/lib.rs +0 -4
  58. package/crates/wrightty-core/src/screen.rs +0 -325
  59. package/crates/wrightty-core/src/session.rs +0 -249
  60. package/crates/wrightty-core/src/session_manager.rs +0 -77
  61. package/crates/wrightty-protocol/Cargo.toml +0 -13
  62. package/crates/wrightty-protocol/src/error.rs +0 -8
  63. package/crates/wrightty-protocol/src/events.rs +0 -138
  64. package/crates/wrightty-protocol/src/lib.rs +0 -4
  65. package/crates/wrightty-protocol/src/methods.rs +0 -321
  66. package/crates/wrightty-protocol/src/types.rs +0 -201
  67. package/crates/wrightty-server/Cargo.toml +0 -23
  68. package/crates/wrightty-server/src/lib.rs +0 -2
  69. package/crates/wrightty-server/src/main.rs +0 -65
  70. package/crates/wrightty-server/src/rpc.rs +0 -455
  71. package/crates/wrightty-server/src/state.rs +0 -39
  72. package/examples/basic_command.py +0 -53
  73. package/examples/interactive_tui.py +0 -86
  74. package/examples/record_session.py +0 -96
  75. package/install.sh +0 -81
  76. package/sdks/node/package-lock.json +0 -85
  77. package/sdks/node/package.json +0 -44
  78. package/sdks/node/src/client.ts +0 -94
  79. package/sdks/node/src/index.ts +0 -19
  80. package/sdks/node/src/terminal.ts +0 -258
  81. package/sdks/node/src/types.ts +0 -105
  82. package/sdks/node/tsconfig.json +0 -17
  83. package/sdks/python/README.md +0 -96
  84. package/sdks/python/pyproject.toml +0 -42
  85. package/sdks/python/wrightty/__init__.py +0 -6
  86. package/sdks/python/wrightty/cli.py +0 -210
  87. package/sdks/python/wrightty/client.py +0 -136
  88. package/sdks/python/wrightty/mcp_server.py +0 -434
  89. package/sdks/python/wrightty/terminal.py +0 -333
  90. package/skills/wrightty/SKILL.md +0 -261
  91. package/src/lib.rs +0 -1
  92. package/tests/integration_test.rs +0 -618
@@ -1,124 +0,0 @@
1
- use std::net::{SocketAddr, TcpListener};
2
- use std::process;
3
- use std::time::Duration;
4
-
5
- use clap::Parser;
6
- use jsonrpsee::server::Server;
7
- use tracing_subscriber::EnvFilter;
8
-
9
- use wrightty_bridge_kitty::kitty;
10
- use wrightty_bridge_kitty::rpc::build_rpc_module;
11
-
12
- const PORT_RANGE_START: u16 = 9461;
13
- const PORT_RANGE_END: u16 = 9480;
14
-
15
- #[derive(Parser)]
16
- #[command(
17
- name = "wrightty-bridge-kitty",
18
- about = "Bridge that translates wrightty protocol calls into kitty remote control commands"
19
- )]
20
- struct Cli {
21
- #[arg(long, default_value = "127.0.0.1")]
22
- host: String,
23
-
24
- /// Port to listen on. If not specified, auto-selects the next available port starting at 9461.
25
- #[arg(long)]
26
- port: Option<u16>,
27
-
28
- /// Interval in seconds to check if kitty is still running. 0 to disable.
29
- #[arg(long, default_value_t = 10)]
30
- watchdog_interval: u64,
31
- }
32
-
33
- fn find_available_port(host: &str, start: u16, end: u16) -> Option<u16> {
34
- for port in start..=end {
35
- if TcpListener::bind(format!("{host}:{port}")).is_ok() {
36
- return Some(port);
37
- }
38
- }
39
- None
40
- }
41
-
42
- #[tokio::main]
43
- async fn main() -> anyhow::Result<()> {
44
- tracing_subscriber::fmt()
45
- .with_env_filter(
46
- EnvFilter::from_default_env()
47
- .add_directive("wrightty_bridge_kitty=info".parse()?),
48
- )
49
- .init();
50
-
51
- let cli = Cli::parse();
52
-
53
- // --- Startup health check ---
54
- tracing::info!("Checking kitty connectivity...");
55
- match kitty::health_check().await {
56
- Ok(()) => tracing::info!("kitty is reachable"),
57
- Err(e) => {
58
- eprintln!("error: Cannot connect to kitty: {e}");
59
- eprintln!();
60
- eprintln!("Make sure kitty is running with remote control enabled.");
61
- eprintln!("Add to kitty.conf:");
62
- eprintln!(" allow_remote_control yes");
63
- eprintln!("Or launch kitty with:");
64
- eprintln!(" kitty --listen-on unix:/tmp/kitty.sock");
65
- eprintln!("And set:");
66
- eprintln!(" KITTY_LISTEN_ON=unix:/tmp/kitty.sock");
67
- process::exit(1);
68
- },
69
- }
70
-
71
- let port = match cli.port {
72
- Some(p) => p,
73
- None => find_available_port(&cli.host, PORT_RANGE_START, PORT_RANGE_END)
74
- .ok_or_else(|| anyhow::anyhow!("No available port in range {PORT_RANGE_START}-{PORT_RANGE_END}"))?,
75
- };
76
-
77
- let addr: SocketAddr = format!("{}:{}", cli.host, port).parse()?;
78
-
79
- let module = build_rpc_module()?;
80
-
81
- let server = Server::builder().build(addr).await?;
82
-
83
- let handle = server.start(module);
84
-
85
- tracing::info!("wrightty-bridge-kitty listening on ws://{addr}");
86
- println!("wrightty-bridge-kitty listening on ws://{addr}");
87
-
88
- // --- Watchdog: periodically check kitty is still alive ---
89
- if cli.watchdog_interval > 0 {
90
- let interval = Duration::from_secs(cli.watchdog_interval);
91
- let server_handle = handle.clone();
92
- tokio::spawn(async move {
93
- let mut consecutive_failures = 0u32;
94
- loop {
95
- tokio::time::sleep(interval).await;
96
- match kitty::health_check().await {
97
- Ok(()) => {
98
- if consecutive_failures > 0 {
99
- tracing::info!("kitty reconnected");
100
- consecutive_failures = 0;
101
- }
102
- },
103
- Err(e) => {
104
- consecutive_failures += 1;
105
- tracing::warn!(
106
- "kitty health check failed ({consecutive_failures}): {e}"
107
- );
108
- if consecutive_failures >= 3 {
109
- tracing::error!(
110
- "kitty unreachable after {consecutive_failures} checks, shutting down"
111
- );
112
- server_handle.stop().unwrap();
113
- return;
114
- }
115
- },
116
- }
117
- }
118
- });
119
- }
120
-
121
- handle.stopped().await;
122
-
123
- Ok(())
124
- }
@@ -1,304 +0,0 @@
1
- //! jsonrpsee RPC module that maps wrightty protocol methods to kitty remote control commands.
2
-
3
- use jsonrpsee::types::ErrorObjectOwned;
4
- use jsonrpsee::RpcModule;
5
-
6
- use wrightty_protocol::error;
7
- use wrightty_protocol::methods::*;
8
- use wrightty_protocol::types::*;
9
-
10
- use crate::kitty;
11
-
12
- fn proto_err(code: i32, msg: impl Into<String>) -> ErrorObjectOwned {
13
- ErrorObjectOwned::owned(code, msg.into(), None::<()>)
14
- }
15
-
16
- fn not_supported(method: &str) -> ErrorObjectOwned {
17
- proto_err(error::NOT_SUPPORTED, format!("{method} is not supported by the kitty bridge"))
18
- }
19
-
20
- /// Parse a session ID string into a kitty window ID.
21
- fn parse_window_id(session_id: &str) -> Result<u64, ErrorObjectOwned> {
22
- session_id
23
- .parse::<u64>()
24
- .map_err(|_| proto_err(error::SESSION_NOT_FOUND, format!("invalid session id: {session_id}")))
25
- }
26
-
27
- /// Convert a wrightty KeyInput to a kitty key name string.
28
- ///
29
- /// kitty `send-key` uses its own key naming convention:
30
- /// - Modifiers: `ctrl+c`, `alt+x`, `ctrl+shift+t`
31
- /// - Named keys: `enter`, `tab`, `backspace`, `delete`, `escape`
32
- /// - Arrows: `up`, `down`, `left`, `right`
33
- /// - Function keys: `f1`..`f12`
34
- fn encode_key_to_kitty(key: &KeyInput) -> String {
35
- match key {
36
- KeyInput::Shorthand(s) => shorthand_to_kitty(s),
37
- KeyInput::Structured(event) => key_event_to_kitty(event),
38
- }
39
- }
40
-
41
- fn shorthand_to_kitty(s: &str) -> String {
42
- if let Some((modifier, key)) = s.split_once('+') {
43
- let mod_lower = modifier.to_lowercase();
44
- return format!("{}+{}", mod_lower, key.to_lowercase());
45
- }
46
-
47
- match s {
48
- "Enter" | "Return" => "enter".to_string(),
49
- "Tab" => "tab".to_string(),
50
- "Backspace" => "backspace".to_string(),
51
- "Delete" => "delete".to_string(),
52
- "Escape" | "Esc" => "escape".to_string(),
53
- "ArrowUp" | "Up" => "up".to_string(),
54
- "ArrowDown" | "Down" => "down".to_string(),
55
- "ArrowRight" | "Right" => "right".to_string(),
56
- "ArrowLeft" | "Left" => "left".to_string(),
57
- "Home" => "home".to_string(),
58
- "End" => "end".to_string(),
59
- "PageUp" => "page_up".to_string(),
60
- "PageDown" => "page_down".to_string(),
61
- "Insert" => "insert".to_string(),
62
- _ => s.to_lowercase(),
63
- }
64
- }
65
-
66
- fn key_event_to_kitty(event: &KeyEvent) -> String {
67
- let has_ctrl = event.modifiers.iter().any(|m| matches!(m, Modifier::Ctrl));
68
- let has_alt = event.modifiers.iter().any(|m| matches!(m, Modifier::Alt));
69
- let has_shift = event.modifiers.iter().any(|m| matches!(m, Modifier::Shift));
70
-
71
- let base = match &event.key {
72
- KeyType::Char => event.char.as_deref().unwrap_or("").to_lowercase(),
73
- KeyType::Enter => "enter".to_string(),
74
- KeyType::Tab => "tab".to_string(),
75
- KeyType::Backspace => "backspace".to_string(),
76
- KeyType::Delete => "delete".to_string(),
77
- KeyType::Escape => "escape".to_string(),
78
- KeyType::ArrowUp => "up".to_string(),
79
- KeyType::ArrowDown => "down".to_string(),
80
- KeyType::ArrowRight => "right".to_string(),
81
- KeyType::ArrowLeft => "left".to_string(),
82
- KeyType::Home => "home".to_string(),
83
- KeyType::End => "end".to_string(),
84
- KeyType::PageUp => "page_up".to_string(),
85
- KeyType::PageDown => "page_down".to_string(),
86
- KeyType::Insert => "insert".to_string(),
87
- KeyType::F => format!("f{}", event.n.unwrap_or(1)),
88
- };
89
-
90
- let mut modifiers = Vec::new();
91
- if has_ctrl { modifiers.push("ctrl"); }
92
- if has_alt { modifiers.push("alt"); }
93
- if has_shift { modifiers.push("shift"); }
94
-
95
- if modifiers.is_empty() {
96
- base
97
- } else {
98
- format!("{}+{base}", modifiers.join("+"))
99
- }
100
- }
101
-
102
- pub fn build_rpc_module() -> anyhow::Result<RpcModule<()>> {
103
- let mut module = RpcModule::new(());
104
-
105
- // --- Wrightty.getInfo ---
106
- module.register_async_method("Wrightty.getInfo", |_params, _state, _| async move {
107
- serde_json::to_value(GetInfoResult {
108
- info: ServerInfo {
109
- version: "0.1.0".to_string(),
110
- implementation: "wrightty-bridge-kitty".to_string(),
111
- capabilities: Capabilities {
112
- screenshot: vec![ScreenshotFormat::Text],
113
- max_sessions: 256,
114
- supports_resize: true,
115
- supports_scrollback: true,
116
- supports_mouse: false,
117
- supports_session_create: true,
118
- supports_color_palette: false,
119
- supports_raw_output: false,
120
- supports_shell_integration: false,
121
- events: vec![],
122
- },
123
- },
124
- })
125
- .map_err(|e| proto_err(-32603, e.to_string()))
126
- })?;
127
-
128
- // --- Session.create ---
129
- module.register_async_method("Session.create", |_params, _state, _| async move {
130
- let window_id = kitty::launch_window()
131
- .await
132
- .map_err(|e| proto_err(error::SPAWN_FAILED, e.to_string()))?;
133
-
134
- serde_json::to_value(SessionCreateResult {
135
- session_id: window_id.to_string(),
136
- })
137
- .map_err(|e| proto_err(-32603, e.to_string()))
138
- })?;
139
-
140
- // --- Session.destroy ---
141
- module.register_async_method("Session.destroy", |params, _state, _| async move {
142
- let p: SessionDestroyParams = params.parse()?;
143
- let window_id = parse_window_id(&p.session_id)?;
144
-
145
- kitty::close_window(window_id)
146
- .await
147
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
148
-
149
- serde_json::to_value(SessionDestroyResult { exit_code: None })
150
- .map_err(|e| proto_err(-32603, e.to_string()))
151
- })?;
152
-
153
- // --- Session.list ---
154
- module.register_async_method("Session.list", |_params, _state, _| async move {
155
- let windows = kitty::list_windows()
156
- .await
157
- .map_err(|e| proto_err(-32603, e.to_string()))?;
158
-
159
- let sessions: Vec<SessionInfo> = windows
160
- .into_iter()
161
- .map(|w| SessionInfo {
162
- session_id: w.id.to_string(),
163
- title: w.title,
164
- cwd: w.cwd,
165
- cols: w.columns,
166
- rows: w.lines,
167
- pid: w.pid,
168
- running: true,
169
- alternate_screen: false,
170
- })
171
- .collect();
172
-
173
- serde_json::to_value(SessionListResult { sessions })
174
- .map_err(|e| proto_err(-32603, e.to_string()))
175
- })?;
176
-
177
- // --- Session.getInfo ---
178
- module.register_async_method("Session.getInfo", |params, _state, _| async move {
179
- let p: SessionGetInfoParams = params.parse()?;
180
- let window_id = parse_window_id(&p.session_id)?;
181
-
182
- let w = kitty::find_window(window_id)
183
- .await
184
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
185
-
186
- let info = SessionInfo {
187
- session_id: w.id.to_string(),
188
- title: w.title,
189
- cwd: w.cwd,
190
- cols: w.columns,
191
- rows: w.lines,
192
- pid: w.pid,
193
- running: true,
194
- alternate_screen: false,
195
- };
196
-
197
- serde_json::to_value(info).map_err(|e| proto_err(-32603, e.to_string()))
198
- })?;
199
-
200
- // --- Input.sendText ---
201
- module.register_async_method("Input.sendText", |params, _state, _| async move {
202
- let p: InputSendTextParams = params.parse()?;
203
- let window_id = parse_window_id(&p.session_id)?;
204
-
205
- kitty::send_text(window_id, &p.text)
206
- .await
207
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
208
-
209
- Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
210
- })?;
211
-
212
- // --- Input.sendKeys ---
213
- module.register_async_method("Input.sendKeys", |params, _state, _| async move {
214
- let p: InputSendKeysParams = params.parse()?;
215
- let window_id = parse_window_id(&p.session_id)?;
216
-
217
- for key in &p.keys {
218
- // Plain single characters go via send-text; everything else via send-key
219
- let is_literal_char = matches!(key, KeyInput::Shorthand(s) if s.len() == 1 && !s.contains('+'));
220
- if is_literal_char {
221
- let text = match key {
222
- KeyInput::Shorthand(s) => s.clone(),
223
- _ => unreachable!(),
224
- };
225
- kitty::send_text(window_id, &text)
226
- .await
227
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
228
- } else {
229
- let kitty_key = encode_key_to_kitty(key);
230
- kitty::send_key(window_id, &kitty_key)
231
- .await
232
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
233
- }
234
- }
235
-
236
- Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
237
- })?;
238
-
239
- // --- Screen.getText ---
240
- module.register_async_method("Screen.getText", |params, _state, _| async move {
241
- let p: ScreenGetTextParams = params.parse()?;
242
- let window_id = parse_window_id(&p.session_id)?;
243
-
244
- let mut text = kitty::get_text(window_id)
245
- .await
246
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
247
-
248
- if p.trim_trailing_whitespace {
249
- text = text
250
- .lines()
251
- .map(|line| line.trim_end())
252
- .collect::<Vec<_>>()
253
- .join("\n");
254
- }
255
-
256
- serde_json::to_value(ScreenGetTextResult { text })
257
- .map_err(|e| proto_err(-32603, e.to_string()))
258
- })?;
259
-
260
- // --- Terminal.getSize ---
261
- module.register_async_method("Terminal.getSize", |params, _state, _| async move {
262
- let p: TerminalGetSizeParams = params.parse()?;
263
- let window_id = parse_window_id(&p.session_id)?;
264
-
265
- let w = kitty::find_window(window_id)
266
- .await
267
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
268
-
269
- serde_json::to_value(TerminalGetSizeResult {
270
- cols: w.columns,
271
- rows: w.lines,
272
- })
273
- .map_err(|e| proto_err(-32603, e.to_string()))
274
- })?;
275
-
276
- // --- Terminal.resize ---
277
- module.register_async_method("Terminal.resize", |params, _state, _| async move {
278
- let p: TerminalResizeParams = params.parse()?;
279
- let window_id = parse_window_id(&p.session_id)?;
280
-
281
- kitty::resize_window(window_id, p.cols, p.rows)
282
- .await
283
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
284
-
285
- Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
286
- })?;
287
-
288
- // --- Screen.getContents (not supported) ---
289
- module.register_async_method("Screen.getContents", |_params, _state, _| async move {
290
- Err::<serde_json::Value, _>(not_supported("Screen.getContents"))
291
- })?;
292
-
293
- // --- Screen.screenshot (not supported) ---
294
- module.register_async_method("Screen.screenshot", |_params, _state, _| async move {
295
- Err::<serde_json::Value, _>(not_supported("Screen.screenshot"))
296
- })?;
297
-
298
- // --- Input.sendMouse (not supported) ---
299
- module.register_async_method("Input.sendMouse", |_params, _state, _| async move {
300
- Err::<serde_json::Value, _>(not_supported("Input.sendMouse"))
301
- })?;
302
-
303
- Ok(module)
304
- }
@@ -1,26 +0,0 @@
1
- [package]
2
- name = "wrightty-bridge-tmux"
3
- version.workspace = true
4
- edition.workspace = true
5
- license.workspace = true
6
- authors.workspace = true
7
- repository.workspace = true
8
- homepage.workspace = true
9
- description = "Bridge that translates wrightty protocol calls into tmux CLI commands"
10
-
11
- [[bin]]
12
- name = "wrightty-bridge-tmux"
13
- path = "src/main.rs"
14
-
15
- [dependencies]
16
- wrightty-protocol = { version = "0.1.0", path = "../wrightty-protocol" }
17
-
18
- jsonrpsee = { version = "0.24", features = ["server"] }
19
- tokio = { version = "1", features = ["full"] }
20
- clap = { version = "4", features = ["derive"] }
21
- tracing = "0.1"
22
- tracing-subscriber = { version = "0.3", features = ["env-filter"] }
23
- anyhow = "1"
24
- serde = { version = "1", features = ["derive"] }
25
- serde_json = "1"
26
- thiserror = "1"
@@ -1,2 +0,0 @@
1
- pub mod rpc;
2
- pub mod tmux;
@@ -1,119 +0,0 @@
1
- use std::net::{SocketAddr, TcpListener};
2
- use std::process;
3
- use std::time::Duration;
4
-
5
- use clap::Parser;
6
- use jsonrpsee::server::Server;
7
- use tracing_subscriber::EnvFilter;
8
-
9
- use wrightty_bridge_tmux::rpc::build_rpc_module;
10
- use wrightty_bridge_tmux::tmux;
11
-
12
- const PORT_RANGE_START: u16 = 9441;
13
- const PORT_RANGE_END: u16 = 9460;
14
-
15
- #[derive(Parser)]
16
- #[command(
17
- name = "wrightty-bridge-tmux",
18
- about = "Bridge that translates wrightty protocol calls into tmux CLI commands"
19
- )]
20
- struct Cli {
21
- #[arg(long, default_value = "127.0.0.1")]
22
- host: String,
23
-
24
- /// Port to listen on. If not specified, auto-selects the next available port starting at 9441.
25
- #[arg(long)]
26
- port: Option<u16>,
27
-
28
- /// Interval in seconds to check if tmux is still running. 0 to disable.
29
- #[arg(long, default_value_t = 10)]
30
- watchdog_interval: u64,
31
- }
32
-
33
- fn find_available_port(host: &str, start: u16, end: u16) -> Option<u16> {
34
- for port in start..=end {
35
- if TcpListener::bind(format!("{host}:{port}")).is_ok() {
36
- return Some(port);
37
- }
38
- }
39
- None
40
- }
41
-
42
- #[tokio::main]
43
- async fn main() -> anyhow::Result<()> {
44
- tracing_subscriber::fmt()
45
- .with_env_filter(
46
- EnvFilter::from_default_env()
47
- .add_directive("wrightty_bridge_tmux=info".parse()?),
48
- )
49
- .init();
50
-
51
- let cli = Cli::parse();
52
-
53
- // --- Startup health check ---
54
- tracing::info!("Checking tmux connectivity...");
55
- match tmux::health_check().await {
56
- Ok(()) => tracing::info!("tmux is reachable"),
57
- Err(e) => {
58
- eprintln!("error: Cannot connect to tmux: {e}");
59
- eprintln!();
60
- eprintln!("Make sure a tmux server is running. Start one with:");
61
- eprintln!(" tmux new-session -d -s main");
62
- process::exit(1);
63
- },
64
- }
65
-
66
- let port = match cli.port {
67
- Some(p) => p,
68
- None => find_available_port(&cli.host, PORT_RANGE_START, PORT_RANGE_END)
69
- .ok_or_else(|| anyhow::anyhow!("No available port in range {PORT_RANGE_START}-{PORT_RANGE_END}"))?,
70
- };
71
-
72
- let addr: SocketAddr = format!("{}:{}", cli.host, port).parse()?;
73
-
74
- let module = build_rpc_module()?;
75
-
76
- let server = Server::builder().build(addr).await?;
77
-
78
- let handle = server.start(module);
79
-
80
- tracing::info!("wrightty-bridge-tmux listening on ws://{addr}");
81
- println!("wrightty-bridge-tmux listening on ws://{addr}");
82
-
83
- // --- Watchdog: periodically check tmux is still alive ---
84
- if cli.watchdog_interval > 0 {
85
- let interval = Duration::from_secs(cli.watchdog_interval);
86
- let server_handle = handle.clone();
87
- tokio::spawn(async move {
88
- let mut consecutive_failures = 0u32;
89
- loop {
90
- tokio::time::sleep(interval).await;
91
- match tmux::health_check().await {
92
- Ok(()) => {
93
- if consecutive_failures > 0 {
94
- tracing::info!("tmux reconnected");
95
- consecutive_failures = 0;
96
- }
97
- },
98
- Err(e) => {
99
- consecutive_failures += 1;
100
- tracing::warn!(
101
- "tmux health check failed ({consecutive_failures}): {e}"
102
- );
103
- if consecutive_failures >= 3 {
104
- tracing::error!(
105
- "tmux unreachable after {consecutive_failures} checks, shutting down"
106
- );
107
- server_handle.stop().unwrap();
108
- return;
109
- }
110
- },
111
- }
112
- }
113
- });
114
- }
115
-
116
- handle.stopped().await;
117
-
118
- Ok(())
119
- }