emdash-cli 0.1.43__tar.gz → 0.1.58__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.

Potentially problematic release.


This version of emdash-cli might be problematic. Click here for more details.

Files changed (58) hide show
  1. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/PKG-INFO +2 -2
  2. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/client.py +12 -28
  3. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/__init__.py +2 -2
  4. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/constants.py +10 -0
  5. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/handlers/__init__.py +10 -0
  6. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/handlers/agents.py +177 -49
  7. emdash_cli-0.1.58/emdash_cli/commands/agent/handlers/index.py +183 -0
  8. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/handlers/misc.py +119 -0
  9. emdash_cli-0.1.58/emdash_cli/commands/agent/handlers/registry.py +72 -0
  10. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/handlers/rules.py +48 -31
  11. emdash_cli-0.1.58/emdash_cli/commands/agent/handlers/setup.py +715 -0
  12. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/handlers/skills.py +42 -4
  13. emdash_cli-0.1.58/emdash_cli/commands/agent/handlers/todos.py +119 -0
  14. emdash_cli-0.1.58/emdash_cli/commands/agent/handlers/verify.py +653 -0
  15. emdash_cli-0.1.58/emdash_cli/commands/agent/help.py +236 -0
  16. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/interactive.py +223 -38
  17. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/menus.py +116 -84
  18. emdash_cli-0.1.58/emdash_cli/commands/agent/onboarding.py +619 -0
  19. emdash_cli-0.1.58/emdash_cli/commands/agent/session_restore.py +210 -0
  20. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/index.py +111 -13
  21. emdash_cli-0.1.58/emdash_cli/commands/registry.py +635 -0
  22. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/skills.py +72 -6
  23. emdash_cli-0.1.58/emdash_cli/design.py +328 -0
  24. emdash_cli-0.1.58/emdash_cli/diff_renderer.py +438 -0
  25. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/main.py +2 -2
  26. emdash_cli-0.1.58/emdash_cli/sse_renderer.py +1194 -0
  27. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/pyproject.toml +2 -2
  28. emdash_cli-0.1.43/emdash_cli/commands/agent/handlers/todos.py +0 -98
  29. emdash_cli-0.1.43/emdash_cli/commands/agent/handlers/verify.py +0 -282
  30. emdash_cli-0.1.43/emdash_cli/commands/swarm.py +0 -86
  31. emdash_cli-0.1.43/emdash_cli/sse_renderer.py +0 -794
  32. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/__init__.py +0 -0
  33. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/clipboard.py +0 -0
  34. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/__init__.py +0 -0
  35. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/cli.py +0 -0
  36. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/file_utils.py +0 -0
  37. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/handlers/auth.py +0 -0
  38. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/handlers/doctor.py +0 -0
  39. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/handlers/hooks.py +0 -0
  40. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/handlers/mcp.py +0 -0
  41. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent/handlers/sessions.py +0 -0
  42. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/agent.py +0 -0
  43. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/analyze.py +0 -0
  44. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/auth.py +0 -0
  45. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/db.py +0 -0
  46. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/embed.py +0 -0
  47. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/plan.py +0 -0
  48. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/projectmd.py +0 -0
  49. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/research.py +0 -0
  50. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/rules.py +0 -0
  51. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/search.py +0 -0
  52. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/server.py +0 -0
  53. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/spec.py +0 -0
  54. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/tasks.py +0 -0
  55. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/commands/team.py +0 -0
  56. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/keyboard.py +0 -0
  57. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/server_manager.py +0 -0
  58. {emdash_cli-0.1.43 → emdash_cli-0.1.58}/emdash_cli/session_store.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: emdash-cli
3
- Version: 0.1.43
3
+ Version: 0.1.58
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.43)
14
+ Requires-Dist: emdash-core (>=0.1.58)
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)
@@ -259,6 +259,18 @@ class EmdashClient:
259
259
  """
260
260
  return self._client.get(f"{self.base_url}{path}")
261
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)
273
+
262
274
  def list_sessions(self) -> list[dict]:
263
275
  """List active agent sessions.
