emdash-cli 0.1.60__tar.gz → 0.1.72__tar.gz

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 (61) hide show
  1. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/PKG-INFO +2 -2
  2. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/constants.py +70 -0
  3. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/__init__.py +2 -0
  4. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/sessions.py +1 -1
  5. emdash_cli-0.1.72/emdash_cli/commands/agent/handlers/telegram.py +523 -0
  6. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/interactive.py +124 -10
  7. emdash_cli-0.1.72/emdash_cli/integrations/__init__.py +1 -0
  8. emdash_cli-0.1.72/emdash_cli/integrations/telegram/__init__.py +15 -0
  9. emdash_cli-0.1.72/emdash_cli/integrations/telegram/bot.py +402 -0
  10. emdash_cli-0.1.72/emdash_cli/integrations/telegram/bridge.py +980 -0
  11. emdash_cli-0.1.72/emdash_cli/integrations/telegram/config.py +155 -0
  12. emdash_cli-0.1.72/emdash_cli/integrations/telegram/formatter.py +392 -0
  13. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/main.py +50 -0
  14. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/sse_renderer.py +26 -11
  15. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/pyproject.toml +3 -3
  16. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/__init__.py +0 -0
  17. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/client.py +0 -0
  18. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/clipboard.py +0 -0
  19. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/__init__.py +0 -0
  20. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/__init__.py +0 -0
  21. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/cli.py +0 -0
  22. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/file_utils.py +0 -0
  23. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/agents.py +0 -0
  24. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/auth.py +0 -0
  25. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/doctor.py +0 -0
  26. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/hooks.py +0 -0
  27. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/index.py +0 -0
  28. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/mcp.py +0 -0
  29. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/misc.py +0 -0
  30. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/registry.py +0 -0
  31. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/rules.py +0 -0
  32. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/setup.py +0 -0
  33. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/skills.py +0 -0
  34. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/todos.py +0 -0
  35. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/handlers/verify.py +0 -0
  36. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/help.py +0 -0
  37. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/menus.py +0 -0
  38. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/onboarding.py +0 -0
  39. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent/session_restore.py +0 -0
  40. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/agent.py +0 -0
  41. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/analyze.py +0 -0
  42. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/auth.py +0 -0
  43. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/db.py +0 -0
  44. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/embed.py +0 -0
  45. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/index.py +0 -0
  46. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/plan.py +0 -0
  47. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/projectmd.py +0 -0
  48. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/registry.py +0 -0
  49. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/research.py +0 -0
  50. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/rules.py +0 -0
  51. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/search.py +0 -0
  52. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/server.py +0 -0
  53. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/skills.py +0 -0
  54. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/spec.py +0 -0
  55. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/tasks.py +0 -0
  56. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/commands/team.py +0 -0
  57. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/design.py +0 -0
  58. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/diff_renderer.py +0 -0
  59. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/keyboard.py +0 -0
  60. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/server_manager.py +0 -0
  61. {emdash_cli-0.1.60 → emdash_cli-0.1.72}/emdash_cli/session_store.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: emdash-cli
3
- Version: 0.1.60
3
+ Version: 0.1.72
4
4
  Summary: EmDash CLI - Command-line interface for code intelligence
5
5
  Author: Em Dash Team
6
6
  Requires-Python: >=3.10,<4.0
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: Programming Language :: Python :: 3.13
12
12
  Classifier: Programming Language :: Python :: 3.14
13
13
  Requires-Dist: click (>=8.1.7,<9.0.0)
14
- Requires-Dist: emdash-core (>=0.1.60)
14
+ Requires-Dist: emdash-core (>=0.1.72)
15
15
  Requires-Dist: httpx (>=0.25.0)
16
16
  Requires-Dist: prompt_toolkit (>=3.0.43,<4.0.0)
17
17
  Requires-Dist: rich (>=13.7.0)
@@ -9,6 +9,74 @@ class AgentMode(Enum):
9
9
  CODE = "code"
10
10
 
11
11
 
