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 +8 -0
- emdash_cli/clipboard.py +123 -0
- emdash_cli/commands/agent.py +41 -14
- emdash_cli/sse_renderer.py +0 -10
- {emdash_cli-0.1.25.dist-info → emdash_cli-0.1.30.dist-info}/METADATA +4 -2
- {emdash_cli-0.1.25.dist-info → emdash_cli-0.1.30.dist-info}/RECORD +8 -7
- {emdash_cli-0.1.25.dist-info → emdash_cli-0.1.30.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.25.dist-info → emdash_cli-0.1.30.dist-info}/entry_points.txt +0 -0
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(
|
emdash_cli/clipboard.py
ADDED
|
@@ -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
|
emdash_cli/commands/agent.py
CHANGED
|
@@ -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
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
return
|
|
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]
|
|
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] |
|
|
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"
|
|
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(
|
|
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
|
|
emdash_cli/sse_renderer.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
25
|
-
emdash_cli-0.1.
|
|
26
|
-
emdash_cli-0.1.
|
|
27
|
-
emdash_cli-0.1.
|
|
28
|
-
emdash_cli-0.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|