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/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()