emdash-cli 0.1.8__tar.gz → 0.1.60__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.60}/PKG-INFO +2 -2
- emdash_cli-0.1.60/emdash_cli/__init__.py +23 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/client.py +203 -41
- emdash_cli-0.1.60/emdash_cli/clipboard.py +92 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/__init__.py +4 -2
- emdash_cli-0.1.60/emdash_cli/commands/agent/__init__.py +14 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/cli.py +100 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/constants.py +61 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/file_utils.py +178 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/__init__.py +49 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/agents.py +449 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/auth.py +69 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/doctor.py +319 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/hooks.py +121 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/index.py +183 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/mcp.py +183 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/misc.py +319 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/registry.py +72 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/rules.py +411 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/sessions.py +168 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/setup.py +715 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/skills.py +478 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/todos.py +119 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/verify.py +653 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/help.py +236 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/interactive.py +837 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/menus.py +760 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/onboarding.py +619 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent/session_restore.py +210 -0
- emdash_cli-0.1.60/emdash_cli/commands/agent.py +10 -0
- emdash_cli-0.1.60/emdash_cli/commands/index.py +300 -0
- emdash_cli-0.1.60/emdash_cli/commands/registry.py +635 -0
- emdash_cli-0.1.60/emdash_cli/commands/server.py +176 -0
- emdash_cli-0.1.60/emdash_cli/commands/skills.py +403 -0
- emdash_cli-0.1.60/emdash_cli/design.py +328 -0
- emdash_cli-0.1.60/emdash_cli/diff_renderer.py +438 -0
- emdash_cli-0.1.60/emdash_cli/keyboard.py +146 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/main.py +7 -3
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/server_manager.py +70 -10
- emdash_cli-0.1.60/emdash_cli/session_store.py +321 -0
- emdash_cli-0.1.60/emdash_cli/sse_renderer.py +1240 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/pyproject.toml +4 -3
- emdash_cli-0.1.8/emdash_cli/__init__.py +0 -3
- emdash_cli-0.1.8/emdash_cli/commands/agent.py +0 -883
- emdash_cli-0.1.8/emdash_cli/commands/index.py +0 -134
- emdash_cli-0.1.8/emdash_cli/commands/server.py +0 -117
- emdash_cli-0.1.8/emdash_cli/commands/swarm.py +0 -86
- emdash_cli-0.1.8/emdash_cli/sse_renderer.py +0 -545
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/analyze.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/auth.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/db.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/embed.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/plan.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/projectmd.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/research.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/rules.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/search.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/spec.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/tasks.py +0 -0
- {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/team.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.60
|
|
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
|
|
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
13
13
|
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
14
|
-
Requires-Dist: emdash-core (>=0.1.
|
|
14
|
+
Requires-Dist: emdash-core (>=0.1.60)
|
|
15
15
|
Requires-Dist: httpx (>=0.25.0)
|
|
16
16
|
Requires-Dist: prompt_toolkit (>=3.0.43,<4.0.0)
|
|
17
17
|
Requires-Dist: rich (>=13.7.0)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""EmDash CLI - Command-line interface for code intelligence."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# Load .env files early so env vars are available for server subprocess
|
|
7
|
+
try:
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
# Try to find .env in current dir or parent dirs
|
|
10
|
+
current = Path.cwd()
|
|
11
|
+
for _ in range(5):
|
|
12
|
+
env_path = current / ".env"
|
|
13
|
+
if env_path.exists():
|
|
14
|
+
load_dotenv(env_path, override=True)
|
|
15
|
+
break
|
|
16
|
+
current = current.parent
|
|
17
|
+
except ImportError:
|
|
18
|
+
pass # dotenv not installed
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
__version__ = version("emdash-cli")
|
|
22
|
+
except PackageNotFoundError:
|
|
23
|
+
__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,10 @@ 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,
|
|
56
|
+
history: Optional[list[dict]] = None,
|
|
49
57
|
) -> Iterator[str]:
|
|
50
58
|
"""Stream agent chat response via SSE.
|
|
51
59
|
|
|
@@ -55,6 +63,8 @@ class EmdashClient:
|
|
|
55
63
|
session_id: Session ID for continuity (optional)
|
|
56
64
|
max_iterations: Max agent iterations
|
|
57
65
|
options: Additional options (mode, save, no_graph_tools, etc.)
|
|
66
|
+
images: List of images [{"data": base64_str, "format": "png"}]
|
|
67
|
+
history: Pre-loaded conversation history from saved session
|
|
58
68
|
|
|
59
69
|
Yields:
|
|
60
70
|
SSE lines from the response
|
|
@@ -81,40 +91,185 @@ class EmdashClient:
|
|
|
81
91
|
payload["model"] = model
|
|
82
92
|
if session_id:
|
|
83
93
|
payload["session_id"] = session_id
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
if images:
|
|
95
|
+
payload["images"] = images
|
|
96
|
+
if history:
|
|
97
|
+
payload["history"] = history
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
with self._client.stream(
|
|
101
|
+
"POST",
|
|
102
|
+
f"{self.base_url}/api/agent/chat",
|
|
103
|
+
json=payload,
|
|
104
|
+
) as response:
|
|
105
|
+
response.raise_for_status()
|
|
106
|
+
for line in response.iter_lines():
|
|
107
|
+
yield line
|
|
108
|
+
except GeneratorExit:
|
|
109
|
+
# Stream was closed early (interrupted)
|
|
110
|
+
pass
|
|
93
111
|
|
|
94
112
|
def agent_continue_stream(
|
|
95
113
|
self,
|
|
96
114
|
session_id: str,
|
|
97
115
|
message: str,
|
|
116
|
+
images: Optional[list[dict]] = None,
|
|
98
117
|
) -> Iterator[str]:
|
|
99
118
|
"""Continue an existing agent session.
|
|
100
119
|
|
|
101
120
|
Args:
|
|
102
121
|
session_id: Existing session ID
|
|
103
122
|
message: Continuation message
|
|
123
|
+
images: List of images [{"data": base64_str, "format": "png"}]
|
|
104
124
|
|
|
105
125
|
Yields:
|
|
106
126
|
SSE lines from the response
|
|
107
127
|
"""
|
|
108
128
|
payload = {"message": message}
|
|
129
|
+
if images:
|
|
130
|
+
payload["images"] = images
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
with self._client.stream(
|
|
134
|
+
"POST",
|
|
135
|
+
f"{self.base_url}/api/agent/chat/{session_id}/continue",
|
|
136
|
+
json=payload,
|
|
137
|
+
) as response:
|
|
138
|
+
response.raise_for_status()
|
|
139
|
+
for line in response.iter_lines():
|
|
140
|
+
yield line
|
|
141
|
+
except GeneratorExit:
|
|
142
|
+
# Stream was closed early (interrupted)
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
def plan_approve_stream(self, session_id: str) -> Iterator[str]:
|
|
146
|
+
"""Approve a pending plan and start implementation.
|
|
109
147
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
148
|
+
Args:
|
|
149
|
+
session_id: Session ID with pending plan
|
|
150
|
+
|
|
151
|
+
Yields:
|
|
152
|
+
SSE lines from the response
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
with self._client.stream(
|
|
156
|
+
"POST",
|
|
157
|
+
f"{self.base_url}/api/agent/chat/{session_id}/plan/approve",
|
|
158
|
+
) as response:
|
|
159
|
+
response.raise_for_status()
|
|
160
|
+
for line in response.iter_lines():
|
|
161
|
+
yield line
|
|
162
|
+
except GeneratorExit:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
def plan_reject_stream(self, session_id: str, feedback: str = "") -> Iterator[str]:
|
|
166
|
+
"""Reject a pending plan with feedback.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
session_id: Session ID with pending plan
|
|
170
|
+
feedback: Feedback explaining rejection
|
|
171
|
+
|
|
172
|
+
Yields:
|
|
173
|
+
SSE lines from the response
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
with self._client.stream(
|
|
177
|
+
"POST",
|
|
178
|
+
f"{self.base_url}/api/agent/chat/{session_id}/plan/reject",
|
|
179
|
+
params={"feedback": feedback},
|
|
180
|
+
) as response:
|
|
181
|
+
response.raise_for_status()
|
|
182
|
+
for line in response.iter_lines():
|
|
183
|
+
yield line
|
|
184
|
+
except GeneratorExit:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
def planmode_approve_stream(self, session_id: str) -> Iterator[str]:
|
|
188
|
+
"""Approve entering plan mode.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
session_id: Session ID requesting plan mode
|
|
192
|
+
|
|
193
|
+
Yields:
|
|
194
|
+
SSE lines from the response
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
with self._client.stream(
|
|
198
|
+
"POST",
|
|
199
|
+
f"{self.base_url}/api/agent/chat/{session_id}/planmode/approve",
|
|
200
|
+
) as response:
|
|
201
|
+
response.raise_for_status()
|
|
202
|
+
for line in response.iter_lines():
|
|
203
|
+
yield line
|
|
204
|
+
except GeneratorExit:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
def planmode_reject_stream(self, session_id: str, feedback: str = "") -> Iterator[str]:
|
|
208
|
+
"""Reject entering plan mode.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
session_id: Session ID requesting plan mode
|
|
212
|
+
feedback: Feedback explaining rejection
|
|
213
|
+
|
|
214
|
+
Yields:
|
|
215
|
+
SSE lines from the response
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
with self._client.stream(
|
|
219
|
+
"POST",
|
|
220
|
+
f"{self.base_url}/api/agent/chat/{session_id}/planmode/reject",
|
|
221
|
+
params={"feedback": feedback},
|
|
222
|
+
) as response:
|
|
223
|
+
response.raise_for_status()
|
|
224
|
+
for line in response.iter_lines():
|
|
225
|
+
yield line
|
|
226
|
+
except GeneratorExit:
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
def clarification_answer_stream(self, session_id: str, answer: str) -> Iterator[str]:
|
|
230
|
+
"""Answer a pending clarification question.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
session_id: Session ID with pending clarification
|
|
234
|
+
answer: User's answer to the clarification question
|
|
235
|
+
|
|
236
|
+
Yields:
|
|
237
|
+
SSE lines from the response
|
|
238
|
+
"""
|
|
239
|
+
try:
|
|
240
|
+
with self._client.stream(
|
|
241
|
+
"POST",
|
|
242
|
+
f"{self.base_url}/api/agent/chat/{session_id}/clarification/answer",
|
|
243
|
+
params={"answer": answer},
|
|
244
|
+
) as response:
|
|
245
|
+
response.raise_for_status()
|
|
246
|
+
for line in response.iter_lines():
|
|
247
|
+
yield line
|
|
248
|
+
except GeneratorExit:
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
def get(self, path: str) -> "httpx.Response":
|
|
252
|
+
"""Make a GET request to the API.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
path: API path (e.g., "/api/agent/sessions")
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
HTTP response
|
|
259
|
+
"""
|
|
260
|
+
return self._client.get(f"{self.base_url}{path}")
|
|
261
|
+
|
|
262
|
+
def post(self, path: str, json: dict | None = None) -> "httpx.Response":
|
|
263
|
+
"""Make a POST request to the API.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
path: API path (e.g., "/api/agent/chat/123/compact")
|
|
267
|
+
json: Optional JSON body
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
HTTP response
|
|
271
|
+
"""
|
|
272
|
+
return self._client.post(f"{self.base_url}{path}", json=json)
|
|
118
273
|
|
|
119
274
|
def list_sessions(self) -> list[dict]:
|
|
120
275
|
"""List active agent sessions.
|
|
@@ -476,7 +631,7 @@ class EmdashClient:
|
|
|
476
631
|
def research_stream(
|
|
477
632
|
self,
|
|
478
633
|
goal: str,
|
|
479
|
-
max_iterations: int =
|
|
634
|
+
max_iterations: int = _get_max_iterations(),
|
|
480
635
|
budget: int = 50,
|
|
481
636
|
model: Optional[str] = None,
|
|
482
637
|
) -> Iterator[str]:
|
|
@@ -517,31 +672,38 @@ class EmdashClient:
|
|
|
517
672
|
response.raise_for_status()
|
|
518
673
|
return response.json()
|
|
519
674
|
|
|
520
|
-
# ====================
|
|
675
|
+
# ==================== Todos ====================
|
|
521
676
|
|
|
522
|
-
def
|
|
523
|
-
|
|
524
|
-
tasks: list[str],
|
|
525
|
-
model: Optional[str] = None,
|
|
526
|
-
auto_merge: bool = False,
|
|
527
|
-
) -> Iterator[str]:
|
|
528
|
-
"""Run multi-agent swarm with SSE streaming."""
|
|
529
|
-
payload = {"tasks": tasks, "auto_merge": auto_merge}
|
|
530
|
-
if model:
|
|
531
|
-
payload["model"] = model
|
|
677
|
+
def get_todos(self, session_id: str) -> dict:
|
|
678
|
+
"""Get the current todo list for a session.
|
|
532
679
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
f"{self.base_url}/api/swarm/run",
|
|
536
|
-
json=payload,
|
|
537
|
-
) as response:
|
|
538
|
-
response.raise_for_status()
|
|
539
|
-
for line in response.iter_lines():
|
|
540
|
-
yield line
|
|
680
|
+
Args:
|
|
681
|
+
session_id: Session ID
|
|
541
682
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
683
|
+
Returns:
|
|
684
|
+
Dict with todos list and summary
|
|
685
|
+
"""
|
|
686
|
+
response = self._client.get(
|
|
687
|
+
f"{self.base_url}/api/agent/chat/{session_id}/todos"
|
|
688
|
+
)
|
|
689
|
+
response.raise_for_status()
|
|
690
|
+
return response.json()
|
|
691
|
+
|
|
692
|
+
def add_todo(self, session_id: str, title: str, description: str = "") -> dict:
|
|
693
|
+
"""Add a new todo item to the agent's task list.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
session_id: Session ID
|
|
697
|
+
title: Todo title
|
|
698
|
+
description: Optional description
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
Dict with created task info
|
|
702
|
+
"""
|
|
703
|
+
response = self._client.post(
|
|
704
|
+
f"{self.base_url}/api/agent/chat/{session_id}/todos",
|
|
705
|
+
params={"title": title, "description": description},
|
|
706
|
+
)
|
|
545
707
|
response.raise_for_status()
|
|
546
708
|
return response.json()
|
|
547
709
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Clipboard utilities for image handling.
|
|
2
|
+
|
|
3
|
+
Uses platform-native clipboard access (no Pillow dependency).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
from typing import Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from emdash_core.utils.image import (
|
|
10
|
+
read_clipboard_image,
|
|
11
|
+
is_clipboard_image_available,
|
|
12
|
+
get_image_info,
|
|
13
|
+
ClipboardImageError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_clipboard_image() -> Optional[Tuple[str, str]]:
|
|
18
|
+
"""Get image from clipboard if available.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Tuple of (base64_data, format) if image found, None otherwise.
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
if not is_clipboard_image_available():
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
image_data = read_clipboard_image()
|
|
28
|
+
if image_data is None:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
# Encode to base64
|
|
32
|
+
base64_data = base64.b64encode(image_data).decode('utf-8')
|
|
33
|
+
return base64_data, 'png'
|
|
34
|
+
|
|
35
|
+
except ClipboardImageError:
|
|
36
|
+
return None
|
|
37
|
+
except Exception:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_image_from_path(path: str) -> Optional[Tuple[str, str]]:
|
|
42
|
+
"""Load image from file path.
|
|
43
|
+
|
|
44
|
+
Only PNG files are fully supported. Other formats will be read as raw bytes.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
path: Path to image file
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Tuple of (base64_data, format) if successful, None otherwise.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
with open(path, 'rb') as f:
|
|
54
|
+
image_data = f.read()
|
|
55
|
+
|
|
56
|
+
# Determine format from file extension
|
|
57
|
+
ext = path.lower().split('.')[-1]
|
|
58
|
+
if ext in ('jpg', 'jpeg'):
|
|
59
|
+
img_format = 'jpeg'
|
|
60
|
+
elif ext == 'png':
|
|
61
|
+
img_format = 'png'
|
|
62
|
+
elif ext == 'gif':
|
|
63
|
+
img_format = 'gif'
|
|
64
|
+
elif ext == 'webp':
|
|
65
|
+
img_format = 'webp'
|
|
66
|
+
else:
|
|
67
|
+
img_format = 'png'
|
|
68
|
+
|
|
69
|
+
base64_data = base64.b64encode(image_data).decode('utf-8')
|
|
70
|
+
return base64_data, img_format
|
|
71
|
+
|
|
72
|
+
except Exception:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_image_dimensions(base64_data: str) -> Optional[Tuple[int, int]]:
|
|
77
|
+
"""Get dimensions of base64-encoded PNG image.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
base64_data: Base64-encoded image data
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Tuple of (width, height) if successful, None otherwise.
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
image_bytes = base64.b64decode(base64_data)
|
|
87
|
+
info = get_image_info(image_bytes)
|
|
88
|
+
if info.get("width") and info.get("height"):
|
|
89
|
+
return info["width"], info["height"]
|
|
90
|
+
return None
|
|
91
|
+
except Exception:
|
|
92
|
+
return None
|
|
@@ -7,11 +7,12 @@ from .analyze import analyze
|
|
|
7
7
|
from .embed import embed
|
|
8
8
|
from .index import index
|
|
9
9
|
from .plan import plan
|
|
10
|
+
from .registry import registry
|
|
10
11
|
from .rules import rules
|
|
11
12
|
from .search import search
|
|
12
13
|
from .server import server
|
|
14
|
+
from .skills import skills
|
|
13
15
|
from .team import team
|
|
14
|
-
from .swarm import swarm
|
|
15
16
|
from .projectmd import projectmd
|
|
16
17
|
from .research import research
|
|
17
18
|
from .spec import spec
|
|
@@ -25,11 +26,12 @@ __all__ = [
|
|
|
25
26
|
"embed",
|
|
26
27
|
"index",
|
|
27
28
|
"plan",
|
|
29
|
+
"registry",
|
|
28
30
|
"rules",
|
|
29
31
|
"search",
|
|
30
32
|
"server",
|
|
33
|
+
"skills",
|
|
31
34
|
"team",
|
|
32
|
-
"swarm",
|
|
33
35
|
"projectmd",
|
|
34
36
|
"research",
|
|
35
37
|
"spec",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Agent CLI commands package.
|
|
2
|
+
|
|
3
|
+
This package contains the refactored agent CLI code, split into:
|
|
4
|
+
- cli.py: Click command definitions
|
|
5
|
+
- constants.py: Enums and constants
|
|
6
|
+
- file_utils.py: File reference expansion utilities
|
|
7
|
+
- menus.py: Interactive prompt_toolkit menus
|
|
8
|
+
- interactive.py: Main REPL loop
|
|
9
|
+
- handlers/: Slash command handlers
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .cli import agent, agent_code
|
|
13
|
+
|
|
14
|
+
__all__ = ["agent", "agent_code"]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Click CLI commands for the agent."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from ...client import EmdashClient
|
|
9
|
+
from ...server_manager import get_server_manager
|
|
10
|
+
from ...sse_renderer import SSERenderer
|
|
11
|
+
from .interactive import run_interactive, run_single_task
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def agent():
|
|
18
|
+
"""AI agent commands."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@agent.command("code")
|
|
23
|
+
@click.argument("task", required=False)
|
|
24
|
+
@click.option("--model", "-m", default=None, help="Model to use")
|
|
25
|
+
@click.option("--mode", type=click.Choice(["plan", "code"]), default="code",
|
|
26
|
+
help="Starting mode")
|
|
27
|
+
@click.option("--quiet", "-q", is_flag=True, help="Less verbose output")
|
|
28
|
+
@click.option("--max-iterations", default=int(os.getenv("EMDASH_MAX_ITERATIONS", "100")), help="Max agent iterations")
|
|
29
|
+
@click.option("--no-graph-tools", is_flag=True, help="Skip graph exploration tools")
|
|
30
|
+
@click.option("--save", is_flag=True, help="Save specs to specs/<feature>/")
|
|
31
|
+
def agent_code(
|
|
32
|
+
task: str | None,
|
|
33
|
+
model: str | None,
|
|
34
|
+
mode: str,
|
|
35
|
+
quiet: bool,
|
|
36
|
+
max_iterations: int,
|
|
37
|
+
no_graph_tools: bool,
|
|
38
|
+
save: bool,
|
|
39
|
+
):
|
|
40
|
+
"""Start the coding agent.
|
|
41
|
+
|
|
42
|
+
With TASK: Run single task and exit
|
|
43
|
+
Without TASK: Start interactive REPL mode
|
|
44
|
+
|
|
45
|
+
MODES:
|
|
46
|
+
plan - Explore codebase and create plans (read-only)
|
|
47
|
+
code - Execute code changes (default)
|
|
48
|
+
|
|
49
|
+
SLASH COMMANDS (in interactive mode):
|
|
50
|
+
/plan - Switch to plan mode
|
|
51
|
+
/code - Switch to code mode
|
|
52
|
+
/help - Show available commands
|
|
53
|
+
/reset - Reset session
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
emdash # Interactive code mode
|
|
57
|
+
emdash agent code # Same as above
|
|
58
|
+
emdash agent code --mode plan # Start in plan mode
|
|
59
|
+
emdash agent code "Fix the login bug" # Single task
|
|
60
|
+
"""
|
|
61
|
+
# Get server URL (starts server if needed)
|
|
62
|
+
server = get_server_manager()
|
|
63
|
+
base_url = server.get_server_url()
|
|
64
|
+
|
|
65
|
+
client = EmdashClient(base_url)
|
|
66
|
+
renderer = SSERenderer(console=console, verbose=not quiet)
|
|
67
|
+
|
|
68
|
+
options = {
|
|
69
|
+
"mode": mode,
|
|
70
|
+
"no_graph_tools": no_graph_tools,
|
|
71
|
+
"save": save,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if task:
|
|
75
|
+
# Single task mode
|
|
76
|
+
run_single_task(client, renderer, task, model, max_iterations, options)
|
|
77
|
+
else:
|
|
78
|
+
# Interactive REPL mode
|
|
79
|
+
run_interactive(client, renderer, model, max_iterations, options)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@agent.command("sessions")
|
|
83
|
+
def list_sessions():
|
|
84
|
+
"""List active agent sessions."""
|
|
85
|
+
server = get_server_manager()
|
|
86
|
+
base_url = server.get_server_url()
|
|
87
|
+
|
|
88
|
+
client = EmdashClient(base_url)
|
|
89
|
+
sessions = client.list_sessions()
|
|
90
|
+
|
|
91
|
+
if not sessions:
|
|
92
|
+
console.print("[dim]No active sessions[/dim]")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
for s in sessions:
|
|
96
|
+
console.print(
|
|
97
|
+
f" {s['session_id'][:8]}... "
|
|
98
|
+
f"[dim]({s.get('model', 'unknown')}, "
|
|
99
|
+
f"{s.get('message_count', 0)} messages)[/dim]"
|
|
100
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Constants and enums for the agent CLI."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentMode(Enum):
|
|
7
|
+
"""Agent operation modes."""
|
|
8
|
+
PLAN = "plan"
|
|
9
|
+
CODE = "code"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Slash commands available in interactive mode
|
|
13
|
+
SLASH_COMMANDS = {
|
|
14
|
+
# Mode switching
|
|
15
|
+
"/plan": "Switch to plan mode (explore codebase, create plans)",
|
|
16
|
+
"/code": "Switch to code mode (execute file changes)",
|
|
17
|
+
"/mode": "Show current mode",
|
|
18
|
+
# Generation commands
|
|
19
|
+
"/pr [url]": "Review a pull request",
|
|
20
|
+
"/projectmd": "Generate PROJECT.md for the codebase",
|
|
21
|
+
"/research [goal]": "Deep research on a topic",
|
|
22
|
+
# Status commands
|
|
23
|
+
"/status": "Show index and PROJECT.md status",
|
|
24
|
+
"/diff": "Show uncommitted changes in GitHub-style diff view",
|
|
25
|
+
"/agents": "Manage agents (interactive menu, or /agents [create|show|edit|delete] <name>)",
|
|
26
|
+
# Todo management
|
|
27
|
+
"/todos": "Show current agent todo list",
|
|
28
|
+
"/todo-add [title]": "Add a todo item for the agent (e.g., /todo-add Fix tests)",
|
|
29
|
+
# Session management
|
|
30
|
+
"/session": "Save, load, or list sessions (e.g., /session save my-task)",
|
|
31
|
+
"/spec": "Show current specification",
|
|
32
|
+
"/reset": "Reset session state",
|
|
33
|
+
# Hooks
|
|
34
|
+
"/hooks": "Manage hooks (list, add, remove, toggle)",
|
|
35
|
+
# Rules
|
|
36
|
+
"/rules": "Manage rules (list, add, delete)",
|
|
37
|
+
# Skills
|
|
38
|
+
"/skills": "Manage skills (list, show, add, delete)",
|
|
39
|
+
# Index
|
|
40
|
+
"/index": "Manage codebase index (status, start, hook install/uninstall)",
|
|
41
|
+
# MCP
|
|
42
|
+
"/mcp": "Manage global MCP servers (list, edit)",
|
|
43
|
+
# Registry
|
|
44
|
+
"/registry": "Browse and install community skills, rules, agents, verifiers",
|
|
45
|
+
# Auth
|
|
46
|
+
"/auth": "GitHub authentication (login, logout, status)",
|
|
47
|
+
# Context
|
|
48
|
+
"/context": "Show current context frame (tokens, reranked items)",
|
|
49
|
+
"/compact": "Compact message history using LLM summarization",
|
|
50
|
+
# Image
|
|
51
|
+
"/paste": "Attach image from clipboard (or use Ctrl+V)",
|
|
52
|
+
# Diagnostics
|
|
53
|
+
"/doctor": "Check Python environment and diagnose issues",
|
|
54
|
+
# Verification
|
|
55
|
+
"/verify": "Run verification checks on current work",
|
|
56
|
+
"/verify-loop [task]": "Run task in loop until verifications pass",
|
|
57
|
+
# Setup wizard
|
|
58
|
+
"/setup": "Setup wizard for rules, agents, skills, and verifiers",
|
|
59
|
+
"/help": "Show available commands",
|
|
60
|
+
"/quit": "Exit the agent",
|
|
61
|
+
}
|