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,438 @@
1
+ """Diff rendering utilities for emdash CLI.
2
+
3
+ Provides GitHub-style diff display with line numbers and syntax highlighting.
4
+ """
5
+
6
+ import re
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ from rich.console import Console
12
+ from rich.syntax import Syntax
13
+ from rich.text import Text
14
+
15
+ from .design import Colors, EM_DASH, STATUS_ACTIVE
16
+
17
+ # File extension to lexer mapping
18
+ EXTENSION_LEXERS = {
19
+ ".py": "python",
20
+ ".js": "javascript",
21
+ ".jsx": "jsx",
22
+ ".ts": "typescript",
23
+ ".tsx": "tsx",
24
+ ".json": "json",
25
+ ".yaml": "yaml",
26
+ ".yml": "yaml",
27
+ ".md": "markdown",
28
+ ".html": "html",
29
+ ".css": "css",
30
+ ".scss": "scss",
31
+ ".sql": "sql",
32
+ ".sh": "bash",
33
+ ".bash": "bash",
34
+ ".zsh": "zsh",
35
+ ".rs": "rust",
36
+ ".go": "go",
37
+ ".java": "java",
38
+ ".kt": "kotlin",
39
+ ".swift": "swift",
40
+ ".rb": "ruby",
41
+ ".php": "php",
42
+ ".c": "c",
43
+ ".cpp": "cpp",
44
+ ".h": "c",
45
+ ".hpp": "cpp",
46
+ ".toml": "toml",
47
+ ".xml": "xml",
48
+ ".dockerfile": "dockerfile",
49
+ }
50
+
51
+
52
+ def get_lexer_for_file(filepath: str) -> str:
53
+ """Get the syntax lexer for a file based on extension."""
54
+ ext = Path(filepath).suffix.lower()
55
+
56
+ # Special case for Dockerfile
57
+ if Path(filepath).name.lower() == "dockerfile":
58
+ return "dockerfile"
59
+
60
+ return EXTENSION_LEXERS.get(ext, "text")
61
+
62
+
63
+ def render_diff(
64
+ diff_output: str,
65
+ console: Optional[Console] = None,
66
+ compact: bool = False,
67
+ max_lines: int = 50,
68
+ ) -> None:
69
+ """Render git diff output with line numbers and syntax highlighting.
70
+
71
+ Args:
72
+ diff_output: Raw git diff output
73
+ console: Rich console to render to
74
+ compact: If True, show compact view (fewer context lines)
75
+ max_lines: Maximum diff lines to show per file
76
+ """
77
+ if console is None:
78
+ console = Console()
79
+
80
+ # Parse diff into files
81
+ files = _parse_diff(diff_output)
82
+
83
+ for filepath, hunks in files.items():
84
+ _render_file_diff(console, filepath, hunks, compact, max_lines)
85
+
86
+
87
+ def render_file_change(
88
+ console: Console,
89
+ filepath: str,
90
+ old_content: str = "",
91
+ new_content: str = "",
92
+ diff_lines: Optional[list] = None,
93
+ compact: bool = True,
94
+ ) -> None:
95
+ """Render a file change as inline diff (for agent edits).
96
+
97
+ Args:
98
+ console: Rich console to render to
99
+ filepath: Path to the file
100
+ old_content: Original file content
101
+ new_content: New file content
102
+ diff_lines: Pre-computed diff lines (optional)
103
+ compact: If True, show compact view
104
+ """
105
+ # Try to get git diff if no diff_lines provided
106
+ if not diff_lines:
107
+ diff_lines = _get_git_diff_for_file(filepath)
108
+
109
+ # Count changes
110
+ additions = 0
111
+ deletions = 0
112
+
113
+ if diff_lines:
114
+ additions = sum(1 for l in diff_lines if l.startswith("+") and not l.startswith("+++"))
115
+ deletions = sum(1 for l in diff_lines if l.startswith("-") and not l.startswith("---"))
116
+ elif old_content and new_content:
117
+ old_lines = set(old_content.split("\n"))
118
+ new_lines = set(new_content.split("\n"))
119
+ additions = len(new_lines - old_lines)
120
+ deletions = len(old_lines - new_lines)
121
+
122
+ # Shorten path for display
123
+ display_path = filepath
124
+ if len(display_path) > 50:
125
+ display_path = "..." + display_path[-47:]
126
+
127
+ # Header line
128
+ header = Text()
129
+ header.append(f"{STATUS_ACTIVE} ", style=Colors.WARNING)
130
+ header.append("Update", style=f"{Colors.TEXT} bold")
131
+ header.append(f"({display_path})", style=Colors.MUTED)
132
+ console.print(header)
133
+
134
+ # Summary line
135
+ if additions or deletions:
136
+ summary = Text()
137
+ summary.append(" └ ", style=Colors.DIM)
138
+ if additions and deletions:
139
+ summary.append(f"Changed ", style=Colors.DIM)
140
+ summary.append(f"{additions + deletions}", style=Colors.TEXT)
141
+ summary.append(" lines", style=Colors.DIM)
142
+ elif additions:
143
+ summary.append("Added ", style=Colors.DIM)
144
+ summary.append(f"{additions}", style=Colors.SUCCESS)
145
+ summary.append(" lines", style=Colors.DIM)
146
+ else:
147
+ summary.append("Removed ", style=Colors.DIM)
148
+ summary.append(f"{deletions}", style=Colors.ERROR)
149
+ summary.append(" lines", style=Colors.DIM)
150
+ console.print(summary)
151
+ else:
152
+ # No diff available - just show modified
153
+ summary = Text()
154
+ summary.append(" └ ", style=Colors.DIM)
155
+ summary.append("modified", style=Colors.MUTED)
156
+ console.print(summary)
157
+
158
+ # Render diff lines with line numbers
159
+ if diff_lines:
160
+ _render_diff_lines_compact(console, filepath, diff_lines)
161
+
162
+
163
+ def _get_git_diff_for_file(filepath: str) -> list:
164
+ """Get git diff for a specific file.
165
+
166
+ Returns list of diff lines, or empty list if not available.
167
+ """
168
+ try:
169
+ # Try unstaged changes first
170
+ result = subprocess.run(
171
+ ["git", "diff", "--no-color", "--", filepath],
172
+ capture_output=True,
173
+ text=True,
174
+ timeout=5,
175
+ )
176
+ diff_output = result.stdout
177
+
178
+ # If no unstaged, try staged
179
+ if not diff_output:
180
+ result = subprocess.run(
181
+ ["git", "diff", "--staged", "--no-color", "--", filepath],
182
+ capture_output=True,
183
+ text=True,
184
+ timeout=5,
185
+ )
186
+ diff_output = result.stdout
187
+
188
+ if diff_output:
189
+ # Parse and return just the diff lines (skip headers)
190
+ lines = []
191
+ in_hunk = False
192
+ for line in diff_output.split("\n"):
193
+ if line.startswith("@@"):
194
+ in_hunk = True
195
+ lines.append(line)
196
+ elif in_hunk:
197
+ lines.append(line)
198
+ return lines
199
+
200
+ except Exception:
201
+ pass
202
+
203
+ return []
204
+
205
+
206
+ def _parse_diff(diff_output: str) -> dict:
207
+ """Parse git diff output into structured format.
208
+
209
+ Returns:
210
+ Dict mapping filepath to list of hunks, where each hunk is
211
+ (old_start, old_count, new_start, new_count, lines)
212
+ """
213
+ files = {}
214
+ current_file = None
215
+ current_hunks = []
216
+ current_hunk_lines = []
217
+ hunk_info = None
218
+
219
+ for line in diff_output.split("\n"):
220
+ if line.startswith("diff --git"):
221
+ # Save previous file
222
+ if current_file and current_hunks:
223
+ if current_hunk_lines and hunk_info:
224
+ current_hunks.append((hunk_info, current_hunk_lines))
225
+ files[current_file] = current_hunks
226
+
227
+ # Extract filename
228
+ parts = line.split(" b/")
229
+ current_file = parts[-1] if len(parts) > 1 else "unknown"
230
+ current_hunks = []
231
+ current_hunk_lines = []
232
+ hunk_info = None
233
+
234
+ elif line.startswith("@@"):
235
+ # New hunk - save previous
236
+ if current_hunk_lines and hunk_info:
237
+ current_hunks.append((hunk_info, current_hunk_lines))
238
+
239
+ # Parse hunk header: @@ -old_start,old_count +new_start,new_count @@
240
+ match = re.match(r"@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", line)
241
+ if match:
242
+ old_start = int(match.group(1))
243
+ old_count = int(match.group(2)) if match.group(2) else 1
244
+ new_start = int(match.group(3))
245
+ new_count = int(match.group(4)) if match.group(4) else 1
246
+ hunk_info = (old_start, old_count, new_start, new_count)
247
+ else:
248
+ hunk_info = (1, 0, 1, 0)
249
+
250
+ current_hunk_lines = []
251
+
252
+ elif current_file and hunk_info is not None:
253
+ if not line.startswith("---") and not line.startswith("+++"):
254
+ current_hunk_lines.append(line)
255
+
256
+ # Don't forget last file/hunk
257
+ if current_file:
258
+ if current_hunk_lines and hunk_info:
259
+ current_hunks.append((hunk_info, current_hunk_lines))
260
+ if current_hunks:
261
+ files[current_file] = current_hunks
262
+
263
+ return files
264
+
265
+
266
+ def _render_file_diff(
267
+ console: Console,
268
+ filepath: str,
269
+ hunks: list,
270
+ compact: bool,
271
+ max_lines: int,
272
+ ) -> None:
273
+ """Render diff for a single file with line numbers."""
274
+ # Count changes
275
+ additions = 0
276
+ deletions = 0
277
+ for _, lines in hunks:
278
+ for line in lines:
279
+ if line.startswith("+"):
280
+ additions += 1
281
+ elif line.startswith("-"):
282
+ deletions += 1
283
+
284
+ # File header
285
+ header = Text()
286
+ header.append(f"{EM_DASH * 3} ", style=Colors.DIM)
287
+ header.append(filepath, style=f"{Colors.PRIMARY} bold")
288
+ header.append(f" {EM_DASH * max(1, 60 - len(filepath))}", style=Colors.DIM)
289
+ console.print()
290
+ console.print(header)
291
+
292
+ # Stats line
293
+ stats = Text()
294
+ if additions:
295
+ stats.append(f"+{additions}", style=Colors.SUCCESS)
296
+ if additions and deletions:
297
+ stats.append(" ")
298
+ if deletions:
299
+ stats.append(f"-{deletions}", style=Colors.ERROR)
300
+ if additions or deletions:
301
+ console.print(stats)
302
+
303
+ # Get lexer for syntax highlighting
304
+ lexer = get_lexer_for_file(filepath)
305
+
306
+ # Render hunks
307
+ total_lines = 0
308
+ for hunk_info, lines in hunks:
309
+ if total_lines >= max_lines:
310
+ remaining = sum(len(h[1]) for h in hunks) - total_lines
311
+ if remaining > 0:
312
+ console.print(f" [{Colors.DIM}]... {remaining} more lines[/{Colors.DIM}]")
313
+ break
314
+
315
+ old_line, _, new_line, _ = hunk_info
316
+
317
+ for line in lines:
318
+ if total_lines >= max_lines:
319
+ break
320
+
321
+ if line.startswith("+"):
322
+ # Addition
323
+ line_text = Text()
324
+ line_text.append(f"{new_line:4} ", style=Colors.DIM)
325
+ line_text.append("+ ", style=f"{Colors.SUCCESS} bold")
326
+ _append_highlighted(line_text, line[1:], lexer, Colors.SUCCESS)
327
+ console.print(line_text)
328
+ new_line += 1
329
+ total_lines += 1
330
+
331
+ elif line.startswith("-"):
332
+ # Deletion
333
+ line_text = Text()
334
+ line_text.append(f"{old_line:4} ", style=Colors.DIM)
335
+ line_text.append("- ", style=f"{Colors.ERROR} bold")
336
+ _append_highlighted(line_text, line[1:], lexer, Colors.ERROR)
337
+ console.print(line_text)
338
+ old_line += 1
339
+ total_lines += 1
340
+
341
+ else:
342
+ # Context line
343
+ if not compact or total_lines < 3:
344
+ line_text = Text()
345
+ line_text.append(f"{new_line:4} ", style=Colors.DIM)
346
+ line_text.append(" ", style=Colors.DIM)
347
+ line_text.append(line[1:] if line.startswith(" ") else line, style=Colors.DIM)
348
+ console.print(line_text)
349
+ total_lines += 1
350
+
351
+ old_line += 1
352
+ new_line += 1
353
+
354
+ console.print()
355
+
356
+
357
+ def _render_diff_lines_compact(
358
+ console: Console,
359
+ filepath: str,
360
+ diff_lines: list,
361
+ max_lines: int = 8,
362
+ ) -> None:
363
+ """Render diff lines in compact format for inline display."""
364
+ lexer = get_lexer_for_file(filepath)
365
+
366
+ shown = 0
367
+ line_num = 1 # Approximate line number
368
+
369
+ for line in diff_lines:
370
+ if shown >= max_lines:
371
+ remaining = len(diff_lines) - shown
372
+ if remaining > 0:
373
+ console.print(f" [{Colors.DIM}]... {remaining} more lines[/{Colors.DIM}]")
374
+ break
375
+
376
+ if line.startswith("+") and not line.startswith("+++"):
377
+ line_text = Text()
378
+ line_text.append(f" {line_num:4} ", style=Colors.DIM)
379
+ line_text.append("+ ", style=f"{Colors.SUCCESS} bold")
380
+ _append_highlighted(line_text, line[1:], lexer, Colors.SUCCESS)
381
+ console.print(line_text)
382
+ shown += 1
383
+ line_num += 1
384
+
385
+ elif line.startswith("-") and not line.startswith("---"):
386
+ line_text = Text()
387
+ line_text.append(f" {line_num:4} ", style=Colors.DIM)
388
+ line_text.append("- ", style=f"{Colors.ERROR} bold")
389
+ _append_highlighted(line_text, line[1:], lexer, Colors.ERROR)
390
+ console.print(line_text)
391
+ shown += 1
392
+ # Don't increment line_num for deletions
393
+
394
+ elif not line.startswith("@@") and not line.startswith("---") and not line.startswith("+++"):
395
+ # Context line - show sparingly
396
+ if shown < 2:
397
+ line_text = Text()
398
+ line_text.append(f" {line_num:4} ", style=Colors.DIM)
399
+ line_text.append(" ", style=Colors.DIM)
400
+ content = line[1:] if line.startswith(" ") else line
401
+ line_text.append(content, style=Colors.DIM)
402
+ console.print(line_text)
403
+ shown += 1
404
+ line_num += 1
405
+
406
+
407
+ def _append_highlighted(text: Text, content: str, lexer: str, base_style: str) -> None:
408
+ """Append syntax-highlighted content to a Text object.
409
+
410
+ For simplicity, we apply the base style and let keywords stand out.
411
+ Full syntax highlighting would require more complex handling.
412
+ """
413
+ # Simple keyword highlighting for common languages
414
+ if lexer in ("python", "javascript", "typescript", "java", "go", "rust"):
415
+ keywords = {
416
+ "python": ["def", "class", "import", "from", "if", "else", "elif", "try", "except", "finally", "for", "while", "return", "yield", "with", "as", "None", "True", "False", "and", "or", "not", "in", "is", "lambda", "async", "await"],
417
+ "javascript": ["function", "const", "let", "var", "if", "else", "for", "while", "return", "import", "export", "from", "class", "new", "this", "async", "await", "try", "catch", "finally", "null", "undefined", "true", "false"],
418
+ "typescript": ["function", "const", "let", "var", "if", "else", "for", "while", "return", "import", "export", "from", "class", "new", "this", "async", "await", "try", "catch", "finally", "null", "undefined", "true", "false", "interface", "type", "enum"],
419
+ "java": ["public", "private", "protected", "class", "interface", "extends", "implements", "if", "else", "for", "while", "return", "new", "this", "try", "catch", "finally", "null", "true", "false", "void", "static", "final"],
420
+ "go": ["func", "package", "import", "if", "else", "for", "return", "var", "const", "type", "struct", "interface", "nil", "true", "false", "go", "defer", "chan", "select", "case"],
421
+ "rust": ["fn", "let", "mut", "if", "else", "for", "while", "return", "use", "mod", "pub", "struct", "impl", "trait", "enum", "match", "Some", "None", "Ok", "Err", "self", "Self", "async", "await"],
422
+ }
423
+
424
+ kw_list = keywords.get(lexer, [])
425
+ if kw_list:
426
+ # Simple word-based highlighting
427
+ words = re.split(r'(\s+|\W)', content)
428
+ for word in words:
429
+ if word in kw_list:
430
+ text.append(word, style=f"{Colors.WARNING} bold")
431
+ elif word.startswith('"') or word.startswith("'") or word.startswith('`'):
432
+ text.append(word, style=Colors.SUCCESS)
433
+ else:
434
+ text.append(word, style=base_style)
435
+ return
436
+
437
+ # Default: just apply base style
438
+ text.append(content, style=base_style)
@@ -0,0 +1 @@
1
+ """EmDash CLI integrations."""
@@ -0,0 +1,15 @@
1
+ """Telegram integration for EmDash CLI.
2
+
3
+ This module provides integration with Telegram Bot API to receive
4
+ and respond to messages via Telegram while running the local EmDash agent.
5
+ """
6
+
7
+ from .config import TelegramConfig, get_config, save_config
8
+ from .bot import TelegramBot
9
+
10
+ __all__ = [
11
+ "TelegramConfig",
12
+ "TelegramBot",
13
+ "get_config",
14
+ "save_config",
15
+ ]