emdash-cli 0.1.35__py3-none-any.whl → 0.1.67__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 +41 -22
- emdash_cli/clipboard.py +30 -61
- emdash_cli/commands/__init__.py +2 -2
- emdash_cli/commands/agent/__init__.py +14 -0
- emdash_cli/commands/agent/cli.py +100 -0
- emdash_cli/commands/agent/constants.py +63 -0
- emdash_cli/commands/agent/file_utils.py +178 -0
- emdash_cli/commands/agent/handlers/__init__.py +51 -0
- emdash_cli/commands/agent/handlers/agents.py +449 -0
- emdash_cli/commands/agent/handlers/auth.py +69 -0
- emdash_cli/commands/agent/handlers/doctor.py +319 -0
- emdash_cli/commands/agent/handlers/hooks.py +121 -0
- emdash_cli/commands/agent/handlers/index.py +183 -0
- emdash_cli/commands/agent/handlers/mcp.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +319 -0
- emdash_cli/commands/agent/handlers/registry.py +72 -0
- emdash_cli/commands/agent/handlers/rules.py +411 -0
- emdash_cli/commands/agent/handlers/sessions.py +168 -0
- emdash_cli/commands/agent/handlers/setup.py +715 -0
- emdash_cli/commands/agent/handlers/skills.py +478 -0
- emdash_cli/commands/agent/handlers/telegram.py +475 -0
- emdash_cli/commands/agent/handlers/todos.py +119 -0
- emdash_cli/commands/agent/handlers/verify.py +653 -0
- emdash_cli/commands/agent/help.py +236 -0
- emdash_cli/commands/agent/interactive.py +842 -0
- emdash_cli/commands/agent/menus.py +760 -0
- emdash_cli/commands/agent/onboarding.py +619 -0
- emdash_cli/commands/agent/session_restore.py +210 -0
- emdash_cli/commands/agent.py +7 -1321
- emdash_cli/commands/index.py +111 -13
- emdash_cli/commands/registry.py +635 -0
- emdash_cli/commands/server.py +99 -40
- 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 +865 -0
- emdash_cli/integrations/telegram/config.py +155 -0
- emdash_cli/integrations/telegram/formatter.py +385 -0
- emdash_cli/main.py +52 -2
- emdash_cli/server_manager.py +70 -10
- emdash_cli/sse_renderer.py +659 -167
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/METADATA +2 -4
- emdash_cli-0.1.67.dist-info/RECORD +63 -0
- emdash_cli/commands/swarm.py +0 -86
- emdash_cli-0.1.35.dist-info/RECORD +0 -30
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""Telegram Bot API wrapper.
|
|
2
|
+
|
|
3
|
+
Provides a simple async client for interacting with the Telegram Bot API.
|
|
4
|
+
Uses httpx for HTTP requests (already a dependency of the CLI).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, AsyncIterator
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Telegram Bot API base URL
|
|
15
|
+
API_BASE = "https://api.telegram.org/bot{token}"
|
|
16
|
+
|
|
17
|
+
# Timeout for long-polling (seconds)
|
|
18
|
+
LONG_POLL_TIMEOUT = 30
|
|
19
|
+
|
|
20
|
+
# HTTP timeout (slightly longer than long-poll to account for network)
|
|
21
|
+
HTTP_TIMEOUT = LONG_POLL_TIMEOUT + 10
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class TelegramUser:
|
|
26
|
+
"""Represents a Telegram user."""
|
|
27
|
+
|
|
28
|
+
id: int
|
|
29
|
+
first_name: str
|
|
30
|
+
last_name: str | None = None
|
|
31
|
+
username: str | None = None
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_dict(cls, data: dict[str, Any]) -> "TelegramUser":
|
|
35
|
+
return cls(
|
|
36
|
+
id=data["id"],
|
|
37
|
+
first_name=data["first_name"],
|
|
38
|
+
last_name=data.get("last_name"),
|
|
39
|
+
username=data.get("username"),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def display_name(self) -> str:
|
|
44
|
+
"""Get a display name for the user."""
|
|
45
|
+
if self.username:
|
|
46
|
+
return f"@{self.username}"
|
|
47
|
+
if self.last_name:
|
|
48
|
+
return f"{self.first_name} {self.last_name}"
|
|
49
|
+
return self.first_name
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class TelegramChat:
|
|
54
|
+
"""Represents a Telegram chat."""
|
|
55
|
+
|
|
56
|
+
id: int
|
|
57
|
+
type: str # "private", "group", "supergroup", "channel"
|
|
58
|
+
title: str | None = None # For groups/channels
|
|
59
|
+
username: str | None = None
|
|
60
|
+
first_name: str | None = None # For private chats
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_dict(cls, data: dict[str, Any]) -> "TelegramChat":
|
|
64
|
+
return cls(
|
|
65
|
+
id=data["id"],
|
|
66
|
+
type=data["type"],
|
|
67
|
+
title=data.get("title"),
|
|
68
|
+
username=data.get("username"),
|
|
69
|
+
first_name=data.get("first_name"),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def display_name(self) -> str:
|
|
74
|
+
"""Get a display name for the chat."""
|
|
75
|
+
if self.title:
|
|
76
|
+
return self.title
|
|
77
|
+
if self.first_name:
|
|
78
|
+
return self.first_name
|
|
79
|
+
return str(self.id)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class TelegramMessage:
|
|
84
|
+
"""Represents a Telegram message."""
|
|
85
|
+
|
|
86
|
+
message_id: int
|
|
87
|
+
chat: TelegramChat
|
|
88
|
+
text: str | None = None
|
|
89
|
+
from_user: TelegramUser | None = None
|
|
90
|
+
date: int = 0 # Unix timestamp
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_dict(cls, data: dict[str, Any]) -> "TelegramMessage":
|
|
94
|
+
from_user = None
|
|
95
|
+
if "from" in data:
|
|
96
|
+
from_user = TelegramUser.from_dict(data["from"])
|
|
97
|
+
|
|
98
|
+
return cls(
|
|
99
|
+
message_id=data["message_id"],
|
|
100
|
+
chat=TelegramChat.from_dict(data["chat"]),
|
|
101
|
+
text=data.get("text"),
|
|
102
|
+
from_user=from_user,
|
|
103
|
+
date=data.get("date", 0),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class TelegramUpdate:
|
|
109
|
+
"""Represents a Telegram update (incoming event)."""
|
|
110
|
+
|
|
111
|
+
update_id: int
|
|
112
|
+
message: TelegramMessage | None = None
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_dict(cls, data: dict[str, Any]) -> "TelegramUpdate":
|
|
116
|
+
message = None
|
|
117
|
+
if "message" in data:
|
|
118
|
+
message = TelegramMessage.from_dict(data["message"])
|
|
119
|
+
|
|
120
|
+
return cls(
|
|
121
|
+
update_id=data["update_id"],
|
|
122
|
+
message=message,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TelegramAPIError(Exception):
|
|
127
|
+
"""Error from Telegram Bot API."""
|
|
128
|
+
|
|
129
|
+
def __init__(self, error_code: int, description: str):
|
|
130
|
+
self.error_code = error_code
|
|
131
|
+
self.description = description
|
|
132
|
+
super().__init__(f"Telegram API error {error_code}: {description}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TelegramBot:
|
|
136
|
+
"""Async Telegram Bot API client."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, token: str):
|
|
139
|
+
"""Initialize the bot with a token.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
token: Bot token from @BotFather
|
|
143
|
+
"""
|
|
144
|
+
self.token = token
|
|
145
|
+
self._base_url = API_BASE.format(token=token)
|
|
146
|
+
self._client: httpx.AsyncClient | None = None
|
|
147
|
+
|
|
148
|
+
async def __aenter__(self) -> "TelegramBot":
|
|
149
|
+
"""Enter async context."""
|
|
150
|
+
self._client = httpx.AsyncClient(timeout=HTTP_TIMEOUT)
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
154
|
+
"""Exit async context."""
|
|
155
|
+
if self._client:
|
|
156
|
+
await self._client.aclose()
|
|
157
|
+
self._client = None
|
|
158
|
+
|
|
159
|
+
def _get_client(self) -> httpx.AsyncClient:
|
|
160
|
+
"""Get the HTTP client, creating if needed."""
|
|
161
|
+
if self._client is None:
|
|
162
|
+
self._client = httpx.AsyncClient(timeout=HTTP_TIMEOUT)
|
|
163
|
+
return self._client
|
|
164
|
+
|
|
165
|
+
async def _request(
|
|
166
|
+
self,
|
|
167
|
+
method: str,
|
|
168
|
+
**params: Any,
|
|
169
|
+
) -> dict[str, Any]:
|
|
170
|
+
"""Make a request to the Telegram Bot API.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
method: API method name (e.g., "getMe", "sendMessage")
|
|
174
|
+
**params: Method parameters
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Response data from the API
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
TelegramAPIError: If the API returns an error
|
|
181
|
+
"""
|
|
182
|
+
url = f"{self._base_url}/{method}"
|
|
183
|
+
client = self._get_client()
|
|
184
|
+
|
|
185
|
+
# Filter out None values
|
|
186
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
187
|
+
|
|
188
|
+
response = await client.post(url, json=params)
|
|
189
|
+
data = response.json()
|
|
190
|
+
|
|
191
|
+
if not data.get("ok"):
|
|
192
|
+
raise TelegramAPIError(
|
|
193
|
+
error_code=data.get("error_code", 0),
|
|
194
|
+
description=data.get("description", "Unknown error"),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return data.get("result", {})
|
|
198
|
+
|
|
199
|
+
async def get_me(self) -> TelegramUser:
|
|
200
|
+
"""Get information about the bot.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
TelegramUser representing the bot
|
|
204
|
+
"""
|
|
205
|
+
result = await self._request("getMe")
|
|
206
|
+
return TelegramUser.from_dict(result)
|
|
207
|
+
|
|
208
|
+
async def send_message(
|
|
209
|
+
self,
|
|
210
|
+
chat_id: int,
|
|
211
|
+
text: str,
|
|
212
|
+
parse_mode: str | None = "Markdown",
|
|
213
|
+
reply_to_message_id: int | None = None,
|
|
214
|
+
disable_notification: bool = False,
|
|
215
|
+
) -> TelegramMessage:
|
|
216
|
+
"""Send a text message.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
chat_id: Target chat ID
|
|
220
|
+
text: Message text (max 4096 characters)
|
|
221
|
+
parse_mode: "Markdown", "HTML", or None for plain text
|
|
222
|
+
reply_to_message_id: Message ID to reply to
|
|
223
|
+
disable_notification: Send silently
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
The sent message
|
|
227
|
+
"""
|
|
228
|
+
# Truncate if needed
|
|
229
|
+
if len(text) > 4096:
|
|
230
|
+
text = text[:4093] + "..."
|
|
231
|
+
|
|
232
|
+
result = await self._request(
|
|
233
|
+
"sendMessage",
|
|
234
|
+
chat_id=chat_id,
|
|
235
|
+
text=text,
|
|
236
|
+
parse_mode=parse_mode,
|
|
237
|
+
reply_to_message_id=reply_to_message_id,
|
|
238
|
+
disable_notification=disable_notification,
|
|
239
|
+
)
|
|
240
|
+
return TelegramMessage.from_dict(result)
|
|
241
|
+
|
|
242
|
+
async def edit_message_text(
|
|
243
|
+
self,
|
|
244
|
+
chat_id: int,
|
|
245
|
+
message_id: int,
|
|
246
|
+
text: str,
|
|
247
|
+
parse_mode: str | None = "Markdown",
|
|
248
|
+
) -> TelegramMessage:
|
|
249
|
+
"""Edit a message's text.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
chat_id: Chat containing the message
|
|
253
|
+
message_id: ID of the message to edit
|
|
254
|
+
text: New text
|
|
255
|
+
parse_mode: "Markdown", "HTML", or None
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
The edited message
|
|
259
|
+
"""
|
|
260
|
+
# Truncate if needed
|
|
261
|
+
if len(text) > 4096:
|
|
262
|
+
text = text[:4093] + "..."
|
|
263
|
+
|
|
264
|
+
result = await self._request(
|
|
265
|
+
"editMessageText",
|
|
266
|
+
chat_id=chat_id,
|
|
267
|
+
message_id=message_id,
|
|
268
|
+
text=text,
|
|
269
|
+
parse_mode=parse_mode,
|
|
270
|
+
)
|
|
271
|
+
return TelegramMessage.from_dict(result)
|
|
272
|
+
|
|
273
|
+
async def delete_message(
|
|
274
|
+
self,
|
|
275
|
+
chat_id: int,
|
|
276
|
+
message_id: int,
|
|
277
|
+
) -> bool:
|
|
278
|
+
"""Delete a message.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
chat_id: Chat containing the message
|
|
282
|
+
message_id: ID of the message to delete
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
True if deletion was successful
|
|
286
|
+
"""
|
|
287
|
+
result = await self._request(
|
|
288
|
+
"deleteMessage",
|
|
289
|
+
chat_id=chat_id,
|
|
290
|
+
message_id=message_id,
|
|
291
|
+
)
|
|
292
|
+
return bool(result)
|
|
293
|
+
|
|
294
|
+
async def send_chat_action(
|
|
295
|
+
self,
|
|
296
|
+
chat_id: int,
|
|
297
|
+
action: str = "typing",
|
|
298
|
+
) -> bool:
|
|
299
|
+
"""Send a chat action (typing indicator, etc.).
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
chat_id: Target chat ID
|
|
303
|
+
action: Action type ("typing", "upload_document", etc.)
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
True if successful
|
|
307
|
+
"""
|
|
308
|
+
result = await self._request(
|
|
309
|
+
"sendChatAction",
|
|
310
|
+
chat_id=chat_id,
|
|
311
|
+
action=action,
|
|
312
|
+
)
|
|
313
|
+
return bool(result)
|
|
314
|
+
|
|
315
|
+
async def get_updates(
|
|
316
|
+
self,
|
|
317
|
+
offset: int | None = None,
|
|
318
|
+
timeout: int = LONG_POLL_TIMEOUT,
|
|
319
|
+
allowed_updates: list[str] | None = None,
|
|
320
|
+
) -> list[TelegramUpdate]:
|
|
321
|
+
"""Get updates (incoming messages) using long-polling.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
offset: Identifier of the first update to be returned
|
|
325
|
+
timeout: Timeout in seconds for long polling
|
|
326
|
+
allowed_updates: List of update types to receive
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
List of updates
|
|
330
|
+
"""
|
|
331
|
+
if allowed_updates is None:
|
|
332
|
+
allowed_updates = ["message"]
|
|
333
|
+
|
|
334
|
+
result = await self._request(
|
|
335
|
+
"getUpdates",
|
|
336
|
+
offset=offset,
|
|
337
|
+
timeout=timeout,
|
|
338
|
+
allowed_updates=allowed_updates,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return [TelegramUpdate.from_dict(u) for u in result]
|
|
342
|
+
|
|
343
|
+
async def poll_updates(
|
|
344
|
+
self,
|
|
345
|
+
offset: int = 0,
|
|
346
|
+
) -> AsyncIterator[TelegramUpdate]:
|
|
347
|
+
"""Continuously poll for updates.
|
|
348
|
+
|
|
349
|
+
This is an async generator that yields updates as they arrive.
|
|
350
|
+
It handles reconnection on errors.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
offset: Starting update offset
|
|
354
|
+
|
|
355
|
+
Yields:
|
|
356
|
+
TelegramUpdate instances
|
|
357
|
+
"""
|
|
358
|
+
current_offset = offset
|
|
359
|
+
|
|
360
|
+
while True:
|
|
361
|
+
try:
|
|
362
|
+
updates = await self.get_updates(
|
|
363
|
+
offset=current_offset + 1 if current_offset else None,
|
|
364
|
+
timeout=LONG_POLL_TIMEOUT,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
for update in updates:
|
|
368
|
+
current_offset = update.update_id
|
|
369
|
+
yield update
|
|
370
|
+
|
|
371
|
+
except httpx.TimeoutException:
|
|
372
|
+
# Normal timeout, continue polling
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
except httpx.HTTPError as e:
|
|
376
|
+
# Network error, wait and retry
|
|
377
|
+
await asyncio.sleep(5)
|
|
378
|
+
continue
|
|
379
|
+
|
|
380
|
+
except TelegramAPIError as e:
|
|
381
|
+
if e.error_code == 409:
|
|
382
|
+
# Conflict: another instance is polling
|
|
383
|
+
raise
|
|
384
|
+
# Other API errors, wait and retry
|
|
385
|
+
await asyncio.sleep(5)
|
|
386
|
+
continue
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
async def verify_token(token: str) -> TelegramUser | None:
|
|
390
|
+
"""Verify a bot token is valid.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
token: Bot token to verify
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
TelegramUser representing the bot if valid, None otherwise
|
|
397
|
+
"""
|
|
398
|
+
try:
|
|
399
|
+
async with TelegramBot(token) as bot:
|
|
400
|
+
return await bot.get_me()
|
|
401
|
+
except (TelegramAPIError, httpx.HTTPError):
|
|
402
|
+
return None
|