telemux 1.0.5__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.
- telemux/__init__.py +24 -0
- telemux/cleanup.py +185 -0
- telemux/cli.py +82 -0
- telemux/config.py +77 -0
- telemux/control.py +302 -0
- telemux/installer.py +421 -0
- telemux/listener.py +345 -0
- telemux/shell_functions.sh +117 -0
- telemux-1.0.5.dist-info/METADATA +478 -0
- telemux-1.0.5.dist-info/RECORD +14 -0
- telemux-1.0.5.dist-info/WHEEL +5 -0
- telemux-1.0.5.dist-info/entry_points.txt +15 -0
- telemux-1.0.5.dist-info/licenses/LICENSE +21 -0
- telemux-1.0.5.dist-info/top_level.txt +1 -0
telemux/installer.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TeleMux Interactive Installer
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Dict, Optional
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from . import TELEMUX_DIR
|
|
13
|
+
from .config import ensure_directories, save_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_prerequisites() -> bool:
|
|
17
|
+
"""Check if required tools are installed."""
|
|
18
|
+
print("Checking prerequisites...")
|
|
19
|
+
|
|
20
|
+
required = {
|
|
21
|
+
'tmux': 'tmux -V',
|
|
22
|
+
'python3': 'python3 --version',
|
|
23
|
+
'curl': 'curl --version'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
all_present = True
|
|
27
|
+
for name, cmd in required.items():
|
|
28
|
+
try:
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
cmd.split(),
|
|
31
|
+
capture_output=True,
|
|
32
|
+
check=False
|
|
33
|
+
)
|
|
34
|
+
if result.returncode == 0:
|
|
35
|
+
continue
|
|
36
|
+
except FileNotFoundError:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
print(f"ERROR: {name} is required but not installed. Aborting.")
|
|
40
|
+
all_present = False
|
|
41
|
+
|
|
42
|
+
if all_present:
|
|
43
|
+
# Check Python version
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
['python3', '--version'],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
check=False
|
|
49
|
+
)
|
|
50
|
+
print(f"Python version: {result.stdout.strip().split()[1]}")
|
|
51
|
+
print("All prerequisites met")
|
|
52
|
+
|
|
53
|
+
print("")
|
|
54
|
+
return all_present
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_bot_info(bot_token: str) -> Optional[Dict]:
|
|
58
|
+
"""Get bot information from Telegram."""
|
|
59
|
+
try:
|
|
60
|
+
response = requests.get(
|
|
61
|
+
f"https://api.telegram.org/bot{bot_token}/getMe",
|
|
62
|
+
timeout=10
|
|
63
|
+
)
|
|
64
|
+
data = response.json()
|
|
65
|
+
if data.get("ok"):
|
|
66
|
+
return data["result"]
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_available_chats(bot_token: str) -> List[Dict]:
|
|
73
|
+
"""Fetch available chats from Telegram."""
|
|
74
|
+
try:
|
|
75
|
+
response = requests.get(
|
|
76
|
+
f"https://api.telegram.org/bot{bot_token}/getUpdates",
|
|
77
|
+
timeout=10
|
|
78
|
+
)
|
|
79
|
+
data = response.json()
|
|
80
|
+
|
|
81
|
+
if not data.get("ok"):
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
# Extract unique chats
|
|
85
|
+
chats = {}
|
|
86
|
+
for update in data.get("result", []):
|
|
87
|
+
if "message" in update:
|
|
88
|
+
chat = update["message"]["chat"]
|
|
89
|
+
chat_id = str(chat["id"])
|
|
90
|
+
|
|
91
|
+
if chat_id not in chats:
|
|
92
|
+
chat_info = {
|
|
93
|
+
"id": chat_id,
|
|
94
|
+
"type": chat["type"],
|
|
95
|
+
"name": (chat.get("title") or
|
|
96
|
+
f"{chat.get('first_name', '')} {chat.get('last_name', '')}".strip() or
|
|
97
|
+
"Unknown")
|
|
98
|
+
}
|
|
99
|
+
chats[chat_id] = chat_info
|
|
100
|
+
|
|
101
|
+
return list(chats.values())
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"Error fetching chats: {e}")
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def display_chats(chats: List[Dict]):
|
|
109
|
+
"""Display available chats to the user."""
|
|
110
|
+
print("Available chats (send a message to your bot first if empty):")
|
|
111
|
+
print("-" * 60)
|
|
112
|
+
|
|
113
|
+
for chat in chats:
|
|
114
|
+
print(f" Chat ID: {chat['id']}")
|
|
115
|
+
print(f" Type: {chat['type']}")
|
|
116
|
+
print(f" Name: {chat['name']}")
|
|
117
|
+
print("-" * 60)
|
|
118
|
+
|
|
119
|
+
print("")
|
|
120
|
+
print("Note: Group chat IDs are negative, personal chat IDs are positive")
|
|
121
|
+
print("")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_chat_id_interactive(bot_token: str) -> Optional[str]:
|
|
125
|
+
"""Interactively get chat ID from user with retry logic."""
|
|
126
|
+
chats = get_available_chats(bot_token)
|
|
127
|
+
|
|
128
|
+
if chats:
|
|
129
|
+
display_chats(chats)
|
|
130
|
+
|
|
131
|
+
# If exactly one chat, offer to use it
|
|
132
|
+
if len(chats) == 1:
|
|
133
|
+
chat = chats[0]
|
|
134
|
+
print(f"Found only one chat: {chat['name']} (ID: {chat['id']})")
|
|
135
|
+
response = input("Use this chat? (y/n): ").strip().lower()
|
|
136
|
+
if response == 'y':
|
|
137
|
+
return chat['id']
|
|
138
|
+
else:
|
|
139
|
+
return input("Enter your Chat ID manually: ").strip()
|
|
140
|
+
else:
|
|
141
|
+
return input("Enter your Chat ID (from above): ").strip()
|
|
142
|
+
else:
|
|
143
|
+
# No chats found - offer retry
|
|
144
|
+
print(" No chats found. You need to:")
|
|
145
|
+
print(" 1. Start a conversation with your bot (send any message)")
|
|
146
|
+
print(" 2. Or add the bot to a group and send a message")
|
|
147
|
+
print("")
|
|
148
|
+
|
|
149
|
+
while True:
|
|
150
|
+
choice = input("Try again after sending a message? (y/n/manual): ").strip().lower()
|
|
151
|
+
|
|
152
|
+
if choice == 'y':
|
|
153
|
+
print("")
|
|
154
|
+
print("Checking for new chats...")
|
|
155
|
+
chats = get_available_chats(bot_token)
|
|
156
|
+
|
|
157
|
+
if chats:
|
|
158
|
+
print("Found chats! Displaying available options...")
|
|
159
|
+
display_chats(chats)
|
|
160
|
+
|
|
161
|
+
if len(chats) == 1:
|
|
162
|
+
chat = chats[0]
|
|
163
|
+
print(f"Found only one chat: {chat['name']} (ID: {chat['id']})")
|
|
164
|
+
response = input("Use this chat? (y/n): ").strip().lower()
|
|
165
|
+
if response == 'y':
|
|
166
|
+
return chat['id']
|
|
167
|
+
else:
|
|
168
|
+
return input("Enter your Chat ID manually: ").strip()
|
|
169
|
+
else:
|
|
170
|
+
return input("Enter your Chat ID (from above): ").strip()
|
|
171
|
+
else:
|
|
172
|
+
print(" Still no chats found. Make sure you sent a message to your bot.")
|
|
173
|
+
print("")
|
|
174
|
+
|
|
175
|
+
elif choice == 'n':
|
|
176
|
+
print("Exiting installation. Run the installer again when ready.")
|
|
177
|
+
sys.exit(0)
|
|
178
|
+
|
|
179
|
+
elif choice == 'manual' or choice == 'm':
|
|
180
|
+
print("")
|
|
181
|
+
return input("Enter your Chat ID manually: ").strip()
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
print("Please enter 'y' to try again, 'n' to exit, or 'manual' to enter chat ID manually.")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_telegram_connection(bot_token: str, chat_id: str) -> bool:
|
|
188
|
+
"""Send a test message to verify configuration."""
|
|
189
|
+
print("Verifying chat ID...")
|
|
190
|
+
date_output = subprocess.run(['date'], capture_output=True, text=True).stdout.strip()
|
|
191
|
+
test_message = f"TeleMux installation test - {date_output}"
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
response = requests.post(
|
|
195
|
+
f"https://api.telegram.org/bot{bot_token}/sendMessage",
|
|
196
|
+
json={
|
|
197
|
+
"chat_id": chat_id,
|
|
198
|
+
"text": test_message,
|
|
199
|
+
"parse_mode": "HTML"
|
|
200
|
+
},
|
|
201
|
+
timeout=10
|
|
202
|
+
)
|
|
203
|
+
data = response.json()
|
|
204
|
+
|
|
205
|
+
if data.get("ok"):
|
|
206
|
+
print(f"Test message sent successfully to chat {chat_id}")
|
|
207
|
+
return True
|
|
208
|
+
else:
|
|
209
|
+
print("ERROR: Failed to send test message. Please verify your chat ID.")
|
|
210
|
+
print(f"Response: {data}")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
print(f"ERROR: Failed to send test message: {e}")
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def install_shell_functions(shell_rc: Path) -> bool:
|
|
219
|
+
"""Install shell functions to user's rc file."""
|
|
220
|
+
# Check if already installed
|
|
221
|
+
if shell_rc.exists():
|
|
222
|
+
with open(shell_rc, 'r') as f:
|
|
223
|
+
content = f.read()
|
|
224
|
+
if "# TELEGRAM NOTIFICATIONS" in content or "TELEMUX" in content:
|
|
225
|
+
print(f"WARNING: Shell functions already exist in {shell_rc}")
|
|
226
|
+
response = input("Overwrite? (y/n): ").strip().lower()
|
|
227
|
+
if response != 'y':
|
|
228
|
+
print("Skipping shell function installation")
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
# Copy shell_functions.sh to ~/.telemux/
|
|
232
|
+
print("Deploying shell functions...")
|
|
233
|
+
import telemux
|
|
234
|
+
package_dir = Path(telemux.__file__).parent
|
|
235
|
+
source_functions = package_dir / "shell_functions.sh"
|
|
236
|
+
|
|
237
|
+
if not source_functions.exists():
|
|
238
|
+
print(f"WARNING: shell_functions.sh not found at {source_functions}")
|
|
239
|
+
print("Shell functions will not be installed")
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
# Copy to ~/.telemux/
|
|
243
|
+
import shutil
|
|
244
|
+
dest_functions = TELEMUX_DIR / "shell_functions.sh"
|
|
245
|
+
shutil.copy(source_functions, dest_functions)
|
|
246
|
+
dest_functions.chmod(0o755)
|
|
247
|
+
print(f"Shell functions deployed to {dest_functions}")
|
|
248
|
+
|
|
249
|
+
# Add sourcing line to shell RC
|
|
250
|
+
print(f"Adding shell functions to {shell_rc}...")
|
|
251
|
+
with open(shell_rc, 'a') as f:
|
|
252
|
+
f.write('\n')
|
|
253
|
+
f.write('# ' + '=' * 77 + '\n')
|
|
254
|
+
f.write('# TELEGRAM NOTIFICATIONS (TeleMux)\n')
|
|
255
|
+
f.write('# ' + '=' * 77 + '\n')
|
|
256
|
+
f.write('# Source TeleMux shell functions (single source of truth)\n')
|
|
257
|
+
f.write('if [[ -f "$HOME/.telemux/shell_functions.sh" ]]; then\n')
|
|
258
|
+
f.write(' source "$HOME/.telemux/shell_functions.sh"\n')
|
|
259
|
+
f.write('fi\n')
|
|
260
|
+
f.write('\n')
|
|
261
|
+
|
|
262
|
+
print(f"Shell functions added (sourced from {dest_functions})")
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def update_claude_config():
|
|
267
|
+
"""Optionally add TeleMux documentation to Claude Code config."""
|
|
268
|
+
claude_config = Path.home() / ".claude" / "CLAUDE.md"
|
|
269
|
+
|
|
270
|
+
if not claude_config.exists():
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
print("")
|
|
274
|
+
print("=" * 60)
|
|
275
|
+
print("Claude Code Integration")
|
|
276
|
+
print("=" * 60)
|
|
277
|
+
print("")
|
|
278
|
+
print(f"Found Claude Code configuration at {claude_config}")
|
|
279
|
+
print("")
|
|
280
|
+
|
|
281
|
+
response = input("Add TeleMux documentation to Claude config? (y/n): ").strip().lower()
|
|
282
|
+
if response != 'y':
|
|
283
|
+
print("Skipped Claude config update")
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
# Check if already exists
|
|
287
|
+
with open(claude_config, 'r') as f:
|
|
288
|
+
content = f.read()
|
|
289
|
+
if "# TeleMux" in content:
|
|
290
|
+
print("WARNING: TeleMux section already exists in Claude config")
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
# Add TeleMux documentation
|
|
294
|
+
with open(claude_config, 'a') as f:
|
|
295
|
+
f.write('\n')
|
|
296
|
+
f.write('---\n')
|
|
297
|
+
f.write('\n')
|
|
298
|
+
f.write('# TeleMux - Telegram Integration\n')
|
|
299
|
+
f.write('\n')
|
|
300
|
+
f.write('TeleMux is installed and available for bidirectional communication with Telegram.\n')
|
|
301
|
+
f.write('\n')
|
|
302
|
+
f.write('## Available Functions\n')
|
|
303
|
+
f.write('\n')
|
|
304
|
+
f.write('- `tg_alert "message"` - Send one-way notifications to Telegram\n')
|
|
305
|
+
f.write('- `tg_agent "agent-name" "message"` - Send message and receive replies\n')
|
|
306
|
+
f.write('- `tg_done` - Alert when previous command completes\n')
|
|
307
|
+
f.write('\n')
|
|
308
|
+
f.write('## Control Commands\n')
|
|
309
|
+
f.write('\n')
|
|
310
|
+
f.write('- `tg-start` - Start the listener daemon\n')
|
|
311
|
+
f.write('- `tg-stop` - Stop the listener daemon\n')
|
|
312
|
+
f.write('- `tg-status` - Check daemon status\n')
|
|
313
|
+
f.write('- `tg-logs` - View listener logs\n')
|
|
314
|
+
f.write('\n')
|
|
315
|
+
f.write('## Usage in Agents\n')
|
|
316
|
+
f.write('\n')
|
|
317
|
+
f.write('When running in tmux, agents can use `tg_agent` to ask questions and receive user '
|
|
318
|
+
'replies via Telegram. Replies are delivered directly back to the tmux session.\n')
|
|
319
|
+
f.write('\n')
|
|
320
|
+
f.write('**Example:**\n')
|
|
321
|
+
f.write('```bash\n')
|
|
322
|
+
f.write('tg_agent "deploy-agent" "Ready to deploy to production?"\n')
|
|
323
|
+
f.write('# User replies via Telegram: "session-name: yes"\n')
|
|
324
|
+
f.write('# Reply appears in terminal\n')
|
|
325
|
+
f.write('```\n')
|
|
326
|
+
f.write('\n')
|
|
327
|
+
f.write('See: `~/.telemux/` for configuration and logs.\n')
|
|
328
|
+
f.write('\n')
|
|
329
|
+
|
|
330
|
+
print("TeleMux documentation added to Claude config")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def main():
|
|
334
|
+
"""Main installer entry point."""
|
|
335
|
+
print("=" * 60)
|
|
336
|
+
print("TeleMux Installation")
|
|
337
|
+
print("=" * 60)
|
|
338
|
+
print("")
|
|
339
|
+
|
|
340
|
+
# Check prerequisites
|
|
341
|
+
if not check_prerequisites():
|
|
342
|
+
sys.exit(1)
|
|
343
|
+
|
|
344
|
+
# Get Telegram credentials
|
|
345
|
+
print("=== Telegram Configuration ===")
|
|
346
|
+
print("")
|
|
347
|
+
bot_token = input("Enter your Telegram Bot Token: ").strip()
|
|
348
|
+
|
|
349
|
+
# Test bot token and fetch chats
|
|
350
|
+
print("")
|
|
351
|
+
print("Testing bot token and fetching available chats...")
|
|
352
|
+
bot_info = get_bot_info(bot_token)
|
|
353
|
+
|
|
354
|
+
if not bot_info:
|
|
355
|
+
print("ERROR: Invalid bot token. Please check and try again.")
|
|
356
|
+
sys.exit(1)
|
|
357
|
+
|
|
358
|
+
bot_name = bot_info.get("first_name", "Unknown")
|
|
359
|
+
print(f"Bot token valid: {bot_name}")
|
|
360
|
+
print("")
|
|
361
|
+
|
|
362
|
+
# Get chat ID (with retry logic)
|
|
363
|
+
chat_id = get_chat_id_interactive(bot_token)
|
|
364
|
+
|
|
365
|
+
if not chat_id:
|
|
366
|
+
print("ERROR: Chat ID is required")
|
|
367
|
+
sys.exit(1)
|
|
368
|
+
|
|
369
|
+
# Create TeleMux directory structure
|
|
370
|
+
print("")
|
|
371
|
+
print("Creating ~/.telemux directory...")
|
|
372
|
+
ensure_directories()
|
|
373
|
+
print("Directory structure created")
|
|
374
|
+
print("")
|
|
375
|
+
|
|
376
|
+
# Save configuration
|
|
377
|
+
print("Creating ~/.telemux/telegram_config...")
|
|
378
|
+
save_config(bot_token, chat_id)
|
|
379
|
+
print("Config file created and secured")
|
|
380
|
+
print("")
|
|
381
|
+
|
|
382
|
+
# Detect shell and install functions
|
|
383
|
+
shell_name = os.environ.get('SHELL', '').split('/')[-1]
|
|
384
|
+
if shell_name == 'zsh':
|
|
385
|
+
shell_rc = Path.home() / ".zshrc"
|
|
386
|
+
elif shell_name == 'bash':
|
|
387
|
+
shell_rc = Path.home() / ".bashrc"
|
|
388
|
+
else:
|
|
389
|
+
print("WARNING: Could not detect shell (bash/zsh). Unsupported shell.")
|
|
390
|
+
print(" Please manually add functions to your rc file.")
|
|
391
|
+
print(" See shell_functions.sh in ~/.telemux/")
|
|
392
|
+
shell_rc = None
|
|
393
|
+
|
|
394
|
+
if shell_rc:
|
|
395
|
+
install_shell_functions(shell_rc)
|
|
396
|
+
print("")
|
|
397
|
+
|
|
398
|
+
# Test installation
|
|
399
|
+
print("=== Testing Installation ===")
|
|
400
|
+
if not test_telegram_connection(bot_token, chat_id):
|
|
401
|
+
sys.exit(1)
|
|
402
|
+
|
|
403
|
+
print("")
|
|
404
|
+
print("=" * 60)
|
|
405
|
+
print("Installation Complete!")
|
|
406
|
+
print("=" * 60)
|
|
407
|
+
print("")
|
|
408
|
+
print("Next steps:")
|
|
409
|
+
if shell_rc:
|
|
410
|
+
print(f" 1. Reload your shell: source {shell_rc}")
|
|
411
|
+
print(" 2. Start the listener: telemux-start (or tg-start)")
|
|
412
|
+
print("")
|
|
413
|
+
print("For full documentation, visit: https://github.com/malmazan/telemux")
|
|
414
|
+
print("")
|
|
415
|
+
|
|
416
|
+
# Optional: Update Claude config
|
|
417
|
+
update_claude_config()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
if __name__ == "__main__":
|
|
421
|
+
main()
|