emdash-cli 0.1.8__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/PKG-INFO +17 -0
- emdash_cli-0.1.8/emdash_cli/__init__.py +3 -0
- emdash_cli-0.1.8/emdash_cli/client.py +556 -0
- emdash_cli-0.1.8/emdash_cli/commands/__init__.py +37 -0
- emdash_cli-0.1.8/emdash_cli/commands/agent.py +883 -0
- emdash_cli-0.1.8/emdash_cli/commands/analyze.py +137 -0
- emdash_cli-0.1.8/emdash_cli/commands/auth.py +121 -0
- emdash_cli-0.1.8/emdash_cli/commands/db.py +95 -0
- emdash_cli-0.1.8/emdash_cli/commands/embed.py +103 -0
- emdash_cli-0.1.8/emdash_cli/commands/index.py +134 -0
- emdash_cli-0.1.8/emdash_cli/commands/plan.py +77 -0
- emdash_cli-0.1.8/emdash_cli/commands/projectmd.py +51 -0
- emdash_cli-0.1.8/emdash_cli/commands/research.py +47 -0
- emdash_cli-0.1.8/emdash_cli/commands/rules.py +93 -0
- emdash_cli-0.1.8/emdash_cli/commands/search.py +56 -0
- emdash_cli-0.1.8/emdash_cli/commands/server.py +117 -0
- emdash_cli-0.1.8/emdash_cli/commands/spec.py +49 -0
- emdash_cli-0.1.8/emdash_cli/commands/swarm.py +86 -0
- emdash_cli-0.1.8/emdash_cli/commands/tasks.py +52 -0
- emdash_cli-0.1.8/emdash_cli/commands/team.py +51 -0
- emdash_cli-0.1.8/emdash_cli/main.py +104 -0
- emdash_cli-0.1.8/emdash_cli/server_manager.py +231 -0
- emdash_cli-0.1.8/emdash_cli/sse_renderer.py +545 -0
- emdash_cli-0.1.8/pyproject.toml +33 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: emdash-cli
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: EmDash CLI - Command-line interface for code intelligence
|
|
5
|
+
Author: Em Dash Team
|
|
6
|
+
Requires-Python: >=3.10,<4.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
14
|
+
Requires-Dist: emdash-core (>=0.1.8)
|
|
15
|
+
Requires-Dist: httpx (>=0.25.0)
|
|
16
|
+
Requires-Dist: prompt_toolkit (>=3.0.43,<4.0.0)
|
|
17
|
+
Requires-Dist: rich (>=13.7.0)
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
"""HTTP client for emdash-core API."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Iterator, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EmdashClient:
|
|
9
|
+
"""HTTP client for interacting with emdash-core API.
|
|
10
|
+
|
|
11
|
+
This client handles:
|
|
12
|
+
- Regular JSON API calls
|
|
13
|
+
- SSE streaming for agent chat
|
|
14
|
+
- Health checks
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, base_url: str):
|
|
18
|
+
"""Initialize the client.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
base_url: Base URL of emdash-core server
|
|
22
|
+
"""
|
|
23
|
+
self.base_url = base_url.rstrip("/")
|
|
24
|
+
self._client = httpx.Client(timeout=None) # No timeout for streaming
|
|
25
|
+
|
|
26
|
+
def health(self) -> dict:
|
|
27
|
+
"""Check server health.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Health status dict
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
httpx.HTTPError: If request fails
|
|
34
|
+
"""
|
|
35
|
+
response = self._client.get(
|
|
36
|
+
f"{self.base_url}/api/health",
|
|
37
|
+
timeout=5.0,
|
|
38
|
+
)
|
|
39
|
+
response.raise_for_status()
|
|
40
|
+
return response.json()
|
|
41
|
+
|
|
42
|
+
def agent_chat_stream(
|
|
43
|
+
self,
|
|
44
|
+
message: str,
|
|
45
|
+
model: Optional[str] = None,
|
|
46
|
+
session_id: Optional[str] = None,
|
|
47
|
+
max_iterations: int = 20,
|
|
48
|
+
options: Optional[dict] = None,
|
|
49
|
+
) -> Iterator[str]:
|
|
50
|
+
"""Stream agent chat response via SSE.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
message: User message/task
|
|
54
|
+
model: Model to use (optional)
|
|
55
|
+
session_id: Session ID for continuity (optional)
|
|
56
|
+
max_iterations: Max agent iterations
|
|
57
|
+
options: Additional options (mode, save, no_graph_tools, etc.)
|
|
58
|
+
|
|
59
|
+
Yields:
|
|
60
|
+
SSE lines from the response
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
httpx.HTTPError: If request fails
|
|
64
|
+
"""
|
|
65
|
+
# Build options with defaults
|
|
66
|
+
request_options = {
|
|
67
|
+
"max_iterations": max_iterations,
|
|
68
|
+
"verbose": True,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Merge additional options
|
|
72
|
+
if options:
|
|
73
|
+
request_options.update(options)
|
|
74
|
+
|
|
75
|
+
payload = {
|
|
76
|
+
"message": message,
|
|
77
|
+
"options": request_options,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if model:
|
|
81
|
+
payload["model"] = model
|
|
82
|
+
if session_id:
|
|
83
|
+
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
|
|
93
|
+
|
|
94
|
+
def agent_continue_stream(
|
|
95
|
+
self,
|
|
96
|
+
session_id: str,
|
|
97
|
+
message: str,
|
|
98
|
+
) -> Iterator[str]:
|
|
99
|
+
"""Continue an existing agent session.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
session_id: Existing session ID
|
|
103
|
+
message: Continuation message
|
|
104
|
+
|
|
105
|
+
Yields:
|
|
106
|
+
SSE lines from the response
|
|
107
|
+
"""
|
|
108
|
+
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
|
|
118
|
+
|
|
119
|
+
def list_sessions(self) -> list[dict]:
|
|
120
|
+
"""List active agent sessions.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of session info dicts
|
|
124
|
+
"""
|
|
125
|
+
response = self._client.get(f"{self.base_url}/api/agent/sessions")
|
|
126
|
+
response.raise_for_status()
|
|
127
|
+
return response.json().get("sessions", [])
|
|
128
|
+
|
|
129
|
+
def delete_session(self, session_id: str) -> bool:
|
|
130
|
+
"""Delete an agent session.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
session_id: Session to delete
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if deleted
|
|
137
|
+
"""
|
|
138
|
+
response = self._client.delete(
|
|
139
|
+
f"{self.base_url}/api/agent/sessions/{session_id}"
|
|
140
|
+
)
|
|
141
|
+
return response.status_code == 200
|
|
142
|
+
|
|
143
|
+
def search(
|
|
144
|
+
self,
|
|
145
|
+
query: str,
|
|
146
|
+
search_type: str = "semantic",
|
|
147
|
+
limit: int = 20,
|
|
148
|
+
) -> dict:
|
|
149
|
+
"""Search the codebase.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
query: Search query
|
|
153
|
+
search_type: Type of search (semantic, text, grep)
|
|
154
|
+
limit: Maximum results
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Search response dict
|
|
158
|
+
"""
|
|
159
|
+
response = self._client.post(
|
|
160
|
+
f"{self.base_url}/api/query/search",
|
|
161
|
+
json={
|
|
162
|
+
"query": query,
|
|
163
|
+
"type": search_type,
|
|
164
|
+
"filters": {"limit": limit},
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
response.raise_for_status()
|
|
168
|
+
return response.json()
|
|
169
|
+
|
|
170
|
+
def index_status(self, repo_path: str) -> dict:
|
|
171
|
+
"""Get indexing status for a repository.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
repo_path: Path to repository
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Index status dict
|
|
178
|
+
"""
|
|
179
|
+
response = self._client.get(
|
|
180
|
+
f"{self.base_url}/api/index/status",
|
|
181
|
+
params={"repo_path": repo_path}
|
|
182
|
+
)
|
|
183
|
+
response.raise_for_status()
|
|
184
|
+
return response.json()
|
|
185
|
+
|
|
186
|
+
def index_start_stream(
|
|
187
|
+
self,
|
|
188
|
+
repo_path: str,
|
|
189
|
+
incremental: bool = False,
|
|
190
|
+
) -> Iterator[str]:
|
|
191
|
+
"""Start indexing with SSE streaming progress.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
repo_path: Path to repository
|
|
195
|
+
incremental: Only index changed files
|
|
196
|
+
|
|
197
|
+
Yields:
|
|
198
|
+
SSE lines from the response
|
|
199
|
+
"""
|
|
200
|
+
payload = {
|
|
201
|
+
"repo_path": repo_path,
|
|
202
|
+
"options": {"incremental": incremental},
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
with self._client.stream(
|
|
206
|
+
"POST",
|
|
207
|
+
f"{self.base_url}/api/index/start",
|
|
208
|
+
json=payload,
|
|
209
|
+
) as response:
|
|
210
|
+
response.raise_for_status()
|
|
211
|
+
for line in response.iter_lines():
|
|
212
|
+
yield line
|
|
213
|
+
|
|
214
|
+
# ==================== Auth ====================
|
|
215
|
+
|
|
216
|
+
def auth_login(self) -> dict:
|
|
217
|
+
"""Start GitHub OAuth device flow.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Dict with user_code, verification_uri, expires_in, interval
|
|
221
|
+
"""
|
|
222
|
+
response = self._client.post(f"{self.base_url}/api/auth/login")
|
|
223
|
+
response.raise_for_status()
|
|
224
|
+
return response.json()
|
|
225
|
+
|
|
226
|
+
def auth_poll(self, user_code: str) -> dict:
|
|
227
|
+
"""Poll for login completion.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
user_code: The user code from auth_login
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Dict with status (pending/success/expired/error), username, error
|
|
234
|
+
"""
|
|
235
|
+
response = self._client.post(
|
|
236
|
+
f"{self.base_url}/api/auth/login/poll/{user_code}"
|
|
237
|
+
)
|
|
238
|
+
response.raise_for_status()
|
|
239
|
+
return response.json()
|
|
240
|
+
|
|
241
|
+
def auth_logout(self) -> dict:
|
|
242
|
+
"""Sign out by removing stored credentials."""
|
|
243
|
+
response = self._client.post(f"{self.base_url}/api/auth/logout")
|
|
244
|
+
response.raise_for_status()
|
|
245
|
+
return response.json()
|
|
246
|
+
|
|
247
|
+
def auth_status(self) -> dict:
|
|
248
|
+
"""Get current authentication status.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Dict with authenticated, username, scope
|
|
252
|
+
"""
|
|
253
|
+
response = self._client.get(f"{self.base_url}/api/auth/status")
|
|
254
|
+
response.raise_for_status()
|
|
255
|
+
return response.json()
|
|
256
|
+
|
|
257
|
+
# ==================== Database ====================
|
|
258
|
+
|
|
259
|
+
def db_init(self) -> dict:
|
|
260
|
+
"""Initialize the database schema."""
|
|
261
|
+
response = self._client.post(f"{self.base_url}/api/db/init")
|
|
262
|
+
response.raise_for_status()
|
|
263
|
+
return response.json()
|
|
264
|
+
|
|
265
|
+
def db_clear(self, confirm: bool = True) -> dict:
|
|
266
|
+
"""Clear all data from the database."""
|
|
267
|
+
response = self._client.post(
|
|
268
|
+
f"{self.base_url}/api/db/clear",
|
|
269
|
+
params={"confirm": confirm},
|
|
270
|
+
)
|
|
271
|
+
response.raise_for_status()
|
|
272
|
+
return response.json()
|
|
273
|
+
|
|
274
|
+
def db_stats(self) -> dict:
|
|
275
|
+
"""Get database statistics."""
|
|
276
|
+
response = self._client.get(f"{self.base_url}/api/db/stats")
|
|
277
|
+
response.raise_for_status()
|
|
278
|
+
return response.json()
|
|
279
|
+
|
|
280
|
+
def db_test(self) -> dict:
|
|
281
|
+
"""Test database connection."""
|
|
282
|
+
response = self._client.get(f"{self.base_url}/api/db/test")
|
|
283
|
+
response.raise_for_status()
|
|
284
|
+
return response.json()
|
|
285
|
+
|
|
286
|
+
# ==================== Analytics ====================
|
|
287
|
+
|
|
288
|
+
def analyze_pagerank(self, top: int = 20, damping: float = 0.85) -> dict:
|
|
289
|
+
"""Compute PageRank scores."""
|
|
290
|
+
response = self._client.post(
|
|
291
|
+
f"{self.base_url}/api/analyze/pagerank",
|
|
292
|
+
json={"top": top, "damping": damping},
|
|
293
|
+
)
|
|
294
|
+
response.raise_for_status()
|
|
295
|
+
return response.json()
|
|
296
|
+
|
|
297
|
+
def analyze_communities(
|
|
298
|
+
self,
|
|
299
|
+
resolution: float = 1.0,
|
|
300
|
+
min_size: int = 3,
|
|
301
|
+
top: int = 20,
|
|
302
|
+
) -> dict:
|
|
303
|
+
"""Detect code communities."""
|
|
304
|
+
response = self._client.post(
|
|
305
|
+
f"{self.base_url}/api/analyze/communities",
|
|
306
|
+
json={"resolution": resolution, "min_size": min_size, "top": top},
|
|
307
|
+
)
|
|
308
|
+
response.raise_for_status()
|
|
309
|
+
return response.json()
|
|
310
|
+
|
|
311
|
+
def analyze_areas(
|
|
312
|
+
self,
|
|
313
|
+
depth: int = 2,
|
|
314
|
+
days: int = 30,
|
|
315
|
+
top: int = 20,
|
|
316
|
+
sort: str = "focus",
|
|
317
|
+
files: bool = False,
|
|
318
|
+
) -> dict:
|
|
319
|
+
"""Get importance metrics by directory or file."""
|
|
320
|
+
response = self._client.post(
|
|
321
|
+
f"{self.base_url}/api/analyze/areas",
|
|
322
|
+
json={
|
|
323
|
+
"depth": depth,
|
|
324
|
+
"days": days,
|
|
325
|
+
"top": top,
|
|
326
|
+
"sort": sort,
|
|
327
|
+
"files": files,
|
|
328
|
+
},
|
|
329
|
+
)
|
|
330
|
+
response.raise_for_status()
|
|
331
|
+
return response.json()
|
|
332
|
+
|
|
333
|
+
# ==================== Embeddings ====================
|
|
334
|
+
|
|
335
|
+
def embed_status(self) -> dict:
|
|
336
|
+
"""Get embedding coverage statistics."""
|
|
337
|
+
response = self._client.get(f"{self.base_url}/api/embed/status")
|
|
338
|
+
response.raise_for_status()
|
|
339
|
+
return response.json()
|
|
340
|
+
|
|
341
|
+
def embed_models(self) -> list:
|
|
342
|
+
"""List available embedding models."""
|
|
343
|
+
response = self._client.get(f"{self.base_url}/api/embed/models")
|
|
344
|
+
response.raise_for_status()
|
|
345
|
+
return response.json().get("models", [])
|
|
346
|
+
|
|
347
|
+
def embed_index(
|
|
348
|
+
self,
|
|
349
|
+
include_prs: bool = True,
|
|
350
|
+
include_functions: bool = True,
|
|
351
|
+
include_classes: bool = True,
|
|
352
|
+
reindex: bool = False,
|
|
353
|
+
) -> dict:
|
|
354
|
+
"""Generate embeddings for graph entities."""
|
|
355
|
+
response = self._client.post(
|
|
356
|
+
f"{self.base_url}/api/embed/index",
|
|
357
|
+
json={
|
|
358
|
+
"include_prs": include_prs,
|
|
359
|
+
"include_functions": include_functions,
|
|
360
|
+
"include_classes": include_classes,
|
|
361
|
+
"reindex": reindex,
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
response.raise_for_status()
|
|
365
|
+
return response.json()
|
|
366
|
+
|
|
367
|
+
# ==================== Rules/Templates ====================
|
|
368
|
+
|
|
369
|
+
def rules_list(self) -> list:
|
|
370
|
+
"""List all templates."""
|
|
371
|
+
response = self._client.get(f"{self.base_url}/api/rules/list")
|
|
372
|
+
response.raise_for_status()
|
|
373
|
+
return response.json().get("templates", [])
|
|
374
|
+
|
|
375
|
+
def rules_get(self, template_name: str) -> dict:
|
|
376
|
+
"""Get a template's content."""
|
|
377
|
+
response = self._client.get(f"{self.base_url}/api/rules/{template_name}")
|
|
378
|
+
response.raise_for_status()
|
|
379
|
+
return response.json()
|
|
380
|
+
|
|
381
|
+
def rules_init(self, global_templates: bool = False, force: bool = False) -> dict:
|
|
382
|
+
"""Initialize custom templates."""
|
|
383
|
+
response = self._client.post(
|
|
384
|
+
f"{self.base_url}/api/rules/init",
|
|
385
|
+
params={"global_templates": global_templates, "force": force},
|
|
386
|
+
)
|
|
387
|
+
response.raise_for_status()
|
|
388
|
+
return response.json()
|
|
389
|
+
|
|
390
|
+
# ==================== Project MD ====================
|
|
391
|
+
|
|
392
|
+
def projectmd_generate_stream(
|
|
393
|
+
self,
|
|
394
|
+
output: str = "PROJECT.md",
|
|
395
|
+
save: bool = True,
|
|
396
|
+
model: Optional[str] = None,
|
|
397
|
+
) -> Iterator[str]:
|
|
398
|
+
"""Generate PROJECT.md with SSE streaming.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
output: Output file path
|
|
402
|
+
save: Whether to save to file
|
|
403
|
+
model: Model to use
|
|
404
|
+
|
|
405
|
+
Yields:
|
|
406
|
+
SSE lines from the response
|
|
407
|
+
"""
|
|
408
|
+
payload = {"output": output, "save": save}
|
|
409
|
+
if model:
|
|
410
|
+
payload["model"] = model
|
|
411
|
+
|
|
412
|
+
with self._client.stream(
|
|
413
|
+
"POST",
|
|
414
|
+
f"{self.base_url}/api/projectmd/generate",
|
|
415
|
+
json=payload,
|
|
416
|
+
) as response:
|
|
417
|
+
response.raise_for_status()
|
|
418
|
+
for line in response.iter_lines():
|
|
419
|
+
yield line
|
|
420
|
+
|
|
421
|
+
# ==================== Spec & Tasks ====================
|
|
422
|
+
|
|
423
|
+
def spec_generate_stream(
|
|
424
|
+
self,
|
|
425
|
+
feature: str,
|
|
426
|
+
model: Optional[str] = None,
|
|
427
|
+
save: bool = False,
|
|
428
|
+
) -> Iterator[str]:
|
|
429
|
+
"""Generate feature specification with SSE streaming."""
|
|
430
|
+
payload = {"feature": feature, "save": save}
|
|
431
|
+
if model:
|
|
432
|
+
payload["model"] = model
|
|
433
|
+
|
|
434
|
+
with self._client.stream(
|
|
435
|
+
"POST",
|
|
436
|
+
f"{self.base_url}/api/spec/generate",
|
|
437
|
+
json=payload,
|
|
438
|
+
) as response:
|
|
439
|
+
response.raise_for_status()
|
|
440
|
+
for line in response.iter_lines():
|
|
441
|
+
yield line
|
|
442
|
+
|
|
443
|
+
def tasks_generate_stream(
|
|
444
|
+
self,
|
|
445
|
+
spec_name: Optional[str] = None,
|
|
446
|
+
model: Optional[str] = None,
|
|
447
|
+
save: bool = False,
|
|
448
|
+
) -> Iterator[str]:
|
|
449
|
+
"""Generate implementation tasks with SSE streaming."""
|
|
450
|
+
payload = {"save": save}
|
|
451
|
+
if spec_name:
|
|
452
|
+
payload["spec_name"] = spec_name
|
|
453
|
+
if model:
|
|
454
|
+
payload["model"] = model
|
|
455
|
+
|
|
456
|
+
with self._client.stream(
|
|
457
|
+
"POST",
|
|
458
|
+
f"{self.base_url}/api/tasks/generate",
|
|
459
|
+
json=payload,
|
|
460
|
+
) as response:
|
|
461
|
+
response.raise_for_status()
|
|
462
|
+
for line in response.iter_lines():
|
|
463
|
+
yield line
|
|
464
|
+
|
|
465
|
+
def plan_context(self, description: str, similar_prs: int = 5) -> dict:
|
|
466
|
+
"""Get planning context for a feature."""
|
|
467
|
+
response = self._client.post(
|
|
468
|
+
f"{self.base_url}/api/plan/context",
|
|
469
|
+
json={"description": description, "similar_prs": similar_prs},
|
|
470
|
+
)
|
|
471
|
+
response.raise_for_status()
|
|
472
|
+
return response.json()
|
|
473
|
+
|
|
474
|
+
# ==================== Research ====================
|
|
475
|
+
|
|
476
|
+
def research_stream(
|
|
477
|
+
self,
|
|
478
|
+
goal: str,
|
|
479
|
+
max_iterations: int = 50,
|
|
480
|
+
budget: int = 50,
|
|
481
|
+
model: Optional[str] = None,
|
|
482
|
+
) -> Iterator[str]:
|
|
483
|
+
"""Deep research with SSE streaming."""
|
|
484
|
+
payload = {
|
|
485
|
+
"goal": goal,
|
|
486
|
+
"max_iterations": max_iterations,
|
|
487
|
+
"budget": budget,
|
|
488
|
+
}
|
|
489
|
+
if model:
|
|
490
|
+
payload["model"] = model
|
|
491
|
+
|
|
492
|
+
with self._client.stream(
|
|
493
|
+
"POST",
|
|
494
|
+
f"{self.base_url}/api/research/run",
|
|
495
|
+
json=payload,
|
|
496
|
+
) as response:
|
|
497
|
+
response.raise_for_status()
|
|
498
|
+
for line in response.iter_lines():
|
|
499
|
+
yield line
|
|
500
|
+
|
|
501
|
+
# ==================== Team ====================
|
|
502
|
+
|
|
503
|
+
def team_focus(
|
|
504
|
+
self,
|
|
505
|
+
days: int = 14,
|
|
506
|
+
model: Optional[str] = None,
|
|
507
|
+
) -> dict:
|
|
508
|
+
"""Get team's recent focus analysis."""
|
|
509
|
+
payload = {"days": days}
|
|
510
|
+
if model:
|
|
511
|
+
payload["model"] = model
|
|
512
|
+
|
|
513
|
+
response = self._client.post(
|
|
514
|
+
f"{self.base_url}/api/team/focus",
|
|
515
|
+
json=payload,
|
|
516
|
+
)
|
|
517
|
+
response.raise_for_status()
|
|
518
|
+
return response.json()
|
|
519
|
+
|
|
520
|
+
# ==================== Swarm ====================
|
|
521
|
+
|
|
522
|
+
def swarm_run_stream(
|
|
523
|
+
self,
|
|
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
|
|
532
|
+
|
|
533
|
+
with self._client.stream(
|
|
534
|
+
"POST",
|
|
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
|
|
541
|
+
|
|
542
|
+
def swarm_status(self) -> dict:
|
|
543
|
+
"""Get swarm execution status."""
|
|
544
|
+
response = self._client.get(f"{self.base_url}/api/swarm/status")
|
|
545
|
+
response.raise_for_status()
|
|
546
|
+
return response.json()
|
|
547
|
+
|
|
548
|
+
def close(self) -> None:
|
|
549
|
+
"""Close the HTTP client."""
|
|
550
|
+
self._client.close()
|
|
551
|
+
|
|
552
|
+
def __enter__(self) -> "EmdashClient":
|
|
553
|
+
return self
|
|
554
|
+
|
|
555
|
+
def __exit__(self, *args: Any) -> None:
|
|
556
|
+
self.close()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""CLI command implementations."""
|
|
2
|
+
|
|
3
|
+
from .agent import agent
|
|
4
|
+
from .db import db
|
|
5
|
+
from .auth import auth
|
|
6
|
+
from .analyze import analyze
|
|
7
|
+
from .embed import embed
|
|
8
|
+
from .index import index
|
|
9
|
+
from .plan import plan
|
|
10
|
+
from .rules import rules
|
|
11
|
+
from .search import search
|
|
12
|
+
from .server import server
|
|
13
|
+
from .team import team
|
|
14
|
+
from .swarm import swarm
|
|
15
|
+
from .projectmd import projectmd
|
|
16
|
+
from .research import research
|
|
17
|
+
from .spec import spec
|
|
18
|
+
from .tasks import tasks
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"agent",
|
|
22
|
+
"db",
|
|
23
|
+
"auth",
|
|
24
|
+
"analyze",
|
|
25
|
+
"embed",
|
|
26
|
+
"index",
|
|
27
|
+
"plan",
|
|
28
|
+
"rules",
|
|
29
|
+
"search",
|
|
30
|
+
"server",
|
|
31
|
+
"team",
|
|
32
|
+
"swarm",
|
|
33
|
+
"projectmd",
|
|
34
|
+
"research",
|
|
35
|
+
"spec",
|
|
36
|
+
"tasks",
|
|
37
|
+
]
|