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.
Files changed (28) hide show
  1. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/PKG-INFO +4 -2
  2. emdash_cli-0.1.30/emdash_cli/__init__.py +8 -0
  3. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/client.py +42 -20
  4. emdash_cli-0.1.30/emdash_cli/clipboard.py +123 -0
  5. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/__init__.py +2 -0
  6. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/agent.py +212 -236
  7. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/index.py +76 -8
  8. emdash_cli-0.1.30/emdash_cli/commands/skills.py +337 -0
  9. emdash_cli-0.1.30/emdash_cli/keyboard.py +146 -0
  10. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/main.py +5 -1
  11. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/sse_renderer.py +124 -21
  12. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/pyproject.toml +8 -2
  13. emdash_cli-0.1.8/emdash_cli/__init__.py +0 -3
  14. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/analyze.py +0 -0
  15. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/auth.py +0 -0
  16. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/db.py +0 -0
  17. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/embed.py +0 -0
  18. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/plan.py +0 -0
  19. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/projectmd.py +0 -0
  20. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/research.py +0 -0
  21. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/rules.py +0 -0
  22. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/search.py +0 -0
  23. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/server.py +0 -0
  24. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/spec.py +0 -0
  25. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/swarm.py +0 -0
  26. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/tasks.py +0 -0
  27. {emdash_cli-0.1.8 → emdash_cli-0.1.30}/emdash_cli/commands/team.py +0 -0
  28. {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.8
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.8)
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)
@@ -0,0 +1,8 @@
1
+ """EmDash CLI - Command-line interface for code intelligence."""
2
+
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ try:
6
+ __version__ = version("emdash-cli")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0-dev"
@@ -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 = 20,
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
- with self._client.stream(
86
- "POST",
87
- f"{self.base_url}/api/agent/chat",
88
- json=payload,
89
- ) as response:
90
- response.raise_for_status()
91
- for line in response.iter_lines():
92
- yield line
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
- with self._client.stream(
111
- "POST",
112
- f"{self.base_url}/api/agent/chat/{session_id}/continue",
113
- json=payload,
114
- ) as response:
115
- response.raise_for_status()
116
- for line in response.iter_lines():
117
- yield line
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 = 50,
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",