emdash-cli 0.1.4__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/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """EmDash CLI - Command-line interface for code intelligence."""
2
+
3
+ __version__ = "0.1.3"
emdash_cli/client.py ADDED
@@ -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 = 10,
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
+ ]