emdash-cli 0.1.35__py3-none-any.whl → 0.1.46__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.
Files changed (30) hide show
  1. emdash_cli/client.py +35 -0
  2. emdash_cli/clipboard.py +30 -61
  3. emdash_cli/commands/agent/__init__.py +14 -0
  4. emdash_cli/commands/agent/cli.py +100 -0
  5. emdash_cli/commands/agent/constants.py +53 -0
  6. emdash_cli/commands/agent/file_utils.py +178 -0
  7. emdash_cli/commands/agent/handlers/__init__.py +41 -0
  8. emdash_cli/commands/agent/handlers/agents.py +421 -0
  9. emdash_cli/commands/agent/handlers/auth.py +69 -0
  10. emdash_cli/commands/agent/handlers/doctor.py +319 -0
  11. emdash_cli/commands/agent/handlers/hooks.py +121 -0
  12. emdash_cli/commands/agent/handlers/mcp.py +183 -0
  13. emdash_cli/commands/agent/handlers/misc.py +200 -0
  14. emdash_cli/commands/agent/handlers/rules.py +394 -0
  15. emdash_cli/commands/agent/handlers/sessions.py +168 -0
  16. emdash_cli/commands/agent/handlers/setup.py +582 -0
  17. emdash_cli/commands/agent/handlers/skills.py +440 -0
  18. emdash_cli/commands/agent/handlers/todos.py +98 -0
  19. emdash_cli/commands/agent/handlers/verify.py +648 -0
  20. emdash_cli/commands/agent/interactive.py +657 -0
  21. emdash_cli/commands/agent/menus.py +728 -0
  22. emdash_cli/commands/agent.py +7 -1321
  23. emdash_cli/commands/server.py +99 -40
  24. emdash_cli/server_manager.py +70 -10
  25. emdash_cli/sse_renderer.py +36 -5
  26. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/METADATA +2 -4
  27. emdash_cli-0.1.46.dist-info/RECORD +49 -0
  28. emdash_cli-0.1.35.dist-info/RECORD +0 -30
  29. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/WHEEL +0 -0
  30. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,421 @@
