emdash-cli 0.1.25__py3-none-any.whl → 0.1.30__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 CHANGED
@@ -52,6 +52,7 @@ class EmdashClient:
52
52
  session_id: Optional[str] = None,
53
53
  max_iterations: int = _get_max_iterations(),
54
54
  options: Optional[dict] = None,
55
+ images: Optional[list[dict]] = None,
55
56
  ) -> Iterator[str]:
56
57
  """Stream agent chat response via SSE.
57
58
 
@@ -61,6 +62,7 @@ class EmdashClient:
61
62
  session_id: Session ID for continuity (optional)
62
63
  max_iterations: Max agent iterations
63
64
  options: Additional options (mode, save, no_graph_tools, etc.)
65
+ images: List of images [{"data": base64_str, "format": "png"}]
64
66
 
65
67
  Yields:
66
68
  SSE lines from the response
@@ -87,6 +89,8 @@ class EmdashClient:
87
89
  payload["model"] = model
88
90
  if session_id:
89
91
  payload["session_id"] = session_id
92
+ if images:
93
+ payload["images"] = images
90
94
 
91
95
  try:
92
96
  with self._client.stream(
@@ -105,17 +109,21 @@ class EmdashClient:
105
109
  self,
106
110
  session_id: str,
107
111
  message: str,
112
+ images: Optional[list[dict]] = None,
108
113
  ) -> Iterator[str]:
109
114
  """Continue an existing agent session.
110
115
 
111
116
  Args:
112
117
  session_id: Existing session ID
113
118
  message: Continuation message
119
+ images: List of images [{"data": base64_str, "format": "png"}]
114
120
 
115
121
  Yields:
116
122
  SSE lines from the response
