emdash-cli 0.1.8__tar.gz → 0.1.30__tar.gz
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-0.1.8 → emdash_cli-0.1.30}/PKG-INFO +4 -2
- emdash_cli-0.1.30/emdash_cli/__init__.py +8 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/client.py +42 -20
- emdash_cli-0.1.30/emdash_cli/clipboard.py +123 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/__init__.py +2 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/agent.py +212 -236
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/index.py +76 -8
- emdash_cli-0.1.30/emdash_cli/commands/skills.py +337 -0
- emdash_cli-0.1.30/emdash_cli/keyboard.py +146 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/main.py +5 -1
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/sse_renderer.py +124 -21
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/pyproject.toml +8 -2
- emdash_cli-0.1.8/emdash_cli/__init__.py +0 -3
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/analyze.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/auth.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/db.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/embed.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/plan.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/projectmd.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/research.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/rules.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/search.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/server.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/spec.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/swarm.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/tasks.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/team.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/server_manager.py +0 -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,10 +1,16 @@
|
|
|
1
1
|
"""HTTP client for emdash-core API."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from typing import Any, Iterator, Optional
|
|
4
5
|
|
|
5
6
|
import httpx
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
def _get_max_iterations() -> int:
|
|
10
|
+
"""Get max iterations from env var with default."""
|
|
11
|
+
return int(os.getenv("EMDASH_MAX_ITERATIONS", "100"))
|
|
12
|
+
|
|
13
|
+
|
|
8
14
|
class EmdashClient:
|
|
9
15
|
"""HTTP client for interacting with emdash-core API.
|
|
10
16
|
|
|
@@ -44,8 +50,9 @@ class EmdashClient:
|
|
|
44
50
|
message: str,
|
|
45
51
|
model: Optional[str] = None,
|
|
46
52
|
session_id: Optional[str] = None,
|
|
47
|
-
max_iterations: int =
|
|
53
|
+
max_iterations: int = _get_max_iterations(),
|
|
48
54
|
options: Optional[dict] = None,
|
|
55
|
+
images: Optional[list[dict]] = None,
|
|
49
56
|
) -> Iterator[str]:
|
|
50
57
|
"""Stream agent chat response via SSE.
|
|
51
58
|
|
|
@@ -55,6 +62,7 @@ class EmdashClient:
|
|
|
55
62
|
session_id: Session ID for continuity (optional)
|
|
56
63
|
max_iterations: Max agent iterations
|
|
57
64
|
options: Additional options (mode, save, no_graph_tools, etc.)
|
|
65
|
+
images: List of images [{"data": base64_str, "format": "png"}]
|
|
58
66
|
|
|
59
67
|
Yields:
|
|
60
68
|
SSE lines from the response
|
|
@@ -81,40 +89,54 @@ class EmdashClient:
|
|
|
81
89
|
payload["model"] = model
|
|
82
90
|
if session_id:
|
|
83
91
|
payload["session_id"] = session_id
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
if images:
|
|
93
|
+
payload["images"] = images
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
with self._client.stream(
|
|
97
|
+
"POST",
|
|
98
|
+
f"{self.base_url}/api/agent/chat",
|
|
99
|
+
json=payload,
|
|
100
|
+
) as response:
|
|
101
|
+
response.raise_for_status()
|
|
102
|
+
for line in response.iter_lines():
|
|
103
|
+
yield line
|
|
104
|
+
except GeneratorExit:
|
|
105
|
+
# Stream was closed early (interrupted)
|
|
106
|
+
pass
|
|
93
107
|
|
|
94
108
|
def agent_continue_stream(
|
|
95
109
|
self,
|
|
96
110
|
session_id: str,
|
|
97
111
|
message: str,
|
|
112
|
+
images: Optional[list[dict]] = None,
|
|
98
113
|
) -> Iterator[str]:
|
|
99
114
|
"""Continue an existing agent session.
|
|
100
115
|
|
|
101
116
|
Args:
|
|
102
117
|
session_id: Existing session ID
|
|
103
118
|
message: Continuation message
|
|
119
|
+
images: List of images [{"data": base64_str, "format": "png"}]
|
|
104
120
|
|
|
105
121
|
Yields:
|
|
106
122
|
SSE lines from the response
|
|
107
123
|
"""
|
|
108
124
|
payload = {"message": message}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
if images:
|
|
126
|
+
payload["images"] = images
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
with self._client.stream(
|
|
130
|
+
"POST",
|
|
131
|
+
f"{self.base_url}/api/agent/chat/{session_id}/continue",
|
|
132
|
+
json=payload,
|
|
133
|
+
) as response:
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
for line in response.iter_lines():
|
|
136
|
+
yield line
|
|
137
|
+
except GeneratorExit:
|
|
138
|
+
# Stream was closed early (interrupted)
|
|
139
|
+
pass
|
|
118
140
|
|
|
119
141
|
def list_sessions(self) -> list[dict]:
|
|
120
142
|
"""List active agent sessions.
|
|
@@ -476,7 +498,7 @@ class EmdashClient:
|
|
|
476
498
|
def research_stream(
|
|
477
499
|
self,
|
|
478
500
|
goal: str,
|
|
479
|
-
max_iterations: int =
|
|
501
|
+
max_iterations: int = _get_max_iterations(),
|
|
480
502
|
budget: int = 50,
|
|
481
503
|
model: Optional[str] = None,
|
|
482
504
|
) -> Iterator[str]:
|
|
@@ -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
|
|
@@ -10,6 +10,7 @@ from .plan import plan
|
|
|
10
10
|
from .rules import rules
|
|
11
11
|
from .search import search
|
|
12
12
|
from .server import server
|
|
13
|
+
from .skills import skills
|
|
13
14
|
from .team import team
|
|
14
15
|
from .swarm import swarm
|
|
15
16
|
from .projectmd import projectmd
|
|
@@ -28,6 +29,7 @@ __all__ = [
|
|
|
28
29
|
"rules",
|
|
29
30
|
"search",
|
|
30
31
|
"server",
|
|
32
|
+
"skills",
|
|
31
33
|
"team",
|
|
32
34
|
"swarm",
|
|
33
35
|
"projectmd",
|