264
276
 
@@ -660,34 +672,6 @@ class EmdashClient:
660
672
  response.raise_for_status()
661
673
  return response.json()
662
674
 
663
- # ==================== Swarm ====================
664
-
665
- def swarm_run_stream(
666
- self,
667
- tasks: list[str],
668
- model: Optional[str] = None,
669
- auto_merge: bool = False,
670
- ) -> Iterator[str]:
671
- """Run multi-agent swarm with SSE streaming."""
672
- payload = {"tasks": tasks, "auto_merge": auto_merge}
673
- if model:
674
- payload["model"] = model
675
-
676
- with self._client.stream(
677
- "POST",
678
- f"{self.base_url}/api/swarm/run",
679
- json=payload,
680
- ) as response:
681
- response.raise_for_status()
682
- for line in response.iter_lines():
683
- yield line
684
-
685
- def swarm_status(self) -> dict:
686
- """Get swarm execution status."""
687
- response = self._client.get(f"{self.base_url}/api/swarm/status")
688
- response.raise_for_status()
689
- return response.json()
690
-
691
675
  # ==================== Todos ====================
692
676
 
693
677
  def get_todos(self, session_id: str) -> dict:
@@ -7,12 +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
13
14
  from .skills import skills
14
15
  from .team import team
15
- from .swarm import swarm
16
16
  from .projectmd import projectmd
17
17
  from .research import research
18
18
  from .spec import spec