12
+ # Subcommands for slash commands that have them
13
+ SLASH_SUBCOMMANDS = {
14
+ "/telegram": {
15
+ "setup": "Configure bot token and authorize chats",
16
+ "connect": "Start the Telegram bridge",
17
+ "status": "Show current configuration",
18
+ "test": "Send a test message",
19
+ "disconnect": "Disable Telegram integration",
20
+ "settings": "View/modify settings",
21
+ },
22
+ "/session": {
23
+ "save": "Save current session (e.g., /session save my-task)",
24
+ "load": "Load a saved session",
25
+ "list": "List all saved sessions",
26
+ "delete": "Delete a saved session",
27
+ },
28
+ "/agents": {
29
+ "create": "Create a new agent",
30
+ "show": "Show agent details",
31
+ "edit": "Edit an existing agent",
32
+ "delete": "Delete an agent",
33
+ "list": "List all agents",
34
+ },
35
+ "/hooks": {
36
+ "list": "List all hooks",
37
+ "add": "Add a new hook",
38
+ "remove": "Remove a hook",
39
+ "toggle": "Enable/disable a hook",
40
+ },
41
+ "/rules": {
42
+ "list": "List all rules",
43
+ "add": "Add a new rule",
44
+ "delete": "Delete a rule",
45
+ },
46
+ "/skills": {
47
+ "list": "List all skills",
48
+ "show": "Show skill details",
49
+ "add": "Add a new skill",
50
+ "delete": "Delete a skill",
51
+ },
52
+ "/index": {
53
+ "status": "Show index status",
54
+ "start": "Start indexing",
55
+ "hook": "Manage index hooks (install/uninstall)",
56
+ },
57
+ "/mcp": {
58
+ "list": "List MCP servers",
59
+ "edit": "Edit MCP configuration",
60
+ },
61
+ "/auth": {
62
+ "login": "Login to GitHub",
63
+ "logout": "Logout from GitHub",
64
+ "status": "Show auth status",
65
+ },
66
+ "/registry": {
67
+ "skills": "Browse skills",
68
+ "rules": "Browse rules",
69
+ "agents": "Browse agents",
70
+ "verifiers": "Browse verifiers",
71
+ "install": "Install from registry",
72
+ },
73
+ "/verify": {
74
+ "run": "Run verification checks",
75
+ "list": "List available verifiers",
76
+ "add": "Add a verifier",
77
+ },
78
+ }
79
+
12
80
  # Slash commands available in interactive mode
