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.
Files changed (60) hide show
  1. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/PKG-INFO +2 -2
  2. emdash_cli-0.1.60/emdash_cli/__init__.py +23 -0
  3. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/client.py +203 -41
  4. emdash_cli-0.1.60/emdash_cli/clipboard.py +92 -0
  5. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/__init__.py +4 -2
  6. emdash_cli-0.1.60/emdash_cli/commands/agent/__init__.py +14 -0
  7. emdash_cli-0.1.60/emdash_cli/commands/agent/cli.py +100 -0
  8. emdash_cli-0.1.60/emdash_cli/commands/agent/constants.py +61 -0
  9. emdash_cli-0.1.60/emdash_cli/commands/agent/file_utils.py +178 -0
  10. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/__init__.py +49 -0
  11. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/agents.py +449 -0
  12. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/auth.py +69 -0
  13. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/doctor.py +319 -0
  14. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/hooks.py +121 -0
  15. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/index.py +183 -0
  16. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/mcp.py +183 -0
  17. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/misc.py +319 -0
  18. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/registry.py +72 -0
  19. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/rules.py +411 -0
  20. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/sessions.py +168 -0
  21. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/setup.py +715 -0
  22. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/skills.py +478 -0
  23. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/todos.py +119 -0
  24. emdash_cli-0.1.60/emdash_cli/commands/agent/handlers/verify.py +653 -0
  25. emdash_cli-0.1.60/emdash_cli/commands/agent/help.py +236 -0
  26. emdash_cli-0.1.60/emdash_cli/commands/agent/interactive.py +837 -0
  27. emdash_cli-0.1.60/emdash_cli/commands/agent/menus.py +760 -0
  28. emdash_cli-0.1.60/emdash_cli/commands/agent/onboarding.py +619 -0
  29. emdash_cli-0.1.60/emdash_cli/commands/agent/session_restore.py +210 -0
  30. emdash_cli-0.1.60/emdash_cli/commands/agent.py +10 -0
  31. emdash_cli-0.1.60/emdash_cli/commands/index.py +300 -0
  32. emdash_cli-0.1.60/emdash_cli/commands/registry.py +635 -0
  33. emdash_cli-0.1.60/emdash_cli/commands/server.py +176 -0
  34. emdash_cli-0.1.60/emdash_cli/commands/skills.py +403 -0
  35. emdash_cli-0.1.60/emdash_cli/design.py +328 -0
  36. emdash_cli-0.1.60/emdash_cli/diff_renderer.py +438 -0
  37. emdash_cli-0.1.60/emdash_cli/keyboard.py +146 -0
  38. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/main.py +7 -3
  39. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/server_manager.py +70 -10
  40. emdash_cli-0.1.60/emdash_cli/session_store.py +321 -0
  41. emdash_cli-0.1.60/emdash_cli/sse_renderer.py +1240 -0
  42. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/pyproject.toml +4 -3
  43. emdash_cli-0.1.8/emdash_cli/__init__.py +0 -3
  44. emdash_cli-0.1.8/emdash_cli/commands/agent.py +0 -883
  45. emdash_cli-0.1.8/emdash_cli/commands/index.py +0 -134
  46. emdash_cli-0.1.8/emdash_cli/commands/server.py +0 -117
  47. emdash_cli-0.1.8/emdash_cli/commands/swarm.py +0 -86
  48. emdash_cli-0.1.8/emdash_cli/sse_renderer.py +0 -545
  49. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/analyze.py +0 -0
  50. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/auth.py +0 -0
  51. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/db.py +0 -0
  52. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/embed.py +0 -0
  53. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/plan.py +0 -0
  54. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/projectmd.py +0 -0
  55. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/research.py +0 -0
  56. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/rules.py +0 -0
  57. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/search.py +0 -0
  58. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/spec.py +0 -0
  59. {emdash_cli-0.1.8 → emdash_cli-0.1.60}/emdash_cli/commands/tasks.py +0 -0
  60. {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.8
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.8)
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 = 20,
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
- 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
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
- 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
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 = 50,
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
- # ==================== Swarm ====================
675
+ # ==================== Todos ====================
521
676
 
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
677
+ def get_todos(self, session_id: str) -> dict:
678
+ """Get the current todo list for a session.
532
679
 
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
680
+ Args:
681
+ session_id: Session ID
541
682
 
542
- def swarm_status(self) -> dict:
543
- """Get swarm execution status."""
544
- response = self._client.get(f"{self.base_url}/api/swarm/status")
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
+ }