@@ -26,12 +26,12 @@ __all__ = [
26
26
  "embed",
27
27
  "index",
28
28
  "plan",
29
+ "registry",
29
30
  "rules",
30
31
  "search",
31
32
  "server",
32
33
  "skills",
33
34
  "team",
34
- "swarm",
35
35
  "projectmd",
36
36
  "research",
37
37
  "spec",
@@ -21,6 +21,7 @@ SLASH_COMMANDS = {
21
21
  "/research [goal]": "Deep research on a topic",
22
22
  # Status commands
23
23
  "/status": "Show index and PROJECT.md status",
24
+ "/diff": "Show uncommitted changes in GitHub-style diff view",
24
25
  "/agents": "Manage agents (interactive menu, or /agents [create|show|edit|delete] <name>)",
25
26
  # Todo management
26
27
  "/todos": "Show current agent todo list",
@@ -35,17 +36,26 @@ SLASH_COMMANDS = {
35
36
  "/rules": "Manage rules (list, add, delete)",
36
37
  # Skills
37
38
  "/skills": "Manage skills (list, show, add, delete)",
39
+ # Index
40
+ "/index": "Manage codebase index (status, start, hook install/uninstall)",
38
41
  # MCP
39
42
  "/mcp": "Manage global MCP servers (list, edit)",
43
+ # Registry
44
+ "/registry": "Browse and install community skills, rules, agents, verifiers",
40
45
  # Auth
41
46
  "/auth": "GitHub authentication (login, logout, status)",
42
47
  # Context
43
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)",
44
52
  # Diagnostics
45
53
  "/doctor": "Check Python environment and diagnose issues",
46
54
  # Verification
47
55
  "/verify": "Run verification checks on current work",
48
56
  "/verify-loop [task]": "Run task in loop until verifications pass",
57
+ # Setup wizard
58
+ "/setup": "Setup wizard for rules, agents, skills, and verifiers",
49
59
  "/help": "Show available commands",
50
60
  "/quit": "Exit the agent",
51
61
  }
@@ -6,16 +6,21 @@ from .todos import handle_todos, handle_todo_add
6
6
  from .hooks import handle_hooks
7
7
  from .rules import handle_rules
8
8
  from .skills import handle_skills
9
+ from .index import handle_index
9
10
  from .mcp import handle_mcp
11
+ from .registry import handle_registry
10
12
  from .auth import handle_auth
11
13
  from .doctor import handle_doctor
12
14
  from .verify import handle_verify, handle_verify_loop
15
+ from .setup import handle_setup
13
16
  from .misc import (
14
17
  handle_status,
15
18
  handle_pr,
16
19
  handle_projectmd,
17
20
  handle_research,
18
21
  handle_context,
22
+ handle_compact,
23
+ handle_diff,
19
24
  )
20
25
 
21
26
  __all__ = [
@@ -26,14 +31,19 @@ __all__ = [
26
31
  "handle_hooks",
27
32
  "handle_rules",
28
33
  "handle_skills",
34
+ "handle_index",
29
35
  "handle_mcp",
36
+ "handle_registry",
30
37
  "handle_auth",
31
38
  "handle_doctor",
32
39
  "handle_verify",
33
40
  "handle_verify_loop",
41
+ "handle_setup",
34
42
  "handle_status",
35
43
  "handle_pr",
36
44
  "handle_projectmd",
37
45
  "handle_research",
38
46
  "handle_context",
47
+ "handle_compact",
48
+ "handle_diff",
39
49
  ]
@@ -8,6 +8,14 @@ from rich.console import Console
8
8
  from rich.panel import Panel
9
9
 
10
10
  from ..menus import show_agents_interactive_menu, prompt_agent_name, confirm_delete
11
+ from ....design import (
12
+ Colors,
13
+ header,
14
+ footer,
15
+ SEPARATOR_WIDTH,
16
+ STATUS_ACTIVE,
17
+ ARROW_PROMPT,
18
+ )
11
19
 
12
20
  console = Console()
13
21
 
@@ -26,6 +34,9 @@ def create_agent(name: str) -> bool:
26
34
  template = f'''---
27
35
  description: Custom agent for specific tasks
28
36
  tools: [grep, glob, read_file, semantic_search]
37
+ # rules: [typescript, security] # Optional: reference rules from .emdash/rules/
38
+ # skills: [code-review] # Optional: reference skills from .emdash/skills/
39
+ # verifiers: [eslint] # Optional: reference verifiers from .emdash/verifiers.json
29
40
  ---
30
41
 
31
42
  # System Prompt
@@ -52,8 +63,10 @@ Describe what this agent should accomplish:
52
63
  Describe how the agent should format its responses.
53
64
  '''
54
65
  agent_file.write_text(template)
55
- console.print(f"[green]Created agent: {name}[/green]")
56
- console.print(f"[dim]File: {agent_file}[/dim]")
66
+ console.print()
67
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.TEXT}]created:[/{Colors.TEXT}] {name}")
68
+ console.print(f" [{Colors.DIM}]{agent_file}[/{Colors.DIM}]")
69
+ console.print()
57
70
  return True
58
71
 
59
72
 
@@ -64,57 +77,67 @@ def show_agent_details(name: str) -> None:
64
77
  builtin_agents = ["Explore", "Plan"]
65
78
 
66
79
  console.print()