117
123
  """
118
124
  payload = {"message": message}
125
+ if images:
126
+ payload["images"] = images
119
127
 
120
128
  try:
121
129
  with self._client.stream(
@@ -0,0 +1,123 @@
1
+ """Clipboard utilities for image handling."""
2
+
3
+ import base64
4
+ import io
5
+ from typing import Optional, Tuple
6
+
7
+
8
+ def get_clipboard_image() -> Optional[Tuple[str, str]]:
9
+ """Get image from clipboard if available.
10
+
11
+ Returns:
12
+ Tuple of (base64_data, format) if image found, None otherwise.
13
+ """
14
+ try:
15
+ from PIL import ImageGrab, Image
16
+
17
+ # Try to grab image from clipboard
18
+ image = ImageGrab.grabclipboard()
19
+
20
+ if image is None:
21
+ return None
22
+
23
+ # Handle list of file paths (Windows)
24
+ if isinstance(image, list):
25
+ # It's a list of file paths
26
+ if image and isinstance(image[0], str):
27
+ try:
28
+ image = Image.open(image[0])
29
+ except Exception:
30
+ return None
31
+ else:
32
+ return None
33
+
34
+ # Convert to PNG bytes
35
+ if isinstance(image, Image.Image):
36
+ buffer = io.BytesIO()
37
+ # Convert to RGB if necessary (for RGBA images)
38
+ if image.mode in ('RGBA', 'LA') or (image.mode == 'P' and 'transparency' in image.info):
39
+ # Keep as PNG to preserve transparency
40
+ image.save(buffer, format='PNG')
41
+ img_format = 'png'
42
+ else:
43
+ # Convert to JPEG for smaller size
44
+ if image.mode != 'RGB':
45
+ image = image.convert('RGB')
46
+ image.save(buffer, format='JPEG', quality=85)
47
+ img_format = 'jpeg'
48
+
49
+ buffer.seek(0)
50
+ base64_data = base64.b64encode(buffer.read()).decode('utf-8')
51
+ return base64_data, img_format
52
+
53
+ except ImportError:
54
+ # PIL not available
55
+ return None
56
+ except Exception:
57
+ # Any other error (no clipboard access, etc.)
58
+ return None
59
+
60
+ return None
61
+
62
+
63
+ def get_image_from_path(path: str) -> Optional[Tuple[str, str]]:
64
+ """Load image from file path.
65
+
66
+ Args:
67
+ path: Path to image file
68
+
69
+ Returns:
70
+ Tuple of (base64_data, format) if successful, None otherwise.
71
+ """
72
+ try:
73
+ from PIL import Image
74
+
75
+ image = Image.open(path)
76
+ buffer = io.BytesIO()
77
+
78
+ # Determine format from file extension
79
+ ext = path.lower().split('.')[-1]
80
+ if ext in ('jpg', 'jpeg'):
81
+ if image.mode != 'RGB':
82
+ image = image.convert('RGB')
83
+ image.save(buffer, format='JPEG', quality=85)
84
+ img_format = 'jpeg'
85
+ elif ext == 'png':
86
+ image.save(buffer, format='PNG')
87
+ img_format = 'png'
88
+ elif ext == 'gif':
89
+ image.save(buffer, format='GIF')
90
+ img_format = 'gif'
91
+ elif ext == 'webp':
92
+ image.save(buffer, format='WEBP')
93
+ img_format = 'webp'
94
+ else:
95
+ # Default to PNG
96
+ image.save(buffer, format='PNG')
97
+ img_format = 'png'
98
+
99
+ buffer.seek(0)
100
+ base64_data = base64.b64encode(buffer.read()).decode('utf-8')
101
+ return base64_data, img_format
102
+
103
+ except Exception:
104
+ return None
105
+
106
+
107
+ def get_image_dimensions(base64_data: str) -> Optional[Tuple[int, int]]:
108
+ """Get dimensions of base64-encoded image.
109
+
110
+ Args:
111
+ base64_data: Base64-encoded image data
112
+
113
+ Returns:
114
+ Tuple of (width, height) if successful, None otherwise.
115
+ """
116
+ try:
117
+ from PIL import Image
118
+
119
+ image_bytes = base64.b64decode(base64_data)
120
+ image = Image.open(io.BytesIO(image_bytes))
121
+ return image.size
122
+ except Exception:
123
+ return None
@@ -465,12 +465,15 @@ def _run_interactive(
465
465
  current_mode = AgentMode(options.get("mode", "code"))
466
466
  session_id = None
467
467
  current_spec = None
468
+ # Attached images for next message
469
+ attached_images: list[dict] = []
468
470
 
469
471
  # Style for prompt
470
472
  PROMPT_STYLE = Style.from_dict({
471
473
  "prompt.mode.plan": "#ffcc00 bold",
472
474
  "prompt.mode.code": "#00cc66 bold",
473
475
  "prompt.prefix": "#888888",
476
+ "prompt.image": "#00ccff",
474
477
  "completion-menu": "bg:#1a1a2e #ffffff",
475
478
  "completion-menu.completion": "bg:#1a1a2e #ffffff",
476
479
  "completion-menu.completion.current": "bg:#4a4a6e #ffffff bold",
@@ -517,6 +520,22 @@ def _run_interactive(
517
520
  """Insert a newline character with Alt+Enter or Ctrl+J."""
518
521
  event.current_buffer.insert_text("\n")
519
522
 
523
+ @kb.add("c-v") # Ctrl+V to paste (check for images)
524
+ def paste_with_image_check(event):
525
+ """Paste text or attach image from clipboard."""
526
+ nonlocal attached_images
527
+ from ..clipboard import get_clipboard_image
528
+
529
+ # Try to get image from clipboard
530
+ image_data = get_clipboard_image()
531
+ if image_data:
532
+ base64_data, img_format = image_data
533
+ attached_images.append({"data": base64_data, "format": img_format})
534
+ console.print(f"[green]📎 Image attached[/green] [dim]({img_format})[/dim]")
535
+ else:
536
+ # No image, do normal paste
537
+ event.current_buffer.paste_clipboard_data(event.app.clipboard.get_data())
538
+
520
539
  session = PromptSession(
521
540
  history=history,
522
541
  completer=SlashCommandCompleter(),
@@ -528,14 +547,14 @@ def _run_interactive(
528
547
  )
529
548
 
530
549
  def get_prompt():
531
- """Get formatted prompt based on current mode."""
532
- mode_colors = {
533
- AgentMode.PLAN: "class:prompt.mode.plan",
534
- AgentMode.CODE: "class:prompt.mode.code",
535
- }
536
- mode_name = current_mode.value
537
- color_class = mode_colors.get(current_mode, "class:prompt.mode.code")
538
- return [(color_class, f"[{mode_name}]"), ("", " "), ("class:prompt.prefix", "> ")]
550
+ """Get formatted prompt."""
551
+ nonlocal attached_images
552
+ parts = []
553
+ # Add image indicator if images attached
554
+ if attached_images:
555
+ parts.append(("class:prompt.image", f"📎{len(attached_images)} "))
556
+ parts.append(("class:prompt.prefix", "> "))
557
+ return parts
539
558
 
540
559
  def show_help():
541
560
  """Show available commands."""
@@ -695,14 +714,13 @@ def _run_interactive(
695
714
  except Exception:
696
715
  pass
697
716
 
717
+ # Welcome banner
698
718
  console.print()
699
- console.print(f"[bold cyan]EmDash Agent[/bold cyan] [dim]v{__version__}[/dim]")
700
- console.print(f"Mode: [bold]{current_mode.value}[/bold] | Model: [dim]{model or 'default'}[/dim]")
719
+ console.print(f"[bold cyan]Emdash Code[/bold cyan] [dim]v{__version__}[/dim]")
701
720
  if git_repo:
702
- console.print(f"Repo: [bold green]{git_repo}[/bold green] | Path: [dim]{cwd}[/dim]")
721
+ console.print(f"[dim]Repo:[/dim] [bold green]{git_repo}[/bold green] [dim]| Mode:[/dim] [bold]{current_mode.value}[/bold] [dim]| Model:[/dim] {model or 'default'}")
703
722
  else:
704
- console.print(f"Path: [dim]{cwd}[/dim]")
705
- console.print("Type your task or [cyan]/help[/cyan] for commands. Use Ctrl+C to exit.")
723
+ console.print(f"[dim]Mode:[/dim] [bold]{current_mode.value}[/bold] [dim]| Model:[/dim] {model or 'default'}")
706
724
  console.print()
707
725
 
708
726
  while True:
@@ -731,16 +749,25 @@ def _run_interactive(
731
749
 
732
750
  # Run agent with current mode
733
751
  try:
752
+ # Prepare images for API call
753
+ images_to_send = attached_images if attached_images else None
754
+
734
755
  if session_id:
735
- stream = client.agent_continue_stream(session_id, user_input)
756
+ stream = client.agent_continue_stream(
757
+ session_id, user_input, images=images_to_send
758
+ )
736
759
  else:
737
760
  stream = client.agent_chat_stream(
738
761
  message=user_input,
739
762
  model=model,
740
763
  max_iterations=max_iterations,
741
764
  options=request_options,
765
+ images=images_to_send,
742
766
  )
743
767
 
768
+ # Clear attached images after sending
769
+ attached_images = []
770
+
744
771
  # Render the stream and capture any spec output
745
772
  result = _render_with_interrupt(renderer, stream)
746
773
 
@@ -611,7 +611,6 @@ class SSERenderer:
611
611
  entities_found = adding.get("entities_found", 0)
612
612
  context_tokens = adding.get("context_tokens", 0)
613
613
  context_breakdown = adding.get("context_breakdown", {})
614
- largest_messages = adding.get("largest_messages", [])
615
614
 
616
615
  # Get reading stats
617
616
  item_count = reading.get("item_count", 0)
@@ -636,15 +635,6 @@ class SSERenderer:
636
635
  if breakdown_parts:
637
636
  self.console.print(f" [dim]Breakdown: {' | '.join(breakdown_parts)}[/dim]")
638
637
 
639
- # Show largest messages (context hogs)
640
- if largest_messages:
641
- self.console.print(f" [yellow]Largest messages:[/yellow]")
642
- for msg in largest_messages[:5]:
643
- label = msg.get("label", "unknown")
644
- tokens = msg.get("tokens", 0)
645
- preview = msg.get("preview", "")[:50].replace("\n", " ")
646
- self.console.print(f" [dim]{tokens:,} tokens[/dim] - {label}: {preview}...")
647
-
648
638
  # Show other stats
649
639
  stats = []
650
640
  if step_count > 0:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: emdash-cli
3
- Version: 0.1.25
3
+ Version: 0.1.30
4
4
  Summary: EmDash CLI - Command-line interface for code intelligence
5
5
  Author: Em Dash Team
6
6
  Requires-Python: >=3.10,<4.0
@@ -10,8 +10,10 @@ Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: Programming Language :: Python :: 3.13
12
12
  Classifier: Programming Language :: Python :: 3.14
13
+ Provides-Extra: images
13
14
  Requires-Dist: click (>=8.1.7,<9.0.0)
14
- Requires-Dist: emdash-core (>=0.1.25)
15
+ Requires-Dist: emdash-core (>=0.1.30)
15
16
  Requires-Dist: httpx (>=0.25.0)
17
+ Requires-Dist: pillow (>=10.0.0) ; extra == "images"
16
18
  Requires-Dist: prompt_toolkit (>=3.0.43,<4.0.0)
17
19
  Requires-Dist: rich (>=13.7.0)
@@ -1,7 +1,8 @@
1
1
  emdash_cli/__init__.py,sha256=Rnn2O7B8OCEKlVtNRbWOU2-GN75_KLmhEJgOZzY-KwE,232
2
- emdash_cli/client.py,sha256=sPgX2CEfiHc4TXN9TYqFhc8DVKgPyk20Efm66KQAOhE,16761
2
+ emdash_cli/client.py,sha256=aQO_wF4XQaqig6RWhTA3FIJslweo7LsZHCk_GBVGdvw,17117
3
+ emdash_cli/clipboard.py,sha256=hcg5sbIhbixqzpJdonoFLGBlSo2AKjplNrWy5PGnqaY,3564
3
4
  emdash_cli/commands/__init__.py,sha256=D9edXBHm69tueUtE4DggTA1_Yjsl9YZaKjBVDY2D_gQ,712
4
- emdash_cli/commands/agent.py,sha256=RCgSYW6OBx3UFGVAM_byhyrtBKlsXN2xfJaT3w51uDY,27735
5
+ emdash_cli/commands/agent.py,sha256=dLNNyJNL9uzZcBapzFghdjb-Xa27PglpbFNAMPGa3qc,28772
5
6
  emdash_cli/commands/analyze.py,sha256=c9ztbv0Ra7g2AlDmMOy-9L51fDVuoqbuzxnRfomoFIQ,4403
6
7
  emdash_cli/commands/auth.py,sha256=SpWdqO1bJCgt4x1B4Pr7hNOucwTuBFJ1oGPOzXtvwZM,3816
7
8
  emdash_cli/commands/db.py,sha256=nZK7gLDVE2lAQVYrMx6Swscml5OAtkbg-EcSNSvRIlA,2922
@@ -21,8 +22,8 @@ emdash_cli/commands/team.py,sha256=K1-IJg6iG-9HMF_3JmpNDlNs1PYbb-ThFHU9KU_jKRo,1
21
22
  emdash_cli/keyboard.py,sha256=haYYAuhYGtdjomzhIFy_3Z3eN3BXfMdb4uRQjwB0tbk,4593
22
23
  emdash_cli/main.py,sha256=c-faWp-jzf9a0BbXhVoPvPQfGWSryXpYfswehqZCYPM,2593
23
24
  emdash_cli/server_manager.py,sha256=RrLteSHUmcFV4cyHJAEmgM9qHru2mJS08QNLWno6Y3Y,7051
24
- emdash_cli/sse_renderer.py,sha256=PEbD53ZohMp9yvii_1ELGwVKb8nnA_n17jICeaURkuY,23738
25
- emdash_cli-0.1.25.dist-info/METADATA,sha256=RFuFUhHJlRcpfzwztnQXTWheB853OVlyWd1tAIzRKsE,662
26
- emdash_cli-0.1.25.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
27
- emdash_cli-0.1.25.dist-info/entry_points.txt,sha256=31CuYD0k-tM8csFWDunc-JoZTxXaifj3oIXz4V0p6F0,122
28
- emdash_cli-0.1.25.dist-info/RECORD,,
25
+ emdash_cli/sse_renderer.py,sha256=aDOoHKglOkaYEXuKg937mH6yFPDxjU7Rqa_-APyM9Dc,23215
26
+ emdash_cli-0.1.30.dist-info/METADATA,sha256=czNXf-GzyfHKHe8g8Dfd7Bm7kI189H-VLRpFDVgtejc,738
27
+ emdash_cli-0.1.30.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
28
+ emdash_cli-0.1.30.dist-info/entry_points.txt,sha256=31CuYD0k-tM8csFWDunc-JoZTxXaifj3oIXz4V0p6F0,122
29
+ emdash_cli-0.1.30.dist-info/RECORD,,