1
+ """Handler for /agents command."""
2
+
3
+ import os
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+
10
+ from ..menus import show_agents_interactive_menu, prompt_agent_name, confirm_delete
11
+
12
+ console = Console()
13
+
14
+
15
+ def create_agent(name: str) -> bool:
16
+ """Create a new agent with the given name."""
17
+ agents_dir = Path.cwd() / ".emdash" / "agents"
18
+ agent_file = agents_dir / f"{name}.md"
19
+
20
+ if agent_file.exists():
21
+ console.print(f"[yellow]Agent '{name}' already exists[/yellow]")
22
+ return False
23
+
24
+ agents_dir.mkdir(parents=True, exist_ok=True)
25
+
26
+ template = f'''---
27
+ description: Custom agent for specific tasks
28
+ tools: [grep, glob, read_file, semantic_search]
29
+ ---
30
+
31
+ # System Prompt
32
+
33
+ You are a specialized assistant for {name.replace("-", " ")} tasks.
34
+
35
+ ## Your Mission
36
+
37
+ Describe what this agent should accomplish:
38
+ - Task 1
39
+ - Task 2
40
+ - Task 3
41
+
42
+ ## Approach
43
+
44
+ 1. **Step One**
45
+ - Details about the first step
46
+
47
+ 2. **Step Two**
48
+ - Details about the second step
49
+
50
+ ## Output Format
51
+
52
+ Describe how the agent should format its responses.
53
+ '''
54
+ agent_file.write_text(template)
55
+ console.print(f"[green]Created agent: {name}[/green]")
56
+ console.print(f"[dim]File: {agent_file}[/dim]")
57
+ return True
58
+
59
+
60
+ def show_agent_details(name: str) -> None:
61
+ """Show detailed view of an agent."""
62
+ from emdash_core.agent.toolkits import get_custom_agent
63
+
64
+ builtin_agents = ["Explore", "Plan"]
65
+
66
+ console.print()
67
+ console.print("[dim]─" * 50 + "[/dim]")
68
+ console.print()
69
+ if name in builtin_agents:
70
+ console.print(f"[bold cyan]{name}[/bold cyan] [dim](built-in)[/dim]\n")
71
+ 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")
74
+ 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]")
78
+ else:
79
+ agent = get_custom_agent(name, Path.cwd())
80
+ if agent:
81
+ console.print(f"[bold cyan]{agent.name}[/bold cyan] [dim](custom)[/dim]\n")
82
+
83
+ # Show description
84
+ if agent.description:
85
+ console.print(f"[bold]Description:[/bold] {agent.description}")
86
+
87
+ # Show model
88
+ if agent.model:
89
+ console.print(f"[bold]Model:[/bold] {agent.model}")
90
+
91
+ # Show tools
92
+ if agent.tools:
93
+ console.print(f"[bold]Tools:[/bold] {', '.join(agent.tools)}")
94
+
95
+ # Show MCP servers
96
+ if agent.mcp_servers:
97
+ console.print(f"\n[bold]MCP Servers:[/bold]")
98
+ 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]")
102
+
103
+ # Show file path
104
+ if agent.file_path:
105
+ console.print(f"\n[bold]File:[/bold] {agent.file_path}")
106
+
107
+ # Show system prompt preview
108
+ 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:
112
+ preview += "..."
113
+ console.print(Panel(preview, border_style="dim"))
114
+ else:
115
+ console.print(f"[yellow]Agent '{name}' not found[/yellow]")
116
+ console.print()
117
+ console.print("[dim]─" * 50 + "[/dim]")
118
+
119
+
120
+ def delete_agent(name: str) -> bool:
121
+ """Delete a custom agent."""
122
+ agents_dir = Path.cwd() / ".emdash" / "agents"
123
+ agent_file = agents_dir / f"{name}.md"
124
+
125
+ if not agent_file.exists():
126
+ console.print(f"[yellow]Agent file not found: {agent_file}[/yellow]")
127
+ return False
128
+
129
+ if confirm_delete(name):
130
+ agent_file.unlink()
131
+ console.print(f"[green]Deleted agent: {name}[/green]")
132
+ return True
133
+ else:
134
+ console.print("[dim]Cancelled[/dim]")
135
+ return False
136
+
137
+
138
+ def edit_agent(name: str) -> None:
139
+ """Open agent file in editor."""
140
+ agents_dir = Path.cwd() / ".emdash" / "agents"
141
+ agent_file = agents_dir / f"{name}.md"
142
+
143
+ if not agent_file.exists():
144
+ console.print(f"[yellow]Agent file not found: {agent_file}[/yellow]")
145
+ return
146
+
147
+ # Try to open in editor
148
+ editor = os.environ.get("EDITOR", "")
149
+ if not editor:
150
+ # Try common editors
151
+ for ed in ["code", "vim", "nano", "vi"]:
152
+ try:
153
+ subprocess.run(["which", ed], capture_output=True, check=True)
154
+ editor = ed
155
+ break
156
+ except (subprocess.CalledProcessError, FileNotFoundError):
157
+ continue
158
+
159
+ if editor:
160
+ console.print(f"[dim]Opening {agent_file} in {editor}...[/dim]")
161
+ try:
162
+ subprocess.run([editor, str(agent_file)])
163
+ except Exception as e:
164
+ console.print(f"[red]Failed to open editor: {e}[/red]")
165
+ console.print(f"[dim]Edit manually: {agent_file}[/dim]")
166
+ else:
167
+ console.print(f"[yellow]No editor found. Edit manually:[/yellow]")
168
+ console.print(f" {agent_file}")
169
+
170
+
171
+ def chat_edit_agent(name: str, client, renderer, model, max_iterations, render_with_interrupt) -> None:
172
+ """Start a chat session to edit an agent with AI assistance."""
173
+ from prompt_toolkit import PromptSession
174
+ from prompt_toolkit.styles import Style
175
+
176
+ agents_dir = Path.cwd() / ".emdash" / "agents"
177
+ agent_file = agents_dir / f"{name}.md"
178
+
179
+ if not agent_file.exists():
180
+ console.print(f"[yellow]Agent file not found: {agent_file}[/yellow]")
181
+ return
182
+
183
+ # Read current content
184
+ content = agent_file.read_text()
185
+
186
+ 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]")
189
+ console.print()
190
+
191
+ chat_style = Style.from_dict({
192
+ "prompt": "#00cc66 bold",
193
+ })
194
+
195
+ ps = PromptSession(style=chat_style)
196
+ chat_session_id = None
197
+ first_message = True
198
+
199
+ # Chat loop
200
+ while True:
201
+ try:
202
+ user_input = ps.prompt([("class:prompt", "› ")]).strip()
203
+
204
+ if not user_input:
205
+ continue
206
+
207
+ if user_input.lower() in ("done", "quit", "exit", "q"):
208
+ console.print("[dim]Finished editing agent[/dim]")
209
+ break
210
+
211
+ # First message includes agent context
212
+ if first_message:
213
+ message_with_context = f"""I want to edit my custom agent "{name}".
214
+
215
+ **File:** `{agent_file}`
216
+
217
+ **Current content:**
218
+ ```markdown
219
+ {content}
220
+ ```
221
+
222
+ **My request:** {user_input}
223
+
224
+ Please make the requested changes using the Edit tool."""
225
+ stream = client.agent_chat_stream(
226
+ message=message_with_context,
227
+ model=model,
228
+ max_iterations=max_iterations,
229
+ options={"mode": "code"},
230
+ )
231
+ first_message = False
232
+ elif chat_session_id:
233
+ stream = client.agent_continue_stream(
234
+ chat_session_id, user_input
235
+ )
236
+ else:
237
+ stream = client.agent_chat_stream(
238
+ message=user_input,
239
+ model=model,
240
+ max_iterations=max_iterations,
241
+ options={"mode": "code"},
242
+ )
243
+
244
+ result = render_with_interrupt(renderer, stream)
245
+ if result and result.get("session_id"):
246
+ chat_session_id = result["session_id"]
247
+
248
+ except (KeyboardInterrupt, EOFError):
249
+ console.print()
250
+ console.print("[dim]Finished editing agent[/dim]")
251
+ break
252
+ except Exception as e:
253
+ console.print(f"[red]Error: {e}[/red]")
254
+
255
+
256
+ def chat_create_agent(client, renderer, model, max_iterations, render_with_interrupt) -> str | None:
257
+ """Start a chat session to create a new agent with AI assistance.
258
+
259
+ Returns:
260
+ The name of the created agent, or None if cancelled.
261
+ """
262
+ from prompt_toolkit import PromptSession
263
+ from prompt_toolkit.styles import Style
264
+
265
+ agents_dir = Path.cwd() / ".emdash" / "agents"
266
+
267
+ console.print()
268
+ console.print("[bold cyan]Create New Agent[/bold cyan]")
269
+ console.print("[dim]Describe what agent you want to create. The AI will help you design it.[/dim]")
270
+ console.print("[dim]Type 'done' to finish, Ctrl+C to cancel[/dim]")
271
+ console.print()
272
+
273
+ chat_style = Style.from_dict({
274
+ "prompt": "#00cc66 bold",
275
+ })
276
+
277
+ ps = PromptSession(style=chat_style)
278
+ chat_session_id = None
279
+ first_message = True
280
+
281
+ # Ensure agents directory exists
282
+ agents_dir.mkdir(parents=True, exist_ok=True)
283
+
284
+ # Chat loop
285
+ while True:
286
+ try:
287
+ user_input = ps.prompt([("class:prompt", "› ")]).strip()
288
+
289
+ if not user_input:
290
+ continue
291
+
292
+ if user_input.lower() in ("done", "quit", "exit", "q"):
293
+ console.print("[dim]Finished[/dim]")
294
+ break
295
+
296
+ # First message includes context about agents
297
+ if first_message:
298
+ message_with_context = f"""I want to create a new custom agent for my project.
299
+
300
+ **Agents directory:** `{agents_dir}`
301
+
302
+ Agents are markdown files with YAML frontmatter that define specialized assistants with custom system prompts and tools.
303
+
304
+ **Agent file format:**
305
+ ```markdown
306
+ ---
307
+ description: Brief description of what this agent does
308
+ model: claude-sonnet # optional, defaults to main model
309
+ tools: [grep, glob, read_file, edit_file, bash] # tools this agent can use
310
+ mcp_servers: # optional, MCP servers for this agent
311
+ - name: server-name
312
+ command: npx
313
+ args: ["-y", "@modelcontextprotocol/server-name"]
314
+ ---
315
+
316
+ # System Prompt
317
+
318
+ You are a specialized assistant for [purpose].
319
+
320
+ ## Your Mission
321
+ [What this agent should accomplish]
322
+
323
+ ## Approach
324
+ [How this agent should work]
325
+
326
+ ## Output Format
327
+ [How the agent should format responses]
328
+ ```
329
+
330
+ **Available tools:** grep, glob, read_file, edit_file, write_file, bash, semantic_search, list_files, etc.
331
+
332
+ **My request:** {user_input}
333
+
334
+ 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`."""
335
+ stream = client.agent_chat_stream(
336
+ message=message_with_context,
337
+ model=model,
338
+ max_iterations=max_iterations,
339
+ options={"mode": "code"},
340
+ )
341
+ first_message = False
342
+ elif chat_session_id:
343
+ stream = client.agent_continue_stream(
344
+ chat_session_id, user_input
345
+ )
346
+ else:
347
+ stream = client.agent_chat_stream(
348
+ message=user_input,
349
+ model=model,
350
+ max_iterations=max_iterations,
351
+ options={"mode": "code"},
352
+ )
353
+
354
+ result = render_with_interrupt(renderer, stream)
355
+ if result and result.get("session_id"):
356
+ chat_session_id = result["session_id"]
357
+
358
+ except (KeyboardInterrupt, EOFError):
359
+ console.print()
360
+ console.print("[dim]Cancelled[/dim]")
361
+ break
362
+ except Exception as e:
363
+ console.print(f"[red]Error: {e}[/red]")
364
+
365
+ return None
366
+
367
+
368
+ def handle_agents(args: str, client, renderer, model, max_iterations, render_with_interrupt) -> None:
369
+ """Handle /agents command."""
370
+ from prompt_toolkit import PromptSession
371
+
372
+ # Handle subcommands for backward compatibility
373
+ if args:
374
+ subparts = args.split(maxsplit=1)
375
+ subcommand = subparts[0].lower()
376
+ subargs = subparts[1] if len(subparts) > 1 else ""
377
+
378
+ if subcommand == "create" and subargs:
379
+ create_agent(subargs.strip().lower().replace(" ", "-"))
380
+ elif subcommand == "show" and subargs:
381
+ show_agent_details(subargs.strip())
382
+ elif subcommand == "delete" and subargs:
383
+ delete_agent(subargs.strip())
384
+ elif subcommand == "edit" and subargs:
385
+ edit_agent(subargs.strip())
386
+ else:
387
+ console.print("[yellow]Usage: /agents [create|show|delete|edit] <name>[/yellow]")
388
+ console.print("[dim]Or just /agents for interactive menu[/dim]")
389
+ else:
390
+ # Interactive menu
391
+ while True:
392
+ action, agent_name = show_agents_interactive_menu()
393
+
394
+ if action == "cancel":
395
+ break
396
+ elif action == "view":
397
+ show_agent_details(agent_name)
398
+ # After viewing, show options based on agent type
399
+ is_custom = agent_name not in ("Explore", "Plan")
400
+ try:
401
+ if is_custom:
402
+ console.print("[dim]'c' chat • 'e' edit • Enter back[/dim]", end="")
403
+ else:
404
+ console.print("[dim]Press Enter to go back...[/dim]", end="")
405
+ ps = PromptSession()
406
+ resp = ps.prompt(" ").strip().lower()
407
+ if is_custom and resp == 'c':
408
+ chat_edit_agent(agent_name, client, renderer, model, max_iterations, render_with_interrupt)
409
+ elif is_custom and resp == 'e':
410
+ edit_agent(agent_name)
411
+ console.print() # Add spacing before menu reappears
412
+ except (KeyboardInterrupt, EOFError):
413
+ break
414
+ elif action == "create":
415
+ # Use AI-assisted creation
416
+ chat_create_agent(client, renderer, model, max_iterations, render_with_interrupt)
417
+ elif action == "delete":
418
+ delete_agent(agent_name)
419
+ elif action == "edit":
420
+ edit_agent(agent_name)
421
+ break # Exit menu after editing
@@ -0,0 +1,69 @@
1
+ """Handler for /auth command."""
2
+
3
+ from rich.console import Console
4
+
5
+ console = Console()
6
+
7
+
8
+ def handle_auth(args: str) -> None:
9
+ """Handle /auth command.
10
+
11
+ Args:
12
+ args: Command arguments
13
+ """
14
+ from emdash_core.auth.github import GitHubAuth, get_auth_status
15
+
16
+ # Parse subcommand
17
+ subparts = args.split(maxsplit=1) if args else []
18
+ subcommand = subparts[0].lower() if subparts else "status"
19
+
20
+ if subcommand == "status" or subcommand == "":
21
+ # Show auth status
22
+ status = get_auth_status()
23
+ console.print()
24
+ console.print("[bold cyan]GitHub Authentication[/bold cyan]\n")
25
+
26
+ if status["authenticated"]:
27
+ console.print(f" Status: [green]Authenticated[/green]")
28
+ console.print(f" Source: {status['source']}")
29
+ if status["username"]:
30
+ console.print(f" Username: @{status['username']}")
31
+ if status["scopes"]:
32
+ console.print(f" Scopes: {', '.join(status['scopes'])}")
33
+ else:
34
+ console.print(f" Status: [yellow]Not authenticated[/yellow]")
35
+ console.print("\n[dim]Run /auth login to authenticate with GitHub[/dim]")
36
+
37
+ console.print()
38
+
39
+ elif subcommand == "login":
40
+ # Start GitHub OAuth device flow
41
+ console.print()
42
+ console.print("[bold cyan]GitHub Login[/bold cyan]")
43
+ console.print("[dim]Starting device authorization flow...[/dim]\n")
44
+
45
+ auth = GitHubAuth()
46
+ try:
47
+ config = auth.login(open_browser=True)
48
+ if config:
49
+ console.print()
50
+ console.print("[green]Authentication successful![/green]")
51
+ console.print("[dim]MCP servers can now use ${GITHUB_TOKEN}[/dim]")
52
+ else:
53
+ console.print("[red]Authentication failed or was cancelled.[/red]")
54
+ except Exception as e:
55
+ console.print(f"[red]Login failed: {e}[/red]")
56
+
57
+ console.print()
58
+
59
+ elif subcommand == "logout":
60
+ # Remove stored authentication
61
+ auth = GitHubAuth()
62
+ if auth.logout():
63
+ console.print("[green]Logged out successfully[/green]")
64
+ else:
65
+ console.print("[dim]No stored authentication to remove[/dim]")
66
+
67
+ else:
68
+ console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
69
+ console.print("[dim]Usage: /auth [status|login|logout][/dim]")