claude-code-tools 0.1.8__py3-none-any.whl

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.

Potentially problematic release.


This version of claude-code-tools might be problematic. Click here for more details.

@@ -0,0 +1,705 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tmux CLI Controller for Claude Code
4
+ This script provides functions to interact with CLI applications running in tmux panes.
5
+ """
6
+
7
+ import subprocess
8
+ import time
9
+ import re
10
+ from typing import Optional, List, Dict, Tuple, Callable, Union
11
+ import json
12
+ import os
13
+ import hashlib
14
+ from pathlib import Path
15
+
16
+
17
+ class TmuxCLIController:
18
+ """Controller for interacting with CLI applications in tmux panes."""
19
+
20
+ def __init__(self, session_name: Optional[str] = None, window_name: Optional[str] = None):
21
+ """
22
+ Initialize the controller.
23
+
24
+ Args:
25
+ session_name: Name of tmux session (defaults to current)
26
+ window_name: Name of tmux window (defaults to current)
27
+ """
28
+ self.session_name = session_name
29
+ self.window_name = window_name
30
+ self.target_pane = None
31
+
32
+ def _run_tmux_command(self, command: List[str]) -> Tuple[str, int]:
33
+ """
34
+ Run a tmux command and return output and exit code.
35
+
36
+ Args:
37
+ command: List of command components
38
+
39
+ Returns:
40
+ Tuple of (output, exit_code)
41
+ """
42
+ result = subprocess.run(
43
+ ['tmux'] + command,
44
+ capture_output=True,
45
+ text=True
46
+ )
47
+ return result.stdout.strip(), result.returncode
48
+
49
+ def get_current_session(self) -> Optional[str]:
50
+ """Get the name of the current tmux session."""
51
+ output, code = self._run_tmux_command(['display-message', '-p', '#{session_name}'])
52
+ return output if code == 0 else None
53
+
54
+ def get_current_window(self) -> Optional[str]:
55
+ """Get the name of the current tmux window."""
56
+ output, code = self._run_tmux_command(['display-message', '-p', '#{window_name}'])
57
+ return output if code == 0 else None
58
+
59
+ def get_current_pane(self) -> Optional[str]:
60
+ """Get the ID of the current tmux pane."""
61
+ output, code = self._run_tmux_command(['display-message', '-p', '#{pane_id}'])
62
+ return output if code == 0 else None
63
+
64
+ def list_panes(self) -> List[Dict[str, str]]:
65
+ """
66
+ List all panes in the current window.
67
+
68
+ Returns:
69
+ List of dicts with pane info (id, index, title, active, size)
70
+ """
71
+ target = f"{self.session_name}:{self.window_name}" if self.session_name and self.window_name else ""
72
+
73
+ output, code = self._run_tmux_command([
74
+ 'list-panes',
75
+ '-t', target,
76
+ '-F', '#{pane_id}|#{pane_index}|#{pane_title}|#{pane_active}|#{pane_width}x#{pane_height}'
77
+ ] if target else [
78
+ 'list-panes',
79
+ '-F', '#{pane_id}|#{pane_index}|#{pane_title}|#{pane_active}|#{pane_width}x#{pane_height}'
80
+ ])
81
+
82
+ if code != 0:
83
+ return []
84
+
85
+ panes = []
86
+ for line in output.split('\n'):
87
+ if line:
88
+ parts = line.split('|')
89
+ panes.append({
90
+ 'id': parts[0],
91
+ 'index': parts[1],
92
+ 'title': parts[2],
93
+ 'active': parts[3] == '1',
94
+ 'size': parts[4]
95
+ })
96
+ return panes
97
+
98
+ def create_pane(self, vertical: bool = True, size: Optional[int] = None,
99
+ start_command: Optional[str] = None) -> Optional[str]:
100
+ """
101
+ Create a new pane in the current window.
102
+
103
+ Args:
104
+ vertical: If True, split vertically (side by side), else horizontally
105
+ size: Size percentage for the new pane (e.g., 50 for 50%)
106
+ start_command: Command to run in the new pane
107
+
108
+ Returns:
109
+ Pane ID of the created pane
110
+ """
111
+ cmd = ['split-window']
112
+
113
+ if vertical:
114
+ cmd.append('-h')
115
+ else:
116
+ cmd.append('-v')
117
+
118
+ if size:
119
+ cmd.extend(['-p', str(size)])
120
+
121
+ cmd.extend(['-P', '-F', '#{pane_id}'])
122
+
123
+ if start_command:
124
+ cmd.append(start_command)
125
+
126
+ output, code = self._run_tmux_command(cmd)
127
+
128
+ if code == 0:
129
+ self.target_pane = output
130
+ return output
131
+ return None
132
+
133
+ def select_pane(self, pane_id: Optional[str] = None, pane_index: Optional[int] = None):
134
+ """
135
+ Select a pane as the target for operations.
136
+
137
+ Args:
138
+ pane_id: Pane ID (e.g., %0, %1)
139
+ pane_index: Pane index (0-based)
140
+ """
141
+ if pane_id:
142
+ self.target_pane = pane_id
143
+ elif pane_index is not None:
144
+ panes = self.list_panes()
145
+ for pane in panes:
146
+ if int(pane['index']) == pane_index:
147
+ self.target_pane = pane['id']
148
+ break
149
+
150
+ def send_keys(self, text: str, pane_id: Optional[str] = None, enter: bool = True,
151
+ delay_enter: Union[bool, float] = True):
152
+ """
153
+ Send keystrokes to a pane.
154
+
155
+ Args:
156
+ text: Text to send
157
+ pane_id: Target pane (uses self.target_pane if not specified)
158
+ enter: Whether to send Enter key after text
159
+ delay_enter: If True, use 1.0s delay; if float, use that delay in seconds (default: True)
160
+ """
161
+ target = pane_id or self.target_pane
162
+ if not target:
163
+ raise ValueError("No target pane specified")
164
+
165
+ if enter and delay_enter:
166
+ # Send text without Enter first
167
+ cmd = ['send-keys', '-t', target, text]
168
+ self._run_tmux_command(cmd)
169
+
170
+ # Determine delay duration
171
+ if isinstance(delay_enter, bool):
172
+ delay = 1.0 # Default delay
173
+ else:
174
+ delay = float(delay_enter)
175
+
176
+ # Apply delay
177
+ time.sleep(delay)
178
+
179
+ # Then send just Enter
180
+ cmd = ['send-keys', '-t', target, 'Enter']
181
+ self._run_tmux_command(cmd)
182
+ else:
183
+ # Original behavior
184
+ cmd = ['send-keys', '-t', target, text]
185
+ if enter:
186
+ cmd.append('Enter')
187
+ self._run_tmux_command(cmd)
188
+
189
+ def capture_pane(self, pane_id: Optional[str] = None, lines: Optional[int] = None) -> str:
190
+ """
191
+ Capture the contents of a pane.
192
+
193
+ Args:
194
+ pane_id: Target pane (uses self.target_pane if not specified)
195
+ lines: Number of lines to capture from bottom (captures all if None)
196
+
197
+ Returns:
198
+ Captured text content
199
+ """
200
+ target = pane_id or self.target_pane
201
+ if not target:
202
+ raise ValueError("No target pane specified")
203
+
204
+ cmd = ['capture-pane', '-t', target, '-p']
205
+
206
+ if lines:
207
+ cmd.extend(['-S', f'-{lines}'])
208
+
209
+ output, _ = self._run_tmux_command(cmd)
210
+ return output
211
+
212
+ def wait_for_prompt(self, prompt_pattern: str, pane_id: Optional[str] = None,
213
+ timeout: int = 10, check_interval: float = 0.5) -> bool:
214
+ """
215
+ Wait for a specific prompt pattern to appear in the pane.
216
+
217
+ Args:
218
+ prompt_pattern: Regex pattern to match
219
+ pane_id: Target pane
220
+ timeout: Maximum seconds to wait
221
+ check_interval: Seconds between checks
222
+
223
+ Returns:
224
+ True if prompt found, False if timeout
225
+ """
226
+ target = pane_id or self.target_pane
227
+ if not target:
228
+ raise ValueError("No target pane specified")
229
+
230
+ pattern = re.compile(prompt_pattern)
231
+ start_time = time.time()
232
+
233
+ while time.time() - start_time < timeout:
234
+ content = self.capture_pane(target, lines=50)
235
+ if pattern.search(content):
236
+ return True
237
+ time.sleep(check_interval)
238
+
239
+ return False
240
+
241
+ def wait_for_idle(self, pane_id: Optional[str] = None, idle_time: float = 2.0,
242
+ check_interval: float = 0.5, timeout: Optional[int] = None) -> bool:
243
+ """
244
+ Wait for a pane to become idle (no output changes for idle_time seconds).
245
+
246
+ Args:
247
+ pane_id: Target pane
248
+ idle_time: Seconds of no change to consider idle
249
+ check_interval: Seconds between checks
250
+ timeout: Maximum seconds to wait (None for no timeout)
251
+
252
+ Returns:
253
+ True if idle detected, False if timeout
254
+ """
255
+ target = pane_id or self.target_pane
256
+ if not target:
257
+ raise ValueError("No target pane specified")
258
+
259
+ start_time = time.time()
260
+ last_change_time = time.time()
261
+ last_hash = ""
262
+
263
+ while True:
264
+ if timeout and (time.time() - start_time > timeout):
265
+ return False
266
+
267
+ content = self.capture_pane(target)
268
+ content_hash = hashlib.md5(content.encode()).hexdigest()
269
+
270
+ if content_hash != last_hash:
271
+ last_hash = content_hash
272
+ last_change_time = time.time()
273
+ elif time.time() - last_change_time >= idle_time:
274
+ return True
275
+
276
+ time.sleep(check_interval)
277
+
278
+ def kill_pane(self, pane_id: Optional[str] = None):
279
+ """
280
+ Kill a pane.
281
+
282
+ Args:
283
+ pane_id: Target pane (uses self.target_pane if not specified)
284
+ """
285
+ target = pane_id or self.target_pane
286
+ if not target:
287
+ raise ValueError("No target pane specified")
288
+
289
+ # Safety check: prevent killing own pane ONLY when explicitly specified
290
+ # If using target_pane (a pane we created), it should be safe to kill
291
+ if pane_id is not None: # Only check when pane_id was explicitly provided
292
+ current_pane = self.get_current_pane()
293
+ if current_pane and target == current_pane:
294
+ raise ValueError("Error: Cannot kill own pane! This would terminate your session.")
295
+
296
+ self._run_tmux_command(['kill-pane', '-t', target])
297
+
298
+ if target == self.target_pane:
299
+ self.target_pane = None
300
+
301
+ def resize_pane(self, direction: str, amount: int = 5, pane_id: Optional[str] = None):
302
+ """
303
+ Resize a pane.
304
+
305
+ Args:
306
+ direction: One of 'up', 'down', 'left', 'right'
307
+ amount: Number of cells to resize
308
+ pane_id: Target pane
309
+ """
310
+ target = pane_id or self.target_pane
311
+ if not target:
312
+ raise ValueError("No target pane specified")
313
+
314
+ direction_map = {
315
+ 'up': '-U',
316
+ 'down': '-D',
317
+ 'left': '-L',
318
+ 'right': '-R'
319
+ }
320
+
321
+ if direction not in direction_map:
322
+ raise ValueError(f"Invalid direction: {direction}")
323
+
324
+ self._run_tmux_command(['resize-pane', '-t', target, direction_map[direction], str(amount)])
325
+
326
+ def focus_pane(self, pane_id: Optional[str] = None):
327
+ """
328
+ Focus (select) a pane.
329
+
330
+ Args:
331
+ pane_id: Target pane
332
+ """
333
+ target = pane_id or self.target_pane
334
+ if not target:
335
+ raise ValueError("No target pane specified")
336
+
337
+ self._run_tmux_command(['select-pane', '-t', target])
338
+
339
+ def send_interrupt(self, pane_id: Optional[str] = None):
340
+ """
341
+ Send Ctrl+C to a pane.
342
+
343
+ Args:
344
+ pane_id: Target pane
345
+ """
346
+ target = pane_id or self.target_pane
347
+ if not target:
348
+ raise ValueError("No target pane specified")
349
+
350
+ self._run_tmux_command(['send-keys', '-t', target, 'C-c'])
351
+
352
+ def send_escape(self, pane_id: Optional[str] = None):
353
+ """
354
+ Send Escape key to a pane.
355
+
356
+ Args:
357
+ pane_id: Target pane
358
+ """
359
+ target = pane_id or self.target_pane
360
+ if not target:
361
+ raise ValueError("No target pane specified")
362
+
363
+ self._run_tmux_command(['send-keys', '-t', target, 'Escape'])
364
+
365
+ def clear_pane(self, pane_id: Optional[str] = None):
366
+ """
367
+ Clear the pane screen.
368
+
369
+ Args:
370
+ pane_id: Target pane
371
+ """
372
+ target = pane_id or self.target_pane
373
+ if not target:
374
+ raise ValueError("No target pane specified")
375
+
376
+ self._run_tmux_command(['send-keys', '-t', target, 'C-l'])
377
+
378
+ def launch_cli(self, command: str, vertical: bool = True, size: int = 50) -> Optional[str]:
379
+ """
380
+ Convenience method to launch a CLI application in a new pane.
381
+
382
+ Args:
383
+ command: Command to launch
384
+ vertical: Split direction
385
+ size: Pane size percentage
386
+
387
+ Returns:
388
+ Pane ID of the created pane
389
+ """
390
+ return self.create_pane(vertical=vertical, size=size, start_command=command)
391
+
392
+
393
+ class CLI:
394
+ """Unified CLI interface that auto-detects tmux environment.
395
+
396
+ Automatically uses:
397
+ - TmuxCLIController when inside tmux (for pane management)
398
+ - RemoteTmuxController when outside tmux (for window management)
399
+ """
400
+
401
+ def __init__(self, session: Optional[str] = None):
402
+ """Initialize with auto-detection of tmux environment.
403
+
404
+ Args:
405
+ session: Optional session name for remote mode (ignored in local mode)
406
+ """
407
+ self.in_tmux = bool(os.environ.get('TMUX'))
408
+
409
+ if self.in_tmux:
410
+ # Inside tmux - use local controller
411
+ self.controller = TmuxCLIController()
412
+ self.mode = 'local'
413
+ else:
414
+ # Outside tmux - use remote controller
415
+ from .tmux_remote_controller import RemoteTmuxController
416
+ session_name = session or "remote-cli-session"
417
+ self.controller = RemoteTmuxController(session_name=session_name)
418
+ self.mode = 'remote'
419
+
420
+ def list_panes(self):
421
+ """List all panes in current window."""
422
+ panes = self.controller.list_panes()
423
+ print(json.dumps(panes, indent=2))
424
+
425
+ def launch(self, command: str, vertical: bool = True, size: int = 50, name: Optional[str] = None):
426
+ """Launch a command in a new pane/window.
427
+
428
+ Args:
429
+ command: Command to launch
430
+ vertical: Split direction (only used in local mode)
431
+ size: Pane size percentage (only used in local mode)
432
+ name: Window name (only used in remote mode)
433
+ """
434
+ if self.mode == 'local':
435
+ pane_id = self.controller.launch_cli(command, vertical=vertical, size=size)
436
+ print(f"Launched in pane: {pane_id}")
437
+ else:
438
+ # Remote mode
439
+ pane_id = self.controller.launch_cli(command, name=name)
440
+ print(f"Launched in window: {pane_id}")
441
+ return pane_id
442
+
443
+ def send(self, text: str, pane: Optional[str] = None, enter: bool = True,
444
+ delay_enter: Union[bool, float] = True):
445
+ """Send text to a pane.
446
+
447
+ Args:
448
+ text: Text to send
449
+ pane: Target pane ID or index
450
+ enter: Whether to send Enter key after text
451
+ delay_enter: If True, use 1.0s delay; if float, use that delay in seconds (default: True)
452
+ """
453
+ if self.mode == 'local':
454
+ # Local mode - use select_pane
455
+ if pane:
456
+ if pane.isdigit():
457
+ self.controller.select_pane(pane_index=int(pane))
458
+ else:
459
+ self.controller.select_pane(pane_id=pane)
460
+ self.controller.send_keys(text, enter=enter, delay_enter=delay_enter)
461
+ else:
462
+ # Remote mode - pass pane_id directly
463
+ self.controller.send_keys(text, pane_id=pane, enter=enter,
464
+ delay_enter=delay_enter)
465
+ print("Text sent")
466
+
467
+ def capture(self, pane: Optional[str] = None, lines: Optional[int] = None):
468
+ """Capture and print pane content."""
469
+ if self.mode == 'local':
470
+ # Local mode - use select_pane
471
+ if pane:
472
+ if pane.isdigit():
473
+ self.controller.select_pane(pane_index=int(pane))
474
+ else:
475
+ self.controller.select_pane(pane_id=pane)
476
+ content = self.controller.capture_pane(lines=lines)
477
+ else:
478
+ # Remote mode - pass pane_id directly
479
+ content = self.controller.capture_pane(pane_id=pane, lines=lines)
480
+ print(content)
481
+ return content
482
+
483
+ def interrupt(self, pane: Optional[str] = None):
484
+ """Send Ctrl+C to a pane."""
485
+ if self.mode == 'local':
486
+ # Local mode - use select_pane
487
+ if pane:
488
+ if pane.isdigit():
489
+ self.controller.select_pane(pane_index=int(pane))
490
+ else:
491
+ self.controller.select_pane(pane_id=pane)
492
+ self.controller.send_interrupt()
493
+ else:
494
+ # Remote mode - resolve and pass pane_id
495
+ target = self.controller._resolve_pane_id(pane)
496
+ self.controller.send_interrupt(pane_id=target)
497
+ print("Sent interrupt signal")
498
+
499
+ def escape(self, pane: Optional[str] = None):
500
+ """Send Escape key to a pane."""
501
+ if self.mode == 'local':
502
+ # Local mode - use select_pane
503
+ if pane:
504
+ if pane.isdigit():
505
+ self.controller.select_pane(pane_index=int(pane))
506
+ else:
507
+ self.controller.select_pane(pane_id=pane)
508
+ self.controller.send_escape()
509
+ else:
510
+ # Remote mode - resolve and pass pane_id
511
+ target = self.controller._resolve_pane_id(pane)
512
+ self.controller.send_escape(pane_id=target)
513
+ print("Sent escape key")
514
+
515
+ def kill(self, pane: Optional[str] = None):
516
+ """Kill a pane/window."""
517
+ if self.mode == 'local':
518
+ # Local mode - kill pane
519
+ if pane:
520
+ if pane.isdigit():
521
+ self.controller.select_pane(pane_index=int(pane))
522
+ else:
523
+ self.controller.select_pane(pane_id=pane)
524
+ try:
525
+ self.controller.kill_pane()
526
+ print("Pane killed")
527
+ except ValueError as e:
528
+ print(str(e))
529
+ else:
530
+ # Remote mode - kill window
531
+ try:
532
+ self.controller.kill_window(window_id=pane)
533
+ print("Window killed")
534
+ except ValueError as e:
535
+ print(str(e))
536
+
537
+ def wait_idle(self, pane: Optional[str] = None, idle_time: float = 2.0,
538
+ timeout: Optional[int] = None):
539
+ """Wait for pane to become idle (no output changes)."""
540
+ if self.mode == 'local':
541
+ # Local mode - use select_pane
542
+ if pane:
543
+ if pane.isdigit():
544
+ self.controller.select_pane(pane_index=int(pane))
545
+ else:
546
+ self.controller.select_pane(pane_id=pane)
547
+ target = None
548
+ else:
549
+ # Remote mode - resolve pane_id
550
+ target = self.controller._resolve_pane_id(pane)
551
+
552
+ print(f"Waiting for pane to become idle (no changes for {idle_time}s)...")
553
+ if self.controller.wait_for_idle(pane_id=target, idle_time=idle_time, timeout=timeout):
554
+ print("Pane is idle")
555
+ return True
556
+ else:
557
+ print("Timeout waiting for idle")
558
+ return False
559
+
560
+ def attach(self):
561
+ """Attach to the managed session (remote mode only)."""
562
+ if self.mode == 'local':
563
+ print("Attach is only available in remote mode (when outside tmux)")
564
+ return
565
+ self.controller.attach_session()
566
+
567
+ def cleanup(self):
568
+ """Kill the entire managed session (remote mode only)."""
569
+ if self.mode == 'local':
570
+ print("Cleanup is only available in remote mode (when outside tmux)")
571
+ return
572
+ self.controller.cleanup_session()
573
+
574
+ def list_windows(self):
575
+ """List all windows in the session (remote mode only)."""
576
+ if self.mode == 'local':
577
+ print("List_windows is only available in remote mode. Use list_panes instead.")
578
+ return
579
+
580
+ windows = self.controller.list_windows()
581
+ if not windows:
582
+ print(f"No windows in session '{self.controller.session_name}'")
583
+ return
584
+
585
+ print(f"Windows in session '{self.controller.session_name}':")
586
+ for w in windows:
587
+ active = " (active)" if w['active'] else ""
588
+ print(f" {w['index']}: {w['name']}{active} - pane {w['pane_id']}")
589
+
590
+ def demo(self):
591
+ """Run a demo showing tmux CLI control capabilities."""
592
+ print("Running demo...")
593
+
594
+ if self.mode == 'local':
595
+ # Original local demo
596
+ print("\nCurrent panes:")
597
+ panes = self.controller.list_panes()
598
+ for pane in panes:
599
+ print(f" Pane {pane['index']}: {pane['id']} - {pane['title']}")
600
+
601
+ # Create a new pane with Python REPL
602
+ print("\nCreating new pane with Python...")
603
+ pane_id = self.controller.launch_cli('python3')
604
+ print(f"Created pane: {pane_id}")
605
+
606
+ # Wait for Python prompt
607
+ time.sleep(1)
608
+ if self.controller.wait_for_prompt('>>>', timeout=5):
609
+ print("Python prompt detected")
610
+
611
+ # Send a command
612
+ print("\nSending Python command...")
613
+ self.controller.send_keys('print("Hello from tmux!")')
614
+ time.sleep(0.5)
615
+
616
+ # Capture output
617
+ output = self.controller.capture_pane(lines=10)
618
+ print(f"\nCaptured output:\n{output}")
619
+
620
+ # Clean up
621
+ print("\nCleaning up...")
622
+ self.controller.send_keys('exit()')
623
+ time.sleep(0.5)
624
+ self.controller.kill_pane()
625
+ print("Demo complete!")
626
+ else:
627
+ print("Failed to detect Python prompt")
628
+ self.controller.kill_pane()
629
+ else:
630
+ # Remote demo
631
+ print("\nCreating new window with Python...")
632
+ pane_id = self.launch('python3', name='demo-python')
633
+
634
+ # Wait for idle (Python prompt)
635
+ time.sleep(1)
636
+ if self.wait_idle(pane=pane_id, idle_time=1.0, timeout=5):
637
+ print("Python is ready")
638
+
639
+ # Send a command
640
+ print("\nSending Python command...")
641
+ self.send('print("Hello from remote tmux!")', pane=pane_id)
642
+ time.sleep(0.5)
643
+
644
+ # Capture output
645
+ print("\nCaptured output:")
646
+ self.capture(pane=pane_id, lines=10)
647
+
648
+ # Clean up
649
+ print("\nCleaning up...")
650
+ self.send('exit()', pane=pane_id)
651
+ time.sleep(0.5)
652
+ self.kill(pane=pane_id)
653
+ print("Demo complete!")
654
+ else:
655
+ print("Failed to wait for Python")
656
+ self.kill(pane=pane_id)
657
+
658
+ def help(self):
659
+ """Display tmux-cli usage instructions."""
660
+ # Find the instructions file relative to this module
661
+ module_dir = Path(__file__).parent.parent
662
+ instructions_file = module_dir / "docs" / "tmux-cli-instructions.md"
663
+
664
+ # Add mode-specific header
665
+ mode_info = f"\n{'='*60}\n"
666
+ if self.mode == 'local':
667
+ mode_info += "MODE: LOCAL (inside tmux) - Managing panes in current window\n"
668
+ else:
669
+ mode_info += f"MODE: REMOTE (outside tmux) - Managing windows in session '{self.controller.session_name}'\n"
670
+ mode_info += f"{'='*60}\n"
671
+
672
+ print(mode_info)
673
+
674
+ if instructions_file.exists():
675
+ print(instructions_file.read_text())
676
+ else:
677
+ print("Error: tmux-cli-instructions.md not found")
678
+ print(f"Expected location: {instructions_file}")
679
+
680
+ if self.mode == 'remote':
681
+ print("\n" + "="*60)
682
+ print("REMOTE MODE SPECIFIC COMMANDS:")
683
+ print("- tmux-cli attach: Attach to the managed session to view live")
684
+ print("- tmux-cli cleanup: Kill the entire managed session")
685
+ print("- tmux-cli list_windows: List all windows in the session")
686
+ print("\nNote: In remote mode, 'panes' are actually windows for better isolation.")
687
+ print("="*60)
688
+
689
+
690
+ def main():
691
+ """Main entry point using fire."""
692
+ import fire
693
+ import sys
694
+
695
+ # Check for --help flag
696
+ if '--help' in sys.argv:
697
+ cli = CLI()
698
+ cli.help()
699
+ sys.exit(0)
700
+
701
+ fire.Fire(CLI)
702
+
703
+
704
+ if __name__ == '__main__':
705
+ main()