67
- console.print("[dim]─" * 50 + "[/dim]")
80
+ console.print(f"[{Colors.MUTED}]{header(name, SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
68
81
  console.print()
82
+
69
83
  if name in builtin_agents:
70
- console.print(f"[bold cyan]{name}[/bold cyan] [dim](built-in)[/dim]\n")
84
+ console.print(f" [{Colors.DIM}]type[/{Colors.DIM}] [{Colors.MUTED}]built-in[/{Colors.MUTED}]")
71
85
  if name == "Explore":
72
- console.print("[bold]Description:[/bold] Fast codebase exploration (read-only)")
73
- console.print("[bold]Tools:[/bold] glob, grep, read_file, list_files, semantic_search")
86
+ console.print(f" [{Colors.DIM}]desc[/{Colors.DIM}] Fast codebase exploration (read-only)")
87
+ console.print(f" [{Colors.DIM}]tools[/{Colors.DIM}] glob, grep, read_file, list_files, semantic_search")
74
88
  elif name == "Plan":
75
- console.print("[bold]Description:[/bold] Design implementation plans")
76
- console.print("[bold]Tools:[/bold] glob, grep, read_file, list_files, semantic_search")
77
- console.print("\n[dim]Built-in agents cannot be edited or deleted.[/dim]")
89
+ console.print(f" [{Colors.DIM}]desc[/{Colors.DIM}] Design implementation plans")
90
+ console.print(f" [{Colors.DIM}]tools[/{Colors.DIM}] glob, grep, read_file, list_files, semantic_search")
91
+ console.print()
92
+ console.print(f" [{Colors.DIM}]Built-in agents cannot be edited or deleted.[/{Colors.DIM}]")
78
93
  else:
79
94
  agent = get_custom_agent(name, Path.cwd())
80
95
  if agent:
81
- console.print(f"[bold cyan]{agent.name}[/bold cyan] [dim](custom)[/dim]\n")
96
+ console.print(f" [{Colors.DIM}]type[/{Colors.DIM}] [{Colors.PRIMARY}]custom[/{Colors.PRIMARY}]")
82
97
 
83
- # Show description
84
98
  if agent.description:
85
- console.print(f"[bold]Description:[/bold] {agent.description}")
99
+ console.print(f" [{Colors.DIM}]desc[/{Colors.DIM}] {agent.description}")
86
100
 
87
- # Show model
88
101
  if agent.model:
89
- console.print(f"[bold]Model:[/bold] {agent.model}")
102
+ console.print(f" [{Colors.DIM}]model[/{Colors.DIM}] {agent.model}")
90
103
 
91
- # Show tools
92
104
  if agent.tools:
93
- console.print(f"[bold]Tools:[/bold] {', '.join(agent.tools)}")
105
+ console.print(f" [{Colors.DIM}]tools[/{Colors.DIM}] {', '.join(agent.tools)}")
94
106
 
95
- # Show MCP servers
96
107
  if agent.mcp_servers:
97
- console.print(f"\n[bold]MCP Servers:[/bold]")
108
+ console.print()
109
+ console.print(f" [{Colors.DIM}]mcp servers:[/{Colors.DIM}]")
98
110
  for server in agent.mcp_servers:
99
- status = "[green]enabled[/green]" if server.enabled else "[dim]disabled[/dim]"
100
- console.print(f" [cyan]{server.name}[/cyan] ({status})")
101
- console.print(f" [dim]{server.command} {' '.join(server.args)}[/dim]")
111
+ status = f"[{Colors.SUCCESS}][/{Colors.SUCCESS}]" if server.enabled else f"[{Colors.MUTED}][/{Colors.MUTED}]"
112
+ console.print(f" {status} [{Colors.PRIMARY}]{server.name}[/{Colors.PRIMARY}]")
113
+ console.print(f" [{Colors.DIM}]{server.command} {' '.join(server.args)}[/{Colors.DIM}]")
114
+
115
+ if agent.rules:
116
+ console.print(f" [{Colors.DIM}]rules[/{Colors.DIM}] {', '.join(agent.rules)}")
117
+
118
+ if agent.skills:
119
+ console.print(f" [{Colors.DIM}]skills[/{Colors.DIM}] {', '.join(agent.skills)}")
120
+
121
+ if agent.verifiers:
122
+ console.print(f" [{Colors.DIM}]verify[/{Colors.DIM}] {', '.join(agent.verifiers)}")
102
123
 
103
- # Show file path
104
124
  if agent.file_path:
105
- console.print(f"\n[bold]File:[/bold] {agent.file_path}")
125
+ console.print()
126
+ console.print(f" [{Colors.DIM}]file[/{Colors.DIM}] {agent.file_path}")
106
127
 
107
- # Show system prompt preview
108
128
  if agent.system_prompt:
109
- console.print(f"\n[bold]System Prompt Preview:[/bold]")
110
- preview = agent.system_prompt[:300]
111
- if len(agent.system_prompt) > 300:
129
+ console.print()
130
+ console.print(f" [{Colors.DIM}]prompt preview:[/{Colors.DIM}]")
131
+ preview = agent.system_prompt[:250]
132
+ if len(agent.system_prompt) > 250:
112
133
  preview += "..."
113
- console.print(Panel(preview, border_style="dim"))
134
+ for line in preview.split('\n')[:6]:
135
+ console.print(f" [{Colors.MUTED}]{line}[/{Colors.MUTED}]")
114
136
  else:
115
- console.print(f"[yellow]Agent '{name}' not found[/yellow]")
137
+ console.print(f" [{Colors.WARNING}]Agent '{name}' not found[/{Colors.WARNING}]")
138
+
116
139
  console.print()
117
- console.print("[dim]─" * 50 + "[/dim]")
140
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
118
141
 
119
142
 
120
143
  def delete_agent(name: str) -> bool:
@@ -177,19 +200,20 @@ def chat_edit_agent(name: str, client, renderer, model, max_iterations, render_w
177
200
  agent_file = agents_dir / f"{name}.md"
178
201
 
179
202
  if not agent_file.exists():
180
- console.print(f"[yellow]Agent file not found: {agent_file}[/yellow]")
203
+ console.print(f" [{Colors.WARNING}]Agent file not found: {agent_file}[/{Colors.WARNING}]")
181
204
  return
182
205
 
183
206
  # Read current content
184
207
  content = agent_file.read_text()
185
208
 
186
209
  console.print()
187
- console.print(f"[bold cyan]Chat: Editing agent '{name}'[/bold cyan]")
188
- console.print("[dim]What would you like to change? Type 'done' to finish, Ctrl+C to cancel[/dim]")
210
+ console.print(f"[{Colors.MUTED}]{header(f'Edit: {name}', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
211
+ console.print()
212
+ console.print(f" [{Colors.DIM}]Describe changes. Type 'done' to finish.[/{Colors.DIM}]")
189
213
  console.print()
190
214
 
191
215
  chat_style = Style.from_dict({
192
- "prompt": "#00cc66 bold",
216
+ "prompt": f"{Colors.PRIMARY} bold",
193
217
  })
194
218
 
195
219
  ps = PromptSession(style=chat_style)
@@ -253,6 +277,119 @@ Please make the requested changes using the Edit tool."""
253
277
  console.print(f"[red]Error: {e}[/red]")
254
278
 
255
279
 
280
+ def chat_create_agent(client, renderer, model, max_iterations, render_with_interrupt) -> str | None:
281
+ """Start a chat session to create a new agent with AI assistance.
282
+
283
+ Returns:
284
+ The name of the created agent, or None if cancelled.
285
+ """
286
+ from prompt_toolkit import PromptSession
287
+ from prompt_toolkit.styles import Style
288
+
289
+ agents_dir = Path.cwd() / ".emdash" / "agents"
290
+
291
+ console.print()
292
+ console.print(f"[{Colors.MUTED}]{header('Create Agent', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
293
+ console.print()
294
+ console.print(f" [{Colors.DIM}]Describe your agent. AI will help design it.[/{Colors.DIM}]")
295
+ console.print(f" [{Colors.DIM}]Type 'done' to finish.[/{Colors.DIM}]")
296
+ console.print()
297
+
298
+ chat_style = Style.from_dict({
299
+ "prompt": f"{Colors.PRIMARY} bold",
300
+ })
301
+
302
+ ps = PromptSession(style=chat_style)
303
+ chat_session_id = None
304
+ first_message = True
305
+
306
+ # Ensure agents directory exists
307
+ agents_dir.mkdir(parents=True, exist_ok=True)
308
+
309
+ # Chat loop
310
+ while True:
311
+ try:
312
+ user_input = ps.prompt([("class:prompt", "› ")]).strip()
313
+
314
+ if not user_input:
315
+ continue
316
+
317
+ if user_input.lower() in ("done", "quit", "exit", "q"):
318
+ console.print("[dim]Finished[/dim]")
319
+ break
320
+
321
+ # First message includes context about agents
322
+ if first_message:
323
+ message_with_context = f"""I want to create a new custom agent for my project.
324
+
325
+ **Agents directory:** `{agents_dir}`
326
+
327
+ Agents are markdown files with YAML frontmatter that define specialized assistants with custom system prompts and tools.
328
+
329
+ **Agent file format:**
330
+ ```markdown
331
+ ---
332
+ description: Brief description of what this agent does
333
+ model: claude-sonnet # optional, defaults to main model
334
+ tools: [grep, glob, read_file, edit_file, bash] # tools this agent can use
335
+ mcp_servers: # optional, MCP servers for this agent
336
+ - name: server-name
337
+ command: npx
338
+ args: ["-y", "@modelcontextprotocol/server-name"]
339
+ ---
340
+
341
+ # System Prompt
342
+
343
+ You are a specialized assistant for [purpose].
344
+
345
+ ## Your Mission
346
+ [What this agent should accomplish]
347
+
348
+ ## Approach
349
+ [How this agent should work]
350
+
351
+ ## Output Format
352
+ [How the agent should format responses]
353
+ ```
354
+
355
+ **Available tools:** grep, glob, read_file, edit_file, write_file, bash, semantic_search, list_files, etc.
356
+
357
+ **My request:** {user_input}
358
+
359
+ Please help me design and create an agent. Ask me questions about what I need, then use the Write tool to create the file at `{agents_dir}/<agent-name>.md`."""
360
+ stream = client.agent_chat_stream(
361
+ message=message_with_context,
362
+ model=model,
363
+ max_iterations=max_iterations,
364
+ options={"mode": "code"},
365
+ )
366
+ first_message = False
367
+ elif chat_session_id:
368
+ stream = client.agent_continue_stream(
369
+ chat_session_id, user_input
370
+ )
371
+ else:
372
+ stream = client.agent_chat_stream(
373
+ message=user_input,
374
+ model=model,
375
+ max_iterations=max_iterations,
376
+ options={"mode": "code"},
377
+ )
378
+
379
+ result = render_with_interrupt(renderer, stream)
380
+ if result and result.get("session_id"):
381
+ chat_session_id = result["session_id"]
382
+
383
+ except (KeyboardInterrupt, EOFError):
384
+ console.print()
385
+ console.print("[dim]Cancelled[/dim]")
386
+ break
387
+ except Exception as e:
388
+ console.print(f"[red]Error: {e}[/red]")
389
+
390
+ return None
391
+
392
+
256
393
  def handle_agents(args: str, client, renderer, model, max_iterations, render_with_interrupt) -> None:
257
394
  """Handle /agents command."""
258
395
  from prompt_toolkit import PromptSession
@@ -287,7 +424,7 @@ def handle_agents(args: str, client, renderer, model, max_iterations, render_wit
287
424
  is_custom = agent_name not in ("Explore", "Plan")
288
425
  try:
289
426
  if is_custom:
290
- console.print("[dim]'c' chat • 'e' edit • Enter back[/dim]", end="")
427
+ console.print("[cyan]'c'[/cyan] chat • [cyan]'e'[/cyan] edit • [red]'d'[/red] delete • [dim]Enter back[/dim]", end="")
291
428
  else:
292
429
  console.print("[dim]Press Enter to go back...[/dim]", end="")
293
430
  ps = PromptSession()
@@ -296,24 +433,15 @@ def handle_agents(args: str, client, renderer, model, max_iterations, render_wit
296
433
  chat_edit_agent(agent_name, client, renderer, model, max_iterations, render_with_interrupt)
297
434
  elif is_custom and resp == 'e':
298
435
  edit_agent(agent_name)
436
+ elif is_custom and resp == 'd':
437
+ if delete_agent(agent_name):
438
+ continue # Refresh menu after deletion
299
439
  console.print() # Add spacing before menu reappears
300
440
  except (KeyboardInterrupt, EOFError):
301
441
  break
302
442
  elif action == "create":
303
- new_name = prompt_agent_name()
304
- if new_name:
305
- if create_agent(new_name):
306
- # Ask if they want to edit it
307
- console.print("[dim]Press 'e' to edit, or Enter to continue...[/dim]")
308
- try:
309
- ps = PromptSession()
310
- resp = ps.prompt("> ")
311
- if resp.strip().lower() == 'e':
312
- edit_agent(new_name)
313
- except (KeyboardInterrupt, EOFError):
314
- pass
315
- else:
316
- console.print("[dim]Cancelled[/dim]")
443
+ # Use AI-assisted creation
444
+ chat_create_agent(client, renderer, model, max_iterations, render_with_interrupt)
317
445
  elif action == "delete":
318
446
  delete_agent(agent_name)
319
447
  elif action == "edit":
@@ -0,0 +1,183 @@
1
+ """Handler for /index command."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from rich.console import Console
7
+
8
+ console = Console()
9
+
10
+
11
+ def handle_index(args: str, client) -> None:
12
+ """Handle /index command.
13
+
14
+ Args:
15
+ args: Command arguments (status, start, hook install/uninstall)
16
+ client: EmdashClient instance
17
+ """
18
+ # Parse subcommand
19
+ subparts = args.split(maxsplit=1) if args else []
20
+ subcommand = subparts[0].lower() if subparts else "status"
21
+ subargs = subparts[1].strip() if len(subparts) > 1 else ""
22
+
23
+ repo_path = os.getcwd()
24
+
25
+ if subcommand == "status":
26
+ _show_status(client, repo_path)
27
+
28
+ elif subcommand == "start":
29
+ _start_index(client, repo_path, subargs)
30
+
31
+ elif subcommand == "hook":
32
+ _handle_hook(repo_path, subargs)
33
+
34
+ else:
35
+ console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
36
+ console.print("[dim]Usage: /index [status|start|hook][/dim]")
37
+ console.print("[dim] /index - Show index status[/dim]")
38
+ console.print("[dim] /index start - Start incremental indexing[/dim]")
39
+ console.print("[dim] /index start --full - Force full reindex[/dim]")
40
+ console.print("[dim] /index hook install - Install post-commit hook[/dim]")
41
+ console.print("[dim] /index hook uninstall - Remove post-commit hook[/dim]")
42
+
43
+
44
+ def _show_status(client, repo_path: str) -> None:
45
+ """Show index status."""
46
+ try:
47
+ status = client.index_status(repo_path)
48
+
49
+ console.print("\n[bold cyan]Index Status[/bold cyan]\n")
50
+ is_indexed = status.get("is_indexed", False)
51
+ console.print(f" Indexed: {'[green]Yes[/green]' if is_indexed else '[yellow]No[/yellow]'}")
52
+
53
+ if is_indexed:
54
+ console.print(f" Files: {status.get('file_count', 0)}")
55
+ console.print(f" Functions: {status.get('function_count', 0)}")
56
+ console.print(f" Classes: {status.get('class_count', 0)}")
57
+ console.print(f" Communities: {status.get('community_count', 0)}")
58
+
59
+ if status.get("last_indexed"):
60
+ console.print(f" Last indexed: {status.get('last_indexed')}")
61
+ if status.get("last_commit"):
62
+ console.print(f" Last commit: {status.get('last_commit')[:8]}")
63
+
64
+ # Check hook status
65
+ hooks_dir = Path(repo_path) / ".git" / "hooks"
66
+ hook_path = hooks_dir / "post-commit"
67
+ if hook_path.exists() and "emdash" in hook_path.read_text():
68
+ console.print(f" Auto-index: [green]Enabled[/green] (post-commit hook)")
69
+ else:
70
+ console.print(f" Auto-index: [dim]Disabled[/dim] (run /index hook install)")
71
+
72
+ console.print()
73
+
74
+ except Exception as e:
75
+ console.print(f"[red]Error getting status: {e}[/red]")
76
+
77
+
78
+ def _start_index(client, repo_path: str, args: str) -> None:
79
+ """Start indexing."""
80
+ import json
81
+ from rich.progress import Progress, BarColumn, TaskProgressColumn, TextColumn
82
+
83
+ # Parse options
84
+ full = "--full" in args
85
+
86
+ console.print(f"\n[bold cyan]Indexing[/bold cyan] {repo_path}\n")
87
+
88
+ try:
89
+ with Progress(
90
+ TextColumn("[bold cyan]{task.description}[/bold cyan]"),
91
+ BarColumn(bar_width=40, complete_style="cyan", finished_style="green"),
92
+ TaskProgressColumn(),
93
+ console=console,
94
+ transient=True,
95
+ ) as progress:
96
+ task = progress.add_task("Starting...", total=100)
97
+
98
+ for line in client.index_start_stream(repo_path, not full):
99
+ line = line.strip()
100
+ if line.startswith("event: "):
101
+ continue
102
+ if line.startswith("data: "):
103
+ try:
104
+ data = json.loads(line[6:])
105
+ step = data.get("step") or data.get("message", "")
106
+ percent = data.get("percent")
107
+
108
+ if step:
109
+ progress.update(task, description=step)
110
+ if percent is not None:
111
+ progress.update(task, completed=percent)
112
+ except json.JSONDecodeError:
113
+ pass
114
+
115
+ progress.update(task, completed=100, description="Complete")
116
+
117
+ console.print("[bold green]Indexing complete![/bold green]\n")
118
+
119
+ except Exception as e:
120
+ console.print(f"[red]Error: {e}[/red]")
121
+
122
+
123
+ def _handle_hook(repo_path: str, args: str) -> None:
124
+ """Handle hook install/uninstall."""
125
+ action = args.lower() if args else ""
126
+
127
+ if action not in ("install", "uninstall"):
128
+ console.print("[yellow]Usage: /index hook [install|uninstall][/yellow]")
129
+ return
130
+
131
+ hooks_dir = Path(repo_path) / ".git" / "hooks"
132
+ hook_path = hooks_dir / "post-commit"
133
+
134
+ if not hooks_dir.exists():
135
+ console.print(f"[red]Error:[/red] Not a git repository: {repo_path}")
136
+ return
137
+
138
+ hook_content = """#!/bin/sh
139
+ # emdash post-commit hook - auto-reindex on commit
140
+ # Installed by: emdash index hook install
141
+
142
+ # Run indexing in background to not block the commit
143
+ emdash index start > /dev/null 2>&1 &
144
+ """
145
+
146
+ if action == "install":
147
+ if hook_path.exists():
148
+ existing = hook_path.read_text()
149
+ if "emdash" in existing:
150
+ console.print("[yellow]Hook already installed[/yellow]")
151
+ return
152
+ else:
153
+ console.print("[yellow]Appending to existing post-commit hook[/yellow]")
154
+ with open(hook_path, "a") as f:
155
+ f.write("\n# emdash auto-index\nemdash index start > /dev/null 2>&1 &\n")
156
+ else:
157
+ hook_path.write_text(hook_content)
158
+
159
+ hook_path.chmod(0o755)
160
+ console.print(f"[green]Post-commit hook installed[/green]")
161
+ console.print("[dim]Index will update automatically after each commit[/dim]")
162
+
163
+ elif action == "uninstall":
164
+ if not hook_path.exists():
165
+ console.print("[yellow]No post-commit hook found[/yellow]")
166
+ return
167
+
168
+ existing = hook_path.read_text()
169
+ if "emdash" not in existing:
170
+ console.print("[yellow]No emdash hook found in post-commit[/yellow]")
171
+ return
172
+
173
+ if existing.strip() == hook_content.strip():
174
+ hook_path.unlink()
175
+ console.print("[green]Post-commit hook removed[/green]")
176
+ else:
177
+ lines = existing.split("\n")
178
+ new_lines = [
179
+ line for line in lines
180
+ if "emdash" not in line and "auto-reindex" not in line
181
+ ]
182
+ hook_path.write_text("\n".join(new_lines))
183
+ console.print("[green]Emdash hook lines removed from post-commit[/green]")