telemux 1.0.1__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 telemux might be problematic. Click here for more details.
- telemux/__init__.py +24 -0
- telemux/cleanup.py +189 -0
- telemux/cli.py +76 -0
- telemux/config.py +78 -0
- telemux/control.py +305 -0
- telemux/installer.py +419 -0
- telemux/listener.py +345 -0
- telemux/shell_functions.sh +110 -0
- telemux-1.0.1.dist-info/METADATA +479 -0
- telemux-1.0.1.dist-info/RECORD +14 -0
- telemux-1.0.1.dist-info/WHEEL +5 -0
- telemux-1.0.1.dist-info/entry_points.txt +15 -0
- telemux-1.0.1.dist-info/licenses/LICENSE +21 -0
- telemux-1.0.1.dist-info/top_level.txt +1 -0
telemux/listener.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Telegram Listener Daemon for TeleMux
|
|
4
|
+
Monitors Telegram bot for incoming messages and routes them to LLM agents
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
import logging
|
|
13
|
+
import requests
|
|
14
|
+
import subprocess
|
|
15
|
+
import shlex
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import Dict, List, Optional, Tuple, Any
|
|
19
|
+
|
|
20
|
+
from . import TELEMUX_DIR, MESSAGE_QUEUE_DIR, LOG_FILE, TMUX_SESSION
|
|
21
|
+
from .config import load_config
|
|
22
|
+
|
|
23
|
+
# Message queue files
|
|
24
|
+
OUTGOING_LOG = MESSAGE_QUEUE_DIR / "outgoing.log"
|
|
25
|
+
INCOMING_LOG = MESSAGE_QUEUE_DIR / "incoming.log"
|
|
26
|
+
LISTENER_STATE = MESSAGE_QUEUE_DIR / "listener_state.json"
|
|
27
|
+
|
|
28
|
+
# Logging setup
|
|
29
|
+
ERROR_LOG_FILE = TELEMUX_DIR / "telegram_errors.log"
|
|
30
|
+
|
|
31
|
+
# Get log level from environment variable (default: INFO)
|
|
32
|
+
LOG_LEVEL = os.environ.get('TELEMUX_LOG_LEVEL', 'INFO').upper()
|
|
33
|
+
LOG_LEVEL_MAP = {
|
|
34
|
+
'DEBUG': logging.DEBUG,
|
|
35
|
+
'INFO': logging.INFO,
|
|
36
|
+
'WARNING': logging.WARNING,
|
|
37
|
+
'ERROR': logging.ERROR,
|
|
38
|
+
'CRITICAL': logging.CRITICAL
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Configure logging with multiple handlers
|
|
42
|
+
logger = logging.getLogger('TelegramListener')
|
|
43
|
+
logger.setLevel(LOG_LEVEL_MAP.get(LOG_LEVEL, logging.INFO))
|
|
44
|
+
|
|
45
|
+
# Main log file handler (all levels)
|
|
46
|
+
main_handler = logging.FileHandler(LOG_FILE)
|
|
47
|
+
main_handler.setLevel(logging.DEBUG)
|
|
48
|
+
main_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
49
|
+
main_handler.setFormatter(main_formatter)
|
|
50
|
+
|
|
51
|
+
# Error log file handler (errors only)
|
|
52
|
+
error_handler = logging.FileHandler(ERROR_LOG_FILE)
|
|
53
|
+
error_handler.setLevel(logging.ERROR)
|
|
54
|
+
error_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
|
|
55
|
+
error_handler.setFormatter(error_formatter)
|
|
56
|
+
|
|
57
|
+
# Console handler (configurable level)
|
|
58
|
+
console_handler = logging.StreamHandler()
|
|
59
|
+
console_handler.setLevel(LOG_LEVEL_MAP.get(LOG_LEVEL, logging.INFO))
|
|
60
|
+
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
61
|
+
console_handler.setFormatter(console_formatter)
|
|
62
|
+
|
|
63
|
+
logger.addHandler(main_handler)
|
|
64
|
+
logger.addHandler(error_handler)
|
|
65
|
+
logger.addHandler(console_handler)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_state() -> Dict:
|
|
69
|
+
"""Load listener state (last update ID)"""
|
|
70
|
+
if LISTENER_STATE.exists():
|
|
71
|
+
with open(LISTENER_STATE) as f:
|
|
72
|
+
return json.load(f)
|
|
73
|
+
return {"last_update_id": 0}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def save_state(state: Dict):
|
|
77
|
+
"""Save listener state"""
|
|
78
|
+
MESSAGE_QUEUE_DIR.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
with open(LISTENER_STATE, 'w') as f:
|
|
80
|
+
json.dump(state, f, indent=2)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_telegram_updates(bot_token: str, offset: int = 0, max_retries: int = 3) -> List[Dict]:
|
|
84
|
+
"""Poll Telegram for new messages with retry logic"""
|
|
85
|
+
url = f"https://api.telegram.org/bot{bot_token}/getUpdates"
|
|
86
|
+
params = {
|
|
87
|
+
"offset": offset,
|
|
88
|
+
"timeout": 30 # Long polling
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for attempt in range(max_retries):
|
|
92
|
+
try:
|
|
93
|
+
response = requests.get(url, params=params, timeout=35)
|
|
94
|
+
response.raise_for_status()
|
|
95
|
+
data = response.json()
|
|
96
|
+
|
|
97
|
+
if data.get("ok"):
|
|
98
|
+
return data.get("result", [])
|
|
99
|
+
else:
|
|
100
|
+
logger.warning(f"Telegram API returned not ok: {data}")
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
except requests.exceptions.Timeout:
|
|
104
|
+
# Timeout is expected with long polling, only log if it's a problem
|
|
105
|
+
if attempt < max_retries - 1:
|
|
106
|
+
logger.debug(f"Telegram long-poll timeout (attempt {attempt + 1}/{max_retries})")
|
|
107
|
+
time.sleep(2 ** attempt)
|
|
108
|
+
else:
|
|
109
|
+
logger.warning(f"Failed to get updates after {max_retries} timeout attempts")
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
except requests.exceptions.ConnectionError as e:
|
|
113
|
+
logger.warning(f"Connection error (attempt {attempt + 1}/{max_retries}): {e}")
|
|
114
|
+
if attempt < max_retries - 1:
|
|
115
|
+
time.sleep(2 ** attempt) # Exponential backoff
|
|
116
|
+
else:
|
|
117
|
+
logger.error(f"Failed to connect after {max_retries} attempts. Is the network down?")
|
|
118
|
+
return []
|
|
119
|
+
|
|
120
|
+
except requests.exceptions.RequestException as e:
|
|
121
|
+
logger.warning(f"Request error (attempt {attempt + 1}/{max_retries}): {e}")
|
|
122
|
+
if attempt < max_retries - 1:
|
|
123
|
+
time.sleep(2 ** attempt)
|
|
124
|
+
else:
|
|
125
|
+
logger.error(f"Failed to get updates after {max_retries} attempts: {e}")
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Unexpected error getting Telegram updates: {e}")
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def send_telegram_message(bot_token: str, chat_id: str, text: str, max_retries: int = 3):
|
|
136
|
+
"""Send a message to Telegram with retry logic"""
|
|
137
|
+
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
|
138
|
+
payload = {
|
|
139
|
+
"chat_id": chat_id,
|
|
140
|
+
"text": text,
|
|
141
|
+
"parse_mode": "HTML"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for attempt in range(max_retries):
|
|
145
|
+
try:
|
|
146
|
+
response = requests.post(url, json=payload, timeout=10)
|
|
147
|
+
response.raise_for_status()
|
|
148
|
+
logger.info(f"Sent message to Telegram: {text[:50]}...")
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
except requests.exceptions.Timeout:
|
|
152
|
+
logger.warning(f"Telegram API timeout (attempt {attempt + 1}/{max_retries})")
|
|
153
|
+
if attempt < max_retries - 1:
|
|
154
|
+
time.sleep(2 ** attempt) # Exponential backoff: 1s, 2s, 4s
|
|
155
|
+
else:
|
|
156
|
+
logger.error(f"Failed to send message after {max_retries} attempts (timeout)")
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
except requests.exceptions.RequestException as e:
|
|
160
|
+
logger.warning(f"Telegram API error (attempt {attempt + 1}/{max_retries}): {e}")
|
|
161
|
+
if attempt < max_retries - 1:
|
|
162
|
+
time.sleep(2 ** attempt) # Exponential backoff
|
|
163
|
+
else:
|
|
164
|
+
logger.error(f"Failed to send message after {max_retries} attempts: {e}")
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Unexpected error sending Telegram message: {e}")
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def parse_message_id(text: str) -> Optional[Tuple[str, str]]:
|
|
175
|
+
"""
|
|
176
|
+
Parse message ID and response from text
|
|
177
|
+
Expected formats:
|
|
178
|
+
- session-name: Your response here (new format)
|
|
179
|
+
- msg-1234567890-12345: Your response here (old format)
|
|
180
|
+
Returns: (message_id, response) or None
|
|
181
|
+
"""
|
|
182
|
+
# Match either session name (letters, numbers, dashes, underscores) or old msg format
|
|
183
|
+
pattern = r'^([\w-]+):\s*(.+)$'
|
|
184
|
+
match = re.match(pattern, text, re.DOTALL)
|
|
185
|
+
if match:
|
|
186
|
+
return match.group(1), match.group(2)
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def process_update(update: Dict[str, Any], bot_token: str, chat_id: str) -> None:
|
|
191
|
+
"""Process a single Telegram update - SESSION-BASED ROUTING
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
update: Telegram update dict containing message data
|
|
195
|
+
bot_token: Telegram bot token
|
|
196
|
+
chat_id: Telegram chat ID
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
None
|
|
200
|
+
"""
|
|
201
|
+
if "message" not in update:
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
message = update["message"]
|
|
205
|
+
text = message.get("text", "")
|
|
206
|
+
from_user = message.get("from", {}).get("first_name", "Unknown")
|
|
207
|
+
|
|
208
|
+
logger.info(f"Received message from {from_user}: {text[:50]}...")
|
|
209
|
+
|
|
210
|
+
# Parse message for session: message format
|
|
211
|
+
parsed = parse_message_id(text)
|
|
212
|
+
if not parsed:
|
|
213
|
+
logger.info("Message doesn't match format (session-name: message), ignoring")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
session_name, response = parsed
|
|
217
|
+
logger.info(f"Parsed message - Target session: {session_name}")
|
|
218
|
+
|
|
219
|
+
# Check if tmux session exists
|
|
220
|
+
try:
|
|
221
|
+
result = subprocess.run(
|
|
222
|
+
['tmux', 'list-sessions', '-F', '#{session_name}'],
|
|
223
|
+
capture_output=True,
|
|
224
|
+
text=True,
|
|
225
|
+
check=False
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if result.returncode != 0:
|
|
229
|
+
# No tmux sessions at all
|
|
230
|
+
logger.warning("No tmux sessions found")
|
|
231
|
+
send_telegram_message(bot_token, chat_id, f"No tmux sessions are running")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
active_sessions = [s for s in result.stdout.strip().split('\n') if s]
|
|
235
|
+
|
|
236
|
+
if session_name not in active_sessions:
|
|
237
|
+
logger.warning(f"Tmux session not found: {session_name}")
|
|
238
|
+
# Security: Show count only, not all session names
|
|
239
|
+
send_telegram_message(bot_token, chat_id, f"Session <b>{session_name}</b> not found. {len(active_sessions)} active session(s).")
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
# SECURITY: Sanitize user input to prevent command injection
|
|
243
|
+
# tmux send-keys interprets special characters like $(), ``, &&, ;
|
|
244
|
+
# Without sanitization, malicious input could execute arbitrary commands
|
|
245
|
+
safe_response = shlex.quote(response)
|
|
246
|
+
formatted_message = f"{safe_response}\n # Respond using this terminal command: tg_agent \"{session_name}\" \"your response\""
|
|
247
|
+
|
|
248
|
+
# Send message to tmux session
|
|
249
|
+
result = subprocess.run(
|
|
250
|
+
['tmux', 'send-keys', '-t', session_name, formatted_message],
|
|
251
|
+
capture_output=True,
|
|
252
|
+
text=True,
|
|
253
|
+
check=False
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if result.returncode != 0:
|
|
257
|
+
logger.error(f"Failed to send message to tmux: {result.stderr}")
|
|
258
|
+
send_telegram_message(bot_token, chat_id, f"Failed to deliver message to session")
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
# CRITICAL: Sleep required for tmux to buffer text before Enter is sent
|
|
262
|
+
# Without this delay, tmux doesn't have time to process send-keys and
|
|
263
|
+
# the message gets lost. See: https://github.com/tmux/tmux/issues/1254
|
|
264
|
+
time.sleep(1)
|
|
265
|
+
|
|
266
|
+
# Send Enter to execute the command
|
|
267
|
+
result = subprocess.run(
|
|
268
|
+
['tmux', 'send-keys', '-t', session_name, 'C-m'],
|
|
269
|
+
capture_output=True,
|
|
270
|
+
text=True,
|
|
271
|
+
check=False
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if result.returncode != 0:
|
|
275
|
+
logger.error(f"Failed to send Enter to tmux: {result.stderr}")
|
|
276
|
+
send_telegram_message(bot_token, chat_id, f"Message sent but not executed")
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
logger.info(f"Message delivered to tmux session: {session_name}")
|
|
280
|
+
logger.info(f"Content: {response}")
|
|
281
|
+
|
|
282
|
+
# Send confirmation
|
|
283
|
+
send_telegram_message(bot_token, chat_id, f"Message delivered to <b>{session_name}</b>")
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.error(f"Failed to send message: {e}")
|
|
287
|
+
send_telegram_message(bot_token, chat_id, f"Error: {str(e)}")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def main():
|
|
291
|
+
"""Main listener loop"""
|
|
292
|
+
logger.info("=" * 60)
|
|
293
|
+
logger.info("Telegram Listener Daemon Starting")
|
|
294
|
+
logger.info("=" * 60)
|
|
295
|
+
|
|
296
|
+
# Load config
|
|
297
|
+
bot_token, chat_id = load_config()
|
|
298
|
+
if not bot_token or not chat_id:
|
|
299
|
+
logger.error("Failed to load Telegram config. Please run: telemux-install")
|
|
300
|
+
sys.exit(1)
|
|
301
|
+
|
|
302
|
+
logger.info(f"Loaded Telegram config - Chat ID: {chat_id}")
|
|
303
|
+
|
|
304
|
+
# Create directories
|
|
305
|
+
MESSAGE_QUEUE_DIR.mkdir(parents=True, exist_ok=True)
|
|
306
|
+
|
|
307
|
+
# Load state
|
|
308
|
+
state = load_state()
|
|
309
|
+
offset = state["last_update_id"]
|
|
310
|
+
|
|
311
|
+
logger.info(f"Starting from update offset: {offset}")
|
|
312
|
+
logger.info("Listening for messages...")
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
while True:
|
|
316
|
+
updates = get_telegram_updates(bot_token, offset)
|
|
317
|
+
|
|
318
|
+
for update in updates:
|
|
319
|
+
update_id = update["update_id"]
|
|
320
|
+
|
|
321
|
+
# Process update
|
|
322
|
+
try:
|
|
323
|
+
process_update(update, bot_token, chat_id)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logger.error(f"Error processing update {update_id}: {e}")
|
|
326
|
+
|
|
327
|
+
# Update offset
|
|
328
|
+
offset = update_id + 1
|
|
329
|
+
state["last_update_id"] = offset
|
|
330
|
+
save_state(state)
|
|
331
|
+
|
|
332
|
+
# Small sleep if no updates
|
|
333
|
+
if not updates:
|
|
334
|
+
time.sleep(1)
|
|
335
|
+
|
|
336
|
+
except KeyboardInterrupt:
|
|
337
|
+
logger.info("\nListener stopped by user")
|
|
338
|
+
sys.exit(0)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.error(f"Fatal error: {e}")
|
|
341
|
+
sys.exit(1)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
if __name__ == "__main__":
|
|
345
|
+
main()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# TeleMux Shell Functions - Single Source of Truth
|
|
4
|
+
#
|
|
5
|
+
# This file contains all shell functions for TeleMux.
|
|
6
|
+
# Source this file to get: tg_alert, tg_agent, tg_done
|
|
7
|
+
#
|
|
8
|
+
# Deployed to: ~/.telemux/shell_functions.sh
|
|
9
|
+
# Sourced by: ~/.zshrc (via INSTALL.sh)
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
# Load TeleMux configuration
|
|
13
|
+
if [ -f "$HOME/.telemux/telegram_config" ]; then
|
|
14
|
+
source "$HOME/.telemux/telegram_config"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Simple alert function - NOW BIDIRECTIONAL (can receive replies)
|
|
18
|
+
tg_alert() {
|
|
19
|
+
local message="$*"
|
|
20
|
+
if [[ -z "$message" ]]; then
|
|
21
|
+
echo "Usage: tg_alert <message>"
|
|
22
|
+
return 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
if [[ -z "$TELEMUX_TG_BOT_TOKEN" ]] || [[ -z "$TELEMUX_TG_CHAT_ID" ]]; then
|
|
26
|
+
echo "Error: TeleMux not configured. Check ~/.telemux/telegram_config"
|
|
27
|
+
return 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Get tmux session name for context AND routing
|
|
31
|
+
local tmux_session="$(tmux display-message -p '#S' 2>/dev/null || echo 'terminal')"
|
|
32
|
+
|
|
33
|
+
# NEW: Send with reply instructions (makes tg_alert bidirectional)
|
|
34
|
+
curl -s -X POST "https://api.telegram.org/bot${TELEMUX_TG_BOT_TOKEN}/sendMessage" \
|
|
35
|
+
-d chat_id="${TELEMUX_TG_CHAT_ID}" \
|
|
36
|
+
-d text="[!] <b>[${tmux_session}]</b> ${message}
|
|
37
|
+
|
|
38
|
+
<i>Reply: ${tmux_session}: your response</i>" \
|
|
39
|
+
-d parse_mode="HTML" > /dev/null && echo "Message sent to Telegram"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Bidirectional agent alert - sends message and can receive replies
|
|
43
|
+
tg_agent() {
|
|
44
|
+
local agent_name="$1"
|
|
45
|
+
local message="$2"
|
|
46
|
+
|
|
47
|
+
if [[ -z "$agent_name" ]] || [[ -z "$message" ]]; then
|
|
48
|
+
echo "Usage: tg_agent <agent_name> <message>"
|
|
49
|
+
return 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Use tmux session name as message ID (much simpler!)
|
|
53
|
+
local tmux_session="$(tmux display-message -p '#S' 2>/dev/null || echo 'unknown')"
|
|
54
|
+
local msg_id="${tmux_session}"
|
|
55
|
+
|
|
56
|
+
# Record mapping for listener daemon (backward compatibility)
|
|
57
|
+
# NOTE: New routing doesn't require this, but kept for transition period
|
|
58
|
+
mkdir -p "$HOME/.telemux/message_queue"
|
|
59
|
+
echo "${msg_id}:${agent_name}:${tmux_session}:$(date -Iseconds)" >> "$HOME/.telemux/message_queue/outgoing.log"
|
|
60
|
+
|
|
61
|
+
# Send to Telegram with identifier
|
|
62
|
+
curl -s -X POST "https://api.telegram.org/bot${TELEMUX_TG_BOT_TOKEN}/sendMessage" \
|
|
63
|
+
-d chat_id="${TELEMUX_TG_CHAT_ID}" \
|
|
64
|
+
-d text="[>] <b>[${agent_name}:${msg_id}]</b>
|
|
65
|
+
|
|
66
|
+
${message}
|
|
67
|
+
|
|
68
|
+
<i>Reply with: ${msg_id}: your response</i>" \
|
|
69
|
+
-d parse_mode="HTML" > /dev/null && echo "Agent alert sent: ${msg_id}"
|
|
70
|
+
|
|
71
|
+
echo "$msg_id" # Return message ID
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Alert when command completes
|
|
75
|
+
tg_done() {
|
|
76
|
+
local exit_code=$?
|
|
77
|
+
local cmd
|
|
78
|
+
|
|
79
|
+
# Bash-compatible history access
|
|
80
|
+
if [ -n "$BASH_VERSION" ]; then
|
|
81
|
+
cmd="$(fc -ln -1 2>/dev/null || echo 'unknown command')"
|
|
82
|
+
else
|
|
83
|
+
# zsh
|
|
84
|
+
cmd="${history[$((HISTCMD-1))]}"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Trim leading/trailing whitespace
|
|
88
|
+
cmd="$(echo "$cmd" | xargs)"
|
|
89
|
+
|
|
90
|
+
if [[ $exit_code -eq 0 ]]; then
|
|
91
|
+
tg_alert "Command completed: ${cmd}"
|
|
92
|
+
else
|
|
93
|
+
tg_alert "Command failed (exit $exit_code): ${cmd}"
|
|
94
|
+
fi
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Control aliases (defined here for consistency)
|
|
98
|
+
# Uses Python-based telemux commands
|
|
99
|
+
alias tg-start="telemux-start"
|
|
100
|
+
alias tg-stop="telemux-stop"
|
|
101
|
+
alias tg-restart="telemux-restart"
|
|
102
|
+
alias tg-status="telemux-status"
|
|
103
|
+
alias tg-logs="telemux-logs"
|
|
104
|
+
alias tg-attach="telemux-attach"
|
|
105
|
+
alias tg-cleanup="telemux-cleanup"
|
|
106
|
+
alias tg-doctor="telemux-doctor"
|
|
107
|
+
|
|
108
|
+
# Backward compatibility aliases (for users upgrading from old Team Mux)
|
|
109
|
+
alias alert_telegram="tg_alert"
|
|
110
|
+
alias agent_telegram="tg_agent"
|