emdash-cli 0.1.46__py3-none-any.whl → 0.1.70__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.
Files changed (39) hide show
  1. emdash_cli/client.py +12 -28
  2. emdash_cli/commands/__init__.py +2 -2
  3. emdash_cli/commands/agent/constants.py +78 -0
  4. emdash_cli/commands/agent/handlers/__init__.py +10 -0
  5. emdash_cli/commands/agent/handlers/agents.py +67 -39
  6. emdash_cli/commands/agent/handlers/index.py +183 -0
  7. emdash_cli/commands/agent/handlers/misc.py +119 -0
  8. emdash_cli/commands/agent/handlers/registry.py +72 -0
  9. emdash_cli/commands/agent/handlers/rules.py +48 -31
  10. emdash_cli/commands/agent/handlers/sessions.py +1 -1
  11. emdash_cli/commands/agent/handlers/setup.py +187 -54
  12. emdash_cli/commands/agent/handlers/skills.py +42 -4
  13. emdash_cli/commands/agent/handlers/telegram.py +523 -0
  14. emdash_cli/commands/agent/handlers/todos.py +55 -34
  15. emdash_cli/commands/agent/handlers/verify.py +10 -5
  16. emdash_cli/commands/agent/help.py +236 -0
  17. emdash_cli/commands/agent/interactive.py +278 -47
  18. emdash_cli/commands/agent/menus.py +116 -84
  19. emdash_cli/commands/agent/onboarding.py +619 -0
  20. emdash_cli/commands/agent/session_restore.py +210 -0
  21. emdash_cli/commands/index.py +111 -13
  22. emdash_cli/commands/registry.py +635 -0
  23. emdash_cli/commands/skills.py +72 -6
  24. emdash_cli/design.py +328 -0
  25. emdash_cli/diff_renderer.py +438 -0
  26. emdash_cli/integrations/__init__.py +1 -0
  27. emdash_cli/integrations/telegram/__init__.py +15 -0
  28. emdash_cli/integrations/telegram/bot.py +402 -0
  29. emdash_cli/integrations/telegram/bridge.py +980 -0
  30. emdash_cli/integrations/telegram/config.py +155 -0
  31. emdash_cli/integrations/telegram/formatter.py +392 -0
  32. emdash_cli/main.py +52 -2
  33. emdash_cli/sse_renderer.py +632 -171
  34. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/METADATA +2 -2
  35. emdash_cli-0.1.70.dist-info/RECORD +63 -0
  36. emdash_cli/commands/swarm.py +0 -86
  37. emdash_cli-0.1.46.dist-info/RECORD +0 -49
  38. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/WHEEL +0 -0
  39. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/entry_points.txt +0 -0
@@ -26,7 +26,7 @@ def list_skills() -> list[dict]:
26
26
  """List all skills (both user and built-in).
27
27
 
28
28
  Returns:
29
- List of dicts with name, description, user_invocable, is_builtin, file_path
29
+ List of dicts with name, description, user_invocable, is_builtin, file_path, scripts
30
30
  """
31
31
  from emdash_core.agent.skills import SkillRegistry
32
32
 
@@ -44,6 +44,7 @@ def list_skills() -> list[dict]:
44
44
  "user_invocable": skill.user_invocable,
45
45
  "is_builtin": getattr(skill, "_builtin", False),
46
46
  "file_path": str(skill.file_path) if skill.file_path else None,
47
+ "scripts": [str(s) for s in skill.scripts] if skill.scripts else [],
47
48
  })
48
49
 
49
50
  return skills
@@ -71,7 +72,8 @@ def show_skills_interactive_menu() -> tuple[str, str]:
71
72
 
72
73
  for skill in skills:
73
74
  builtin_marker = " [built-in]" if skill["is_builtin"] else ""
