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.
- emdash_cli/client.py +12 -28
- emdash_cli/commands/__init__.py +2 -2
- emdash_cli/commands/agent/constants.py +78 -0
- emdash_cli/commands/agent/handlers/__init__.py +10 -0
- emdash_cli/commands/agent/handlers/agents.py +67 -39
- emdash_cli/commands/agent/handlers/index.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +119 -0
- emdash_cli/commands/agent/handlers/registry.py +72 -0
- emdash_cli/commands/agent/handlers/rules.py +48 -31
- emdash_cli/commands/agent/handlers/sessions.py +1 -1
- emdash_cli/commands/agent/handlers/setup.py +187 -54
- emdash_cli/commands/agent/handlers/skills.py +42 -4
- emdash_cli/commands/agent/handlers/telegram.py +523 -0
- emdash_cli/commands/agent/handlers/todos.py +55 -34
- emdash_cli/commands/agent/handlers/verify.py +10 -5
- emdash_cli/commands/agent/help.py +236 -0
- emdash_cli/commands/agent/interactive.py +278 -47
- emdash_cli/commands/agent/menus.py +116 -84
- emdash_cli/commands/agent/onboarding.py +619 -0
- emdash_cli/commands/agent/session_restore.py +210 -0
- emdash_cli/commands/index.py +111 -13
- emdash_cli/commands/registry.py +635 -0
- emdash_cli/commands/skills.py +72 -6
- emdash_cli/design.py +328 -0
- emdash_cli/diff_renderer.py +438 -0
- emdash_cli/integrations/__init__.py +1 -0
- emdash_cli/integrations/telegram/__init__.py +15 -0
- emdash_cli/integrations/telegram/bot.py +402 -0
- emdash_cli/integrations/telegram/bridge.py +980 -0
- emdash_cli/integrations/telegram/config.py +155 -0
- emdash_cli/integrations/telegram/formatter.py +392 -0
- emdash_cli/main.py +52 -2
- emdash_cli/sse_renderer.py +632 -171
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/METADATA +2 -2
- emdash_cli-0.1.70.dist-info/RECORD +63 -0
- emdash_cli/commands/swarm.py +0 -86
- emdash_cli-0.1.46.dist-info/RECORD +0 -49
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
|
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("[
|
|
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()
|