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.
Files changed (50) hide show
  1. emdash_cli/client.py +41 -22
  2. emdash_cli/clipboard.py +30 -61
  3. emdash_cli/commands/__init__.py +2 -2
  4. emdash_cli/commands/agent/__init__.py +14 -0
  5. emdash_cli/commands/agent/cli.py +100 -0
  6. emdash_cli/commands/agent/constants.py +63 -0
  7. emdash_cli/commands/agent/file_utils.py +178 -0
  8. emdash_cli/commands/agent/handlers/__init__.py +51 -0
  9. emdash_cli/commands/agent/handlers/agents.py +449 -0
  10. emdash_cli/commands/agent/handlers/auth.py +69 -0
  11. emdash_cli/commands/agent/handlers/doctor.py +319 -0
  12. emdash_cli/commands/agent/handlers/hooks.py +121 -0
  13. emdash_cli/commands/agent/handlers/index.py +183 -0
  14. emdash_cli/commands/agent/handlers/mcp.py +183 -0
  15. emdash_cli/commands/agent/handlers/misc.py +319 -0
  16. emdash_cli/commands/agent/handlers/registry.py +72 -0
  17. emdash_cli/commands/agent/handlers/rules.py +411 -0
  18. emdash_cli/commands/agent/handlers/sessions.py +168 -0
  19. emdash_cli/commands/agent/handlers/setup.py +715 -0
  20. emdash_cli/commands/agent/handlers/skills.py +478 -0
  21. emdash_cli/commands/agent/handlers/telegram.py +475 -0
  22. emdash_cli/commands/agent/handlers/todos.py +119 -0
  23. emdash_cli/commands/agent/handlers/verify.py +653 -0
  24. emdash_cli/commands/agent/help.py +236 -0
  25. emdash_cli/commands/agent/interactive.py +842 -0
  26. emdash_cli/commands/agent/menus.py +760 -0
  27. emdash_cli/commands/agent/onboarding.py +619 -0
  28. emdash_cli/commands/agent/session_restore.py +210 -0
  29. emdash_cli/commands/agent.py +7 -1321
  30. emdash_cli/commands/index.py +111 -13
  31. emdash_cli/commands/registry.py +635 -0
  32. emdash_cli/commands/server.py +99 -40
  33. emdash_cli/commands/skills.py +72 -6
  34. emdash_cli/design.py +328 -0
  35. emdash_cli/diff_renderer.py +438 -0
  36. emdash_cli/integrations/__init__.py +1 -0
  37. emdash_cli/integrations/telegram/__init__.py +15 -0
  38. emdash_cli/integrations/telegram/bot.py +402 -0
  39. emdash_cli/integrations/telegram/bridge.py +865 -0
  40. emdash_cli/integrations/telegram/config.py +155 -0
  41. emdash_cli/integrations/telegram/formatter.py +385 -0
  42. emdash_cli/main.py +52 -2
  43. emdash_cli/server_manager.py +70 -10
  44. emdash_cli/sse_renderer.py +659 -167
  45. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/METADATA +2 -4
  46. emdash_cli-0.1.67.dist-info/RECORD +63 -0
  47. emdash_cli/commands/swarm.py +0 -86
  48. emdash_cli-0.1.35.dist-info/RECORD +0 -30
  49. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/WHEEL +0 -0
  50. {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