74
- menu_items.append((skill["name"], skill["description"] + builtin_marker, skill["is_builtin"], False))
75
+ scripts_marker = f" [{len(skill['scripts'])} scripts]" if skill.get("scripts") else ""
76
+ menu_items.append((skill["name"], skill["description"] + builtin_marker + scripts_marker, skill["is_builtin"], False))
75
77
 
76
78
  # Add action items at the bottom
77
79
  menu_items.append(("+ Create New Skill", "Create a new skill with AI assistance", False, True))
@@ -220,6 +222,15 @@ def show_skill_details(name: str) -> None:
220
222
  console.print(f"[bold]Description:[/bold] {skill.description}")
221
223
  console.print(f"[bold]User Invocable:[/bold] {invocable}")
222
224
  console.print(f"[bold]Tools:[/bold] {tools}")
225
+
226
+ # Show scripts
227
+ if skill.scripts:
228
+ console.print(f"[bold]Scripts:[/bold] {len(skill.scripts)}")
229
+ for script in skill.scripts:
230
+ console.print(f" [yellow]{script.name}[/yellow]: {script}")
231
+ else:
232
+ console.print(f"[bold]Scripts:[/bold] None")
233
+
223
234
  console.print(f"[bold]File:[/bold] {skill.file_path}\n")
224
235
  console.print("[bold]Instructions:[/bold]")
225
236
  console.print(Panel(skill.instructions, border_style="dim"))
@@ -332,6 +343,10 @@ tools: [tool1, tool2]
332
343
  # Skill Title
333
344
 
334
345
  Instructions for the skill...
346
+
347
+ ## Scripts (optional)
348
+
349
+ If scripts are included, document them here.
335
350
  ```
336
351
 
337
352
  **Frontmatter fields:**
@@ -340,9 +355,32 @@ Instructions for the skill...
340
355
  - `user_invocable`: Whether skill can be invoked with /name (true/false)
341
356
  - `tools`: List of tools this skill needs (optional)
342
357
 
358
+ **Skill Scripts (optional):**
359
+ Skills can include executable bash scripts in the same directory as SKILL.md. These scripts:
360
+ - Must be self-contained bash executables (with shebang like `#!/bin/bash`)
361
+ - Are automatically discovered and made available to the agent
362
+ - Can be named anything (e.g., `run.sh`, `deploy.sh`, `validate.sh`)
363
+ - Are executed by the agent using the Bash tool when needed
364
+
365
+ Example script (`run.sh`):
366
+ ```bash
367
+ #!/bin/bash
368
+ set -e
369
+ echo "Running skill script..."
370
+ # Add script logic here
371
+ ```
372
+
343
373
  **My request:** {user_input}
344
374
 
345
- Please help me create a skill. Ask me questions if needed to understand what I want, then use the Write tool to create the file at `{skills_dir}/<skill-name>/SKILL.md`."""
375
+ Please help me create a skill. Ask me questions if needed to understand what I want:
376
+ 1. What should the skill do?
377
+ 2. Does it need any scripts to execute code?
378
+
379
+ Then use the Write tool to create:
380
+ 1. The SKILL.md file at `{skills_dir}/<skill-name>/SKILL.md`
381
+ 2. Any scripts the user wants (e.g., `{skills_dir}/<skill-name>/run.sh`)
382
+
383
+ Remember to make scripts executable by including the shebang."""
346
384
  stream = client.agent_chat_stream(
347
385
  message=message_with_context,
348
386
  model=model,
@@ -425,7 +463,7 @@ def handle_skills(args: str, client, renderer, model, max_iterations, render_wit
425
463
  if is_builtin:
426
464
  console.print("[dim]Enter to go back[/dim]", end="")
427
465
  else:
428
- console.print("[dim]'d' delete | Enter back[/dim]", end="")
466
+ console.print("[red]'d'[/red] delete [dim]Enter back[/dim]", end="")
429
467
  ps = PromptSession()
430
468
  resp = ps.prompt(" ").strip().lower()
431
469
  if resp == 'd' and not is_builtin:
@@ -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()