13
81
  SLASH_COMMANDS = {
14
82
  # Mode switching
@@ -56,6 +124,8 @@ SLASH_COMMANDS = {
56
124
  "/verify-loop [task]": "Run task in loop until verifications pass",
57
125
  # Setup wizard
58
126
  "/setup": "Setup wizard for rules, agents, skills, and verifiers",
127
+ # Telegram integration
128
+ "/telegram": "Telegram integration (setup, connect, status, test)",
59
129
  "/help": "Show available commands",
60
130
  "/quit": "Exit the agent",
61
131
  }
@@ -22,6 +22,7 @@ from .misc import (
22
22
  handle_compact,
23
23
  handle_diff,
24
24
  )
25
+ from .telegram import handle_telegram
25
26
 
26
27
  __all__ = [
27
28
  "handle_agents",
@@ -46,4 +47,5 @@ __all__ = [
46
47
  "handle_context",
47
48
  "handle_compact",
48
49
  "handle_diff",
50
+ "handle_telegram",
49
51
  ]
@@ -30,7 +30,7 @@ def handle_session(
30
30
  current_mode_ref: Reference to current_mode (list wrapper for mutation)
31
31
  loaded_messages_ref: Reference to loaded_messages (list wrapper for mutation)
32
32
  """
33
- from ...session_store import SessionStore
33
+ from ....session_store import SessionStore
34
34
 
35
35
  store = SessionStore(Path.cwd())
36
36
 
@@ -0,0 +1,523 @@
1
+ """Handler for /telegram command."""
2
+
3
+ import asyncio
4
+ from datetime import datetime
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+
10
+ from ....design import Colors, header, footer, SEPARATOR_WIDTH
11
+
12
+ console = Console()
13
+
14
+
15
+ def handle_telegram(args: str) -> None:
16
+ """Handle /telegram command.
17
+
18
+ Args:
19
+ args: Command arguments (setup, connect, status, test, disconnect)
20
+ """
21
+ from ....integrations.telegram import TelegramConfig, get_config, save_config
22
+ from ....integrations.telegram.bot import verify_token, TelegramBot
23
+
24
+ # Parse subcommand
25
+ subparts = args.split(maxsplit=1) if args else []
26
+ subcommand = subparts[0].lower() if subparts else ""
27
+ subargs = subparts[1] if len(subparts) > 1 else ""
28
+
29
+ if subcommand == "" or subcommand == "help":
30
+ _show_telegram_help()
31
+
32
+ elif subcommand == "setup":
33
+ _handle_setup()
34
+
35
+ elif subcommand == "status":
36
+ _handle_status()
37
+
38
+ elif subcommand == "test":
39
+ _handle_test()
40
+
41
+ elif subcommand == "connect":
42
+ _handle_connect()
43
+
44
+ elif subcommand == "disconnect":
45
+ _handle_disconnect()
46
+
47
+ elif subcommand == "settings":
48
+ _handle_settings(subargs)
49
+
50
+ elif subcommand == "commands":
51
+ _handle_commands()
52
+
53
+ else:
54
+ console.print(f"[{Colors.WARNING}]Unknown subcommand: {subcommand}[/{Colors.WARNING}]")
55
+ console.print(f"[{Colors.DIM}]Run /telegram help for usage[/{Colors.DIM}]")
56
+
57
+
58
+ def _show_telegram_help() -> None:
59
+ """Show help for /telegram command."""
60
+ from ....integrations.telegram import get_config
61
+
62
+ config = get_config()
63
+
64
+ console.print()
65
+ console.print(f"[{Colors.MUTED}]{header('Telegram Integration', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
66
+ console.print()
67
+
68
+ # Status indicator
69
+ if config.is_configured():
70
+ status = f"[{Colors.SUCCESS}]configured[/{Colors.SUCCESS}]"
71
+ if config.state.enabled:
72
+ status += f" [{Colors.SUCCESS}](enabled)[/{Colors.SUCCESS}]"
73
+ else:
74
+ status = f"[{Colors.WARNING}]not configured[/{Colors.WARNING}]"
75
+
76
+ console.print(f" [{Colors.DIM}]Status:[/{Colors.DIM}] {status}")
77
+ console.print()
78
+
79
+ # Commands table
80
+ commands = [
81
+ ("/telegram setup", "Configure bot token and authorize chats"),
82
+ ("/telegram status", "Show current configuration and state"),
83
+ ("/telegram test", "Send a test message to authorized chats"),
84
+ ("/telegram connect", "Start the Telegram bridge (foreground)"),
85
+ ("/telegram disconnect", "Disable Telegram integration"),
86
+ ("/telegram settings", "View/modify settings"),
87
+ ("/telegram commands", "Show BotFather command list to copy"),
88
+ ]
89
+
90
+ console.print(f" [{Colors.DIM}]Commands:[/{Colors.DIM}]")
91
+ console.print()
92
+ for cmd, desc in commands:
93
+ console.print(f" [{Colors.PRIMARY}]{cmd:24}[/{Colors.PRIMARY}] [{Colors.DIM}]{desc}[/{Colors.DIM}]")
94
+
95
+ console.print()
96
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
97
+ console.print()
98
+
99
+
100
+ def _handle_setup() -> None:
101
+ """Interactive setup wizard for Telegram bot."""
102
+ from prompt_toolkit import prompt
103
+ from prompt_toolkit.styles import Style
104
+
105
+ from ....integrations.telegram import TelegramConfig, get_config, save_config
106
+ from ....integrations.telegram.bot import verify_token, TelegramBot, TelegramAPIError
107
+
108
+ console.print()
109
+ console.print(f"[{Colors.MUTED}]{header('Telegram Setup', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
110
+ console.print()
111
+ console.print(f" [{Colors.DIM}]To create a Telegram bot:[/{Colors.DIM}]")
112
+ console.print()
113
+ console.print(f" [{Colors.SUBTLE}]1. Open Telegram and search for @BotFather[/{Colors.SUBTLE}]")
114
+ console.print(f" [{Colors.SUBTLE}]2. Send /newbot and follow the prompts[/{Colors.SUBTLE}]")
115
+ console.print(f" [{Colors.SUBTLE}]3. Copy the bot token and paste below[/{Colors.SUBTLE}]")
116
+ console.print()
117
+
118
+ # Get current config
119
+ config = get_config()
120
+
121
+ # Prompt for bot token
122
+ style = Style.from_dict({"": Colors.PRIMARY})
123
+
124
+ try:
125
+ token = prompt(
126
+ " Bot Token: ",
127
+ style=style,
128
+ default=config.bot_token or "",
129
+ ).strip()
130
+ except (KeyboardInterrupt, EOFError):
131
+ console.print(f"\n [{Colors.DIM}]Setup cancelled[/{Colors.DIM}]")
132
+ return
133
+
134
+ if not token:
135
+ console.print(f" [{Colors.WARNING}]No token provided, setup cancelled[/{Colors.WARNING}]")
136
+ return
137
+
138
+ # Verify token
139
+ console.print()
140
+ console.print(f" [{Colors.DIM}]Verifying token...[/{Colors.DIM}]")
141
+
142
+ bot_info = asyncio.run(verify_token(token))
143
+
144
+ if not bot_info:
145
+ console.print(f" [{Colors.ERROR}]Invalid token. Please check and try again.[/{Colors.ERROR}]")
146
+ return
147
+
148
+ console.print(f" [{Colors.SUCCESS}]Bot verified: @{bot_info.username}[/{Colors.SUCCESS}]")
149
+ console.print()
150
+
151
+ # Save config with token
152
+ config.bot_token = token
153
+ save_config(config)
154
+
155
+ # Now wait for a user to message the bot to authorize
156
+ console.print(f" [{Colors.ACCENT}]Now send /start to your bot from Telegram to authorize.[/{Colors.ACCENT}]")
157
+ console.print(f" [{Colors.DIM}]Waiting for connection... (Ctrl+C to skip)[/{Colors.DIM}]")
158
+ console.print()
159
+
160
+ try:
161
+ authorized_chat = asyncio.run(_wait_for_authorization(token))
162
+ if authorized_chat:
163
+ config.add_authorized_chat(authorized_chat["id"])
164
+ config.state.enabled = True
165
+ config.state.last_connected = datetime.now().isoformat()
166
+ save_config(config)
167
+
168
+ console.print(f" [{Colors.SUCCESS}]Authorized: {authorized_chat['name']} (ID: {authorized_chat['id']})[/{Colors.SUCCESS}]")
169
+ console.print()
170
+ console.print(f" [{Colors.SUCCESS}]Setup complete![/{Colors.SUCCESS}]")
171
+ console.print(f" [{Colors.DIM}]Run /telegram connect to start the bridge[/{Colors.DIM}]")
172
+ except KeyboardInterrupt:
173
+ # Save config without authorization (user can authorize later)
174
+ save_config(config)
175
+ console.print()
176
+ console.print(f" [{Colors.DIM}]Token saved. Run /telegram setup again to authorize chats.[/{Colors.DIM}]")
177
+
178
+ console.print()
179
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
180
+ console.print()
181
+
182
+
183
+ async def _wait_for_authorization(token: str, timeout: int = 60) -> dict | None:
184
+ """Wait for a user to send /start to the bot.
185
+
186
+ Args:
187
+ token: Bot token
188
+ timeout: Maximum seconds to wait
189
+
190
+ Returns:
191
+ Dict with chat id and name, or None if timeout
192
+ """
193
+ from ....integrations.telegram.bot import TelegramBot
194
+
195
+ async with TelegramBot(token) as bot:
196
+ start_time = asyncio.get_event_loop().time()
197
+
198
+ while (asyncio.get_event_loop().time() - start_time) < timeout:
199
+ try:
200
+ updates = await bot.get_updates(timeout=5)
201
+
202
+ for update in updates:
203
+ if update.message and update.message.text:
204
+ text = update.message.text.strip()
205
+ # Accept /start or any message as authorization
206
+ if text.startswith("/start") or text:
207
+ chat = update.message.chat
208
+ user = update.message.from_user
209
+
210
+ name = chat.display_name
211
+ if user:
212
+ name = user.display_name
213
+
214
+ return {
215
+ "id": chat.id,
216
+ "name": name,
217
+ }
218
+ except Exception:
219
+ await asyncio.sleep(1)
220
+ continue
221
+
222
+ return None
223
+
224
+
225
+ def _handle_status() -> None:
226
+ """Show current Telegram configuration and status."""
227
+ from ....integrations.telegram import get_config
228
+ from ....integrations.telegram.bot import verify_token
229
+
230
+ config = get_config()
231
+
232
+ console.print()
233
+ console.print(f"[{Colors.MUTED}]{header('Telegram Status', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
234
+ console.print()
235
+
236
+ # Configuration
237
+ if config.is_configured():
238
+ console.print(f" [{Colors.DIM}]Bot Token:[/{Colors.DIM}] [{Colors.SUCCESS}]configured[/{Colors.SUCCESS}]")
239
+
240
+ # Verify token is still valid
241
+ bot_info = asyncio.run(verify_token(config.bot_token))
242
+ if bot_info:
243
+ console.print(f" [{Colors.DIM}]Bot:[/{Colors.DIM}] @{bot_info.username}")
244
+ else:
245
+ console.print(f" [{Colors.DIM}]Bot:[/{Colors.DIM}] [{Colors.ERROR}]token invalid[/{Colors.ERROR}]")
246
+ else:
247
+ console.print(f" [{Colors.DIM}]Bot Token:[/{Colors.DIM}] [{Colors.WARNING}]not configured[/{Colors.WARNING}]")
248
+ console.print()
249
+ console.print(f" [{Colors.DIM}]Run /telegram setup to configure[/{Colors.DIM}]")
250
+ console.print()
251
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
252
+ console.print()
253
+ return
254
+
255
+ # Authorized chats
256
+ if config.authorized_chats:
257
+ console.print(f" [{Colors.DIM}]Authorized:[/{Colors.DIM}] {len(config.authorized_chats)} chat(s)")
258
+ for chat_id in config.authorized_chats:
259
+ console.print(f" [{Colors.SUBTLE}]{chat_id}[/{Colors.SUBTLE}]")
260
+ else:
261
+ console.print(f" [{Colors.DIM}]Authorized:[/{Colors.DIM}] [{Colors.WARNING}]no chats authorized[/{Colors.WARNING}]")
262
+
263
+ # State
264
+ console.print()
265
+ enabled_status = f"[{Colors.SUCCESS}]yes[/{Colors.SUCCESS}]" if config.state.enabled else f"[{Colors.DIM}]no[/{Colors.DIM}]"
266
+ console.print(f" [{Colors.DIM}]Enabled:[/{Colors.DIM}] {enabled_status}")
267
+
268
+ if config.state.last_connected:
269
+ console.print(f" [{Colors.DIM}]Last Active:[/{Colors.DIM}] {config.state.last_connected}")
270
+
271
+ # Settings
272
+ console.print()
273
+ console.print(f" [{Colors.DIM}]Settings:[/{Colors.DIM}]")
274
+ console.print(f" [{Colors.SUBTLE}]Streaming mode:[/{Colors.SUBTLE}] {config.settings.streaming_mode}")
275
+ console.print(f" [{Colors.SUBTLE}]Update interval:[/{Colors.SUBTLE}] {config.settings.update_interval_ms}ms")
276
+ console.print(f" [{Colors.SUBTLE}]Show thinking:[/{Colors.SUBTLE}] {config.settings.show_thinking}")
277
+ console.print(f" [{Colors.SUBTLE}]Show tools:[/{Colors.SUBTLE}] {config.settings.show_tool_calls}")
278
+
279
+ console.print()
280
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
281
+ console.print()
282
+
283
+
284
+ def _handle_test() -> None:
285
+ """Send a test message to authorized chats."""
286
+ from ....integrations.telegram import get_config
287
+ from ....integrations.telegram.bot import TelegramBot
288
+
289
+ config = get_config()
290
+
291
+ if not config.is_configured():
292
+ console.print(f"[{Colors.WARNING}]Telegram not configured. Run /telegram setup first.[/{Colors.WARNING}]")
293
+ return
294
+
295
+ if not config.authorized_chats:
296
+ console.print(f"[{Colors.WARNING}]No authorized chats. Run /telegram setup to authorize.[/{Colors.WARNING}]")
297
+ return
298
+
299
+ console.print()
300
+ console.print(f" [{Colors.DIM}]Sending test message...[/{Colors.DIM}]")
301
+
302
+ async def send_test():
303
+ async with TelegramBot(config.bot_token) as bot:
304
+ bot_info = await bot.get_me()
305
+ for chat_id in config.authorized_chats:
306
+ try:
307
+ await bot.send_message(
308
+ chat_id,
309
+ f"*EmDash Test*\n\nBot `@{bot_info.username}` is connected and working.",
310
+ parse_mode="Markdown",
311
+ )
312
+ console.print(f" [{Colors.SUCCESS}]Sent to chat {chat_id}[/{Colors.SUCCESS}]")
313
+ except Exception as e:
314
+ console.print(f" [{Colors.ERROR}]Failed to send to {chat_id}: {e}[/{Colors.ERROR}]")
315
+
316
+ asyncio.run(send_test())
317
+ console.print()
318
+
319
+
320
+ def _handle_connect() -> None:
321
+ """Start the Telegram bridge in foreground mode."""
322
+ from ....integrations.telegram import get_config, save_config
323
+
324
+ config = get_config()
325
+
326
+ if not config.is_configured():
327
+ console.print(f"[{Colors.WARNING}]Telegram not configured. Run /telegram setup first.[/{Colors.WARNING}]")
328
+ return
329
+
330
+ console.print()
331
+ console.print(f"[{Colors.MUTED}]{header('Telegram Bridge', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
332
+ console.print()
333
+ console.print(f" [{Colors.SUCCESS}]Bridge starting...[/{Colors.SUCCESS}]")
334
+ console.print(f" [{Colors.DIM}]Listening for messages. Press Ctrl+C to stop.[/{Colors.DIM}]")
335
+ console.print()
336
+
337
+ # Update state
338
+ config.state.enabled = True
339
+ config.state.last_connected = datetime.now().isoformat()
340
+ save_config(config)
341
+
342
+ try:
343
+ asyncio.run(_run_bridge(config))
344
+ except KeyboardInterrupt:
345
+ console.print()
346
+ console.print(f" [{Colors.DIM}]Bridge stopped[/{Colors.DIM}]")
347
+
348
+ console.print()
349
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
350
+ console.print()
351
+
352
+
353
+ async def _run_bridge(config) -> None:
354
+ """Run the Telegram bridge (receives messages and forwards to agent).
355
+
356
+ Connects Telegram messages to the EmDash agent and streams
357
+ SSE responses back as Telegram messages.
358
+ """
359
+ from ....integrations.telegram.bridge import TelegramBridge
360
+ from ....integrations.telegram import save_config
361
+ from ....server_manager import get_server_manager
362
+
363
+ # Get server URL from server manager (starts server if needed)
364
+ server = get_server_manager()
365
+ server_url = server.get_server_url()
366
+ console.print(f" [{Colors.SUBTLE}]Server: {server_url}[/{Colors.SUBTLE}]")
367
+
368
+ def on_message(event_type: str, data: dict) -> None:
369
+ """Log bridge events to the console."""
370
+ if event_type == "bridge_started":
371
+ console.print(f" [{Colors.SUBTLE}]Bot: @{data.get('bot', 'unknown')}[/{Colors.SUBTLE}]")
372
+ console.print()
373
+ elif event_type == "message_received":
374
+ user = data.get("user", "Unknown")
375
+ text = data.get("text", "")[:50]
376
+ if len(data.get("text", "")) > 50:
377
+ text += "..."
378
+ console.print(f" [{Colors.ACCENT}]{user}:[/{Colors.ACCENT}] {text}")
379
+ elif event_type == "error":
380
+ console.print(f" [{Colors.ERROR}]Error: {data.get('error', 'unknown')}[/{Colors.ERROR}]")
381
+ elif event_type == "send_error":
382
+ console.print(f" [{Colors.WARNING}]Send failed: {data.get('error', 'unknown')}[/{Colors.WARNING}]")
383
+
384
+ bridge = TelegramBridge(config, server_url=server_url, on_message=on_message)
385
+
386
+ try:
387
+ await bridge.start()
388
+ finally:
389
+ # Save config with updated state (last_update_id, etc.)
390
+ save_config(config)
391
+
392
+
393
+ def _handle_disconnect() -> None:
394
+ """Disable Telegram integration."""
395
+ from ....integrations.telegram import get_config, save_config, delete_config
396
+ from prompt_toolkit import prompt
397
+
398
+ config = get_config()
399
+
400
+ if not config.is_configured():
401
+ console.print(f"[{Colors.DIM}]Telegram is not configured.[/{Colors.DIM}]")
402
+ return
403
+
404
+ console.print()
405
+ console.print(f" [{Colors.WARNING}]This will remove your Telegram configuration.[/{Colors.WARNING}]")
406
+
407
+ try:
408
+ confirm = prompt(" Type 'yes' to confirm: ").strip().lower()
409
+ except (KeyboardInterrupt, EOFError):
410
+ console.print(f"\n [{Colors.DIM}]Cancelled[/{Colors.DIM}]")
411
+ return
412
+
413
+ if confirm == "yes":
414
+ delete_config()
415
+ console.print(f" [{Colors.SUCCESS}]Telegram configuration removed.[/{Colors.SUCCESS}]")
416
+ else:
417
+ console.print(f" [{Colors.DIM}]Cancelled[/{Colors.DIM}]")
418
+
419
+ console.print()
420
+
421
+
422
+ def _handle_settings(args: str) -> None:
423
+ """View or modify Telegram settings."""
424
+ from ....integrations.telegram import get_config, save_config
425
+
426
+ config = get_config()
427
+
428
+ if not config.is_configured():
429
+ console.print(f"[{Colors.WARNING}]Telegram not configured. Run /telegram setup first.[/{Colors.WARNING}]")
430
+ return
431
+
432
+ if not args:
433
+ # Show current settings
434
+ console.print()
435
+ console.print(f" [{Colors.DIM}]Current Settings:[/{Colors.DIM}]")
436
+ console.print()
437
+ console.print(f" [{Colors.PRIMARY}]streaming_mode[/{Colors.PRIMARY}] = {config.settings.streaming_mode}")
438
+ console.print(f" [{Colors.PRIMARY}]update_interval_ms[/{Colors.PRIMARY}] = {config.settings.update_interval_ms}")
439
+ console.print(f" [{Colors.PRIMARY}]show_thinking[/{Colors.PRIMARY}] = {config.settings.show_thinking}")
440
+ console.print(f" [{Colors.PRIMARY}]show_tool_calls[/{Colors.PRIMARY}] = {config.settings.show_tool_calls}")
441
+ console.print(f" [{Colors.PRIMARY}]compact_mode[/{Colors.PRIMARY}] = {config.settings.compact_mode}")
442
+ console.print()
443
+ console.print(f" [{Colors.DIM}]Usage: /telegram settings <key> <value>[/{Colors.DIM}]")
444
+ console.print()
445
+ return
446
+
447
+ # Parse key=value or key value
448
+ parts = args.replace("=", " ").split()
449
+ if len(parts) < 2:
450
+ console.print(f"[{Colors.WARNING}]Usage: /telegram settings <key> <value>[/{Colors.WARNING}]")
451
+ return
452
+
453
+ key = parts[0]
454
+ value = parts[1]
455
+
456
+ # Update setting
457
+ if key == "streaming_mode":
458
+ if value not in ("edit", "append"):
459
+ console.print(f"[{Colors.WARNING}]streaming_mode must be 'edit' or 'append'[/{Colors.WARNING}]")
460
+ return
461
+ config.settings.streaming_mode = value
462
+ elif key == "update_interval_ms":
463
+ try:
464
+ config.settings.update_interval_ms = int(value)
465
+ except ValueError:
466
+ console.print(f"[{Colors.WARNING}]update_interval_ms must be a number[/{Colors.WARNING}]")
467
+ return
468
+ elif key == "show_thinking":
469
+ config.settings.show_thinking = value.lower() in ("true", "1", "yes")
470
+ elif key == "show_tool_calls":
471
+ config.settings.show_tool_calls = value.lower() in ("true", "1", "yes")
472
+ elif key == "compact_mode":
473
+ config.settings.compact_mode = value.lower() in ("true", "1", "yes")
474
+ else:
475
+ console.print(f"[{Colors.WARNING}]Unknown setting: {key}[/{Colors.WARNING}]")
476
+ return
477
+
478
+ save_config(config)
479
+ console.print(f" [{Colors.SUCCESS}]Setting updated: {key} = {value}[/{Colors.SUCCESS}]")
480
+
481
+
482
+ def _handle_commands() -> None:
483
+ """Show BotFather command list for easy copy-paste."""
484
+ from ..constants import SLASH_COMMANDS
485
+
486
+ console.print()
487
+ console.print(f"[{Colors.MUTED}]{header('BotFather Commands', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
488
+ console.print()
489
+ console.print(f" [{Colors.DIM}]Copy the following and paste to @BotFather after /setcommands:[/{Colors.DIM}]")
490
+ console.print()
491
+ console.print(f" [{Colors.MUTED}]{'─' * 60}[/{Colors.MUTED}]")
492
+
493
+ # Generate command list in BotFather format
494
+ # Format: command - description (no leading slash, underscores instead of hyphens)
495
+ for cmd, desc in SLASH_COMMANDS.items():
496
+ # Skip telegram and quit commands (not useful via Telegram)
497
+ if cmd in ("/telegram", "/quit", "/paste"):
498
+ continue
499
+
500
+ # Convert /cmd-name to cmd_name (BotFather format)
501
+ botfather_cmd = cmd.lstrip("/").replace("-", "_")
502
+
503
+ # Remove any argument placeholders from command
504
+ if " " in botfather_cmd:
505
+ botfather_cmd = botfather_cmd.split()[0]
506
+
507
+ # Truncate description if too long (BotFather has limits)
508
+ short_desc = desc.split("(")[0].strip() # Remove parenthetical notes
509
+ if len(short_desc) > 50:
510
+ short_desc = short_desc[:47] + "..."
511
+
512
+ console.print(f" {botfather_cmd} - {short_desc}")
513
+
514
+ console.print(f" [{Colors.MUTED}]{'─' * 60}[/{Colors.MUTED}]")
515
+ console.print()
516
+ console.print(f" [{Colors.DIM}]Steps:[/{Colors.DIM}]")
517
+ console.print(f" [{Colors.SUBTLE}]1. Open @BotFather in Telegram[/{Colors.SUBTLE}]")
518
+ console.print(f" [{Colors.SUBTLE}]2. Send /setcommands[/{Colors.SUBTLE}]")
519
+ console.print(f" [{Colors.SUBTLE}]3. Select your bot[/{Colors.SUBTLE}]")
520
+ console.print(f" [{Colors.SUBTLE}]4. Paste the command list above[/{Colors.SUBTLE}]")
521
+ console.print()
522
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
523
+ console.print()